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