@magic-spells/collapsible-content 0.1.3 → 0.1.4

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.
@@ -1,108 +1,176 @@
1
1
  'use strict';
2
2
 
3
+ /**
4
+ * custom element that creates a collapsible/expandable component with proper accessibility
5
+ */
3
6
  class CollapsibleComponent extends HTMLElement {
4
7
  #handleClick;
5
8
 
9
+ /**
10
+ * initializes the component and sets up references to child elements
11
+ */
6
12
  constructor() {
7
13
  super();
14
+
15
+ // store references to elements once to avoid re-querying
16
+ this.button = null;
17
+ this.content = null;
18
+
19
+ // define click handler with proper this binding
8
20
  this.#handleClick = (e) => {
9
21
  e.preventDefault();
10
- const button = this.querySelector('button');
11
- const content = this.querySelector('collapsible-content');
12
- const expanded =
13
- button.getAttribute('aria-expanded') !== 'true';
14
- button.setAttribute('aria-expanded', expanded);
15
- content.hidden = !expanded;
22
+ // toggle expanded value
23
+ const expanded = this.button.getAttribute('aria-expanded') !== 'true';
24
+ this.button.setAttribute('aria-expanded', expanded);
25
+ this.content.hidden = !expanded;
16
26
  };
17
27
  }
18
28
 
