-
Notifications
You must be signed in to change notification settings - Fork 202
Expand file tree
/
Copy pathuseInertBackdrop.ts
More file actions
98 lines (85 loc) · 3.08 KB
/
Copy pathuseInertBackdrop.ts
File metadata and controls
98 lines (85 loc) · 3.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/**
* Marks all sibling/parent background content as inert while modal is open.
* Uses the `inert` attribute (or aria-hidden + pointer-events fallback)
* to make background content non-interactive and hidden from assistive tech.
*
* WCAG 2.1 AA: 4.1.2 (Name, Role, Value)
*/
import { useEffect } from 'react';
interface UseInertBackdropOptions {
/** Whether to make backdrop inert */
isInert: boolean;
/** ID of the modal container (elements outside this are made inert) */
modalId: string;
}
/**
* Make everything outside a given modal container unreachable to both
* pointer input and assistive technology while the modal is open.
*
* Prefers the native `inert` attribute when supported; falls back to
* `aria-hidden` plus `pointer-events: none` for older browsers. Original
* attribute values are restored on cleanup so this hook is safe to
* compose with manually managed `aria-hidden` regions.
*/
export function useInertBackdrop({ isInert, modalId }: UseInertBackdropOptions) {
useEffect(() => {
if (!isInert) return;
const modal = document.getElementById(modalId);
if (!modal) return;
const elementsToInert = new Set<Element>();
const originalStates: Array<{
element: Element;
inert: boolean | null;
ariaHidden: string | null;
pointerEvents: string;
}> = [];
// Inert only siblings outside the modal subtree so we never disable an
// ancestor that still contains the active dialog.
let current: Element | null = modal;
while (current && current !== document.body) {
const parent = current.parentElement;
if (!parent) break;
Array.from(parent.children).forEach((sibling) => {
if (sibling === current) return;
const tagName = sibling.tagName.toLowerCase();
if (['script', 'style', 'link', 'meta', 'noscript'].includes(tagName)) {
return;
}
elementsToInert.add(sibling);
});
current = parent;
}
elementsToInert.forEach((element) => {
originalStates.push({
element,
inert: element.hasAttribute('inert') ? true : null,
ariaHidden: element.getAttribute('aria-hidden'),
pointerEvents: (element as HTMLElement).style.pointerEvents,
});
if ('inert' in HTMLElement.prototype) {
(element as HTMLElement).inert = true;
} else {
element.setAttribute('aria-hidden', 'true');
(element as HTMLElement).style.pointerEvents = 'none';
}
});
return () => {
originalStates.forEach(({ element, inert, ariaHidden, pointerEvents }) => {
if ('inert' in HTMLElement.prototype) {
if (inert === null) {
(element as HTMLElement).removeAttribute('inert');
} else {
(element as HTMLElement).inert = true;
}
} else {
if (ariaHidden === null) {
element.removeAttribute('aria-hidden');
} else {
element.setAttribute('aria-hidden', ariaHidden);
}
(element as HTMLElement).style.pointerEvents = pointerEvents;
}
});
};
}, [isInert, modalId]);
}