@magic-spells/collapsible-content 0.1.4 → 1.0.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 +1 -1
- package/README.md +116 -0
- package/dist/collapsible-content.cjs.js +133 -101
- package/dist/collapsible-content.cjs.js.map +1 -1
- package/dist/collapsible-content.css +31 -0
- package/dist/collapsible-content.esm.js +133 -101
- package/dist/collapsible-content.esm.js.map +1 -1
- package/dist/collapsible-content.js +217 -0
- package/dist/collapsible-content.js.map +1 -0
- package/dist/collapsible-content.min.css +1 -1
- package/dist/collapsible-content.min.js +1 -1
- package/package.json +8 -7
- package/src/collapsible-content.css +31 -0
- package/src/collapsible-content.js +134 -102
- package/src/styles.css +0 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magic-spells/collapsible-content",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Collapsible content web component",
|
|
5
5
|
"author": "Cory Schulz",
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"require": "./dist/collapsible-content.cjs.js",
|
|
16
16
|
"default": "./dist/collapsible-content.esm.js"
|
|
17
17
|
},
|
|
18
|
-
"./
|
|
18
|
+
"./css": "./dist/collapsible-content.css",
|
|
19
|
+
"./css/min": "./dist/collapsible-content.min.css"
|
|
19
20
|
},
|
|
20
21
|
"sideEffects": true,
|
|
21
22
|
"repository": {
|
|
@@ -56,16 +57,16 @@
|
|
|
56
57
|
"not ie <= 11"
|
|
57
58
|
],
|
|
58
59
|
"devDependencies": {
|
|
59
|
-
"@eslint/js": "^
|
|
60
|
+
"@eslint/js": "^9.38.0",
|
|
60
61
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
61
62
|
"@rollup/plugin-terser": "^0.4.4",
|
|
62
|
-
"eslint": "^
|
|
63
|
-
"globals": "^
|
|
63
|
+
"eslint": "^9.38.0",
|
|
64
|
+
"globals": "^15.15.0",
|
|
65
|
+
"postcss-lightningcss": "^1.0.1",
|
|
64
66
|
"prettier": "^3.3.3",
|
|
65
67
|
"rollup": "^3.0.0",
|
|
66
68
|
"rollup-plugin-copy": "^3.5.0",
|
|
67
69
|
"rollup-plugin-postcss": "^4.0.2",
|
|
68
|
-
"rollup-plugin-serve": "^1.1.1"
|
|
69
|
-
"postcss-lightningcss": "^1.0.1"
|
|
70
|
+
"rollup-plugin-serve": "^1.1.1"
|
|
70
71
|
}
|
|
71
72
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
collapsible-component {
|
|
2
|
+
display: block;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
collapsible-component button {
|
|
6
|
+
width: 100%;
|
|
7
|
+
text-align: left;
|
|
8
|
+
background: none;
|
|
9
|
+
border: none;
|
|
10
|
+
cursor: pointer;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
collapsible-component button:focus-visible {
|
|
14
|
+
outline: 2px solid currentColor;
|
|
15
|
+
outline-offset: 2px;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
collapsible-content {
|
|
19
|
+
--collapsible-duration: 0.35s;
|
|
20
|
+
--collapsible-easing: ease-out;
|
|
21
|
+
|
|
22
|
+
display: block;
|
|
23
|
+
overflow: hidden;
|
|
24
|
+
transition: height var(--collapsible-duration) var(--collapsible-easing);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@media (prefers-reduced-motion: reduce) {
|
|
28
|
+
collapsible-content {
|
|
29
|
+
transition: none;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -1,177 +1,209 @@
|
|
|
1
|
-
import './
|
|
1
|
+
import './collapsible-content.css';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Custom element that creates a collapsible/expandable component with proper accessibility
|
|
5
5
|
*/
|
|
6
6
|
class CollapsibleComponent extends HTMLElement {
|
|
7
7
|
#handleClick;
|
|
8
|
+
#abortController;
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
+
* Initializes the component and sets up references to child elements
|
|
11
12
|
*/
|
|
12
13
|
constructor() {
|
|
13
14
|
super();
|
|
15
|
+
const _ = this;
|
|
14
16
|
|
|
15
17
|
// store references to elements once to avoid re-querying
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
_.button = null;
|
|
19
|
+
_.content = null;
|
|
18
20
|
|
|
19
21
|
// define click handler with proper this binding
|
|
20
|
-
|
|
22
|
+
_.#handleClick = (e) => {
|
|
21
23
|
e.preventDefault();
|
|
22
24
|
// toggle expanded value
|
|
23
|
-
const expanded =
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
const expanded = _.button.getAttribute('aria-expanded') !== 'true';
|
|
26
|
+
_.button.setAttribute('aria-expanded', expanded);
|
|
27
|
+
_.content.collapsed = !expanded;
|
|
26
28
|
};
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
+
* Called when element is added to the DOM
|
|
33
|
+
* Sets up accessibility attributes and event listeners
|
|
32
34
|
*/
|
|
33
35
|
connectedCallback() {
|
|
34
|
-
|
|
35
|
-
this.button = this.querySelector('button');
|
|
36
|
-
this.content = this.querySelector('collapsible-content');
|
|
36
|
+
const _ = this;
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
// initialize element references once
|
|
39
|
+
_.button = _.querySelector('button');
|
|
40
|
+
_.content = _.querySelector('collapsible-content');
|
|
41
|
+
|
|
42
|
+
if (!_.button || !_.content) {
|
|
43
|
+
const error = new Error(
|
|
44
|
+
'CollapsibleComponent requires a <button> and a <collapsible-content>.'
|
|
45
|
+
);
|
|
46
|
+
console.error(error.message);
|
|
47
|
+
_.dispatchEvent(
|
|
48
|
+
new CustomEvent('collapsible-error', {
|
|
49
|
+
bubbles: true,
|
|
50
|
+
detail: { error },
|
|
51
|
+
})
|
|
52
|
+
);
|
|
40
53
|
return;
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
// generate ids if not provided
|
|
44
|
-
|
|
45
|
-
|
|
57
|
+
_.button.id ||= `collapsible-button-${crypto.randomUUID().slice(0, 8)}`;
|
|
58
|
+
_.content.id ||= `collapsible-content-${crypto.randomUUID().slice(0, 8)}`;
|
|
46
59
|
|
|
47
60
|
// set accessibility attributes
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// set initial state based on
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
_.button.type ||= 'button';
|
|
62
|
+
_.button.setAttribute('aria-controls', _.content.id);
|
|
63
|
+
_.content.setAttribute('aria-labelledby', _.button.id);
|
|
64
|
+
|
|
65
|
+
// set initial state based on open attribute
|
|
66
|
+
const open = _.content.hasAttribute('open');
|
|
67
|
+
_.button.setAttribute('aria-expanded', open);
|
|
68
|
+
_.content.collapsed = !open;
|
|
69
|
+
|
|
70
|
+
// use AbortController to prevent duplicate listeners on reconnection
|
|
71
|
+
_.#abortController = new AbortController();
|
|
72
|
+
_.button.addEventListener('click', _.#handleClick, {
|
|
73
|
+
signal: _.#abortController.signal,
|
|
74
|
+
});
|
|
60
75
|
}
|
|
61
76
|
|
|
62
77
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
78
|
+
* Called when element is removed from the DOM
|
|
79
|
+
* Cleans up event listeners
|
|
65
80
|
*/
|
|
66
81
|
disconnectedCallback() {
|
|
67
|
-
|
|
68
|
-
|
|
82
|
+
const _ = this;
|
|
83
|
+
if (_.#abortController) {
|
|
84
|
+
_.#abortController.abort();
|
|
85
|
+
_.#abortController = null;
|
|
69
86
|
}
|
|
70
87
|
}
|
|
71
88
|
}
|
|
72
89
|
|
|
73
90
|
/**
|
|
74
|
-
*
|
|
91
|
+
* Custom element that provides animated collapsible content
|
|
75
92
|
*/
|
|
76
93
|
class CollapsibleContent extends HTMLElement {
|
|
94
|
+
#handleTransitionEnd;
|
|
95
|
+
#abortController;
|
|
96
|
+
#animating = false;
|
|
97
|
+
|
|
77
98
|
/**
|
|
78
|
-
*
|
|
99
|
+
* Initializes the content element and binds event handlers
|
|
79
100
|
*/
|
|
80
101
|
constructor() {
|
|
81
102
|
super();
|
|
103
|
+
const _ = this;
|
|
82
104
|
|
|
83
|
-
//
|
|
84
|
-
|
|
105
|
+
// define event handler using arrow function for proper binding
|
|
106
|
+
_.#handleTransitionEnd = (event) => {
|
|
107
|
+
// exit if isn't height property
|
|
108
|
+
if (event.propertyName !== 'height') return;
|
|
85
109
|
|
|
86
|
-
|
|
87
|
-
|
|
110
|
+
_.#animating = false;
|
|
111
|
+
|
|
112
|
+
// remove the inline height to allow dynamic content changes
|
|
113
|
+
if (!_.collapsed) {
|
|
114
|
+
_.style.height = 'auto';
|
|
115
|
+
}
|
|
116
|
+
};
|
|
88
117
|
}
|
|
89
118
|
|
|
90
119
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
120
|
+
* Called when element is added to the DOM
|
|
121
|
+
* Sets initial height based on open attribute
|
|
93
122
|
*/
|
|
94
123
|
connectedCallback() {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
this.style.height = '0';
|
|
104
|
-
}
|
|
124
|
+
const _ = this;
|
|
125
|
+
_.style.height = _.hasAttribute('open') ? 'auto' : '0';
|
|
126
|
+
|
|
127
|
+
// use AbortController to prevent duplicate listeners on reconnection
|
|
128
|
+
_.#abortController = new AbortController();
|
|
129
|
+
_.addEventListener('transitionend', _.#handleTransitionEnd, {
|
|
130
|
+
signal: _.#abortController.signal,
|
|
131
|
+
});
|
|
105
132
|
}
|
|
106
133
|
|
|
107
134
|
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
135
|
+
* Called when element is removed from the DOM
|
|
136
|
+
* Cleans up event listeners
|
|
110
137
|
*/
|
|
111
138
|
disconnectedCallback() {
|
|
112
|
-
|
|
139
|
+
const _ = this;
|
|
140
|
+
if (_.#abortController) {
|
|
141
|
+
_.#abortController.abort();
|
|
142
|
+
_.#abortController = null;
|
|
143
|
+
}
|
|
113
144
|
}
|
|
114
145
|
|
|
115
146
|
/**
|
|
116
|
-
*
|
|
117
|
-
* @param {boolean} value -
|
|
147
|
+
* Handles setting the collapsed state with animation
|
|
148
|
+
* @param {boolean} value - Whether element should be collapsed
|
|
118
149
|
*/
|
|
119
|
-
set
|
|
120
|
-
|
|
150
|
+
set collapsed(value) {
|
|
151
|
+
const _ = this;
|
|
152
|
+
|
|
153
|
+
// prevent rapid clicking during animation
|
|
154
|
+
if (_.#animating) return;
|
|
155
|
+
|
|
156
|
+
// check if transitions are enabled (respects prefers-reduced-motion)
|
|
157
|
+
const hasTransition = getComputedStyle(_).transitionDuration !== '0s';
|
|
158
|
+
|
|
121
159
|
if (value) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
160
|
+
if (hasTransition) {
|
|
161
|
+
_.#animating = true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// animate to closed
|
|
165
|
+
_.style.height = `${_.scrollHeight}px`;
|
|
166
|
+
|
|
167
|
+
// use requestAnimationFrame for reliable frame timing
|
|
168
|
+
requestAnimationFrame(() => {
|
|
169
|
+
requestAnimationFrame(() => {
|
|
170
|
+
_.style.height = '0px';
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
_.removeAttribute('open');
|
|
175
|
+
_.setAttribute('aria-hidden', 'true');
|
|
176
|
+
_.setAttribute('inert', '');
|
|
134
177
|
} else {
|
|
135
|
-
// only animate if not
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
|
|
178
|
+
// only animate if not already auto
|
|
179
|
+
if (_.style.height !== 'auto') {
|
|
180
|
+
if (hasTransition) {
|
|
181
|
+
_.#animating = true;
|
|
182
|
+
}
|
|
183
|
+
_.style.height = `${_.scrollHeight}px`;
|
|
139
184
|
}
|
|
140
185
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
this.removeAttribute('hidden');
|
|
186
|
+
_.setAttribute('open', '');
|
|
187
|
+
_.removeAttribute('aria-hidden');
|
|
188
|
+
_.removeAttribute('inert');
|
|
145
189
|
}
|
|
146
190
|
}
|
|
147
191
|
|
|
148
192
|
/**
|
|
149
|
-
*
|
|
150
|
-
* @
|
|
151
|
-
*/
|
|
152
|
-
get hidden() {
|
|
153
|
-
return this.hasAttribute('hidden');
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* handles the end of css transitions
|
|
158
|
-
* @param {TransitionEvent} event - the transition event
|
|
193
|
+
* Gets the collapsed state
|
|
194
|
+
* @returns {boolean} Whether element is collapsed
|
|
159
195
|
*/
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
}
|
|
196
|
+
get collapsed() {
|
|
197
|
+
return !this.hasAttribute('open');
|
|
170
198
|
}
|
|
171
199
|
}
|
|
172
200
|
|
|
173
|
-
// register custom elements
|
|
174
|
-
customElements.
|
|
175
|
-
customElements.define('collapsible-
|
|
201
|
+
// register custom elements if not already defined
|
|
202
|
+
if (!customElements.get('collapsible-content')) {
|
|
203
|
+
customElements.define('collapsible-content', CollapsibleContent);
|
|
204
|
+
}
|
|
205
|
+
if (!customElements.get('collapsible-component')) {
|
|
206
|
+
customElements.define('collapsible-component', CollapsibleComponent);
|
|
207
|
+
}
|
|
176
208
|
|
|
177
209
|
export { CollapsibleContent, CollapsibleComponent };
|
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
|
-
}
|