@y14e/disclosure-css 1.3.13 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,27 +13,51 @@ npm i @y14e/disclosure-css
13
13
  import Disclosure from '@y14e/disclosure-css';
14
14
 
15
15
  // CDNs
16
- import Disclosure from 'https://esm.sh/@y14e/disclosure-css@1.3.13';
16
+ import Disclosure from 'https://esm.sh/@y14e/disclosure-css@2.0.0';
17
17
  // or
18
- import Disclosure from 'https://cdn.jsdelivr.net/npm/@y14e/disclosure-css@1.3.13/+esm';
18
+ import Disclosure from 'https://cdn.jsdelivr.net/npm/@y14e/disclosure-css@2.0.0/+esm';
19
19
  // or
20
- import Disclosure from 'https://esm.unpkg.com/@y14e/disclosure-css@1.3.13';
20
+ import Disclosure from 'https://esm.unpkg.com/@y14e/disclosure-css@2.0.0';
21
21
  ```
22
22
 
23
23
  ## Usage
24
24
 
25
25
  ```ts
26
- new Disclosure(root);
26
+ new Disclosure(root, options);
27
27
  // => Disclosure
28
+ //
29
+ // root: HTMLElement
30
+ // options (optional): DisclosureOptions
31
+ ```
32
+
33
+ ## 🪄 Options
34
+
35
+ ```ts
36
+ interface DisclosureOptions {
37
+ collapsible?: boolean; // default: true
38
+ }
39
+ ```
28
40
 
41
+ ### ⚙️ Customize defaults
42
+
43
+ Override the global default settings applied to all disclosure instances.
44
+
45
+ ```ts
46
+ import Disclosure from '@y14e/disclosure-css';
47
+
48
+ Disclosure.defaults = {
49
+ collapsible: false,
50
+ };
51
+
52
+ new Disclosure(root);
29
53
  ```
30
54
 
31
55
  ## 📦 APIs
32
56
 
33
- ### `close`
57
+ ### `collapse`
34
58
 
35
59
  ```ts
36
- disclosure.close(details);
60
+ disclosure.collapse(details);
37
61
  // => void
38
62
  //
39
63
  // details: HTMLDetailsElement
@@ -48,10 +72,10 @@ disclosure.destroy();
48
72
  // => void
49
73
  ```
50
74
 
51
- ### `open`
75
+ ### `expand`
52
76
 
53
77
  ```ts
54
- disclosure.open(details);
78
+ disclosure.expand(details);
55
79
  // => void
56
80
  //
57
81
  // details: HTMLDetailsElement
@@ -4,12 +4,12 @@
4
4
  var DEFAULT_PARSER = (value) => value.split(/\s+/);
5
5
  var DEFAULT_SERIALIZER = (tokens) => tokens.join(" ");
