@magic-spells/collapsible-content 0.2.0 → 1.1.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.
@@ -4,11 +4,37 @@
4
4
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.CollapsibleContent = {}));
5
5
  })(this, (function (exports) { 'use strict';
6
6
 
7
+ const DEFAULT_SPEED = 900; // px per second
8
+ const MIN_DURATION = 0.25; // seconds
9
+ const MAX_DURATION = 1.0; // seconds
10
+
7
11
  /**
8
12
  * Custom element that creates a collapsible/expandable component with proper accessibility
9
13
  */
10
14
  class CollapsibleComponent extends HTMLElement {
15
+ static #groups = new Map();
16
+
17
+ static #addToGroup(name, instance) {
18
+ if (!CollapsibleComponent.#groups.has(name)) {
19
+ CollapsibleComponent.#groups.set(name, new Set());
20
+ }
21
+ CollapsibleComponent.#groups.get(name).add(instance);
22
+ }
23
+
24
+ static #removeFromGroup(name, instance) {
25
+ const group = CollapsibleComponent.#groups.get(name);
26
+ if (group) {
27
+ group.delete(instance);
28
+ if (group.size === 0) CollapsibleComponent.#groups.delete(name);
29
+ }
30
+ }
31
+
32
+ static get observedAttributes() {
33
+ return ['group'];
34
+ }
35
+
11
36
  #handleClick;
37
+ #abortController;
12
38
 
13
39
  /**
14
40
  * Initializes the component and sets up references to child elements
@@ -21,13 +47,12 @@
21
47
  _.button = null;
22
48
  _.content = null;
23
49
 
24
- // define click handler with proper this binding
25
- _.#handleClick = (e) => {
26
- e.preventDefault();
27
- // toggle expanded value
28
- const expanded = _.button.getAttribute('aria-expanded') !== 'true';
29
- _.button.setAttribute('aria-expanded', expanded);
30
- _.content.hidden = !expanded;
50
+ // define click handler content is source of truth
51
+ _.#handleClick = () => {
52
+ const wasCollapsed = _.content.collapsed;
53
+ _.content.collapsed = !wasCollapsed;
54
+ _.button.setAttribute('aria-expanded', !_.content.collapsed);
55
+ if (wasCollapsed) _.#closeSiblings();
31
56
  };
32
57
  }
33
58
 
@@ -43,7 +68,16 @@
43
68
  _.content = _.querySelector('collapsible-content');
44
69
 
45
70
  if (!_.button || !_.content) {
46
- console.error('CollapsibleComponent requires a <button> and a <collapsible-content>.');
71
+ const error = new Error(
72
+ 'CollapsibleComponent requires a <button> and a <collapsible-content>.'
73
+ );
74
+ console.error(error.message);
75
+ _.dispatchEvent(
76
+ new CustomEvent('collapsible-error', {
77
+ bubbles: true,
78
+ detail: { error },
79
+ })
80
+ );
47
81
  return;
48
82
  }
49
83
 
@@ -52,18 +86,38 @@
52
86
  _.content.id ||= `collapsible-content-${crypto.randomUUID().slice(0, 8)}`;
53
87
 
54
88
  // set accessibility attributes
89
+ if (!_.button.hasAttribute('type')) {
90
+ _.button.type = 'button';
91
+ }
55
92
  _.button.setAttribute('aria-controls', _.content.id);
56
- _.content.setAttribute('role', 'region');
57
93
  _.content.setAttribute('aria-labelledby', _.button.id);
94
+ if (!_.content.hasAttribute('role')) {
95
+ _.content.setAttribute('role', 'region');
96
+ }
58
97
 
59
- // set initial state based on aria-expanded attribute
60
- const expanded = _.button.getAttribute('aria-expanded') === 'true';
98
+ // set initial state without triggering an opening/closing animation
99
+ const open = _.content.hasAttribute('open');
100
+ _.button.setAttribute('aria-expanded', open);
101
+ _.content.style.height = open ? 'auto' : '0px';
102
+ if (open) {
103
+ _.content.removeAttribute('aria-hidden');
104
+ _.content.removeAttribute('inert');
105
+ } else {
106
+ _.content.setAttribute('aria-hidden', 'true');
107
+ _.content.setAttribute('inert', '');
108
+ }
61
109
 
62
- // make sure content state matches button state
63
- _.content.hidden = !expanded;
110
+ // use AbortController to prevent duplicate listeners on reconnection
111
+ _.#abortController = new AbortController();
112
+ _.button.addEventListener('click', _.#handleClick, {
113
+ signal: _.#abortController.signal,
114
+ });
115
+ }
64
116
 
65
- // add event listener
66
- _.button.addEventListener('click', _.#handleClick);
117
+ attributeChangedCallback(name, oldValue, newValue) {
118
+ if (name !== 'group') return;
119
+ if (oldValue) CollapsibleComponent.#removeFromGroup(oldValue, this);
120
+ if (newValue) CollapsibleComponent.#addToGroup(newValue, this);
67
121
  }
68
122
 
69
123
  /**
@@ -72,8 +126,26 @@
72
126
  */
73
127
  disconnectedCallback() {
74
128
  const _ = this;
75
- if (_.button) {
76
- _.button.removeEventListener('click', _.#handleClick);
129
+ const group = _.getAttribute('group');
130
+ if (group) CollapsibleComponent.#removeFromGroup(group, _);
131
+ if (_.#abortController) {
132
+ _.#abortController.abort();
133
+ _.#abortController = null;
134
+ }
135
+ }
136
+
137
+ #closeSiblings() {
138
+ const _ = this;
139
+ const groupName = _.getAttribute('group');
140
+ if (!groupName) return;
141
+ const group = CollapsibleComponent.#groups.get(groupName);
142
+ if (!group) return;
143
+ for (const sibling of group) {
144
+ if (sibling === _) continue;
145
+ if (!sibling.content.collapsed) {
146
+ sibling.content.collapsed = true;
147
+ sibling.button.setAttribute('aria-expanded', 'false');
148
+ }
77
149
  }
78
150
  }
79
151
  }
@@ -83,6 +155,8 @@
83
155
  */
84
156
  class CollapsibleContent extends HTMLElement {
85
157
  #handleTransitionEnd;
158
+ #abortController;
159
+ #animating = false;
86
160
 
87
161
  /**
88
162
  * Initializes the content element and binds event handlers
@@ -93,36 +167,32 @@
93
167
 
94
168
  // define event handler using arrow function for proper binding
95
169
  _.#handleTransitionEnd = (event) => {
96
- // exit if isn't height property
97
- if (event.propertyName != 'height') return;
170
+ if (event.target !== _) return;
171
+ if (event.propertyName !== 'height') return;
172
+
173
+ _.#animating = false;
174
+ _.style.removeProperty('--collapsible-duration');
98
175
 
99
176
  // remove the inline height to allow dynamic content changes
100
- if (!_.hidden) {
177
+ if (!_.collapsed) {
101
178
  _.style.height = 'auto';
102
179
  }
103
180
  };
104
-
105
- // add event listener to remove height after transition
106
- _.addEventListener('transitionend', _.#handleTransitionEnd);
107
181
  }
108
182
 
109
183
  /**
110
184
  * Called when element is added to the DOM
111
- * Sets initial height based on hidden state
185
+ * Sets initial height based on open attribute
112
186
  */
113
187
  connectedCallback() {
114
188
  const _ = this;
189
+ _.style.height = _.hasAttribute('open') ? 'auto' : '0';
115
190
 
116
- // get aria-expanded state from button
117
- const component = _.closest('collapsible-component');
118
- const button = component.querySelector('button');
119
- const expanded = button.getAttribute('aria-expanded') === 'true';
120
-
121
- if (expanded) {
122
- _.style.height = 'auto';
123
- } else {
124
- _.style.height = '0';
125
- }
191
+ // use AbortController to prevent duplicate listeners on reconnection
192
+ _.#abortController = new AbortController();
193
+ _.addEventListener('transitionend', _.#handleTransitionEnd, {
194
+ signal: _.#abortController.signal,
195
+ });
126
196
  }
127
197
 
128
198
  /**
@@ -131,57 +201,106 @@
131
201
  */
132
202
  disconnectedCallback() {
133
203
  const _ = this;
134
- _.removeEventListener('transitionend', _.#handleTransitionEnd);
204
+ _.#animating = false;
205
+ if (_.#abortController) {
206
+ _.#abortController.abort();
207
+ _.#abortController = null;
208
+ }
209
+ }
210
+
211
+ get #speed() {
212
+ const attr = this.getAttribute('speed');
213
+ if (attr === null) return DEFAULT_SPEED;
214
+ const value = Number(attr);
215
+ return value > 0 ? value : DEFAULT_SPEED;
216
+ }
217
+
218
+ get #minDuration() {
219
+ const attr = this.getAttribute('min-duration');
220
+ if (attr === null) return MIN_DURATION;
221
+ const value = Number(attr);
222
+ return value > 0 ? value : MIN_DURATION;
223
+ }
224
+
225
+ get #maxDuration() {
226
+ const attr = this.getAttribute('max-duration');
227
+ if (attr === null) return MAX_DURATION;
228
+ const value = Number(attr);
229
+ return value > 0 ? value : MAX_DURATION;
230
+ }
231
+
232
+ #setDynamicDuration(currentHeight, targetHeight) {
233
+ const _ = this;
234
+ const delta = Math.abs(targetHeight - currentHeight);
235
+ const duration = Math.min(_.#maxDuration, Math.max(_.#minDuration, delta / _.#speed));
236
+ _.style.setProperty('--collapsible-duration', `${duration.toFixed(3)}s`);
135
237
  }
