@y14e/disclosure 1.2.8 → 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 +539 -43
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +539 -43
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -27,6 +27,494 @@ function saveAttributes(elements, attributes) {
|
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
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
|
+
|
|
30
518
|
// src/index.ts
|
|
31
519
|
var Disclosure = class _Disclosure {
|
|
32
520
|
static defaults = {};
|
|
@@ -45,6 +533,7 @@ var Disclosure = class _Disclosure {
|
|
|
45
533
|
#eventController = null;
|
|
46
534
|
#animationController = null;
|
|
47
535
|
#observers = [];
|
|
536
|
+
#cleanupRovingTabIndex = null;
|
|
48
537
|
#isDestroyed = false;
|
|
49
538
|
constructor(root, options = {}) {
|
|
50
539
|
if (!(root instanceof HTMLElement)) {
|
|
@@ -97,6 +586,12 @@ var Disclosure = class _Disclosure {
|
|
|
97
586
|
this.#bindings.set(summary, binding);
|
|
98
587
|
this.#bindings.set(content, binding);
|
|
99
588
|
});
|
|
589
|
+
this.#cleanupRovingTabIndex = createRovingTabIndex(this.#rootElement, {
|
|
590
|
+
direction: "vertical",
|
|
591
|
+
navigationOnly: true,
|
|
592
|
+
selector: `summary${NOT_NESTED}`,
|
|
593
|
+
wrap: true
|
|
594
|
+
});
|
|
100
595
|
this.#initialize();
|
|
101
596
|
}
|
|
102
597
|
open(details) {
|
|
@@ -137,6 +632,8 @@ var Disclosure = class _Disclosure {
|
|
|
137
632
|
});
|
|
138
633
|
this.#animationController?.abort();
|
|
139
634
|
this.#animationController = null;
|
|
635
|
+
this.#cleanupRovingTabIndex?.();
|
|
636
|
+
this.#cleanupRovingTabIndex = null;
|
|
140
637
|
this.#detailsElements.forEach((details) => {
|
|
141
638
|
details.removeAttribute("data-disclosure-name");
|
|
142
639
|
details.removeAttribute("data-disclosure-open");
|
|
@@ -163,14 +660,13 @@ var Disclosure = class _Disclosure {
|
|
|
163
660
|
if (!summary) {
|
|
164
661
|
return;
|
|
165
662
|
}
|
|
166
|
-
if (!
|
|
663
|
+
if (!isFocusable2(summary)) {
|
|
167
664
|
saveAttributes([summary], ["aria-disabled", "style", "tabindex"]);
|
|
168
665
|
summary.setAttribute("aria-disabled", "true");
|
|
169
666
|
summary.setAttribute("tabindex", "-1");
|
|
170
667
|
summary.style.setProperty("pointer-events", "none");
|
|
171
668
|
}
|
|
172
669
|
summary.addEventListener("click", this.#onSummaryClick, { signal });
|
|
173
|
-
summary.addEventListener("keydown", this.#onSummaryKeyDown, { signal });
|
|
174
670
|
});
|
|
175
671
|
this.#rootElement.setAttribute("data-disclosure-initialized", "");
|
|
176
672
|
}
|
|
@@ -187,38 +683,6 @@ var Disclosure = class _Disclosure {
|
|
|
187
683
|
const { details } = binding;
|
|
188
684
|
this.#toggle(details, !details.hasAttribute("data-disclosure-open"));
|
|
189
685
|
};
|
|
190
|
-
#onSummaryKeyDown = (event) => {
|
|
191
|
-
const { key, altKey, ctrlKey, metaKey, shiftKey } = event;
|
|
192
|
-
if (altKey || ctrlKey || metaKey || shiftKey) {
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
if (!["End", "Home", "ArrowUp", "ArrowDown"].includes(key)) {
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
const focusables = this.#summaryElements.filter(isFocusable);
|
|
199
|
-
const active = getActiveElement();
|
|
200
|
-
if (!(active instanceof HTMLElement)) {
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
event.preventDefault();
|
|
204
|
-
const currentIndex = focusables.indexOf(active);
|
|
205
|
-
let newIndex = currentIndex;
|
|
206
|
-
switch (key) {
|
|
207
|
-
case "End":
|
|
208
|
-
newIndex = -1;
|
|
209
|
-
break;
|
|
210
|
-
case "Home":
|
|
211
|
-
newIndex = 0;
|
|
212
|
-
break;
|
|
213
|
-
case "ArrowUp":
|
|
214
|
-
newIndex = currentIndex - 1;
|
|
215
|
-
break;
|
|
216
|
-
case "ArrowDown":
|
|
217
|
-
newIndex = (currentIndex + 1) % focusables.length;
|
|
218
|
-
break;
|
|
219
|
-
}
|
|
220
|
-
focusables.at(newIndex)?.focus();
|
|
221
|
-
};
|
|
222
686
|
#toggle(details, isOpen) {
|
|
223
687
|
if (details.hasAttribute("data-disclosure-open") === isOpen) {
|
|
224
688
|
return;
|
|
@@ -307,14 +771,7 @@ var Disclosure = class _Disclosure {
|
|
|
307
771
|
function createBinding(details, summary, content) {
|
|
308
772
|
return { details, summary, content, timer: void 0, animation: null };
|
|
309
773
|
}
|
|
310
|
-
function
|
|
311
|
-
let current = document.activeElement;
|
|
312
|
-
while (current?.shadowRoot?.activeElement) {
|
|
313
|
-
current = current.shadowRoot.activeElement;
|
|
314
|
-
}
|
|
315
|
-
return current;
|
|
316
|
-
}
|
|
317
|
-
function isFocusable(element) {
|
|
774
|
+
function isFocusable2(element) {
|
|
318
775
|
return element.tabIndex >= 0;
|
|
319
776
|
}
|
|
320
777
|
function waitAnimationFinish(animation) {
|
|
@@ -331,7 +788,7 @@ function waitAnimationFinish(animation) {
|
|
|
331
788
|
* WAI-ARIA compliant disclosure pattern implementation in TypeScript.
|
|
332
789
|
* Using the <details> and <summary> element.
|
|
333
790
|
*
|
|
334
|
-
* @version 1.
|
|
791
|
+
* @version 1.3.0
|
|
335
792
|
* @author Yusuke Kamiyamane
|
|
336
793
|
* @license MIT
|
|
337
794
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -349,6 +806,45 @@ function waitAnimationFinish(animation) {
|
|
|
349
806
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
350
807
|
* @see {@link https://github.com/y14e/attributes-utils}
|
|
351
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
|
+
*)
|
|
352
848
|
*/
|
|
353
849
|
|
|
354
850
|
module.exports = Disclosure;
|
package/dist/index.d.cts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -25,6 +25,494 @@ function saveAttributes(elements, attributes) {
|
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
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
|
+
|
|
28
516
|
// src/index.ts
|
|
29
517
|
var Disclosure = class _Disclosure {
|
|
30
518
|
static defaults = {};
|
|
@@ -43,6 +531,7 @@ var Disclosure = class _Disclosure {
|
|
|
43
531
|
#eventController = null;
|
|
44
532
|
#animationController = null;
|
|
45
533
|
#observers = [];
|
|
534
|
+
#cleanupRovingTabIndex = null;
|
|
46
535
|
#isDestroyed = false;
|
|
47
536
|
constructor(root, options = {}) {
|
|
48
537
|
if (!(root instanceof HTMLElement)) {
|
|
@@ -95,6 +584,12 @@ var Disclosure = class _Disclosure {
|
|
|
95
584
|
this.#bindings.set(summary, binding);
|
|
96
585
|
this.#bindings.set(content, binding);
|
|
97
586
|
});
|
|
587
|
+
this.#cleanupRovingTabIndex = createRovingTabIndex(this.#rootElement, {
|
|
588
|
+
direction: "vertical",
|
|
589
|
+
navigationOnly: true,
|
|
590
|
+
selector: `summary${NOT_NESTED}`,
|
|
591
|
+
wrap: true
|
|
592
|
+
});
|
|
98
593
|
this.#initialize();
|
|
99
594
|
}
|
|
100
595
|
open(details) {
|
|
@@ -135,6 +630,8 @@ var Disclosure = class _Disclosure {
|
|
|
135
630
|
});
|
|
136
631
|
this.#animationController?.abort();
|
|
137
632
|
this.#animationController = null;
|
|
633
|
+
this.#cleanupRovingTabIndex?.();
|
|
634
|
+
this.#cleanupRovingTabIndex = null;
|
|
138
635
|
this.#detailsElements.forEach((details) => {
|
|
139
636
|
details.removeAttribute("data-disclosure-name");
|
|
140
637
|
details.removeAttribute("data-disclosure-open");
|
|
@@ -161,14 +658,13 @@ var Disclosure = class _Disclosure {
|
|
|
161
658
|
if (!summary) {
|
|
162
659
|
return;
|
|
163
660
|
}
|
|
164
|
-
if (!
|
|
661
|
+
if (!isFocusable2(summary)) {
|
|
165
662
|
saveAttributes([summary], ["aria-disabled", "style", "tabindex"]);
|
|
166
663
|
summary.setAttribute("aria-disabled", "true");
|
|
167
664
|
summary.setAttribute("tabindex", "-1");
|
|
168
665
|
summary.style.setProperty("pointer-events", "none");
|
|
169
666
|
}
|
|
170
667
|
summary.addEventListener("click", this.#onSummaryClick, { signal });
|
|
171
|
-
summary.addEventListener("keydown", this.#onSummaryKeyDown, { signal });
|
|
172
668
|
});
|
|
173
669
|
this.#rootElement.setAttribute("data-disclosure-initialized", "");
|
|
174
670
|
}
|
|
@@ -185,38 +681,6 @@ var Disclosure = class _Disclosure {
|
|
|
185
681
|
const { details } = binding;
|
|
186
682
|
this.#toggle(details, !details.hasAttribute("data-disclosure-open"));
|
|
187
683
|
};
|
|
188
|
-
#onSummaryKeyDown = (event) => {
|
|
189
|
-
const { key, altKey, ctrlKey, metaKey, shiftKey } = event;
|
|
190
|
-
if (altKey || ctrlKey || metaKey || shiftKey) {
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
if (!["End", "Home", "ArrowUp", "ArrowDown"].includes(key)) {
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
const focusables = this.#summaryElements.filter(isFocusable);
|
|
197
|
-
const active = getActiveElement();
|
|
198
|
-
if (!(active instanceof HTMLElement)) {
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
event.preventDefault();
|
|
202
|
-
const currentIndex = focusables.indexOf(active);
|
|
203
|
-
let newIndex = currentIndex;
|
|
204
|
-
switch (key) {
|
|
205
|
-
case "End":
|
|
206
|
-
newIndex = -1;
|
|
207
|
-
break;
|
|
208
|
-
case "Home":
|
|
209
|
-
newIndex = 0;
|
|
210
|
-
break;
|
|
211
|
-
case "ArrowUp":
|
|
212
|
-
newIndex = currentIndex - 1;
|
|
213
|
-
break;
|
|
214
|
-
case "ArrowDown":
|
|
215
|
-
newIndex = (currentIndex + 1) % focusables.length;
|
|
216
|
-
break;
|
|
217
|
-
}
|
|
218
|
-
focusables.at(newIndex)?.focus();
|
|
219
|
-
};
|
|
220
684
|
#toggle(details, isOpen) {
|
|
221
685
|
if (details.hasAttribute("data-disclosure-open") === isOpen) {
|
|
222
686
|
return;
|
|
@@ -305,14 +769,7 @@ var Disclosure = class _Disclosure {
|
|
|
305
769
|
function createBinding(details, summary, content) {
|
|
306
770
|
return { details, summary, content, timer: void 0, animation: null };
|
|
307
771
|
}
|
|
308
|
-
function
|
|
309
|
-
let current = document.activeElement;
|
|
310
|
-
while (current?.shadowRoot?.activeElement) {
|
|
311
|
-
current = current.shadowRoot.activeElement;
|
|
312
|
-
}
|
|
313
|
-
return current;
|
|
314
|
-
}
|
|
315
|
-
function isFocusable(element) {
|
|
772
|
+
function isFocusable2(element) {
|
|
316
773
|
return element.tabIndex >= 0;
|
|
317
774
|
}
|
|
318
775
|
function waitAnimationFinish(animation) {
|
|
@@ -329,7 +786,7 @@ function waitAnimationFinish(animation) {
|
|
|
329
786
|
* WAI-ARIA compliant disclosure pattern implementation in TypeScript.
|
|
330
787
|
* Using the <details> and <summary> element.
|
|
331
788
|
*
|
|
332
|
-
* @version 1.
|
|
789
|
+
* @version 1.3.0
|
|
333
790
|
* @author Yusuke Kamiyamane
|
|
334
791
|
* @license MIT
|
|
335
792
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
@@ -347,6 +804,45 @@ function waitAnimationFinish(animation) {
|
|
|
347
804
|
* @copyright Copyright (c) Yusuke Kamiyamane
|
|
348
805
|
* @see {@link https://github.com/y14e/attributes-utils}
|
|
349
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
|
+
*)
|
|
350
846
|
*/
|
|
351
847
|
|
|
352
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",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"homepage": "https://github.com/y14e/disclosure#readme",
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@y14e/attributes-utils": "^1.0.5",
|
|
47
|
+
"@y14e/roving-tabindex": "^1.3.0",
|
|
47
48
|
"bun-types": "latest",
|
|
48
49
|
"tsup": "^8.0.0",
|
|
49
50
|
"typescript": "^5.6.0",
|