6
6
  function addTokenToAttribute(element, attribute, token, options = {}) {
7
+ const value = element.getAttribute(attribute)?.trim();
7
8
  const {
8
9
  caseInsensitive = false,
9
10
  parse = DEFAULT_PARSER,
10
11
  serialize = DEFAULT_SERIALIZER
11
12
  } = options;
12
- const value = element.getAttribute(attribute)?.trim();
13
13
  const tokens = value ? parse(value).filter(Boolean) : [];
14
14
  if (caseInsensitive) {
15
15
  const lower = token.toLowerCase();
@@ -353,13 +353,7 @@ var RovingTabIndex = class _RovingTabIndex {
353
353
  console.warn("Invalid wrap option. Fallback: false.");
354
354
  wrap = false;
355
355
  }
356
- this.#settings = {
357
- navigationOnly,
358
- noMemory,
359
- noStart,
360
- typeahead,
361
- wrap
362
- };
356
+ this.#settings = { navigationOnly, noMemory, noStart, typeahead, wrap };
363
357
  direction && Object.assign(this.#settings, { direction });
364
358
  selector && Object.assign(this.#settings, { selector });
365
359
  this.#selectorFilter = this.#createSelectorFilter();
@@ -378,29 +372,32 @@ var RovingTabIndex = class _RovingTabIndex {
378
372
  }
379
373
  #initialize() {
380
374
  this.#update(getActiveElement());
381
- if (!(this.#container instanceof HTMLElement)) {
382
- return;
383
- }
384
375
  this.#controller = new AbortController();
385
376
  const { signal } = this.#controller;
386
- document.addEventListener("focusin", this.#onFocusIn, { signal });
387
- this.#settings.noMemory && document.addEventListener("focusout", this.#onFocusOut, { signal });
377
+ this.#container.addEventListener("focusin", this.#onFocusIn, { signal });
378
+ this.#settings.noMemory && this.#container.addEventListener("focusout", this.#onFocusOut, {
379
+ signal
380
+ });
388
381
  this.#container.addEventListener("keydown", this.#onKeyDown, { signal });
389
382
  }
390
383
  #onFocusIn = (event) => {
391
- const { target } = event;
392
- if (!(target instanceof Element)) {
384
+ if (!(event instanceof FocusEvent)) {
393
385
  return;
394
386
  }
395
- const isFocusable2 = this.#focusables.has(target);
396
- this.#settings.noMemory && !isFocusable2 ? this.#update() : isFocusable2 && this.#update(target);
387
+ const { target } = event;
388
+ target instanceof Element && this.#update(target);
397
389
  };
398
390
  #onFocusOut = (event) => {
399
- if (!event.relatedTarget) {
400
- this.#update();
391
+ if (!(event instanceof FocusEvent)) {
392
+ return;
401
393
  }
394
+ const target = event.relatedTarget;
395
+ (!(target instanceof Element) || !this.#focusables.has(target)) && this.#update();
402
396
  };
403
397
  #onKeyDown = (event) => {
398
+ if (!(event instanceof KeyboardEvent)) {
399
+ return;
400
+ }
404
401
  const { key, altKey, ctrlKey, metaKey, shiftKey } = event;
405
402
  if (altKey || ctrlKey || metaKey || shiftKey) {
406
403
  return;
@@ -419,7 +416,7 @@ var RovingTabIndex = class _RovingTabIndex {
419
416
  }
420
417
  }
421
418
  const active = getActiveElement();
422
- if (!(active instanceof HTMLElement)) {
419
+ if (!(active instanceof Element)) {
423
420
  return;
424
421
  }
425
422
  const current = this.#getFocusables();
@@ -534,15 +531,19 @@ var RovingTabIndex = class _RovingTabIndex {
534
531
  };
535
532
 
536
533
  // src/index.ts
537
- var Disclosure = class {
534
+ var Disclosure = class _Disclosure {
535
+ static defaults = {};
538
536
  #rootElement;
537
+ #defaults = { collapsible: true };
538
+ #settings;
539
539
  #detailsElements;
540
540
  #summaryElements;
541
541
  #contentElements;
542
542
  #bindings = /* @__PURE__ */ new WeakMap();
543
+ #controller = null;
543
544
  #cleanupRovingTabIndex = null;
544
545
  #isDestroyed = false;
545
- constructor(root) {
546
+ constructor(root, options = {}) {
546
547
  if (!(root instanceof HTMLElement)) {
547
548
  throw new TypeError("Invalid root element");
548
549
  }
@@ -551,6 +552,8 @@ var Disclosure = class {
551
552
  return;
552
553
  }
553
554
  this.#rootElement = root;
555
+ this.#defaults = this.#mergeOptions(this.#defaults, _Disclosure.defaults);
556
+ this.#settings = this.#mergeOptions(this.#defaults, options);
554
557
  const NOT_NESTED = ":not(:scope summary + * *)";
555
558
  this.#detailsElements = [
556
559
  ...this.#rootElement.querySelectorAll(
@@ -592,7 +595,7 @@ var Disclosure = class {
592
595
  });
593
596
  this.#initialize();
594
597
  }
595
- close(details) {
598
+ collapse(details) {
596
599
  if (this.#isDestroyed) {
597
600
  return;
598
601
  }
@@ -607,6 +610,8 @@ var Disclosure = class {
607
610
  return;
608
611
  }
609
612
  this.#isDestroyed = true;
613
+ this.#controller?.abort();
614
+ this.#controller = null;
610
615
  this.#cleanupRovingTabIndex?.();
611
616
  this.#cleanupRovingTabIndex = null;
612
617
  this.#detailsElements.length = 0;
@@ -615,7 +620,7 @@ var Disclosure = class {
615
620
  this.#contentElements.length = 0;
616
621
  this.#rootElement.removeAttribute("data-disclosure-initialized");
617
622
  }
618
- open(details) {
623
+ expand(details) {
619
624
  if (this.#isDestroyed) {
620
625
  return;
621
626
  }
@@ -631,10 +636,15 @@ var Disclosure = class {
631
636
  "style",
632
637
  "tabindex"
633
638
  ]);
634
- this.#summaryElements.filter((summary) => !this.#isFocusable(summary)).forEach((summary) => {
635
- summary.setAttribute("aria-disabled", "true");
636
- summary.setAttribute("tabindex", "-1");
637
- summary.style.setProperty("pointer-events", "none");
639
+ this.#controller = new AbortController();
640
+ const { signal } = this.#controller;
641
+ this.#summaryElements.forEach((summary) => {
642
+ if (!this.#isFocusable(summary)) {
643
+ summary.setAttribute("aria-disabled", "true");
644
+ summary.setAttribute("tabindex", "-1");
645
+ summary.style.setProperty("pointer-events", "none");
646
+ }
647
+ summary.addEventListener("click", this.#onSummaryClick, { signal });
638
648
  });
639
649
  this.#cleanupRovingTabIndex = createRovingTabIndex(this.#rootElement, {
640
650
  direction: "vertical",
@@ -644,10 +654,27 @@ var Disclosure = class {
644
654
  });
645
655
  this.#rootElement.setAttribute("data-disclosure-initialized", "");
646
656
  }
647
- #toggle(details, isOpen) {
648
- if (details.open !== isOpen) {
649
- details.open = isOpen;
657
+ #onSummaryClick = (event) => {
658
+ event.preventDefault();
659
+ const summary = event.currentTarget;
660
+ if (!(summary instanceof HTMLElement)) {
661
+ return;
662
+ }
663
+ const binding = this.#bindings.get(summary);
664
+ if (!binding) {
665
+ return;
666
+ }
667
+ const { details } = binding;
668
+ this.#toggle(details, !details.open);
669
+ };
670
+ #toggle(details, isExpand) {
671
+ if (details.open === isExpand) {
672
+ return;
650
673
  }
674
+ if (!isExpand && !this.#settings.collapsible && this.#detailsElements.filter((details2) => details2.open).length <= 1) {
675
+ return;
676
+ }
677
+ details.open = isExpand;
651
678
  }
652
679
  #createBinding(details, summary, content) {
653
680
  return { details, summary, content };
@@ -655,13 +682,16 @@ var Disclosure = class {
655
682
  #isFocusable(element) {
656
683
  return element.tabIndex >= 0;
657
684
  }
685
+ #mergeOptions(target, source) {
686
+ return { ...target, ...source };
687
+ }
658
688
  };
659
689
  /**
660
690
  * Disclosure (CSS Animation)
661
691
  * WAI-ARIA compliant disclosure pattern implementation in TypeScript.
662
692
  * Using the <details> and <summary> element.
663
693
  *
664
- * @version 1.3.13
694
+ * @version 2.0.1
665
695
  * @author Yusuke Kamiyamane
666
696
  * @license MIT
667
697
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -673,7 +703,7 @@ var Disclosure = class {
673
703
  (**
674
704
  * Attributes Utils
675
705
  *
676
- * @version 1.1.2
706
+ * @version 1.1.3
677
707
  * @author Yusuke Kamiyamane
678
708
  * @license MIT
679
709
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -686,7 +716,7 @@ power-focusable/dist/index.js:
686
716
  * High-precision focus management utility with full composed tree support.
687
717
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
688
718
  *
689
- * @version 4.3.5
719
+ * @version 4.3.8
690
720
  * @author Yusuke Kamiyamane
691
721
  * @license MIT
692
722
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -699,7 +729,7 @@ power-focusable/dist/index.js:
699
729
  * Lightweight roving tabindex utility with fully focus management.
700
730
  * Designed for accessible menus, tabs, toolbars, and composite widgets.
701
731
  *
702
- * @version 3.1.2
732
+ * @version 3.1.6
703
733
  * @author Yusuke Kamiyamane
704
734
  * @license MIT
705
735
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -2,12 +2,12 @@
2
2
  var DEFAULT_PARSER = (value) => value.split(/\s+/);
3
3
  var DEFAULT_SERIALIZER = (tokens) => tokens.join(" ");
4
4
  function addTokenToAttribute(element, attribute, token, options = {}) {
5
+ const value = element.getAttribute(attribute)?.trim();
5
6
  const {
6
7
  caseInsensitive = false,
7
8
  parse = DEFAULT_PARSER,
8
9
  serialize = DEFAULT_SERIALIZER
9
10
  } = options;
10
- const value = element.getAttribute(attribute)?.trim();
11
11
  const tokens = value ? parse(value).filter(Boolean) : [];
12
12
  if (caseInsensitive) {
13
13
  const lower = token.toLowerCase();
@@ -351,13 +351,7 @@ var RovingTabIndex = class _RovingTabIndex {
351
351
  console.warn("Invalid wrap option. Fallback: false.");
352
352
  wrap = false;
353
353
  }
354
- this.#settings = {
355
- navigationOnly,
356
- noMemory,
357
- noStart,
358
- typeahead,
359
- wrap
360
- };
354
+ this.#settings = { navigationOnly, noMemory, noStart, typeahead, wrap };
361
355
  direction && Object.assign(this.#settings, { direction });
362
356
  selector && Object.assign(this.#settings, { selector });
363
357
  this.#selectorFilter = this.#createSelectorFilter();
@@ -376,29 +370,32 @@ var RovingTabIndex = class _RovingTabIndex {
376
370
  }
377
371
  #initialize() {
378
372
  this.#update(getActiveElement());
379
- if (!(this.#container instanceof HTMLElement)) {
380
- return;
381
- }
382
373
  this.#controller = new AbortController();
383
374
  const { signal } = this.#controller;
384
- document.addEventListener("focusin", this.#onFocusIn, { signal });
385
- this.#settings.noMemory && document.addEventListener("focusout", this.#onFocusOut, { signal });
375
+ this.#container.addEventListener("focusin", this.#onFocusIn, { signal });
376
+ this.#settings.noMemory && this.#container.addEventListener("focusout", this.#onFocusOut, {
377
+ signal
378
+ });
386
379
  this.#container.addEventListener("keydown", this.#onKeyDown, { signal });
387
380
  }
388
381
  #onFocusIn = (event) => {
389
- const { target } = event;
390
- if (!(target instanceof Element)) {
382
+ if (!(event instanceof FocusEvent)) {
391
383
  return;
392
384
  }
393
- const isFocusable2 = this.#focusables.has(target);
394
- this.#settings.noMemory && !isFocusable2 ? this.#update() : isFocusable2 && this.#update(target);
385
+ const { target } = event;
386
+ target instanceof Element && this.#update(target);
395
387
  };
396
388
  #onFocusOut = (event) => {
397
- if (!event.relatedTarget) {
398
- this.#update();
389
+ if (!(event instanceof FocusEvent)) {
390
+ return;
399
391
  }
392
+ const target = event.relatedTarget;
393
+ (!(target instanceof Element) || !this.#focusables.has(target)) && this.#update();
400
394
  };
401
395
  #onKeyDown = (event) => {
396
+ if (!(event instanceof KeyboardEvent)) {
397
+ return;
398
+ }
402
399
  const { key, altKey, ctrlKey, metaKey, shiftKey } = event;
403
400
  if (altKey || ctrlKey || metaKey || shiftKey) {
404
401
  return;
@@ -417,7 +414,7 @@ var RovingTabIndex = class _RovingTabIndex {
417
414
  }
418
415
  }
419
416
  const active = getActiveElement();
420
- if (!(active instanceof HTMLElement)) {
417
+ if (!(active instanceof Element)) {
421
418
  return;
422
419
  }
423
420
  const current = this.#getFocusables();
@@ -532,15 +529,19 @@ var RovingTabIndex = class _RovingTabIndex {
532
529
  };
533
530
 
534
531
  // src/index.ts
535
- var Disclosure = class {
532
+ var Disclosure = class _Disclosure {
533
+ static defaults = {};
536
534
  #rootElement;
535
+ #defaults = { collapsible: true };
536
+ #settings;
537
537
  #detailsElements;
538
538
  #summaryElements;
539
539
  #contentElements;
540
540
  #bindings = /* @__PURE__ */ new WeakMap();
541
+ #controller = null;
541
542
  #cleanupRovingTabIndex = null;
542
543
  #isDestroyed = false;
543
- constructor(root) {
544
+ constructor(root, options = {}) {
544
545
  if (!(root instanceof HTMLElement)) {
545
546
  throw new TypeError("Invalid root element");
546
547
  }
@@ -549,6 +550,8 @@ var Disclosure = class {
549
550
  return;
550
551
  }
551
552
  this.#rootElement = root;
553
+ this.#defaults = this.#mergeOptions(this.#defaults, _Disclosure.defaults);
554
+ this.#settings = this.#mergeOptions(this.#defaults, options);
552
555
  const NOT_NESTED = ":not(:scope summary + * *)";
553
556
  this.#detailsElements = [
554
557
  ...this.#rootElement.querySelectorAll(
@@ -590,7 +593,7 @@ var Disclosure = class {
590
593
  });
591
594
  this.#initialize();
592
595
  }
593
- close(details) {
596
+ collapse(details) {
594
597
  if (this.#isDestroyed) {
595
598
  return;
596
599
  }
@@ -605,6 +608,8 @@ var Disclosure = class {
605
608
  return;
606
609
  }
607
610
  this.#isDestroyed = true;
611
+ this.#controller?.abort();
612
+ this.#controller = null;
608
613
  this.#cleanupRovingTabIndex?.();
609
614
  this.#cleanupRovingTabIndex = null;
610
615
  this.#detailsElements.length = 0;
@@ -613,7 +618,7 @@ var Disclosure = class {
613
618
  this.#contentElements.length = 0;
614
619
  this.#rootElement.removeAttribute("data-disclosure-initialized");
615
620
  }
616
- open(details) {
621
+ expand(details) {
617
622
  if (this.#isDestroyed) {
618
623
  return;
619
624
  }
@@ -629,10 +634,15 @@ var Disclosure = class {
629
634
  "style",
630
635
  "tabindex"
631
636
  ]);
632
- this.#summaryElements.filter((summary) => !this.#isFocusable(summary)).forEach((summary) => {
633
- summary.setAttribute("aria-disabled", "true");
634
- summary.setAttribute("tabindex", "-1");
635
- summary.style.setProperty("pointer-events", "none");
637
+ this.#controller = new AbortController();
638
+ const { signal } = this.#controller;
639
+ this.#summaryElements.forEach((summary) => {
640
+ if (!this.#isFocusable(summary)) {
641
+ summary.setAttribute("aria-disabled", "true");
642
+ summary.setAttribute("tabindex", "-1");
643
+ summary.style.setProperty("pointer-events", "none");
644
+ }
645
+ summary.addEventListener("click", this.#onSummaryClick, { signal });
636
646
  });
637
647
  this.#cleanupRovingTabIndex = createRovingTabIndex(this.#rootElement, {
638
648
  direction: "vertical",
@@ -642,10 +652,27 @@ var Disclosure = class {
642
652
  });
643
653
  this.#rootElement.setAttribute("data-disclosure-initialized", "");
644
654
  }
645
- #toggle(details, isOpen) {
646
- if (details.open !== isOpen) {
647
- details.open = isOpen;
655
+ #onSummaryClick = (event) => {
656
+ event.preventDefault();
657
+ const summary = event.currentTarget;
658
+ if (!(summary instanceof HTMLElement)) {
659
+ return;
660
+ }
661
+ const binding = this.#bindings.get(summary);
662
+ if (!binding) {
663
+ return;
664
+ }
665
+ const { details } = binding;
666
+ this.#toggle(details, !details.open);
667
+ };
668
+ #toggle(details, isExpand) {
669
+ if (details.open === isExpand) {
670
+ return;
648
671
  }
672
+ if (!isExpand && !this.#settings.collapsible && this.#detailsElements.filter((details2) => details2.open).length <= 1) {
673
+ return;
674
+ }
675
+ details.open = isExpand;
649
676
  }
650
677
  #createBinding(details, summary, content) {
651
678
  return { details, summary, content };
@@ -653,13 +680,16 @@ var Disclosure = class {
653
680
  #isFocusable(element) {
654
681
  return element.tabIndex >= 0;
655
682
  }
683
+ #mergeOptions(target, source) {
684
+ return { ...target, ...source };
685
+ }
656
686
  };
