@oscarpalmer/oui 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/css/popover.css +9 -0
- package/dist/css/tooltip.css +0 -3
- package/dist/js/floatable.cjs +21 -8
- package/dist/js/floatable.js +21 -8
- package/dist/js/focus-trap/embedded.cjs +150 -0
- package/dist/js/focus-trap/embedded.js +150 -0
- package/dist/js/focus-trap/standalone.cjs +8 -0
- package/dist/js/focus-trap/standalone.js +7 -0
- package/dist/js/index.cjs +5 -1
- package/dist/js/index.js +6 -1
- package/dist/js/popover.cjs +160 -0
- package/dist/js/popover.js +160 -0
- package/dist/js/tooltip.cjs +2 -3
- package/dist/js/tooltip.js +2 -3
- package/package.json +5 -5
- package/src/css/popover.scss +9 -0
- package/src/css/tooltip.scss +0 -3
- package/src/js/floatable.ts +52 -24
- package/src/js/focus-trap/embedded.ts +215 -0
- package/src/js/focus-trap/standalone.ts +9 -0
- package/src/js/index.ts +3 -1
- package/src/js/popover.ts +221 -0
- package/src/js/tooltip.ts +2 -4
- package/types/floatable.d.cts +2 -2
- package/types/floatable.d.ts +2 -2
- package/types/focus-trap/embedded.d.cts +15 -0
- package/types/focus-trap/embedded.d.ts +11 -0
- package/types/index.d.cts +11 -0
- package/types/index.d.ts +3 -1
- package/types/popover.d.cts +14 -0
- package/types/popover.d.ts +15 -0
- package/dist/js/focus-trap.cjs +0 -104
- package/dist/js/focus-trap.js +0 -103
- package/src/js/focus-trap.ts +0 -133
- /package/types/{focus-trap.d.cts → focus-trap/standalone.d.cts} +0 -0
- /package/types/{focus-trap.d.ts → focus-trap/standalone.d.ts} +0 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import {isNullableOrWhitespace} from '@oscarpalmer/atoms/is';
|
|
2
|
+
import {on} from '@oscarpalmer/toretto/event';
|
|
3
|
+
import {findAncestor} from '@oscarpalmer/toretto/find';
|
|
4
|
+
import {getFocusable} from '@oscarpalmer/toretto/focusable';
|
|
5
|
+
import {activate, deactivate, Floatable} from './floatable';
|
|
6
|
+
import {createFocusTrap, type FocusTrap} from './focus-trap/embedded';
|
|
7
|
+
|
|
8
|
+
declare global {
|
|
9
|
+
interface HTMLElementTagNameMap {
|
|
10
|
+
'oui-popover': OuiPopoverElement;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const mapped = new Map<HTMLElement, Floatable>();
|
|
15
|
+
|
|
16
|
+
let index = 0;
|
|
17
|
+
|
|
18
|
+
export class OuiPopoverElement extends HTMLElement {
|
|
19
|
+
#floatable: Floatable;
|
|
20
|
+
#focusTrap: FocusTrap;
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
super();
|
|
24
|
+
|
|
25
|
+
const content = this.querySelector(':scope > [oui-popover-content]');
|
|
26
|
+
const toggle = this.querySelector(':scope > [oui-popover-toggle]');
|
|
27
|
+
|
|
28
|
+
if (!(content instanceof HTMLElement)) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
'A oui-popover must have a direct child element with the [oui-popover-content]-attribute',
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!(toggle instanceof HTMLButtonElement)) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
'A oui-popover must have a direct child element with the [oui-popover-toggle]-attribute and be a button',
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
content.hidden = true;
|
|
41
|
+
|
|
42
|
+
content.setAttribute('role', 'dialog');
|
|
43
|
+
content.setAttribute('oui-focus-trap', '');
|
|
44
|
+
content.setAttribute('oui-focus-trap-noescape', '');
|
|
45
|
+
|
|
46
|
+
if (isNullableOrWhitespace(content.id)) {
|
|
47
|
+
content.setAttribute('id', `oui-popover-content-${++index}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
toggle.setAttribute('aria-controls', content.id);
|
|
51
|
+
toggle.setAttribute('aria-haspopup', 'dialog');
|
|
52
|
+
|
|
53
|
+
this.#floatable = new Floatable(toggle, content);
|
|
54
|
+
this.#focusTrap = createFocusTrap(content);
|
|
55
|
+
|
|
56
|
+
mapped.set(toggle, this.#floatable);
|
|
57
|
+
mapped.set(content, this.#floatable);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
close(): void {
|
|
61
|
+
this.hidePopover();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
hidePopover(): void {
|
|
65
|
+
if (this.#floatable.active) {
|
|
66
|
+
closeAbove(this.#floatable.content);
|
|
67
|
+
deactivate(this.#floatable, active, order);
|
|
68
|
+
|
|
69
|
+
this.#floatable.anchor.focus();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
open(): void {
|
|
74
|
+
this.showPopover();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
showPopover(): void {
|
|
78
|
+
if (!this.#floatable.active) {
|
|
79
|
+
toggle(this.#floatable.anchor);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
toggle(): void {
|
|
84
|
+
this.togglePopover();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
togglePopover(options?: boolean): boolean {
|
|
88
|
+
if (typeof options === 'boolean') {
|
|
89
|
+
if (options === true) {
|
|
90
|
+
this.showPopover();
|
|
91
|
+
} else {
|
|
92
|
+
this.hidePopover();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return options;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (this.#floatable.active) {
|
|
99
|
+
this.hidePopover();
|
|
100
|
+
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.showPopover();
|
|
105
|
+
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
//
|
|
111
|
+
|
|
112
|
+
function closeAll(): void {
|
|
113
|
+
for (const floatable of active) {
|
|
114
|
+
deactivate(floatable, active, order);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function closeAbove(element: HTMLElement): void {
|
|
119
|
+
const index = order.findIndex(
|
|
120
|
+
floatable => floatable.anchor === element || floatable.content === element,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
if (index === -1 || index === order.length - 1) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
while (index < order.length - 1) {
|
|
128
|
+
const floatable = order.pop();
|
|
129
|
+
|
|
130
|
+
if (floatable != null) {
|
|
131
|
+
deactivate(floatable, active, order);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
order[index].content.focus();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function focus(content: HTMLElement): void {
|
|
139
|
+
let target = getFocusable(content)[0];
|
|
140
|
+
|
|
141
|
+
if (!(target instanceof HTMLElement)) {
|
|
142
|
+
target = content;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
requestAnimationFrame(() => {
|
|
146
|
+
(target as HTMLElement).focus();
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function onClick(event: MouseEvent): void {
|
|
151
|
+
const related = findAncestor(
|
|
152
|
+
event.target as never,
|
|
153
|
+
'[oui-popover-toggle], [oui-popover-content]',
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
if (!(related instanceof HTMLElement)) {
|
|
157
|
+
closeAll();
|
|
158
|
+
} else if (related.hasAttribute('oui-popover-content')) {
|
|
159
|
+
closeAbove(related);
|
|
160
|
+
} else {
|
|
161
|
+
toggle(related);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function onKeydown(event: KeyboardEvent): void {
|
|
166
|
+
if (event.key !== 'Escape' || order.length === 0) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const related = findAncestor(event.target as never, '[oui-popover-content]');
|
|
171
|
+
|
|
172
|
+
if (!(related instanceof HTMLElement)) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
closeAbove(related);
|
|
177
|
+
|
|
178
|
+
const floatable = mapped.get(related);
|
|
179
|
+
|
|
180
|
+
if (floatable != null) {
|
|
181
|
+
deactivate(floatable, active, order);
|
|
182
|
+
|
|
183
|
+
floatable.anchor.focus();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function toggle(anchor: HTMLElement): void {
|
|
188
|
+
const content = findAncestor(anchor, '[oui-popover-content]');
|
|
189
|
+
|
|
190
|
+
if (content instanceof HTMLElement) {
|
|
191
|
+
closeAbove(content);
|
|
192
|
+
} else {
|
|
193
|
+
closeAll();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const floatable = mapped.get(anchor);
|
|
197
|
+
|
|
198
|
+
if (floatable == null) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (floatable.active) {
|
|
203
|
+
deactivate(floatable, active, order);
|
|
204
|
+
} else {
|
|
205
|
+
activate(floatable, active, order);
|
|
206
|
+
focus(floatable.content);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
//
|
|
211
|
+
|
|
212
|
+
customElements.define('oui-popover', OuiPopoverElement);
|
|
213
|
+
|
|
214
|
+
on(document, 'click', onClick);
|
|
215
|
+
on(document, 'keydown', onKeydown);
|
|
216
|
+
|
|
217
|
+
//
|
|
218
|
+
|
|
219
|
+
const active = new Set<Floatable>();
|
|
220
|
+
|
|
221
|
+
const order: Floatable[] = [];
|
package/src/js/tooltip.ts
CHANGED
|
@@ -162,7 +162,7 @@ function initialize(tooltip: Tooltip): void {
|
|
|
162
162
|
element,
|
|
163
163
|
event,
|
|
164
164
|
() => {
|
|
165
|
-
activate(floatable, active, 250);
|
|
165
|
+
activate(floatable, active, undefined, 250);
|
|
166
166
|
},
|
|
167
167
|
{
|
|
168
168
|
signal: abortController.signal,
|
|
@@ -175,7 +175,7 @@ function initialize(tooltip: Tooltip): void {
|
|
|
175
175
|
element,
|
|
176
176
|
event,
|
|
177
177
|
() => {
|
|
178
|
-
deactivate(floatable, active, 250);
|
|
178
|
+
deactivate(floatable, active, undefined, 250);
|
|
179
179
|
},
|
|
180
180
|
{
|
|
181
181
|
signal: abortController.signal,
|
|
@@ -191,8 +191,6 @@ function removeTooltip(element: HTMLElement): void {
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
function reset(anchor: HTMLElement, attributes: Attributes): void {
|
|
194
|
-
console.log('reset', anchor, attributes);
|
|
195
|
-
|
|
196
194
|
if (attributes.ariaDescribedby == null) {
|
|
197
195
|
anchor.removeAttribute('aria-describedby');
|
|
198
196
|
} else {
|
package/types/floatable.d.cts
CHANGED
|
@@ -10,7 +10,7 @@ export declare class Floatable {
|
|
|
10
10
|
constructor(anchor: HTMLElement, content: HTMLElement);
|
|
11
11
|
destroy(): void;
|
|
12
12
|
}
|
|
13
|
-
export declare function activate(floatable: Floatable, active: Set<Floatable>, time?: number): void;
|
|
14
|
-
export declare function deactivate(floatable: Floatable, active: Set<Floatable>, time?: number): void;
|
|
13
|
+
export declare function activate(floatable: Floatable, active: Set<Floatable>, order?: Floatable[], time?: number): void;
|
|
14
|
+
export declare function deactivate(floatable: Floatable, active: Set<Floatable>, order?: Floatable[], time?: number): void;
|
|
15
15
|
|
|
16
16
|
export {};
|
package/types/floatable.d.ts
CHANGED
|
@@ -8,5 +8,5 @@ export declare class Floatable {
|
|
|
8
8
|
constructor(anchor: HTMLElement, content: HTMLElement);
|
|
9
9
|
destroy(): void;
|
|
10
10
|
}
|
|
11
|
-
export declare function activate(floatable: Floatable, active: Set<Floatable>, time?: number): void;
|
|
12
|
-
export declare function deactivate(floatable: Floatable, active: Set<Floatable>, time?: number): void;
|
|
11
|
+
export declare function activate(floatable: Floatable, active: Set<Floatable>, order?: Floatable[], time?: number): void;
|
|
12
|
+
export declare function deactivate(floatable: Floatable, active: Set<Floatable>, order?: Floatable[], time?: number): void;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Generated by dts-bundle-generator v9.5.1
|
|
2
|
+
|
|
3
|
+
export declare class FocusTrap {
|
|
4
|
+
element: HTMLElement;
|
|
5
|
+
active: boolean;
|
|
6
|
+
constructor(element: HTMLElement);
|
|
7
|
+
destroy(): void;
|
|
8
|
+
disable(): void;
|
|
9
|
+
enable(): void;
|
|
10
|
+
}
|
|
11
|
+
export declare function createFocusTrap(element: HTMLElement): FocusTrap;
|
|
12
|
+
export declare const focusTraps: WeakMap<HTMLElement, FocusTrap>;
|
|
13
|
+
export declare const selector = "oui-focus-trap";
|
|
14
|
+
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare class FocusTrap {
|
|
2
|
+
element: HTMLElement;
|
|
3
|
+
active: boolean;
|
|
4
|
+
constructor(element: HTMLElement);
|
|
5
|
+
destroy(): void;
|
|
6
|
+
disable(): void;
|
|
7
|
+
enable(): void;
|
|
8
|
+
}
|
|
9
|
+
export declare function createFocusTrap(element: HTMLElement): FocusTrap;
|
|
10
|
+
export declare const focusTraps: WeakMap<HTMLElement, FocusTrap>;
|
|
11
|
+
export declare const selector = "oui-focus-trap";
|
package/types/index.d.cts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
1
|
// Generated by dts-bundle-generator v9.5.1
|
|
2
2
|
|
|
3
|
+
export declare class OuiPopoverElement extends HTMLElement {
|
|
4
|
+
#private;
|
|
5
|
+
constructor();
|
|
6
|
+
close(): void;
|
|
7
|
+
hidePopover(): void;
|
|
8
|
+
open(): void;
|
|
9
|
+
showPopover(): void;
|
|
10
|
+
toggle(): void;
|
|
11
|
+
togglePopover(options?: boolean): boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
3
14
|
export {};
|
package/types/index.d.ts
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Generated by dts-bundle-generator v9.5.1
|
|
2
|
+
|
|
3
|
+
export declare class OuiPopoverElement extends HTMLElement {
|
|
4
|
+
#private;
|
|
5
|
+
constructor();
|
|
6
|
+
close(): void;
|
|
7
|
+
hidePopover(): void;
|
|
8
|
+
open(): void;
|
|
9
|
+
showPopover(): void;
|
|
10
|
+
toggle(): void;
|
|
11
|
+
togglePopover(options?: boolean): boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
interface HTMLElementTagNameMap {
|
|
3
|
+
'oui-popover': OuiPopoverElement;
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
export declare class OuiPopoverElement extends HTMLElement {
|
|
7
|
+
#private;
|
|
8
|
+
constructor();
|
|
9
|
+
close(): void;
|
|
10
|
+
hidePopover(): void;
|
|
11
|
+
open(): void;
|
|
12
|
+
showPopover(): void;
|
|
13
|
+
toggle(): void;
|
|
14
|
+
togglePopover(options?: boolean): boolean;
|
|
15
|
+
}
|
package/dist/js/focus-trap.cjs
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
const number = require("@oscarpalmer/atoms/number");
|
|
3
|
-
const event = require("@oscarpalmer/toretto/event");
|
|
4
|
-
const find = require("@oscarpalmer/toretto/find");
|
|
5
|
-
const focusable = require("@oscarpalmer/toretto/focusable");
|
|
6
|
-
const attributable = require("./attributable.cjs");
|
|
7
|
-
class FocusTrap {
|
|
8
|
-
constructor(element) {
|
|
9
|
-
this.element = element;
|
|
10
|
-
element.setAttribute("aria-modal", "true");
|
|
11
|
-
element.setAttribute("tabindex", "-1");
|
|
12
|
-
}
|
|
13
|
-
active = true;
|
|
14
|
-
activate() {
|
|
15
|
-
this.element.setAttribute("aria-modal", "true");
|
|
16
|
-
this.active = true;
|
|
17
|
-
}
|
|
18
|
-
destroy() {
|
|
19
|
-
this.active = false;
|
|
20
|
-
this.element = void 0;
|
|
21
|
-
}
|
|
22
|
-
inactivate() {
|
|
23
|
-
this.element.setAttribute("aria-modal", "false");
|
|
24
|
-
this.active = false;
|
|
25
|
-
inactive.add(this);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
function addFocusTrap(element) {
|
|
29
|
-
if (!focusTraps.has(element)) {
|
|
30
|
-
focusTraps.set(element, new FocusTrap(element));
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
function onEscape(event2) {
|
|
34
|
-
if (event2.key !== "Escape") {
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
const element = find.findAncestor(
|
|
38
|
-
document.activeElement,
|
|
39
|
-
`[${selector}]`
|
|
40
|
-
);
|
|
41
|
-
focusTraps.get(element)?.inactivate();
|
|
42
|
-
}
|
|
43
|
-
function onFocusOut() {
|
|
44
|
-
if (inactive.size === 0) {
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
requestAnimationFrame(() => {
|
|
48
|
-
const element = find.findAncestor(
|
|
49
|
-
document.activeElement,
|
|
50
|
-
`[${selector}]`
|
|
51
|
-
);
|
|
52
|
-
if (!focusTraps.has(element)) {
|
|
53
|
-
for (const focusTrap of inactive) {
|
|
54
|
-
focusTrap.activate();
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
function onTab(event2) {
|
|
60
|
-
if (event2.key !== "Tab") {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
const element = find.findAncestor(
|
|
64
|
-
event2.target,
|
|
65
|
-
`[${selector}]`
|
|
66
|
-
);
|
|
67
|
-
if (!(focusTraps.get(element)?.active ?? false)) {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
event2.preventDefault();
|
|
71
|
-
const elements = focusable.getFocusable(element);
|
|
72
|
-
const { length } = elements;
|
|
73
|
-
const index = elements.indexOf(event2.target);
|
|
74
|
-
if (index === -1 || length === 0) {
|
|
75
|
-
if (length === 0) {
|
|
76
|
-
element.focus();
|
|
77
|
-
} else {
|
|
78
|
-
elements[0].focus();
|
|
79
|
-
}
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
const next = number.clamp(index + (event2.shiftKey ? -1 : 1), 0, length - 1, true);
|
|
83
|
-
if (next !== index) {
|
|
84
|
-
elements[next].focus();
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
function removeFocusTraps(element) {
|
|
88
|
-
focusTraps.get(element)?.destroy();
|
|
89
|
-
focusTraps.delete(element);
|
|
90
|
-
}
|
|
91
|
-
const focusTraps = /* @__PURE__ */ new WeakMap();
|
|
92
|
-
const inactive = /* @__PURE__ */ new Set();
|
|
93
|
-
const selector = "oui-focus-trap";
|
|
94
|
-
attributable.attributable(selector, addFocusTrap, removeFocusTraps);
|
|
95
|
-
event.on(document, "focusout", onFocusOut, {
|
|
96
|
-
capture: true
|
|
97
|
-
});
|
|
98
|
-
event.on(document, "keydown", onTab, {
|
|
99
|
-
capture: true,
|
|
100
|
-
passive: false
|
|
101
|
-
});
|
|
102
|
-
event.on(document, "keydown", onEscape, {
|
|
103
|
-
passive: false
|
|
104
|
-
});
|
package/dist/js/focus-trap.js
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { clamp } from "@oscarpalmer/atoms/number";
|
|
2
|
-
import { on } from "@oscarpalmer/toretto/event";
|
|
3
|
-
import { findAncestor } from "@oscarpalmer/toretto/find";
|
|
4
|
-
import { getFocusable } from "@oscarpalmer/toretto/focusable";
|
|
5
|
-
import { attributable } from "./attributable.js";
|
|
6
|
-
class FocusTrap {
|
|
7
|
-
constructor(element) {
|
|
8
|
-
this.element = element;
|
|
9
|
-
element.setAttribute("aria-modal", "true");
|
|
10
|
-
element.setAttribute("tabindex", "-1");
|
|
11
|
-
}
|
|
12
|
-
active = true;
|
|
13
|
-
activate() {
|
|
14
|
-
this.element.setAttribute("aria-modal", "true");
|
|
15
|
-
this.active = true;
|
|
16
|
-
}
|
|
17
|
-
destroy() {
|
|
18
|
-
this.active = false;
|
|
19
|
-
this.element = void 0;
|
|
20
|
-
}
|
|
21
|
-
inactivate() {
|
|
22
|
-
this.element.setAttribute("aria-modal", "false");
|
|
23
|
-
this.active = false;
|
|
24
|
-
inactive.add(this);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
function addFocusTrap(element) {
|
|
28
|
-
if (!focusTraps.has(element)) {
|
|
29
|
-
focusTraps.set(element, new FocusTrap(element));
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
function onEscape(event) {
|
|
33
|
-
if (event.key !== "Escape") {
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
const element = findAncestor(
|
|
37
|
-
document.activeElement,
|
|
38
|
-
`[${selector}]`
|
|
39
|
-
);
|
|
40
|
-
focusTraps.get(element)?.inactivate();
|
|
41
|
-
}
|
|
42
|
-
function onFocusOut() {
|
|
43
|
-
if (inactive.size === 0) {
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
requestAnimationFrame(() => {
|
|
47
|
-
const element = findAncestor(
|
|
48
|
-
document.activeElement,
|
|
49
|
-
`[${selector}]`
|
|
50
|
-
);
|
|
51
|
-
if (!focusTraps.has(element)) {
|
|
52
|
-
for (const focusTrap of inactive) {
|
|
53
|
-
focusTrap.activate();
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
function onTab(event) {
|
|
59
|
-
if (event.key !== "Tab") {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
const element = findAncestor(
|
|
63
|
-
event.target,
|
|
64
|
-
`[${selector}]`
|
|
65
|
-
);
|
|
66
|
-
if (!(focusTraps.get(element)?.active ?? false)) {
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
event.preventDefault();
|
|
70
|
-
const elements = getFocusable(element);
|
|
71
|
-
const { length } = elements;
|
|
72
|
-
const index = elements.indexOf(event.target);
|
|
73
|
-
if (index === -1 || length === 0) {
|
|
74
|
-
if (length === 0) {
|
|
75
|
-
element.focus();
|
|
76
|
-
} else {
|
|
77
|
-
elements[0].focus();
|
|
78
|
-
}
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
const next = clamp(index + (event.shiftKey ? -1 : 1), 0, length - 1, true);
|
|
82
|
-
if (next !== index) {
|
|
83
|
-
elements[next].focus();
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
function removeFocusTraps(element) {
|
|
87
|
-
focusTraps.get(element)?.destroy();
|
|
88
|
-
focusTraps.delete(element);
|
|
89
|
-
}
|
|
90
|
-
const focusTraps = /* @__PURE__ */ new WeakMap();
|
|
91
|
-
const inactive = /* @__PURE__ */ new Set();
|
|
92
|
-
const selector = "oui-focus-trap";
|
|
93
|
-
attributable(selector, addFocusTrap, removeFocusTraps);
|
|
94
|
-
on(document, "focusout", onFocusOut, {
|
|
95
|
-
capture: true
|
|
96
|
-
});
|
|
97
|
-
on(document, "keydown", onTab, {
|
|
98
|
-
capture: true,
|
|
99
|
-
passive: false
|
|
100
|
-
});
|
|
101
|
-
on(document, "keydown", onEscape, {
|
|
102
|
-
passive: false
|
|
103
|
-
});
|
package/src/js/focus-trap.ts
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import {clamp} from '@oscarpalmer/atoms/number';
|
|
2
|
-
import {on} from '@oscarpalmer/toretto/event';
|
|
3
|
-
import {findAncestor} from '@oscarpalmer/toretto/find';
|
|
4
|
-
import {getFocusable} from '@oscarpalmer/toretto/focusable';
|
|
5
|
-
import {attributable} from './attributable';
|
|
6
|
-
|
|
7
|
-
class FocusTrap {
|
|
8
|
-
active = true;
|
|
9
|
-
|
|
10
|
-
constructor(private element: HTMLElement) {
|
|
11
|
-
element.setAttribute('aria-modal', 'true');
|
|
12
|
-
element.setAttribute('tabindex', '-1');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
activate() {
|
|
16
|
-
this.element.setAttribute('aria-modal', 'true');
|
|
17
|
-
|
|
18
|
-
this.active = true;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
destroy() {
|
|
22
|
-
this.active = false;
|
|
23
|
-
this.element = undefined as never;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
inactivate() {
|
|
27
|
-
this.element.setAttribute('aria-modal', 'false');
|
|
28
|
-
|
|
29
|
-
this.active = false;
|
|
30
|
-
|
|
31
|
-
inactive.add(this);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function addFocusTrap(element: HTMLElement): void {
|
|
36
|
-
if (!focusTraps.has(element)) {
|
|
37
|
-
focusTraps.set(element, new FocusTrap(element));
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function onEscape(event: KeyboardEvent): void {
|
|
42
|
-
if (event.key !== 'Escape') {
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const element = findAncestor(
|
|
47
|
-
document.activeElement as never,
|
|
48
|
-
`[${selector}]`,
|
|
49
|
-
) as HTMLElement;
|
|
50
|
-
|
|
51
|
-
focusTraps.get(element)?.inactivate();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function onFocusOut(): void {
|
|
55
|
-
if (inactive.size === 0) {
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
requestAnimationFrame(() => {
|
|
60
|
-
const element = findAncestor(
|
|
61
|
-
document.activeElement as never,
|
|
62
|
-
`[${selector}]`,
|
|
63
|
-
) as HTMLElement;
|
|
64
|
-
|
|
65
|
-
if (!focusTraps.has(element)) {
|
|
66
|
-
for (const focusTrap of inactive) {
|
|
67
|
-
focusTrap.activate();
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function onTab(event: KeyboardEvent): void {
|
|
74
|
-
if (event.key !== 'Tab') {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const element = findAncestor(
|
|
79
|
-
event.target as never,
|
|
80
|
-
`[${selector}]`,
|
|
81
|
-
) as HTMLElement;
|
|
82
|
-
|
|
83
|
-
if (!(focusTraps.get(element)?.active ?? false)) {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
event.preventDefault();
|
|
88
|
-
|
|
89
|
-
const elements = getFocusable(element);
|
|
90
|
-
const {length} = elements;
|
|
91
|
-
|
|
92
|
-
const index = elements.indexOf(event.target as HTMLElement);
|
|
93
|
-
|
|
94
|
-
if (index === -1 || length === 0) {
|
|
95
|
-
if (length === 0) {
|
|
96
|
-
element.focus();
|
|
97
|
-
} else {
|
|
98
|
-
(elements[0] as HTMLElement).focus();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const next = clamp(index + (event.shiftKey ? -1 : 1), 0, length - 1, true);
|
|
105
|
-
|
|
106
|
-
if (next !== index) {
|
|
107
|
-
(elements[next] as HTMLElement).focus();
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function removeFocusTraps(element: HTMLElement): void {
|
|
112
|
-
focusTraps.get(element)?.destroy();
|
|
113
|
-
focusTraps.delete(element);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const focusTraps = new WeakMap<HTMLElement, FocusTrap>();
|
|
117
|
-
const inactive = new Set<FocusTrap>();
|
|
118
|
-
const selector = 'oui-focus-trap';
|
|
119
|
-
|
|
120
|
-
attributable(selector, addFocusTrap, removeFocusTraps);
|
|
121
|
-
|
|
122
|
-
on(document, 'focusout', onFocusOut, {
|
|
123
|
-
capture: true,
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
on(document, 'keydown', onTab, {
|
|
127
|
-
capture: true,
|
|
128
|
-
passive: false,
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
on(document, 'keydown', onEscape, {
|
|
132
|
-
passive: false,
|
|
133
|
-
});
|
|
File without changes
|
|
File without changes
|