@@ -2,15 +2,24 @@ import type { ForwardedRef } from 'react';
2
2
import type {
3
3
OverlayArrowProps as AriaOverlayArrowProps ,
4
4
PopoverProps as AriaPopoverProps ,
5
+ DialogTriggerProps ,
5
6
} from 'react-aria-components' ;
6
7
8
+ import { PressResponder } from '@react-aria/interactions' ;
9
+ import { useId , useLayoutEffect } from '@react-aria/utils' ;
7
10
import { cva } from 'class-variance-authority' ;
8
- import { forwardRef , useContext } from 'react' ;
11
+ import { forwardRef , useCallback , useContext , useRef } from 'react' ;
12
+ import { useHover , useOverlayTrigger } from 'react-aria' ;
9
13
import {
10
14
OverlayArrow as AriaOverlayArrow ,
11
15
Popover as AriaPopover ,
16
+ PopoverContext as AriaPopoverContext ,
17
+ DialogContext ,
18
+ OverlayTriggerStateContext ,
19
+ Provider ,
12
20
composeRenderProps ,
13
21
} from 'react-aria-components' ;
22
+ import { useOverlayTriggerState } from 'react-stately' ;
14
23
15
24
import { PopoverContext } from './ComboBox' ;
16
25
import styles from './styles/Popover.module.css' ;
@@ -62,7 +71,83 @@ const _OverlayArrow = (props: OverlayArrowProps, ref: ForwardedRef<HTMLDivElemen
62
71
) ;
63
72
} ;
64
73
74
+ /**
75
+ * An OverlayArrow renders a custom arrow element relative to an overlay element such as a popover or tooltip such that it aligns with a trigger element.
76
+ *
77
+ * https://react-spectrum.adobe.com/react-aria/Popover.html
78
+ */
65
79
const OverlayArrow = forwardRef ( _OverlayArrow ) ;
66
80
67
- export { OverlayArrow , Popover } ;
81
+ const HoverTrigger = ( props : DialogTriggerProps ) => {
82
+ const state = useOverlayTriggerState ( props ) ;
83
+
84
+ const buttonRef = useRef < HTMLButtonElement > ( null ) ;
85
+ const { triggerProps, overlayProps } = useOverlayTrigger ( { type : 'dialog' } , state , buttonRef ) ;
86
+
87
+ triggerProps . id = useId ( ) ;
88
+ // @ts -expect-error
89
+ overlayProps [ 'aria-labelledby' ] = triggerProps . id ;
90
+
91
+ const ref = useRef < HTMLSpanElement > ( null ) ;
92
+ const openTimeout = useRef < ReturnType < typeof setTimeout > | undefined > ( ) ;
93
+
94
+ // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
95
+ const cancelOpenTimeout = useCallback ( ( ) => {
96
+ if ( openTimeout . current ) {
97
+ clearTimeout ( openTimeout . current ) ;
98
+ openTimeout . current = undefined ;
99
+ }
100
+ } , [ openTimeout ] ) ;
101
+
102
+ const onHoverChange = ( isHovering : boolean ) => {
103
+ if ( ! openTimeout . current ) {
104
+ openTimeout . current = setTimeout ( ( ) => {
105
+ cancelOpenTimeout ( ) ;
106
+ state . setOpen ( isHovering ) ;
107
+ } , 250 ) ;
108
+ } else {
109
+ cancelOpenTimeout ( ) ;
110
+ }
111
+ } ;
112
+
113
+ const { hoverProps } = useHover ( {
114
+ onHoverChange,
115
+ } ) ;
116
+
117
+ useLayoutEffect ( ( ) => {
118
+ return ( ) => {
119
+ cancelOpenTimeout ( ) ;
120
+ } ;
121
+ } , [ cancelOpenTimeout ] ) ;
122
+
123
+ const shouldCloseOnInteractOutside = ( target : Element ) => {
124
+ return target !== buttonRef . current ;
125
+ } ;
126
+
127
+ return (
128
+ < Provider
129
+ values = { [
130
+ [ OverlayTriggerStateContext , state ] ,
131
+ [ DialogContext , overlayProps ] ,
132
+ [
133
+ AriaPopoverContext ,
134
+ {
135
+ trigger : 'DialogTrigger' ,
136
+ triggerRef : buttonRef ,
137
+ UNSTABLE_portalContainer : ref . current || undefined ,
138
+ shouldCloseOnInteractOutside,
139
+ } ,
140
+ ] ,
141
+ ] }
142
+ >
143
+ < span className = { styles . hover } ref = { ref } { ...hoverProps } >
144
+ < PressResponder { ...triggerProps } ref = { buttonRef } isPressed = { state . isOpen } >
145
+ { props . children }
146
+ </ PressResponder >
147
+ </ span >
148
+ </ Provider >
149
+ ) ;
150
+ } ;
151
+
152
+ export { HoverTrigger , OverlayArrow , Popover } ;
68
153
export type { OverlayArrowProps , PopoverProps } ;
0 commit comments