Skip to content

Commit

Permalink
feat(MenuContainer): default arrow key handling to focus items
Browse files Browse the repository at this point in the history
  • Loading branch information
adamviktora committed Oct 25, 2024
1 parent 039e095 commit fd712a7
Showing 1 changed file with 40 additions and 2 deletions.
42 changes: 40 additions & 2 deletions packages/react-core/src/components/Menu/MenuContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface MenuPopperProps {
/** Flag to prevent the popper from overflowing its container and becoming partially obscured. */
preventOverflow?: boolean;
}

export interface MenuContainerProps {
/** Menu to be rendered */
menu: React.ReactElement<any, string | React.JSXElementConstructor<any>>;
Expand All @@ -33,6 +34,8 @@ export interface MenuContainerProps {
onOpenChange?: (isOpen: boolean) => void;
/** 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. */
onOpenChangeKeys?: string[];
/** 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). */
onArrowUpDownKeyDown?: (event: KeyboardEvent) => void;
/** z-index of the dropdown menu */
zIndex?: number;
/** Additional properties to pass to the Popper */
Expand All @@ -55,10 +58,11 @@ export const MenuContainer: React.FunctionComponent<MenuContainerProps> = ({
toggle,
toggleRef,
onOpenChange,
onArrowUpDownKeyDown,
zIndex = 9999,
popperProps,
onOpenChangeKeys = ['Escape', 'Tab'],
shouldFocusFirstItemOnOpen = true,
shouldFocusFirstItemOnOpen = false,
shouldPreventScrollOnItemFocus = true,
focusTimeoutDelay = 0
}: MenuContainerProps) => {
Expand All @@ -79,6 +83,23 @@ export const MenuContainer: React.FunctionComponent<MenuContainerProps> = ({
}, [isOpen]);

React.useEffect(() => {
const onArrowUpDownKeyDownDefault = (event: KeyboardEvent) => {
event.preventDefault();

let listItem: HTMLLIElement;
if (event.key === 'ArrowDown') {
listItem = menuRef.current?.querySelector('li');
} else {
const allItems = menuRef.current?.querySelectorAll('li');
listItem = allItems ? allItems[allItems.length - 1] : null;
}

const focusableElement = listItem?.querySelector(
'button:not(:disabled),input:not(:disabled),a:not([aria-disabled="true"])'
);
focusableElement && (focusableElement as HTMLElement).focus();
};

const handleMenuKeys = (event: KeyboardEvent) => {
// Close the menu on tab or escape if onOpenChange is provided
if (
Expand All @@ -90,6 +111,14 @@ export const MenuContainer: React.FunctionComponent<MenuContainerProps> = ({
toggleRef.current?.focus();
}
}

if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
if (onArrowUpDownKeyDown) {
onArrowUpDownKeyDown(event);
} else {
onArrowUpDownKeyDownDefault(event);
}
}
};

const handleClick = (event: MouseEvent) => {
Expand All @@ -108,7 +137,16 @@ export const MenuContainer: React.FunctionComponent<MenuContainerProps> = ({
window.removeEventListener('keydown', handleMenuKeys);
window.removeEventListener('click', handleClick);
};
}, [focusTimeoutDelay, isOpen, menuRef, onOpenChange, onOpenChangeKeys, shouldPreventScrollOnItemFocus, toggleRef]);
}, [
focusTimeoutDelay,
isOpen,
menuRef,
onOpenChange,
onOpenChangeKeys,
onArrowUpDownKeyDown,
shouldPreventScrollOnItemFocus,
toggleRef
]);

return (
<Popper
Expand Down

0 comments on commit fd712a7

Please sign in to comment.