summaryrefslogtreecommitdiff
path: root/packages/frontend/src/scripts/focus-trap.ts
blob: a5df36f520149d01178082dc52e3f9cbd30133e1 (plain)
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
/*
 * SPDX-FileCopyrightText: syuilo and misskey-project
 * SPDX-License-Identifier: AGPL-3.0-only
 */
import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';

const focusTrapElements = new Set<HTMLElement>();
const ignoreElements = [
	'script',
	'style',
];

function containsFocusTrappedElements(el: HTMLElement): boolean {
	return Array.from(focusTrapElements).some((focusTrapElement) => {
		return el.contains(focusTrapElement);
	});
}

function releaseFocusTrap(el: HTMLElement): void {
	focusTrapElements.delete(el);
	if (el.inert === true) {
		el.inert = false;
	}
	if (el.parentElement != null && el !== document.body) {
		el.parentElement.childNodes.forEach((siblingNode) => {
			const siblingEl = getHTMLElementOrNull(siblingNode);
			if (!siblingEl) return;
			if (siblingEl !== el && (focusTrapElements.has(siblingEl) || containsFocusTrappedElements(siblingEl) || focusTrapElements.size === 0)) {
				siblingEl.inert = false;
			} else if (
				focusTrapElements.size > 0 &&
				!containsFocusTrappedElements(siblingEl) &&
				!focusTrapElements.has(siblingEl) &&
				!ignoreElements.includes(siblingEl.tagName.toLowerCase())
			) {
				siblingEl.inert = true;
			} else {
				siblingEl.inert = false;
			}
		});
		releaseFocusTrap(el.parentElement);
	}
}

export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls: boolean, parent: true): void;
export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls?: boolean, parent?: false): { release: () => void; };
export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls = false, parent = false): { release: () => void; } | void {
	if (el.inert === true) {
		el.inert = false;
	}
	if (el.parentElement != null && el !== document.body) {
		el.parentElement.childNodes.forEach((siblingNode) => {
			const siblingEl = getHTMLElementOrNull(siblingNode);
			if (!siblingEl) return;
			if (
				siblingEl !== el &&
				(
					hasInteractionWithOtherFocusTrappedEls === false ||
					(!focusTrapElements.has(siblingEl) && !containsFocusTrappedElements(siblingEl))
				) &&
				!ignoreElements.includes(siblingEl.tagName.toLowerCase())
			) {
				siblingEl.inert = true;
			}
		});
		focusTrap(el.parentElement, hasInteractionWithOtherFocusTrappedEls, true);
	}

	if (!parent) {
		focusTrapElements.add(el);

		return {
			release: () => {
				releaseFocusTrap(el);
			},
		};
	}
}