@y14e/disclosure-css 2.0.0 → 2.0.2

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,19 +13,43 @@ 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@2.0.0';
16
+ import Disclosure from 'https://esm.sh/@y14e/disclosure-css@2.0.2';
17
17
  // or
18
- import Disclosure from 'https://cdn.jsdelivr.net/npm/@y14e/disclosure-css@2.0.0/+esm';
18
+ import Disclosure from 'https://cdn.jsdelivr.net/npm/@y14e/disclosure-css@2.0.2/+esm';
19
19
  // or
20
- import Disclosure from 'https://esm.unpkg.com/@y14e/disclosure-css@2.0.0';
20
+ import Disclosure from 'https://esm.unpkg.com/@y14e/disclosure-css@2.0.2';
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
@@ -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();
@@ -372,29 +372,32 @@ var RovingTabIndex = class _RovingTabIndex {
372
372
  }
373
373
  #initialize() {
374
374
  this.#update(getActiveElement());
375
- if (!(this.#container instanceof HTMLElement)) {
376
- return;
377
- }
378
375
  this.#controller = new AbortController();
379
376
  const { signal } = this.#controller;
380
- document.addEventListener("focusin", this.#onFocusIn, { signal });
381
- 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
+ });
382
381
  this.#container.addEventListener("keydown", this.#onKeyDown, { signal });
383
382
  }
384
383
  #onFocusIn = (event) => {
385
- const { target } = event;
386
- if (!(target instanceof Element)) {
384
+ if (!(event instanceof FocusEvent)) {
387
385
  return;
388
386
  }
389
- const isFocusable2 = this.#focusables.has(target);
390
- this.#settings.noMemory && !isFocusable2 ? this.#update() : isFocusable2 && this.#update(target);
387
+ const { target } = event;
388
+ target instanceof Element && this.#update(target);
391
389
  };
392
390
  #onFocusOut = (event) => {
393
- if (!event.relatedTarget) {
394
- this.#update();
391
+ if (!(event instanceof FocusEvent)) {
392
+ return;
395
393
  }
394
+ const target = event.relatedTarget;
395
+ (!(target instanceof Element) || !this.#focusables.has(target)) && this.#update();
396
396
  };
397
397
  #onKeyDown = (event) => {
398
+ if (!(event instanceof KeyboardEvent)) {
399
+ return;
400
+ }
398
401
  const { key, altKey, ctrlKey, metaKey, shiftKey } = event;
399
402
  if (altKey || ctrlKey || metaKey || shiftKey) {
400
403
  return;
@@ -413,7 +416,7 @@ var RovingTabIndex = class _RovingTabIndex {
413
416
  }
414
417
  }
415
418
  const active = getActiveElement();
416
- if (!(active instanceof HTMLElement)) {
419
+ if (!(active instanceof Element)) {
417
420
  return;
418
421
  }
419
422
  const current = this.#getFocusables();
@@ -528,15 +531,19 @@ var RovingTabIndex = class _RovingTabIndex {
528
531
  };
529
532
 
530
533
  // src/index.ts
531
- var Disclosure = class {
534
+ var Disclosure = class _Disclosure {
535
+ static defaults = {};
532
536
  #rootElement;
537
+ #defaults = { collapsible: true };
538
+ #settings;
533
539
  #detailsElements;
534
540
  #summaryElements;
535
541
  #contentElements;
536
542
  #bindings = /* @__PURE__ */ new WeakMap();
543
+ #controller = null;
537
544
  #cleanupRovingTabIndex = null;
538
545
  #isDestroyed = false;
539
- constructor(root) {
546
+ constructor(root, options = {}) {
540
547
  if (!(root instanceof HTMLElement)) {
541
548
  throw new TypeError("Invalid root element");
542
549
  }
@@ -545,6 +552,8 @@ var Disclosure = class {
545
552
  return;
546
553
  }
547
554
  this.#rootElement = root;
555
+ this.#defaults = this.#mergeOptions(this.#defaults, _Disclosure.defaults);
556
+ this.#settings = this.#mergeOptions(this.#defaults, options);
548
557
  const NOT_NESTED = ":not(:scope summary + * *)";
549
558
  this.#detailsElements = [
550
559
  ...this.#rootElement.querySelectorAll(
@@ -601,6 +610,8 @@ var Disclosure = class {
601
610
  return;
602
611
  }
603
612
  this.#isDestroyed = true;
613
+ this.#controller?.abort();
614
+ this.#controller = null;
604
615
  this.#cleanupRovingTabIndex?.();
605
616
  this.#cleanupRovingTabIndex = null;
606
617
  this.#detailsElements.length = 0;
@@ -625,10 +636,15 @@ var Disclosure = class {
625
636
  "style",
626
637
  "tabindex"
627
638
  ]);
628
- this.#summaryElements.filter((summary) => !this.#isFocusable(summary)).forEach((summary) => {
629
- summary.setAttribute("aria-disabled", "true");
630
- summary.setAttribute("tabindex", "-1");
631
- 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 });
632
648
  });
633
649
  this.#cleanupRovingTabIndex = createRovingTabIndex(this.#rootElement, {
634
650
  direction: "vertical",
@@ -638,10 +654,27 @@ var Disclosure = class {
638
654
  });
