Skip to content

Commit fd712a7

Browse files
committed
feat(MenuContainer): default arrow key handling to focus items
1 parent 039e095 commit fd712a7

File tree

1 file changed

+40
-2
lines changed

1 file changed

+40
-2
lines changed

packages/react-core/src/components/Menu/MenuContainer.tsx

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface MenuPopperProps {
1717
/** Flag to prevent the popper from overflowing its container and becoming partially obscured. */
1818
preventOverflow?: boolean;
1919
}
20+
2021
export interface MenuContainerProps {
2122
/** Menu to be rendered */
2223
menu: React.ReactElement<any, string | React.JSXElementConstructor<any>>;
@@ -33,6 +34,8 @@ export interface MenuContainerProps {
3334
onOpenChange?: (isOpen: boolean) => void;
3435
/** Keys that trigger onOpenChange, defaults to tab and escape. It is highly recommended to include Escape in the array, while Tab may be omitted if the menu contains non-menu items that are focusable. */
3536
onOpenChangeKeys?: string[];
37+
/** Custom callback to override the default behaviour when pressing up/down arrows. Default is focusing the menu items (first item on arrow down, last item on arrow up). */
38+
onArrowUpDownKeyDown?: (event: KeyboardEvent) => void;
3639
/** z-index of the dropdown menu */
3740
zIndex?: number;
3841
/** Additional properties to pass to the Popper */
@@ -55,10 +58,11 @@ export const MenuContainer: React.FunctionComponent<MenuContainerProps> = ({
5558
toggle,
5659
toggleRef,
5760
onOpenChange,
61+
onArrowUpDownKeyDown,
5862
zIndex = 9999,
5963
popperProps,
6064
onOpenChangeKeys = ['Escape', 'Tab'],
61-
shouldFocusFirstItemOnOpen = true,
65+
shouldFocusFirstItemOnOpen = false,
6266
shouldPreventScrollOnItemFocus = true,
6367
focusTimeoutDelay = 0
6468
}: MenuContainerProps) => {
@@ -79,6 +83,23 @@ export const MenuContainer: React.FunctionComponent<MenuContainerProps> = ({
7983
}, [isOpen]);
8084

8185
React.useEffect(() => {
86+
const onArrowUpDownKeyDownDefault = (event: KeyboardEvent) => {
87+
event.preventDefault();
88+
89+
let listItem: HTMLLIElement;
90+
if (event.key === 'ArrowDown') {
91+
listItem = menuRef.current?.querySelector('li');
92+
} else {
93+
const allItems = menuRef.current?.querySelectorAll('li');
94+
listItem = allItems ? allItems[allItems.length - 1] : null;
95+
}
96+
97+
const focusableElement = listItem?.querySelector(
98+
'button:not(:disabled),input:not(:disabled),a:not([aria-disabled="true"])'
99+
);
100+
focusableElement && (focusableElement as HTMLElement).focus();
101+
};
102+
82103
const handleMenuKeys = (event: KeyboardEvent) => {
83104
// Close the menu on tab or escape if onOpenChange is provided
84105
if (
@@ -90,6 +111,14 @@ export const MenuContainer: React.FunctionComponent<MenuContainerProps> = ({
90111
toggleRef.current?.focus();
91112
}
92113
}
114+
115+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
116+
if (onArrowUpDownKeyDown) {
117+
onArrowUpDownKeyDown(event);
118+
} else {
119+
onArrowUpDownKeyDownDefault(event);
120+
}
121+
}
93122
};
94123

95124
const handleClick = (event: MouseEvent) => {
@@ -108,7 +137,16 @@ export const MenuContainer: React.FunctionComponent<MenuContainerProps> = ({
108137
window.removeEventListener('keydown', handleMenuKeys);
109138
window.removeEventListener('click', handleClick);
110139
};
111-
}, [focusTimeoutDelay, isOpen, menuRef, onOpenChange, onOpenChangeKeys, shouldPreventScrollOnItemFocus, toggleRef]);
140+
}, [
141+
focusTimeoutDelay,
142+
isOpen,
143+
menuRef,
144+
onOpenChange,
145+
onOpenChangeKeys,
146+
onArrowUpDownKeyDown,
147+
shouldPreventScrollOnItemFocus,
148+
toggleRef
149+
]);
112150

113151
return (
114152
<Popper

0 commit comments

Comments
 (0)