@rcarls/rc-disclosure 0.5.0 → 0.6.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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @rcarls/rc-disclosure
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 57370e4: Add `defaultOpen` and `default-open` for uncontrolled disclosure state. Host writes to `open`
8
+ now establish controlled mode, user toggles report the requested state without overriding the
9
+ host value, and assigning `undefined` releases control.
10
+
3
11
  ## 0.5.0
4
12
 
5
13
  ### Minor Changes
package/README.md CHANGED
@@ -1,3 +1,20 @@
1
1
  # @rcarls/rc-disclosure
2
2
 
3
- Native details/summary disclosure web component with light-DOM styling hooks.
3
+ Native `<details>`/`<summary>` disclosure web component with light-DOM styling hooks and
4
+ controlled or uncontrolled open state.
5
+
6
+ Docs: [https://richardcarls.github.io/rc-webcomponents/components/rc-disclosure](https://richardcarls.github.io/rc-webcomponents/components/rc-disclosure).
7
+
8
+ ```html
9
+ <rc-disclosure default-open>
10
+ <details>
11
+ <summary>Shipping details</summary>
12
+ <p>Orders usually ship within two business days.</p>
13
+ </details>
14
+ </rc-disclosure>
15
+ ```
16
+
17
+ Use `default-open` for uncontrolled initial state. Writing `open` enables controlled mode;
18
+ user interaction emits `rc-disclosure-toggle` with `{ open: boolean }`, while the rendered
19
+ state continues to follow the host value. Assign `undefined` to the JavaScript `open` property
20
+ to release control.
@@ -46,7 +46,7 @@
46
46
  "declarations": [
47
47
  {
48
48
  "kind": "class",
49
- "description": "Disclosure wrapper for a native <details>/<summary> pair with controlled open state,\nfollowing the WAI-ARIA Disclosure pattern.\n\nThe browser keeps ownership of open/close behavior, keyboard support, and\naccessibility. This element mirrors state, emits a consistent custom event,\nand automatically opens when the URL hash matches any id within its subtree.",
49
+ "description": "Disclosure wrapper for a native <details>/<summary> pair with controlled and uncontrolled\nopen state,\nfollowing the WAI-ARIA Disclosure pattern.\n\nThe browser keeps ownership of open/close behavior, keyboard support, and\naccessibility. This element mirrors state, emits a consistent custom event,\nand automatically opens when the URL hash matches any id within its subtree.",
50
50
  "name": "RCDisclosure",
51
51
  "members": [
52
52
  {
@@ -75,25 +75,84 @@
75
75
  },
76
76
  {
77
77
  "kind": "field",
78
- "name": "open",
78
+ "name": "_controlledOpen",
79
+ "type": {
80
+ "text": "boolean | undefined"
81
+ },
82
+ "privacy": "private"
83
+ },
84
+ {
85
+ "kind": "field",
86
+ "name": "_defaultOpen",
79
87
  "type": {
80
88
  "text": "boolean"
81
89
  },
82
- "description": "Current open state mirrored to the child `<details>`."
90
+ "privacy": "private",
91
+ "default": "false"
83
92
  },
84
93
  {
85
- "kind": "method",
86
- "name": "_setupDetails",
94
+ "kind": "field",
95
+ "name": "_defaultOpenInitialized",
96
+ "type": {
97
+ "text": "boolean"
98
+ },
87
99
  "privacy": "private",
88
- "return": {
89
- "type": {
90
- "text": "void"
91
- }
92
- }
100
+ "default": "false"
101
+ },
102
+ {
103
+ "kind": "field",
104
+ "name": "_uncontrolledOpen",
105
+ "type": {
106
+ "text": "boolean | undefined"
107
+ },
108
+ "privacy": "private"
109
+ },
110
+ {
111
+ "kind": "field",
112
+ "name": "_openInitialized",
113
+ "type": {
114
+ "text": "boolean"
115
+ },
116
+ "privacy": "private",
117
+ "default": "false"
118
+ },
119
+ {
120
+ "kind": "field",
121
+ "name": "_reflectingOpen",
122
+ "type": {
123
+ "text": "boolean"
124
+ },
125
+ "privacy": "private",
126
+ "default": "false"
127
+ },
128
+ {
129
+ "kind": "field",
130
+ "name": "_suppressNextToggle",
131
+ "type": {
132
+ "text": "boolean"
133
+ },
134
+ "privacy": "private",
135
+ "default": "false"
136
+ },
137
+ {
138
+ "kind": "field",
139
+ "name": "open",
140
+ "type": {
141
+ "text": "boolean"
142
+ },
143
+ "description": "Current open state. Host writes are silent; assign `undefined` to release control."
144
+ },
145
+ {
146
+ "kind": "field",
147
+ "name": "defaultOpen",
148
+ "type": {
149
+ "text": "boolean"
150
+ },
151
+ "description": "Initial open state for uncontrolled usage."
93
152
  },
94
153
  {
95
154
  "kind": "method",
96
- "name": "_teardownDetails",
155
+ "name": "_setupDetails",
97
156
  "privacy": "private",
98
157
  "return": {
99
158
  "type": {
@@ -103,7 +162,7 @@
103
162
  },
104
163
  {
105
164
  "kind": "method",
106
- "name": "_syncDetailsOpen",
165
+ "name": "_teardownDetails",
107
166
  "privacy": "private",
108
167
  "return": {
109
168
  "type": {
@@ -113,7 +172,7 @@
113
172
  },
114
173
  {
115
174
  "kind": "method",
116
- "name": "_setOpen",
175
+ "name": "_applyOpen",
117
176
  "privacy": "private",
118
177
  "return": {
119
178
  "type": {
@@ -122,16 +181,14 @@
122
181
  },
123
182
  "parameters": [
124
183
  {
125
- "name": "value",
184
+ "name": "open",
126
185
  "type": {
127
186
  "text": "boolean"
128
187
  }
129
188
  },
130
189
  {
131
- "name": "reflect",
132
- "type": {
133
- "text": "boolean"
134
- }
190
+ "name": "suppressToggle",
191
+ "default": "true"
135
192
  }
136
193
  ]
137
194
  },
@@ -175,6 +232,12 @@
175
232
  }
176
233
  ],
177
234
  "events": [
235
+ {
236
+ "name": "TOGGLE_EVENT",
237
+ "type": {
238
+ "text": "CustomEvent"
239
+ }
240
+ },
178
241
  {
179
242
  "description": "Fires when the child `<details>` toggles.",
180
243
  "name": "rc-disclosure-toggle"
@@ -183,7 +246,11 @@
183
246
  "attributes": [
184
247
  {
185
248
  "name": "open",
186
- "description": "Current open state mirrored to the child `<details>`."
249
+ "description": "Controlled open state mirrored to the child `<details>`."
250
+ },
251
+ {
252
+ "name": "default-open",
253
+ "description": "Initial open state for uncontrolled usage."
187
254
  }
188
255
  ],
189
256
  "superclass": {
@@ -1,9 +1,13 @@
1
1
  const n = "rc-disclosure-toggle", r = ":scope > details", i = ":scope > summary";
2
- class a extends HTMLElement {
2
+ class l extends HTMLElement {
3
3
  constructor() {
4
- super(...arguments), this._observer = new MutationObserver(() => this._setupDetails()), this._$details = null, this._scrollFrame = 0, this._onToggle = () => {
4
+ super(...arguments), this._observer = new MutationObserver(() => this._setupDetails()), this._$details = null, this._scrollFrame = 0, this._defaultOpen = !1, this._defaultOpenInitialized = !1, this._openInitialized = !1, this._reflectingOpen = !1, this._suppressNextToggle = !1, this._onToggle = () => {
5
+ if (this._suppressNextToggle) {
6
+ this._suppressNextToggle = !1;
7
+ return;
8
+ }
5
9
  const e = this._$details?.open ?? !1;
6
- this._reflectOpen(e), this.dispatchEvent(
10
+ this._controlledOpen === void 0 ? (this._uncontrolledOpen = e, this._reflectOpen(e)) : (this._applyOpen(this._controlledOpen, !0), this._reflectOpen(this._controlledOpen)), this.dispatchEvent(
7
11
  new CustomEvent(n, {
8
12
  bubbles: !0,
9
13
  composed: !0,
@@ -15,14 +19,21 @@ class a extends HTMLElement {
15
19
  };
16
20
  }
17
21
  static get observedAttributes() {
18
- return ["open"];
22
+ return ["open", "default-open"];
19
23
  }
20
- /** Current open state mirrored to the child `<details>`. */
24
+ /** Current open state. Host writes are silent; assign `undefined` to release control. */
21
25
  get open() {
22
- return this._$details?.open ?? this.hasAttribute("open");
26
+ return this._controlledOpen ?? this._uncontrolledOpen ?? this._defaultOpen;
23
27
  }
24
28
  set open(e) {
25
- this._setOpen(e, !0);
29
+ this._controlledOpen = e, this._openInitialized = !0, this._applyOpen(this.open), this._reflectOpen(this.open);
30
+ }
31
+ /** Initial open state for uncontrolled usage. */
32
+ get defaultOpen() {
33
+ return this._defaultOpen;
34
+ }
35
+ set defaultOpen(e) {
36
+ this._defaultOpen = e, this._defaultOpenInitialized = !0, !this._openInitialized && this._controlledOpen === void 0 && this._uncontrolledOpen === void 0 && (this._applyOpen(e), this._reflectOpen(e));
26
37
  }
27
38
  connectedCallback() {
28
39
  this._observer.observe(this, { childList: !0 }), window.addEventListener("hashchange", this._onHashChange), this._setupDetails();
@@ -30,35 +41,30 @@ class a extends HTMLElement {
30
41
  disconnectedCallback() {
31
42
  this._observer.disconnect(), window.removeEventListener("hashchange", this._onHashChange), cancelAnimationFrame(this._scrollFrame), this._teardownDetails();
32
43
  }
33
- attributeChangedCallback(e, t, s) {
34
- t !== s && this._syncDetailsOpen();
44
+ attributeChangedCallback(e, s, t) {
45
+ if (s !== t) {
46
+ if (e === "default-open") {
47
+ this.defaultOpen = t !== null;
48
+ return;
49
+ }
50
+ this._reflectingOpen || (this.open = t !== null);
51
+ }
35
52
  }
36
53
  _setupDetails() {
37
54
  const e = this.querySelector(r);
38
55
  if (e === this._$details || (this._teardownDetails(), this._$details = e, !e))
39
56
  return;
40
- e.id || (e.id = crypto.randomUUID()), e.querySelector(i)?.setAttribute("aria-controls", e.id), e.addEventListener("toggle", this._onToggle), !this.hasAttribute("open") && e.open && this._reflectOpen(!0), this._syncDetailsOpen(), this._reflectOpen(e.open), this._openForCurrentHash();
57
+ e.id || (e.id = crypto.randomUUID()), e.querySelector(i)?.setAttribute("aria-controls", e.id), e.addEventListener("toggle", this._onToggle), this._controlledOpen === void 0 && this._uncontrolledOpen === void 0 && (this._uncontrolledOpen = this._defaultOpenInitialized ? this._defaultOpen : e.open), this._applyOpen(this.open, !0), this._reflectOpen(this.open), this._openForCurrentHash();
41
58
  }
42
59
  _teardownDetails() {
43
60
  this._$details?.querySelector(i)?.removeAttribute("aria-controls"), this._$details?.removeEventListener("toggle", this._onToggle), this._$details = null;
44
61
  }
45
- _syncDetailsOpen() {
46
- const e = this._$details;
47
- if (!e)
48
- return;
49
- const t = this.hasAttribute("open");
50
- e.open !== t && (e.open = t);
51
- }
52
- _setOpen(e, t) {
53
- const s = this._$details;
54
- s && s.open !== e && (s.open = e), t && this._reflectOpen(e);
62
+ _applyOpen(e, s = !0) {
63
+ const t = this._$details;
64
+ t && t.open !== e && (this._suppressNextToggle = s, t.open = e);
55
65
  }
56
66
  _reflectOpen(e) {
57
- if (e) {
58
- this.hasAttribute("open") || this.setAttribute("open", "");
59
- return;
60
- }
61
- this.hasAttribute("open") && this.removeAttribute("open");
67
+ this.hasAttribute("open") !== e && (this._reflectingOpen = !0, this.toggleAttribute("open", e), this._reflectingOpen = !1);
62
68
  }
63
69
  _openForCurrentHash() {
64
70
  if (!location.hash)
@@ -66,13 +72,19 @@ class a extends HTMLElement {
66
72
  const e = decodeURIComponent(location.hash.slice(1));
67
73
  if (!e || !this.querySelector(`#${CSS.escape(e)}`))
68
74
  return;
69
- const s = this.open;
70
- this.open = !0, !s && (cancelAnimationFrame(this._scrollFrame), this._scrollFrame = requestAnimationFrame(() => {
75
+ const t = this.open;
76
+ this._controlledOpen === void 0 ? (this._uncontrolledOpen = !0, this._applyOpen(!0), this._reflectOpen(!0)) : this.dispatchEvent(
77
+ new CustomEvent(n, {
78
+ bubbles: !0,
79
+ composed: !0,
80
+ detail: { open: !0 }
81
+ })
82
+ ), this.open && (t || (cancelAnimationFrame(this._scrollFrame), this._scrollFrame = requestAnimationFrame(() => {
71
83
  document.getElementById(e)?.scrollIntoView();
72
- }));
84
+ })));
73
85
  }
74
86
  }
75
87
  export {
76
- a as RCDisclosure
88
+ l as RCDisclosure
77
89
  };
78
90
  //# sourceMappingURL=rc-disclosure.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rc-disclosure.js","sources":["../src/rc-disclosure.ts"],"sourcesContent":["export interface RCDisclosureToggleEvent {\n /** Whether the disclosure is now open. */\n open: boolean;\n}\n\nconst TOGGLE_EVENT = 'rc-disclosure-toggle';\nconst DETAILS_SELECTOR = ':scope > details';\nconst SUMMARY_SELECTOR = ':scope > summary';\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'rc-disclosure': RCDisclosure;\n }\n}\n\n/**\n * Disclosure wrapper for a native <details>/<summary> pair with controlled open state,\n * following the WAI-ARIA Disclosure pattern.\n *\n * The browser keeps ownership of open/close behavior, keyboard support, and\n * accessibility. This element mirrors state, emits a consistent custom event,\n * and automatically opens when the URL hash matches any id within its subtree.\n *\n * @see {@link https://richardcarls.github.io/rc-webcomponents/components/rc-disclosure rc-disclosure docs}\n * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/ WAI-ARIA Disclosure pattern}\n *\n * @fires rc-disclosure-toggle - Fires when the child `<details>` toggles.\n *\n * @attr open - Current open state mirrored to the child `<details>`.\n */\nexport class RCDisclosure extends HTMLElement {\n private _observer = new MutationObserver(() => this._setupDetails());\n\n private _$details: HTMLDetailsElement | null = null;\n\n private _scrollFrame = 0;\n\n static get observedAttributes(): string[] {\n return ['open'];\n }\n\n /** Current open state mirrored to the child `<details>`. */\n get open(): boolean {\n return this._$details?.open ?? this.hasAttribute('open');\n }\n\n set open(value: boolean) {\n this._setOpen(value, true);\n }\n\n connectedCallback(): void {\n this._observer.observe(this, { childList: true });\n window.addEventListener('hashchange', this._onHashChange);\n this._setupDetails();\n }\n\n disconnectedCallback(): void {\n this._observer.disconnect();\n window.removeEventListener('hashchange', this._onHashChange);\n cancelAnimationFrame(this._scrollFrame);\n this._teardownDetails();\n }\n\n attributeChangedCallback(_name: string, oldValue: string | null, newValue: string | null): void {\n if (oldValue === newValue) {\n return;\n }\n\n this._syncDetailsOpen();\n }\n\n private _setupDetails(): void {\n const $details = this.querySelector<HTMLDetailsElement>(DETAILS_SELECTOR);\n if ($details === this._$details) {\n return;\n }\n\n this._teardownDetails();\n this._$details = $details;\n\n if (!$details) {\n if (import.meta.env.DEV) {\n console.warn(\n '[rc-disclosure] No direct child <details> found. Place native ' +\n '<details><summary>...</summary>...</details> inside <rc-disclosure>.',\n this,\n );\n }\n\n return;\n }\n\n if (!$details.id) {\n $details.id = crypto.randomUUID();\n }\n\n const $summary = $details.querySelector<HTMLElement>(SUMMARY_SELECTOR);\n $summary?.setAttribute('aria-controls', $details.id);\n\n $details.addEventListener('toggle', this._onToggle);\n\n // If the host hasn't been given an explicit initial `open` of its own,\n // adopt whatever the wrapped <details> markup already says instead of\n // silently forcing it closed on connect — plain `<details open>`\n // should still behave the way it always has when nothing overrides it.\n if (!this.hasAttribute('open') && $details.open) {\n this._reflectOpen(true);\n }\n\n this._syncDetailsOpen();\n this._reflectOpen($details.open);\n this._openForCurrentHash();\n }\n\n private _teardownDetails(): void {\n const $summary = this._$details?.querySelector<HTMLElement>(SUMMARY_SELECTOR);\n $summary?.removeAttribute('aria-controls');\n\n this._$details?.removeEventListener('toggle', this._onToggle);\n this._$details = null;\n }\n\n private _syncDetailsOpen(): void {\n const $details = this._$details;\n if (!$details) {\n return;\n }\n\n const nextOpen = this.hasAttribute('open');\n if ($details.open !== nextOpen) {\n $details.open = nextOpen;\n }\n }\n\n private _setOpen(value: boolean, reflect: boolean): void {\n const $details = this._$details;\n if ($details && $details.open !== value) {\n $details.open = value;\n }\n\n // _reflectOpen is called here for programmatic opens because the <details>\n // toggle event is async (queued task per HTML spec). _onToggle also calls\n // _reflectOpen for user-initiated clicks, but that arrives after a tick.\n if (reflect) {\n this._reflectOpen(value);\n }\n }\n\n private _reflectOpen(value: boolean): void {\n if (value) {\n if (!this.hasAttribute('open')) {\n this.setAttribute('open', '');\n }\n\n return;\n }\n\n if (this.hasAttribute('open')) {\n this.removeAttribute('open');\n }\n }\n\n private _onToggle = (): void => {\n const open = this._$details?.open ?? false;\n\n this._reflectOpen(open);\n this.dispatchEvent(\n new CustomEvent<RCDisclosureToggleEvent>(TOGGLE_EVENT, {\n bubbles: true,\n composed: true,\n detail: { open },\n }),\n );\n };\n\n private _onHashChange = (): void => {\n this._openForCurrentHash();\n };\n\n private _openForCurrentHash(): void {\n if (!location.hash) {\n return;\n }\n\n const targetId = decodeURIComponent(location.hash.slice(1));\n if (!targetId) {\n return;\n }\n\n const $target = this.querySelector(`#${CSS.escape(targetId)}`);\n if (!$target) {\n return;\n }\n\n const wasOpen = this.open;\n\n this.open = true;\n\n if (wasOpen) {\n return;\n }\n\n cancelAnimationFrame(this._scrollFrame);\n this._scrollFrame = requestAnimationFrame(() => {\n document.getElementById(targetId)?.scrollIntoView();\n });\n }\n}\n\nexport default RCDisclosure;\n"],"names":["TOGGLE_EVENT","DETAILS_SELECTOR","SUMMARY_SELECTOR","RCDisclosure","open","value","_name","oldValue","newValue","$details","nextOpen","reflect","targetId","wasOpen"],"mappings":"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"}
1
+ {"version":3,"file":"rc-disclosure.js","sources":["../src/rc-disclosure.ts"],"sourcesContent":["export interface RCDisclosureToggleEvent {\n /** Whether the disclosure is now open. */\n open: boolean;\n}\n\nconst TOGGLE_EVENT = 'rc-disclosure-toggle';\nconst DETAILS_SELECTOR = ':scope > details';\nconst SUMMARY_SELECTOR = ':scope > summary';\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'rc-disclosure': RCDisclosure;\n }\n}\n\n/**\n * Disclosure wrapper for a native <details>/<summary> pair with controlled and uncontrolled\n * open state,\n * following the WAI-ARIA Disclosure pattern.\n *\n * The browser keeps ownership of open/close behavior, keyboard support, and\n * accessibility. This element mirrors state, emits a consistent custom event,\n * and automatically opens when the URL hash matches any id within its subtree.\n *\n * @see {@link https://richardcarls.github.io/rc-webcomponents/components/rc-disclosure rc-disclosure docs}\n * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/ WAI-ARIA Disclosure pattern}\n *\n * @fires rc-disclosure-toggle - Fires when the child `<details>` toggles.\n *\n * @attr open - Controlled open state mirrored to the child `<details>`.\n * @attr default-open - Initial open state for uncontrolled usage.\n */\nexport class RCDisclosure extends HTMLElement {\n private _observer = new MutationObserver(() => this._setupDetails());\n\n private _$details: HTMLDetailsElement | null = null;\n\n private _scrollFrame = 0;\n private _controlledOpen: boolean | undefined;\n private _defaultOpen = false;\n private _defaultOpenInitialized = false;\n private _uncontrolledOpen: boolean | undefined;\n private _openInitialized = false;\n private _reflectingOpen = false;\n private _suppressNextToggle = false;\n\n static get observedAttributes(): string[] {\n return ['open', 'default-open'];\n }\n\n /** Current open state. Host writes are silent; assign `undefined` to release control. */\n get open(): boolean {\n return this._controlledOpen ?? this._uncontrolledOpen ?? this._defaultOpen;\n }\n\n set open(value: boolean | undefined) {\n this._controlledOpen = value;\n this._openInitialized = true;\n this._applyOpen(this.open);\n this._reflectOpen(this.open);\n }\n\n /** Initial open state for uncontrolled usage. */\n get defaultOpen(): boolean {\n return this._defaultOpen;\n }\n\n set defaultOpen(value: boolean) {\n this._defaultOpen = value;\n this._defaultOpenInitialized = true;\n\n if (\n !this._openInitialized &&\n this._controlledOpen === undefined &&\n this._uncontrolledOpen === undefined\n ) {\n this._applyOpen(value);\n this._reflectOpen(value);\n }\n }\n\n connectedCallback(): void {\n this._observer.observe(this, { childList: true });\n window.addEventListener('hashchange', this._onHashChange);\n this._setupDetails();\n }\n\n disconnectedCallback(): void {\n this._observer.disconnect();\n window.removeEventListener('hashchange', this._onHashChange);\n cancelAnimationFrame(this._scrollFrame);\n this._teardownDetails();\n }\n\n attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {\n if (oldValue === newValue) {\n return;\n }\n\n if (name === 'default-open') {\n this.defaultOpen = newValue !== null;\n\n return;\n }\n\n if (this._reflectingOpen) {\n return;\n }\n\n this.open = newValue === null ? false : true;\n }\n\n private _setupDetails(): void {\n const $details = this.querySelector<HTMLDetailsElement>(DETAILS_SELECTOR);\n\n if ($details === this._$details) {\n return;\n }\n\n this._teardownDetails();\n this._$details = $details;\n\n if (!$details) {\n if (import.meta.env.DEV) {\n console.warn(\n '[rc-disclosure] No direct child <details> found. Place native ' +\n '<details><summary>...</summary>...</details> inside <rc-disclosure>.',\n this,\n );\n }\n\n return;\n }\n\n if (!$details.id) {\n $details.id = crypto.randomUUID();\n }\n\n const $summary = $details.querySelector<HTMLElement>(SUMMARY_SELECTOR);\n\n $summary?.setAttribute('aria-controls', $details.id);\n\n $details.addEventListener('toggle', this._onToggle);\n\n if (this._controlledOpen === undefined && this._uncontrolledOpen === undefined) {\n this._uncontrolledOpen = this._defaultOpenInitialized ? this._defaultOpen : $details.open;\n }\n\n this._applyOpen(this.open, true);\n this._reflectOpen(this.open);\n this._openForCurrentHash();\n }\n\n private _teardownDetails(): void {\n const $summary = this._$details?.querySelector<HTMLElement>(SUMMARY_SELECTOR);\n\n $summary?.removeAttribute('aria-controls');\n\n this._$details?.removeEventListener('toggle', this._onToggle);\n this._$details = null;\n }\n\n private _applyOpen(open: boolean, suppressToggle = true): void {\n const $details = this._$details;\n\n if (!$details) {\n return;\n }\n\n if ($details.open !== open) {\n this._suppressNextToggle = suppressToggle;\n $details.open = open;\n }\n }\n\n private _reflectOpen(value: boolean): void {\n if (this.hasAttribute('open') === value) {\n return;\n }\n\n this._reflectingOpen = true;\n this.toggleAttribute('open', value);\n this._reflectingOpen = false;\n }\n\n private _onToggle = (): void => {\n if (this._suppressNextToggle) {\n this._suppressNextToggle = false;\n\n return;\n }\n\n const requestedOpen = this._$details?.open ?? false;\n\n if (this._controlledOpen === undefined) {\n this._uncontrolledOpen = requestedOpen;\n this._reflectOpen(requestedOpen);\n } else {\n this._applyOpen(this._controlledOpen, true);\n this._reflectOpen(this._controlledOpen);\n }\n\n this.dispatchEvent(\n new CustomEvent<RCDisclosureToggleEvent>(TOGGLE_EVENT, {\n bubbles: true,\n composed: true,\n detail: { open: requestedOpen },\n }),\n );\n };\n\n private _onHashChange = (): void => {\n this._openForCurrentHash();\n };\n\n private _openForCurrentHash(): void {\n if (!location.hash) {\n return;\n }\n\n const targetId = decodeURIComponent(location.hash.slice(1));\n\n if (!targetId) {\n return;\n }\n\n const $target = this.querySelector(`#${CSS.escape(targetId)}`);\n\n if (!$target) {\n return;\n }\n\n const wasOpen = this.open;\n\n if (this._controlledOpen === undefined) {\n this._uncontrolledOpen = true;\n this._applyOpen(true);\n this._reflectOpen(true);\n } else {\n this.dispatchEvent(\n new CustomEvent<RCDisclosureToggleEvent>(TOGGLE_EVENT, {\n bubbles: true,\n composed: true,\n detail: { open: true },\n }),\n );\n }\n\n if (!this.open) {\n return;\n }\n\n if (wasOpen) {\n return;\n }\n\n cancelAnimationFrame(this._scrollFrame);\n\n this._scrollFrame = requestAnimationFrame(() => {\n document.getElementById(targetId)?.scrollIntoView();\n });\n }\n}\n\nexport default RCDisclosure;\n"],"names":["TOGGLE_EVENT","DETAILS_SELECTOR","SUMMARY_SELECTOR","RCDisclosure","requestedOpen","value","name","oldValue","newValue","$details","open","suppressToggle","targetId","wasOpen"],"mappings":"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"}
@@ -8,7 +8,8 @@ declare global {
8
8
  }
9
9
  }
10
10
  /**
11
- * Disclosure wrapper for a native <details>/<summary> pair with controlled open state,
11
+ * Disclosure wrapper for a native <details>/<summary> pair with controlled and uncontrolled
12
+ * open state,
12
13
  * following the WAI-ARIA Disclosure pattern.
13
14
  *
14
15
  * The browser keeps ownership of open/close behavior, keyboard support, and
@@ -20,23 +21,33 @@ declare global {
20
21
  *
21
22
  * @fires rc-disclosure-toggle - Fires when the child `<details>` toggles.
22
23
  *
23
- * @attr open - Current open state mirrored to the child `<details>`.
24
+ * @attr open - Controlled open state mirrored to the child `<details>`.
25
+ * @attr default-open - Initial open state for uncontrolled usage.
24
26
  */
25
27
  export declare class RCDisclosure extends HTMLElement {
26
28
  private _observer;
27
29
  private _$details;
28
30
  private _scrollFrame;
31
+ private _controlledOpen;
32
+ private _defaultOpen;
33
+ private _defaultOpenInitialized;
34
+ private _uncontrolledOpen;
35
+ private _openInitialized;
36
+ private _reflectingOpen;
37
+ private _suppressNextToggle;
29
38
  static get observedAttributes(): string[];
30
- /** Current open state mirrored to the child `<details>`. */
39
+ /** Current open state. Host writes are silent; assign `undefined` to release control. */
31
40
  get open(): boolean;
32
- set open(value: boolean);
41
+ set open(value: boolean | undefined);
42
+ /** Initial open state for uncontrolled usage. */
43
+ get defaultOpen(): boolean;
44
+ set defaultOpen(value: boolean);
33
45
  connectedCallback(): void;
34
46
  disconnectedCallback(): void;
35
- attributeChangedCallback(_name: string, oldValue: string | null, newValue: string | null): void;
47
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
36
48
  private _setupDetails;
37
49
  private _teardownDetails;
38
- private _syncDetailsOpen;
39
- private _setOpen;
50
+ private _applyOpen;
40
51
  private _reflectOpen;
41
52
  private _onToggle;
42
53
  private _onHashChange;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.5.0",
6
+ "version": "0.6.0",
7
7
  "description": "Disclosure wrapper for a native <details>/<summary> pair with controlled open state.",
8
8
  "repository": {
9
9
  "type": "git",