136
238
 
137
239
  /**
138
- * Handles setting the hidden attribute with animation
139
- * @param {boolean} value - Whether element should be hidden
240
+ * Handles setting the collapsed state with animation
241
+ * @param {boolean} value - Whether element should be collapsed
140
242
  */
141
- set hidden(value) {
243
+ set collapsed(value) {
142
244
  const _ = this;
245
+ const collapsed = Boolean(value);
246
+ if (_.collapsed === collapsed) return;
143
247
 
144
- // animate to closed
145
- if (value) {
146
- // reset height to animate from
147
- _.style.height = `${_.scrollHeight}px`;
148
-
149
- // wait one frame and animate to 0
150
- setTimeout(() => {
151
- _.style.height = '0px';
152
- }, 1);
248
+ // check if transitions are enabled (respects prefers-reduced-motion)
249
+ const hasTransition = getComputedStyle(_).transitionDuration !== '0s';
153
250
 
154
- // set accessibility attributes
251
+ if (collapsed) {
252
+ _.removeAttribute('open');
155
253
  _.setAttribute('aria-hidden', 'true');
156
254
  _.setAttribute('inert', '');
157
- _.setAttribute('hidden', '');
158
- } else {
159
- // only animate if not set to auto
160
- if (_.style.height !== 'auto') {
161
- // animate to open
162
- _.style.height = `${_.scrollHeight}px`;
255
+
256
+ if (!hasTransition) {
257
+ _.style.height = '0px';
258
+ return;
163
259
  }
164
260
 
165
- // remove accessibility attributes
261
+ // capture current height in px (converts auto or mid-animation value)
262
+ const currentHeight = _.getBoundingClientRect().height;
263
+ _.style.height = `${currentHeight}px`;
264
+ _.#setDynamicDuration(currentHeight, 0);
265
+ _.#animating = true;
266
+ _.offsetHeight; // force reflow — browser commits the px start value
267
+ _.style.height = '0px';
268
+ } else {
269
+ _.setAttribute('open', '');
166
270
  _.removeAttribute('aria-hidden');
167
271
  _.removeAttribute('inert');
168
- _.removeAttribute('hidden');
272
+
273
+ if (!hasTransition) {
274
+ _.style.height = 'auto';
275
+ return;
276
+ }
277
+
278
+ // capture current height in px (0px or mid-animation value)
279
+ const currentHeight = _.getBoundingClientRect().height;
280
+ _.style.height = `${currentHeight}px`;
281
+ _.#setDynamicDuration(currentHeight, _.scrollHeight);
282
+ _.#animating = true;
283
+ _.offsetHeight; // force reflow — browser commits the px start value
284
+ _.style.height = `${_.scrollHeight}px`;
169
285
  }
170
286
  }
171
287
 
172
288
  /**
173
- * Gets the hidden state from the attribute
174
- * @returns {boolean} Whether element is hidden
289
+ * Gets the collapsed state
290
+ * @returns {boolean} Whether element is collapsed
175
291
  */
176
- get hidden() {
177
- const _ = this;
178
- return _.hasAttribute('hidden');
292
+ get collapsed() {
293
+ return !this.hasAttribute('open');
179
294
  }
180
295
  }
181
296
 
182
- // register custom elements
183
- customElements.define('collapsible-content', CollapsibleContent);
184
- customElements.define('collapsible-component', CollapsibleComponent);
297
+ // register custom elements if not already defined
298
+ if (!customElements.get('collapsible-content')) {
299
+ customElements.define('collapsible-content', CollapsibleContent);
300
+ }
301
+ if (!customElements.get('collapsible-component')) {
302
+ customElements.define('collapsible-component', CollapsibleComponent);
303
+ }
185
304
 
186
305
  exports.CollapsibleComponent = CollapsibleComponent;
187
306
  exports.CollapsibleContent = CollapsibleContent;
@@ -1 +1 @@
1
- {"version":3,"file":"collapsible-content.js","sources":["../src/collapsible-content.js"],"sourcesContent":["import './index.scss';\n\n/**\n * Custom element that creates a collapsible/expandable component with proper accessibility\n */\nclass CollapsibleComponent extends HTMLElement {\n\t#handleClick;\n\n\t/**\n\t * Initializes the component and sets up references to child elements\n\t */\n\tconstructor() {\n\t\tsuper();\n\t\tconst _ = this;\n\n\t\t// store references to elements once to avoid re-querying\n\t\t_.button = null;\n\t\t_.content = null;\n\n\t\t// define click handler with proper this binding\n\t\t_.#handleClick = (e) => {\n\t\t\te.preventDefault();\n\t\t\t// toggle expanded value\n\t\t\tconst expanded = _.button.getAttribute('aria-expanded') !== 'true';\n\t\t\t_.button.setAttribute('aria-expanded', expanded);\n\t\t\t_.content.hidden = !expanded;\n\t\t};\n\t}\n\n\t/**\n\t * Called when element is added to the DOM\n\t * Sets up accessibility attributes and event listeners\n\t */\n\tconnectedCallback() {\n\t\tconst _ = this;\n\n\t\t// initialize element references once\n\t\t_.button = _.querySelector('button');\n\t\t_.content = _.querySelector('collapsible-content');\n\n\t\tif (!_.button || !_.content) {\n\t\t\tconsole.error('CollapsibleComponent requires a <button> and a <collapsible-content>.');\n\t\t\treturn;\n\t\t}\n\n\t\t// generate ids if not provided\n\t\t_.button.id ||= `collapsible-button-${crypto.randomUUID().slice(0, 8)}`;\n\t\t_.content.id ||= `collapsible-content-${crypto.randomUUID().slice(0, 8)}`;\n\n\t\t// set accessibility attributes\n\t\t_.button.setAttribute('aria-controls', _.content.id);\n\t\t_.content.setAttribute('role', 'region');\n\t\t_.content.setAttribute('aria-labelledby', _.button.id);\n\n\t\t// set initial state based on aria-expanded attribute\n\t\tconst expanded = _.button.getAttribute('aria-expanded') === 'true';\n\n\t\t// make sure content state matches button state\n\t\t_.content.hidden = !expanded;\n\n\t\t// add event listener\n\t\t_.button.addEventListener('click', _.#handleClick);\n\t}\n\n\t/**\n\t * Called when element is removed from the DOM\n\t * Cleans up event listeners\n\t */\n\tdisconnectedCallback() {\n\t\tconst _ = this;\n\t\tif (_.button) {\n\t\t\t_.button.removeEventListener('click', _.#handleClick);\n\t\t}\n\t}\n}\n\n/**\n * Custom element that provides animated collapsible content\n */\nclass CollapsibleContent extends HTMLElement {\n\t#handleTransitionEnd;\n\n\t/**\n\t * Initializes the content element and binds event handlers\n\t */\n\tconstructor() {\n\t\tsuper();\n\t\tconst _ = this;\n\n\t\t// define event handler using arrow function for proper binding\n\t\t_.#handleTransitionEnd = (event) => {\n\t\t\t// exit if isn't height property\n\t\t\tif (event.propertyName != 'height') return;\n\n\t\t\t// remove the inline height to allow dynamic content changes\n\t\t\tif (!_.hidden) {\n\t\t\t\t_.style.height = 'auto';\n\t\t\t}\n\t\t};\n\n\t\t// add event listener to remove height after transition\n\t\t_.addEventListener('transitionend', _.#handleTransitionEnd);\n\t}\n\n\t/**\n\t * Called when element is added to the DOM\n\t * Sets initial height based on hidden state\n\t */\n\tconnectedCallback() {\n\t\tconst _ = this;\n\n\t\t// get aria-expanded state from button\n\t\tconst component = _.closest('collapsible-component');\n\t\tconst button = component.querySelector('button');\n\t\tconst expanded = button.getAttribute('aria-expanded') === 'true';\n\n\t\tif (expanded) {\n\t\t\t_.style.height = 'auto';\n\t\t} else {\n\t\t\t_.style.height = '0';\n\t\t}\n\t}\n\n\t/**\n\t * Called when element is removed from the DOM\n\t * Cleans up event listeners\n\t */\n\tdisconnectedCallback() {\n\t\tconst _ = this;\n\t\t_.removeEventListener('transitionend', _.#handleTransitionEnd);\n\t}\n\n\t/**\n\t * Handles setting the hidden attribute with animation\n\t * @param {boolean} value - Whether element should be hidden\n\t */\n\tset hidden(value) {\n\t\tconst _ = this;\n\n\t\t// animate to closed\n\t\tif (value) {\n\t\t\t// reset height to animate from\n\t\t\t_.style.height = `${_.scrollHeight}px`;\n\n\t\t\t// wait one frame and animate to 0\n\t\t\tsetTimeout(() => {\n\t\t\t\t_.style.height = '0px';\n\t\t\t}, 1);\n\n\t\t\t// set accessibility attributes\n\t\t\t_.setAttribute('aria-hidden', 'true');\n\t\t\t_.setAttribute('inert', '');\n\t\t\t_.setAttribute('hidden', '');\n\t\t} else {\n\t\t\t// only animate if not set to auto\n\t\t\tif (_.style.height !== 'auto') {\n\t\t\t\t// animate to open\n\t\t\t\t_.style.height = `${_.scrollHeight}px`;\n\t\t\t}\n\n\t\t\t// remove accessibility attributes\n\t\t\t_.removeAttribute('aria-hidden');\n\t\t\t_.removeAttribute('inert');\n\t\t\t_.removeAttribute('hidden');\n\t\t}\n\t}\n\n\t/**\n\t * Gets the hidden state from the attribute\n\t * @returns {boolean} Whether element is hidden\n\t */\n\tget hidden() {\n\t\tconst _ = this;\n\t\treturn _.hasAttribute('hidden');\n\t}\n}\n\n// register custom elements\ncustomElements.define('collapsible-content', CollapsibleContent);\ncustomElements.define('collapsible-component', CollapsibleComponent);\n\nexport { CollapsibleContent, CollapsibleComponent };\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"collapsible-content.js","sources":["../src/collapsible-content.js"],"sourcesContent":["import './collapsible-content.css';\n\nconst DEFAULT_SPEED = 900; // px per second\nconst MIN_DURATION = 0.25; // seconds\nconst MAX_DURATION = 1.0; // seconds\n\n/**\n * Custom element that creates a collapsible/expandable component with proper accessibility\n */\nclass CollapsibleComponent extends HTMLElement {\n\tstatic #groups = new Map();\n\n\tstatic #addToGroup(name, instance) {\n\t\tif (!CollapsibleComponent.#groups.has(name)) {\n\t\t\tCollapsibleComponent.#groups.set(name, new Set());\n\t\t}\n\t\tCollapsibleComponent.#groups.get(name).add(instance);\n\t}\n\n\tstatic #removeFromGroup(name, instance) {\n\t\tconst group = CollapsibleComponent.#groups.get(name);\n\t\tif (group) {\n\t\t\tgroup.delete(instance);\n\t\t\tif (group.size === 0) CollapsibleComponent.#groups.delete(name);\n\t\t}\n\t}\n\n\tstatic get observedAttributes() {\n\t\treturn ['group'];\n\t}\n\n\t#handleClick;\n\t#abortController;\n\n\t/**\n\t * Initializes the component and sets up references to child elements\n\t */\n\tconstructor() {\n\t\tsuper();\n\t\tconst _ = this;\n\n\t\t// store references to elements once to avoid re-querying\n\t\t_.button = null;\n\t\t_.content = null;\n\n\t\t// define click handler — content is source of truth\n\t\t_.#handleClick = () => {\n\t\t\tconst wasCollapsed = _.content.collapsed;\n\t\t\t_.content.collapsed = !wasCollapsed;\n\t\t\t_.button.setAttribute('aria-expanded', !_.content.collapsed);\n\t\t\tif (wasCollapsed) _.#closeSiblings();\n\t\t};\n\t}\n\n\t/**\n\t * Called when element is added to the DOM\n\t * Sets up accessibility attributes and event listeners\n\t */\n\tconnectedCallback() {\n\t\tconst _ = this;\n\n\t\t// initialize element references once\n\t\t_.button = _.querySelector('button');\n\t\t_.content = _.querySelector('collapsible-content');\n\n\t\tif (!_.button || !_.content) {\n\t\t\tconst error = new Error(\n\t\t\t\t'CollapsibleComponent requires a <button> and a <collapsible-content>.'\n\t\t\t);\n\t\t\tconsole.error(error.message);\n\t\t\t_.dispatchEvent(\n\t\t\t\tnew CustomEvent('collapsible-error', {\n\t\t\t\t\tbubbles: true,\n\t\t\t\t\tdetail: { error },\n\t\t\t\t})\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\t// generate ids if not provided\n\t\t_.button.id ||= `collapsible-button-${crypto.randomUUID().slice(0, 8)}`;\n\t\t_.content.id ||= `collapsible-content-${crypto.randomUUID().slice(0, 8)}`;\n\n\t\t// set accessibility attributes\n\t\tif (!_.button.hasAttribute('type')) {\n\t\t\t_.button.type = 'button';\n\t\t}\n\t\t_.button.setAttribute('aria-controls', _.content.id);\n\t\t_.content.setAttribute('aria-labelledby', _.button.id);\n\t\tif (!_.content.hasAttribute('role')) {\n\t\t\t_.content.setAttribute('role', 'region');\n\t\t}\n\n\t\t// set initial state without triggering an opening/closing animation\n\t\tconst open = _.content.hasAttribute('open');\n\t\t_.button.setAttribute('aria-expanded', open);\n\t\t_.content.style.height = open ? 'auto' : '0px';\n\t\tif (open) {\n\t\t\t_.content.removeAttribute('aria-hidden');\n\t\t\t_.content.removeAttribute('inert');\n\t\t} else {\n\t\t\t_.content.setAttribute('aria-hidden', 'true');\n\t\t\t_.content.setAttribute('inert', '');\n\t\t}\n\n\t\t// use AbortController to prevent duplicate listeners on reconnection\n\t\t_.#abortController = new AbortController();\n\t\t_.button.addEventListener('click', _.#handleClick, {\n\t\t\tsignal: _.#abortController.signal,\n\t\t});\n\t}\n\n\tattributeChangedCallback(name, oldValue, newValue) {\n\t\tif (name !== 'group') return;\n\t\tif (oldValue) CollapsibleComponent.#removeFromGroup(oldValue, this);\n\t\tif (newValue) CollapsibleComponent.#addToGroup(newValue, this);\n\t}\n\n\t/**\n\t * Called when element is removed from the DOM\n\t * Cleans up event listeners\n\t */\n\tdisconnectedCallback() {\n\t\tconst _ = this;\n\t\tconst group = _.getAttribute('group');\n\t\tif (group) CollapsibleComponent.#removeFromGroup(group, _);\n\t\tif (_.#abortController) {\n\t\t\t_.#abortController.abort();\n\t\t\t_.#abortController = null;\n\t\t}\n\t}\n\n\t#closeSiblings() {\n\t\tconst _ = this;\n\t\tconst groupName = _.getAttribute('group');\n\t\tif (!groupName) return;\n\t\tconst group = CollapsibleComponent.#groups.get(groupName);\n\t\tif (!group) return;\n\t\tfor (const sibling of group) {\n\t\t\tif (sibling === _) continue;\n\t\t\tif (!sibling.content.collapsed) {\n\t\t\t\tsibling.content.collapsed = true;\n\t\t\t\tsibling.button.setAttribute('aria-expanded', 'false');\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Custom element that provides animated collapsible content\n */\nclass CollapsibleContent extends HTMLElement {\n\t#handleTransitionEnd;\n\t#abortController;\n\t#animating = false;\n\n\t/**\n\t * Initializes the content element and binds event handlers\n\t */\n\tconstructor() {\n\t\tsuper();\n\t\tconst _ = this;\n\n\t\t// define event handler using arrow function for proper binding\n\t\t_.#handleTransitionEnd = (event) => {\n\t\t\tif (event.target !== _) return;\n\t\t\tif (event.propertyName !== 'height') return;\n\n\t\t\t_.#animating = false;\n\t\t\t_.style.removeProperty('--collapsible-duration');\n\n\t\t\t// remove the inline height to allow dynamic content changes\n\t\t\tif (!_.collapsed) {\n\t\t\t\t_.style.height = 'auto';\n\t\t\t}\n\t\t};\n\t}\n\n\t/**\n\t * Called when element is added to the DOM\n\t * Sets initial height based on open attribute\n\t */\n\tconnectedCallback() {\n\t\tconst _ = this;\n\t\t_.style.height = _.hasAttribute('open') ? 'auto' : '0';\n\n\t\t// use AbortController to prevent duplicate listeners on reconnection\n\t\t_.#abortController = new AbortController();\n\t\t_.addEventListener('transitionend', _.#handleTransitionEnd, {\n\t\t\tsignal: _.#abortController.signal,\n\t\t});\n\t}\n\n\t/**\n\t * Called when element is removed from the DOM\n\t * Cleans up event listeners\n\t */\n\tdisconnectedCallback() {\n\t\tconst _ = this;\n\t\t_.#animating = false;\n\t\tif (_.#abortController) {\n\t\t\t_.#abortController.abort();\n\t\t\t_.#abortController = null;\n\t\t}\n\t}\n\n\tget #speed() {\n\t\tconst attr = this.getAttribute('speed');\n\t\tif (attr === null) return DEFAULT_SPEED;\n\t\tconst value = Number(attr);\n\t\treturn value > 0 ? value : DEFAULT_SPEED;\n\t}\n\n\tget #minDuration() {\n\t\tconst attr = this.getAttribute('min-duration');\n\t\tif (attr === null) return MIN_DURATION;\n\t\tconst value = Number(attr);\n\t\treturn value > 0 ? value : MIN_DURATION;\n\t}\n\n\tget #maxDuration() {\n\t\tconst attr = this.getAttribute('max-duration');\n\t\tif (attr === null) return MAX_DURATION;\n\t\tconst value = Number(attr);\n\t\treturn value > 0 ? value : MAX_DURATION;\n\t}\n\n\t#setDynamicDuration(currentHeight, targetHeight) {\n\t\tconst _ = this;\n\t\tconst delta = Math.abs(targetHeight - currentHeight);\n\t\tconst duration = Math.min(_.#maxDuration, Math.max(_.#minDuration, delta / _.#speed));\n\t\t_.style.setProperty('--collapsible-duration', `${duration.toFixed(3)}s`);\n\t}\n\n\t/**\n\t * Handles setting the collapsed state with animation\n\t * @param {boolean} value - Whether element should be collapsed\n\t */\n\tset collapsed(value) {\n\t\tconst _ = this;\n\t\tconst collapsed = Boolean(value);\n\t\tif (_.collapsed === collapsed) return;\n\n\t\t// check if transitions are enabled (respects prefers-reduced-motion)\n\t\tconst hasTransition = getComputedStyle(_).transitionDuration !== '0s';\n\n\t\tif (collapsed) {\n\t\t\t_.removeAttribute('open');\n\t\t\t_.setAttribute('aria-hidden', 'true');\n\t\t\t_.setAttribute('inert', '');\n\n\t\t\tif (!hasTransition) {\n\t\t\t\t_.style.height = '0px';\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// capture current height in px (converts auto or mid-animation value)\n\t\t\tconst currentHeight = _.getBoundingClientRect().height;\n\t\t\t_.style.height = `${currentHeight}px`;\n\t\t\t_.#setDynamicDuration(currentHeight, 0);\n\t\t\t_.#animating = true;\n\t\t\t_.offsetHeight; // force reflow — browser commits the px start value\n\t\t\t_.style.height = '0px';\n\t\t} else {\n\t\t\t_.setAttribute('open', '');\n\t\t\t_.removeAttribute('aria-hidden');\n\t\t\t_.removeAttribute('inert');\n\n\t\t\tif (!hasTransition) {\n\t\t\t\t_.style.height = 'auto';\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// capture current height in px (0px or mid-animation value)\n\t\t\tconst currentHeight = _.getBoundingClientRect().height;\n\t\t\t_.style.height = `${currentHeight}px`;\n\t\t\t_.#setDynamicDuration(currentHeight, _.scrollHeight);\n\t\t\t_.#animating = true;\n\t\t\t_.offsetHeight; // force reflow — browser commits the px start value\n\t\t\t_.style.height = `${_.scrollHeight}px`;\n\t\t}\n\t}\n\n\t/**\n\t * Gets the collapsed state\n\t * @returns {boolean} Whether element is collapsed\n\t */\n\tget collapsed() {\n\t\treturn !this.hasAttribute('open');\n\t}\n}\n\n// register custom elements if not already defined\nif (!customElements.get('collapsible-content')) {\n\tcustomElements.define('collapsible-content', CollapsibleContent);\n}\nif (!customElements.get('collapsible-component')) {\n\tcustomElements.define('collapsible-component', CollapsibleComponent);\n}\n\nexport { CollapsibleContent, CollapsibleComponent };\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- :root{--cc-component-display:block;--cc-button-width:100%;--cc-button-text-align:left;--cc-button-background:none;--cc-button-border:none;--cc-button-cursor:pointer;--cc-content-padding:0px;--cc-content-display:block;--cc-content-overflow:hidden;--cc-transition-duration:0.35s;--cc-transition-timing:ease-out}collapsible-component{display:var(--cc-component-display,block)}collapsible-component button{background:var(--cc-button-background,none);border:var(--cc-button-border,none);cursor:var(--cc-button-cursor,pointer);text-align:var(--cc-button-text-align,left);width:var(--cc-button-width,100%)}collapsible-component collapsible-content[hidden],collapsible-content{display:var(--cc-content-display,block)}collapsible-content{overflow:var(--cc-content-overflow,hidden);padding:var(--cc-content-padding,0);transition:height var(--cc-transition-duration,.35s) var(--cc-transition-timing,ease-out)}
1
+ collapsible-component{display:block}collapsible-component button{background:none;border:none;cursor:pointer;text-align:left;width:100%}collapsible-component button:focus-visible{outline:2px solid currentColor;outline-offset:2px}collapsible-content{--collapsible-duration:0.35s;--collapsible-easing:ease;display:block;overflow:hidden;transition:height var(--collapsible-duration) var(--collapsible-easing)}@media (prefers-reduced-motion:reduce){collapsible-content{transition:none}}
@@ -1 +1 @@
1
- !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).CollapsibleContent={})}(this,(function(t){"use strict";class CollapsibleComponent extends HTMLElement{#t;constructor(){super();const t=this;t.button=null,t.content=null,t.#t=e=>{e.preventDefault();const n="true"!==t.button.getAttribute("aria-expanded");t.button.setAttribute("aria-expanded",n),t.content.hidden=!n}}connectedCallback(){const t=this;if(t.button=t.querySelector("button"),t.content=t.querySelector("collapsible-content"),!t.button||!t.content)return void console.error("CollapsibleComponent requires a <button> and a <collapsible-content>.");t.button.id||=`collapsible-button-${crypto.randomUUID().slice(0,8)}`,t.content.id||=`collapsible-content-${crypto.randomUUID().slice(0,8)}`,t.button.setAttribute("aria-controls",t.content.id),t.content.setAttribute("role","region"),t.content.setAttribute("aria-labelledby",t.button.id);const e="true"===t.button.getAttribute("aria-expanded");t.content.hidden=!e,t.button.addEventListener("click",t.#t)}disconnectedCallback(){const t=this;t.button&&t.button.removeEventListener("click",t.#t)}}class CollapsibleContent extends HTMLElement{#e;constructor(){super();const t=this;t.#e=e=>{"height"==e.propertyName&&(t.hidden||(t.style.height="auto"))},t.addEventListener("transitionend",t.#e)}connectedCallback(){const t=this,e="true"===t.closest("collapsible-component").querySelector("button").getAttribute("aria-expanded");t.style.height=e?"auto":"0"}disconnectedCallback(){this.removeEventListener("transitionend",this.#e)}set hidden(t){const e=this;t?(e.style.height=`${e.scrollHeight}px`,setTimeout((()=>{e.style.height="0px"}),1),e.setAttribute("aria-hidden","true"),e.setAttribute("inert",""),e.setAttribute("hidden","")):("auto"!==e.style.height&&(e.style.height=`${e.scrollHeight}px`),e.removeAttribute("aria-hidden"),e.removeAttribute("inert"),e.removeAttribute("hidden"))}get hidden(){return this.hasAttribute("hidden")}}customElements.define("collapsible-content",CollapsibleContent),customElements.define("collapsible-component",CollapsibleComponent),t.CollapsibleComponent=CollapsibleComponent,t.CollapsibleContent=CollapsibleContent}));
1
+ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).CollapsibleContent={})}(this,(function(t){"use strict";class CollapsibleComponent extends HTMLElement{static#t=new Map;static#e(t,e){CollapsibleComponent.#t.has(t)||CollapsibleComponent.#t.set(t,new Set),CollapsibleComponent.#t.get(t).add(e)}static#o(t,e){const o=CollapsibleComponent.#t.get(t);o&&(o.delete(e),0===o.size&&CollapsibleComponent.#t.delete(t))}static get observedAttributes(){return["group"]}#n;#l;constructor(){super();const t=this;t.button=null,t.content=null,t.#n=()=>{const e=t.content.collapsed;t.content.collapsed=!e,t.button.setAttribute("aria-expanded",!t.content.collapsed),e&&t.#r()}}connectedCallback(){const t=this;if(t.button=t.querySelector("button"),t.content=t.querySelector("collapsible-content"),!t.button||!t.content){const e=new Error("CollapsibleComponent requires a <button> and a <collapsible-content>.");return console.error(e.message),void t.dispatchEvent(new CustomEvent("collapsible-error",{bubbles:!0,detail:{error:e}}))}t.button.id||=`collapsible-button-${crypto.randomUUID().slice(0,8)}`,t.content.id||=`collapsible-content-${crypto.randomUUID().slice(0,8)}`,t.button.hasAttribute("type")||(t.button.type="button"),t.button.setAttribute("aria-controls",t.content.id),t.content.setAttribute("aria-labelledby",t.button.id),t.content.hasAttribute("role")||t.content.setAttribute("role","region");const e=t.content.hasAttribute("open");t.button.setAttribute("aria-expanded",e),t.content.style.height=e?"auto":"0px",e?(t.content.removeAttribute("aria-hidden"),t.content.removeAttribute("inert")):(t.content.setAttribute("aria-hidden","true"),t.content.setAttribute("inert","")),t.#l=new AbortController,t.button.addEventListener("click",t.#n,{signal:t.#l.signal})}attributeChangedCallback(t,e,o){"group"===t&&(e&&CollapsibleComponent.#o(e,this),o&&CollapsibleComponent.#e(o,this))}disconnectedCallback(){const t=this,e=t.getAttribute("group");e&&CollapsibleComponent.#o(e,t),t.#l&&(t.#l.abort(),t.#l=null)}#r(){const t=this,e=t.getAttribute("group");if(!e)return;const o=CollapsibleComponent.#t.get(e);if(o)for(const e of o)e!==t&&(e.content.collapsed||(e.content.collapsed=!0,e.button.setAttribute("aria-expanded","false")))}}class CollapsibleContent extends HTMLElement{#i;#l;#s=!1;constructor(){super();const t=this;t.#i=e=>{e.target===t&&"height"===e.propertyName&&(t.#s=!1,t.style.removeProperty("--collapsible-duration"),t.collapsed||(t.style.height="auto"))}}connectedCallback(){const t=this;t.style.height=t.hasAttribute("open")?"auto":"0",t.#l=new AbortController,t.addEventListener("transitionend",t.#i,{signal:t.#l.signal})}disconnectedCallback(){const t=this;t.#s=!1,t.#l&&(t.#l.abort(),t.#l=null)}get#a(){const t=this.getAttribute("speed");if(null===t)return 900;const e=Number(t);return e>0?e:900}get#c(){const t=this.getAttribute("min-duration");if(null===t)return.25;const e=Number(t);return e>0?e:.25}get#u(){const t=this.getAttribute("max-duration");if(null===t)return 1;const e=Number(t);return e>0?e:1}#b(t,e){const o=this,n=Math.abs(e-t),l=Math.min(o.#u,Math.max(o.#c,n/o.#a));o.style.setProperty("--collapsible-duration",`${l.toFixed(3)}s`)}set collapsed(t){const e=this,o=Boolean(t);if(e.collapsed===o)return;const n="0s"!==getComputedStyle(e).transitionDuration;if(o){if(e.removeAttribute("open"),e.setAttribute("aria-hidden","true"),e.setAttribute("inert",""),!n)return void(e.style.height="0px");const t=e.getBoundingClientRect().height;e.style.height=`${t}px`,e.#b(t,0),e.#s=!0,e.offsetHeight,e.style.height="0px"}else{if(e.setAttribute("open",""),e.removeAttribute("aria-hidden"),e.removeAttribute("inert"),!n)return void(e.style.height="auto");const t=e.getBoundingClientRect().height;e.style.height=`${t}px`,e.#b(t,e.scrollHeight),e.#s=!0,e.offsetHeight,e.style.height=`${e.scrollHeight}px`}}get collapsed(){return!this.hasAttribute("open")}}customElements.get("collapsible-content")||customElements.define("collapsible-content",CollapsibleContent),customElements.get("collapsible-component")||customElements.define("collapsible-component",CollapsibleComponent),t.CollapsibleComponent=CollapsibleComponent,t.CollapsibleContent=CollapsibleContent}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magic-spells/collapsible-content",
3
- "version": "0.2.0",
3
+ "version": "1.1.0",
4
4
  "description": "Collapsible content web component",
5
5
  "author": "Cory Schulz",
6
6
  "license": "MIT",
@@ -9,7 +9,6 @@
9
9
  "module": "dist/collapsible-content.esm.js",
10
10
  "unpkg": "dist/collapsible-content.min.js",
11
11
  "style": "dist/collapsible-content.min.css",
12
- "sass": "dist/collapsible-content.scss",
13
12
  "exports": {
14
13
  ".": {
15
14
  "import": "./dist/collapsible-content.esm.js",
@@ -17,9 +16,7 @@
17
16
  "default": "./dist/collapsible-content.esm.js"
18
17
  },
19
18
  "./css": "./dist/collapsible-content.css",
20
- "./css/min": "./dist/collapsible-content.min.css",
21
- "./scss": "./dist/collapsible-content.scss",
22
- "./scss/*": "./dist/scss/*"
19
+ "./css/min": "./dist/collapsible-content.min.css"
23
20
  },
24
21
  "sideEffects": true,
25
22
  "repository": {
@@ -60,17 +57,16 @@
60
57
  "not ie <= 11"
61
58
  ],
62
59
  "devDependencies": {
63
- "@eslint/js": "^8.57.0",
60
+ "@eslint/js": "^9.38.0",
64
61
  "@rollup/plugin-node-resolve": "^15.2.3",
65
- "@rollup/plugin-terser": "^0.4.4",
66
- "eslint": "^8.0.0",
67
- "globals": "^13.24.0",
62
+ "@rollup/plugin-terser": "^1.0.0",
63
+ "eslint": "^9.38.0",
64
+ "globals": "^15.15.0",
68
65
  "postcss-lightningcss": "^1.0.1",
69
66
  "prettier": "^3.3.3",
70
67
  "rollup": "^3.0.0",
71
68
  "rollup-plugin-copy": "^3.5.0",
72
69
  "rollup-plugin-postcss": "^4.0.2",
73
- "rollup-plugin-serve": "^1.1.1",
74
- "sass": "^1.86.3"
70
+ "rollup-plugin-serve": "^1.1.1"
75
71
  }
76
72
  }
@@ -0,0 +1,31 @@
1
+ collapsible-component {
2
+ display: block;
3
+ }
4
+
5
+ collapsible-component button {
6
+ width: 100%;
7
+ text-align: left;
8
+ background: none;
9
+ border: none;
10
+ cursor: pointer;
11
+ }
12
+
13
+ collapsible-component button:focus-visible {
14
+ outline: 2px solid currentColor;
15
+ outline-offset: 2px;
16
+ }
17
+
18
+ collapsible-content {
19
+ --collapsible-duration: 0.35s;
20
+ --collapsible-easing: ease;
21
+
22
+ display: block;
23
+ overflow: hidden;
24
+ transition: height var(--collapsible-duration) var(--collapsible-easing);
25
+ }
26
+
27
+ @media (prefers-reduced-motion: reduce) {
28
+ collapsible-content {
29
+ transition: none;
30
+ }
31
+ }