Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.
1import { RestoreSelection, snapshotSelection, restoreSelection } from './utils/selection';
2import { getFirstFocusTarget, getFocusTargets } from './utils/focus';
3import { useLayoutEffect } from './utils/react';
4import { contains } from './utils/element';
5import { makePriorityHook } from './usePriority';
6import { Ref } from './types';
7
8const usePriority = makePriorityHook();
9
10export interface ModalFocusOptions {
11 disabled?: boolean;
12}
13
14export function useModalFocus<T extends HTMLElement>(ref: Ref<T>, options?: ModalFocusOptions) {
15 const disabled = !!(options && options.disabled);
16 const hasPriority = usePriority(ref, disabled);
17
18 useLayoutEffect(() => {
19 if (!ref.current || !hasPriority || disabled) return;
20
21 let selection: RestoreSelection | null = null;
22 if (!document.activeElement || !ref.current.contains(document.activeElement)) {
23 const newTarget = getFirstFocusTarget(ref.current);
24 if (newTarget) {
25 selection = snapshotSelection(ref.current);
26 newTarget.focus();
27 }
28 }
29
30 function onBlur(event: FocusEvent) {
31 const parent = ref.current;
32 if (!parent || event.defaultPrevented) return;
33
34 if (contains(parent, event.target) && !contains(parent, event.relatedTarget)) {
35 const target = getFirstFocusTarget(parent);
36 if (target) target.focus();
37 }
38 }
39
40 function onKeyDown(event: KeyboardEvent) {
41 const parent = ref.current;
42 if (!parent || event.defaultPrevented) return;
43
44 if (event.code === 'Tab') {
45 const activeElement = document.activeElement as HTMLElement;
46 const targets = getFocusTargets(parent);
47 const index = targets.indexOf(activeElement);
48 if (event.shiftKey && index === 0) {
49 event.preventDefault();
50 targets[targets.length - 1].focus();
51 } else if (!event.shiftKey && index === targets.length - 1) {
52 event.preventDefault();
53 targets[0].focus();
54 }
55
56 }
57 }
58
59 document.body.addEventListener('focusout', onBlur);
60 document.addEventListener('keydown', onKeyDown);
61
62 return () => {
63 restoreSelection(selection);
64 document.body.removeEventListener('focusout', onBlur);
65 document.removeEventListener('keydown', onKeyDown);
66 };
67 }, [ref, hasPriority, disabled]);
68}