@magic-spells/tab-group 0.1.0 → 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 +37 -188
- package/dist/tab-group.cjs.js +102 -109
- package/dist/tab-group.cjs.js.map +1 -1
- package/dist/tab-group.css +0 -76
- package/dist/tab-group.esm.js +103 -109
- package/dist/tab-group.esm.js.map +1 -1
- package/dist/tab-group.js +102 -109
- 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 +33 -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/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2026 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
CHANGED
|
@@ -1,46 +1,35 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Tab Group
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|

|
|
5
5
|
|
|
6
|
-
A lightweight, accessible tab interface web component with keyboard navigation
|
|
6
|
+
A lightweight, accessible tab interface web component with keyboard navigation. Ships only structural CSS — you bring your own styles.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
[Live Demo](https://magic-spells.github.io/tab-group/demo/) - See it in action!
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Features
|
|
11
11
|
|
|
12
12
|
- **Fully Accessible** - Built following WAI-ARIA Tab pattern guidelines
|
|
13
13
|
- **Custom Events** - Listen for tab changes with detailed event data
|
|
14
14
|
- **Keyboard Navigation** - Complete keyboard support for accessibility
|
|
15
15
|
- **Auto Consistency** - Ensures tab buttons and panels stay in sync
|
|
16
|
-
- **
|
|
16
|
+
- **Zero Opinions** - No cosmetic CSS; style it however you want
|
|
17
17
|
- **Zero Dependencies** - Lightweight and standalone
|
|
18
18
|
- **Easy Integration** - Works with any framework or vanilla JS
|
|
19
19
|
|
|
20
|
-
##
|
|
20
|
+
## Installation
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
# npm
|
|
24
23
|
npm install @magic-spells/tab-group
|
|
25
|
-
|
|
26
|
-
# yarn
|
|
27
|
-
yarn add @magic-spells/tab-group
|
|
28
|
-
|
|
29
|
-
# pnpm
|
|
30
|
-
pnpm add @magic-spells/tab-group
|
|
31
24
|
```
|
|
32
25
|
|
|
33
|
-
##
|
|
34
|
-
|
|
35
|
-
### Basic Implementation
|
|
26
|
+
## Usage
|
|
36
27
|
|
|
37
28
|
```html
|
|
38
|
-
<!-- Import the component -->
|
|
39
29
|
<script type="module">
|
|
40
30
|
import '@magic-spells/tab-group';
|
|
41
31
|
</script>
|
|
42
32
|
|
|
43
|
-
<!-- Use the component -->
|
|
44
33
|
<tab-group>
|
|
45
34
|
<tab-list>
|
|
46
35
|
<tab-button>First Tab</tab-button>
|
|
@@ -71,190 +60,50 @@ pnpm add @magic-spells/tab-group
|
|
|
71
60
|
document
|
|
72
61
|
.querySelector('tab-group')
|
|
73
62
|
.addEventListener('tabchange', (event) => {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// Access detailed event data
|
|
77
|
-
const {
|
|
78
|
-
previousIndex, // Index of previous tab
|
|
79
|
-
currentIndex, // Index of current tab
|
|
80
|
-
previousTab, // Previous tab element
|
|
81
|
-
currentTab, // Current tab element
|
|
82
|
-
previousPanel, // Previous panel element
|
|
83
|
-
currentPanel, // Current panel element
|
|
84
|
-
} = event.detail;
|
|
85
|
-
|
|
86
|
-
// Do something with the data
|
|
87
|
-
console.log(
|
|
88
|
-
`Changed from tab ${previousIndex} to tab ${currentIndex}`
|
|
89
|
-
);
|
|
63
|
+
const { previousIndex, currentIndex } = event.detail;
|
|
64
|
+
console.log(`Changed from tab ${previousIndex} to tab ${currentIndex}`);
|
|
90
65
|
});
|
|
91
66
|
```
|
|
92
67
|
|
|
93
|
-
##
|
|
68
|
+
## Styling
|
|
94
69
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
The Tab Group component can be extensively customized using CSS variables:
|
|
70
|
+
The component ships only structural CSS (display modes, overflow, hidden state). All visual styling is yours. Target the custom elements and ARIA attributes directly:
|
|
98
71
|
|
|
99
72
|
```css
|
|
100
|
-
tab-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
--color-text: #333333;
|
|
104
|
-
--color-border: #dddddd;
|
|
105
|
-
--color-border-hover: #bbbbbb;
|
|
106
|
-
--color-primary: #3366ff;
|
|
107
|
-
--color-hover: #f0f5ff;
|
|
108
|
-
--color-focus: #b3cbff;
|
|
109
|
-
|
|
110
|
-
/* Panel styling */
|
|
111
|
-
--panel-background: white;
|
|
112
|
-
--panel-border: 1px solid var(--color-border);
|
|
113
|
-
--panel-padding: 1rem;
|
|
114
|
-
--panel-radius: 0 0 0.5rem 0.5rem;
|
|
115
|
-
--panel-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
116
|
-
|
|
117
|
-
/* Tab list styling */
|
|
118
|
-
--tab-list-gap: 0.25rem;
|
|
119
|
-
--tab-list-padding: 0.5rem 0.5rem 0;
|
|
120
|
-
--tab-list-background: transparent;
|
|
121
|
-
--tab-list-border-bottom: 1px solid var(--color-border);
|
|
122
|
-
--tab-list-radius: 0.5rem 0.5rem 0 0;
|
|
123
|
-
|
|
124
|
-
/* Tab button styling */
|
|
125
|
-
--tab-button-radius: 0.25rem 0.25rem 0 0;
|
|
126
|
-
--tab-active-background: white;
|
|
127
|
-
--tab-active-color: var(--color-primary);
|
|
128
|
-
--tab-active-font-weight: 500;
|
|
129
|
-
--tab-active-shadow: none;
|
|
130
|
-
--tab-active-transform: translateY(0);
|
|
131
|
-
|
|
132
|
-
/* Tab indicator styling */
|
|
133
|
-
--tab-indicator-height: 2px;
|
|
134
|
-
--tab-indicator-color: var(--color-primary);
|
|
135
|
-
--tab-indicator-left: 0;
|
|
136
|
-
--tab-indicator-right: 0;
|
|
73
|
+
tab-list {
|
|
74
|
+
gap: 0.25rem;
|
|
75
|
+
border-bottom: 1px solid #ddd;
|
|
137
76
|
}
|
|
138
|
-
```
|
|
139
77
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
```scss
|
|
145
|
-
// Option 1: Use the main entry point (recommended)
|
|
146
|
-
@use "@magic-spells/tab-group/scss" with (
|
|
147
|
-
$color-primary: #3366ff,
|
|
148
|
-
$border-radius: 0.5rem
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
// Option 2: Import individual files
|
|
152
|
-
@use "@magic-spells/tab-group/scss/variables" with (
|
|
153
|
-
$color-primary: #3366ff,
|
|
154
|
-
$border-radius: 0.5rem
|
|
155
|
-
);
|
|
156
|
-
@use "@magic-spells/tab-group/scss/tab-group";
|
|
78
|
+
tab-button {
|
|
79
|
+
padding: 0.5rem 1rem;
|
|
80
|
+
border-bottom: 2px solid transparent;
|
|
81
|
+
}
|
|
157
82
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
83
|
+
tab-button[aria-selected="true"] {
|
|
84
|
+
color: #3366ff;
|
|
85
|
+
border-bottom-color: #3366ff;
|
|
86
|
+
}
|
|
162
87
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
```scss
|
|
168
|
-
// Colors
|
|
169
|
-
$color-background: #ffffff !default;
|
|
170
|
-
$color-text: #333333 !default;
|
|
171
|
-
$color-border: #dddddd !default;
|
|
172
|
-
$color-border-hover: #bbbbbb !default;
|
|
173
|
-
$color-border-dark: #999999 !default;
|
|
174
|
-
$color-primary: #3366ff !default;
|
|
175
|
-
$color-hover: #f0f5ff !default;
|
|
176
|
-
$color-focus: #b3cbff !default;
|
|
177
|
-
|
|
178
|
-
// Border radius
|
|
179
|
-
$border-radius: 0.5rem !default;
|
|
180
|
-
|
|
181
|
-
// Box shadow
|
|
182
|
-
$box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !default;
|
|
183
|
-
|
|
184
|
-
// Tab list
|
|
185
|
-
$tab-list-gap: 0.25rem !default;
|
|
186
|
-
$tab-list-padding: 0.5rem 0.5rem 0 !default;
|
|
187
|
-
$tab-list-background: transparent !default;
|
|
188
|
-
$tab-list-border-bottom: 1px solid $color-border !default;
|
|
189
|
-
$tab-list-radius: $border-radius $border-radius 0 0 !default;
|
|
190
|
-
|
|
191
|
-
// Tab button
|
|
192
|
-
$tab-button-radius: 0.25rem 0.25rem 0 0 !default;
|
|
193
|
-
$tab-active-background: white !default;
|
|
194
|
-
$tab-active-color: $color-primary !default;
|
|
195
|
-
$tab-active-font-weight: 500 !default;
|
|
196
|
-
$tab-active-shadow: none !default;
|
|
197
|
-
$tab-active-transform: translateY(0) !default;
|
|
198
|
-
|
|
199
|
-
// Tab indicator
|
|
200
|
-
$tab-indicator-height: 2px !default;
|
|
201
|
-
$tab-indicator-color: $color-primary !default;
|
|
202
|
-
$tab-indicator-left: 0 !default;
|
|
203
|
-
$tab-indicator-right: 0 !default;
|
|
204
|
-
|
|
205
|
-
// Panel
|
|
206
|
-
$panel-background: white !default;
|
|
207
|
-
$panel-border: 1px solid $color-border !default;
|
|
208
|
-
$panel-padding: 1rem !default;
|
|
209
|
-
$panel-radius: 0 0 $border-radius $border-radius !default;
|
|
210
|
-
$panel-shadow: $box-shadow !default;
|
|
88
|
+
tab-panel {
|
|
89
|
+
padding: 1rem;
|
|
90
|
+
}
|
|
211
91
|
```
|
|
212
92
|
|
|
213
|
-
|
|
93
|
+
Or use Tailwind utility classes — no overrides needed.
|
|
214
94
|
|
|
215
|
-
###
|
|
95
|
+
### What the component CSS includes
|
|
216
96
|
|
|
217
97
|
```css
|
|
218
|
-
tab-group
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
--color-border-hover: #718096;
|
|
223
|
-
--color-primary: #4299e1;
|
|
224
|
-
--color-hover: #1a202c;
|
|
225
|
-
--color-focus: #2c5282;
|
|
226
|
-
--panel-background: #1e293b;
|
|
227
|
-
--panel-border: 1px solid #4a5568;
|
|
228
|
-
--panel-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
|
229
|
-
--tab-active-color: #63b3ed;
|
|
230
|
-
--tab-indicator-color: #63b3ed;
|
|
231
|
-
}
|
|
98
|
+
tab-group { display: block; }
|
|
99
|
+
tab-list { display: flex; overflow-x: auto; overflow-y: hidden; }
|
|
100
|
+
tab-button { display: block; cursor: pointer; user-select: none; }
|
|
101
|
+
tab-panel[hidden] { display: none; }
|
|
232
102
|
```
|
|
233
103
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
```css
|
|
237
|
-
tab-group {
|
|
238
|
-
--border-radius: 0.5rem;
|
|
239
|
-
--color-primary: #9f7aea;
|
|
240
|
-
--color-focus: #e9d8fd;
|
|
241
|
-
--tab-button-radius: 999px;
|
|
242
|
-
--tab-list-padding: 0.75rem 0.5rem 0;
|
|
243
|
-
--tab-list-gap: 0.5rem;
|
|
244
|
-
--panel-radius: 1rem;
|
|
245
|
-
--panel-shadow: 0 4px 12px rgba(159, 122, 234, 0.15);
|
|
246
|
-
--panel-border: 1px solid #e9d8fd;
|
|
247
|
-
--panel-padding: 1.5rem;
|
|
248
|
-
--tab-active-background: #9f7aea;
|
|
249
|
-
--tab-active-color: white;
|
|
250
|
-
--tab-active-font-weight: 500;
|
|
251
|
-
--tab-active-transform: translateY(-3px);
|
|
252
|
-
--tab-active-shadow: 0 4px 8px rgba(159, 122, 234, 0.3);
|
|
253
|
-
--tab-indicator-height: 0;
|
|
254
|
-
}
|
|
255
|
-
```
|
|
104
|
+
That's it. No fonts, colors, spacing, borders, transitions, shadows, or media queries.
|
|
256
105
|
|
|
257
|
-
##
|
|
106
|
+
## Accessibility
|
|
258
107
|
|
|
259
108
|
The Tab Group component follows the [WAI-ARIA Tabs Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) with:
|
|
260
109
|
|
|
@@ -266,7 +115,7 @@ The Tab Group component follows the [WAI-ARIA Tabs Pattern](https://www.w3.org/W
|
|
|
266
115
|
- `Home` to move to the first tab
|
|
267
116
|
- `End` to move to the last tab
|
|
268
117
|
|
|
269
|
-
##
|
|
118
|
+
## API Reference
|
|
270
119
|
|
|
271
120
|
### Components
|
|
272
121
|
|
|
@@ -283,16 +132,16 @@ The Tab Group component follows the [WAI-ARIA Tabs Pattern](https://www.w3.org/W
|
|
|
283
132
|
| ----------- | --------------------------------------------------------------------------------------- | --------------------------------- |
|
|
284
133
|
| `tabchange` | `{ previousIndex, currentIndex, previousTab, currentTab, previousPanel, currentPanel }` | Fired when the active tab changes |
|
|
285
134
|
|
|
286
|
-
##
|
|
135
|
+
## Examples
|
|
287
136
|
|
|
288
137
|
Check out more examples in the [demo directory](https://github.com/magic-spells/tab-group/tree/main/demo).
|
|
289
138
|
|
|
290
|
-
##
|
|
139
|
+
## License
|
|
291
140
|
|
|
292
141
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
293
142
|
|
|
294
143
|
---
|
|
295
144
|
|
|
296
145
|
<p align="center">
|
|
297
|
-
Made with
|
|
146
|
+
Made with magic by <a href="https://github.com/cory-schulz">Cory Schulz</a>
|
|
298
147
|
</p>
|
package/dist/tab-group.cjs.js
CHANGED
|
@@ -7,23 +7,13 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
7
7
|
* A fully accessible tab group web component
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
let instanceCount = 0;
|
|
11
|
+
|
|
10
12
|
/**
|
|
11
13
|
* @class TabGroup
|
|
12
14
|
* the parent container that coordinates tabs and panels
|
|
13
15
|
*/
|
|
14
16
|
class TabGroup extends HTMLElement {
|
|
15
|
-
// static counter to ensure global unique ids for tabs and panels
|
|
16
|
-
static tabCount = 0;
|
|
17
|
-
static panelCount = 0;
|
|
18
|
-
|
|
19
|
-
constructor() {
|
|
20
|
-
super();
|
|
21
|
-
// ensure that the number of <tab-button> and <tab-panel> elements match
|
|
22
|
-
// note: in some scenarios the child elements might not be available in the constructor,
|
|
23
|
-
// so adjust as necessary or consider running this check in connectedCallback()
|
|
24
|
-
this.ensureConsistentTabsAndPanels();
|
|
25
|
-
}
|
|
26
|
-
|
|
27
17
|
/**
|
|
28
18
|
* @function ensureConsistentTabsAndPanels
|
|
29
19
|
* makes sure there is an equal number of <tab-button> and <tab-panel> elements.
|
|
@@ -31,24 +21,24 @@ class TabGroup extends HTMLElement {
|
|
|
31
21
|
* if there are more tabs than panels, inject extra panels.
|
|
32
22
|
*/
|
|
33
23
|
ensureConsistentTabsAndPanels() {
|
|
34
|
-
// get current tabs and panels
|
|
35
|
-
let tabs = this.querySelectorAll(
|
|
36
|
-
let panels = this.querySelectorAll(
|
|
24
|
+
// get current tabs and panels scoped to direct children only
|
|
25
|
+
let tabs = this.querySelectorAll(':scope > tab-list > tab-button');
|
|
26
|
+
let panels = this.querySelectorAll(':scope > tab-panel');
|
|
37
27
|
|
|
38
28
|
// if there are more panels than tabs
|
|
39
29
|
if (panels.length > tabs.length) {
|
|
40
30
|
const difference = panels.length - tabs.length;
|
|
41
31
|
// try to find a <tab-list> to insert new tabs
|
|
42
|
-
let tabList = this.querySelector(
|
|
32
|
+
let tabList = this.querySelector(':scope > tab-list');
|
|
43
33
|
if (!tabList) {
|
|
44
34
|
// if not present, create one and insert it at the beginning
|
|
45
|
-
tabList = document.createElement(
|
|
35
|
+
tabList = document.createElement('tab-list');
|
|
46
36
|
this.insertBefore(tabList, this.firstChild);
|
|
47
37
|
}
|
|
48
38
|
// inject extra <tab-button> elements into the tab list
|
|
49
39
|
for (let i = 0; i < difference; i++) {
|
|
50
|
-
const newTab = document.createElement(
|
|
51
|
-
newTab.textContent =
|
|
40
|
+
const newTab = document.createElement('tab-button');
|
|
41
|
+
newTab.textContent = 'default tab';
|
|
52
42
|
tabList.appendChild(newTab);
|
|
53
43
|
}
|
|
54
44
|
}
|
|
@@ -57,8 +47,8 @@ class TabGroup extends HTMLElement {
|
|
|
57
47
|
const difference = tabs.length - panels.length;
|
|
58
48
|
// inject extra <tab-panel> elements at the end of the tab group
|
|
59
49
|
for (let i = 0; i < difference; i++) {
|
|
60
|
-
const newPanel = document.createElement(
|
|
61
|
-
newPanel.innerHTML =
|
|
50
|
+
const newPanel = document.createElement('tab-panel');
|
|
51
|
+
newPanel.innerHTML = '<p>default panel content</p>';
|
|
62
52
|
this.appendChild(newPanel);
|
|
63
53
|
}
|
|
64
54
|
}
|
|
@@ -68,58 +58,77 @@ class TabGroup extends HTMLElement {
|
|
|
68
58
|
* called when the element is connected to the dom
|
|
69
59
|
*/
|
|
70
60
|
connectedCallback() {
|
|
71
|
-
|
|
61
|
+
// assign a stable instance id on first connect
|
|
62
|
+
if (!this._instanceId) {
|
|
63
|
+
this._instanceId = `tg-${instanceCount++}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ensure that the number of <tab-button> and <tab-panel> elements match
|
|
67
|
+
this.ensureConsistentTabsAndPanels();
|
|
72
68
|
|
|
73
69
|
// find the <tab-list> element (should be exactly one)
|
|
74
|
-
|
|
75
|
-
if (!
|
|
70
|
+
this.tabList = this.querySelector(':scope > tab-list');
|
|
71
|
+
if (!this.tabList) return;
|
|
76
72
|
|
|
77
73
|
// find all <tab-button> elements inside the <tab-list>
|
|
78
|
-
|
|
74
|
+
this.tabButtons = Array.from(
|
|
75
|
+
this.tabList.querySelectorAll('tab-button')
|
|
76
|
+
);
|
|
79
77
|
|
|
80
78
|
// find all <tab-panel> elements inside the <tab-group>
|
|
81
|
-
|
|
79
|
+
this.tabPanels = Array.from(this.querySelectorAll(':scope > tab-panel'));
|
|
82
80
|
|
|
83
|
-
|
|
84
|
-
_.tabButtons.forEach((tab, index) => {
|
|
85
|
-
const tabIndex = TabGroup.tabCount++;
|
|
81
|
+
const prefix = this._instanceId;
|
|
86
82
|
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
// initialize each tab-button with roles, ids and aria attributes
|
|
84
|
+
this.tabButtons.forEach((tab, index) => {
|
|
85
|
+
const tabId = `${prefix}-tab-${index}`;
|
|
86
|
+
const panelId = `${prefix}-panel-${index}`;
|
|
89
87
|
tab.id = tabId;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const panelId = `panel-${tabIndex}`;
|
|
93
|
-
tab.setAttribute("role", "tab");
|
|
94
|
-
tab.setAttribute("aria-controls", panelId);
|
|
88
|
+
tab.setAttribute('role', 'tab');
|
|
89
|
+
tab.setAttribute('aria-controls', panelId);
|
|
95
90
|
|
|
96
91
|
// first tab is active by default
|
|
97
92
|
if (index === 0) {
|
|
98
|
-
tab.setAttribute(
|
|
99
|
-
tab.setAttribute(
|
|
93
|
+
tab.setAttribute('aria-selected', 'true');
|
|
94
|
+
tab.setAttribute('tabindex', '0');
|
|
100
95
|
} else {
|
|
101
|
-
tab.setAttribute(
|
|
102
|
-
tab.setAttribute(
|
|
96
|
+
tab.setAttribute('aria-selected', 'false');
|
|
97
|
+
tab.setAttribute('tabindex', '-1');
|
|
103
98
|
}
|
|
104
99
|
});
|
|
105
100
|
|
|
106
101
|
// initialize each tab-panel with roles, ids and aria attributes
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
const panelId = `panel-${panelIndex}`;
|
|
102
|
+
this.tabPanels.forEach((panel, index) => {
|
|
103
|
+
const panelId = `${prefix}-panel-${index}`;
|
|
110
104
|
panel.id = panelId;
|
|
111
|
-
|
|
112
|
-
panel.setAttribute(
|
|
113
|
-
panel.setAttribute("aria-labelledby", `tab-${panelIndex}`);
|
|
105
|
+
panel.setAttribute('role', 'tabpanel');
|
|
106
|
+
panel.setAttribute('aria-labelledby', `${prefix}-tab-${index}`);
|
|
114
107
|
|
|
115
108
|
// hide panels except for the first one
|
|
116
109
|
panel.hidden = index !== 0;
|
|
117
110
|
});
|
|
118
111
|
|
|
119
112
|
// set up keyboard navigation and click delegation on the <tab-list>
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
113
|
+
this.tabList.setAttribute('role', 'tablist');
|
|
114
|
+
|
|
115
|
+
// store bound handlers so we can remove them in disconnectedCallback
|
|
116
|
+
if (!this._onKeyDown) {
|
|
117
|
+
this._onKeyDown = (e) => this.onKeyDown(e);
|
|
118
|
+
this._onClick = (e) => this.onClick(e);
|
|
119
|
+
}
|
|
120
|
+
this.tabList.addEventListener('keydown', this._onKeyDown);
|
|
121
|
+
this.tabList.addEventListener('click', this._onClick);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* called when the element is disconnected from the dom
|
|
126
|
+
*/
|
|
127
|
+
disconnectedCallback() {
|
|
128
|
+
if (this.tabList && this._onKeyDown) {
|
|
129
|
+
this.tabList.removeEventListener('keydown', this._onKeyDown);
|
|
130
|
+
this.tabList.removeEventListener('click', this._onClick);
|
|
131
|
+
}
|
|
123
132
|
}
|
|
124
133
|
|
|
125
134
|
/**
|
|
@@ -128,21 +137,23 @@ class TabGroup extends HTMLElement {
|
|
|
128
137
|
* @param {number} index - index of the tab to activate
|
|
129
138
|
*/
|
|
130
139
|
setActiveTab(index) {
|
|
131
|
-
|
|
132
|
-
const previousIndex =
|
|
140
|
+
if (index < 0 || index >= this.tabButtons.length) return;
|
|
141
|
+
const previousIndex = this.tabButtons.findIndex(
|
|
142
|
+
(tab) => tab.getAttribute('aria-selected') === 'true'
|
|
143
|
+
);
|
|
133
144
|
|
|
134
145
|
// update each tab-button
|
|
135
|
-
|
|
146
|
+
this.tabButtons.forEach((tab, i) => {
|
|
136
147
|
const isActive = i === index;
|
|
137
|
-
tab.setAttribute(
|
|
138
|
-
tab.setAttribute(
|
|
148
|
+
tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
|
149
|
+
tab.setAttribute('tabindex', isActive ? '0' : '-1');
|
|
139
150
|
if (isActive) {
|
|
140
151
|
tab.focus();
|
|
141
152
|
}
|
|
142
153
|
});
|
|
143
154
|
|
|
144
155
|
// update each tab-panel
|
|
145
|
-
|
|
156
|
+
this.tabPanels.forEach((panel, i) => {
|
|
146
157
|
panel.hidden = i !== index;
|
|
147
158
|
});
|
|
148
159
|
|
|
@@ -151,12 +162,14 @@ class TabGroup extends HTMLElement {
|
|
|
151
162
|
const detail = {
|
|
152
163
|
previousIndex,
|
|
153
164
|
currentIndex: index,
|
|
154
|
-
previousTab:
|
|
155
|
-
currentTab:
|
|
156
|
-
previousPanel:
|
|
157
|
-
currentPanel:
|
|
165
|
+
previousTab: this.tabButtons[previousIndex],
|
|
166
|
+
currentTab: this.tabButtons[index],
|
|
167
|
+
previousPanel: this.tabPanels[previousIndex],
|
|
168
|
+
currentPanel: this.tabPanels[index],
|
|
158
169
|
};
|
|
159
|
-
|
|
170
|
+
this.dispatchEvent(
|
|
171
|
+
new CustomEvent('tabchange', { detail, bubbles: true })
|
|
172
|
+
);
|
|
160
173
|
}
|
|
161
174
|
}
|
|
162
175
|
|
|
@@ -166,17 +179,16 @@ class TabGroup extends HTMLElement {
|
|
|
166
179
|
* @param {MouseEvent} e - the click event
|
|
167
180
|
*/
|
|
168
181
|
onClick(e) {
|
|
169
|
-
const _ = this;
|
|
170
182
|
// check if the click occurred on or within a <tab-button>
|
|
171
|
-
const tabButton = e.target.closest(
|
|
183
|
+
const tabButton = e.target.closest('tab-button');
|
|
172
184
|
if (!tabButton) return;
|
|
173
185
|
|
|
174
186
|
// determine the index of the clicked tab-button
|
|
175
|
-
const index =
|
|
187
|
+
const index = this.tabButtons.indexOf(tabButton);
|
|
176
188
|
if (index === -1) return;
|
|
177
189
|
|
|
178
190
|
// activate the tab with the corresponding index
|
|
179
|
-
|
|
191
|
+
this.setActiveTab(index);
|
|
180
192
|
}
|
|
181
193
|
|
|
182
194
|
/**
|
|
@@ -185,39 +197,39 @@ class TabGroup extends HTMLElement {
|
|
|
185
197
|
* @param {KeyboardEvent} e - the keydown event
|
|
186
198
|
*/
|
|
187
199
|
onKeyDown(e) {
|
|
188
|
-
const _ = this;
|
|
189
200
|
// only process keys if focus is on a <tab-button>
|
|
190
|
-
const targetIndex =
|
|
201
|
+
const targetIndex = this.tabButtons.indexOf(e.target);
|
|
191
202
|
if (targetIndex === -1) return;
|
|
192
203
|
|
|
193
204
|
let newIndex = targetIndex;
|
|
194
205
|
switch (e.key) {
|
|
195
|
-
case
|
|
196
|
-
case
|
|
206
|
+
case 'ArrowLeft':
|
|
207
|
+
case 'ArrowUp':
|
|
197
208
|
// move to the previous tab (wrap around if necessary)
|
|
198
|
-
newIndex =
|
|
209
|
+
newIndex =
|
|
210
|
+
targetIndex > 0 ? targetIndex - 1 : this.tabButtons.length - 1;
|
|
199
211
|
e.preventDefault();
|
|
200
212
|
break;
|
|
201
|
-
case
|
|
202
|
-
case
|
|
213
|
+
case 'ArrowRight':
|
|
214
|
+
case 'ArrowDown':
|
|
203
215
|
// move to the next tab (wrap around if necessary)
|
|
204
|
-
newIndex = (targetIndex + 1) %
|
|
216
|
+
newIndex = (targetIndex + 1) % this.tabButtons.length;
|
|
205
217
|
e.preventDefault();
|
|
206
218
|
break;
|
|
207
|
-
case
|
|
219
|
+
case 'Home':
|
|
208
220
|
// jump to the first tab
|
|
209
221
|
newIndex = 0;
|
|
210
222
|
e.preventDefault();
|
|
211
223
|
break;
|
|
212
|
-
case
|
|
224
|
+
case 'End':
|
|
213
225
|
// jump to the last tab
|
|
214
|
-
newIndex =
|
|
226
|
+
newIndex = this.tabButtons.length - 1;
|
|
215
227
|
e.preventDefault();
|
|
216
228
|
break;
|
|
217
229
|
default:
|
|
218
230
|
return; // ignore other keys
|
|
219
231
|
}
|
|
220
|
-
|
|
232
|
+
this.setActiveTab(newIndex);
|
|
221
233
|
}
|
|
222
234
|
}
|
|
223
235
|
|
|
@@ -225,50 +237,31 @@ class TabGroup extends HTMLElement {
|
|
|
225
237
|
* @class TabList
|
|
226
238
|
* a container for the <tab-button> elements
|
|
227
239
|
*/
|
|
228
|
-
class TabList extends HTMLElement {
|
|
229
|
-
constructor() {
|
|
230
|
-
super();
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
connectedCallback() {
|
|
234
|
-
// additional logic or styling can be added here if desired
|
|
235
|
-
}
|
|
236
|
-
}
|
|
240
|
+
class TabList extends HTMLElement {}
|
|
237
241
|
|
|
238
242
|
/**
|
|
239
243
|
* @class TabButton
|
|
240
244
|
* a single tab button element
|
|
241
245
|
*/
|
|
242
|
-
class TabButton extends HTMLElement {
|
|
243
|
-
constructor() {
|
|
244
|
-
super();
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
connectedCallback() {
|
|
248
|
-
// note: role and other attributes are handled by the parent
|
|
249
|
-
}
|
|
250
|
-
}
|
|
246
|
+
class TabButton extends HTMLElement {}
|
|
251
247
|
|
|
252
248
|
/**
|
|
253
249
|
* @class TabPanel
|
|
254
250
|
* a single tab panel element
|
|
255
251
|
*/
|
|
256
|
-
class TabPanel extends HTMLElement {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
252
|
+
class TabPanel extends HTMLElement {}
|
|
253
|
+
|
|
254
|
+
// define the custom elements (guarded against double-registration and SSR)
|
|
255
|
+
if (typeof window !== 'undefined' && window.customElements) {
|
|
256
|
+
if (!customElements.get('tab-group'))
|
|
257
|
+
customElements.define('tab-group', TabGroup);
|
|
258
|
+
if (!customElements.get('tab-list'))
|
|
259
|
+
customElements.define('tab-list', TabList);
|
|
260
|
+
if (!customElements.get('tab-button'))
|
|
261
|
+
customElements.define('tab-button', TabButton);
|
|
262
|
+
if (!customElements.get('tab-panel'))
|
|
263
|
+
customElements.define('tab-panel', TabPanel);
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
// define the custom elements
|
|
267
|
-
customElements.define("tab-group", TabGroup);
|
|
268
|
-
customElements.define("tab-list", TabList);
|
|
269
|
-
customElements.define("tab-button", TabButton);
|
|
270
|
-
customElements.define("tab-panel", TabPanel);
|
|
271
|
-
|
|
272
|
-
exports.TabGroup = TabGroup;
|
|
273
266
|
exports.default = TabGroup;
|
|
274
267
|
//# sourceMappingURL=tab-group.cjs.js.map
|