29
+ /**
30
+ * called when element is added to the dom
31
+ * sets up accessibility attributes and event listeners
32
+ */
19
33
  connectedCallback() {
20
- const button = this.querySelector('button');
21
- const content = this.querySelector('collapsible-content');
34
+ // initialize element references once
35
+ this.button = this.querySelector('button');
36
+ this.content = this.querySelector('collapsible-content');
22
37
 
23
- if (!button || !content) {
24
- console.error(
25
- 'CollapsibleComponent requires a <button> and a <collapsible-content>.'
26
- );
38
+ if (!this.button || !this.content) {
39
+ console.error('CollapsibleComponent requires a <button> and a <collapsible-content>.');
27
40
  return;
28
41
  }
29
42
 
30
- button.id ||= `collapsible-button-${Math.random().toString(36).slice(2, 9)}`;
31
- content.id ||= `collapsible-content-${Math.random().toString(36).slice(2, 9)}`;
43
+ // generate ids if not provided
44
+ this.button.id ||= `collapsible-button-${crypto.randomUUID().slice(0, 8)}`;
45
+ this.content.id ||= `collapsible-content-${crypto.randomUUID().slice(0, 8)}`;
32
46
 
33
- button.setAttribute('aria-controls', content.id);
34
- content.setAttribute('role', 'region');
35
- content.setAttribute('aria-labelledby', button.id);
47
+ // set accessibility attributes
48
+ this.button.setAttribute('aria-controls', this.content.id);
49
+ this.content.setAttribute('role', 'region');
50
+ this.content.setAttribute('aria-labelledby', this.button.id);
36
51
 
37
- const expanded = button.getAttribute('aria-expanded') === 'true';
38
- content.hidden = !expanded;
52
+ // set initial state based on aria-expanded attribute
53
+ const expanded = this.button.getAttribute('aria-expanded') === 'true';
39
54
 
40
- button.addEventListener('click', this.#handleClick);
55
+ // make shure content state matches button state
56
+ this.content.hidden = !expanded;
57
+
58
+ // add event listener
59
+ this.button.addEventListener('click', this.#handleClick);
41
60
  }
42
61
 
62
+ /**
63
+ * called when element is removed from the dom
64
+ * cleans up event listeners
65
+ */
43
66
  disconnectedCallback() {
44
- const button = this.querySelector('button');
45
- button?.removeEventListener('click', this.#handleClick);
67
+ if (this.button) {
68
+ this.button.removeEventListener('click', this.#handleClick);
69
+ }
46
70
  }
47
71
  }
48
72
 
73
+ /**
74
+ * custom element that provides animated collapsible content
75
+ */
49
76
  class CollapsibleContent extends HTMLElement {
77
+ /**
78
+ * initializes the content element and binds event handlers
79
+ */
50
80
  constructor() {
51
81
  super();
82
+
83
+ // bind event handler to this instance
52
84
  this._onTransitionEnd = this._onTransitionEnd.bind(this);
53
- // Add event listener to remove height after transition
85
+
86
+ // add event listener to remove height after transition
54
87
  this.addEventListener('transitionend', this._onTransitionEnd);
55
88
  }
56
89
 
90
+ /**
91
+ * called when element is added to the dom
92
+ * sets initial height based on hidden state
93
+ */
57
94
  connectedCallback() {
58
- if (this.hidden) {
59
- this.style.height = '0';
95
+ // get aria-expanded state from buton
96
+ const component = this.closest('collapsible-component');
97
+ const button = component.querySelector('button');
98
+ const expanded = button.getAttribute('aria-expanded') === 'true';
99
+
100
+ if (expanded) {
101
+ this.style.height = 'auto';
60
102
  } else {
61
- this.style.height = `${this.scrollHeight}px`;
103
+ this.style.height = '0';
62
104
  }
63
105
  }
64
106
 
107
+ /**
108
+ * called when element is removed from the dom
109
+ * cleans up event listeners
110
+ */
65
111
  disconnectedCallback() {
66
112
  this.removeEventListener('transitionend', this._onTransitionEnd);
67
113
  }
68
114
 
115
+ /**
116
+ * handles setting the hidden attribute with animation
117
+ * @param {boolean} value - whether element should be hidden
118
+ */
69
119
  set hidden(value) {
70
120
  // animate to closed
71
121
  if (value) {
72
122
  // reset height to animate from
73
123
  this.style.height = `${this.scrollHeight}px`;
124
+
74
125
  // wait one frame and animate to 0
75
126
  setTimeout(() => {
76
127
  this.style.height = '0px';
77
128
  }, 1);
78
129
 
130
+ // set accessibility attributes
79
131
  this.setAttribute('aria-hidden', 'true');
80
132
  this.setAttribute('inert', '');
81
133
  this.setAttribute('hidden', '');
82
134
  } else {
83
- // animate to open
84
- this.style.height = `${this.scrollHeight}px`;
135
+ // only animate if not set to auto
136
+ if (this.style.height !== 'auto') {
137
+ // animate to open
138
+ this.style.height = `${this.scrollHeight}px`;
139
+ }
140
+
141
+ // remove accessibility attributes
85
142
  this.removeAttribute('aria-hidden');
86
143
  this.removeAttribute('inert');
87
144
  this.removeAttribute('hidden');
88
145
  }
89
146
  }
90
147
 
148
+ /**
149
+ * gets the hidden state from the attribute
150
+ * @return {boolean} whether element is hidden
151
+ */
91
152
  get hidden() {
92
153
  return this.hasAttribute('hidden');
93
154
  }
94
155
 
156
+ /**
157
+ * handles the end of css transitions
158
+ * @param {TransitionEvent} event - the transition event
159
+ */
95
160
  _onTransitionEnd(event) {
161
+ console.log('on transition end');
162
+
96
163
  // exit if isn't height property
97
164
  if (event.propertyName != 'height') return;
98
165
 
99
- // Remove the inline height to allow dynamic content changes
166
+ // remove the inline height to allow dynamic content changes
100
167
  if (!this.hidden) {
101
168
  this.style.height = 'auto';
102
169
  }
103
170
  }
104
171
  }
105
172
 
173
+ // register custom elements
106
174
  customElements.define('collapsible-content', CollapsibleContent);
107
175
  customElements.define('collapsible-component', CollapsibleComponent);
108
176
 
@@ -1 +1 @@
1
- {"version":3,"file":"collapsible-content.cjs.js","sources":["../src/collapsible-content.js"],"sourcesContent":["import './styles.css';\n\nclass CollapsibleComponent extends HTMLElement {\n\t#handleClick;\n\n\tconstructor() {\n\t\tsuper();\n\t\tthis.#handleClick = (e) => {\n\t\t\te.preventDefault();\n\t\t\tconst button = this.querySelector('button');\n\t\t\tconst content = this.querySelector('collapsible-content');\n\t\t\tconst expanded =\n\t\t\t\tbutton.getAttribute('aria-expanded') !== 'true';\n\t\t\tbutton.setAttribute('aria-expanded', expanded);\n\t\t\tcontent.hidden = !expanded;\n\t\t};\n\t}\n\n\tconnectedCallback() {\n\t\tconst button = this.querySelector('button');\n\t\tconst content = this.querySelector('collapsible-content');\n\n\t\tif (!button || !content) {\n\t\t\tconsole.error(\n\t\t\t\t'CollapsibleComponent requires a <button> and a <collapsible-content>.'\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tbutton.id ||= `collapsible-button-${Math.random().toString(36).slice(2, 9)}`;\n\t\tcontent.id ||= `collapsible-content-${Math.random().toString(36).slice(2, 9)}`;\n\n\t\tbutton.setAttribute('aria-controls', content.id);\n\t\tcontent.setAttribute('role', 'region');\n\t\tcontent.setAttribute('aria-labelledby', button.id);\n\n\t\tconst expanded = button.getAttribute('aria-expanded') === 'true';\n\t\tcontent.hidden = !expanded;\n\n\t\tbutton.addEventListener('click', this.#handleClick);\n\t}\n\n\tdisconnectedCallback() {\n\t\tconst button = this.querySelector('button');\n\t\tbutton?.removeEventListener('click', this.#handleClick);\n\t}\n}\n\nclass CollapsibleContent extends HTMLElement {\n\tconstructor() {\n\t\tsuper();\n\t\tthis._onTransitionEnd = this._onTransitionEnd.bind(this);\n\t\t// Add event listener to remove height after transition\n\t\tthis.addEventListener('transitionend', this._onTransitionEnd);\n\t}\n\n\tconnectedCallback() {\n\t\tif (this.hidden) {\n\t\t\tthis.style.height = '0';\n\t\t} else {\n\t\t\tthis.style.height = `${this.scrollHeight}px`;\n\t\t}\n\t}\n\n\tdisconnectedCallback() {\n\t\tthis.removeEventListener('transitionend', this._onTransitionEnd);\n\t}\n\n\tset hidden(value) {\n\t\t// animate to closed\n\t\tif (value) {\n\t\t\t// reset height to animate from\n\t\t\tthis.style.height = `${this.scrollHeight}px`;\n\t\t\t// wait one frame and animate to 0\n\t\t\tsetTimeout(() => {\n\t\t\t\tthis.style.height = '0px';\n\t\t\t}, 1);\n\n\t\t\tthis.setAttribute('aria-hidden', 'true');\n\t\t\tthis.setAttribute('inert', '');\n\t\t\tthis.setAttribute('hidden', '');\n\t\t} else {\n\t\t\t// animate to open\n\t\t\tthis.style.height = `${this.scrollHeight}px`;\n\t\t\tthis.removeAttribute('aria-hidden');\n\t\t\tthis.removeAttribute('inert');\n\t\t\tthis.removeAttribute('hidden');\n\t\t}\n\t}\n\n\tget hidden() {\n\t\treturn this.hasAttribute('hidden');\n\t}\n\n\t_onTransitionEnd(event) {\n\t\t// exit if isn't height property\n\t\tif (event.propertyName != 'height') return;\n\n\t\t// Remove the inline height to allow dynamic content changes\n\t\tif (!this.hidden) {\n\t\t\tthis.style.height = 'auto';\n\t\t}\n\t}\n}\n\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.cjs.js","sources":["../src/collapsible-content.js"],"sourcesContent":["import './styles.css';\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\n\t\t// store references to elements once to avoid re-querying\n\t\tthis.button = null;\n\t\tthis.content = null;\n\n\t\t// define click handler with proper this binding\n\t\tthis.#handleClick = (e) => {\n\t\t\te.preventDefault();\n\t\t\t// toggle expanded value\n\t\t\tconst expanded = this.button.getAttribute('aria-expanded') !== 'true';\n\t\t\tthis.button.setAttribute('aria-expanded', expanded);\n\t\t\tthis.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\t// initialize element references once\n\t\tthis.button = this.querySelector('button');\n\t\tthis.content = this.querySelector('collapsible-content');\n\n\t\tif (!this.button || !this.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\tthis.button.id ||= `collapsible-button-${crypto.randomUUID().slice(0, 8)}`;\n\t\tthis.content.id ||= `collapsible-content-${crypto.randomUUID().slice(0, 8)}`;\n\n\t\t// set accessibility attributes\n\t\tthis.button.setAttribute('aria-controls', this.content.id);\n\t\tthis.content.setAttribute('role', 'region');\n\t\tthis.content.setAttribute('aria-labelledby', this.button.id);\n\n\t\t// set initial state based on aria-expanded attribute\n\t\tconst expanded = this.button.getAttribute('aria-expanded') === 'true';\n\n\t\t// make shure content state matches button state\n\t\tthis.content.hidden = !expanded;\n\n\t\t// add event listener\n\t\tthis.button.addEventListener('click', this.#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\tif (this.button) {\n\t\t\tthis.button.removeEventListener('click', this.#handleClick);\n\t\t}\n\t}\n}\n\n/**\n * custom element that provides animated collapsible content\n */\nclass CollapsibleContent extends HTMLElement {\n\t/**\n\t * initializes the content element and binds event handlers\n\t */\n\tconstructor() {\n\t\tsuper();\n\n\t\t// bind event handler to this instance\n\t\tthis._onTransitionEnd = this._onTransitionEnd.bind(this);\n\n\t\t// add event listener to remove height after transition\n\t\tthis.addEventListener('transitionend', this._onTransitionEnd);\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\t// get aria-expanded state from buton\n\t\tconst component = this.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\tthis.style.height = 'auto';\n\t\t} else {\n\t\t\tthis.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\tthis.removeEventListener('transitionend', this._onTransitionEnd);\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\t// animate to closed\n\t\tif (value) {\n\t\t\t// reset height to animate from\n\t\t\tthis.style.height = `${this.scrollHeight}px`;\n\n\t\t\t// wait one frame and animate to 0\n\t\t\tsetTimeout(() => {\n\t\t\t\tthis.style.height = '0px';\n\t\t\t}, 1);\n\n\t\t\t// set accessibility attributes\n\t\t\tthis.setAttribute('aria-hidden', 'true');\n\t\t\tthis.setAttribute('inert', '');\n\t\t\tthis.setAttribute('hidden', '');\n\t\t} else {\n\t\t\t// only animate if not set to auto\n\t\t\tif (this.style.height !== 'auto') {\n\t\t\t\t// animate to open\n\t\t\t\tthis.style.height = `${this.scrollHeight}px`;\n\t\t\t}\n\n\t\t\t// remove accessibility attributes\n\t\t\tthis.removeAttribute('aria-hidden');\n\t\t\tthis.removeAttribute('inert');\n\t\t\tthis.removeAttribute('hidden');\n\t\t}\n\t}\n\n\t/**\n\t * gets the hidden state from the attribute\n\t * @return {boolean} whether element is hidden\n\t */\n\tget hidden() {\n\t\treturn this.hasAttribute('hidden');\n\t}\n\n\t/**\n\t * handles the end of css transitions\n\t * @param {TransitionEvent} event - the transition event\n\t */\n\t_onTransitionEnd(event) {\n\t\tconsole.log('on transition end');\n\n\t\t// exit if isn't height property\n\t\tif (event.propertyName != 'height') return;\n\n\t\t// remove the inline height to allow dynamic content changes\n\t\tif (!this.hidden) {\n\t\t\tthis.style.height = 'auto';\n\t\t}\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,106 +1,174 @@
1
+ /**
2
+ * custom element that creates a collapsible/expandable component with proper accessibility
3
+ */
1
4
  class CollapsibleComponent extends HTMLElement {
2
5
  #handleClick;
3
6
 
7
+ /**
8
+ * initializes the component and sets up references to child elements
9
+ */
4
10
  constructor() {
5
11
  super();
12
+
13
+ // store references to elements once to avoid re-querying
14
+ this.button = null;
15
+ this.content = null;
16
+
17
+ // define click handler with proper this binding
6
18
  this.#handleClick = (e) => {
7
19
  e.preventDefault();
8
- const button = this.querySelector('button');
9
- const content = this.querySelector('collapsible-content');
10
- const expanded =
11
- button.getAttribute('aria-expanded') !== 'true';
12
- button.setAttribute('aria-expanded', expanded);
13
- content.hidden = !expanded;
20
+ // toggle expanded value
21
+ const expanded = this.button.getAttribute('aria-expanded') !== 'true';
22
+ this.button.setAttribute('aria-expanded', expanded);
23
+ this.content.hidden = !expanded;
14
24
  };
15
25
  }
16
26
 
27
+ /**
28
+ * called when element is added to the dom
29
+ * sets up accessibility attributes and event listeners
30
+ */
17
31
  connectedCallback() {
18
- const button = this.querySelector('button');
19
- const content = this.querySelector('collapsible-content');
32
+ // initialize element references once
33
+ this.button = this.querySelector('button');
34
+ this.content = this.querySelector('collapsible-content');
20
35
 
21
- if (!button || !content) {
22
- console.error(
23
- 'CollapsibleComponent requires a <button> and a <collapsible-content>.'
24
- );
36
+ if (!this.button || !this.content) {
37
+ console.error('CollapsibleComponent requires a <button> and a <collapsible-content>.');
25
38
  return;
26
39
  }
27
40
 
28
- button.id ||= `collapsible-button-${Math.random().toString(36).slice(2, 9)}`;
29
- content.id ||= `collapsible-content-${Math.random().toString(36).slice(2, 9)}`;
41
+ // generate ids if not provided
42
+ this.button.id ||= `collapsible-button-${crypto.randomUUID().slice(0, 8)}`;
43
+ this.content.id ||= `collapsible-content-${crypto.randomUUID().slice(0, 8)}`;
30
44
 
31
- button.setAttribute('aria-controls', content.id);
32
- content.setAttribute('role', 'region');
33
- content.setAttribute('aria-labelledby', button.id);
45
+ // set accessibility attributes
46
+ this.button.setAttribute('aria-controls', this.content.id);
47
+ this.content.setAttribute('role', 'region');
48
+ this.content.setAttribute('aria-labelledby', this.button.id);
34
49
 
35
- const expanded = button.getAttribute('aria-expanded') === 'true';
36
- content.hidden = !expanded;
50
+ // set initial state based on aria-expanded attribute
51
+ const expanded = this.button.getAttribute('aria-expanded') === 'true';
37
52
 
38
- button.addEventListener('click', this.#handleClick);
53
+ // make shure content state matches button state
54
+ this.content.hidden = !expanded;
55
+
56
+ // add event listener
57
+ this.button.addEventListener('click', this.#handleClick);
39
58
  }
40
59
 
60
+ /**
61
+ * called when element is removed from the dom
62
+ * cleans up event listeners
63
+ */
41
64
  disconnectedCallback() {
42
- const button = this.querySelector('button');
43
- button?.removeEventListener('click', this.#handleClick);
65
+ if (this.button) {
66
+ this.button.removeEventListener('click', this.#handleClick);
67
+ }
44
68
  }
45
69
  }
46
70
 
71
+ /**
72
+ * custom element that provides animated collapsible content
73
+ */
47
74
  class CollapsibleContent extends HTMLElement {
75
+ /**
76
+ * initializes the content element and binds event handlers
77
+ */
48
78
  constructor() {
49
79
  super();
80
+
81
+ // bind event handler to this instance
50
82
  this._onTransitionEnd = this._onTransitionEnd.bind(this);
51
- // Add event listener to remove height after transition
83
+
84
+ // add event listener to remove height after transition
52
85
  this.addEventListener('transitionend', this._onTransitionEnd);
53
86
  }
54
87
 
88
+ /**
89
+ * called when element is added to the dom
90
+ * sets initial height based on hidden state
91
+ */
55
92
  connectedCallback() {
56
- if (this.hidden) {
57
- this.style.height = '0';
93
+ // get aria-expanded state from buton
94
+ const component = this.closest('collapsible-component');
95
+ const button = component.querySelector('button');
96
+ const expanded = button.getAttribute('aria-expanded') === 'true';
97
+
98
+ if (expanded) {
99
+ this.style.height = 'auto';
58
100
  } else {
59
- this.style.height = `${this.scrollHeight}px`;
101
+ this.style.height = '0';
60
102
  }
61
103
  }
62
104
 
105
+ /**
106
+ * called when element is removed from the dom
107
+ * cleans up event listeners
108
+ */
63
109
  disconnectedCallback() {
64
110
  this.removeEventListener('transitionend', this._onTransitionEnd);
65
111
  }
66
112
 
113
+ /**
114
+ * handles setting the hidden attribute with animation
115
+ * @param {boolean} value - whether element should be hidden
116
+ */
67
117
  set hidden(value) {
68
118
  // animate to closed
69
119
  if (value) {
70
120
  // reset height to animate from
71
121
  this.style.height = `${this.scrollHeight}px`;
122
+
72
123
  // wait one frame and animate to 0
73
124
  setTimeout(() => {
74
125
  this.style.height = '0px';
75
126
  }, 1);
76
127
 
128
+ // set accessibility attributes
77
129
  this.setAttribute('aria-hidden', 'true');
78
130
  this.setAttribute('inert', '');
79
131
  this.setAttribute('hidden', '');
80
132
  } else {
81
- // animate to open
82
- this.style.height = `${this.scrollHeight}px`;
133
+ // only animate if not set to auto
134
+ if (this.style.height !== 'auto') {
135
+ // animate to open
136
+ this.style.height = `${this.scrollHeight}px`;
137
+ }
138
+
139
+ // remove accessibility attributes
83
140
  this.removeAttribute('aria-hidden');
84
141
  this.removeAttribute('inert');
85
142
  this.removeAttribute('hidden');
86
143
  }
87
144
  }
88
145
 
146
+ /**
147
+ * gets the hidden state from the attribute
148
+ * @return {boolean} whether element is hidden
149
+ */
89
150
  get hidden() {
90
151
  return this.hasAttribute('hidden');
91
152
  }
92
153
 
154
+ /**
155
+ * handles the end of css transitions
156
+ * @param {TransitionEvent} event - the transition event
157
+ */
93
158
  _onTransitionEnd(event) {
159
+ console.log('on transition end');
160
+
94
161
  // exit if isn't height property
95
162
  if (event.propertyName != 'height') return;
96
163
 
97
- // Remove the inline height to allow dynamic content changes
164
+ // remove the inline height to allow dynamic content changes
98
165
  if (!this.hidden) {
99
166
  this.style.height = 'auto';
100
167
  }
101
168
  }
102
169
  }
103
170
 
171
+ // register custom elements
104
172
  customElements.define('collapsible-content', CollapsibleContent);
105
173
  customElements.define('collapsible-component', CollapsibleComponent);
106
174
 
@@ -1 +1 @@
1
- {"version":3,"file":"collapsible-content.esm.js","sources":["../src/collapsible-content.js"],"sourcesContent":["import './styles.css';\n\nclass CollapsibleComponent extends HTMLElement {\n\t#handleClick;\n\n\tconstructor() {\n\t\tsuper();\n\t\tthis.#handleClick = (e) => {\n\t\t\te.preventDefault();\n\t\t\tconst button = this.querySelector('button');\n\t\t\tconst content = this.querySelector('collapsible-content');\n\t\t\tconst expanded =\n\t\t\t\tbutton.getAttribute('aria-expanded') !== 'true';\n\t\t\tbutton.setAttribute('aria-expanded', expanded);\n\t\t\tcontent.hidden = !expanded;\n\t\t};\n\t}\n\n\tconnectedCallback() {\n\t\tconst button = this.querySelector('button');\n\t\tconst content = this.querySelector('collapsible-content');\n\n\t\tif (!button || !content) {\n\t\t\tconsole.error(\n\t\t\t\t'CollapsibleComponent requires a <button> and a <collapsible-content>.'\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tbutton.id ||= `collapsible-button-${Math.random().toString(36).slice(2, 9)}`;\n\t\tcontent.id ||= `collapsible-content-${Math.random().toString(36).slice(2, 9)}`;\n\n\t\tbutton.setAttribute('aria-controls', content.id);\n\t\tcontent.setAttribute('role', 'region');\n\t\tcontent.setAttribute('aria-labelledby', button.id);\n\n\t\tconst expanded = button.getAttribute('aria-expanded') === 'true';\n\t\tcontent.hidden = !expanded;\n\n\t\tbutton.addEventListener('click', this.#handleClick);\n\t}\n\n\tdisconnectedCallback() {\n\t\tconst button = this.querySelector('button');\n\t\tbutton?.removeEventListener('click', this.#handleClick);\n\t}\n}\n\nclass CollapsibleContent extends HTMLElement {\n\tconstructor() {\n\t\tsuper();\n\t\tthis._onTransitionEnd = this._onTransitionEnd.bind(this);\n\t\t// Add event listener to remove height after transition\n\t\tthis.addEventListener('transitionend', this._onTransitionEnd);\n\t}\n\n\tconnectedCallback() {\n\t\tif (this.hidden) {\n\t\t\tthis.style.height = '0';\n\t\t} else {\n\t\t\tthis.style.height = `${this.scrollHeight}px`;\n\t\t}\n\t}\n\n\tdisconnectedCallback() {\n\t\tthis.removeEventListener('transitionend', this._onTransitionEnd);\n\t}\n\n\tset hidden(value) {\n\t\t// animate to closed\n\t\tif (value) {\n\t\t\t// reset height to animate from\n\t\t\tthis.style.height = `${this.scrollHeight}px`;\n\t\t\t// wait one frame and animate to 0\n\t\t\tsetTimeout(() => {\n\t\t\t\tthis.style.height = '0px';\n\t\t\t}, 1);\n\n\t\t\tthis.setAttribute('aria-hidden', 'true');\n\t\t\tthis.setAttribute('inert', '');\n\t\t\tthis.setAttribute('hidden', '');\n\t\t} else {\n\t\t\t// animate to open\n\t\t\tthis.style.height = `${this.scrollHeight}px`;\n\t\t\tthis.removeAttribute('aria-hidden');\n\t\t\tthis.removeAttribute('inert');\n\t\t\tthis.removeAttribute('hidden');\n\t\t}\n\t}\n\n\tget hidden() {\n\t\treturn this.hasAttribute('hidden');\n\t}\n\n\t_onTransitionEnd(event) {\n\t\t// exit if isn't height property\n\t\tif (event.propertyName != 'height') return;\n\n\t\t// Remove the inline height to allow dynamic content changes\n\t\tif (!this.hidden) {\n\t\t\tthis.style.height = 'auto';\n\t\t}\n\t}\n}\n\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.esm.js","sources":["../src/collapsible-content.js"],"sourcesContent":["import './styles.css';\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\n\t\t// store references to elements once to avoid re-querying\n\t\tthis.button = null;\n\t\tthis.content = null;\n\n\t\t// define click handler with proper this binding\n\t\tthis.#handleClick = (e) => {\n\t\t\te.preventDefault();\n\t\t\t// toggle expanded value\n\t\t\tconst expanded = this.button.getAttribute('aria-expanded') !== 'true';\n\t\t\tthis.button.setAttribute('aria-expanded', expanded);\n\t\t\tthis.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\t// initialize element references once\n\t\tthis.button = this.querySelector('button');\n\t\tthis.content = this.querySelector('collapsible-content');\n\n\t\tif (!this.button || !this.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\tthis.button.id ||= `collapsible-button-${crypto.randomUUID().slice(0, 8)}`;\n\t\tthis.content.id ||= `collapsible-content-${crypto.randomUUID().slice(0, 8)}`;\n\n\t\t// set accessibility attributes\n\t\tthis.button.setAttribute('aria-controls', this.content.id);\n\t\tthis.content.setAttribute('role', 'region');\n\t\tthis.content.setAttribute('aria-labelledby', this.button.id);\n\n\t\t// set initial state based on aria-expanded attribute\n\t\tconst expanded = this.button.getAttribute('aria-expanded') === 'true';\n\n\t\t// make shure content state matches button state\n\t\tthis.content.hidden = !expanded;\n\n\t\t// add event listener\n\t\tthis.button.addEventListener('click', this.#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\tif (this.button) {\n\t\t\tthis.button.removeEventListener('click', this.#handleClick);\n\t\t}\n\t}\n}\n\n/**\n * custom element that provides animated collapsible content\n */\nclass CollapsibleContent extends HTMLElement {\n\t/**\n\t * initializes the content element and binds event handlers\n\t */\n\tconstructor() {\n\t\tsuper();\n\n\t\t// bind event handler to this instance\n\t\tthis._onTransitionEnd = this._onTransitionEnd.bind(this);\n\n\t\t// add event listener to remove height after transition\n\t\tthis.addEventListener('transitionend', this._onTransitionEnd);\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\t// get aria-expanded state from buton\n\t\tconst component = this.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\tthis.style.height = 'auto';\n\t\t} else {\n\t\t\tthis.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\tthis.removeEventListener('transitionend', this._onTransitionEnd);\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\t// animate to closed\n\t\tif (value) {\n\t\t\t// reset height to animate from\n\t\t\tthis.style.height = `${this.scrollHeight}px`;\n\n\t\t\t// wait one frame and animate to 0\n\t\t\tsetTimeout(() => {\n\t\t\t\tthis.style.height = '0px';\n\t\t\t}, 1);\n\n\t\t\t// set accessibility attributes\n\t\t\tthis.setAttribute('aria-hidden', 'true');\n\t\t\tthis.setAttribute('inert', '');\n\t\t\tthis.setAttribute('hidden', '');\n\t\t} else {\n\t\t\t// only animate if not set to auto\n\t\t\tif (this.style.height !== 'auto') {\n\t\t\t\t// animate to open\n\t\t\t\tthis.style.height = `${this.scrollHeight}px`;\n\t\t\t}\n\n\t\t\t// remove accessibility attributes\n\t\t\tthis.removeAttribute('aria-hidden');\n\t\t\tthis.removeAttribute('inert');\n\t\t\tthis.removeAttribute('hidden');\n\t\t}\n\t}\n\n\t/**\n\t * gets the hidden state from the attribute\n\t * @return {boolean} whether element is hidden\n\t */\n\tget hidden() {\n\t\treturn this.hasAttribute('hidden');\n\t}\n\n\t/**\n\t * handles the end of css transitions\n\t * @param {TransitionEvent} event - the transition event\n\t */\n\t_onTransitionEnd(event) {\n\t\tconsole.log('on transition end');\n\n\t\t// exit if isn't height property\n\t\tif (event.propertyName != 'height') return;\n\n\t\t// remove the inline height to allow dynamic content changes\n\t\tif (!this.hidden) {\n\t\t\tthis.style.height = 'auto';\n\t\t}\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 +1 @@
1
- var CollapsibleContent=function(t){"use strict";class CollapsibleComponent extends HTMLElement{#t;constructor(){super(),this.#t=t=>{t.preventDefault();const e=this.querySelector("button"),i=this.querySelector("collapsible-content"),n="true"!==e.getAttribute("aria-expanded");e.setAttribute("aria-expanded",n),i.hidden=!n}}connectedCallback(){const t=this.querySelector("button"),e=this.querySelector("collapsible-content");if(!t||!e)return void console.error("CollapsibleComponent requires a <button> and a <collapsible-content>.");t.id||=`collapsible-button-${Math.random().toString(36).slice(2,9)}`,e.id||=`collapsible-content-${Math.random().toString(36).slice(2,9)}`,t.setAttribute("aria-controls",e.id),e.setAttribute("role","region"),e.setAttribute("aria-labelledby",t.id);const i="true"===t.getAttribute("aria-expanded");e.hidden=!i,t.addEventListener("click",this.#t)}disconnectedCallback(){const t=this.querySelector("button");t?.removeEventListener("click",this.#t)}}class CollapsibleContent extends HTMLElement{constructor(){super(),this._onTransitionEnd=this._onTransitionEnd.bind(this),this.addEventListener("transitionend",this._onTransitionEnd)}connectedCallback(){this.hidden?this.style.height="0":this.style.height=`${this.scrollHeight}px`}disconnectedCallback(){this.removeEventListener("transitionend",this._onTransitionEnd)}set hidden(t){t?(this.style.height=`${this.scrollHeight}px`,setTimeout((()=>{this.style.height="0px"}),1),this.setAttribute("aria-hidden","true"),this.setAttribute("inert",""),this.setAttribute("hidden","")):(this.style.height=`${this.scrollHeight}px`,this.removeAttribute("aria-hidden"),this.removeAttribute("inert"),this.removeAttribute("hidden"))}get hidden(){return this.hasAttribute("hidden")}_onTransitionEnd(t){"height"==t.propertyName&&(this.hidden||(this.style.height="auto"))}}return customElements.define("collapsible-content",CollapsibleContent),customElements.define("collapsible-component",CollapsibleComponent),t.CollapsibleComponent=CollapsibleComponent,t.CollapsibleContent=CollapsibleContent,t}({});
1
+ var CollapsibleContent=function(t){"use strict";class CollapsibleComponent extends HTMLElement{#t;constructor(){super(),this.button=null,this.content=null,this.#t=t=>{t.preventDefault();const e="true"!==this.button.getAttribute("aria-expanded");this.button.setAttribute("aria-expanded",e),this.content.hidden=!e}}connectedCallback(){if(this.button=this.querySelector("button"),this.content=this.querySelector("collapsible-content"),!this.button||!this.content)return void console.error("CollapsibleComponent requires a <button> and a <collapsible-content>.");this.button.id||=`collapsible-button-${crypto.randomUUID().slice(0,8)}`,this.content.id||=`collapsible-content-${crypto.randomUUID().slice(0,8)}`,this.button.setAttribute("aria-controls",this.content.id),this.content.setAttribute("role","region"),this.content.setAttribute("aria-labelledby",this.button.id);const t="true"===this.button.getAttribute("aria-expanded");this.content.hidden=!t,this.button.addEventListener("click",this.#t)}disconnectedCallback(){this.button&&this.button.removeEventListener("click",this.#t)}}class CollapsibleContent extends HTMLElement{constructor(){super(),this._onTransitionEnd=this._onTransitionEnd.bind(this),this.addEventListener("transitionend",this._onTransitionEnd)}connectedCallback(){const t="true"===this.closest("collapsible-component").querySelector("button").getAttribute("aria-expanded");this.style.height=t?"auto":"0"}disconnectedCallback(){this.removeEventListener("transitionend",this._onTransitionEnd)}set hidden(t){t?(this.style.height=`${this.scrollHeight}px`,setTimeout((()=>{this.style.height="0px"}),1),this.setAttribute("aria-hidden","true"),this.setAttribute("inert",""),this.setAttribute("hidden","")):("auto"!==this.style.height&&(this.style.height=`${this.scrollHeight}px`),this.removeAttribute("aria-hidden"),this.removeAttribute("inert"),this.removeAttribute("hidden"))}get hidden(){return this.hasAttribute("hidden")}_onTransitionEnd(t){console.log("on transition end"),"height"==t.propertyName&&(this.hidden||(this.style.height="auto"))}}return customElements.define("collapsible-content",CollapsibleContent),customElements.define("collapsible-component",CollapsibleComponent),t.CollapsibleComponent=CollapsibleComponent,t.CollapsibleContent=CollapsibleContent,t}({});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magic-spells/collapsible-content",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Collapsible content web component",
5
5
  "author": "Cory Schulz",
6
6
  "license": "MIT",
@@ -1,108 +1,176 @@
1
1
  import './styles.css';
2
2
 
3
+ /**
4
+ * custom element that creates a collapsible/expandable component with proper accessibility
5
+ */
3
6
  class CollapsibleComponent extends HTMLElement {
4
7
  #handleClick;
5
8
 
9
+ /**
10
+ * initializes the component and sets up references to child elements
11
+ */
6
12
  constructor() {
7
13
  super();
14
+
15
+ // store references to elements once to avoid re-querying
16
+ this.button = null;
17
+ this.content = null;
18
+
19
+ // define click handler with proper this binding
8
20
  this.#handleClick = (e) => {
9
21
  e.preventDefault();
10
- const button = this.querySelector('button');
11
- const content = this.querySelector('collapsible-content');
12
- const expanded =
13
- button.getAttribute('aria-expanded') !== 'true';
14
- button.setAttribute('aria-expanded', expanded);
15
- content.hidden = !expanded;
22
+ // toggle expanded value
23
+ const expanded = this.button.getAttribute('aria-expanded') !== 'true';
24
+ this.button.setAttribute('aria-expanded', expanded);
25
+ this.content.hidden = !expanded;
16
26
  };
17
27
  }
18
28
 
29
+ /**
30
+ * called when element is added to the dom
31
+ * sets up accessibility attributes and event listeners
32
+ */
19
33
  connectedCallback() {
20
- const button = this.querySelector('button');
21
- const content = this.querySelector('collapsible-content');
34
+ // initialize element references once
35
+ this.button = this.querySelector('button');
36
+ this.content = this.querySelector('collapsible-content');
22
37
 
23
- if (!button || !content) {
24
- console.error(
25
- 'CollapsibleComponent requires a <button> and a <collapsible-content>.'
26
- );
38
+ if (!this.button || !this.content) {
39
+ console.error('CollapsibleComponent requires a <button> and a <collapsible-content>.');
27
40
  return;
28
41
  }
29
42
 
30
- button.id ||= `collapsible-button-${Math.random().toString(36).slice(2, 9)}`;
31
- content.id ||= `collapsible-content-${Math.random().toString(36).slice(2, 9)}`;
43
+ // generate ids if not provided
44
+ this.button.id ||= `collapsible-button-${crypto.randomUUID().slice(0, 8)}`;
45
+ this.content.id ||= `collapsible-content-${crypto.randomUUID().slice(0, 8)}`;
32
46
 
33
- button.setAttribute('aria-controls', content.id);
34
- content.setAttribute('role', 'region');
35
- content.setAttribute('aria-labelledby', button.id);
47
+ // set accessibility attributes
48
+ this.button.setAttribute('aria-controls', this.content.id);
49
+ this.content.setAttribute('role', 'region');
50
+ this.content.setAttribute('aria-labelledby', this.button.id);
36
51
 
37
- const expanded = button.getAttribute('aria-expanded') === 'true';
38
- content.hidden = !expanded;
52
+ // set initial state based on aria-expanded attribute
53
+ const expanded = this.button.getAttribute('aria-expanded') === 'true';
39
54
 
40
- button.addEventListener('click', this.#handleClick);
55
+ // make shure content state matches button state
56
+ this.content.hidden = !expanded;
57
+
58
+ // add event listener
59
+ this.button.addEventListener('click', this.#handleClick);
41
60
  }
42
61
 
62
+ /**
63
+ * called when element is removed from the dom
64
+ * cleans up event listeners
65
+ */
43
66
  disconnectedCallback() {
44
- const button = this.querySelector('button');
45
- button?.removeEventListener('click', this.#handleClick);
67
+ if (this.button) {
68
+ this.button.removeEventListener('click', this.#handleClick);
69
+ }
46
70
  }
47
71
  }
48
72
 
73
+ /**
74
+ * custom element that provides animated collapsible content
75
+ */
49
76
  class CollapsibleContent extends HTMLElement {
77
+ /**
78
+ * initializes the content element and binds event handlers
79
+ */
50
80
  constructor() {
51
81
  super();
82
+
83
+ // bind event handler to this instance
52
84
  this._onTransitionEnd = this._onTransitionEnd.bind(this);
53
- // Add event listener to remove height after transition
85
+
86
+ // add event listener to remove height after transition
54
87
  this.addEventListener('transitionend', this._onTransitionEnd);
55
88
  }
56
89
 
90
+ /**
91
+ * called when element is added to the dom
92
+ * sets initial height based on hidden state
93
+ */
57
94
  connectedCallback() {
58
- if (this.hidden) {
59
- this.style.height = '0';
95
+ // get aria-expanded state from buton
96
+ const component = this.closest('collapsible-component');
97
+ const button = component.querySelector('button');
98
+ const expanded = button.getAttribute('aria-expanded') === 'true';
99
+
100
+ if (expanded) {
101
+ this.style.height = 'auto';
60
102
  } else {
61
- this.style.height = `${this.scrollHeight}px`;
103
+ this.style.height = '0';
62
104
  }
63
105
  }
64
106
 
107
+ /**
108
+ * called when element is removed from the dom
109
+ * cleans up event listeners
110
+ */
65
111
  disconnectedCallback() {
66
112
  this.removeEventListener('transitionend', this._onTransitionEnd);
67
113
  }
68
114
 
115
+ /**
116
+ * handles setting the hidden attribute with animation
117
+ * @param {boolean} value - whether element should be hidden
118
+ */
69
119
  set hidden(value) {
70
120
  // animate to closed
71
121
  if (value) {
72
122
  // reset height to animate from
73
123
  this.style.height = `${this.scrollHeight}px`;
124
+
74
125
  // wait one frame and animate to 0
75
126
  setTimeout(() => {
76
127
  this.style.height = '0px';
77
128
  }, 1);
78
129
 
130
+ // set accessibility attributes
79
131
  this.setAttribute('aria-hidden', 'true');
80
132
  this.setAttribute('inert', '');
81
133
  this.setAttribute('hidden', '');
82
134
  } else {
83
- // animate to open
84
- this.style.height = `${this.scrollHeight}px`;
135
+ // only animate if not set to auto
136
+ if (this.style.height !== 'auto') {
137
+ // animate to open
138
+ this.style.height = `${this.scrollHeight}px`;
139
+ }
140
+
141
+ // remove accessibility attributes
85
142
  this.removeAttribute('aria-hidden');
86
143
  this.removeAttribute('inert');
87
144
  this.removeAttribute('hidden');
88
145
  }
89
146
  }
90
147
 
148
+ /**
149
+ * gets the hidden state from the attribute
150
+ * @return {boolean} whether element is hidden
151
+ */
91
152
  get hidden() {
92
153
  return this.hasAttribute('hidden');
93
154
  }
94
155
 
156
+ /**
157
+ * handles the end of css transitions
158
+ * @param {TransitionEvent} event - the transition event
159
+ */
95
160
  _onTransitionEnd(event) {
161
+ console.log('on transition end');
162
+
96
163
  // exit if isn't height property
97
164
  if (event.propertyName != 'height') return;
98
165
 
99
- // Remove the inline height to allow dynamic content changes
166
+ // remove the inline height to allow dynamic content changes
100
167
  if (!this.hidden) {
101
168
  this.style.height = 'auto';
102
169
  }
103
170
  }
104
171
  }
105
172
 
173
+ // register custom elements
106
174
  customElements.define('collapsible-content', CollapsibleContent);
107
175
  customElements.define('collapsible-component', CollapsibleComponent);
108
176