657
687
  /**
658
688
  * Disclosure (CSS Animation)
659
689
  * WAI-ARIA compliant disclosure pattern implementation in TypeScript.
660
690
  * Using the <details> and <summary> element.
661
691
  *
662
- * @version 1.3.13
692
+ * @version 2.0.1
663
693
  * @author Yusuke Kamiyamane
664
694
  * @license MIT
665
695
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -671,7 +701,7 @@ var Disclosure = class {
671
701
  (**
672
702
  * Attributes Utils
673
703
  *
674
- * @version 1.1.2
704
+ * @version 1.1.3
675
705
  * @author Yusuke Kamiyamane
676
706
  * @license MIT
677
707
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -684,7 +714,7 @@ power-focusable/dist/index.js:
684
714
  * High-precision focus management utility with full composed tree support.
685
715
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
686
716
  *
687
- * @version 4.3.5
717
+ * @version 4.3.8
688
718
  * @author Yusuke Kamiyamane
689
719
  * @license MIT
690
720
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -697,7 +727,7 @@ power-focusable/dist/index.js:
697
727
  * Lightweight roving tabindex utility with fully focus management.
698
728
  * Designed for accessible menus, tabs, toolbars, and composite widgets.
699
729
  *
700
- * @version 3.1.2
730
+ * @version 3.1.6
701
731
  * @author Yusuke Kamiyamane
702
732
  * @license MIT
703
733
  * @copyright Copyright (c) Yusuke Kamiyamane
package/dist/index.cjs CHANGED
@@ -4,15 +4,19 @@ var attributesUtils = require('@y14e/attributes-utils');
4
4
  var rovingTabindex = require('@y14e/roving-tabindex');
5
5
 
6
6
  // src/index.ts
7
- var Disclosure = class {
7
+ var Disclosure = class _Disclosure {
8
+ static defaults = {};
8
9
  #rootElement;
10
+ #defaults = { collapsible: true };
11
+ #settings;
9
12
  #detailsElements;
10
13
  #summaryElements;
11
14
  #contentElements;
12
15
  #bindings = /* @__PURE__ */ new WeakMap();
