@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Magic Spells (Cory Schulz)
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
- # Tab Group
1
+ # Tab Group
2
2
 
3
3
  ![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)
4
4
  ![Version](https://img.shields.io/badge/version-0.1.0-brightgreen)
5
5
 
6
- A lightweight, accessible tab interface web component with keyboard navigation and rich customization options.
6
+ A lightweight, accessible tab interface web component with keyboard navigation. Ships only structural CSS — you bring your own styles.
7
7
 
8
- 🔍 **[Live Demo](https://magic-spells.github.io/tab-group/demo/)** - See it in action!
8
+ [Live Demo](https://magic-spells.github.io/tab-group/demo/) - See it in action!
9
9
 
10
- ## 🚀 Features
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
- - **Theme Support** - Extensive CSS variable customization
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
- ## 📦 Installation
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
- ## 🔧 Usage
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
- console.log('Tab changed!');
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
- ## 🎨 Styling
68
+ ## Styling
94
69
 
95
- ### CSS Variables
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-group {
101
- /* Base colors */
102
- --color-background: #ffffff;
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
- ### SCSS Integration
141
-
142
- This package provides multiple ways to integrate with your SCSS workflow:
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
- // Option 3: Direct paths (if needed)
159
- @use "node_modules/@magic-spells/tab-group/dist/tab-group.scss";
160
- @use "node_modules/@magic-spells/tab-group/dist/scss/tab-group";
161
- ```
83
+ tab-button[aria-selected="true"] {
84
+ color: #3366ff;
85
+ border-bottom-color: #3366ff;
86
+ }
162
87
 
163
- #### Available SCSS Variables
164
-
165
- You can customize the appearance by overriding these SCSS variables:
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
- ## 🌈 Theme Examples
93
+ Or use Tailwind utility classes — no overrides needed.
214
94
 
215
- ### Dark Theme
95
+ ### What the component CSS includes
216
96
 
217
97
  ```css
218
- tab-group {
219
- --color-background: #2d3748;
220
- --color-text: #e2e8f0;
221
- --color-border: #4a5568;
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
- ### Rounded Pills Theme
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
- ## ⌨️ Accessibility
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
- ## 🛠️ API Reference
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
- ## 🌟 Examples
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
- ## 📄 License
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 by <a href="https://github.com/cory-schulz">Cory Schulz</a>
146
+ Made with magic by <a href="https://github.com/cory-schulz">Cory Schulz</a>
298
147
  </p>
@@ -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 within the tab group
35
- let tabs = this.querySelectorAll("tab-button");
36
- let panels = this.querySelectorAll("tab-panel");
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("tab-list");
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("tab-list");
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("tab-button");
51
- newTab.textContent = "default tab";
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("tab-panel");
61
- newPanel.innerHTML = "<p>default panel content</p>";
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
- const _ = this;
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
- _.tabList = _.querySelector("tab-list");
75
- if (!_.tabList) return;
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
- _.tabButtons = Array.from(_.tabList.querySelectorAll("tab-button"));
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
- _.tabPanels = Array.from(_.querySelectorAll("tab-panel"));
79
+ this.tabPanels = Array.from(this.querySelectorAll(':scope > tab-panel'));
82
80
 
83
- // initialize each tab-button with roles, ids and aria attributes
84
- _.tabButtons.forEach((tab, index) => {
85
- const tabIndex = TabGroup.tabCount++;
81
+ const prefix = this._instanceId;
86
82
 
87
- // generate a unique id for each tab, e.g. "tab-0", "tab-1", ...
88
- const tabId = `tab-${tabIndex}`;
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
- // generate a corresponding panel id, e.g. "panel-0"
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("aria-selected", "true");
99
- tab.setAttribute("tabindex", "0");
93
+ tab.setAttribute('aria-selected', 'true');
94
+ tab.setAttribute('tabindex', '0');
100
95
  } else {
101
- tab.setAttribute("aria-selected", "false");
102
- tab.setAttribute("tabindex", "-1");
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
- _.tabPanels.forEach((panel, index) => {
108
- const panelIndex = TabGroup.panelCount++;
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("role", "tabpanel");
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
- _.tabList.setAttribute("role", "tablist");
121
- _.tabList.addEventListener("keydown", (e) => _.onKeyDown(e));
122
- _.tabList.addEventListener("click", (e) => _.onClick(e));
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
- const _ = this;
132
- const previousIndex = _.tabButtons.findIndex(tab => tab.getAttribute("aria-selected") === "true");
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
- _.tabButtons.forEach((tab, i) => {
146
+ this.tabButtons.forEach((tab, i) => {
136
147
  const isActive = i === index;
137
- tab.setAttribute("aria-selected", isActive ? "true" : "false");
138
- tab.setAttribute("tabindex", isActive ? "0" : "-1");
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
- _.tabPanels.forEach((panel, i) => {
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: _.tabButtons[previousIndex],
155
- currentTab: _.tabButtons[index],
156
- previousPanel: _.tabPanels[previousIndex],
157
- currentPanel: _.tabPanels[index]
165
+ previousTab: this.tabButtons[previousIndex],
166
+ currentTab: this.tabButtons[index],
167
+ previousPanel: this.tabPanels[previousIndex],
168
+ currentPanel: this.tabPanels[index],
158
169
  };
159
- _.dispatchEvent(new CustomEvent('tabchange', { detail, bubbles: true }));
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("tab-button");
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 = _.tabButtons.indexOf(tabButton);
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
- _.setActiveTab(index);
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 = _.tabButtons.indexOf(e.target);
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 "ArrowLeft":
196
- case "ArrowUp":
206
+ case 'ArrowLeft':
207
+ case 'ArrowUp':
197
208
  // move to the previous tab (wrap around if necessary)
198
- newIndex = targetIndex > 0 ? targetIndex - 1 : _.tabButtons.length - 1;
209
+ newIndex =
210
+ targetIndex > 0 ? targetIndex - 1 : this.tabButtons.length - 1;
199
211
  e.preventDefault();
200
212
  break;
201
- case "ArrowRight":
202
- case "ArrowDown":
213
+ case 'ArrowRight':
214
+ case 'ArrowDown':
203
215
  // move to the next tab (wrap around if necessary)
204
- newIndex = (targetIndex + 1) % _.tabButtons.length;
216
+ newIndex = (targetIndex + 1) % this.tabButtons.length;
205
217
  e.preventDefault();
206
218
  break;
207
- case "Home":
219
+ case 'Home':
208
220
  // jump to the first tab
209
221
  newIndex = 0;
210
222
  e.preventDefault();
211
223
  break;
212
- case "End":
224
+ case 'End':
213
225
  // jump to the last tab
214
- newIndex = _.tabButtons.length - 1;
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
- _.setActiveTab(newIndex);
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
- constructor() {
258
- super();
259
- }
260
-
261
- connectedCallback() {
262
- // note: role and other attributes are handled by the parent
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