@y14e/disclosure 1.2.7 → 1.3.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/dist/index.cjs +581 -43
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +581 -43
- package/package.json +3 -1
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,520 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// node_modules/@y14e/attributes-utils/dist/index.js
|
|
4
|
+
var snapshots = /* @__PURE__ */ new WeakMap();
|
|
5
|
+
function restoreAttributes(elements) {
|
|
6
|
+
for (const element of elements) {
|
|
7
|
+
const snapshot = snapshots.get(element);
|
|
8
|
+
if (!snapshot) {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
for (const [attribute, value] of snapshot.entries()) {
|
|
12
|
+
value === null ? element.removeAttribute(attribute) : element.setAttribute(attribute, value);
|
|
13
|
+
}
|
|
14
|
+
snapshots.delete(element);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function saveAttributes(elements, attributes) {
|
|
18
|
+
elements.forEach((element) => {
|
|
19
|
+
let snapshot = snapshots.get(element);
|
|
20
|
+
if (!snapshot) {
|
|
21
|
+
snapshot = /* @__PURE__ */ new Map();
|
|
22
|
+
snapshots.set(element, snapshot);
|
|
23
|
+
}
|
|
24
|
+
attributes.forEach((attribute) => {
|
|
25
|
+
snapshot.set(attribute, element.getAttribute(attribute));
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// node_modules/@y14e/roving-tabindex/dist/index.js
|
|
31
|
+
var defaultParser = (value) => value.split(/\s+/);
|
|
32
|
+
var defaultSerializer = (tokens) => tokens.join(" ");
|
|
33
|
+
function addTokenToAttribute(element, attribute, token, options = {}) {
|
|
34
|
+
const {
|
|
35
|
+
caseInsensitive = false,
|
|
36
|
+
parse = defaultParser,
|
|
37
|
+
serialize = defaultSerializer
|
|
38
|
+
} = options;
|
|
39
|
+
const value = element.getAttribute(attribute)?.trim();
|
|
40
|
+
const tokens = value ? parse(value).filter(Boolean) : [];
|
|
41
|
+
if (caseInsensitive) {
|
|
42
|
+
const lower = token.toLowerCase();
|
|
43
|
+
if (tokens.some((token2) => token2.toLowerCase() === lower)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
tokens.push(token);
|
|
47
|
+
element.setAttribute(attribute, serialize(tokens));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const set = new Set(tokens);
|
|
51
|
+
set.add(token);
|
|
52
|
+
element.setAttribute(attribute, serialize([...set]));
|
|
53
|
+
}
|
|
54
|
+
var snapshots2 = /* @__PURE__ */ new WeakMap();
|
|
55
|
+
function restoreAttributes2(elements) {
|
|
56
|
+
for (const element of elements) {
|
|
57
|
+
const snapshot = snapshots2.get(element);
|
|
58
|
+
if (!snapshot) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
for (const [attribute, value] of snapshot.entries()) {
|
|
62
|
+
value === null ? element.removeAttribute(attribute) : element.setAttribute(attribute, value);
|
|
63
|
+
}
|
|
64
|
+
snapshots2.delete(element);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function saveAttributes2(elements, attributes) {
|
|
68
|
+
elements.forEach((element) => {
|
|
69
|
+
let snapshot = snapshots2.get(element);
|
|
70
|
+
if (!snapshot) {
|
|
71
|
+
snapshot = /* @__PURE__ */ new Map();
|
|
72
|
+
snapshots2.set(element, snapshot);
|
|
73
|
+
}
|
|
74
|
+
attributes.forEach((attribute) => {
|
|
75
|
+
snapshot.set(attribute, element.getAttribute(attribute));
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
var FOCUSABLE_SELECTOR = `:is(a[href], area[href], button, embed, iframe, input:not([type="hidden" i]), object, select, details > summary:first-of-type, textarea, [contenteditable]:not([contenteditable="false" i]), [controls], [tabindex]):not(:disabled, [hidden], [inert], [tabindex="-1"])`;
|
|
80
|
+
function getFocusables(container = document.body, options = {}) {
|
|
81
|
+
if (!(container instanceof Element)) {
|
|
82
|
+
console.warn("Invalid container element. Fallback: <body> element.");
|
|
83
|
+
container = document.body;
|
|
84
|
+
}
|
|
85
|
+
let { composed = false, filter, include } = options;
|
|
86
|
+
if (typeof composed !== "boolean") {
|
|
87
|
+
console.warn("Invalid composed option. Fallback: false.");
|
|
88
|
+
composed = false;
|
|
89
|
+
}
|
|
90
|
+
if (typeof filter !== "undefined" && typeof filter !== "function") {
|
|
91
|
+
console.warn(
|
|
92
|
+
"Invalid filter function. Fallback: no filter function (undefined)."
|
|
93
|
+
);
|
|
94
|
+
filter = void 0;
|
|
95
|
+
}
|
|
96
|
+
if (typeof include !== "undefined" && typeof include !== "function") {
|
|
97
|
+
console.warn(
|
|
98
|
+
"Invalid include function. Fallback: no include function (undefined)."
|
|
99
|
+
);
|
|
100
|
+
include = void 0;
|
|
101
|
+
}
|
|
102
|
+
const elements = [];
|
|
103
|
+
if (composed || include) {
|
|
104
|
+
let traverse2 = function(node) {
|
|
105
|
+
if (node instanceof Element) {
|
|
106
|
+
if (isFocusable(node) || include?.(node)) {
|
|
107
|
+
elements[elements.length] = node;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const children = getComposedChildren(node);
|
|
111
|
+
for (let i = 0, l = children.length; i < l; i++) {
|
|
112
|
+
const child = children[i];
|
|
113
|
+
if (!child) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
traverse2(child);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
traverse2(container);
|
|
120
|
+
} else {
|
|
121
|
+
const candidates = container.querySelectorAll(FOCUSABLE_SELECTOR);
|
|
122
|
+
for (let i = 0, l = candidates.length; i < l; i++) {
|
|
123
|
+
const candidate = candidates[i];
|
|
124
|
+
if (!(candidate instanceof Element)) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (isFocusable(candidate)) {
|
|
128
|
+
elements[elements.length] = candidate;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const unfiltered = normalizeRadioGroup(sortByTabIndex(elements));
|
|
133
|
+
return filter ? unfiltered.filter(filter) : unfiltered;
|
|
134
|
+
}
|
|
135
|
+
function isFocusable(element) {
|
|
136
|
+
if (!(element instanceof Element)) {
|
|
137
|
+
console.warn("Invalid element");
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
if (element.hasAttribute("hidden") || isInert(element)) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
if (getTabIndex(element) < 0) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
if (!element.matches(FOCUSABLE_SELECTOR)) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
if (isDisabledDeep(element)) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
if (!element.checkVisibility({
|
|
153
|
+
contentVisibilityAuto: true,
|
|
154
|
+
opacityProperty: true,
|
|
155
|
+
visibilityProperty: true
|
|
156
|
+
})) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
function isDisabledDeep(element) {
|
|
162
|
+
let current = element;
|
|
163
|
+
while (current) {
|
|
164
|
+
if (current instanceof ShadowRoot) {
|
|
165
|
+
if (current.mode !== "open") {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
current = current.host;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (!(current instanceof Element)) {
|
|
172
|
+
current = current.parentNode;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (current === element && isFormControl(current) && isDisabled(current)) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
if (isInert(current)) {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
if (isFormControl(element) && current.tagName === "FIELDSET" && isDisabled(current)) {
|
|
182
|
+
if (!current.querySelector(":scope > legend:first-of-type")?.contains(element)) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
current = current.parentNode;
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
function normalizeRadioGroup(elements) {
|
|
191
|
+
let map = null;
|
|
192
|
+
for (let i = 0, l = elements.length; i < l; i++) {
|
|
193
|
+
const element = elements[i];
|
|
194
|
+
if (!(element instanceof HTMLInputElement)) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (!isUngroupedRadio(element)) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (!map) {
|
|
201
|
+
map = /* @__PURE__ */ new Map();
|
|
202
|
+
}
|
|
203
|
+
const key = `${element.form?.id ?? "no-form"}::${element.name}`;
|
|
204
|
+
const group = map.get(key) ?? map.set(key, []).get(key);
|
|
205
|
+
if (group) {
|
|
206
|
+
group[group.length] = element;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (!map) {
|
|
210
|
+
return elements;
|
|
211
|
+
}
|
|
212
|
+
const placeholder = /* @__PURE__ */ new Set();
|
|
213
|
+
for (const group of map.values()) {
|
|
214
|
+
placeholder.add(group.find((radio) => radio.checked) ?? group[0]);
|
|
215
|
+
}
|
|
216
|
+
return elements.filter((element) => {
|
|
217
|
+
if (isUngroupedRadio(element)) {
|
|
218
|
+
return placeholder.has(element);
|
|
219
|
+
}
|
|
220
|
+
return true;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
function sortByTabIndex(elements) {
|
|
224
|
+
const ordered = [];
|
|
225
|
+
const natural = [];
|
|
226
|
+
for (let i = 0, l = elements.length; i < l; i++) {
|
|
227
|
+
const element = elements[i];
|
|
228
|
+
if (!element) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const target = getTabIndex(element) > 0 ? ordered : natural;
|
|
232
|
+
target[target.length] = element;
|
|
233
|
+
}
|
|
234
|
+
ordered.sort((a, b) => getTabIndex(a) - getTabIndex(b));
|
|
235
|
+
let count = 0;
|
|
236
|
+
const sorted = new Array(ordered.length + natural.length);
|
|
237
|
+
for (let i = 0, l = ordered.length; i < l; i++) {
|
|
238
|
+
sorted[count++] = ordered[i];
|
|
239
|
+
}
|
|
240
|
+
for (let i = 0, l = natural.length; i < l; i++) {
|
|
241
|
+
sorted[count++] = natural[i];
|
|
242
|
+
}
|
|
243
|
+
return sorted;
|
|
244
|
+
}
|
|
245
|
+
function getComposedChildren(node) {
|
|
246
|
+
if (node instanceof ShadowRoot) {
|
|
247
|
+
return getChildren(node);
|
|
248
|
+
}
|
|
249
|
+
if (!(node instanceof Element)) {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
if (node instanceof HTMLSlotElement) {
|
|
253
|
+
const assigned = node.assignedElements({ flatten: true });
|
|
254
|
+
if (assigned.length) {
|
|
255
|
+
return assigned;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (node instanceof HTMLElement && node.shadowRoot?.mode === "open") {
|
|
259
|
+
return getChildren(node.shadowRoot);
|
|
260
|
+
}
|
|
261
|
+
return getChildren(node);
|
|
262
|
+
}
|
|
263
|
+
function getChildren(node) {
|
|
264
|
+
const elements = [];
|
|
265
|
+
for (let child = node.firstElementChild; child; child = child.nextElementSibling) {
|
|
266
|
+
elements[elements.length] = child;
|
|
267
|
+
}
|
|
268
|
+
return elements;
|
|
269
|
+
}
|
|
270
|
+
function getTabIndex(element) {
|
|
271
|
+
return "tabIndex" in element ? Number(element.tabIndex) : 0;
|
|
272
|
+
}
|
|
273
|
+
function isDisabled(element) {
|
|
274
|
+
return "disabled" in element && !!element.disabled;
|
|
275
|
+
}
|
|
276
|
+
function isFormControl(element) {
|
|
277
|
+
const name = element.tagName;
|
|
278
|
+
return name === "BUTTON" || name === "INPUT" || name === "SELECT" || name === "TEXTAREA";
|
|
279
|
+
}
|
|
280
|
+
function isInert(element) {
|
|
281
|
+
return "inert" in element && !!element.inert;
|
|
282
|
+
}
|
|
283
|
+
function isUngroupedRadio(element) {
|
|
284
|
+
return element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
|
|
285
|
+
}
|
|
286
|
+
function createRovingTabIndex(container, options = {}) {
|
|
287
|
+
if (!(container instanceof Element)) {
|
|
288
|
+
console.warn("Invalid container element");
|
|
289
|
+
return () => {
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
let {
|
|
293
|
+
direction,
|
|
294
|
+
navigationOnly = false,
|
|
295
|
+
selector,
|
|
296
|
+
typeahead = false,
|
|
297
|
+
wrap = false
|
|
298
|
+
} = options;
|
|
299
|
+
if (typeof direction !== "undefined" && !["horizontal", "vertical"].includes(direction)) {
|
|
300
|
+
console.warn("Invalid direction option. Fallback: both (undefined).");
|
|
301
|
+
direction = void 0;
|
|
302
|
+
}
|
|
303
|
+
if (typeof navigationOnly !== "boolean") {
|
|
304
|
+
console.warn("Invalid navigationOnly option. Fallback: false.");
|
|
305
|
+
navigationOnly = false;
|
|
306
|
+
}
|
|
307
|
+
if (typeof selector !== "undefined" && typeof selector !== "string") {
|
|
308
|
+
console.warn(
|
|
309
|
+
"Invalid selector. Fallback: all focusable elements (undefined)."
|
|
310
|
+
);
|
|
311
|
+
selector = void 0;
|
|
312
|
+
}
|
|
313
|
+
if (typeof typeahead !== "boolean") {
|
|
314
|
+
console.warn("Invalid typeahead option. Fallback: false.");
|
|
315
|
+
typeahead = false;
|
|
316
|
+
}
|
|
317
|
+
if (typeof wrap !== "boolean") {
|
|
318
|
+
console.warn("Invalid wrap option. Fallback: false.");
|
|
319
|
+
wrap = false;
|
|
320
|
+
}
|
|
321
|
+
const roving = new RovingTabIndex(container, {
|
|
322
|
+
direction,
|
|
323
|
+
navigationOnly,
|
|
324
|
+
selector,
|
|
325
|
+
typeahead,
|
|
326
|
+
wrap
|
|
327
|
+
});
|
|
328
|
+
return () => roving.destroy();
|
|
329
|
+
}
|
|
330
|
+
var RovingTabIndex = class {
|
|
331
|
+
#container;
|
|
332
|
+
#options;
|
|
333
|
+
#focusables = /* @__PURE__ */ new Set();
|
|
334
|
+
#focusablesByFirstChar = /* @__PURE__ */ new Map();
|
|
335
|
+
#selectorFilter;
|
|
336
|
+
#controller = null;
|
|
337
|
+
#isDestroyed = false;
|
|
338
|
+
constructor(container, options = {}) {
|
|
339
|
+
this.#container = container;
|
|
340
|
+
this.#options = options;
|
|
341
|
+
this.#selectorFilter = this.#createSelectorFilter();
|
|
342
|
+
this.#initialize();
|
|
343
|
+
}
|
|
344
|
+
destroy() {
|
|
345
|
+
if (this.#isDestroyed) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
this.#isDestroyed = true;
|
|
349
|
+
this.#controller?.abort();
|
|
350
|
+
this.#controller = null;
|
|
351
|
+
restoreAttributes2([...this.#focusables]);
|
|
352
|
+
this.#focusables.clear();
|
|
353
|
+
this.#focusablesByFirstChar.clear();
|
|
354
|
+
this.#container.removeAttribute("data-roving-tabindex-initialized");
|
|
355
|
+
}
|
|
356
|
+
#initialize() {
|
|
357
|
+
this.#update(document.activeElement);
|
|
358
|
+
this.#controller = new AbortController();
|
|
359
|
+
document.addEventListener("keydown", this.#onKeyDown, {
|
|
360
|
+
capture: true,
|
|
361
|
+
signal: this.#controller.signal
|
|
362
|
+
});
|
|
363
|
+
this.#container.setAttribute("data-roving-tabindex-initialized", "");
|
|
364
|
+
}
|
|
365
|
+
#onKeyDown = (event) => {
|
|
366
|
+
if (!event.composedPath().includes(this.#container)) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const { key, altKey, ctrlKey, metaKey } = event;
|
|
370
|
+
if (altKey || ctrlKey || metaKey) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const { direction, typeahead, wrap } = this.#options;
|
|
374
|
+
const isBoth = !direction;
|
|
375
|
+
const isHorizontal = direction === "horizontal";
|
|
376
|
+
if (![
|
|
377
|
+
"End",
|
|
378
|
+
"Home",
|
|
379
|
+
...isBoth ? ["ArrowLeft", "ArrowUp"] : [`Arrow${isHorizontal ? "Left" : "Up"}`],
|
|
380
|
+
...isBoth ? ["ArrowRight", "ArrowDown"] : [`Arrow${isHorizontal ? "Right" : "Down"}`]
|
|
381
|
+
].includes(key)) {
|
|
382
|
+
if (!typeahead || !/^\S$/i.test(key) || !this.#focusablesByFirstChar.has(key.toUpperCase())) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const active = getActiveElement();
|
|
387
|
+
if (!(active instanceof HTMLElement)) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const current = this.#getFocusables();
|
|
391
|
+
if (!current.includes(active)) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
event.preventDefault();
|
|
395
|
+
event.stopPropagation();
|
|
396
|
+
const currentIndex = current.indexOf(active);
|
|
397
|
+
let rawIndex;
|
|
398
|
+
let newIndex = currentIndex;
|
|
399
|
+
let target = current;
|
|
400
|
+
switch (key) {
|
|
401
|
+
case "End":
|
|
402
|
+
newIndex = -1;
|
|
403
|
+
break;
|
|
404
|
+
case "Home":
|
|
405
|
+
newIndex = 0;
|
|
406
|
+
break;
|
|
407
|
+
case "ArrowLeft":
|
|
408
|
+
case "ArrowUp":
|
|
409
|
+
rawIndex = currentIndex - 1;
|
|
410
|
+
newIndex = wrap ? rawIndex : Math.max(rawIndex, 0);
|
|
411
|
+
break;
|
|
412
|
+
case "ArrowRight":
|
|
413
|
+
case "ArrowDown":
|
|
414
|
+
rawIndex = currentIndex + 1;
|
|
415
|
+
newIndex = wrap ? rawIndex % current.length : Math.min(rawIndex, current.length - 1);
|
|
416
|
+
break;
|
|
417
|
+
default: {
|
|
418
|
+
target = this.#focusablesByFirstChar.get(key.toUpperCase()) ?? [];
|
|
419
|
+
const foundIndex = target.findIndex(
|
|
420
|
+
(focusable2) => current.indexOf(focusable2) > currentIndex
|
|
421
|
+
);
|
|
422
|
+
newIndex = foundIndex >= 0 ? foundIndex : 0;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const focusable = target.at(newIndex);
|
|
426
|
+
if (!focusable) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
this.#update(focusable);
|
|
430
|
+
focusElement(focusable);
|
|
431
|
+
};
|
|
432
|
+
#update(active) {
|
|
433
|
+
const current = /* @__PURE__ */ new Set([
|
|
434
|
+
...this.#getFocusables(),
|
|
435
|
+
...getFocusables(this.#container, {
|
|
436
|
+
composed: true,
|
|
437
|
+
filter: this.#selectorFilter
|
|
438
|
+
})
|
|
439
|
+
]);
|
|
440
|
+
for (const focusable of this.#focusables) {
|
|
441
|
+
if (current.has(focusable)) {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
focusable.isConnected && restoreAttributes2([focusable]);
|
|
445
|
+
this.#focusables.delete(focusable);
|
|
446
|
+
this.#focusablesByFirstChar.forEach((focusables) => {
|
|
447
|
+
const index = focusables.indexOf(focusable);
|
|
448
|
+
index >= 0 && focusables.splice(index, 1);
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
const { navigationOnly } = this.#options;
|
|
452
|
+
for (const focusable of current) {
|
|
453
|
+
if (this.#focusables.has(focusable)) {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
this.#focusables.add(focusable);
|
|
457
|
+
if (!navigationOnly) {
|
|
458
|
+
saveAttributes2([focusable], ["tabindex"]);
|
|
459
|
+
focusable.setAttribute("tabindex", "-1");
|
|
460
|
+
}
|
|
461
|
+
if (!this.#options.typeahead) {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
const value = focusable.ariaKeyShortcuts?.trim();
|
|
465
|
+
const keys = new Set(
|
|
466
|
+
value ? value.split(/\s+/).filter((key) => /^\S$/i.test(key)).map((key) => key.toUpperCase()) : []
|
|
467
|
+
);
|
|
468
|
+
const char = focusable.textContent?.trim()?.at(0)?.toUpperCase();
|
|
469
|
+
if (char) {
|
|
470
|
+
keys.add(char);
|
|
471
|
+
saveAttributes2([focusable], ["aria-keyshortcuts"]);
|
|
472
|
+
addTokenToAttribute(focusable, "aria-keyshortcuts", char, {
|
|
473
|
+
caseInsensitive: true
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
keys.forEach((key) => {
|
|
477
|
+
const focusables = this.#focusablesByFirstChar.get(key) ?? [];
|
|
478
|
+
focusables.push(focusable);
|
|
479
|
+
this.#focusablesByFirstChar.set(key, focusables);
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
if (navigationOnly) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (active && this.#focusables.has(active)) {
|
|
486
|
+
this.#focusables.forEach((focusable) => {
|
|
487
|
+
focusable.setAttribute("tabindex", focusable === active ? "0" : "-1");
|
|
488
|
+
});
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
[...this.#focusables].forEach((focusable, i) => {
|
|
492
|
+
focusable.setAttribute("tabindex", i ? "-1" : "0");
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
#createSelectorFilter() {
|
|
496
|
+
const { selector } = this.#options;
|
|
497
|
+
return (element) => !selector || [...this.#container.querySelectorAll(selector)].includes(element);
|
|
498
|
+
}
|
|
499
|
+
#getFocusables() {
|
|
500
|
+
return getFocusables(this.#container, {
|
|
501
|
+
composed: true,
|
|
502
|
+
filter: this.#selectorFilter,
|
|
503
|
+
include: (element) => this.#focusables.has(element)
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
function focusElement(element) {
|
|
508
|
+
"focus" in element && typeof element.focus === "function" && element.focus();
|
|
509
|
+
}
|
|
510
|
+
function getActiveElement() {
|
|
511
|
+
let current = document.activeElement;
|
|
512
|
+
while (current?.shadowRoot?.activeElement) {
|
|
513
|
+
current = current.shadowRoot.activeElement;
|
|
514
|
+
}
|
|
515
|
+
return current;
|
|
516
|
+
}
|
|
517
|
+
|
|
3
518
|
// src/index.ts
|
|
4
519
|
var Disclosure = class _Disclosure {
|
|
5
520
|
static defaults = {};
|
|
@@ -18,6 +533,7 @@ var Disclosure = class _Disclosure {
|
|
|
18
533
|
#eventController = null;
|
|
19
534
|
#animationController = null;
|
|
20
535
|
#observers = [];
|
|
536
|
+
#cleanupRovingTabIndex = null;
|
|
21
537
|
#isDestroyed = false;
|
|
22
538
|
constructor(root, options = {}) {
|
|
23
539
|
if (!(root instanceof HTMLElement)) {
|
|
@@ -70,6 +586,12 @@ var Disclosure = class _Disclosure {
|
|
|
70
586
|
this.#bindings.set(summary, binding);
|
|
71
587
|
this.#bindings.set(content, binding);
|
|
72
588
|
});
|
|
589
|
+
this.#cleanupRovingTabIndex = createRovingTabIndex(this.#rootElement, {
|
|
590
|
+
direction: "vertical",
|
|
591
|
+
navigationOnly: true,
|
|
592
|
+
selector: `summary${NOT_NESTED}`,
|
|
593
|
+
wrap: true
|
|
594
|
+
});
|
|
73
595
|
this.#initialize();
|
|
74
596
|
}
|
|
75
597
|
open(details) {
|
|
@@ -110,11 +632,14 @@ var Disclosure = class _Disclosure {
|
|
|
110
632
|
});
|
|
111
633
|
this.#animationController?.abort();
|
|
112
634
|
this.#animationController = null;
|
|
635
|
+
this.#cleanupRovingTabIndex?.();
|
|
636
|
+
this.#cleanupRovingTabIndex = null;
|
|
113
637
|
this.#detailsElements.forEach((details) => {
|
|
114
638
|
details.removeAttribute("data-disclosure-name");
|
|
115
639
|
details.removeAttribute("data-disclosure-open");
|
|
116
640
|
});
|
|
117
641
|
this.#detailsElements.length = 0;
|
|
642
|
+
restoreAttributes(this.#summaryElements);
|
|
118
643
|
this.#summaryElements.length = 0;
|
|
119
644
|
this.#contentElements.length = 0;
|
|
120
645
|
this.#rootElement.removeAttribute("data-disclosure-initialized");
|
|
@@ -135,13 +660,13 @@ var Disclosure = class _Disclosure {
|
|
|
135
660
|
if (!summary) {
|
|
136
661
|
return;
|
|
137
662
|
}
|
|
138
|
-
if (!
|
|
663
|
+
if (!isFocusable2(summary)) {
|
|
664
|
+
saveAttributes([summary], ["aria-disabled", "style", "tabindex"]);
|
|
139
665
|
summary.setAttribute("aria-disabled", "true");
|
|
140
666
|
summary.setAttribute("tabindex", "-1");
|
|
141
667
|
summary.style.setProperty("pointer-events", "none");
|
|
142
668
|
}
|
|
143
669
|
summary.addEventListener("click", this.#onSummaryClick, { signal });
|
|
144
|
-
summary.addEventListener("keydown", this.#onSummaryKeyDown, { signal });
|
|
145
670
|
});
|
|
146
671
|
this.#rootElement.setAttribute("data-disclosure-initialized", "");
|
|
147
672
|
}
|
|
@@ -158,38 +683,6 @@ var Disclosure = class _Disclosure {
|
|
|
158
683
|
const { details } = binding;
|
|
159
684
|
this.#toggle(details, !details.hasAttribute("data-disclosure-open"));
|
|
160
685
|
};
|
|
161
|
-
#onSummaryKeyDown = (event) => {
|
|
162
|
-
const { key, altKey, ctrlKey, metaKey, shiftKey } = event;
|
|
163
|
-
if (altKey || ctrlKey || metaKey || shiftKey) {
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
if (!["End", "Home", "ArrowUp", "ArrowDown"].includes(key)) {
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
const focusables = this.#summaryElements.filter(isFocusable);
|
|
170
|
-
const active = getActiveElement();
|
|
171
|
-
if (!(active instanceof HTMLElement)) {
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
event.preventDefault();
|
|
175
|
-
const currentIndex = focusables.indexOf(active);
|
|
176
|
-
let newIndex = currentIndex;
|
|
177
|
-
switch (key) {
|
|
178
|
-
case "End":
|
|
179
|
-
newIndex = -1;
|
|
180
|
-
break;
|
|
181
|
-
case "Home":
|
|
182
|
-
newIndex = 0;
|
|
183
|
-
break;
|
|
184
|
-
case "ArrowUp":
|
|
185
|
-
newIndex = currentIndex - 1;
|
|
186
|
-
break;
|
|
187
|
-
case "ArrowDown":
|
|
188
|
-
newIndex = (currentIndex + 1) % focusables.length;
|
|
189
|
-
break;
|
|
190
|
-
}
|
|
191
|
-
focusables.at(newIndex)?.focus();
|
|
192
|
-
};
|
|
193
686
|
#toggle(details, isOpen) {
|
|
194
687
|
if (details.hasAttribute("data-disclosure-open") === isOpen) {
|
|
195
688
|
return;
|
|
@@ -278,14 +771,7 @@ var Disclosure = class _Disclosure {
|
|
|
278
771
|
function createBinding(details, summary, content) {
|
|
279
772
|
return { details, summary, content, timer: void 0, animation: null };
|
|
280
773
|
}
|
|
281
|
-
function
|
|
282
|
-
let current = document.activeElement;
|
|
283
|
-
while (current?.shadowRoot?.activeElement) {
|
|
284
|
-
current = current.shadowRoot.activeElement;
|
|
285
|
-
}
|
|
286
|
-
return current;
|
|
287
|
-
}
|
|
288
|
-
function isFocusable(element) {
|
|
774
|
+
function isFocusable2(element) {
|
|
289
775
|
return element.tabIndex >= 0;
|
|
290
776
|
}
|
|
291
777
|
function waitAnimationFinish(animation) {
|
|
@@ -302,11 +788,63 @@ function waitAnimationFinish(animation) {
|
|
|
302
788
|
* WAI-ARIA compliant disclosure pattern implementation in TypeScript.
|
|
303
789
|
* Using the <details> and <summary> element.
|
|
304
790
|
*
|
|
305
|
-
* @version 1.
|
|
791
|
+
* @version 1.3.0
|
|
306
792
|
* @author Yusuke Kamiyamane
|
|
307
793
|
* @license MIT
|
|
308
794
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
309
795
|
* @see {@link https://github.com/y14e/disclosure}
|
|
310
796
|
*/
|
|
797
|
+
/*! Bundled license information:
|
|
798
|
+
|
|
799
|
+
@y14e/attributes-utils/dist/index.js:
|
|
800
|
+
(**
|
|
801
|
+
* Attributes Utils
|
|
802
|
+
*
|
|
803
|
+
* @version 1.0.5
|
|
804
|
+
* @author Yusuke Kamiyamane
|
|
805
|
+
* @license MIT
|
|
806
|
+
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
807
|
+
* @see {@link https://github.com/y14e/attributes-utils}
|
|
808
|
+
*)
|
|
809
|
+
|
|
810
|
+
@y14e/roving-tabindex/dist/index.js:
|
|
811
|
+
(**
|
|
812
|
+
* Roving Tabindex
|
|
813
|
+
* Lightweight roving tabindex utility with fully focus management.
|
|
814
|
+
* Designed for accessible menus, tabs, toolbars, and composite widgets.
|
|
815
|
+
*
|
|
816
|
+
* @version 1.3.0
|
|
817
|
+
* @author Yusuke Kamiyamane
|
|
818
|
+
* @license MIT
|
|
819
|
+
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
820
|
+
* @see {@link https://github.com/y14e/roving-tabindex}
|
|
821
|
+
*)
|
|
822
|
+
(*! Bundled license information:
|
|
823
|
+
|
|
824
|
+
@y14e/attributes-utils/dist/index.js:
|
|
825
|
+
(**
|
|
826
|
+
* Attributes Utils
|
|
827
|
+
*
|
|
828
|
+
* @version 1.0.5
|
|
829
|
+
* @author Yusuke Kamiyamane
|
|
830
|
+
* @license MIT
|
|
831
|
+
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
832
|
+
* @see {@link https://github.com/y14e/attributes-utils}
|
|
833
|
+
*)
|
|
834
|
+
|
|
835
|
+
power-focusable/dist/index.js:
|
|
836
|
+
(**
|
|
837
|
+
* Power Focusable
|
|
838
|
+
* High-precision focus management utility with full composed tree support.
|
|
839
|
+
* Handles complex focus rules including tabindex ordering, radio groups, inert.
|
|
840
|
+
*
|
|
841
|
+
* @version 4.1.8
|
|
842
|
+
* @author Yusuke Kamiyamane
|
|
843
|
+
* @license MIT
|
|
844
|
+
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
845
|
+
* @see {@link https://github.com/y14e/power-focusable}
|
|
846
|
+
*)
|
|
847
|
+
*)
|
|
848
|
+
*/
|
|
311
849
|
|
|
312
850
|
module.exports = Disclosure;
|
package/dist/index.d.cts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,3 +1,518 @@
|
|
|
1
|
+
// node_modules/@y14e/attributes-utils/dist/index.js
|
|
2
|
+
var snapshots = /* @__PURE__ */ new WeakMap();
|
|
3
|
+
function restoreAttributes(elements) {
|
|
4
|
+
for (const element of elements) {
|
|
5
|
+
const snapshot = snapshots.get(element);
|
|
6
|
+
if (!snapshot) {
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
for (const [attribute, value] of snapshot.entries()) {
|
|
10
|
+
value === null ? element.removeAttribute(attribute) : element.setAttribute(attribute, value);
|
|
11
|
+
}
|
|
12
|
+
snapshots.delete(element);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function saveAttributes(elements, attributes) {
|
|
16
|
+
elements.forEach((element) => {
|
|
17
|
+
let snapshot = snapshots.get(element);
|
|
18
|
+
if (!snapshot) {
|
|
19
|
+
snapshot = /* @__PURE__ */ new Map();
|
|
20
|
+
snapshots.set(element, snapshot);
|
|
21
|
+
}
|
|
22
|
+
attributes.forEach((attribute) => {
|
|
23
|
+
snapshot.set(attribute, element.getAttribute(attribute));
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// node_modules/@y14e/roving-tabindex/dist/index.js
|
|
29
|
+
var defaultParser = (value) => value.split(/\s+/);
|
|
30
|
+
var defaultSerializer = (tokens) => tokens.join(" ");
|
|
31
|
+
function addTokenToAttribute(element, attribute, token, options = {}) {
|
|
32
|
+
const {
|
|
33
|
+
caseInsensitive = false,
|
|
34
|
+
parse = defaultParser,
|
|
35
|
+
serialize = defaultSerializer
|
|
36
|
+
} = options;
|
|
37
|
+
const value = element.getAttribute(attribute)?.trim();
|
|
38
|
+
const tokens = value ? parse(value).filter(Boolean) : [];
|
|
39
|
+
if (caseInsensitive) {
|
|
40
|
+
const lower = token.toLowerCase();
|
|
41
|
+
if (tokens.some((token2) => token2.toLowerCase() === lower)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
tokens.push(token);
|
|
45
|
+
element.setAttribute(attribute, serialize(tokens));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const set = new Set(tokens);
|
|
49
|
+
set.add(token);
|
|
50
|
+
element.setAttribute(attribute, serialize([...set]));
|
|
51
|
+
}
|
|
52
|
+
var snapshots2 = /* @__PURE__ */ new WeakMap();
|
|
53
|
+
function restoreAttributes2(elements) {
|
|
54
|
+
for (const element of elements) {
|
|
55
|
+
const snapshot = snapshots2.get(element);
|
|
56
|
+
if (!snapshot) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
for (const [attribute, value] of snapshot.entries()) {
|
|
60
|
+
value === null ? element.removeAttribute(attribute) : element.setAttribute(attribute, value);
|
|
61
|
+
}
|
|
62
|
+
snapshots2.delete(element);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function saveAttributes2(elements, attributes) {
|
|
66
|
+
elements.forEach((element) => {
|
|
67
|
+
let snapshot = snapshots2.get(element);
|
|
68
|
+
if (!snapshot) {
|
|
69
|
+
snapshot = /* @__PURE__ */ new Map();
|
|
70
|
+
snapshots2.set(element, snapshot);
|
|
71
|
+
}
|
|
72
|
+
attributes.forEach((attribute) => {
|
|
73
|
+
snapshot.set(attribute, element.getAttribute(attribute));
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
var FOCUSABLE_SELECTOR = `:is(a[href], area[href], button, embed, iframe, input:not([type="hidden" i]), object, select, details > summary:first-of-type, textarea, [contenteditable]:not([contenteditable="false" i]), [controls], [tabindex]):not(:disabled, [hidden], [inert], [tabindex="-1"])`;
|
|
78
|
+
function getFocusables(container = document.body, options = {}) {
|
|
79
|
+
if (!(container instanceof Element)) {
|
|
80
|
+
console.warn("Invalid container element. Fallback: <body> element.");
|
|
81
|
+
container = document.body;
|
|
82
|
+
}
|
|
83
|
+
let { composed = false, filter, include } = options;
|
|
84
|
+
if (typeof composed !== "boolean") {
|
|
85
|
+
console.warn("Invalid composed option. Fallback: false.");
|
|
86
|
+
composed = false;
|
|
87
|
+
}
|
|
88
|
+
if (typeof filter !== "undefined" && typeof filter !== "function") {
|
|
89
|
+
console.warn(
|
|
90
|
+
"Invalid filter function. Fallback: no filter function (undefined)."
|
|
91
|
+
);
|
|
92
|
+
filter = void 0;
|
|
93
|
+
}
|
|
94
|
+
if (typeof include !== "undefined" && typeof include !== "function") {
|
|
95
|
+
console.warn(
|
|
96
|
+
"Invalid include function. Fallback: no include function (undefined)."
|
|
97
|
+
);
|
|
98
|
+
include = void 0;
|
|
99
|
+
}
|
|
100
|
+
const elements = [];
|
|
101
|
+
if (composed || include) {
|
|
102
|
+
let traverse2 = function(node) {
|
|
103
|
+
if (node instanceof Element) {
|
|
104
|
+
if (isFocusable(node) || include?.(node)) {
|
|
105
|
+
elements[elements.length] = node;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const children = getComposedChildren(node);
|
|
109
|
+
for (let i = 0, l = children.length; i < l; i++) {
|
|
110
|
+
const child = children[i];
|
|
111
|
+
if (!child) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
traverse2(child);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
traverse2(container);
|
|
118
|
+
} else {
|
|
119
|
+
const candidates = container.querySelectorAll(FOCUSABLE_SELECTOR);
|
|
120
|
+
for (let i = 0, l = candidates.length; i < l; i++) {
|
|
121
|
+
const candidate = candidates[i];
|
|
122
|
+
if (!(candidate instanceof Element)) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (isFocusable(candidate)) {
|
|
126
|
+
elements[elements.length] = candidate;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const unfiltered = normalizeRadioGroup(sortByTabIndex(elements));
|
|
131
|
+
return filter ? unfiltered.filter(filter) : unfiltered;
|
|
132
|
+
}
|
|
133
|
+
function isFocusable(element) {
|
|
134
|
+
if (!(element instanceof Element)) {
|
|
135
|
+
console.warn("Invalid element");
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
if (element.hasAttribute("hidden") || isInert(element)) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
if (getTabIndex(element) < 0) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
if (!element.matches(FOCUSABLE_SELECTOR)) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
if (isDisabledDeep(element)) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
if (!element.checkVisibility({
|
|
151
|
+
contentVisibilityAuto: true,
|
|
152
|
+
opacityProperty: true,
|
|
153
|
+
visibilityProperty: true
|
|
154
|
+
})) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
function isDisabledDeep(element) {
|
|
160
|
+
let current = element;
|
|
161
|
+
while (current) {
|
|
162
|
+
if (current instanceof ShadowRoot) {
|
|
163
|
+
if (current.mode !== "open") {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
current = current.host;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (!(current instanceof Element)) {
|
|
170
|
+
current = current.parentNode;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (current === element && isFormControl(current) && isDisabled(current)) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
if (isInert(current)) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
if (isFormControl(element) && current.tagName === "FIELDSET" && isDisabled(current)) {
|
|
180
|
+
if (!current.querySelector(":scope > legend:first-of-type")?.contains(element)) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
current = current.parentNode;
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
function normalizeRadioGroup(elements) {
|
|
189
|
+
let map = null;
|
|
190
|
+
for (let i = 0, l = elements.length; i < l; i++) {
|
|
191
|
+
const element = elements[i];
|
|
192
|
+
if (!(element instanceof HTMLInputElement)) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (!isUngroupedRadio(element)) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (!map) {
|
|
199
|
+
map = /* @__PURE__ */ new Map();
|
|
200
|
+
}
|
|
201
|
+
const key = `${element.form?.id ?? "no-form"}::${element.name}`;
|
|
202
|
+
const group = map.get(key) ?? map.set(key, []).get(key);
|
|
203
|
+
if (group) {
|
|
204
|
+
group[group.length] = element;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (!map) {
|
|
208
|
+
return elements;
|
|
209
|
+
}
|
|
210
|
+
const placeholder = /* @__PURE__ */ new Set();
|
|
211
|
+
for (const group of map.values()) {
|
|
212
|
+
placeholder.add(group.find((radio) => radio.checked) ?? group[0]);
|
|
213
|
+
}
|
|
214
|
+
return elements.filter((element) => {
|
|
215
|
+
if (isUngroupedRadio(element)) {
|
|
216
|
+
return placeholder.has(element);
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function sortByTabIndex(elements) {
|
|
222
|
+
const ordered = [];
|
|
223
|
+
const natural = [];
|
|
224
|
+
for (let i = 0, l = elements.length; i < l; i++) {
|
|
225
|
+
const element = elements[i];
|
|
226
|
+
if (!element) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const target = getTabIndex(element) > 0 ? ordered : natural;
|
|
230
|
+
target[target.length] = element;
|
|
231
|
+
}
|
|
232
|
+
ordered.sort((a, b) => getTabIndex(a) - getTabIndex(b));
|
|
233
|
+
let count = 0;
|
|
234
|
+
const sorted = new Array(ordered.length + natural.length);
|
|
235
|
+
for (let i = 0, l = ordered.length; i < l; i++) {
|
|
236
|
+
sorted[count++] = ordered[i];
|
|
237
|
+
}
|
|
238
|
+
for (let i = 0, l = natural.length; i < l; i++) {
|
|
239
|
+
sorted[count++] = natural[i];
|
|
240
|
+
}
|
|
241
|
+
return sorted;
|
|
242
|
+
}
|
|
243
|
+
function getComposedChildren(node) {
|
|
244
|
+
if (node instanceof ShadowRoot) {
|
|
245
|
+
return getChildren(node);
|
|
246
|
+
}
|
|
247
|
+
if (!(node instanceof Element)) {
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
if (node instanceof HTMLSlotElement) {
|
|
251
|
+
const assigned = node.assignedElements({ flatten: true });
|
|
252
|
+
if (assigned.length) {
|
|
253
|
+
return assigned;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (node instanceof HTMLElement && node.shadowRoot?.mode === "open") {
|
|
257
|
+
return getChildren(node.shadowRoot);
|
|
258
|
+
}
|
|
259
|
+
return getChildren(node);
|
|
260
|
+
}
|
|
261
|
+
function getChildren(node) {
|
|
262
|
+
const elements = [];
|
|
263
|
+
for (let child = node.firstElementChild; child; child = child.nextElementSibling) {
|
|
264
|
+
elements[elements.length] = child;
|
|
265
|
+
}
|
|
266
|
+
return elements;
|
|
267
|
+
}
|
|
268
|
+
function getTabIndex(element) {
|
|
269
|
+
return "tabIndex" in element ? Number(element.tabIndex) : 0;
|
|
270
|
+
}
|
|
271
|
+
function isDisabled(element) {
|
|
272
|
+
return "disabled" in element && !!element.disabled;
|
|
273
|
+
}
|
|
274
|
+
function isFormControl(element) {
|
|
275
|
+
const name = element.tagName;
|
|
276
|
+
return name === "BUTTON" || name === "INPUT" || name === "SELECT" || name === "TEXTAREA";
|
|
277
|
+
}
|
|
278
|
+
function isInert(element) {
|
|
279
|
+
return "inert" in element && !!element.inert;
|
|
280
|
+
}
|
|
281
|
+
function isUngroupedRadio(element) {
|
|
282
|
+
return element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
|
|
283
|
+
}
|
|
284
|
+
function createRovingTabIndex(container, options = {}) {
|
|
285
|
+
if (!(container instanceof Element)) {
|
|
286
|
+
console.warn("Invalid container element");
|
|
287
|
+
return () => {
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
let {
|
|
291
|
+
direction,
|
|
292
|
+
navigationOnly = false,
|
|
293
|
+
selector,
|
|
294
|
+
typeahead = false,
|
|
295
|
+
wrap = false
|
|
296
|
+
} = options;
|
|
297
|
+
if (typeof direction !== "undefined" && !["horizontal", "vertical"].includes(direction)) {
|
|
298
|
+
console.warn("Invalid direction option. Fallback: both (undefined).");
|
|
299
|
+
direction = void 0;
|
|
300
|
+
}
|
|
301
|
+
if (typeof navigationOnly !== "boolean") {
|
|
302
|
+
console.warn("Invalid navigationOnly option. Fallback: false.");
|
|
303
|
+
navigationOnly = false;
|
|
304
|
+
}
|
|
305
|
+
if (typeof selector !== "undefined" && typeof selector !== "string") {
|
|
306
|
+
console.warn(
|
|
307
|
+
"Invalid selector. Fallback: all focusable elements (undefined)."
|
|
308
|
+
);
|
|
309
|
+
selector = void 0;
|
|
310
|
+
}
|
|
311
|
+
if (typeof typeahead !== "boolean") {
|
|
312
|
+
console.warn("Invalid typeahead option. Fallback: false.");
|
|
313
|
+
typeahead = false;
|
|
314
|
+
}
|
|
315
|
+
if (typeof wrap !== "boolean") {
|
|
316
|
+
console.warn("Invalid wrap option. Fallback: false.");
|
|
317
|
+
wrap = false;
|
|
318
|
+
}
|
|
319
|
+
const roving = new RovingTabIndex(container, {
|
|
320
|
+
direction,
|
|
321
|
+
navigationOnly,
|
|
322
|
+
selector,
|
|
323
|
+
typeahead,
|
|
324
|
+
wrap
|
|
325
|
+
});
|
|
326
|
+
return () => roving.destroy();
|
|
327
|
+
}
|
|
328
|
+
var RovingTabIndex = class {
|
|
329
|
+
#container;
|
|
330
|
+
#options;
|
|
331
|
+
#focusables = /* @__PURE__ */ new Set();
|
|
332
|
+
#focusablesByFirstChar = /* @__PURE__ */ new Map();
|
|
333
|
+
#selectorFilter;
|
|
334
|
+
#controller = null;
|
|
335
|
+
#isDestroyed = false;
|
|
336
|
+
constructor(container, options = {}) {
|
|
337
|
+
this.#container = container;
|
|
338
|
+
this.#options = options;
|
|
339
|
+
this.#selectorFilter = this.#createSelectorFilter();
|
|
340
|
+
this.#initialize();
|
|
341
|
+
}
|
|
342
|
+
destroy() {
|
|
343
|
+
if (this.#isDestroyed) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
this.#isDestroyed = true;
|
|
347
|
+
this.#controller?.abort();
|
|
348
|
+
this.#controller = null;
|
|
349
|
+
restoreAttributes2([...this.#focusables]);
|
|
350
|
+
this.#focusables.clear();
|
|
351
|
+
this.#focusablesByFirstChar.clear();
|
|
352
|
+
this.#container.removeAttribute("data-roving-tabindex-initialized");
|
|
353
|
+
}
|
|
354
|
+
#initialize() {
|
|
355
|
+
this.#update(document.activeElement);
|
|
356
|
+
this.#controller = new AbortController();
|
|
357
|
+
document.addEventListener("keydown", this.#onKeyDown, {
|
|
358
|
+
capture: true,
|
|
359
|
+
signal: this.#controller.signal
|
|
360
|
+
});
|
|
361
|
+
this.#container.setAttribute("data-roving-tabindex-initialized", "");
|
|
362
|
+
}
|
|
363
|
+
#onKeyDown = (event) => {
|
|
364
|
+
if (!event.composedPath().includes(this.#container)) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const { key, altKey, ctrlKey, metaKey } = event;
|
|
368
|
+
if (altKey || ctrlKey || metaKey) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const { direction, typeahead, wrap } = this.#options;
|
|
372
|
+
const isBoth = !direction;
|
|
373
|
+
const isHorizontal = direction === "horizontal";
|
|
374
|
+
if (![
|
|
375
|
+
"End",
|
|
376
|
+
"Home",
|
|
377
|
+
...isBoth ? ["ArrowLeft", "ArrowUp"] : [`Arrow${isHorizontal ? "Left" : "Up"}`],
|
|
378
|
+
...isBoth ? ["ArrowRight", "ArrowDown"] : [`Arrow${isHorizontal ? "Right" : "Down"}`]
|
|
379
|
+
].includes(key)) {
|
|
380
|
+
if (!typeahead || !/^\S$/i.test(key) || !this.#focusablesByFirstChar.has(key.toUpperCase())) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const active = getActiveElement();
|
|
385
|
+
if (!(active instanceof HTMLElement)) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const current = this.#getFocusables();
|
|
389
|
+
if (!current.includes(active)) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
event.preventDefault();
|
|
393
|
+
event.stopPropagation();
|
|
394
|
+
const currentIndex = current.indexOf(active);
|
|
395
|
+
let rawIndex;
|
|
396
|
+
let newIndex = currentIndex;
|
|
397
|
+
let target = current;
|
|
398
|
+
switch (key) {
|
|
399
|
+
case "End":
|
|
400
|
+
newIndex = -1;
|
|
401
|
+
break;
|
|
402
|
+
case "Home":
|
|
403
|
+
newIndex = 0;
|
|
404
|
+
break;
|
|
405
|
+
case "ArrowLeft":
|
|
406
|
+
case "ArrowUp":
|
|
407
|
+
rawIndex = currentIndex - 1;
|
|
408
|
+
newIndex = wrap ? rawIndex : Math.max(rawIndex, 0);
|
|
409
|
+
break;
|
|
410
|
+
case "ArrowRight":
|
|
411
|
+
case "ArrowDown":
|
|
412
|
+
rawIndex = currentIndex + 1;
|
|
413
|
+
newIndex = wrap ? rawIndex % current.length : Math.min(rawIndex, current.length - 1);
|
|
414
|
+
break;
|
|
415
|
+
default: {
|
|
416
|
+
target = this.#focusablesByFirstChar.get(key.toUpperCase()) ?? [];
|
|
417
|
+
const foundIndex = target.findIndex(
|
|
418
|
+
(focusable2) => current.indexOf(focusable2) > currentIndex
|
|
419
|
+
);
|
|
420
|
+
newIndex = foundIndex >= 0 ? foundIndex : 0;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const focusable = target.at(newIndex);
|
|
424
|
+
if (!focusable) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
this.#update(focusable);
|
|
428
|
+
focusElement(focusable);
|
|
429
|
+
};
|
|
430
|
+
#update(active) {
|
|
431
|
+
const current = /* @__PURE__ */ new Set([
|
|
432
|
+
...this.#getFocusables(),
|
|
433
|
+
...getFocusables(this.#container, {
|
|
434
|
+
composed: true,
|
|
435
|
+
filter: this.#selectorFilter
|
|
436
|
+
})
|
|
437
|
+
]);
|
|
438
|
+
for (const focusable of this.#focusables) {
|
|
439
|
+
if (current.has(focusable)) {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
focusable.isConnected && restoreAttributes2([focusable]);
|
|
443
|
+
this.#focusables.delete(focusable);
|
|
444
|
+
this.#focusablesByFirstChar.forEach((focusables) => {
|
|
445
|
+
const index = focusables.indexOf(focusable);
|
|
446
|
+
index >= 0 && focusables.splice(index, 1);
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
const { navigationOnly } = this.#options;
|
|
450
|
+
for (const focusable of current) {
|
|
451
|
+
if (this.#focusables.has(focusable)) {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
this.#focusables.add(focusable);
|
|
455
|
+
if (!navigationOnly) {
|
|
456
|
+
saveAttributes2([focusable], ["tabindex"]);
|
|
457
|
+
focusable.setAttribute("tabindex", "-1");
|
|
458
|
+
}
|
|
459
|
+
if (!this.#options.typeahead) {
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const value = focusable.ariaKeyShortcuts?.trim();
|
|
463
|
+
const keys = new Set(
|
|
464
|
+
value ? value.split(/\s+/).filter((key) => /^\S$/i.test(key)).map((key) => key.toUpperCase()) : []
|
|
465
|
+
);
|
|
466
|
+
const char = focusable.textContent?.trim()?.at(0)?.toUpperCase();
|
|
467
|
+
if (char) {
|
|
468
|
+
keys.add(char);
|
|
469
|
+
saveAttributes2([focusable], ["aria-keyshortcuts"]);
|
|
470
|
+
addTokenToAttribute(focusable, "aria-keyshortcuts", char, {
|
|
471
|
+
caseInsensitive: true
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
keys.forEach((key) => {
|
|
475
|
+
const focusables = this.#focusablesByFirstChar.get(key) ?? [];
|
|
476
|
+
focusables.push(focusable);
|
|
477
|
+
this.#focusablesByFirstChar.set(key, focusables);
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
if (navigationOnly) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (active && this.#focusables.has(active)) {
|
|
484
|
+
this.#focusables.forEach((focusable) => {
|
|
485
|
+
focusable.setAttribute("tabindex", focusable === active ? "0" : "-1");
|
|
486
|
+
});
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
[...this.#focusables].forEach((focusable, i) => {
|
|
490
|
+
focusable.setAttribute("tabindex", i ? "-1" : "0");
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
#createSelectorFilter() {
|
|
494
|
+
const { selector } = this.#options;
|
|
495
|
+
return (element) => !selector || [...this.#container.querySelectorAll(selector)].includes(element);
|
|
496
|
+
}
|
|
497
|
+
#getFocusables() {
|
|
498
|
+
return getFocusables(this.#container, {
|
|
499
|
+
composed: true,
|
|
500
|
+
filter: this.#selectorFilter,
|
|
501
|
+
include: (element) => this.#focusables.has(element)
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
function focusElement(element) {
|
|
506
|
+
"focus" in element && typeof element.focus === "function" && element.focus();
|
|
507
|
+
}
|
|
508
|
+
function getActiveElement() {
|
|
509
|
+
let current = document.activeElement;
|
|
510
|
+
while (current?.shadowRoot?.activeElement) {
|
|
511
|
+
current = current.shadowRoot.activeElement;
|
|
512
|
+
}
|
|
513
|
+
return current;
|
|
514
|
+
}
|
|
515
|
+
|
|
1
516
|
// src/index.ts
|
|
2
517
|
var Disclosure = class _Disclosure {
|
|
3
518
|
static defaults = {};
|
|
@@ -16,6 +531,7 @@ var Disclosure = class _Disclosure {
|
|
|
16
531
|
#eventController = null;
|
|
17
532
|
#animationController = null;
|
|
18
533
|
#observers = [];
|
|
534
|
+
#cleanupRovingTabIndex = null;
|
|
19
535
|
#isDestroyed = false;
|
|
20
536
|
constructor(root, options = {}) {
|
|
21
537
|
if (!(root instanceof HTMLElement)) {
|
|
@@ -68,6 +584,12 @@ var Disclosure = class _Disclosure {
|
|
|
68
584
|
this.#bindings.set(summary, binding);
|
|
69
585
|
this.#bindings.set(content, binding);
|
|
70
586
|
});
|
|
587
|
+
this.#cleanupRovingTabIndex = createRovingTabIndex(this.#rootElement, {
|
|
588
|
+
direction: "vertical",
|
|
589
|
+
navigationOnly: true,
|
|
590
|
+
selector: `summary${NOT_NESTED}`,
|
|
591
|
+
wrap: true
|
|
592
|
+
});
|
|
71
593
|
this.#initialize();
|
|
72
594
|
}
|
|
73
595
|
open(details) {
|
|
@@ -108,11 +630,14 @@ var Disclosure = class _Disclosure {
|
|
|
108
630
|
});
|
|
109
631
|
this.#animationController?.abort();
|
|
110
632
|
this.#animationController = null;
|
|
633
|
+
this.#cleanupRovingTabIndex?.();
|
|
634
|
+
this.#cleanupRovingTabIndex = null;
|
|
111
635
|
this.#detailsElements.forEach((details) => {
|
|
112
636
|
details.removeAttribute("data-disclosure-name");
|
|
113
637
|
details.removeAttribute("data-disclosure-open");
|
|
114
638
|
});
|
|
115
639
|
this.#detailsElements.length = 0;
|
|
640
|
+
restoreAttributes(this.#summaryElements);
|
|
116
641
|
this.#summaryElements.length = 0;
|
|
117
642
|
this.#contentElements.length = 0;
|
|
118
643
|
this.#rootElement.removeAttribute("data-disclosure-initialized");
|
|
@@ -133,13 +658,13 @@ var Disclosure = class _Disclosure {
|
|
|
133
658
|
if (!summary) {
|
|
134
659
|
return;
|
|
135
660
|
}
|
|
136
|
-
if (!
|
|
661
|
+
if (!isFocusable2(summary)) {
|
|
662
|
+
saveAttributes([summary], ["aria-disabled", "style", "tabindex"]);
|
|
137
663
|
summary.setAttribute("aria-disabled", "true");
|
|
138
664
|
summary.setAttribute("tabindex", "-1");
|
|
139
665
|
summary.style.setProperty("pointer-events", "none");
|
|
140
666
|
}
|
|
141
667
|
summary.addEventListener("click", this.#onSummaryClick, { signal });
|
|
142
|
-
summary.addEventListener("keydown", this.#onSummaryKeyDown, { signal });
|
|
143
668
|
});
|
|
144
669
|
this.#rootElement.setAttribute("data-disclosure-initialized", "");
|
|
145
670
|
}
|
|
@@ -156,38 +681,6 @@ var Disclosure = class _Disclosure {
|
|
|
156
681
|
const { details } = binding;
|
|
157
682
|
this.#toggle(details, !details.hasAttribute("data-disclosure-open"));
|
|
158
683
|
};
|
|
159
|
-
#onSummaryKeyDown = (event) => {
|
|
160
|
-
const { key, altKey, ctrlKey, metaKey, shiftKey } = event;
|
|
161
|
-
if (altKey || ctrlKey || metaKey || shiftKey) {
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
if (!["End", "Home", "ArrowUp", "ArrowDown"].includes(key)) {
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
const focusables = this.#summaryElements.filter(isFocusable);
|
|
168
|
-
const active = getActiveElement();
|
|
169
|
-
if (!(active instanceof HTMLElement)) {
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
event.preventDefault();
|
|
173
|
-
const currentIndex = focusables.indexOf(active);
|
|
174
|
-
let newIndex = currentIndex;
|
|
175
|
-
switch (key) {
|
|
176
|
-
case "End":
|
|
177
|
-
newIndex = -1;
|
|
178
|
-
break;
|
|
179
|
-
case "Home":
|
|
180
|
-
newIndex = 0;
|
|
181
|
-
break;
|
|
182
|
-
case "ArrowUp":
|
|
183
|
-
newIndex = currentIndex - 1;
|
|
184
|
-
break;
|
|
185
|
-
case "ArrowDown":
|
|
186
|
-
newIndex = (currentIndex + 1) % focusables.length;
|
|
187
|
-
break;
|
|
188
|
-
}
|
|
189
|
-
focusables.at(newIndex)?.focus();
|
|
190
|
-
};
|
|
191
684
|
#toggle(details, isOpen) {
|
|
192
685
|
if (details.hasAttribute("data-disclosure-open") === isOpen) {
|
|
193
686
|
return;
|
|
@@ -276,14 +769,7 @@ var Disclosure = class _Disclosure {
|
|
|
276
769
|
function createBinding(details, summary, content) {
|
|
277
770
|
return { details, summary, content, timer: void 0, animation: null };
|
|
278
771
|
}
|
|
279
|
-
function
|
|
280
|
-
let current = document.activeElement;
|
|
281
|
-
while (current?.shadowRoot?.activeElement) {
|
|
282
|
-
current = current.shadowRoot.activeElement;
|
|
283
|
-
}
|
|
284
|
-
return current;
|
|
285
|
-
}
|
|
286
|
-
function isFocusable(element) {
|
|
772
|
+
function isFocusable2(element) {
|
|
287
773
|
return element.tabIndex >= 0;
|
|
288
774
|
}
|
|
289
775
|
function waitAnimationFinish(animation) {
|
|
@@ -300,11 +786,63 @@ function waitAnimationFinish(animation) {
|
|
|
300
786
|
* WAI-ARIA compliant disclosure pattern implementation in TypeScript.
|
|
301
787
|
* Using the <details> and <summary> element.
|
|
302
788
|
*
|
|
303
|
-
* @version 1.
|
|
789
|
+
* @version 1.3.0
|
|
304
790
|
* @author Yusuke Kamiyamane
|
|
305
791
|
* @license MIT
|
|
306
792
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
307
793
|
* @see {@link https://github.com/y14e/disclosure}
|
|
308
794
|
*/
|
|
795
|
+
/*! Bundled license information:
|
|
796
|
+
|
|
797
|
+
@y14e/attributes-utils/dist/index.js:
|
|
798
|
+
(**
|
|
799
|
+
* Attributes Utils
|
|
800
|
+
*
|
|
801
|
+
* @version 1.0.5
|
|
802
|
+
* @author Yusuke Kamiyamane
|
|
803
|
+
* @license MIT
|
|
804
|
+
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
805
|
+
* @see {@link https://github.com/y14e/attributes-utils}
|
|
806
|
+
*)
|
|
807
|
+
|
|
808
|
+
@y14e/roving-tabindex/dist/index.js:
|
|
809
|
+
(**
|
|
810
|
+
* Roving Tabindex
|
|
811
|
+
* Lightweight roving tabindex utility with fully focus management.
|
|
812
|
+
* Designed for accessible menus, tabs, toolbars, and composite widgets.
|
|
813
|
+
*
|
|
814
|
+
* @version 1.3.0
|
|
815
|
+
* @author Yusuke Kamiyamane
|
|
816
|
+
* @license MIT
|
|
817
|
+
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
818
|
+
* @see {@link https://github.com/y14e/roving-tabindex}
|
|
819
|
+
*)
|
|
820
|
+
(*! Bundled license information:
|
|
821
|
+
|
|
822
|
+
@y14e/attributes-utils/dist/index.js:
|
|
823
|
+
(**
|
|
824
|
+
* Attributes Utils
|
|
825
|
+
*
|
|
826
|
+
* @version 1.0.5
|
|
827
|
+
* @author Yusuke Kamiyamane
|
|
828
|
+
* @license MIT
|
|
829
|
+
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
830
|
+
* @see {@link https://github.com/y14e/attributes-utils}
|
|
831
|
+
*)
|
|
832
|
+
|
|
833
|
+
power-focusable/dist/index.js:
|
|
834
|
+
(**
|
|
835
|
+
* Power Focusable
|
|
836
|
+
* High-precision focus management utility with full composed tree support.
|
|
837
|
+
* Handles complex focus rules including tabindex ordering, radio groups, inert.
|
|
838
|
+
*
|
|
839
|
+
* @version 4.1.8
|
|
840
|
+
* @author Yusuke Kamiyamane
|
|
841
|
+
* @license MIT
|
|
842
|
+
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
843
|
+
* @see {@link https://github.com/y14e/power-focusable}
|
|
844
|
+
*)
|
|
845
|
+
*)
|
|
846
|
+
*/
|
|
309
847
|
|
|
310
848
|
export { Disclosure as default };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@y14e/disclosure",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "WAI-ARIA compliant disclosure pattern implementation in TypeScript",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
},
|
|
44
44
|
"homepage": "https://github.com/y14e/disclosure#readme",
|
|
45
45
|
"devDependencies": {
|
|
46
|
+
"@y14e/attributes-utils": "^1.0.5",
|
|
47
|
+
"@y14e/roving-tabindex": "^1.3.0",
|
|
46
48
|
"bun-types": "latest",
|
|
47
49
|
"tsup": "^8.0.0",
|
|
48
50
|
"typescript": "^5.6.0",
|