16
+ #controller = null;
13
17
  #cleanupRovingTabIndex = null;
14
18
  #isDestroyed = false;
15
- constructor(root) {
19
+ constructor(root, options = {}) {
16
20
  if (!(root instanceof HTMLElement)) {
17
21
  throw new TypeError("Invalid root element");
18
22
  }
@@ -21,6 +25,8 @@ var Disclosure = class {
21
25
  return;
22
26
  }
23
27
  this.#rootElement = root;
28
+ this.#defaults = this.#mergeOptions(this.#defaults, _Disclosure.defaults);
29
+ this.#settings = this.#mergeOptions(this.#defaults, options);
24
30
  const NOT_NESTED = ":not(:scope summary + * *)";
25
31
  this.#detailsElements = [
26
32
  ...this.#rootElement.querySelectorAll(
@@ -62,7 +68,7 @@ var Disclosure = class {
62
68
  });
63
69
  this.#initialize();
64
70
  }
65
- close(details) {
71
+ collapse(details) {
66
72
  if (this.#isDestroyed) {
67
73
  return;
68
74
  }
@@ -77,6 +83,8 @@ var Disclosure = class {
77
83
  return;
78
84
  }
79
85
  this.#isDestroyed = true;
86
+ this.#controller?.abort();
87
+ this.#controller = null;
80
88
  this.#cleanupRovingTabIndex?.();
81
89
  this.#cleanupRovingTabIndex = null;
82
90
  this.#detailsElements.length = 0;
@@ -85,7 +93,7 @@ var Disclosure = class {
85
93
  this.#contentElements.length = 0;
86
94
  this.#rootElement.removeAttribute("data-disclosure-initialized");
87
95
  }
88
- open(details) {
96
+ expand(details) {
89
97
  if (this.#isDestroyed) {
90
98
  return;
91
99
  }
@@ -101,10 +109,15 @@ var Disclosure = class {
101
109
  "style",
102
110
  "tabindex"
103
111
  ]);
104
- this.#summaryElements.filter((summary) => !this.#isFocusable(summary)).forEach((summary) => {
105
- summary.setAttribute("aria-disabled", "true");
106
- summary.setAttribute("tabindex", "-1");
107
- summary.style.setProperty("pointer-events", "none");
112
+ this.#controller = new AbortController();
113
+ const { signal } = this.#controller;
114
+ this.#summaryElements.forEach((summary) => {
115
+ if (!this.#isFocusable(summary)) {
116
+ summary.setAttribute("aria-disabled", "true");
117
+ summary.setAttribute("tabindex", "-1");
118
+ summary.style.setProperty("pointer-events", "none");
119
+ }
120
+ summary.addEventListener("click", this.#onSummaryClick, { signal });
108
121
  });
109
122
  this.#cleanupRovingTabIndex = rovingTabindex.createRovingTabIndex(this.#rootElement, {
110
123
  direction: "vertical",
@@ -114,10 +127,27 @@ var Disclosure = class {
114
127
  });
115
128
  this.#rootElement.setAttribute("data-disclosure-initialized", "");
116
129
  }
