@primer/react
Version:
An implementation of GitHub's Primer Design System using React
88 lines (82 loc) • 4.15 kB
JavaScript
import React from 'react';
import { iterateFocusableElements } from '@primer/behaviors/utils';
import { useMenuInitialFocus } from './useMenuInitialFocus.js';
import { useMnemonics } from './useMnemonics.js';
/**
* Keyboard navigation is a mix of 4 hooks
* 1. useMenuInitialFocus
* 2. useTypeaheadFocus
* 3. useCloseMenuOnTab
* 4. useMoveFocusToMenuItem
*/
const useMenuKeyboardNavigation = (open, onClose, containerRef, anchorRef, isSubmenu = false) => {
useMenuInitialFocus(open, containerRef, anchorRef);
useMnemonics(open, containerRef);
useCloseMenuOnTab(open, onClose, containerRef, anchorRef);
useMoveFocusToMenuItem(open, containerRef, anchorRef);
useCloseSubmenuOnArrow(open, isSubmenu, onClose, containerRef);
};
/**
* When Tab or Shift+Tab is pressed, the menu should close
* and the focus should naturally move to the next item
*/
const useCloseMenuOnTab = (open, onClose, containerRef, anchorRef) => {
React.useEffect(() => {
const container = containerRef === null || containerRef === undefined ? undefined : containerRef.current;
const anchor = anchorRef === null || anchorRef === undefined ? undefined : anchorRef.current;
const handler = event => {
if (open && event.key === 'Tab') onClose === null || onClose === undefined ? undefined : onClose('tab');
};
container === null || container === undefined ? undefined : container.addEventListener('keydown', handler);
anchor === null || anchor === undefined ? undefined : anchor.addEventListener('keydown', handler);
return () => {
container === null || container === undefined ? undefined : container.removeEventListener('keydown', handler);
anchor === null || anchor === undefined ? undefined : anchor.removeEventListener('keydown', handler);
};
}, [open, onClose, containerRef, anchorRef]);
};
/**
* Close submenu when left arrow key is pressed
*/
const useCloseSubmenuOnArrow = (open, isSubmenu, onClose, containerRef) => {
React.useEffect(() => {
const container = containerRef === null || containerRef === undefined ? undefined : containerRef.current;
const handler = event => {
if (open && isSubmenu && event.key === 'ArrowLeft') onClose === null || onClose === undefined ? undefined : onClose('arrow-left');
};
container === null || container === undefined ? undefined : container.addEventListener('keydown', handler);
return () => {
container === null || container === undefined ? undefined : container.removeEventListener('keydown', handler);
};
}, [open, onClose, containerRef, isSubmenu]);
};
/**
* When Arrow Keys are pressed and the focus is on the anchor,
* focus should move to a menu item
*/
const useMoveFocusToMenuItem = (open, containerRef, anchorRef) => {
React.useEffect(() => {
const container = containerRef === null || containerRef === undefined ? undefined : containerRef.current;
const anchor = anchorRef === null || anchorRef === undefined ? undefined : anchorRef.current;
const handler = event => {
if (!open || !container) return;
const iterable = iterateFocusableElements(container);
if (event.key === 'ArrowDown') {
// TODO: does commenting this out break anything?
// event.preventDefault() // prevent scroll event
const firstElement = iterable.next().value;
/** We push imperative focus to the next tick to prevent React's batching */
setTimeout(() => firstElement === null || firstElement === undefined ? undefined : firstElement.focus());
} else if (event.key === 'ArrowUp') {
// TODO: does commenting this out break anything?
// event.preventDefault() // prevent scroll event
const elements = [...iterable];
const lastElement = elements[elements.length - 1];
setTimeout(() => lastElement.focus());
}
};
anchor === null || anchor === undefined ? undefined : anchor.addEventListener('keydown', handler);
return () => anchor === null || anchor === undefined ? undefined : anchor.addEventListener('keydown', handler);
}, [open, containerRef, anchorRef]);
};
export { useMenuKeyboardNavigation };