@y14e/disclosure 1.2.7 → 1.3.0

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