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
99
100
101
102
103
104
105
106
107
108
109
|
export function debounce(
node: HTMLInputElement,
{ callback, timeout = 500 }: { callback: () => void; timeout?: number }
) {
let timer: NodeJS.Timeout;
function trigger(event: KeyboardEvent) {
clearTimeout(timer);
if (event.key !== 'Enter') {
timer = setTimeout(callback, timeout);
} else {
callback();
}
}
node.addEventListener('keyup', trigger);
return {
destroy() {
clearTimeout(timer);
node.removeEventListener('keyup', trigger);
}
};
}
export function clickOutside(
node: HTMLElement,
{ handler, ignore }: { handler: () => void; ignore?: HTMLElement }
) {
const handle = (event: Event) => {
const target = event.target as HTMLElement;
if (!target || target === ignore) return;
if (node && !node.contains(target) && !event.defaultPrevented) {
handler();
}
};
document.addEventListener('click', handle, true);
return {
destroy() {
document.removeEventListener('click', handle, true);
}
};
}
export const focusableElements = [
'a[href]',
'area[href]',
'input:not([disabled]):not([type="hidden"]):not([aria-hidden])',
'select:not([disabled]):not([aria-hidden])',
'textarea:not([disabled]):not([aria-hidden])',
'button:not([disabled]):not([aria-hidden])',
'iframe',
'object',
'embed',
'[contenteditable]',
'[tabindex]:not([tabindex^="-"])'
];
let trapped: HTMLElement[] = [];
export function trapFocus(node: HTMLElement) {
function handler(event: KeyboardEvent) {
if (event.target === window) return;
// return if we're not the topmost node to handle
if (trapped.at(0) !== node) return;
const focusable = node.querySelectorAll<HTMLElement>(focusableElements.join());
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.key === 'Tab') {
if (!node.contains(document.activeElement)) {
first.focus();
event.preventDefault();
}
if (event.shiftKey && event.target === first) {
last.focus();
event.preventDefault();
} else if (!event.shiftKey && event.target === last) {
first.focus();
event.preventDefault();
}
}
}
if (document.activeElement instanceof HTMLElement) {
// if we trap focus, make sure to blur any previously selected external
// item such that focus does not remain outside of the node
if (!node.contains(document.activeElement)) {
document.activeElement.blur();
}
}
document.addEventListener('keydown', handler);
trapped.unshift(node);
return {
destroy() {
document.removeEventListener('keydown', handler);
trapped = trapped.filter((i) => i !== node);
}
};
}
|