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