@@ -22,7 +22,14 @@ import React, {
22
22
useRef ,
23
23
useState ,
24
24
} from 'react' ;
25
-
25
+ import {
26
+ useHover ,
27
+ useFloating ,
28
+ useInteractions ,
29
+ safePolygon ,
30
+ autoUpdate ,
31
+ FloatingFocusManager ,
32
+ } from '@floating-ui/react' ;
26
33
import { CaretRight , CaretLeft , Checkmark } from '@carbon/icons-react' ;
27
34
import { keys , match } from '../../internal/keyboard' ;
28
35
import { useControllableState } from '../../internal/useControllableState' ;
@@ -79,9 +86,6 @@ export interface MenuItemProps extends LiHTMLAttributes<HTMLLIElement> {
79
86
shortcut ?: string ;
80
87
}
81
88
82
- const hoverIntentDelay = 150 ; // in ms
83
- const leaveIntentDelay = 300 ; // in ms
84
-
85
89
export const MenuItem = forwardRef < HTMLLIElement , MenuItemProps > (
86
90
function MenuItem (
87
91
{
@@ -97,26 +101,39 @@ export const MenuItem = forwardRef<HTMLLIElement, MenuItemProps>(
97
101
} ,
98
102
forwardRef
99
103
) {
104
+ const [ submenuOpen , setSubmenuOpen ] = useState ( false ) ;
105
+ const [ rtl , setRtl ] = useState ( false ) ;
106
+
107
+ const {
108
+ refs,
109
+ floatingStyles,
110
+ context : floatingContext ,
111
+ } = useFloating ( {
112
+ open : submenuOpen ,
113
+ onOpenChange : setSubmenuOpen ,
114
+ placement : rtl ? 'left-start' : 'right-start' ,
115
+ whileElementsMounted : autoUpdate ,
116
+ } ) ;
117
+ const { getReferenceProps, getFloatingProps } = useInteractions ( [
118
+ useHover ( floatingContext , {
119
+ enabled : true ,
120
+ handleClose : safePolygon ( {
121
+ requireIntent : true ,
122
+ } ) ,
123
+ } ) ,
124
+ ] ) ;
125
+
100
126
const prefix = usePrefix ( ) ;
101
127
const context = useContext ( MenuContext ) ;
102
128
103
129
const menuItem = useRef < HTMLLIElement > ( null ) ;
104
- const ref = useMergedRefs < HTMLLIElement > ( [ forwardRef , menuItem ] ) ;
105
- const [ boundaries , setBoundaries ] = useState < {
106
- x : number | [ number , number ] ;
107
- y : number | [ number , number ] ;
108
- } > ( { x : - 1 , y : - 1 } ) ;
109
- const [ rtl , setRtl ] = useState ( false ) ;
130
+ const ref = useMergedRefs < HTMLLIElement > ( [
131
+ forwardRef ,
132
+ menuItem ,
133
+ refs . setReference ,
134
+ ] ) ;
110
135
111
136
const hasChildren = Boolean ( children ) ;
112
- const [ submenuOpen , setSubmenuOpen ] = useState ( false ) ;
113
- const hoverIntentTimeout = useRef < ReturnType < typeof setTimeout > | null > (
114
- null
115
- ) ;
116
-
117
- const leaveIntentTimeout = useRef < ReturnType < typeof setTimeout > | null > (
118
- null
119
- ) ;
120
137
121
138
const isDisabled = disabled && ! hasChildren ;
122
139
const isDanger = kind === 'danger' && ! hasChildren ;
@@ -136,32 +153,19 @@ export const MenuItem = forwardRef<HTMLLIElement, MenuItemProps>(
136
153
return ;
137
154
}
138
155
139
- const { x, y, width, height } = menuItem . current . getBoundingClientRect ( ) ;
140
- if ( rtl ) {
141
- setBoundaries ( {
142
- x : [ - x , x - width ] ,
143
- y : [ y , y + height ] ,
144
- } ) ;
145
- } else {
146
- setBoundaries ( {
147
- x : [ x , x + width ] ,
148
- y : [ y , y + height ] ,
149
- } ) ;
150
- }
151
-
152
156
setSubmenuOpen ( true ) ;
153
157
}
154
158
155
159
function closeSubmenu ( ) {
156
160
setSubmenuOpen ( false ) ;
157
- setBoundaries ( { x : - 1 , y : - 1 } ) ;
158
161
}
159
162
160
163
function handleClick (
161
164
e : KeyboardEvent < HTMLLIElement > | MouseEvent < HTMLLIElement >
162
165
) {
163
166
if ( ! isDisabled ) {
164
167
if ( hasChildren ) {
168
+ setSubmenuOpen ( true ) ;
165
169
openSubmenu ( ) ;
166
170
} else {
167
171
context . state . requestCloseRoot ( e ) ;
@@ -173,29 +177,6 @@ export const MenuItem = forwardRef<HTMLLIElement, MenuItemProps>(
173
177
}
174
178
}
175
179
176
- function handleMouseEnter ( ) {
177
- if ( leaveIntentTimeout . current ) {
178
- // When mouse reenters before closing keep sub menu open
179
- clearTimeout ( leaveIntentTimeout . current ) ;
180
- leaveIntentTimeout . current = null ;
181
- }
182
- hoverIntentTimeout . current = setTimeout ( ( ) => {
183
- openSubmenu ( ) ;
184
- } , hoverIntentDelay ) ;
185
- }
186
-
187
- function handleMouseLeave ( ) {
188
- if ( hoverIntentTimeout . current ) {
189
- clearTimeout ( hoverIntentTimeout . current ) ;
190
- // Avoid closing the sub menu as soon as mouse leaves
191
- // prevents accidental closure due to scroll bar
192
- leaveIntentTimeout . current = setTimeout ( ( ) => {
193
- closeSubmenu ( ) ;
194
- menuItem . current ?. focus ( ) ;
195
- } , leaveIntentDelay ) ;
196
- }
197
- }
198
-
199
180
function handleKeyDown ( e : React . KeyboardEvent < HTMLLIElement > ) {
200
181
if ( hasChildren && match ( e , keys . ArrowRight ) ) {
201
182
openSubmenu ( ) ;
@@ -245,48 +226,63 @@ export const MenuItem = forwardRef<HTMLLIElement, MenuItemProps>(
245
226
}
246
227
} , [ iconsAllowed , IconElement , context . state . hasIcons , context ] ) ;
247
228
229
+ useEffect ( ( ) => {
230
+ Object . keys ( floatingStyles ) . forEach ( ( style ) => {
231
+ if ( refs . floating . current && style !== 'position' ) {
232
+ refs . floating . current . style [ style ] = floatingStyles [ style ] ;
233
+ }
234
+ } ) ;
235
+ } , [ floatingStyles , refs . floating ] ) ;
236
+
248
237
return (
249
- < li
250
- role = "menuitem"
251
- { ...rest }
252
- ref = { ref }
253
- className = { classNames }
254
- tabIndex = { - 1 }
255
- aria-disabled = { isDisabled ?? undefined }
256
- aria-haspopup = { hasChildren ?? undefined }
257
- aria-expanded = { hasChildren ? submenuOpen : undefined }
258
- onClick = { handleClick }
259
- onMouseEnter = { hasChildren ? handleMouseEnter : undefined }
260
- onMouseLeave = { hasChildren ? handleMouseLeave : undefined }
261
- onKeyDown = { handleKeyDown } >
262
- < div className = { `${ prefix } --menu-item__icon` } >
263
- { iconsAllowed && IconElement && < IconElement /> }
264
- </ div >
265
- < Text as = "div" className = { `${ prefix } --menu-item__label` } title = { label } >
266
- { label }
267
- </ Text >
268
- { shortcut && ! hasChildren && (
269
- < div className = { `${ prefix } --menu-item__shortcut` } > { shortcut } </ div >
270
- ) }
271
- { hasChildren && (
272
- < >
273
- < div className = { `${ prefix } --menu-item__shortcut` } >
274
- { rtl ? < CaretLeft /> : < CaretRight /> }
275
- </ div >
276
- < Menu
277
- label = { label }
278
- open = { submenuOpen }
279
- onClose = { ( ) => {
280
- closeSubmenu ( ) ;
281
- menuItem . current ?. focus ( ) ;
282
- } }
283
- x = { boundaries . x }
284
- y = { boundaries . y } >
285
- { children }
286
- </ Menu >
287
- </ >
288
- ) }
289
- </ li >
238
+ < FloatingFocusManager
239
+ context = { floatingContext }
240
+ order = { [ 'reference' , 'floating' ] }
241
+ visuallyHiddenDismiss >
242
+ < li
243
+ role = "menuitem"
244
+ { ...rest }
245
+ ref = { ref }
246
+ className = { classNames }
247
+ tabIndex = { - 1 }
248
+ aria-disabled = { isDisabled ?? undefined }
249
+ aria-haspopup = { hasChildren ?? undefined }
250
+ aria-expanded = { hasChildren ? submenuOpen : undefined }
251
+ onClick = { handleClick }
252
+ onKeyDown = { handleKeyDown }
253
+ { ...getReferenceProps ( ) } >
254
+ < div className = { `${ prefix } --menu-item__icon` } >
255
+ { iconsAllowed && IconElement && < IconElement /> }
256
+ </ div >
257
+ < Text
258
+ as = "div"
259
+ className = { `${ prefix } --menu-item__label` }
260
+ title = { label } >
261
+ { label }
262
+ </ Text >
263
+ { shortcut && ! hasChildren && (
264
+ < div className = { `${ prefix } --menu-item__shortcut` } > { shortcut } </ div >
265
+ ) }
266
+ { hasChildren && (
267
+ < >
268
+ < div className = { `${ prefix } --menu-item__shortcut` } >
269
+ { rtl ? < CaretLeft /> : < CaretRight /> }
270
+ </ div >
271
+ < Menu
272
+ label = { label }
273
+ open = { submenuOpen }
274
+ onClose = { ( ) => {
275
+ closeSubmenu ( ) ;
276
+ menuItem . current ?. focus ( ) ;
277
+ } }
278
+ ref = { refs . setFloating }
279
+ { ...getFloatingProps ( ) } >
280
+ { children }
281
+ </ Menu >
282
+ </ >
283
+ ) }
284
+ </ li >
285
+ </ FloatingFocusManager >
290
286
) ;
291
287
}
292
288
) ;
0 commit comments