117
- #toggle(details, isOpen) {
118
- if (details.open !== isOpen) {
119
- details.open = isOpen;
130
+ #onSummaryClick = (event) => {
131
+ event.preventDefault();
132
+ const summary = event.currentTarget;
133
+ if (!(summary instanceof HTMLElement)) {
134
+ return;
120
135
  }
136
+ const binding = this.#bindings.get(summary);
137
+ if (!binding) {
138
+ return;
139
+ }
140
+ const { details } = binding;
141
+ this.#toggle(details, !details.open);
142
+ };
143
+ #toggle(details, isExpand) {
144
+ if (details.open === isExpand) {
145
+ return;
146
+ }
147
+ if (!isExpand && !this.#settings.collapsible && this.#detailsElements.filter((details2) => details2.open).length <= 1) {
148
+ return;
149
+ }
150
+ details.open = isExpand;
121
151
  }
122
152
  #createBinding(details, summary, content) {
123
153
  return { details, summary, content };
@@ -125,13 +155,16 @@ var Disclosure = class {
125
155
  #isFocusable(element) {
126
156
  return element.tabIndex >= 0;
127
157
  }
158
+ #mergeOptions(target, source) {
159
+ return { ...target, ...source };
160
+ }
128
161
  };
129
162
  /**
130
163
  * Disclosure (CSS Animation)
131
164
  * WAI-ARIA compliant disclosure pattern implementation in TypeScript.
132
165
  * Using the <details> and <summary> element.
133
166
  *
134
- * @version 1.3.13
167
+ * @version 2.0.1
135
168
  * @author Yusuke Kamiyamane
136
169
  * @license MIT
137
170
  * @copyright Copyright (c) Yusuke Kamiyamane
package/dist/index.d.cts CHANGED
@@ -3,18 +3,22 @@
3
3
  * WAI-ARIA compliant disclosure pattern implementation in TypeScript.
4
4
  * Using the <details> and <summary> element.
5
5
  *
6
- * @version 1.3.13
6
+ * @version 2.0.1
7
7
  * @author Yusuke Kamiyamane
8
8
  * @license MIT
9
9
  * @copyright Copyright (c) Yusuke Kamiyamane
10
10
  * @see {@link https://github.com/y14e/disclosure-css}
11
11
  */