639
655
  this.#rootElement.setAttribute("data-disclosure-initialized", "");
640
656
  }
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
+ };
641
670
  #toggle(details, isExpand) {
642
- if (details.open !== isExpand) {
643
- details.open = isExpand;
671
+ if (details.open === isExpand) {
672
+ return;
644
673
  }
674
+ if (!isExpand && !this.#settings.collapsible && this.#detailsElements.filter((details2) => details2.open).length <= 1) {
675
+ return;
676
+ }
677
+ details.open = isExpand;
645
678
  }
646
679
  #createBinding(details, summary, content) {
647
680
  return { details, summary, content };
@@ -649,13 +682,16 @@ var Disclosure = class {
649
682
  #isFocusable(element) {
650
683
  return element.tabIndex >= 0;
651
684
  }
685
+ #mergeOptions(target, source) {
686
+ return { ...target, ...source };
687
+ }
652
688
  };
653
689
  /**
654
690
  * Disclosure (CSS Animation)
655
691
  * WAI-ARIA compliant disclosure pattern implementation in TypeScript.
656
692
  * Using the <details> and <summary> element.
657
693
  *
658
- * @version 2.0.0
694
+ * @version 2.0.2
659
695
  * @author Yusuke Kamiyamane
660
696
  * @license MIT
661
697
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -667,7 +703,7 @@ var Disclosure = class {
667
703
  (**
668
704
  * Attributes Utils
669
705
  *
670
- * @version 1.1.2
706
+ * @version 1.1.3
671
707
  * @author Yusuke Kamiyamane
672
708
  * @license MIT
673
709
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -680,7 +716,7 @@ power-focusable/dist/index.js:
680
716
  * High-precision focus management utility with full composed tree support.
681
717
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
682
718
  *
683
- * @version 4.3.6
719
+ * @version 4.3.10
684
720
  * @author Yusuke Kamiyamane
685
721
  * @license MIT
686
722
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -693,7 +729,7 @@ power-focusable/dist/index.js:
693
729
  * Lightweight roving tabindex utility with fully focus management.
694
730
  * Designed for accessible menus, tabs, toolbars, and composite widgets.
695
731
  *
696
- * @version 3.1.3
732
+ * @version 3.1.9
697
733
  * @author Yusuke Kamiyamane
698
734
  * @license MIT
699
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();
@@ -370,29 +370,32 @@ var RovingTabIndex = class _RovingTabIndex {
370
370
  }
371
371
  #initialize() {
372
372
  this.#update(getActiveElement());
373
- if (!(this.#container instanceof HTMLElement)) {
374
- return;
375
- }
376
373
  this.#controller = new AbortController();
377
374
  const { signal } = this.#controller;
378
- document.addEventListener("focusin", this.#onFocusIn, { signal });
379
- 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
+ });
380
379
  this.#container.addEventListener("keydown", this.#onKeyDown, { signal });
381
380
  }
382
381
  #onFocusIn = (event) => {
383
- const { target } = event;
384
- if (!(target instanceof Element)) {
382
+ if (!(event instanceof FocusEvent)) {
385
383
  return;
386
384
  }
387
- const isFocusable2 = this.#focusables.has(target);
388
- this.#settings.noMemory && !isFocusable2 ? this.#update() : isFocusable2 && this.#update(target);
385
+ const { target } = event;
386
+ target instanceof Element && this.#update(target);
389
387
  };
390
388
  #onFocusOut = (event) => {
391
- if (!event.relatedTarget) {
392
- this.#update();
389
+ if (!(event instanceof FocusEvent)) {
390
+ return;
393
391
  }
392
+ const target = event.relatedTarget;
393
+ (!(target instanceof Element) || !this.#focusables.has(target)) && this.#update();
394
394
  };
395
395
  #onKeyDown = (event) => {
396
+ if (!(event instanceof KeyboardEvent)) {
397
+ return;
398
+ }
396
399
  const { key, altKey, ctrlKey, metaKey, shiftKey } = event;
397
400
  if (altKey || ctrlKey || metaKey || shiftKey) {
398
401
  return;
@@ -411,7 +414,7 @@ var RovingTabIndex = class _RovingTabIndex {
411
414
  }
412
415
  }
413
416
  const active = getActiveElement();
414
- if (!(active instanceof HTMLElement)) {
417
+ if (!(active instanceof Element)) {
415
418
  return;
416
419
  }
417
420
  const current = this.#getFocusables();
@@ -526,15 +529,19 @@ var RovingTabIndex = class _RovingTabIndex {
526
529
  };
527
530
 
528
531
  // src/index.ts
529
- var Disclosure = class {
532
+ var Disclosure = class _Disclosure {
533
+ static defaults = {};
530
534
  #rootElement;
535
+ #defaults = { collapsible: true };
536
+ #settings;
531
537
  #detailsElements;
532
538
  #summaryElements;
533
539
  #contentElements;
534
540
  #bindings = /* @__PURE__ */ new WeakMap();
