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