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