541
+ #controller = null;
535
542
  #cleanupRovingTabIndex = null;
536
543
  #isDestroyed = false;
537
- constructor(root) {
544
+ constructor(root, options = {}) {
538
545
  if (!(root instanceof HTMLElement)) {
539
546
  throw new TypeError("Invalid root element");
540
547
  }
@@ -543,6 +550,8 @@ var Disclosure = class {
543
550
  return;
544
551
  }
545
552
  this.#rootElement = root;
553
+ this.#defaults = this.#mergeOptions(this.#defaults, _Disclosure.defaults);
554
+ this.#settings = this.#mergeOptions(this.#defaults, options);
546
555
  const NOT_NESTED = ":not(:scope summary + * *)";
547
556
  this.#detailsElements = [
548
557
  ...this.#rootElement.querySelectorAll(
@@ -599,6 +608,8 @@ var Disclosure = class {
599
608
  return;
600
609
  }
601
610
  this.#isDestroyed = true;
611
+ this.#controller?.abort();
612
+ this.#controller = null;
602
613
  this.#cleanupRovingTabIndex?.();
603
614
  this.#cleanupRovingTabIndex = null;
604
615
  this.#detailsElements.length = 0;
@@ -623,10 +634,15 @@ var Disclosure = class {
623
634
  "style",
624
635
  "tabindex"
625
636
  ]);
626
- this.#summaryElements.filter((summary) => !this.#isFocusable(summary)).forEach((summary) => {
627
- summary.setAttribute("aria-disabled", "true");
628
- summary.setAttribute("tabindex", "-1");
629
- 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 });
630
646
  });
631
647
  this.#cleanupRovingTabIndex = createRovingTabIndex(this.#rootElement, {
632
648
  direction: "vertical",
@@ -636,10 +652,27 @@ var Disclosure = class {
636
652
  });
637
653
  this.#rootElement.setAttribute("data-disclosure-initialized", "");
638
654
  }
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
+ };
639
668
  #toggle(details, isExpand) {
640
- if (details.open !== isExpand) {
641
- details.open = isExpand;
669
+ if (details.open === isExpand) {
670
+ return;
642
671
  }
672
+ if (!isExpand && !this.#settings.collapsible && this.#detailsElements.filter((details2) => details2.open).length <= 1) {
673
+ return;
674
+ }
675
+ details.open = isExpand;
643
676
  }
