@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.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Magic Spells (Cory Schulz)
3
+ Copyright (c) 2025 Magic Spells (Cory Schulz)
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # Collapsible Content Web Component
2
+
3
+ A lightweight, customizable Web Component for creating accessible collapsible content sections. Perfect for FAQs, accordions, or any content that needs to be toggled.
4
+
5
+ [**Live Demo**](https://magic-spells.github.io/collapsible-content/demo/)
6
+
7
+ ## Features
8
+
9
+ - No dependencies
10
+ - Lightweight
11
+ - Follows accessibility best practices
12
+ - Smooth open/close animations
13
+ - Uses proper ARIA attributes
14
+ - Keyboard accessible
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @magic-spells/collapsible-content
20
+ ```
21
+
22
+ ```javascript
23
+ // Add to your JavaScript file
24
+ import '@magic-spells/collapsible-content';
25
+ ```
26
+
27
+ Or include directly in your HTML:
28
+
29
+ ```html
30
+ <script src="https://unpkg.com/@magic-spells/collapsible-content"></script>
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ```html
36
+ <collapsible-component>
37
+ <button aria-expanded="false">Product Information</button>
38
+ <collapsible-content>
39
+ <div class="content-wrapper">
40
+ <h3>Details</h3>
41
+ <p>
42
+ This product is made with 100% organic materials. It's durable, eco-friendly, and
43
+ stylish.
44
+ </p>
45
+ </div>
46
+ </collapsible-content>
47
+ </collapsible-component>
48
+ ```
49
+
50
+ ## How It Works
51
+
52
+ - The collapsible content is initially hidden (unless `aria-expanded="true"` is set on the button)
53
+ - Clicking the button toggles the visibility of the content
54
+ - The component handles all the ARIA attributes for accessibility
55
+ - Smooth animations are provided for opening and closing
56
+
57
+ ## Customization
58
+
59
+ ### Styling
60
+
61
+ You can style the collapsible component using CSS custom properties:
62
+
63
+ ```css
64
+ :root {
65
+ /* Layout */
66
+ --cc-component-display: block;
67
+ --cc-button-width: 100%;
68
+ --cc-button-text-align: left;
69
+
70
+ /* Appearance */
71
+ --cc-button-background: none;
72
+ --cc-button-border: none;
73
+ --cc-button-cursor: pointer;
74
+
75
+ /* Animation */
76
+ --cc-content-padding: 10px;
77
+ --cc-transition-duration: 0.5s;
78
+ --cc-transition-timing: ease-in-out;
79
+ }
80
+ ```
81
+
82
+ ### SCSS Integration
83
+
84
+ For more advanced customization, you can import the SCSS directly:
85
+
86
+ ```scss
87
+ // Option 1: Import the compiled CSS
88
+ @import '@magic-spells/collapsible-content/css';
89
+
90
+ // Option 2: Import the SCSS and override variables
91
+ @use '@magic-spells/collapsible-content/scss' with (
92
+ $transition-duration: 0.5s,
93
+ $transition-timing: ease-in-out,
94
+ $content-padding: 10px
95
+ );
96
+
97
+ // Option 3: Import specific parts
98
+ @use '@magic-spells/collapsible-content/scss/variables' with (
99
+ $button-text-align: center
100
+ );
101
+ @use '@magic-spells/collapsible-content/scss/collapsible-content';
102
+ ```
103
+
104
+ ## Accessibility
105
+
106
+ This component follows WCAG guidelines for accessible accordions:
107
+
108
+ - Uses proper `aria-expanded` and `aria-controls` attributes
109
+ - Content regions have proper `role="region"` and `aria-labelledby` attributes
110
+ - Keyboard accessible (toggle with Space or Enter key)
111
+ - Focus management for keyboard users
112
+
113
+ ## Browser Support
114
+
115
+ This component works in all modern browsers that support Web Components.
116
+
117
+ ## License
118
+
119
+ MIT
@@ -1,108 +1,181 @@
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();
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
 
@@ -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 './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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"}
@@ -0,0 +1,34 @@
1
+ :root {
2
+ --cc-component-display: block;
3
+ --cc-button-width: 100%;
4
+ --cc-button-text-align: left;
5
+ --cc-button-background: none;
6
+ --cc-button-border: none;
7
+ --cc-button-cursor: pointer;
8
+ --cc-content-padding: 0px;
9
+ --cc-content-display: block;
10
+ --cc-content-overflow: hidden;
11
+ --cc-transition-duration: 0.35s;
12
+ --cc-transition-timing: ease-out;
13
+ }
14
+
15
+ collapsible-component {
16
+ display: var(--cc-component-display, block);
17
+ }
18
+ collapsible-component button {
19
+ width: var(--cc-button-width, 100%);
20
+ text-align: var(--cc-button-text-align, left);
21
+ background: var(--cc-button-background, none);
22
+ border: var(--cc-button-border, none);
23
+ cursor: var(--cc-button-cursor, pointer);
24
+ }
25
+ collapsible-component collapsible-content[hidden] {
26
+ display: var(--cc-content-display, block);
27
+ }
28
+
29
+ collapsible-content {
30
+ padding: var(--cc-content-padding, 0px);
31
+ display: var(--cc-content-display, block);
32
+ overflow: var(--cc-content-overflow, hidden);
33
+ transition: height var(--cc-transition-duration, 0.35s) var(--cc-transition-timing, ease-out);
34
+ }