@rcarls/rc-accordion 0.1.0 → 0.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/README.md CHANGED
@@ -1,3 +1,7 @@
1
1
  # @rcarls/rc-accordion
2
2
 
3
- Accordion coordinator for `rc-disclosure` components.
3
+ Accordion coordinator for child native `<details>` panels with single- or multiple-open behavior, following the [WAI-ARIA Accordion pattern](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/).
4
+
5
+ Docs: [https://richardcarls.github.io/rc-webcomponents/components/rc-accordion](https://richardcarls.github.io/rc-webcomponents/components/rc-accordion).
6
+
7
+ Also supports [`rc-disclosure`](./rc-disclosure) for granular control and fragment-target.
@@ -38,12 +38,32 @@
38
38
  "declarations": [
39
39
  {
40
40
  "kind": "class",
41
- "description": "Coordinates direct child disclosures so only one stays open at a time.\n\nChild disclosures remain light-DOM behavioral wrappers around native\n`<details>` elements. The accordion only adds group behavior and optional\narrow-key navigation between summaries.",
41
+ "description": "Accordion coordinator for child native <details> panels with single- or multiple-open\nbehavior, following the WAI-ARIA Accordion pattern.\n\nSupports native details and rc-disclosure wrappers.",
42
42
  "name": "RCAccordion",
43
43
  "members": [
44
+ {
45
+ "kind": "field",
46
+ "name": "_$managedGroupNames",
47
+ "privacy": "private",
48
+ "default": "new Map<HTMLDetailsElement, string>()"
49
+ },
50
+ {
51
+ "kind": "field",
52
+ "name": "_childrenObserver",
53
+ "privacy": "private",
54
+ "default": "new MutationObserver((records) => this._onChildrenChanged(records))"
55
+ },
56
+ {
57
+ "kind": "field",
58
+ "name": "multiple",
59
+ "type": {
60
+ "text": "boolean"
61
+ },
62
+ "description": "Allows more than one panel open at a time."
63
+ },
44
64
  {
45
65
  "kind": "method",
46
- "name": "_details",
66
+ "name": "_$details",
47
67
  "privacy": "private",
48
68
  "return": {
49
69
  "type": {
@@ -53,7 +73,7 @@
53
73
  },
54
74
  {
55
75
  "kind": "method",
56
- "name": "_summaries",
76
+ "name": "_$summaries",
57
77
  "privacy": "private",
58
78
  "return": {
59
79
  "type": {
@@ -78,8 +98,99 @@
78
98
  },
79
99
  {
80
100
  "kind": "field",
81
- "name": "_onDisclosureToggle",
101
+ "name": "_onDetailsToggle",
82
102
  "privacy": "private"
103
+ },
104
+ {
105
+ "kind": "method",
106
+ "name": "_setDetailsOpen",
107
+ "privacy": "private",
108
+ "return": {
109
+ "type": {
110
+ "text": "void"
111
+ }
112
+ },
113
+ "parameters": [
114
+ {
115
+ "name": "$target",
116
+ "type": {
117
+ "text": "HTMLDetailsElement"
118
+ }
119
+ },
120
+ {
121
+ "name": "open",
122
+ "type": {
123
+ "text": "boolean"
124
+ }
125
+ }
126
+ ]
127
+ },
128
+ {
129
+ "kind": "method",
130
+ "name": "_onChildrenChanged",
131
+ "privacy": "private",
132
+ "return": {
133
+ "type": {
134
+ "text": "void"
135
+ }
136
+ },
137
+ "parameters": [
138
+ {
139
+ "name": "records",
140
+ "type": {
141
+ "text": "MutationRecord[]"
142
+ }
143
+ }
144
+ ]
145
+ },
146
+ {
147
+ "kind": "method",
148
+ "name": "_findAddedOpenDetails",
149
+ "privacy": "private",
150
+ "return": {
151
+ "type": {
152
+ "text": "HTMLDetailsElement | undefined"
153
+ }
154
+ },
155
+ "parameters": [
156
+ {
157
+ "name": "records",
158
+ "type": {
159
+ "text": "MutationRecord[]"
160
+ }
161
+ }
162
+ ]
163
+ },
164
+ {
165
+ "kind": "method",
166
+ "name": "_enforceSingleOpen",
167
+ "privacy": "private",
168
+ "return": {
169
+ "type": {
170
+ "text": "void"
171
+ }
172
+ },
173
+ "parameters": [
174
+ {
175
+ "name": "$target",
176
+ "optional": true,
177
+ "type": {
178
+ "text": "HTMLDetailsElement"
179
+ }
180
+ }
181
+ ]
182
+ }
183
+ ],
184
+ "attributes": [
185
+ {
186
+ "name": "multiple",
187
+ "type": {
188
+ "text": "boolean"
189
+ },
190
+ "description": "Allows more than one panel open at a time."
191
+ },
192
+ {
193
+ "name": "name"
83
194
  }
84
195
  ],
85
196
  "superclass": {
@@ -1 +1 @@
1
- {"version":3,"file":"rc-accordion-define.js","sources":["../src/define.ts"],"sourcesContent":["import '@rcarls/rc-disclosure/define';\n\nimport { RCAccordion } from './index.js';\n\ncustomElements.get('rc-accordion') || customElements.define('rc-accordion', RCAccordion);\n"],"names":["RCAccordion"],"mappings":";;AAIA,eAAe,IAAI,cAAc,KAAK,eAAe,OAAO,gBAAgBA,CAAW;"}
1
+ {"version":3,"file":"rc-accordion-define.js","sources":["../src/define.ts"],"sourcesContent":["import '@rcarls/rc-disclosure/define';\n\nimport { RCAccordion } from './index.js';\n\nif (!customElements.get('rc-accordion')) {\n customElements.define('rc-accordion', RCAccordion);\n}\n"],"names":["RCAccordion"],"mappings":";;AAIK,eAAe,IAAI,cAAc,KACpC,eAAe,OAAO,gBAAgBA,CAAW;"}
@@ -1,50 +1,118 @@
1
- class i extends HTMLElement {
1
+ const a = ["ArrowDown", "ArrowUp", "Home", "End"];
2
+ function u(l) {
3
+ return a.includes(l);
4
+ }
5
+ function c(l) {
6
+ return l?.localName === "rc-disclosure";
7
+ }
8
+ class m extends HTMLElement {
2
9
  constructor() {
3
- super(...arguments), this._onKeydown = (e) => {
4
- if (e.altKey || e.ctrlKey || e.metaKey || e.key !== "ArrowDown" && e.key !== "ArrowUp" && e.key !== "Home" && e.key !== "End") return;
5
- const t = this._summaries();
6
- if (t.length === 0) return;
7
- const s = document.activeElement, r = s ? t.indexOf(s) : -1;
8
- if (r === -1) return;
10
+ super(...arguments), this._$managedGroupNames = /* @__PURE__ */ new Map(), this._childrenObserver = new MutationObserver((e) => this._onChildrenChanged(e)), this._onKeydown = (e) => {
11
+ if (e.altKey || e.ctrlKey || e.metaKey || !u(e.key))
12
+ return;
13
+ const t = this._$summaries();
14
+ if (t.length === 0)
15
+ return;
16
+ const i = document.activeElement, n = i ? t.indexOf(i) : -1;
17
+ if (n === -1)
18
+ return;
9
19
  if (e.preventDefault(), e.key === "Home") {
10
- t[0].focus();
20
+ const o = t.at(0);
21
+ if (!o)
22
+ return;
23
+ o.focus();
11
24
  return;
12
25
  }
13
26
  if (e.key === "End") {
14
- t[t.length - 1].focus();
27
+ const o = t.at(-1);
28
+ if (!o)
29
+ return;
30
+ o.focus();
15
31
  return;
16
32
  }
17
- const n = e.key === "ArrowDown" ? (r + 1) % t.length : (r - 1 + t.length) % t.length;
18
- t[n].focus();
19
- }, this._onDisclosureToggle = (e) => {
20
- if (!e.detail.open) return;
33
+ const s = e.key === "ArrowDown" ? (n + 1) % t.length : (n - 1 + t.length) % t.length, r = t.at(s);
34
+ r && r.focus();
35
+ }, this._onDetailsToggle = (e) => {
21
36
  const t = e.target;
22
- for (const s of this.querySelectorAll(":scope > rc-disclosure"))
23
- s !== t && (s.open = !1);
37
+ t instanceof HTMLDetailsElement && (this.multiple || t.open && this._enforceSingleOpen(t));
24
38
  };
25
39
  }
40
+ static get observedAttributes() {
41
+ return ["multiple", "name"];
42
+ }
43
+ /** Allows more than one panel open at a time. */
44
+ get multiple() {
45
+ return this.hasAttribute("multiple");
46
+ }
47
+ set multiple(e) {
48
+ this.toggleAttribute("multiple", e);
49
+ }
26
50
  connectedCallback() {
27
- this.addEventListener("rc-disclosure-toggle", this._onDisclosureToggle), this.addEventListener("keydown", this._onKeydown), this._syncNativeGroupNames();
51
+ this._childrenObserver.observe(this, { childList: !0, subtree: !0 }), this.addEventListener("toggle", this._onDetailsToggle, !0), this.addEventListener("keydown", this._onKeydown), this._syncNativeGroupNames(), this._enforceSingleOpen();
28
52
  }
29
53
  disconnectedCallback() {
30
- this.removeEventListener("rc-disclosure-toggle", this._onDisclosureToggle), this.removeEventListener("keydown", this._onKeydown);
54
+ this._childrenObserver.disconnect(), this.removeEventListener("toggle", this._onDetailsToggle, !0), this.removeEventListener("keydown", this._onKeydown);
55
+ }
56
+ attributeChangedCallback(e, t, i) {
57
+ t !== i && ((e === "name" || e === "multiple") && this._syncNativeGroupNames(), e === "multiple" && !this.multiple && this._enforceSingleOpen());
31
58
  }
32
- _details() {
59
+ _$details() {
33
60
  return Array.from(
34
- this.querySelectorAll(":scope > rc-disclosure > details")
61
+ this.querySelectorAll(
62
+ ":scope > details, :scope > rc-disclosure > details"
63
+ )
35
64
  );
36
65
  }
37
- _summaries() {
38
- return this._details().map((e) => e.querySelector(":scope > summary")).filter((e) => e !== null);
66
+ _$summaries() {
67
+ return this._$details().map((e) => e.querySelector(":scope > summary")).filter((e) => e !== null);
39
68
  }
40
69
  _syncNativeGroupNames() {
41
- const e = this.getAttribute("name");
42
- if (e)
43
- for (const t of this._details())
44
- t.hasAttribute("name") || t.setAttribute("name", e);
70
+ const e = this.getAttribute("name"), t = this._$details(), i = new Set(t);
71
+ for (const [n, s] of this._$managedGroupNames)
72
+ (!i.has(n) || this.multiple || !e || s !== e || n.getAttribute("name") !== s) && (n.getAttribute("name") === s && n.removeAttribute("name"), this._$managedGroupNames.delete(n));
73
+ if (!(this.multiple || !e))
74
+ for (const n of t)
75
+ n.hasAttribute("name") && !this._$managedGroupNames.has(n) || (n.setAttribute("name", e), this._$managedGroupNames.set(n, e));
76
+ }
77
+ _setDetailsOpen(e, t) {
78
+ const i = e.parentElement;
79
+ if (c(i)) {
80
+ i.open = t;
81
+ return;
82
+ }
83
+ e.open = t;
84
+ }
85
+ _onChildrenChanged(e) {
86
+ this._syncNativeGroupNames(), !this.multiple && this._enforceSingleOpen(this._findAddedOpenDetails(e));
87
+ }
88
+ _findAddedOpenDetails(e) {
89
+ const i = this._$details().filter(
90
+ (n) => e.some(
91
+ (s) => Array.from(s.addedNodes).some(
92
+ (r) => r === n || r instanceof Element && r.contains(n)
93
+ )
94
+ )
95
+ );
96
+ for (let n = i.length - 1; n >= 0; n -= 1) {
97
+ const s = i.at(n);
98
+ if (s && s.open)
99
+ return s;
100
+ }
101
+ }
102
+ _enforceSingleOpen(e) {
103
+ const t = this._$details();
104
+ if (e && !t.includes(e))
105
+ return;
106
+ const i = t.filter((s) => s.open);
107
+ if (i.length <= 1)
108
+ return;
109
+ const n = e ?? i.at(0);
110
+ if (n)
111
+ for (const s of i)
112
+ s !== n && this._setDetailsOpen(s, !1);
45
113
  }
46
114
  }
47
115
  export {
48
- i as RCAccordion
116
+ m as RCAccordion
49
117
  };
50
118
  //# sourceMappingURL=rc-accordion.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rc-accordion.js","sources":["../src/rc-accordion.ts"],"sourcesContent":["import type { RCDisclosureToggleEvent } from '@rcarls/rc-disclosure';\nimport type { RCDisclosure } from '@rcarls/rc-disclosure';\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'rc-accordion': RCAccordion;\n }\n}\n\n/**\n * Coordinates direct child disclosures so only one stays open at a time.\n *\n * Child disclosures remain light-DOM behavioral wrappers around native\n * `<details>` elements. The accordion only adds group behavior and optional\n * arrow-key navigation between summaries.\n */\nexport class RCAccordion extends HTMLElement {\n connectedCallback(): void {\n this.addEventListener('rc-disclosure-toggle', this._onDisclosureToggle as EventListener);\n this.addEventListener('keydown', this._onKeydown);\n this._syncNativeGroupNames();\n }\n\n disconnectedCallback(): void {\n this.removeEventListener('rc-disclosure-toggle', this._onDisclosureToggle as EventListener);\n this.removeEventListener('keydown', this._onKeydown);\n }\n\n private _details(): HTMLDetailsElement[] {\n return Array.from(\n this.querySelectorAll<HTMLDetailsElement>(':scope > rc-disclosure > details'),\n );\n }\n\n private _summaries(): HTMLElement[] {\n return this._details()\n .map((details) => details.querySelector<HTMLElement>(':scope > summary'))\n .filter((summary): summary is HTMLElement => summary !== null);\n }\n\n private _syncNativeGroupNames(): void {\n const name = this.getAttribute('name');\n if (!name) return;\n\n for (const details of this._details()) {\n if (!details.hasAttribute('name')) details.setAttribute('name', name);\n }\n }\n\n private _onKeydown = (e: KeyboardEvent): void => {\n if (e.altKey || e.ctrlKey || e.metaKey) return;\n if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp' && e.key !== 'Home' && e.key !== 'End') return;\n\n const summaries = this._summaries();\n if (summaries.length === 0) return;\n\n const active = document.activeElement as HTMLElement | null;\n const idx = active ? summaries.indexOf(active) : -1;\n if (idx === -1) return;\n\n e.preventDefault();\n\n if (e.key === 'Home') {\n summaries[0].focus();\n return;\n }\n\n if (e.key === 'End') {\n summaries[summaries.length - 1].focus();\n return;\n }\n\n const next = e.key === 'ArrowDown'\n ? (idx + 1) % summaries.length\n : (idx - 1 + summaries.length) % summaries.length;\n\n summaries[next].focus();\n };\n\n private _onDisclosureToggle = (e: CustomEvent<RCDisclosureToggleEvent>): void => {\n if (!e.detail.open) return;\n\n const opened = e.target;\n for (const disclosure of this.querySelectorAll<RCDisclosure>(':scope > rc-disclosure')) {\n if (disclosure !== opened) disclosure.open = false;\n }\n };\n}\n\nexport default RCAccordion;\n"],"names":["RCAccordion","summaries","active","idx","next","opened","disclosure","details","summary","name"],"mappings":"AAgBO,MAAMA,UAAoB,YAAY;AAAA,EAAtC,cAAA;AAAA,UAAA,GAAA,SAAA,GAiCL,KAAQ,aAAa,CAAC,MAA2B;AAE/C,UADI,EAAE,UAAU,EAAE,WAAW,EAAE,WAC3B,EAAE,QAAQ,eAAe,EAAE,QAAQ,aAAa,EAAE,QAAQ,UAAU,EAAE,QAAQ,MAAO;AAEzF,YAAMC,IAAY,KAAK,WAAA;AACvB,UAAIA,EAAU,WAAW,EAAG;AAE5B,YAAMC,IAAS,SAAS,eAClBC,IAAMD,IAASD,EAAU,QAAQC,CAAM,IAAI;AACjD,UAAIC,MAAQ,GAAI;AAIhB,UAFA,EAAE,eAAA,GAEE,EAAE,QAAQ,QAAQ;AACpB,QAAAF,EAAU,CAAC,EAAE,MAAA;AACb;AAAA,MACF;AAEA,UAAI,EAAE,QAAQ,OAAO;AACnB,QAAAA,EAAUA,EAAU,SAAS,CAAC,EAAE,MAAA;AAChC;AAAA,MACF;AAEA,YAAMG,IAAO,EAAE,QAAQ,eAClBD,IAAM,KAAKF,EAAU,UACrBE,IAAM,IAAIF,EAAU,UAAUA,EAAU;AAE7C,MAAAA,EAAUG,CAAI,EAAE,MAAA;AAAA,IAClB,GAEA,KAAQ,sBAAsB,CAAC,MAAkD;AAC/E,UAAI,CAAC,EAAE,OAAO,KAAM;AAEpB,YAAMC,IAAS,EAAE;AACjB,iBAAWC,KAAc,KAAK,iBAA+B,wBAAwB;AACnF,QAAIA,MAAeD,MAAQC,EAAW,OAAO;AAAA,IAEjD;AAAA,EAAA;AAAA,EArEA,oBAA0B;AACxB,SAAK,iBAAiB,wBAAwB,KAAK,mBAAoC,GACvF,KAAK,iBAAiB,WAAW,KAAK,UAAU,GAChD,KAAK,sBAAA;AAAA,EACP;AAAA,EAEA,uBAA6B;AAC3B,SAAK,oBAAoB,wBAAwB,KAAK,mBAAoC,GAC1F,KAAK,oBAAoB,WAAW,KAAK,UAAU;AAAA,EACrD;AAAA,EAEQ,WAAiC;AACvC,WAAO,MAAM;AAAA,MACX,KAAK,iBAAqC,kCAAkC;AAAA,IAAA;AAAA,EAEhF;AAAA,EAEQ,aAA4B;AAClC,WAAO,KAAK,SAAA,EACT,IAAI,CAACC,MAAYA,EAAQ,cAA2B,kBAAkB,CAAC,EACvE,OAAO,CAACC,MAAoCA,MAAY,IAAI;AAAA,EACjE;AAAA,EAEQ,wBAA8B;AACpC,UAAMC,IAAO,KAAK,aAAa,MAAM;AACrC,QAAKA;AAEL,iBAAWF,KAAW,KAAK;AACzB,QAAKA,EAAQ,aAAa,MAAM,KAAGA,EAAQ,aAAa,QAAQE,CAAI;AAAA,EAExE;AAwCF;"}
1
+ {"version":3,"file":"rc-accordion.js","sources":["../src/rc-accordion.ts"],"sourcesContent":["const NAVIGATION_KEYS = ['ArrowDown', 'ArrowUp', 'Home', 'End'] as const;\n\ntype NavigationKey = (typeof NAVIGATION_KEYS)[number];\n\ntype RCDisclosureElement = HTMLElement & {\n open: boolean;\n};\n\nfunction isNavigationKey(key: string): key is NavigationKey {\n return NAVIGATION_KEYS.includes(key as NavigationKey);\n}\n\nfunction isRCDisclosureElement($el: Element | null): $el is RCDisclosureElement {\n return $el?.localName === 'rc-disclosure';\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'rc-accordion': RCAccordion;\n }\n}\n\n/**\n * Accordion coordinator for child native <details> panels with single- or multiple-open\n * behavior, following the WAI-ARIA Accordion pattern.\n *\n * Supports native details and rc-disclosure wrappers.\n *\n * @see {@link https://richardcarls.github.io/rc-webcomponents/components/rc-accordion rc-accordion docs}\n * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/accordion/ WAI-ARIA Accordion pattern}\n *\n * @attr {boolean} multiple - Allows more than one panel open at a time.\n */\nexport class RCAccordion extends HTMLElement {\n private _$managedGroupNames = new Map<HTMLDetailsElement, string>();\n\n private _childrenObserver = new MutationObserver((records) => this._onChildrenChanged(records));\n\n static get observedAttributes(): string[] {\n return ['multiple', 'name'];\n }\n\n /** Allows more than one panel open at a time. */\n get multiple(): boolean {\n return this.hasAttribute('multiple');\n }\n\n set multiple(value: boolean) {\n this.toggleAttribute('multiple', value);\n }\n\n connectedCallback(): void {\n this._childrenObserver.observe(this, { childList: true, subtree: true });\n\n this.addEventListener('toggle', this._onDetailsToggle, true);\n this.addEventListener('keydown', this._onKeydown);\n\n this._syncNativeGroupNames();\n this._enforceSingleOpen();\n }\n\n disconnectedCallback(): void {\n this._childrenObserver.disconnect();\n\n this.removeEventListener('toggle', this._onDetailsToggle, true);\n this.removeEventListener('keydown', this._onKeydown);\n }\n\n attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {\n if (oldValue === newValue) {\n return;\n }\n\n if (name === 'name' || name === 'multiple') {\n this._syncNativeGroupNames();\n }\n\n if (name === 'multiple' && !this.multiple) {\n this._enforceSingleOpen();\n }\n }\n\n private _$details(): HTMLDetailsElement[] {\n return Array.from(\n this.querySelectorAll<HTMLDetailsElement>(\n ':scope > details, :scope > rc-disclosure > details',\n ),\n );\n }\n\n private _$summaries(): HTMLElement[] {\n return this._$details()\n .map(($details) => $details.querySelector<HTMLElement>(':scope > summary'))\n .filter(($summary): $summary is HTMLElement => $summary !== null);\n }\n\n private _syncNativeGroupNames(): void {\n const groupName = this.getAttribute('name');\n const $details = this._$details();\n const $currentDetails = new Set($details);\n\n for (const [$el, managedName] of this._$managedGroupNames) {\n const shouldReleaseName =\n !$currentDetails.has($el) ||\n this.multiple ||\n !groupName ||\n managedName !== groupName ||\n $el.getAttribute('name') !== managedName;\n\n if (!shouldReleaseName) {\n continue;\n }\n\n if ($el.getAttribute('name') === managedName) {\n $el.removeAttribute('name');\n }\n\n this._$managedGroupNames.delete($el);\n }\n\n if (this.multiple || !groupName) {\n return;\n }\n\n for (const $el of $details) {\n if ($el.hasAttribute('name') && !this._$managedGroupNames.has($el)) {\n continue;\n }\n\n $el.setAttribute('name', groupName);\n this._$managedGroupNames.set($el, groupName);\n }\n }\n\n private _onKeydown = (e: KeyboardEvent): void => {\n if (e.altKey || e.ctrlKey || e.metaKey) {\n return;\n }\n\n if (!isNavigationKey(e.key)) {\n return;\n }\n\n const $summaries = this._$summaries();\n\n if ($summaries.length === 0) {\n return;\n }\n\n const $active = document.activeElement as HTMLElement | null;\n const index = $active ? $summaries.indexOf($active) : -1;\n\n if (index === -1) {\n return;\n }\n\n e.preventDefault();\n\n if (e.key === 'Home') {\n const $firstSummary = $summaries.at(0);\n\n if (!$firstSummary) {\n return;\n }\n\n $firstSummary.focus();\n\n return;\n }\n\n if (e.key === 'End') {\n const $lastSummary = $summaries.at(-1);\n\n if (!$lastSummary) {\n return;\n }\n\n $lastSummary.focus();\n\n return;\n }\n\n const next =\n e.key === 'ArrowDown'\n ? (index + 1) % $summaries.length\n : (index - 1 + $summaries.length) % $summaries.length;\n\n const $nextSummary = $summaries.at(next);\n\n if (!$nextSummary) {\n return;\n }\n\n $nextSummary.focus();\n };\n\n private _onDetailsToggle = (e: Event): void => {\n const $opened = e.target;\n\n if (!($opened instanceof HTMLDetailsElement)) {\n return;\n }\n\n if (this.multiple) {\n return;\n }\n\n if (!$opened.open) {\n return;\n }\n\n this._enforceSingleOpen($opened);\n };\n\n private _setDetailsOpen($target: HTMLDetailsElement, open: boolean): void {\n const $parent = $target.parentElement;\n\n if (isRCDisclosureElement($parent)) {\n $parent.open = open;\n\n return;\n }\n\n $target.open = open;\n }\n\n private _onChildrenChanged(records: MutationRecord[]): void {\n this._syncNativeGroupNames();\n\n if (this.multiple) {\n return;\n }\n\n this._enforceSingleOpen(this._findAddedOpenDetails(records));\n }\n\n private _findAddedOpenDetails(records: MutationRecord[]): HTMLDetailsElement | undefined {\n const $details = this._$details();\n\n const $added = $details.filter(($el) =>\n records.some((record) =>\n Array.from(record.addedNodes).some(\n ($node) => $node === $el || ($node instanceof Element && $node.contains($el)),\n ),\n ),\n );\n\n for (let i = $added.length - 1; i >= 0; i -= 1) {\n const $el = $added.at(i);\n\n if (!$el) {\n continue;\n }\n\n if ($el.open) {\n return $el;\n }\n }\n\n return undefined;\n }\n\n private _enforceSingleOpen($target?: HTMLDetailsElement): void {\n const $details = this._$details();\n\n if ($target && !$details.includes($target)) {\n return;\n }\n\n const $openDetails = $details.filter(($el) => $el.open);\n\n if ($openDetails.length <= 1) {\n return;\n }\n\n const $keepOpen = $target ?? $openDetails.at(0);\n\n if (!$keepOpen) {\n return;\n }\n\n for (const $el of $openDetails) {\n if ($el !== $keepOpen) {\n this._setDetailsOpen($el, false);\n }\n }\n }\n}\n\nexport default RCAccordion;\n"],"names":["NAVIGATION_KEYS","isNavigationKey","key","isRCDisclosureElement","$el","RCAccordion","records","$summaries","$active","index","$firstSummary","$lastSummary","next","$nextSummary","$opened","value","name","oldValue","newValue","$details","$summary","groupName","$currentDetails","managedName","$target","open","$parent","$added","record","$node","i","$openDetails","$keepOpen"],"mappings":"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"}
@@ -4,19 +4,34 @@ declare global {
4
4
  }
5
5
  }
6
6
  /**
7
- * Coordinates direct child disclosures so only one stays open at a time.
7
+ * Accordion coordinator for child native <details> panels with single- or multiple-open
8
+ * behavior, following the WAI-ARIA Accordion pattern.
8
9
  *
9
- * Child disclosures remain light-DOM behavioral wrappers around native
10
- * `<details>` elements. The accordion only adds group behavior and optional
11
- * arrow-key navigation between summaries.
10
+ * Supports native details and rc-disclosure wrappers.
11
+ *
12
+ * @see {@link https://richardcarls.github.io/rc-webcomponents/components/rc-accordion rc-accordion docs}
13
+ * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/accordion/ WAI-ARIA Accordion pattern}
14
+ *
15
+ * @attr {boolean} multiple - Allows more than one panel open at a time.
12
16
  */
13
17
  export declare class RCAccordion extends HTMLElement {
18
+ private _$managedGroupNames;
19
+ private _childrenObserver;
20
+ static get observedAttributes(): string[];
21
+ /** Allows more than one panel open at a time. */
22
+ get multiple(): boolean;
23
+ set multiple(value: boolean);
14
24
  connectedCallback(): void;
15
25
  disconnectedCallback(): void;
16
- private _details;
17
- private _summaries;
26
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
27
+ private _$details;
28
+ private _$summaries;
18
29
  private _syncNativeGroupNames;
19
30
  private _onKeydown;
20
- private _onDisclosureToggle;
31
+ private _onDetailsToggle;
32
+ private _setDetailsOpen;
33
+ private _onChildrenChanged;
34
+ private _findAddedOpenDetails;
35
+ private _enforceSingleOpen;
21
36
  }
22
37
  export default RCAccordion;
package/package.json CHANGED
@@ -3,14 +3,14 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.0",
7
- "description": "Headless WAI-ARIA accordion coordinator for disclosure groups, built with Lit",
6
+ "version": "0.3.0",
7
+ "description": "Accordion coordinator for child native <details> panels with single- or multiple-open behavior.",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/richardcarls/rc-webcomponents.git",
11
11
  "directory": "packages/rc-accordion"
12
12
  },
13
- "homepage": "https://github.com/richardcarls/rc-webcomponents#readme",
13
+ "homepage": "https://richardcarls.github.io/rc-webcomponents/components/rc-accordion",
14
14
  "license": "MIT",
15
15
  "type": "module",
16
16
  "files": [
@@ -37,21 +37,17 @@
37
37
  ],
38
38
  "customElements": "dist/custom-elements.json",
39
39
  "scripts": {
40
- "dev": "vite",
41
40
  "build": "tsc && vite build && cem analyze",
42
41
  "cem:analyze": "cem analyze",
43
- "preview": "vite preview",
44
- "test:browser": "vitest",
45
- "test:browser:chrome": "vitest --project=chromium",
46
- "test:browser:firefox": "vitest --project=firefox",
47
- "yalc:publish": "yalc publish --push"
42
+ "test:browser": "vitest --run",
43
+ "test:browser:chrome": "vitest --run --project=chromium",
44
+ "test:browser:firefox": "vitest --run --project=firefox"
48
45
  },
49
46
  "dependencies": {
50
- "@rcarls/rc-disclosure": "workspace:^"
47
+ "@rcarls/rc-disclosure": "workspace:*"
51
48
  },
52
49
  "devDependencies": {
53
50
  "@custom-elements-manifest/analyzer": "0.11.0",
54
- "@guanghechen/rollup-plugin-copy": "^6.0.9",
55
51
  "@vitest/browser-playwright": "4.1.5",
56
52
  "lit": "^3.0.0",
57
53
  "playwright": "^1.56.0",
package/dist/demo.css DELETED
@@ -1,85 +0,0 @@
1
- *, *::before, *::after {
2
- box-sizing: border-box;
3
- }
4
-
5
- :root {
6
- color-scheme: light dark;
7
- --rc-accent: Highlight; /* Chrome doesn't support AccentColor on the open web */
8
- }
9
-
10
- @supports (color: AccentColor) {
11
- :root {
12
- --rc-accent: AccentColor;
13
- }
14
- }
15
-
16
- [data-theme="light"] { color-scheme: light; }
17
- [data-theme="dark"] { color-scheme: dark; }
18
-
19
- [data-theme="solarized-light"] {
20
- color-scheme: light;
21
- --rc-surface: #fdf6e3;
22
- --rc-text: #657b83;
23
- --rc-border: 1px solid #93a1a1;
24
- --rc-shadow: 0 2px 8px rgba(0, 0, 0, .12);
25
- }
26
-
27
- [data-theme="solarized-dark"] {
28
- color-scheme: dark;
29
- --rc-surface: #002b36;
30
- --rc-text: #839496;
31
- --rc-border: 1px solid #586e75;
32
- --rc-shadow: 0 2px 8px rgba(0, 0, 0, .3);
33
- }
34
-
35
- body {
36
- font-family: system-ui, sans-serif;
37
- margin: 0;
38
- }
39
-
40
- .demo-page {
41
- max-width: 60rem;
42
- margin: 0 auto;
43
- padding: 2rem 1.5rem;
44
- }
45
-
46
- .demo-controls {
47
- display: flex;
48
- align-items: center;
49
- gap: 1rem;
50
- margin-bottom: 2rem;
51
- padding-bottom: 1rem;
52
- border-bottom: 1px solid ButtonBorder;
53
- }
54
-
55
- .demo-controls .demo-title {
56
- flex: 1;
57
- }
58
-
59
- .demo-controls h1 {
60
- margin: 0 0 0.15rem;
61
- font-size: 1.5rem;
62
- }
63
-
64
- .demo-controls a {
65
- font-size: 0.8rem;
66
- }
67
-
68
- .demo-section {
69
- margin-bottom: 2rem;
70
- }
71
-
72
- .demo-section h2 {
73
- margin: 0 0 0.5rem;
74
- }
75
-
76
- .theme-picker {
77
- padding: 0.35em 0.75em;
78
- font-family: inherit;
79
- font-size: 0.8rem;
80
- cursor: pointer;
81
- border: 1px solid ButtonBorder;
82
- border-radius: 4px;
83
- background: Canvas;
84
- color: CanvasText;
85
- }
package/dist/demo.js DELETED
@@ -1,40 +0,0 @@
1
- const THEMES = ['', 'light', 'dark', 'solarized-light', 'solarized-dark'];
2
- const LABELS = ['Auto', 'Light', 'Dark', 'Solarized ☀', 'Solarized ☾'];
3
-
4
- const stored = localStorage.getItem('rc-demo-theme') ?? '';
5
- applyTheme(stored);
6
-
7
- function applyTheme(theme) {
8
- if (theme) {
9
- document.documentElement.dataset.theme = theme;
10
- } else {
11
- delete document.documentElement.dataset.theme;
12
- }
13
- }
14
-
15
- function currentIndex() {
16
- const current = document.documentElement.dataset.theme ?? '';
17
- const idx = THEMES.indexOf(current);
18
- return idx === -1 ? 0 : idx;
19
- }
20
-
21
- function updateButtons() {
22
- const label = LABELS[currentIndex()];
23
- document.querySelectorAll('.theme-picker').forEach((btn) => {
24
- btn.textContent = label;
25
- });
26
- }
27
-
28
- window.cycleTheme = function () {
29
- const next = THEMES[(currentIndex() + 1) % THEMES.length];
30
- applyTheme(next);
31
- localStorage.setItem('rc-demo-theme', next);
32
- updateButtons();
33
- };
34
-
35
- document.addEventListener('DOMContentLoaded', () => {
36
- updateButtons();
37
- document.querySelectorAll('.theme-picker').forEach((btn) => {
38
- btn.addEventListener('click', window.cycleTheme);
39
- });
40
- });