644
677
  #createBinding(details, summary, content) {
645
678
  return { details, summary, content };
@@ -647,13 +680,16 @@ var Disclosure = class {
647
680
  #isFocusable(element) {
648
681
  return element.tabIndex >= 0;
649
682
  }
683
+ #mergeOptions(target, source) {
684
+ return { ...target, ...source };
685
+ }
650
686
  };
651
687
  /**
652
688
  * Disclosure (CSS Animation)
653
689
  * WAI-ARIA compliant disclosure pattern implementation in TypeScript.
654
690
  * Using the <details> and <summary> element.
655
691
  *
656
- * @version 2.0.0
692
+ * @version 2.0.2
657
693
  * @author Yusuke Kamiyamane
658
694
  * @license MIT
659
695
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -665,7 +701,7 @@ var Disclosure = class {
665
701
  (**
666
702
  * Attributes Utils
667
703
  *
668
- * @version 1.1.2
704
+ * @version 1.1.3
669
705
  * @author Yusuke Kamiyamane
670
706
  * @license MIT
671
707
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -678,7 +714,7 @@ power-focusable/dist/index.js:
678
714
  * High-precision focus management utility with full composed tree support.
679
715
  * Handles complex focus rules including tabindex ordering, radio groups, inert.
680
716
  *
681
- * @version 4.3.6
717
+ * @version 4.3.10
682
718
  * @author Yusuke Kamiyamane
683
719
  * @license MIT
684
720
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -691,7 +727,7 @@ power-focusable/dist/index.js:
691
727
  * Lightweight roving tabindex utility with fully focus management.
692
728
  * Designed for accessible menus, tabs, toolbars, and composite widgets.
693
729
  *
694
- * @version 3.1.3
730
+ * @version 3.1.9
695
731
  * @author Yusuke Kamiyamane
696
732
  * @license MIT
697
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(
@@ -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;
@@ -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
  }
130
+ #onSummaryClick = (event) => {
131
+ event.preventDefault();
132
+ const summary = event.currentTarget;
133
+ if (!(summary instanceof HTMLElement)) {
134
+ return;
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
+ };
117
143
  #toggle(details, isExpand) {
118
- if (details.open !== isExpand) {
119
- details.open = 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;
120
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 2.0.0
167
+ * @version 2.0.2
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 2.0.0
6
+ * @version 2.0.2
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
+ collapsible: boolean;
14
+ }
12
15
  declare class Disclosure {
13
16
  #private;
14
- constructor(root: HTMLElement);
17
+ static defaults: Partial<DisclosureOptions>;
18
+ constructor(root: HTMLElement, options?: Partial<DisclosureOptions>);
15
19
  collapse(details: HTMLDetailsElement): void;
16
20
  destroy(): void;
17
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 2.0.0
6
+ * @version 2.0.2
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
+ collapsible: boolean;
14
+ }
12
15
  declare class Disclosure {
13
16
  #private;
14
- constructor(root: HTMLElement);
17
+ static defaults: Partial<DisclosureOptions>;
18
+ constructor(root: HTMLElement, options?: Partial<DisclosureOptions>);
15
19
  collapse(details: HTMLDetailsElement): void;
16
20
  destroy(): void;
17
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(
@@ -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;
@@ -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
  }
128
+ #onSummaryClick = (event) => {
129
+ event.preventDefault();
130
+ const summary = event.currentTarget;
131
+ if (!(summary instanceof HTMLElement)) {
132
+ return;
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
+ };
115
141
  #toggle(details, isExpand) {
116
- if (details.open !== isExpand) {
117
- details.open = 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;
118
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 2.0.0
165
+ * @version 2.0.2
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": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "WAI-ARIA compliant disclosure pattern implementation in TypeScript",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -51,7 +51,7 @@
51
51
  "node": ">=18"
52
52
  },
53
53
  "dependencies": {
54
- "@y14e/attributes-utils": "^1.1.2",
55
- "@y14e/roving-tabindex": "^3.1.3"
54
+ "@y14e/attributes-utils": "^1.1.3",
55
+ "@y14e/roving-tabindex": "^3.1.9"
56
56
  }
57
57
  }