@pindoba/astro-tab 0.0.0-alpha-20250517160011
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 +7 -0
- package/README.md +229 -0
- package/index.ts +14 -0
- package/package.json +48 -0
- package/src/env.d.ts +8 -0
- package/src/tab-group.astro +276 -0
- package/src/tab-nav.astro +184 -0
- package/src/tab-panel.astro +127 -0
- package/src/tab.astro +141 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) 2024 Eduardo Lopes
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the βSoftwareβ), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED βAS ISβ, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Astro Starter Kit: Component Package
|
|
2
|
+
|
|
3
|
+
This is a template for an Astro component library. Use this template for writing components to use in multiple projects or publish to NPM.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm create astro@latest -- --template component
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/non-html-pages)
|
|
10
|
+
[](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/non-html-pages)
|
|
11
|
+
[](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/component/devcontainer.json)
|
|
12
|
+
|
|
13
|
+
## π Project Structure
|
|
14
|
+
|
|
15
|
+
Inside of your Astro project, you'll see the following folders and files:
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
/
|
|
19
|
+
βββ index.ts
|
|
20
|
+
βββ src
|
|
21
|
+
β βββ tab.astro
|
|
22
|
+
β βββ tab-group.astro
|
|
23
|
+
β βββ tab-panel.astro
|
|
24
|
+
β βββ tab-nav.astro
|
|
25
|
+
βββ tsconfig.json
|
|
26
|
+
βββ package.json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The `index.ts` file is the "entry point" for your package. Export your components in `index.ts` to make them importable from your package.
|
|
30
|
+
|
|
31
|
+
## Tab Components
|
|
32
|
+
|
|
33
|
+
### Using TabNav with TabGroup
|
|
34
|
+
|
|
35
|
+
The TabNav component uses the Navigation component for styled tab buttons:
|
|
36
|
+
|
|
37
|
+
```astro
|
|
38
|
+
---
|
|
39
|
+
import { TabGroup, TabNav, TabPanel } from "@pindoba/astro-tab";
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
<TabGroup useNavigation={true} defaultTab="tab1">
|
|
43
|
+
<TabNav
|
|
44
|
+
slot="tab-nav"
|
|
45
|
+
tabs={[
|
|
46
|
+
{ id: "tab1", label: "First Tab" },
|
|
47
|
+
{ id: "tab2", label: "Second Tab" },
|
|
48
|
+
{ id: "tab3", label: "Third Tab", disabled: true },
|
|
49
|
+
]}
|
|
50
|
+
selectedTab="tab1"
|
|
51
|
+
/>
|
|
52
|
+
|
|
53
|
+
<TabPanel id="tab1" slot="panel"> Content for the first tab </TabPanel>
|
|
54
|
+
|
|
55
|
+
<TabPanel id="tab2" slot="panel"> Content for the second tab </TabPanel>
|
|
56
|
+
|
|
57
|
+
<TabPanel id="tab3" slot="panel"> Content for the third tab </TabPanel>
|
|
58
|
+
</TabGroup>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Using Traditional Tabs
|
|
62
|
+
|
|
63
|
+
You can still use the traditional Tab components:
|
|
64
|
+
|
|
65
|
+
```astro
|
|
66
|
+
---
|
|
67
|
+
import { TabGroup, Tab, TabPanel } from "@pindoba/astro-tab";
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
<TabGroup>
|
|
71
|
+
<Tab id="tab1" label="First Tab" slot="tab" />
|
|
72
|
+
<Tab id="tab2" label="Second Tab" slot="tab" />
|
|
73
|
+
<Tab id="tab3" label="Third Tab" slot="tab" disabled />
|
|
74
|
+
|
|
75
|
+
<TabPanel id="tab1" slot="panel"> Content for the first tab </TabPanel>
|
|
76
|
+
|
|
77
|
+
<TabPanel id="tab2" slot="panel"> Content for the second tab </TabPanel>
|
|
78
|
+
|
|
79
|
+
<TabPanel id="tab3" slot="panel"> Content for the third tab </TabPanel>
|
|
80
|
+
</TabGroup>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## π§ Commands
|
|
84
|
+
|
|
85
|
+
All commands are run from the root of the project, from a terminal:
|
|
86
|
+
|
|
87
|
+
| Command | Action |
|
|
88
|
+
| :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
89
|
+
| `npm link` | Registers this package locally. Run `npm link my-component-library` in an Astro project to install your components |
|
|
90
|
+
| `npm publish` | [Publishes](https://docs.npmjs.com/creating-and-publishing-unscoped-public-packages#publishing-unscoped-public-packages) this package to NPM. Requires you to be [logged in](https://docs.npmjs.com/cli/v8/commands/npm-adduser) |
|
|
91
|
+
|
|
92
|
+
# Pindoba Tab Component
|
|
93
|
+
|
|
94
|
+
A flexible and accessible tab component for Astro projects.
|
|
95
|
+
|
|
96
|
+
## Installation
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm install @pindoba/astro-tab
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Usage
|
|
103
|
+
|
|
104
|
+
### Basic Usage
|
|
105
|
+
|
|
106
|
+
```astro
|
|
107
|
+
---
|
|
108
|
+
import { Tab } from "@pindoba/astro-tab";
|
|
109
|
+
|
|
110
|
+
const tabItems = [
|
|
111
|
+
{
|
|
112
|
+
id: "tab1",
|
|
113
|
+
label: "First Tab",
|
|
114
|
+
content: "This is the content of the first tab",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "tab2",
|
|
118
|
+
label: "Second Tab",
|
|
119
|
+
content: "This is the content of the second tab",
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "tab3",
|
|
123
|
+
label: "Third Tab",
|
|
124
|
+
content: "This is the content of the third tab",
|
|
125
|
+
disabled: true,
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
<Tab items={tabItems} defaultTab="tab1" />
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### With Custom Content
|
|
134
|
+
|
|
135
|
+
```astro
|
|
136
|
+
---
|
|
137
|
+
import { Tab } from "@pindoba/astro-tab";
|
|
138
|
+
|
|
139
|
+
const tabItems = [
|
|
140
|
+
{
|
|
141
|
+
id: "tab1",
|
|
142
|
+
label: "Dashboard",
|
|
143
|
+
content: (
|
|
144
|
+
<div class="p-4">
|
|
145
|
+
<h2>Dashboard Content</h2>
|
|
146
|
+
<p>Welcome to your dashboard!</p>
|
|
147
|
+
</div>
|
|
148
|
+
),
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: "tab2",
|
|
152
|
+
label: "Settings",
|
|
153
|
+
content: (
|
|
154
|
+
<div class="p-4">
|
|
155
|
+
<h2>Settings</h2>
|
|
156
|
+
<form>
|
|
157
|
+
<input type="text" placeholder="Username" />
|
|
158
|
+
<button type="submit">Save</button>
|
|
159
|
+
</form>
|
|
160
|
+
</div>
|
|
161
|
+
),
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
<Tab items={tabItems} defaultTab="tab1" class="my-4" />
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### With Custom Styling
|
|
170
|
+
|
|
171
|
+
```astro
|
|
172
|
+
---
|
|
173
|
+
import { Tab } from "@pindoba/astro-tab";
|
|
174
|
+
|
|
175
|
+
const tabItems = [
|
|
176
|
+
{
|
|
177
|
+
id: "tab1",
|
|
178
|
+
label: "First Tab",
|
|
179
|
+
content: "This is the content of the first tab",
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: "tab2",
|
|
183
|
+
label: "Second Tab",
|
|
184
|
+
content: "This is the content of the second tab",
|
|
185
|
+
},
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
const customStyles = {
|
|
189
|
+
root: {
|
|
190
|
+
borderRadius: "xl",
|
|
191
|
+
boxShadow: "md",
|
|
192
|
+
bg: "gray.50",
|
|
193
|
+
},
|
|
194
|
+
panel: {
|
|
195
|
+
p: "6",
|
|
196
|
+
fontSize: "lg",
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
<Tab items={tabItems} defaultTab="tab1" passThrough={customStyles} />
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Props
|
|
205
|
+
|
|
206
|
+
| Name | Type | Default | Description |
|
|
207
|
+
| ------------- | ----------- | -------------- | --------------------------------- |
|
|
208
|
+
| `items` | `TabItem[]` | Required | Array of tab items |
|
|
209
|
+
| `defaultTab` | `string` | First tab's ID | ID of the tab to show by default |
|
|
210
|
+
| `passThrough` | `object` | `{}` | Custom styling for tab components |
|
|
211
|
+
| `class` | `string` | `''` | Additional CSS classes |
|
|
212
|
+
|
|
213
|
+
### TabItem Interface
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
interface TabItem {
|
|
217
|
+
id: string; // Unique identifier
|
|
218
|
+
label: string; // Tab label text
|
|
219
|
+
content: any; // Tab panel content
|
|
220
|
+
selected?: boolean; // Whether tab is selected
|
|
221
|
+
disabled?: boolean; // Whether tab is disabled
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Accessibility
|
|
226
|
+
|
|
227
|
+
- Fully accessible with keyboard navigation
|
|
228
|
+
- ARIA attributes for screen readers
|
|
229
|
+
- Focus management for tab panels
|
package/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Do not write code directly here, instead use the `src` folder!
|
|
2
|
+
// Then, use this file to export everything you want your user to access.
|
|
3
|
+
|
|
4
|
+
import Tab from "./src/tab.astro";
|
|
5
|
+
import TabGroup from "./src/tab-group.astro";
|
|
6
|
+
import TabPanel from "./src/tab-panel.astro";
|
|
7
|
+
import TabNav from "./src/tab-nav.astro";
|
|
8
|
+
|
|
9
|
+
export * from "./src/tab-group.astro";
|
|
10
|
+
export * from "./src/tab-nav.astro";
|
|
11
|
+
export * from "./src/tab-panel.astro";
|
|
12
|
+
export * from "./src/tab.astro";
|
|
13
|
+
|
|
14
|
+
export { Tab, TabGroup, TabPanel, TabNav };
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pindoba/astro-tab",
|
|
3
|
+
"version": "0.0.0-alpha-20250517160011",
|
|
4
|
+
"homepage": "https://github.com/EduardoLopes/pindoba/tree/main/packages/blocks/astro/tab#README.md",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/EduardoLopes/pindoba.git",
|
|
8
|
+
"directory": "packages/ui/astro/tab"
|
|
9
|
+
},
|
|
10
|
+
"sideEffects": false,
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./index.ts"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"src",
|
|
18
|
+
"index.ts"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"astro-component"
|
|
22
|
+
],
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@astrojs/check": "^0.9.4",
|
|
25
|
+
"astro": "^5.7.13",
|
|
26
|
+
"typescript": "^5.8.3"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"astro": "^4.0.0"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@pindoba/astro-badge": "0.0.0-alpha-20250517160011",
|
|
33
|
+
"@pindoba/astro-group": "0.0.0-alpha-20250517160011",
|
|
34
|
+
"@pindoba/astro-radio": "0.0.0-alpha-20250517160011",
|
|
35
|
+
"@pindoba/panda": "0.0.0-alpha-20250517160011",
|
|
36
|
+
"@pindoba/styles": "0.0.0-alpha-20250517160011",
|
|
37
|
+
"@pindoba/utils": "0.0.0-alpha-20250517160011"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"lint": "TIMING=1 eslint . --max-warnings 0",
|
|
41
|
+
"lint:fix": "TIMING=1 eslint . --fix --max-warnings 0",
|
|
42
|
+
"format": "prettier --write .",
|
|
43
|
+
"format:fix": "prettier --write .",
|
|
44
|
+
"check": "astro check",
|
|
45
|
+
"dev": "astro check --watch",
|
|
46
|
+
"clean": "rm -rf .astro && rm -rf node_modules && rm -rf .turbo"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/env.d.ts
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { css, cx } from "@pindoba/panda/css";
|
|
3
|
+
import {
|
|
4
|
+
tabStyles,
|
|
5
|
+
type TabSlots,
|
|
6
|
+
type TabStyleProps,
|
|
7
|
+
} from "@pindoba/styles/tab";
|
|
8
|
+
import type { HTMLAttributes } from "astro/types";
|
|
9
|
+
import type { SystemStyleObject } from "@pindoba/panda/types";
|
|
10
|
+
|
|
11
|
+
export interface TabGroupProps extends HTMLAttributes<"div">, TabStyleProps {
|
|
12
|
+
defaultTab?: string;
|
|
13
|
+
passThrough?: { [key in TabSlots]?: SystemStyleObject };
|
|
14
|
+
id?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type Props = TabGroupProps;
|
|
18
|
+
|
|
19
|
+
const { class: className, defaultTab, passThrough, id, ...props } = Astro.props;
|
|
20
|
+
|
|
21
|
+
const classes = tabStyles.raw();
|
|
22
|
+
// Use provided ID or generate a unique one
|
|
23
|
+
const uniqueId = id || `tab-group-${crypto.randomUUID().slice(0, 8)}`;
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
<div
|
|
27
|
+
is="pindoba-tab-group"
|
|
28
|
+
class={cx(css(classes.root, passThrough?.root), className)}
|
|
29
|
+
data-default-tab={defaultTab}
|
|
30
|
+
id={uniqueId}
|
|
31
|
+
{...props}
|
|
32
|
+
>
|
|
33
|
+
<slot name="tab-nav" />
|
|
34
|
+
<slot name="panel" />
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<script>
|
|
38
|
+
// Define type for tab change event
|
|
39
|
+
interface TabChangeDetail {
|
|
40
|
+
id: string;
|
|
41
|
+
label: string;
|
|
42
|
+
disabled?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class PindobaTabGroup extends HTMLDivElement {
|
|
46
|
+
// Cache DOM queries for better performance
|
|
47
|
+
private tablist: HTMLElement | null = null;
|
|
48
|
+
private panels: NodeListOf<Element> | null = null;
|
|
49
|
+
private activeTabId: string | null = null;
|
|
50
|
+
|
|
51
|
+
constructor() {
|
|
52
|
+
super();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
connectedCallback() {
|
|
56
|
+
// Initialize after the element is connected to ensure all children are available
|
|
57
|
+
this.init();
|
|
58
|
+
|
|
59
|
+
// Listen for tab:change events from tab-nav
|
|
60
|
+
this.addEventListener("tab:change", this.handleTabChange.bind(this));
|
|
61
|
+
|
|
62
|
+
// Listen for keyboard events for a11y support
|
|
63
|
+
this.addEventListener("keydown", this.handleKeyDown.bind(this));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
disconnectedCallback() {
|
|
67
|
+
// Clean up event listeners
|
|
68
|
+
this.removeEventListener("tab:change", this.handleTabChange);
|
|
69
|
+
this.removeEventListener("keydown", this.handleKeyDown);
|
|
70
|
+
|
|
71
|
+
// Clear cache references
|
|
72
|
+
this.tablist = null;
|
|
73
|
+
this.panels = null;
|
|
74
|
+
this.activeTabId = null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private init() {
|
|
78
|
+
// Cache DOM queries
|
|
79
|
+
this.tablist = this.querySelector('[role="tablist"]');
|
|
80
|
+
this.panels = this.querySelectorAll('[role="tabpanel"]');
|
|
81
|
+
|
|
82
|
+
// check if the default tab is set
|
|
83
|
+
const defaultTab = this.getAttribute("data-default-tab");
|
|
84
|
+
|
|
85
|
+
if (defaultTab) {
|
|
86
|
+
this.selectTab(defaultTab);
|
|
87
|
+
} else {
|
|
88
|
+
// Find the first non-disabled tab
|
|
89
|
+
const firstTab = this.findFirstSelectableTab();
|
|
90
|
+
if (firstTab) {
|
|
91
|
+
this.selectTab(firstTab.id);
|
|
92
|
+
} else {
|
|
93
|
+
console.warn(`TabGroup ${this.id}: No selectable tabs found`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private findFirstSelectableTab(): HTMLElement | null {
|
|
99
|
+
if (!this.tablist) return null;
|
|
100
|
+
|
|
101
|
+
const tabs = Array.from(
|
|
102
|
+
this.tablist.querySelectorAll('[role="tab"]'),
|
|
103
|
+
) as HTMLElement[];
|
|
104
|
+
return (
|
|
105
|
+
tabs.find(
|
|
106
|
+
(tab) =>
|
|
107
|
+
!tab.hasAttribute("disabled") &&
|
|
108
|
+
tab.getAttribute("aria-disabled") !== "true",
|
|
109
|
+
) || null
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private handleTabChange(e: Event) {
|
|
114
|
+
try {
|
|
115
|
+
const customEvent = e as CustomEvent<TabChangeDetail>;
|
|
116
|
+
|
|
117
|
+
if (customEvent?.detail?.id) {
|
|
118
|
+
this.selectTab(customEvent.detail.id);
|
|
119
|
+
} else {
|
|
120
|
+
console.warn(
|
|
121
|
+
`TabGroup ${this.id}: Invalid tab:change event`,
|
|
122
|
+
customEvent,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error(`TabGroup ${this.id}: Error handling tab change`, error);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private handleKeyDown(e: KeyboardEvent) {
|
|
131
|
+
// Only handle keyboard events when focus is within the tab group
|
|
132
|
+
if (!this.contains(document.activeElement)) return;
|
|
133
|
+
|
|
134
|
+
// If the user presses Home/End, navigate to the first/last tab
|
|
135
|
+
if (e.key === "Home" || e.key === "End") {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
if (!this.tablist) return;
|
|
138
|
+
|
|
139
|
+
const tabs = Array.from(
|
|
140
|
+
this.tablist.querySelectorAll(
|
|
141
|
+
'[role="tab"]:not([disabled]):not([aria-disabled="true"])',
|
|
142
|
+
),
|
|
143
|
+
) as HTMLElement[];
|
|
144
|
+
if (tabs.length === 0) return;
|
|
145
|
+
|
|
146
|
+
const targetTab = e.key === "Home" ? tabs[0] : tabs[tabs.length - 1];
|
|
147
|
+
targetTab.click();
|
|
148
|
+
targetTab.focus();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// If user presses F6, move focus to the panel
|
|
152
|
+
if (e.key === "F6") {
|
|
153
|
+
e.preventDefault();
|
|
154
|
+
this.focusSelectedTabPanel();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
selectTab(selectedTab?: string) {
|
|
159
|
+
if (!selectedTab) {
|
|
160
|
+
console.warn(`TabGroup ${this.id}: No tab ID provided to selectTab`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Store the active tab id
|
|
165
|
+
this.activeTabId = selectedTab;
|
|
166
|
+
|
|
167
|
+
// Extract the tab ID portion if it contains a group ID
|
|
168
|
+
const tabId = selectedTab.includes("-")
|
|
169
|
+
? selectedTab.split("-").pop()
|
|
170
|
+
: selectedTab;
|
|
171
|
+
|
|
172
|
+
if (!tabId) {
|
|
173
|
+
console.warn(
|
|
174
|
+
`TabGroup ${this.id}: Invalid tab ID format: ${selectedTab}`,
|
|
175
|
+
);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Find the corresponding panel
|
|
180
|
+
const panelElement = this.querySelector(
|
|
181
|
+
`#panel-${tabId}[role="tabpanel"]`,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
if (!panelElement) {
|
|
185
|
+
console.warn(
|
|
186
|
+
`TabGroup ${this.id}: No panel found for tab ID: ${tabId}`,
|
|
187
|
+
);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Update tab state (selected attribute)
|
|
192
|
+
if (this.tablist) {
|
|
193
|
+
const tabs = this.tablist.querySelectorAll('[role="tab"]');
|
|
194
|
+
tabs.forEach((tab) => {
|
|
195
|
+
const isSelected = tab.id.includes(tabId);
|
|
196
|
+
tab.setAttribute("aria-selected", isSelected ? "true" : "false");
|
|
197
|
+
if (isSelected) {
|
|
198
|
+
tab.removeAttribute("tabindex");
|
|
199
|
+
} else {
|
|
200
|
+
tab.setAttribute("tabindex", "-1");
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Get all panels within this specific tab group only
|
|
206
|
+
const panels = this.querySelectorAll(`[role="tabpanel"]`);
|
|
207
|
+
|
|
208
|
+
// Update panel visibility with better accessibility
|
|
209
|
+
panels.forEach((panel: Element) => {
|
|
210
|
+
const isVisible = panel.id === panelElement.id;
|
|
211
|
+
panel.setAttribute("aria-hidden", isVisible ? "false" : "true");
|
|
212
|
+
|
|
213
|
+
// Use tabindex for proper focus management
|
|
214
|
+
if (isVisible) {
|
|
215
|
+
panel.setAttribute("tabindex", "0");
|
|
216
|
+
panel.classList.remove("hidden");
|
|
217
|
+
} else {
|
|
218
|
+
panel.setAttribute("tabindex", "-1");
|
|
219
|
+
panel.classList.add("hidden");
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Dispatch a custom event for external components to react to tab changes
|
|
224
|
+
try {
|
|
225
|
+
const changeEvent = new CustomEvent("tabgroup:selected", {
|
|
226
|
+
bubbles: true,
|
|
227
|
+
detail: {
|
|
228
|
+
tabId: tabId,
|
|
229
|
+
groupId: this.id,
|
|
230
|
+
focusPanel: false, // Default to false; will be set to true for keyboard navigation
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
this.dispatchEvent(changeEvent);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error(
|
|
236
|
+
`TabGroup ${this.id}: Error dispatching selection event`,
|
|
237
|
+
error,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Method to update the keyboard focus to the selected tab's panel
|
|
243
|
+
private focusSelectedTabPanel() {
|
|
244
|
+
if (!this.activeTabId) return;
|
|
245
|
+
|
|
246
|
+
// Extract the tab ID portion if it contains a group ID
|
|
247
|
+
const tabId = this.activeTabId.includes("-")
|
|
248
|
+
? this.activeTabId.split("-").pop()
|
|
249
|
+
: this.activeTabId;
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const changeEvent = new CustomEvent("tabgroup:selected", {
|
|
253
|
+
bubbles: true,
|
|
254
|
+
detail: {
|
|
255
|
+
tabId: tabId,
|
|
256
|
+
groupId: this.id,
|
|
257
|
+
focusPanel: true, // Signal that panel should receive focus
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
this.dispatchEvent(changeEvent);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.error(
|
|
263
|
+
`TabGroup ${this.id}: Error dispatching focus panel event`,
|
|
264
|
+
error,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Register the custom element if not already registered
|
|
271
|
+
if (!customElements.get("pindoba-tab-group")) {
|
|
272
|
+
customElements.define("pindoba-tab-group", PindobaTabGroup, {
|
|
273
|
+
extends: "div",
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
</script>
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
---
|
|
2
|
+
import {
|
|
3
|
+
tabStyles,
|
|
4
|
+
type TabSlots,
|
|
5
|
+
type TabStyleProps,
|
|
6
|
+
} from "@pindoba/styles/tab";
|
|
7
|
+
import type { HTMLAttributes } from "astro/types";
|
|
8
|
+
import type { SystemStyleObject } from "@pindoba/panda/types";
|
|
9
|
+
import Radio, { type RadioProps } from "@pindoba/astro-radio";
|
|
10
|
+
import Group from "@pindoba/astro-group";
|
|
11
|
+
import { cx } from "@pindoba/panda/css";
|
|
12
|
+
import Badge from "@pindoba/astro-badge";
|
|
13
|
+
export interface TabNavItem extends RadioProps {
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
badge?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Props extends HTMLAttributes<"nav">, TabStyleProps {
|
|
21
|
+
tabs: TabNavItem[];
|
|
22
|
+
selectedTab?: string;
|
|
23
|
+
fullWidth?: boolean;
|
|
24
|
+
passThrough?: { [key in TabSlots]?: SystemStyleObject };
|
|
25
|
+
onChange?: (tab: TabNavItem) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const classes = tabStyles();
|
|
29
|
+
|
|
30
|
+
export type TabNavProps = Props;
|
|
31
|
+
|
|
32
|
+
const {
|
|
33
|
+
class: className,
|
|
34
|
+
tabs,
|
|
35
|
+
selectedTab,
|
|
36
|
+
fullWidth = true,
|
|
37
|
+
passThrough,
|
|
38
|
+
...props
|
|
39
|
+
} = Astro.props;
|
|
40
|
+
|
|
41
|
+
// Safety check for required props
|
|
42
|
+
if (!Array.isArray(tabs) || tabs.length === 0) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
'TabNav component requires at least one tab in the "tabs" array',
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Transform tabs into navigation items
|
|
49
|
+
const navItems: TabNavItem[] = tabs.map(
|
|
50
|
+
({ label, id, disabled, badge, ...rest }: TabNavItem) => ({
|
|
51
|
+
label: label || "",
|
|
52
|
+
id: id || crypto.randomUUID(), // Ensure an ID always exists
|
|
53
|
+
disabled: disabled || false,
|
|
54
|
+
badge: badge || "",
|
|
55
|
+
...rest,
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const radioGroupId = crypto.randomUUID();
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
<nav
|
|
63
|
+
is="pindoba-tab-nav"
|
|
64
|
+
class={cx(classes.nav, className)}
|
|
65
|
+
role="tablist"
|
|
66
|
+
data-tablist
|
|
67
|
+
aria-orientation="horizontal"
|
|
68
|
+
{...props}
|
|
69
|
+
>
|
|
70
|
+
<Group direction="horizontal" noBorderRadius="bottom">
|
|
71
|
+
{
|
|
72
|
+
navItems.map((tab) => {
|
|
73
|
+
const { id, label, disabled, badge, ...rest } = tab;
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<Radio
|
|
77
|
+
name={radioGroupId}
|
|
78
|
+
data-tab-id={id}
|
|
79
|
+
id={`${radioGroupId}-${id}`}
|
|
80
|
+
checked={id === selectedTab}
|
|
81
|
+
appearance="tab-horizontal"
|
|
82
|
+
disabled={disabled}
|
|
83
|
+
aria-controls={`panel-${id}`}
|
|
84
|
+
aria-selected={id === selectedTab ? "true" : "false"}
|
|
85
|
+
role="tab"
|
|
86
|
+
fullWidth={fullWidth}
|
|
87
|
+
{...rest}
|
|
88
|
+
>
|
|
89
|
+
{label}
|
|
90
|
+
{badge && <Badge size="sm">{badge}</Badge>}
|
|
91
|
+
</Radio>
|
|
92
|
+
);
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
</Group>
|
|
96
|
+
</nav>
|
|
97
|
+
|
|
98
|
+
<script>
|
|
99
|
+
// Define TabNavItem type for client script
|
|
100
|
+
type TabNavItem = {
|
|
101
|
+
id: string;
|
|
102
|
+
label: string;
|
|
103
|
+
disabled?: boolean;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
class PindobaTabNav extends HTMLElement {
|
|
107
|
+
private radioInputs: NodeListOf<HTMLInputElement> | null = null;
|
|
108
|
+
|
|
109
|
+
connectedCallback() {
|
|
110
|
+
this.initTabNav();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
disconnectedCallback() {
|
|
114
|
+
this.cleanup();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private initTabNav() {
|
|
118
|
+
// Use 'this' directly as it's the nav element with role="tablist"
|
|
119
|
+
this.radioInputs = this.querySelectorAll('input[type="radio"]');
|
|
120
|
+
|
|
121
|
+
if (!this.radioInputs || this.radioInputs.length === 0) {
|
|
122
|
+
console.warn("TabNav: No radio inputs found");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Add listeners directly - this avoids TypeScript issues with event binding
|
|
127
|
+
this.radioInputs.forEach((radio) => {
|
|
128
|
+
radio.addEventListener("change", (e: Event) => {
|
|
129
|
+
this.handleRadioChange(e);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private handleRadioChange(e: Event) {
|
|
135
|
+
const target = e.target as HTMLInputElement;
|
|
136
|
+
if (!target || !target.checked) return;
|
|
137
|
+
|
|
138
|
+
const tabId = target.dataset.tabId;
|
|
139
|
+
if (!tabId) {
|
|
140
|
+
console.warn("TabNav: Changed radio button has no tab ID");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Get the text content from the label element
|
|
145
|
+
const labelElement = target.closest("label");
|
|
146
|
+
if (!labelElement) {
|
|
147
|
+
console.warn("TabNav: Cannot find label element for radio button");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const textDiv = labelElement.querySelector("div");
|
|
152
|
+
const tabLabel = textDiv?.textContent?.trim() || "";
|
|
153
|
+
|
|
154
|
+
// Create tab object to match TabNavItem interface
|
|
155
|
+
const tab: TabNavItem = {
|
|
156
|
+
id: tabId,
|
|
157
|
+
label: tabLabel,
|
|
158
|
+
disabled: target.disabled,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Dispatch custom event that can be listened to by parent components
|
|
162
|
+
try {
|
|
163
|
+
const customEvent = new CustomEvent("tab:change", {
|
|
164
|
+
detail: tab,
|
|
165
|
+
bubbles: true,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Dispatch the event from this element
|
|
169
|
+
this.dispatchEvent(customEvent);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error("TabNav: Error dispatching tab:change event", error);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private cleanup() {
|
|
176
|
+
this.radioInputs = null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Register the web component if not already registered
|
|
181
|
+
if (!customElements.get("pindoba-tab-nav")) {
|
|
182
|
+
customElements.define("pindoba-tab-nav", PindobaTabNav, { extends: "nav" });
|
|
183
|
+
}
|
|
184
|
+
</script>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { css, cx } from "@pindoba/panda/css";
|
|
3
|
+
import {
|
|
4
|
+
tabStyles,
|
|
5
|
+
type TabSlots,
|
|
6
|
+
type TabStyleProps,
|
|
7
|
+
} from "@pindoba/styles/tab";
|
|
8
|
+
import type { HTMLAttributes } from "astro/types";
|
|
9
|
+
import type { SystemStyleObject } from "@pindoba/panda/types";
|
|
10
|
+
|
|
11
|
+
export interface TabPanelProps extends HTMLAttributes<"div">, TabStyleProps {
|
|
12
|
+
id: string;
|
|
13
|
+
selected?: boolean;
|
|
14
|
+
passThrough?: { [key in TabSlots]?: SystemStyleObject };
|
|
15
|
+
ariaLabelledBy?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type Props = TabPanelProps;
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
class: className,
|
|
22
|
+
id,
|
|
23
|
+
selected = false,
|
|
24
|
+
passThrough,
|
|
25
|
+
ariaLabelledBy,
|
|
26
|
+
...props
|
|
27
|
+
} = Astro.props;
|
|
28
|
+
|
|
29
|
+
// Validation for required props
|
|
30
|
+
if (!id) {
|
|
31
|
+
throw new Error('TabPanel component requires an "id" prop');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const classes = tabStyles.raw();
|
|
35
|
+
|
|
36
|
+
const panelId = `panel-${id}`;
|
|
37
|
+
|
|
38
|
+
// For improved accessibility, determine which element labels this panel
|
|
39
|
+
const labelId = ariaLabelledBy || id;
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
<div
|
|
43
|
+
id={panelId}
|
|
44
|
+
role="tabpanel"
|
|
45
|
+
class={cx(
|
|
46
|
+
css(classes.panel, passThrough?.panel),
|
|
47
|
+
!selected && "hidden",
|
|
48
|
+
className,
|
|
49
|
+
)}
|
|
50
|
+
aria-labelledby={labelId}
|
|
51
|
+
aria-hidden={!selected ? "true" : "false"}
|
|
52
|
+
tabindex={selected ? "0" : "-1"}
|
|
53
|
+
data-tab-panel
|
|
54
|
+
data-tab-id={id}
|
|
55
|
+
{...props}
|
|
56
|
+
>
|
|
57
|
+
<slot />
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<script>
|
|
61
|
+
// Client-side behavior for tab panels
|
|
62
|
+
class PindobaTabPanel extends HTMLDivElement {
|
|
63
|
+
private unsubscribe: (() => void) | null = null;
|
|
64
|
+
|
|
65
|
+
connectedCallback() {
|
|
66
|
+
// Find the parent tab group
|
|
67
|
+
const tabGroup = this.closest('[is="pindoba-tab-group"]');
|
|
68
|
+
if (!tabGroup) return;
|
|
69
|
+
|
|
70
|
+
// Listen for selection changes
|
|
71
|
+
const handleSelection = (e: CustomEvent) => {
|
|
72
|
+
if (!e.detail) return;
|
|
73
|
+
|
|
74
|
+
// Get our tab id from the data attribute
|
|
75
|
+
const myTabId = this.dataset.tabId;
|
|
76
|
+
if (!myTabId) return;
|
|
77
|
+
|
|
78
|
+
// Check if this panel should be shown
|
|
79
|
+
const isVisible = e.detail.tabId === myTabId;
|
|
80
|
+
|
|
81
|
+
// Update attributes for accessibility
|
|
82
|
+
this.setAttribute("aria-hidden", isVisible ? "false" : "true");
|
|
83
|
+
this.setAttribute("tabindex", isVisible ? "0" : "-1");
|
|
84
|
+
|
|
85
|
+
// Update visibility
|
|
86
|
+
if (isVisible) {
|
|
87
|
+
this.classList.remove("hidden");
|
|
88
|
+
// Only focus if the selection was triggered by keyboard navigation
|
|
89
|
+
if (e.detail.focusPanel) {
|
|
90
|
+
this.focus();
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
this.classList.add("hidden");
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Add event listener
|
|
98
|
+
tabGroup.addEventListener(
|
|
99
|
+
"tabgroup:selected",
|
|
100
|
+
handleSelection as EventListener,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// Store cleanup function
|
|
104
|
+
this.unsubscribe = () => {
|
|
105
|
+
tabGroup.removeEventListener(
|
|
106
|
+
"tabgroup:selected",
|
|
107
|
+
handleSelection as EventListener,
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
disconnectedCallback() {
|
|
113
|
+
// Clean up event listeners
|
|
114
|
+
if (this.unsubscribe) {
|
|
115
|
+
this.unsubscribe();
|
|
116
|
+
this.unsubscribe = null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Register the custom element if not already registered
|
|
122
|
+
if (!customElements.get("pindoba-tab-panel")) {
|
|
123
|
+
customElements.define("pindoba-tab-panel", PindobaTabPanel, {
|
|
124
|
+
extends: "div",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
</script>
|
package/src/tab.astro
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { cx } from "@pindoba/panda/css";
|
|
3
|
+
import type { SystemStyleObject } from "@pindoba/panda/types";
|
|
4
|
+
import { type TabSlots } from "@pindoba/styles/tab";
|
|
5
|
+
import TabGroup, { type TabGroupProps } from "./tab-group.astro";
|
|
6
|
+
import TabNav, { type TabNavItem } from "./tab-nav.astro";
|
|
7
|
+
import TabPanel from "./tab-panel.astro";
|
|
8
|
+
|
|
9
|
+
export interface TabItem extends TabNavItem {
|
|
10
|
+
selected?: boolean;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TabProps extends Omit<TabGroupProps, "defaultTab"> {
|
|
15
|
+
items: TabItem[];
|
|
16
|
+
defaultTab?: string;
|
|
17
|
+
fullWidth?: boolean;
|
|
18
|
+
passThrough?: { [key in TabSlots]?: SystemStyleObject };
|
|
19
|
+
onTabChange?: (tabId: string) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type Props = TabProps;
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
items,
|
|
26
|
+
defaultTab,
|
|
27
|
+
fullWidth = true,
|
|
28
|
+
passThrough,
|
|
29
|
+
class: className,
|
|
30
|
+
...props
|
|
31
|
+
} = Astro.props;
|
|
32
|
+
|
|
33
|
+
// Validate input props
|
|
34
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
'Tab component requires at least one item in the "items" array',
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Ensure all items have an ID
|
|
41
|
+
items.forEach((item, index) => {
|
|
42
|
+
if (!item.id) {
|
|
43
|
+
throw new Error(`Tab item at index ${index} is missing an "id" property`);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Find the selected tab or use the default tab
|
|
48
|
+
const selectedTabId =
|
|
49
|
+
items.find((item) => item.selected)?.id || defaultTab || items[0]?.id;
|
|
50
|
+
|
|
51
|
+
// Generate a unique ID for this tab component
|
|
52
|
+
const tabGroupId = crypto.randomUUID().slice(0, 8);
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
<TabGroup
|
|
56
|
+
defaultTab={selectedTabId}
|
|
57
|
+
class={cx(className)}
|
|
58
|
+
passThrough={passThrough}
|
|
59
|
+
id={`tab-group-${tabGroupId}`}
|
|
60
|
+
data-tab-component
|
|
61
|
+
{...props}
|
|
62
|
+
>
|
|
63
|
+
<TabNav
|
|
64
|
+
slot="tab-nav"
|
|
65
|
+
tabs={items}
|
|
66
|
+
selectedTab={selectedTabId}
|
|
67
|
+
fullWidth={fullWidth}
|
|
68
|
+
passThrough={passThrough}
|
|
69
|
+
/>
|
|
70
|
+
<div slot="panel" class="tab-panels">
|
|
71
|
+
{
|
|
72
|
+
items.map(async (item) => {
|
|
73
|
+
const hasSlotContent = Astro.slots.has(item.id);
|
|
74
|
+
|
|
75
|
+
// Check for empty content
|
|
76
|
+
let content = "";
|
|
77
|
+
if (hasSlotContent) {
|
|
78
|
+
content = await Astro.slots.render(item.id);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// If there's no content, render a placeholder or warning
|
|
82
|
+
if (!content.trim()) {
|
|
83
|
+
content = `<div class="tab-panel-empty">No content for tab "${item.label || item.id}"</div>`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<TabPanel
|
|
88
|
+
id={item.id}
|
|
89
|
+
selected={item.id === selectedTabId}
|
|
90
|
+
passThrough={passThrough}
|
|
91
|
+
ariaLabelledBy={`${tabGroupId}-${item.id}`}
|
|
92
|
+
data-tab-content={item.id}
|
|
93
|
+
>
|
|
94
|
+
<Fragment set:html={content} />
|
|
95
|
+
</TabPanel>
|
|
96
|
+
);
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
</div>
|
|
100
|
+
</TabGroup>
|
|
101
|
+
|
|
102
|
+
<script>
|
|
103
|
+
// Client-side behavior to enhance the tab component
|
|
104
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
105
|
+
// Find all tab components
|
|
106
|
+
const tabComponents = document.querySelectorAll("[data-tab-component]");
|
|
107
|
+
|
|
108
|
+
tabComponents.forEach((tabComponent) => {
|
|
109
|
+
// Listen for tab selection events
|
|
110
|
+
tabComponent.addEventListener("tabgroup:selected", (e) => {
|
|
111
|
+
const customEvent = e as CustomEvent;
|
|
112
|
+
|
|
113
|
+
// Dispatch a higher-level event that can be listened to by consumers
|
|
114
|
+
if (customEvent?.detail?.tabId) {
|
|
115
|
+
const tabChangeEvent = new CustomEvent("tab:selected", {
|
|
116
|
+
bubbles: true,
|
|
117
|
+
detail: {
|
|
118
|
+
tabId: customEvent.detail.tabId,
|
|
119
|
+
groupId: customEvent.detail.groupId,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
tabComponent.dispatchEvent(tabChangeEvent);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
</script>
|
|
129
|
+
|
|
130
|
+
<style>
|
|
131
|
+
.tab-panels {
|
|
132
|
+
width: 100%;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.tab-panel-empty {
|
|
136
|
+
padding: 1rem;
|
|
137
|
+
color: #666;
|
|
138
|
+
font-style: italic;
|
|
139
|
+
text-align: center;
|
|
140
|
+
}
|
|
141
|
+
</style>
|