@magic-spells/tab-group 0.1.0 → 1.1.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 +78 -183
- package/dist/tab-group.cjs.js +226 -114
- package/dist/tab-group.cjs.js.map +1 -1
- package/dist/tab-group.css +0 -76
- package/dist/tab-group.esm.js +227 -114
- package/dist/tab-group.esm.js.map +1 -1
- package/dist/tab-group.js +226 -114
- package/dist/tab-group.js.map +1 -1
- package/dist/tab-group.min.css +1 -1
- package/dist/tab-group.min.js +1 -1
- package/package.json +12 -13
- package/tab-group.d.ts +46 -0
- package/dist/scss/tab-group.scss +0 -125
- package/dist/scss/variables.scss +0 -0
- package/dist/tab-group.scss +0 -2
- package/src/index.scss +0 -2
- package/src/scss/tab-group.scss +0 -125
- package/src/scss/variables.scss +0 -0
- package/src/tab-group.js +0 -277
package/dist/tab-group.css
CHANGED
|
@@ -1,95 +1,19 @@
|
|
|
1
1
|
tab-group {
|
|
2
2
|
display: block;
|
|
3
|
-
width: 100%;
|
|
4
|
-
font-family: var(--font-family);
|
|
5
|
-
font-size: var(--font-size-base);
|
|
6
|
-
line-height: var(--line-height);
|
|
7
|
-
color: var(--color-text);
|
|
8
3
|
}
|
|
9
4
|
|
|
10
5
|
tab-list {
|
|
11
6
|
display: flex;
|
|
12
|
-
border-bottom: var(--tab-list-border-bottom, 1px solid var(--color-border));
|
|
13
|
-
margin-bottom: var(--spacing-md);
|
|
14
7
|
overflow-x: auto;
|
|
15
8
|
overflow-y: hidden;
|
|
16
|
-
padding: var(--tab-list-padding, 0.5rem 0.25rem 0);
|
|
17
|
-
gap: var(--tab-list-gap, 0.25rem);
|
|
18
|
-
justify-content: var(--tab-list-justify, flex-start);
|
|
19
|
-
background-color: var(--tab-list-background, transparent);
|
|
20
|
-
background-image: var(--tab-list-background-image, none);
|
|
21
|
-
border-radius: var(--tab-list-radius, 0);
|
|
22
9
|
}
|
|
23
10
|
|
|
24
11
|
tab-button {
|
|
25
12
|
display: block;
|
|
26
|
-
padding: var(--spacing-sm) var(--spacing-md);
|
|
27
|
-
margin: var(--spacing-xs);
|
|
28
|
-
border: 1px solid transparent;
|
|
29
|
-
border-bottom: none;
|
|
30
|
-
background-color: var(--color-background);
|
|
31
|
-
color: var(--color-text);
|
|
32
13
|
cursor: pointer;
|
|
33
|
-
border-radius: var(--tab-button-radius, var(--border-radius) var(--border-radius) 0 0);
|
|
34
|
-
white-space: nowrap;
|
|
35
14
|
user-select: none;
|
|
36
|
-
transition: all var(--transition-duration);
|
|
37
|
-
margin-bottom: -1px;
|
|
38
|
-
position: relative;
|
|
39
|
-
font-size: var(--font-size-base);
|
|
40
|
-
font-weight: var(--tab-button-font-weight, normal);
|
|
41
|
-
text-align: center;
|
|
42
|
-
min-width: 100px;
|
|
43
|
-
}
|
|
44
|
-
tab-button:hover {
|
|
45
|
-
background-color: var(--color-hover);
|
|
46
|
-
}
|
|
47
|
-
tab-button:focus {
|
|
48
|
-
outline: none;
|
|
49
|
-
box-shadow: var(--tab-button-focus-shadow, 0 0 0 2px var(--color-primary));
|
|
50
|
-
}
|
|
51
|
-
tab-button[aria-selected=true] {
|
|
52
|
-
background-color: var(--tab-active-background, var(--color-background));
|
|
53
|
-
border-color: var(--color-border);
|
|
54
|
-
border-bottom: 1px solid var(--color-background);
|
|
55
|
-
font-weight: var(--tab-active-font-weight, 600);
|
|
56
|
-
color: var(--tab-active-color, var(--color-primary));
|
|
57
|
-
box-shadow: var(--tab-active-shadow, none);
|
|
58
|
-
transform: var(--tab-active-transform, none);
|
|
59
|
-
}
|
|
60
|
-
tab-button[aria-selected=true]::after {
|
|
61
|
-
content: "";
|
|
62
|
-
position: absolute;
|
|
63
|
-
bottom: 0;
|
|
64
|
-
left: var(--tab-indicator-left, 0);
|
|
65
|
-
right: var(--tab-indicator-right, 0);
|
|
66
|
-
height: var(--tab-indicator-height, 2px);
|
|
67
|
-
background-color: var(--tab-indicator-color, var(--color-primary));
|
|
68
|
-
border-radius: var(--tab-indicator-radius, 0);
|
|
69
15
|
}
|
|
70
16
|
|
|
71
|
-
tab-panel {
|
|
72
|
-
display: block;
|
|
73
|
-
padding: var(--panel-padding, var(--spacing-md));
|
|
74
|
-
background-color: var(--panel-background, var(--color-background));
|
|
75
|
-
border-radius: var(--panel-radius, 0 0 var(--border-radius) var(--border-radius));
|
|
76
|
-
border: var(--panel-border, none);
|
|
77
|
-
border-top: var(--panel-border-top, none);
|
|
78
|
-
box-shadow: var(--panel-shadow, none);
|
|
79
|
-
transition: var(--panel-transition, all 0.3s ease);
|
|
80
|
-
margin: var(--panel-margin, 0);
|
|
81
|
-
min-height: var(--panel-min-height, 150px);
|
|
82
|
-
}
|
|
83
17
|
tab-panel[hidden] {
|
|
84
18
|
display: none;
|
|
85
19
|
}
|
|
86
|
-
|
|
87
|
-
@media (max-width: 768px) {
|
|
88
|
-
tab-list {
|
|
89
|
-
flex-wrap: wrap;
|
|
90
|
-
}
|
|
91
|
-
tab-button {
|
|
92
|
-
flex: 1 0 auto;
|
|
93
|
-
text-align: center;
|
|
94
|
-
}
|
|
95
|
-
}
|
package/dist/tab-group.esm.js
CHANGED
|
@@ -3,23 +3,13 @@
|
|
|
3
3
|
* A fully accessible tab group web component
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
let instanceCount = 0;
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* @class TabGroup
|
|
8
10
|
* the parent container that coordinates tabs and panels
|
|
9
11
|
*/
|
|
10
12
|
class TabGroup extends HTMLElement {
|
|
11
|
-
// static counter to ensure global unique ids for tabs and panels
|
|
12
|
-
static tabCount = 0;
|
|
13
|
-
static panelCount = 0;
|
|
14
|
-
|
|
15
|
-
constructor() {
|
|
16
|
-
super();
|
|
17
|
-
// ensure that the number of <tab-button> and <tab-panel> elements match
|
|
18
|
-
// note: in some scenarios the child elements might not be available in the constructor,
|
|
19
|
-
// so adjust as necessary or consider running this check in connectedCallback()
|
|
20
|
-
this.ensureConsistentTabsAndPanels();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
13
|
/**
|
|
24
14
|
* @function ensureConsistentTabsAndPanels
|
|
25
15
|
* makes sure there is an equal number of <tab-button> and <tab-panel> elements.
|
|
@@ -27,24 +17,24 @@ class TabGroup extends HTMLElement {
|
|
|
27
17
|
* if there are more tabs than panels, inject extra panels.
|
|
28
18
|
*/
|
|
29
19
|
ensureConsistentTabsAndPanels() {
|
|
30
|
-
// get current tabs and panels
|
|
31
|
-
let tabs = this.querySelectorAll(
|
|
32
|
-
let panels = this.querySelectorAll(
|
|
20
|
+
// get current tabs and panels scoped to direct children only
|
|
21
|
+
let tabs = this.querySelectorAll(':scope > tab-list > tab-button');
|
|
22
|
+
let panels = this.querySelectorAll(':scope > tab-panel');
|
|
33
23
|
|
|
34
24
|
// if there are more panels than tabs
|
|
35
25
|
if (panels.length > tabs.length) {
|
|
36
26
|
const difference = panels.length - tabs.length;
|
|
37
27
|
// try to find a <tab-list> to insert new tabs
|
|
38
|
-
let tabList = this.querySelector(
|
|
28
|
+
let tabList = this.querySelector(':scope > tab-list');
|
|
39
29
|
if (!tabList) {
|
|
40
30
|
// if not present, create one and insert it at the beginning
|
|
41
|
-
tabList = document.createElement(
|
|
31
|
+
tabList = document.createElement('tab-list');
|
|
42
32
|
this.insertBefore(tabList, this.firstChild);
|
|
43
33
|
}
|
|
44
34
|
// inject extra <tab-button> elements into the tab list
|
|
45
35
|
for (let i = 0; i < difference; i++) {
|
|
46
|
-
const newTab = document.createElement(
|
|
47
|
-
newTab.textContent =
|
|
36
|
+
const newTab = document.createElement('tab-button');
|
|
37
|
+
newTab.textContent = 'default tab';
|
|
48
38
|
tabList.appendChild(newTab);
|
|
49
39
|
}
|
|
50
40
|
}
|
|
@@ -53,8 +43,8 @@ class TabGroup extends HTMLElement {
|
|
|
53
43
|
const difference = tabs.length - panels.length;
|
|
54
44
|
// inject extra <tab-panel> elements at the end of the tab group
|
|
55
45
|
for (let i = 0; i < difference; i++) {
|
|
56
|
-
const newPanel = document.createElement(
|
|
57
|
-
newPanel.innerHTML =
|
|
46
|
+
const newPanel = document.createElement('tab-panel');
|
|
47
|
+
newPanel.innerHTML = '<p>default panel content</p>';
|
|
58
48
|
this.appendChild(newPanel);
|
|
59
49
|
}
|
|
60
50
|
}
|
|
@@ -64,58 +54,149 @@ class TabGroup extends HTMLElement {
|
|
|
64
54
|
* called when the element is connected to the dom
|
|
65
55
|
*/
|
|
66
56
|
connectedCallback() {
|
|
67
|
-
|
|
57
|
+
// assign a stable instance id on first connect
|
|
58
|
+
if (!this._instanceId) {
|
|
59
|
+
this._instanceId = `tg-${instanceCount++}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ensure that the number of <tab-button> and <tab-panel> elements match
|
|
63
|
+
this.ensureConsistentTabsAndPanels();
|
|
68
64
|
|
|
69
65
|
// find the <tab-list> element (should be exactly one)
|
|
70
|
-
|
|
71
|
-
if (!
|
|
66
|
+
this.tabList = this.querySelector(':scope > tab-list');
|
|
67
|
+
if (!this.tabList) return;
|
|
72
68
|
|
|
73
69
|
// find all <tab-button> elements inside the <tab-list>
|
|
74
|
-
|
|
70
|
+
this.tabButtons = Array.from(
|
|
71
|
+
this.tabList.querySelectorAll('tab-button')
|
|
72
|
+
);
|
|
75
73
|
|
|
76
74
|
// find all <tab-panel> elements inside the <tab-group>
|
|
77
|
-
|
|
75
|
+
this.tabPanels = Array.from(this.querySelectorAll(':scope > tab-panel'));
|
|
78
76
|
|
|
79
|
-
|
|
80
|
-
_.tabButtons.forEach((tab, index) => {
|
|
81
|
-
const tabIndex = TabGroup.tabCount++;
|
|
77
|
+
const prefix = this._instanceId;
|
|
82
78
|
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
// initialize each tab-button with roles, ids and aria attributes
|
|
80
|
+
this.tabButtons.forEach((tab, index) => {
|
|
81
|
+
const tabId = `${prefix}-tab-${index}`;
|
|
82
|
+
const panelId = `${prefix}-panel-${index}`;
|
|
85
83
|
tab.id = tabId;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const panelId = `panel-${tabIndex}`;
|
|
89
|
-
tab.setAttribute("role", "tab");
|
|
90
|
-
tab.setAttribute("aria-controls", panelId);
|
|
84
|
+
tab.setAttribute('role', 'tab');
|
|
85
|
+
tab.setAttribute('aria-controls', panelId);
|
|
91
86
|
|
|
92
87
|
// first tab is active by default
|
|
93
88
|
if (index === 0) {
|
|
94
|
-
tab.setAttribute(
|
|
95
|
-
tab.setAttribute(
|
|
89
|
+
tab.setAttribute('aria-selected', 'true');
|
|
90
|
+
tab.setAttribute('tabindex', '0');
|
|
96
91
|
} else {
|
|
97
|
-
tab.setAttribute(
|
|
98
|
-
tab.setAttribute(
|
|
92
|
+
tab.setAttribute('aria-selected', 'false');
|
|
93
|
+
tab.setAttribute('tabindex', '-1');
|
|
99
94
|
}
|
|
100
95
|
});
|
|
101
96
|
|
|
102
97
|
// initialize each tab-panel with roles, ids and aria attributes
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
const panelId = `panel-${panelIndex}`;
|
|
98
|
+
this.tabPanels.forEach((panel, index) => {
|
|
99
|
+
const panelId = `${prefix}-panel-${index}`;
|
|
106
100
|
panel.id = panelId;
|
|
107
|
-
|
|
108
|
-
panel.setAttribute(
|
|
109
|
-
panel.setAttribute("aria-labelledby", `tab-${panelIndex}`);
|
|
101
|
+
panel.setAttribute('role', 'tabpanel');
|
|
102
|
+
panel.setAttribute('aria-labelledby', `${prefix}-tab-${index}`);
|
|
110
103
|
|
|
111
104
|
// hide panels except for the first one
|
|
112
105
|
panel.hidden = index !== 0;
|
|
113
106
|
});
|
|
114
107
|
|
|
115
108
|
// set up keyboard navigation and click delegation on the <tab-list>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
109
|
+
this.tabList.setAttribute('role', 'tablist');
|
|
110
|
+
|
|
111
|
+
// store bound handlers so we can remove them in disconnectedCallback
|
|
112
|
+
if (!this._onKeyDown) {
|
|
113
|
+
this._onKeyDown = (e) => this.onKeyDown(e);
|
|
114
|
+
this._onClick = (e) => this.onClick(e);
|
|
115
|
+
}
|
|
116
|
+
this.tabList.addEventListener('keydown', this._onKeyDown);
|
|
117
|
+
this.tabList.addEventListener('click', this._onClick);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* called when the element is disconnected from the dom
|
|
122
|
+
*/
|
|
123
|
+
disconnectedCallback() {
|
|
124
|
+
if (this._animationController) {
|
|
125
|
+
this._animationController.abort();
|
|
126
|
+
this._animationController = null;
|
|
127
|
+
}
|
|
128
|
+
if (this.tabList && this._onKeyDown) {
|
|
129
|
+
this.tabList.removeEventListener('keydown', this._onKeyDown);
|
|
130
|
+
this.tabList.removeEventListener('click', this._onClick);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* reads animation attributes from the element
|
|
136
|
+
*/
|
|
137
|
+
_getAnimateConfig() {
|
|
138
|
+
const outClass = this.getAttribute('animate-out-class');
|
|
139
|
+
const inClass = this.getAttribute('animate-in-class');
|
|
140
|
+
const timeout = parseInt(this.getAttribute('animate-timeout'), 10) || 500;
|
|
141
|
+
return { outClass, inClass, timeout, hasAnimation: !!(outClass || inClass) };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* adds a class and waits for animationend (or timeout), with abort support
|
|
146
|
+
*/
|
|
147
|
+
_waitForAnimation(element, className, timeout, signal) {
|
|
148
|
+
return new Promise((resolve) => {
|
|
149
|
+
if (signal.aborted) {
|
|
150
|
+
resolve();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
element.classList.add(className);
|
|
155
|
+
|
|
156
|
+
let timer;
|
|
157
|
+
const cleanup = () => {
|
|
158
|
+
element.classList.remove(className);
|
|
159
|
+
clearTimeout(timer);
|
|
160
|
+
element.removeEventListener('animationend', onEnd);
|
|
161
|
+
signal.removeEventListener('abort', onAbort);
|
|
162
|
+
resolve();
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const onEnd = (e) => {
|
|
166
|
+
if (e.target === element) cleanup();
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const onAbort = () => cleanup();
|
|
170
|
+
|
|
171
|
+
element.addEventListener('animationend', onEnd);
|
|
172
|
+
signal.addEventListener('abort', onAbort);
|
|
173
|
+
timer = setTimeout(cleanup, timeout);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* orchestrates out-animation → swap → in-animation
|
|
179
|
+
*/
|
|
180
|
+
async _animateTransition(oldPanel, newPanel, config, controller) {
|
|
181
|
+
const { signal } = controller;
|
|
182
|
+
|
|
183
|
+
// Phase 1: animate out
|
|
184
|
+
if (config.outClass && oldPanel) {
|
|
185
|
+
await this._waitForAnimation(oldPanel, config.outClass, config.timeout, signal);
|
|
186
|
+
}
|
|
187
|
+
if (signal.aborted) return;
|
|
188
|
+
|
|
189
|
+
// Phase 2: swap hidden
|
|
190
|
+
if (oldPanel) oldPanel.hidden = true;
|
|
191
|
+
newPanel.hidden = false;
|
|
192
|
+
|
|
193
|
+
// Phase 3: animate in
|
|
194
|
+
if (config.inClass) {
|
|
195
|
+
if (signal.aborted) return;
|
|
196
|
+
// force reflow so the browser sees the element before animating
|
|
197
|
+
newPanel.offsetHeight;
|
|
198
|
+
await this._waitForAnimation(newPanel, config.inClass, config.timeout, signal);
|
|
199
|
+
}
|
|
119
200
|
}
|
|
120
201
|
|
|
121
202
|
/**
|
|
@@ -124,35 +205,86 @@ class TabGroup extends HTMLElement {
|
|
|
124
205
|
* @param {number} index - index of the tab to activate
|
|
125
206
|
*/
|
|
126
207
|
setActiveTab(index) {
|
|
127
|
-
|
|
128
|
-
const previousIndex =
|
|
208
|
+
if (index < 0 || index >= this.tabButtons.length) return;
|
|
209
|
+
const previousIndex = this.tabButtons.findIndex(
|
|
210
|
+
(tab) => tab.getAttribute('aria-selected') === 'true'
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// cancel any in-flight animation
|
|
214
|
+
if (this._animationController) {
|
|
215
|
+
this._animationController.abort();
|
|
216
|
+
this._animationController = null;
|
|
217
|
+
// force-hide all panels (clean slate)
|
|
218
|
+
this.tabPanels.forEach((panel) => {
|
|
219
|
+
panel.hidden = true;
|
|
220
|
+
});
|
|
221
|
+
}
|
|
129
222
|
|
|
130
|
-
// update each tab-button
|
|
131
|
-
|
|
223
|
+
// update each tab-button (ARIA updates fire immediately)
|
|
224
|
+
this.tabButtons.forEach((tab, i) => {
|
|
132
225
|
const isActive = i === index;
|
|
133
|
-
tab.setAttribute(
|
|
134
|
-
tab.setAttribute(
|
|
226
|
+
tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
|
227
|
+
tab.setAttribute('tabindex', isActive ? '0' : '-1');
|
|
135
228
|
if (isActive) {
|
|
136
229
|
tab.focus();
|
|
137
230
|
}
|
|
138
231
|
});
|
|
139
232
|
|
|
140
|
-
// update each tab-panel
|
|
141
|
-
_.tabPanels.forEach((panel, i) => {
|
|
142
|
-
panel.hidden = i !== index;
|
|
143
|
-
});
|
|
144
|
-
|
|
145
233
|
// dispatch event only if the tab actually changed
|
|
146
234
|
if (previousIndex !== index) {
|
|
147
235
|
const detail = {
|
|
148
236
|
previousIndex,
|
|
149
237
|
currentIndex: index,
|
|
150
|
-
previousTab:
|
|
151
|
-
currentTab:
|
|
152
|
-
previousPanel:
|
|
153
|
-
currentPanel:
|
|
238
|
+
previousTab: this.tabButtons[previousIndex],
|
|
239
|
+
currentTab: this.tabButtons[index],
|
|
240
|
+
previousPanel: this.tabPanels[previousIndex],
|
|
241
|
+
currentPanel: this.tabPanels[index],
|
|
154
242
|
};
|
|
155
|
-
|
|
243
|
+
this.dispatchEvent(
|
|
244
|
+
new CustomEvent('tabchange', { detail, bubbles: true })
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const config = this._getAnimateConfig();
|
|
249
|
+
const oldPanel = previousIndex >= 0 ? this.tabPanels[previousIndex] : null;
|
|
250
|
+
const newPanel = this.tabPanels[index];
|
|
251
|
+
|
|
252
|
+
if (!config.hasAnimation || previousIndex === index) {
|
|
253
|
+
// instant switch (original behavior)
|
|
254
|
+
this.tabPanels.forEach((panel, i) => {
|
|
255
|
+
panel.hidden = i !== index;
|
|
256
|
+
});
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// animated transition
|
|
261
|
+
const controller = new AbortController();
|
|
262
|
+
this._animationController = controller;
|
|
263
|
+
|
|
264
|
+
// old panel was already force-hidden by abort above, so if we aborted
|
|
265
|
+
// a previous animation, skip animate-out (old panel is already gone)
|
|
266
|
+
const skipOut = oldPanel && oldPanel.hidden;
|
|
267
|
+
|
|
268
|
+
if (skipOut) {
|
|
269
|
+
// just animate in the new panel
|
|
270
|
+
newPanel.hidden = false;
|
|
271
|
+
if (config.inClass) {
|
|
272
|
+
newPanel.offsetHeight;
|
|
273
|
+
this._waitForAnimation(newPanel, config.inClass, config.timeout, controller.signal).then(() => {
|
|
274
|
+
if (this._animationController === controller) {
|
|
275
|
+
this._animationController = null;
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
} else {
|
|
279
|
+
this._animationController = null;
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
// full out → swap → in sequence
|
|
283
|
+
this._animateTransition(oldPanel, newPanel, config, controller).then(() => {
|
|
284
|
+
if (this._animationController === controller) {
|
|
285
|
+
this._animationController = null;
|
|
286
|
+
}
|
|
287
|
+
});
|
|
156
288
|
}
|
|
157
289
|
}
|
|
158
290
|
|
|
@@ -162,17 +294,16 @@ class TabGroup extends HTMLElement {
|
|
|
162
294
|
* @param {MouseEvent} e - the click event
|
|
163
295
|
*/
|
|
164
296
|
onClick(e) {
|
|
165
|
-
const _ = this;
|
|
166
297
|
// check if the click occurred on or within a <tab-button>
|
|
167
|
-
const tabButton = e.target.closest(
|
|
298
|
+
const tabButton = e.target.closest('tab-button');
|
|
168
299
|
if (!tabButton) return;
|
|
169
300
|
|
|
170
301
|
// determine the index of the clicked tab-button
|
|
171
|
-
const index =
|
|
302
|
+
const index = this.tabButtons.indexOf(tabButton);
|
|
172
303
|
if (index === -1) return;
|
|
173
304
|
|
|
174
305
|
// activate the tab with the corresponding index
|
|
175
|
-
|
|
306
|
+
this.setActiveTab(index);
|
|
176
307
|
}
|
|
177
308
|
|
|
178
309
|
/**
|
|
@@ -181,39 +312,39 @@ class TabGroup extends HTMLElement {
|
|
|
181
312
|
* @param {KeyboardEvent} e - the keydown event
|
|
182
313
|
*/
|
|
183
314
|
onKeyDown(e) {
|
|
184
|
-
const _ = this;
|
|
185
315
|
// only process keys if focus is on a <tab-button>
|
|
186
|
-
const targetIndex =
|
|
316
|
+
const targetIndex = this.tabButtons.indexOf(e.target);
|
|
187
317
|
if (targetIndex === -1) return;
|
|
188
318
|
|
|
189
319
|
let newIndex = targetIndex;
|
|
190
320
|
switch (e.key) {
|
|
191
|
-
case
|
|
192
|
-
case
|
|
321
|
+
case 'ArrowLeft':
|
|
322
|
+
case 'ArrowUp':
|
|
193
323
|
// move to the previous tab (wrap around if necessary)
|
|
194
|
-
newIndex =
|
|
324
|
+
newIndex =
|
|
325
|
+
targetIndex > 0 ? targetIndex - 1 : this.tabButtons.length - 1;
|
|
195
326
|
e.preventDefault();
|
|
196
327
|
break;
|
|
197
|
-
case
|
|
198
|
-
case
|
|
328
|
+
case 'ArrowRight':
|
|
329
|
+
case 'ArrowDown':
|
|
199
330
|
// move to the next tab (wrap around if necessary)
|
|
200
|
-
newIndex = (targetIndex + 1) %
|
|
331
|
+
newIndex = (targetIndex + 1) % this.tabButtons.length;
|
|
201
332
|
e.preventDefault();
|
|
202
333
|
break;
|
|
203
|
-
case
|
|
334
|
+
case 'Home':
|
|
204
335
|
// jump to the first tab
|
|
205
336
|
newIndex = 0;
|
|
206
337
|
e.preventDefault();
|
|
207
338
|
break;
|
|
208
|
-
case
|
|
339
|
+
case 'End':
|
|
209
340
|
// jump to the last tab
|
|
210
|
-
newIndex =
|
|
341
|
+
newIndex = this.tabButtons.length - 1;
|
|
211
342
|
e.preventDefault();
|
|
212
343
|
break;
|
|
213
344
|
default:
|
|
214
345
|
return; // ignore other keys
|
|
215
346
|
}
|
|
216
|
-
|
|
347
|
+
this.setActiveTab(newIndex);
|
|
217
348
|
}
|
|
218
349
|
}
|
|
219
350
|
|
|
@@ -221,49 +352,31 @@ class TabGroup extends HTMLElement {
|
|
|
221
352
|
* @class TabList
|
|
222
353
|
* a container for the <tab-button> elements
|
|
223
354
|
*/
|
|
224
|
-
class TabList extends HTMLElement {
|
|
225
|
-
constructor() {
|
|
226
|
-
super();
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
connectedCallback() {
|
|
230
|
-
// additional logic or styling can be added here if desired
|
|
231
|
-
}
|
|
232
|
-
}
|
|
355
|
+
class TabList extends HTMLElement {}
|
|
233
356
|
|
|
234
357
|
/**
|
|
235
358
|
* @class TabButton
|
|
236
359
|
* a single tab button element
|
|
237
360
|
*/
|
|
238
|
-
class TabButton extends HTMLElement {
|
|
239
|
-
constructor() {
|
|
240
|
-
super();
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
connectedCallback() {
|
|
244
|
-
// note: role and other attributes are handled by the parent
|
|
245
|
-
}
|
|
246
|
-
}
|
|
361
|
+
class TabButton extends HTMLElement {}
|
|
247
362
|
|
|
248
363
|
/**
|
|
249
364
|
* @class TabPanel
|
|
250
365
|
* a single tab panel element
|
|
251
366
|
*/
|
|
252
|
-
class TabPanel extends HTMLElement {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
367
|
+
class TabPanel extends HTMLElement {}
|
|
368
|
+
|
|
369
|
+
// define the custom elements (guarded against double-registration and SSR)
|
|
370
|
+
if (typeof window !== 'undefined' && window.customElements) {
|
|
371
|
+
if (!customElements.get('tab-group'))
|
|
372
|
+
customElements.define('tab-group', TabGroup);
|
|
373
|
+
if (!customElements.get('tab-list'))
|
|
374
|
+
customElements.define('tab-list', TabList);
|
|
375
|
+
if (!customElements.get('tab-button'))
|
|
376
|
+
customElements.define('tab-button', TabButton);
|
|
377
|
+
if (!customElements.get('tab-panel'))
|
|
378
|
+
customElements.define('tab-panel', TabPanel);
|
|
260
379
|
}
|
|
261
380
|
|
|
262
|
-
|
|
263
|
-
customElements.define("tab-group", TabGroup);
|
|
264
|
-
customElements.define("tab-list", TabList);
|
|
265
|
-
customElements.define("tab-button", TabButton);
|
|
266
|
-
customElements.define("tab-panel", TabPanel);
|
|
267
|
-
|
|
268
|
-
export { TabGroup, TabGroup as default };
|
|
381
|
+
export { TabGroup as default };
|
|
269
382
|
//# sourceMappingURL=tab-group.esm.js.map
|