Skip to content

Commit 601f081

Browse files
fix: focus issue in menu
1 parent cc284f3 commit 601f081

File tree

2 files changed

+97
-98
lines changed

2 files changed

+97
-98
lines changed

packages/react/src/components/Menu/Menu.stories.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ export const Playground = (args) => {
4949

5050
return (
5151
<Menu {...args} target={target} x={document?.dir === 'rtl' ? 250 : 0}>
52+
<MenuItem label="Copy" shortcut="⌘C" onClick={itemOnClick} />
53+
<MenuItemDivider />
54+
<MenuItem label="Cut" shortcut="⌘X" onClick={itemOnClick}>
55+
<MenuItem label="Cuts" shortcut="⌘ssX" onClick={itemOnClick} />
56+
</MenuItem>
5257
<MenuItem label="Share with">
5358
<MenuItemRadioGroup
5459
label="Share with"
@@ -57,8 +62,6 @@ export const Playground = (args) => {
5762
onChange={radioOnChange}
5863
/>
5964
</MenuItem>
60-
<MenuItemDivider />
61-
<MenuItem label="Cut" shortcut="⌘X" onClick={itemOnClick} />
6265
<MenuItem label="Copy" shortcut="⌘C" onClick={itemOnClick} />
6366
<MenuItem label="Paste" shortcut="⌘V" disabled onClick={itemOnClick} />
6467
<MenuItemDivider />

packages/react/src/components/Menu/MenuItem.tsx

Lines changed: 92 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,14 @@ import React, {
2222
useRef,
2323
useState,
2424
} from 'react';
25-
25+
import {
26+
useHover,
27+
useFloating,
28+
useInteractions,
29+
safePolygon,
30+
autoUpdate,
31+
FloatingFocusManager,
32+
} from '@floating-ui/react';
2633
import { CaretRight, CaretLeft, Checkmark } from '@carbon/icons-react';
2734
import { keys, match } from '../../internal/keyboard';
2835
import { useControllableState } from '../../internal/useControllableState';
@@ -79,9 +86,6 @@ export interface MenuItemProps extends LiHTMLAttributes<HTMLLIElement> {
7986
shortcut?: string;
8087
}
8188

82-
const hoverIntentDelay = 150; // in ms
83-
const leaveIntentDelay = 300; // in ms
84-
8589
export const MenuItem = forwardRef<HTMLLIElement, MenuItemProps>(
8690
function MenuItem(
8791
{
@@ -97,26 +101,39 @@ export const MenuItem = forwardRef<HTMLLIElement, MenuItemProps>(
97101
},
98102
forwardRef
99103
) {
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+
100126
const prefix = usePrefix();
101127
const context = useContext(MenuContext);
102128

103129
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+
]);
110135

111136
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-
);
120137

121138
const isDisabled = disabled && !hasChildren;
122139
const isDanger = kind === 'danger' && !hasChildren;
@@ -136,32 +153,19 @@ export const MenuItem = forwardRef<HTMLLIElement, MenuItemProps>(
136153
return;
137154
}
138155

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-
152156
setSubmenuOpen(true);
153157
}
154158

155159
function closeSubmenu() {
156160
setSubmenuOpen(false);
157-
setBoundaries({ x: -1, y: -1 });
158161
}
159162

160163
function handleClick(
161164
e: KeyboardEvent<HTMLLIElement> | MouseEvent<HTMLLIElement>
162165
) {
163166
if (!isDisabled) {
164167
if (hasChildren) {
168+
setSubmenuOpen(true);
165169
openSubmenu();
166170
} else {
167171
context.state.requestCloseRoot(e);
@@ -173,29 +177,6 @@ export const MenuItem = forwardRef<HTMLLIElement, MenuItemProps>(
173177
}
174178
}
175179

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-
199180
function handleKeyDown(e: React.KeyboardEvent<HTMLLIElement>) {
200181
if (hasChildren && match(e, keys.ArrowRight)) {
201182
openSubmenu();
@@ -245,48 +226,63 @@ export const MenuItem = forwardRef<HTMLLIElement, MenuItemProps>(
245226
}
246227
}, [iconsAllowed, IconElement, context.state.hasIcons, context]);
247228

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+
248237
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>
290286
);
291287
}
292288
);

0 commit comments

Comments
 (0)