@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 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 (!isFocusable(summary)) {
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 getActiveElement() {
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.2.8
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
@@ -3,7 +3,7 @@
3
3
  * WAI-ARIA compliant disclosure pattern implementation in TypeScript.
4
4
  * Using the <details> and <summary> element.
5
5
  *
6
- * @version 1.2.8
6
+ * @version 1.3.0
7
7
  * @author Yusuke Kamiyamane
8
8
  * @license MIT
9
9
  * @copyright Copyright (c) Yusuke Kamiyamane
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * WAI-ARIA compliant disclosure pattern implementation in TypeScript.
4
4
  * Using the <details> and <summary> element.
5
5
  *
6
- * @version 1.2.8
6
+ * @version 1.3.0
7
7
  * @author Yusuke Kamiyamane
8
8
  * @license MIT
9
9
  * @copyright Copyright (c) Yusuke Kamiyamane
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 (!isFocusable(summary)) {
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 getActiveElement() {
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.2.8
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.2.8",
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",