@magic-spells/collapsible-content 0.1.3 → 0.2.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.
@@ -1,108 +1,181 @@
1
- import './styles.css';
1
+ import './index.scss';
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();
8
- this.#handleClick = (e) => {
14
+ const _ = this;
15
+
16
+ // store references to elements once to avoid re-querying
17
+ _.button = null;
18
+ _.content = null;
19
+
20
+ // define click handler with proper this binding
21
+ _.#handleClick = (e) => {
9
22
  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;
23
+ // toggle expanded value
24
+ const expanded = _.button.getAttribute('aria-expanded') !== 'true';
25
+ _.button.setAttribute('aria-expanded', expanded);
26
+ _.content.hidden = !expanded;
16
27
  };
17
28
  }
18
29
 
30
+ /**
31
+ * Called when element is added to the DOM
32
+ * Sets up accessibility attributes and event listeners
33
+ */
19
34
  connectedCallback() {
20
- const button = this.querySelector('button');
21
- const content = this.querySelector('collapsible-content');
35
+ const _ = this;
36
+
37
+ // initialize element references once
38
+ _.button = _.querySelector('button');
39
+ _.content = _.querySelector('collapsible-content');
22
40
 
23
- if (!button || !content) {
24
- console.error(
25
- 'CollapsibleComponent requires a <button> and a <collapsible-content>.'
26
- );
41
+ if (!_.button || !_.content) {
42
+ console.error('CollapsibleComponent requires a <button> and a <collapsible-content>.');
27
43
  return;
28
44
  }
29
45
 
30
- button.id ||= `collapsible-button-${Math.random().toString(36).slice(2, 9)}`;
31
- content.id ||= `collapsible-content-${Math.random().toString(36).slice(2, 9)}`;
46
+ // generate ids if not provided
47
+ _.button.id ||= `collapsible-button-${crypto.randomUUID().slice(0, 8)}`;
48
+ _.content.id ||= `collapsible-content-${crypto.randomUUID().slice(0, 8)}`;
32
49
 
33
- button.setAttribute('aria-controls', content.id);
34
- content.setAttribute('role', 'region');
35
- content.setAttribute('aria-labelledby', button.id);
50
+ // set accessibility attributes
51
+ _.button.setAttribute('aria-controls', _.content.id);
52
+ _.content.setAttribute('role', 'region');
53
+ _.content.setAttribute('aria-labelledby', _.button.id);
36
54
 
37
- const expanded = button.getAttribute('aria-expanded') === 'true';
38
- content.hidden = !expanded;
55
+ // set initial state based on aria-expanded attribute
56
+ const expanded = _.button.getAttribute('aria-expanded') === 'true';
57
+
58
+ // make sure content state matches button state
59
+ _.content.hidden = !expanded;
39
60
 
40
- button.addEventListener('click', this.#handleClick);
61
+ // add event listener
62
+ _.button.addEventListener('click', _.#handleClick);
41
63
  }
42
64
 
65
+ /**
66
+ * Called when element is removed from the DOM
67
+ * Cleans up event listeners
68
+ */
43
69
  disconnectedCallback() {
44
- const button = this.querySelector('button');
45
- button?.removeEventListener('click', this.#handleClick);
70
+ const _ = this;
71
+ if (_.button) {
72
+ _.button.removeEventListener('click', _.#handleClick);
73
+ }
46
74
  }
47
75
  }
48
76
 
77
+ /**
78
+ * Custom element that provides animated collapsible content
79
+ */
49
80
  class CollapsibleContent extends HTMLElement {
81
+ #handleTransitionEnd;
82
+
83
+ /**
84
+ * Initializes the content element and binds event handlers
85
+ */
50
86
  constructor() {
51
87
  super();
52
- this._onTransitionEnd = this._onTransitionEnd.bind(this);
53
- // Add event listener to remove height after transition
54
- this.addEventListener('transitionend', this._onTransitionEnd);
88
+ const _ = this;
89
+
90
+ // define event handler using arrow function for proper binding
91
+ _.#handleTransitionEnd = (event) => {
92
+ // exit if isn't height property
93
+ if (event.propertyName != 'height') return;
94
+
95
+ // remove the inline height to allow dynamic content changes
96
+ if (!_.hidden) {
97
+ _.style.height = 'auto';
98
+ }
99
+ };
100
+
101
+ // add event listener to remove height after transition
102
+ _.addEventListener('transitionend', _.#handleTransitionEnd);
55
103
  }
56
104
 
105
+ /**
106
+ * Called when element is added to the DOM
107
+ * Sets initial height based on hidden state
108
+ */
57
109
  connectedCallback() {
58
- if (this.hidden) {
59
- this.style.height = '0';
110
+ const _ = this;
111
+
112
+ // get aria-expanded state from button
113
+ const component = _.closest('collapsible-component');
114
+ const button = component.querySelector('button');
115
+ const expanded = button.getAttribute('aria-expanded') === 'true';
116
+
117
+ if (expanded) {
118
+ _.style.height = 'auto';
60
119
  } else {
61
- this.style.height = `${this.scrollHeight}px`;
120
+ _.style.height = '0';
62
121
  }
63
122
  }
64
123
 
124
+ /**
125
+ * Called when element is removed from the DOM
126
+ * Cleans up event listeners
127
+ */
65
128
  disconnectedCallback() {
66
- this.removeEventListener('transitionend', this._onTransitionEnd);
129
+ const _ = this;
130
+ _.removeEventListener('transitionend', _.#handleTransitionEnd);
67
131
  }
68
132
 
133
+ /**
134
+ * Handles setting the hidden attribute with animation
135
+ * @param {boolean} value - Whether element should be hidden
136
+ */
69
137
  set hidden(value) {
138
+ const _ = this;
139
+
70
140
  // animate to closed
71
141
  if (value) {
72
142
  // reset height to animate from
73
- this.style.height = `${this.scrollHeight}px`;
143
+ _.style.height = `${_.scrollHeight}px`;
144
+
74
145
  // wait one frame and animate to 0
75
146
  setTimeout(() => {
76
- this.style.height = '0px';
147
+ _.style.height = '0px';
77
148
  }, 1);
78
149
 
79
- this.setAttribute('aria-hidden', 'true');
80
- this.setAttribute('inert', '');
81
- this.setAttribute('hidden', '');
150
+ // set accessibility attributes
151
+ _.setAttribute('aria-hidden', 'true');
152
+ _.setAttribute('inert', '');
153
+ _.setAttribute('hidden', '');
82
154
  } else {
83
- // animate to open
84
- this.style.height = `${this.scrollHeight}px`;
85
- this.removeAttribute('aria-hidden');
86
- this.removeAttribute('inert');
87
- this.removeAttribute('hidden');
155
+ // only animate if not set to auto
156
+ if (_.style.height !== 'auto') {
157
+ // animate to open
158
+ _.style.height = `${_.scrollHeight}px`;
159
+ }
160
+
161
+ // remove accessibility attributes
162
+ _.removeAttribute('aria-hidden');
163
+ _.removeAttribute('inert');
164
+ _.removeAttribute('hidden');
88
165
  }
89
166
  }
90
167
 
168
+ /**
169
+ * Gets the hidden state from the attribute
170
+ * @returns {boolean} Whether element is hidden
171
+ */
91
172
  get hidden() {
92
- return this.hasAttribute('hidden');
93
- }
94
-
95
- _onTransitionEnd(event) {
96
- // exit if isn't height property
97
- if (event.propertyName != 'height') return;
98
-
99
- // Remove the inline height to allow dynamic content changes
100
- if (!this.hidden) {
101
- this.style.height = 'auto';
102
- }
173
+ const _ = this;
174
+ return _.hasAttribute('hidden');
103
175
  }
104
176
  }
105
177
 
178
+ // register custom elements
106
179
  customElements.define('collapsible-content', CollapsibleContent);
107
180
  customElements.define('collapsible-component', CollapsibleComponent);
108
181
 
package/src/index.scss ADDED
@@ -0,0 +1,2 @@
1
+ @forward "scss/variables";
2
+ @forward "scss/collapsible-content";
@@ -0,0 +1,25 @@
1
+ // Import variables using the modern @use rule
2
+ @use 'variables' as vars;
3
+
4
+ collapsible-component {
5
+ display: var(--cc-component-display, block);
6
+
7
+ button {
8
+ width: var(--cc-button-width, 100%);
9
+ text-align: var(--cc-button-text-align, left);
10
+ background: var(--cc-button-background, none);
11
+ border: var(--cc-button-border, none);
12
+ cursor: var(--cc-button-cursor, pointer);
13
+ }
14
+
15
+ collapsible-content[hidden] {
16
+ display: var(--cc-content-display, block);
17
+ }
18
+ }
19
+
20
+ collapsible-content {
21
+ padding: var(--cc-content-padding, 0px);
22
+ display: var(--cc-content-display, block);
23
+ overflow: var(--cc-content-overflow, hidden);
24
+ transition: height var(--cc-transition-duration, 0.35s) var(--cc-transition-timing, ease-out);
25
+ }
@@ -0,0 +1,40 @@
1
+ // Collapsible Content variables
2
+ // SCSS variables for internal component use and customization
3
+ // these variables can be customized by the user
4
+
5
+ // layout
6
+ $component-display: block !default;
7
+ $button-width: 100% !default;
8
+ $button-text-align: left !default;
9
+
10
+ // appearance
11
+ $button-background: none !default;
12
+ $button-border: none !default;
13
+ $button-cursor: pointer !default;
14
+
15
+ // animation
16
+ $content-padding: 0px !default;
17
+ $content-display: block !default;
18
+ $content-overflow: hidden !default;
19
+ $transition-duration: 0.35s !default;
20
+ $transition-timing: ease-out !default;
21
+
22
+ // Define CSS Custom Properties using SCSS values
23
+ :root {
24
+ // layout
25
+ --cc-component-display: #{$component-display};
26
+ --cc-button-width: #{$button-width};
27
+ --cc-button-text-align: #{$button-text-align};
28
+
29
+ // appearance
30
+ --cc-button-background: #{$button-background};
31
+ --cc-button-border: #{$button-border};
32
+ --cc-button-cursor: #{$button-cursor};
33
+
34
+ // animation
35
+ --cc-content-padding: #{$content-padding};
36
+ --cc-content-display: #{$content-display};
37
+ --cc-content-overflow: #{$content-overflow};
38
+ --cc-transition-duration: #{$transition-duration};
39
+ --cc-transition-timing: #{$transition-timing};
40
+ }
package/src/styles.css DELETED
@@ -1,22 +0,0 @@
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-content {
14
- padding: 0px;
15
- display: block;
16
- overflow: hidden;
17
- transition: height 0.35s ease-out;
18
- }
19
-
20
- collapsible-component collapsible-content[hidden] {
21
- display: block;
22
- }