12
+ interface DisclosureOptions {
13
+ readonly collapsible?: boolean;
14
+ }
12
15
  declare class Disclosure {
13
16
  #private;
14
- constructor(root: HTMLElement);
15
- close(details: HTMLDetailsElement): void;
17
+ static defaults: DisclosureOptions;
18
+ constructor(root: HTMLElement, options?: DisclosureOptions);
19
+ collapse(details: HTMLDetailsElement): void;
16
20
  destroy(): void;
17
- open(details: HTMLDetailsElement): void;
21
+ expand(details: HTMLDetailsElement): void;
18
22
  }
19
23
 
20
- export { Disclosure as default };
24
+ export { type DisclosureOptions, Disclosure as default };
package/dist/index.d.ts CHANGED
@@ -3,18 +3,22 @@
3
3
  * WAI-ARIA compliant disclosure pattern implementation in TypeScript.
4
4
  * Using the <details> and <summary> element.
5
5
  *
6
- * @version 1.3.13
6
+ * @version 2.0.1
7
7
  * @author Yusuke Kamiyamane
8
8
  * @license MIT
9
9
  * @copyright Copyright (c) Yusuke Kamiyamane
10
10
  * @see {@link https://github.com/y14e/disclosure-css}
11
11
  */
12
+ interface DisclosureOptions {
13
+ readonly collapsible?: boolean;
14
+ }
12
15
  declare class Disclosure {
13
16
  #private;
14
- constructor(root: HTMLElement);
15
- close(details: HTMLDetailsElement): void;
17
+ static defaults: DisclosureOptions;
18
+ constructor(root: HTMLElement, options?: DisclosureOptions);
19
+ collapse(details: HTMLDetailsElement): void;
16
20
  destroy(): void;
17
- open(details: HTMLDetailsElement): void;
21
+ expand(details: HTMLDetailsElement): void;
18
22
  }
19
23
 
20
- export { Disclosure as default };
24
+ export { type DisclosureOptions, Disclosure as default };
package/dist/index.js CHANGED
@@ -2,15 +2,19 @@ import { restoreAttributes, saveAttributes } from '@y14e/attributes-utils';
2
2
  import { createRovingTabIndex } from '@y14e/roving-tabindex';
3
3
 
4
4
  // src/index.ts
5
- var Disclosure = class {
5
+ var Disclosure = class _Disclosure {
6
+ static defaults = {};
6
7
  #rootElement;
8
+ #defaults = { collapsible: true };
9
+ #settings;
7
10
  #detailsElements;
8
11
  #summaryElements;
9
12
  #contentElements;
10
13
  #bindings = /* @__PURE__ */ new WeakMap();
14
+ #controller = null;
11
15
  #cleanupRovingTabIndex = null;
12
16
  #isDestroyed = false;
13
- constructor(root) {
17
+ constructor(root, options = {}) {
14
18
  if (!(root instanceof HTMLElement)) {
15
19
  throw new TypeError("Invalid root element");
16
20
  }
@@ -19,6 +23,8 @@ var Disclosure = class {
19
23
  return;
20
24
  }
21
25
  this.#rootElement = root;
26
+ this.#defaults = this.#mergeOptions(this.#defaults, _Disclosure.defaults);
27
+ this.#settings = this.#mergeOptions(this.#defaults, options);
22
28
  const NOT_NESTED = ":not(:scope summary + * *)";
23
29
  this.#detailsElements = [
24
30
  ...this.#rootElement.querySelectorAll(
@@ -60,7 +66,7 @@ var Disclosure = class {
60
66
  });
61
67
  this.#initialize();
62
68
  }
63
- close(details) {
69
+ collapse(details) {
64
70
  if (this.#isDestroyed) {
65
71
  return;
66
72
  }
@@ -75,6 +81,8 @@ var Disclosure = class {
75
81
  return;
76
82
  }
77
83
  this.#isDestroyed = true;
84
+ this.#controller?.abort();
85
+ this.#controller = null;
78
86
  this.#cleanupRovingTabIndex?.();
79
87
  this.#cleanupRovingTabIndex = null;
80
88
  this.#detailsElements.length = 0;
@@ -83,7 +91,7 @@ var Disclosure = class {
83
91
  this.#contentElements.length = 0;
84
92
  this.#rootElement.removeAttribute("data-disclosure-initialized");
85
93
  }
86
- open(details) {
94
+ expand(details) {
87
95
  if (this.#isDestroyed) {
88
96
  return;
89
97
  }
@@ -99,10 +107,15 @@ var Disclosure = class {
99
107
  "style",
100
108
  "tabindex"
101
109
  ]);
102
- this.#summaryElements.filter((summary) => !this.#isFocusable(summary)).forEach((summary) => {
103
- summary.setAttribute("aria-disabled", "true");
104
- summary.setAttribute("tabindex", "-1");
105
- summary.style.setProperty("pointer-events", "none");
110
+ this.#controller = new AbortController();
111
+ const { signal } = this.#controller;
112
+ this.#summaryElements.forEach((summary) => {
113
+ if (!this.#isFocusable(summary)) {
114
+ summary.setAttribute("aria-disabled", "true");
115
+ summary.setAttribute("tabindex", "-1");
116
+ summary.style.setProperty("pointer-events", "none");
117
+ }
118
+ summary.addEventListener("click", this.#onSummaryClick, { signal });
106
119
  });
107
120
  this.#cleanupRovingTabIndex = createRovingTabIndex(this.#rootElement, {
108
121
  direction: "vertical",
@@ -112,10 +125,27 @@ var Disclosure = class {
112
125
  });
113
126
  this.#rootElement.setAttribute("data-disclosure-initialized", "");
114
127
  }
115
- #toggle(details, isOpen) {
116
- if (details.open !== isOpen) {
117
- details.open = isOpen;
128
+ #onSummaryClick = (event) => {
129
+ event.preventDefault();
130
+ const summary = event.currentTarget;
131
+ if (!(summary instanceof HTMLElement)) {
132
+ return;
118
133
  }
134
+ const binding = this.#bindings.get(summary);
135
+ if (!binding) {
136
+ return;
137
+ }
138
+ const { details } = binding;
139
+ this.#toggle(details, !details.open);
140
+ };
141
+ #toggle(details, isExpand) {
142
+ if (details.open === isExpand) {
143
+ return;
144
+ }
145
+ if (!isExpand && !this.#settings.collapsible && this.#detailsElements.filter((details2) => details2.open).length <= 1) {
146
+ return;
147
+ }
148
+ details.open = isExpand;
119
149
  }
120
150
  #createBinding(details, summary, content) {
121
151
  return { details, summary, content };
@@ -123,13 +153,16 @@ var Disclosure = class {
123
153
  #isFocusable(element) {
124
154
  return element.tabIndex >= 0;
125
155
  }
156
+ #mergeOptions(target, source) {
157
+ return { ...target, ...source };
158
+ }
126
159
  };
