Something went wrong. Try again.
Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.
Something went wrong. Try again.
4.9 kB · 150 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151import { RestoreSelection, snapshotSelection, restoreSelection,} from './utils/selection';
import { getActive, getFocusTargets, focus } from './utils/focus';import { click } from './utils/click';import { useLayoutEffect } from './utils/react';import { contains, getRoot, isInputElement } from './utils/element';import { Ref } from './types';
export interface MenuFocusOptions { disabled?: boolean; ownerRef?: Ref<HTMLElement>;}
export function useMenuFocus<T extends HTMLElement>( ref: Ref<T>, options?: MenuFocusOptions) { const ownerRef = options && options.ownerRef; const disabled = !!(options && options.disabled);
useLayoutEffect(() => { const { current: element } = ref; if (!element || disabled) return;
const root = getRoot(element); let selection: RestoreSelection | null = null;
function onFocus(event: FocusEvent) { if (!element || event.defaultPrevented) return;
const owner = (ownerRef && ownerRef.current) || (selection && selection.element); const { relatedTarget, target } = event; if (relatedTarget === owner) { // When owner is explicitly passed we can make a snapshot early selection = snapshotSelection(owner); } else if ( contains(element, target) && !contains(element, relatedTarget) && (!ownerRef || contains(relatedTarget, ownerRef.current)) ) { // Check whether focus is about to move into the container and snapshot last focus selection = snapshotSelection(owner); } else if ( contains(element, relatedTarget) && !contains(element, target) ) { // Reset focus if it's lost and has left the menu selection = null; } }
function onKey(event: KeyboardEvent) { if (!element || event.defaultPrevented || event.isComposing) return;
const owner = (ownerRef && ownerRef.current) || (selection && selection.element); const active = getActive(); const focusTargets = getFocusTargets(element); if ( !focusTargets.length || (!contains(element, active) && !contains(owner, active)) ) { // Do nothing if container doesn't contain focus or not targets are available return; }
if ( (!isInputElement(active) && event.code === 'ArrowRight') || event.code === 'ArrowDown' ) { // Implement forward movement in focus targets event.preventDefault(); const focusIndex = focusTargets.indexOf(active!); const nextIndex = focusIndex < focusTargets.length - 1 ? focusIndex + 1 : 0; focus(focusTargets[nextIndex]); } else if ( (!isInputElement(active) && event.code === 'ArrowLeft') || event.code === 'ArrowUp' ) { // Implement backward movement in focus targets event.preventDefault(); const focusIndex = focusTargets.indexOf(active!); const nextIndex = focusIndex > 0 ? focusIndex - 1 : focusTargets.length - 1; focus(focusTargets[nextIndex]); } else if (event.code === 'Home') { // Implement Home => first item event.preventDefault(); focus(focusTargets[0]); } else if (event.code === 'End') { // Implement End => last item event.preventDefault(); focus(focusTargets[focusTargets.length - 1]); } else if ( ownerRef && ownerRef.current && isInputElement(ownerRef.current) && contains(ownerRef.current, active) && event.code === 'Enter' ) { // Move focus to first target when enter is pressed focus(getFocusTargets(element)[0]); } else if ( owner && !contains(ref.current, owner) && !contains(owner, active) && event.code === 'Escape' ) { // Restore selection if escape is pressed event.preventDefault(); restoreSelection(selection); } else if ( (event.code === 'Enter' || event.code === 'Space') && focusTargets.indexOf(active!) > -1 && !isInputElement(active) ) { // Implement virtual click / activation for list items event.preventDefault(); click(active); } else if ( owner && isInputElement(owner) && !contains(owner, active) && /^(?:Key|Digit)/.test(event.code) ) { // Restore selection if a key is pressed on input restoreSelection(selection); } }
root.addEventListener('focusin', onFocus); root.addEventListener('keydown', onKey);
return () => { root.removeEventListener('focusin', onFocus); root.removeEventListener('keydown', onKey);
const active = getActive(); if (!active || contains(element, active)) { restoreSelection(selection); } }; }, [ref.current, disabled]);}