127
160
  /**
128
161
  * Disclosure (CSS Animation)
129
162
  * WAI-ARIA compliant disclosure pattern implementation in TypeScript.
130
163
  * Using the <details> and <summary> element.
131
164
  *
132
- * @version 1.3.13
165
+ * @version 2.0.1
133
166
  * @author Yusuke Kamiyamane
134
167
  * @license MIT
135
168
  * @copyright Copyright (c) Yusuke Kamiyamane
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@y14e/disclosure-css",
3
- "version": "1.3.13",
3
+ "version": "2.0.1",
4
4
  "description": "WAI-ARIA compliant disclosure pattern implementation in TypeScript",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -45,13 +45,14 @@
45
45
  "devDependencies": {
46
46
  "bun-types": "latest",
47
47
  "tsup": "^8.0.0",
48
- "typescript": "^5.6.0"
48
+ "typescript": "^5.6.0",
49
+ "utility-types": "^3.11.0"
49
50
  },
50
51
  "engines": {
51
52
  "node": ">=18"
52
53
  },
53
54
  "dependencies": {
54
- "@y14e/attributes-utils": "^1.1.2",
55
- "@y14e/roving-tabindex": "^3.1.2"
55
+ "@y14e/attributes-utils": "^1.1.3",
56
+ "@y14e/roving-tabindex": "^3.1.6"
56
57
  }
57
58
  }