@dxlbnl/ui 0.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/README.md +94 -0
- package/dist/components/cards/Card.stories.svelte +82 -0
- package/dist/components/cards/Card.stories.svelte.d.ts +19 -0
- package/dist/components/cards/Card.svelte +28 -0
- package/dist/components/cards/Card.svelte.d.ts +12 -0
- package/dist/components/cards/NoteCard.stories.svelte +94 -0
- package/dist/components/cards/NoteCard.stories.svelte.d.ts +19 -0
- package/dist/components/cards/NoteCard.svelte +89 -0
- package/dist/components/cards/NoteCard.svelte.d.ts +18 -0
- package/dist/components/cards/ProductCard.stories.svelte +98 -0
- package/dist/components/cards/ProductCard.stories.svelte.d.ts +19 -0
- package/dist/components/cards/ProductCard.svelte +150 -0
- package/dist/components/cards/ProductCard.svelte.d.ts +22 -0
- package/dist/components/cards/ProjectCard.stories.svelte +88 -0
- package/dist/components/cards/ProjectCard.stories.svelte.d.ts +19 -0
- package/dist/components/cards/ProjectCard.svelte +109 -0
- package/dist/components/cards/ProjectCard.svelte.d.ts +20 -0
- package/dist/components/cards/index.d.ts +4 -0
- package/dist/components/cards/index.js +4 -0
- package/dist/components/data/Accordion.stories.svelte +316 -0
- package/dist/components/data/Accordion.stories.svelte.d.ts +19 -0
- package/dist/components/data/Accordion.svelte +23 -0
- package/dist/components/data/Accordion.svelte.d.ts +9 -0
- package/dist/components/data/AccordionItem.svelte +112 -0
- package/dist/components/data/AccordionItem.svelte.d.ts +11 -0
- package/dist/components/data/Table.composition.stories.svelte +67 -0
- package/dist/components/data/Table.composition.stories.svelte.d.ts +19 -0
- package/dist/components/data/Table.stories.svelte +137 -0
- package/dist/components/data/Table.stories.svelte.d.ts +19 -0
- package/dist/components/data/Table.svelte +83 -0
- package/dist/components/data/Table.svelte.d.ts +14 -0
- package/dist/components/data/Tabs.stories.svelte +386 -0
- package/dist/components/data/Tabs.stories.svelte.d.ts +19 -0
- package/dist/components/data/Tabs.svelte +142 -0
- package/dist/components/data/Tabs.svelte.d.ts +19 -0
- package/dist/components/data/index.d.ts +4 -0
- package/dist/components/data/index.js +4 -0
- package/dist/components/feedback/Modal.stories.svelte +192 -0
- package/dist/components/feedback/Modal.stories.svelte.d.ts +4 -0
- package/dist/components/feedback/Modal.svelte +185 -0
- package/dist/components/feedback/Modal.svelte.d.ts +19 -0
- package/dist/components/feedback/Toast.stories.svelte +203 -0
- package/dist/components/feedback/Toast.stories.svelte.d.ts +19 -0
- package/dist/components/feedback/Toast.svelte +109 -0
- package/dist/components/feedback/Toast.svelte.d.ts +15 -0
- package/dist/components/feedback/ToastRegion.stories.svelte +193 -0
- package/dist/components/feedback/ToastRegion.stories.svelte.d.ts +19 -0
- package/dist/components/feedback/ToastRegion.svelte +102 -0
- package/dist/components/feedback/ToastRegion.svelte.d.ts +9 -0
- package/dist/components/feedback/index.d.ts +3 -0
- package/dist/components/feedback/index.js +3 -0
- package/dist/components/forms/Checkbox.stories.svelte +103 -0
- package/dist/components/forms/Checkbox.stories.svelte.d.ts +19 -0
- package/dist/components/forms/Checkbox.svelte +150 -0
- package/dist/components/forms/Checkbox.svelte.d.ts +11 -0
- package/dist/components/forms/Field.stories.svelte +113 -0
- package/dist/components/forms/Field.stories.svelte.d.ts +19 -0
- package/dist/components/forms/Field.svelte +77 -0
- package/dist/components/forms/Field.svelte.d.ts +17 -0
- package/dist/components/forms/Input.stories.svelte +58 -0
- package/dist/components/forms/Input.stories.svelte.d.ts +19 -0
- package/dist/components/forms/Input.svelte +64 -0
- package/dist/components/forms/Input.svelte.d.ts +9 -0
- package/dist/components/forms/InputWrap.composition.stories.svelte +32 -0
- package/dist/components/forms/InputWrap.composition.stories.svelte.d.ts +19 -0
- package/dist/components/forms/InputWrap.stories.svelte +53 -0
- package/dist/components/forms/InputWrap.stories.svelte.d.ts +19 -0
- package/dist/components/forms/InputWrap.svelte +128 -0
- package/dist/components/forms/InputWrap.svelte.d.ts +21 -0
- package/dist/components/forms/Radio.stories.svelte +70 -0
- package/dist/components/forms/Radio.stories.svelte.d.ts +19 -0
- package/dist/components/forms/Radio.svelte +109 -0
- package/dist/components/forms/Radio.svelte.d.ts +9 -0
- package/dist/components/forms/RadioGroup.stories.svelte +115 -0
- package/dist/components/forms/RadioGroup.stories.svelte.d.ts +19 -0
- package/dist/components/forms/RadioGroup.svelte +116 -0
- package/dist/components/forms/RadioGroup.svelte.d.ts +24 -0
- package/dist/components/forms/Select.stories.svelte +168 -0
- package/dist/components/forms/Select.stories.svelte.d.ts +19 -0
- package/dist/components/forms/Select.svelte +262 -0
- package/dist/components/forms/Select.svelte.d.ts +23 -0
- package/dist/components/forms/Switch.stories.svelte +86 -0
- package/dist/components/forms/Switch.stories.svelte.d.ts +19 -0
- package/dist/components/forms/Switch.svelte +113 -0
- package/dist/components/forms/Switch.svelte.d.ts +11 -0
- package/dist/components/forms/Textarea.stories.svelte +40 -0
- package/dist/components/forms/Textarea.stories.svelte.d.ts +19 -0
- package/dist/components/forms/Textarea.svelte +66 -0
- package/dist/components/forms/Textarea.svelte.d.ts +9 -0
- package/dist/components/forms/field-context.d.ts +7 -0
- package/dist/components/forms/field-context.js +1 -0
- package/dist/components/forms/index.d.ts +9 -0
- package/dist/components/forms/index.js +9 -0
- package/dist/components/layout/Container.stories.svelte +67 -0
- package/dist/components/layout/Container.stories.svelte.d.ts +19 -0
- package/dist/components/layout/Container.svelte +52 -0
- package/dist/components/layout/Container.svelte.d.ts +14 -0
- package/dist/components/layout/Grid.stories.svelte +109 -0
- package/dist/components/layout/Grid.stories.svelte.d.ts +19 -0
- package/dist/components/layout/Grid.svelte +54 -0
- package/dist/components/layout/Grid.svelte.d.ts +19 -0
- package/dist/components/layout/Inline.stories.svelte +136 -0
- package/dist/components/layout/Inline.stories.svelte.d.ts +19 -0
- package/dist/components/layout/Inline.svelte +46 -0
- package/dist/components/layout/Inline.svelte.d.ts +19 -0
- package/dist/components/layout/Prose.stories.svelte +423 -0
- package/dist/components/layout/Prose.stories.svelte.d.ts +19 -0
- package/dist/components/layout/Prose.svelte +176 -0
- package/dist/components/layout/Prose.svelte.d.ts +12 -0
- package/dist/components/layout/Rule.stories.svelte +80 -0
- package/dist/components/layout/Rule.stories.svelte.d.ts +19 -0
- package/dist/components/layout/Rule.svelte +33 -0
- package/dist/components/layout/Rule.svelte.d.ts +9 -0
- package/dist/components/layout/Spread.stories.svelte +118 -0
- package/dist/components/layout/Spread.stories.svelte.d.ts +19 -0
- package/dist/components/layout/Spread.svelte +38 -0
- package/dist/components/layout/Spread.svelte.d.ts +16 -0
- package/dist/components/layout/Stack.stories.svelte +90 -0
- package/dist/components/layout/Stack.stories.svelte.d.ts +19 -0
- package/dist/components/layout/Stack.svelte +37 -0
- package/dist/components/layout/Stack.svelte.d.ts +16 -0
- package/dist/components/layout/index.d.ts +7 -0
- package/dist/components/layout/index.js +7 -0
- package/dist/components/navigation/Breadcrumb.stories.svelte +122 -0
- package/dist/components/navigation/Breadcrumb.stories.svelte.d.ts +19 -0
- package/dist/components/navigation/Breadcrumb.svelte +70 -0
- package/dist/components/navigation/Breadcrumb.svelte.d.ts +13 -0
- package/dist/components/navigation/Nav.stories.svelte +323 -0
- package/dist/components/navigation/Nav.stories.svelte.d.ts +19 -0
- package/dist/components/navigation/Nav.svelte +257 -0
- package/dist/components/navigation/Nav.svelte.d.ts +21 -0
- package/dist/components/navigation/index.d.ts +2 -0
- package/dist/components/navigation/index.js +2 -0
- package/dist/components/patterns/ActivityRow.stories.svelte +45 -0
- package/dist/components/patterns/ActivityRow.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/ActivityRow.svelte +69 -0
- package/dist/components/patterns/ActivityRow.svelte.d.ts +16 -0
- package/dist/components/patterns/Alert.stories.svelte +63 -0
- package/dist/components/patterns/Alert.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/Alert.svelte +91 -0
- package/dist/components/patterns/Alert.svelte.d.ts +16 -0
- package/dist/components/patterns/CtaBlock.stories.svelte +62 -0
- package/dist/components/patterns/CtaBlock.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/CtaBlock.svelte +80 -0
- package/dist/components/patterns/CtaBlock.svelte.d.ts +16 -0
- package/dist/components/patterns/KvList.stories.svelte +48 -0
- package/dist/components/patterns/KvList.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/KvList.svelte +65 -0
- package/dist/components/patterns/KvList.svelte.d.ts +15 -0
- package/dist/components/patterns/PageHero.stories.svelte +62 -0
- package/dist/components/patterns/PageHero.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/PageHero.svelte +62 -0
- package/dist/components/patterns/PageHero.svelte.d.ts +14 -0
- package/dist/components/patterns/ProgressBar.stories.svelte +83 -0
- package/dist/components/patterns/ProgressBar.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/ProgressBar.svelte +71 -0
- package/dist/components/patterns/ProgressBar.svelte.d.ts +13 -0
- package/dist/components/patterns/SectionFoot.stories.svelte +37 -0
- package/dist/components/patterns/SectionFoot.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/SectionFoot.svelte +70 -0
- package/dist/components/patterns/SectionFoot.svelte.d.ts +15 -0
- package/dist/components/patterns/SectionHead.stories.svelte +67 -0
- package/dist/components/patterns/SectionHead.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/SectionHead.svelte +54 -0
- package/dist/components/patterns/SectionHead.svelte.d.ts +14 -0
- package/dist/components/patterns/StatCard.stories.svelte +59 -0
- package/dist/components/patterns/StatCard.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/StatCard.svelte +57 -0
- package/dist/components/patterns/StatCard.svelte.d.ts +15 -0
- package/dist/components/patterns/index.d.ts +9 -0
- package/dist/components/patterns/index.js +9 -0
- package/dist/components/primitives/Button.stories.svelte +132 -0
- package/dist/components/primitives/Button.stories.svelte.d.ts +19 -0
- package/dist/components/primitives/Button.svelte +142 -0
- package/dist/components/primitives/Button.svelte.d.ts +16 -0
- package/dist/components/primitives/Heading.stories.svelte +137 -0
- package/dist/components/primitives/Heading.stories.svelte.d.ts +19 -0
- package/dist/components/primitives/Heading.svelte +107 -0
- package/dist/components/primitives/Heading.svelte.d.ts +23 -0
- package/dist/components/primitives/Led.stories.svelte +63 -0
- package/dist/components/primitives/Led.stories.svelte.d.ts +19 -0
- package/dist/components/primitives/Led.svelte +65 -0
- package/dist/components/primitives/Led.svelte.d.ts +11 -0
- package/dist/components/primitives/TagPill.stories.svelte +90 -0
- package/dist/components/primitives/TagPill.stories.svelte.d.ts +19 -0
- package/dist/components/primitives/TagPill.svelte +44 -0
- package/dist/components/primitives/TagPill.svelte.d.ts +9 -0
- package/dist/components/primitives/Text.stories.svelte +252 -0
- package/dist/components/primitives/Text.stories.svelte.d.ts +19 -0
- package/dist/components/primitives/Text.svelte +101 -0
- package/dist/components/primitives/Text.svelte.d.ts +25 -0
- package/dist/components/primitives/index.d.ts +5 -0
- package/dist/components/primitives/index.js +5 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/stores/toast.d.ts +19 -0
- package/dist/stores/toast.js +22 -0
- package/dist/storybook-utils.d.ts +11 -0
- package/dist/storybook-utils.js +29 -0
- package/dist/tokens/ColorSwatch.svelte +73 -0
- package/dist/tokens/ColorSwatch.svelte.d.ts +10 -0
- package/dist/tokens/layout.css +144 -0
- package/dist/tokens/patterns.css +281 -0
- package/dist/tokens/tokens.css +96 -0
- package/dist/tokens/tokens.stories.svelte +107 -0
- package/dist/tokens/tokens.stories.svelte.d.ts +18 -0
- package/dist/tokens/typography.css +159 -0
- package/package.json +62 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import { defineMeta } from "@storybook/addon-svelte-csf";
|
|
3
|
+
import { expect, within } from "storybook/test";
|
|
4
|
+
import { resolveTokenColor, resolveTokenFgColor } from "../../storybook-utils.js";
|
|
5
|
+
import Table from "./Table.svelte";
|
|
6
|
+
|
|
7
|
+
const { Story } = defineMeta({
|
|
8
|
+
title: "Data/Table",
|
|
9
|
+
component: Table,
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
});
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<Story name="Basic"
|
|
15
|
+
args={{
|
|
16
|
+
headers: ['ID', 'Product', 'Category'],
|
|
17
|
+
rows: [
|
|
18
|
+
['CONDUIT-PDX2', 'Conduit PDX-2', 'Power'],
|
|
19
|
+
['DISTRANS-AR1', 'Distrans AR-1', 'Envelope'],
|
|
20
|
+
]
|
|
21
|
+
}}
|
|
22
|
+
play={async ({ canvasElement }) => {
|
|
23
|
+
const canvas = within(canvasElement);
|
|
24
|
+
|
|
25
|
+
const table = canvas.getByRole("table");
|
|
26
|
+
await expect(table).toBeVisible();
|
|
27
|
+
await expect(table).toHaveClass("dxl-table");
|
|
28
|
+
|
|
29
|
+
const headers = canvas.getAllByRole("columnheader");
|
|
30
|
+
await expect(headers.length).toBe(3);
|
|
31
|
+
await expect(canvas.getByRole("columnheader", { name: /id/i })).toBeVisible();
|
|
32
|
+
await expect(canvas.getByRole("columnheader", { name: /product/i })).toBeVisible();
|
|
33
|
+
await expect(canvas.getByRole("columnheader", { name: /category/i })).toBeVisible();
|
|
34
|
+
|
|
35
|
+
await expect(canvas.getByRole("cell", { name: "CONDUIT-PDX2" })).toBeVisible();
|
|
36
|
+
await expect(canvas.getByRole("cell", { name: "Conduit PDX-2" })).toBeVisible();
|
|
37
|
+
await expect(canvas.getByRole("cell", { name: "Power" })).toBeVisible();
|
|
38
|
+
await expect(canvas.getByRole("cell", { name: "DISTRANS-AR1" })).toBeVisible();
|
|
39
|
+
|
|
40
|
+
const allTh = canvasElement.querySelectorAll("th");
|
|
41
|
+
await expect(allTh.length).toBe(3);
|
|
42
|
+
const allTd = canvasElement.querySelectorAll("td");
|
|
43
|
+
await expect(allTd.length).toBe(6);
|
|
44
|
+
|
|
45
|
+
await expect(getComputedStyle(table).borderCollapse).toBe("collapse");
|
|
46
|
+
|
|
47
|
+
const fontFamily = getComputedStyle(table).fontFamily.toLowerCase();
|
|
48
|
+
await expect(fontFamily.includes("jetbrains") || fontFamily.includes("mono")).toBe(true);
|
|
49
|
+
}} />
|
|
50
|
+
|
|
51
|
+
<Story name="With Caption"
|
|
52
|
+
args={{
|
|
53
|
+
headers: ['ID', 'Product', 'Category'],
|
|
54
|
+
rows: [['CONDUIT-PDX2', 'Conduit PDX-2', 'Power']],
|
|
55
|
+
caption: 'Product inventory',
|
|
56
|
+
}}
|
|
57
|
+
play={async ({ canvasElement }) => {
|
|
58
|
+
const caption = canvasElement.querySelector("caption");
|
|
59
|
+
await expect(caption).not.toBeNull();
|
|
60
|
+
await expect(caption!.textContent!.trim()).toBe("Product inventory");
|
|
61
|
+
}} />
|
|
62
|
+
|
|
63
|
+
<Story name="Empty rows"
|
|
64
|
+
args={{
|
|
65
|
+
headers: ['Col A', 'Col B'],
|
|
66
|
+
}}
|
|
67
|
+
play={async ({ canvasElement }) => {
|
|
68
|
+
const canvas = within(canvasElement);
|
|
69
|
+
|
|
70
|
+
await expect(canvas.getByRole("columnheader", { name: /col a/i })).toBeVisible();
|
|
71
|
+
await expect(canvas.getByRole("columnheader", { name: /col b/i })).toBeVisible();
|
|
72
|
+
|
|
73
|
+
const tbody = canvasElement.querySelector("tbody");
|
|
74
|
+
await expect(tbody).not.toBeNull();
|
|
75
|
+
const rows = tbody!.querySelectorAll("tr");
|
|
76
|
+
await expect(rows.length).toBe(0);
|
|
77
|
+
|
|
78
|
+
const caption = canvasElement.querySelector("caption");
|
|
79
|
+
await expect(caption).toBeNull();
|
|
80
|
+
}} />
|
|
81
|
+
|
|
82
|
+
<Story name="Header cell styles"
|
|
83
|
+
args={{
|
|
84
|
+
headers: ['Name', 'Value'],
|
|
85
|
+
rows: [['Alpha', '1']],
|
|
86
|
+
}}
|
|
87
|
+
play={async ({ canvasElement }) => {
|
|
88
|
+
const canvas = within(canvasElement);
|
|
89
|
+
const th = canvas.getByRole("columnheader", { name: /name/i });
|
|
90
|
+
const style = getComputedStyle(th);
|
|
91
|
+
|
|
92
|
+
await expect(style.textTransform).toBe("uppercase");
|
|
93
|
+
await expect(style.fontWeight).toBe("700");
|
|
94
|
+
|
|
95
|
+
const inkFaint = resolveTokenFgColor("--ink-faint");
|
|
96
|
+
await expect(style.color).toBe(inkFaint);
|
|
97
|
+
|
|
98
|
+
const ruleStrong = resolveTokenFgColor("--rule-strong");
|
|
99
|
+
await expect(style.borderBottomColor).toBe(ruleStrong);
|
|
100
|
+
await expect(style.borderBottomStyle).toBe("solid");
|
|
101
|
+
|
|
102
|
+
await expect(style.fontSize).toBe("10px");
|
|
103
|
+
await expect(style.letterSpacing).toBe("1px");
|
|
104
|
+
}} />
|
|
105
|
+
|
|
106
|
+
<Story name="Body cell styles"
|
|
107
|
+
args={{
|
|
108
|
+
headers: ['Name', 'Value'],
|
|
109
|
+
rows: [['Alpha', '1']],
|
|
110
|
+
}}
|
|
111
|
+
play={async ({ canvasElement }) => {
|
|
112
|
+
const td = canvasElement.querySelector("td");
|
|
113
|
+
await expect(td).not.toBeNull();
|
|
114
|
+
const style = getComputedStyle(td!);
|
|
115
|
+
|
|
116
|
+
const inkDim = resolveTokenFgColor("--ink-dim");
|
|
117
|
+
await expect(style.color).toBe(inkDim);
|
|
118
|
+
|
|
119
|
+
await expect(style.verticalAlign).toBe("middle");
|
|
120
|
+
|
|
121
|
+
await expect(style.borderBottomStyle).toBe("dashed");
|
|
122
|
+
await expect(style.borderBottomWidth).toBe("1px");
|
|
123
|
+
|
|
124
|
+
// On hover: tbody tr:hover td { background: var(--bg-rail) } — visual documentation only;
|
|
125
|
+
// CSS :hover pseudo-class is not reliably triggered in Vitest browser mode.
|
|
126
|
+
}} />
|
|
127
|
+
|
|
128
|
+
<Story name="Attribute forwarding"
|
|
129
|
+
args={{
|
|
130
|
+
headers: ['Col'],
|
|
131
|
+
'aria-label': 'Test table',
|
|
132
|
+
}}
|
|
133
|
+
play={async ({ canvasElement }) => {
|
|
134
|
+
const table = canvasElement.querySelector("table.dxl-table");
|
|
135
|
+
await expect(table).not.toBeNull();
|
|
136
|
+
await expect(table!.getAttribute("aria-label")).toBe("Test table");
|
|
137
|
+
}} />
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import Table from "./Table.svelte";
|
|
2
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
3
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
4
|
+
$$bindings?: Bindings;
|
|
5
|
+
} & Exports;
|
|
6
|
+
(internal: unknown, props: {
|
|
7
|
+
$$events?: Events;
|
|
8
|
+
$$slots?: Slots;
|
|
9
|
+
}): Exports & {
|
|
10
|
+
$set?: any;
|
|
11
|
+
$on?: any;
|
|
12
|
+
};
|
|
13
|
+
z_$$bindings?: Bindings;
|
|
14
|
+
}
|
|
15
|
+
declare const Table: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
16
|
+
[evt: string]: CustomEvent<any>;
|
|
17
|
+
}, {}, {}, string>;
|
|
18
|
+
type Table = InstanceType<typeof Table>;
|
|
19
|
+
export default Table;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLTableAttributes } from 'svelte/elements'
|
|
3
|
+
import type { Snippet } from 'svelte'
|
|
4
|
+
|
|
5
|
+
interface TableProps extends HTMLTableAttributes {
|
|
6
|
+
/** Column header labels. */
|
|
7
|
+
headers: string[]
|
|
8
|
+
/** Data rows — each inner array must match the length of `headers`. */
|
|
9
|
+
rows?: string[][]
|
|
10
|
+
children?: Snippet
|
|
11
|
+
/** Optional `<caption>` text for the table. */
|
|
12
|
+
caption?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let { headers, rows, children, caption, ...rest }: TableProps = $props()
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<table class="dxl-table" {...rest}>
|
|
19
|
+
{#if caption}
|
|
20
|
+
<caption class="dxl-table-caption">{caption}</caption>
|
|
21
|
+
{/if}
|
|
22
|
+
<thead>
|
|
23
|
+
<tr>
|
|
24
|
+
{#each headers as header}
|
|
25
|
+
<th scope="col">{header}</th>
|
|
26
|
+
{/each}
|
|
27
|
+
</tr>
|
|
28
|
+
</thead>
|
|
29
|
+
<tbody>
|
|
30
|
+
{#if children}
|
|
31
|
+
{@render children()}
|
|
32
|
+
{:else if rows}
|
|
33
|
+
{#each rows as row}
|
|
34
|
+
<tr>
|
|
35
|
+
{#each row as cell}
|
|
36
|
+
<td>{cell}</td>
|
|
37
|
+
{/each}
|
|
38
|
+
</tr>
|
|
39
|
+
{/each}
|
|
40
|
+
{/if}
|
|
41
|
+
</tbody>
|
|
42
|
+
</table>
|
|
43
|
+
|
|
44
|
+
<style>
|
|
45
|
+
.dxl-table {
|
|
46
|
+
width: 100%;
|
|
47
|
+
border-collapse: collapse;
|
|
48
|
+
font-family: var(--mono);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.dxl-table-caption {
|
|
52
|
+
font-size: 10px;
|
|
53
|
+
letter-spacing: 0.1em;
|
|
54
|
+
text-transform: uppercase;
|
|
55
|
+
color: var(--ink-faint);
|
|
56
|
+
text-align: left;
|
|
57
|
+
padding-bottom: 6px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.dxl-table thead th {
|
|
61
|
+
text-align: left;
|
|
62
|
+
padding: 8px 12px;
|
|
63
|
+
border-bottom: 1px solid var(--rule-strong);
|
|
64
|
+
font-size: 10px;
|
|
65
|
+
letter-spacing: 0.1em;
|
|
66
|
+
text-transform: uppercase;
|
|
67
|
+
color: var(--ink-faint);
|
|
68
|
+
font-weight: 700;
|
|
69
|
+
white-space: nowrap;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.dxl-table tbody td {
|
|
73
|
+
padding: 8px 12px;
|
|
74
|
+
border-bottom: 1px dashed var(--rule);
|
|
75
|
+
color: var(--ink-dim);
|
|
76
|
+
vertical-align: middle;
|
|
77
|
+
font-size: 12px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.dxl-table tbody tr:hover td {
|
|
81
|
+
background: var(--bg-rail);
|
|
82
|
+
}
|
|
83
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { HTMLTableAttributes } from 'svelte/elements';
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
interface TableProps extends HTMLTableAttributes {
|
|
4
|
+
/** Column header labels. */
|
|
5
|
+
headers: string[];
|
|
6
|
+
/** Data rows — each inner array must match the length of `headers`. */
|
|
7
|
+
rows?: string[][];
|
|
8
|
+
children?: Snippet;
|
|
9
|
+
/** Optional `<caption>` text for the table. */
|
|
10
|
+
caption?: string;
|
|
11
|
+
}
|
|
12
|
+
declare const Table: import("svelte").Component<TableProps, {}, "">;
|
|
13
|
+
type Table = ReturnType<typeof Table>;
|
|
14
|
+
export default Table;
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import { defineMeta } from "@storybook/addon-svelte-csf";
|
|
3
|
+
import { expect, within } from "storybook/test";
|
|
4
|
+
import { resolveTokenColor, resolveTokenFgColor } from "../../storybook-utils.js";
|
|
5
|
+
import Tabs from "./Tabs.svelte";
|
|
6
|
+
|
|
7
|
+
// No component: — Tabs requires Snippet values for tab.panel,
|
|
8
|
+
// which cannot be passed through plain args. Full <Tabs> is rendered
|
|
9
|
+
// in each story slot (composition pattern).
|
|
10
|
+
const { Story } = defineMeta({
|
|
11
|
+
title: "Data/Tabs",
|
|
12
|
+
tags: ["autodocs"],
|
|
13
|
+
});
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<!-- AC-46: "Underline" story — tablist, aria-selected, panel visibility -->
|
|
17
|
+
<Story name="Underline"
|
|
18
|
+
play={async ({ canvasElement }) => {
|
|
19
|
+
const canvas = within(canvasElement);
|
|
20
|
+
|
|
21
|
+
// AC-25: tab bar has role="tablist"
|
|
22
|
+
const tablist = canvas.getByRole("tablist");
|
|
23
|
+
await expect(tablist).toBeVisible();
|
|
24
|
+
|
|
25
|
+
// AC-24: root div has class tabs and tabs--underline
|
|
26
|
+
const root = canvasElement.querySelector(".tabs") as HTMLElement;
|
|
27
|
+
await expect(root).not.toBeNull();
|
|
28
|
+
await expect(root).toHaveClass("tabs--underline");
|
|
29
|
+
|
|
30
|
+
// AC-26: each tab is a button with role="tab"
|
|
31
|
+
const tabs = canvas.getAllByRole("tab");
|
|
32
|
+
await expect(tabs.length).toBe(3);
|
|
33
|
+
|
|
34
|
+
// AC-29: first tab (Overview) is active by default
|
|
35
|
+
const overviewTab = canvas.getByRole("tab", { name: /overview/i });
|
|
36
|
+
await expect(overviewTab).toHaveAttribute("aria-selected", "true");
|
|
37
|
+
|
|
38
|
+
// AC-30: other tabs are inactive
|
|
39
|
+
const specsTab = canvas.getByRole("tab", { name: /specs/i });
|
|
40
|
+
await expect(specsTab).toHaveAttribute("aria-selected", "false");
|
|
41
|
+
const notesTab = canvas.getByRole("tab", { name: /notes/i });
|
|
42
|
+
await expect(notesTab).toHaveAttribute("aria-selected", "false");
|
|
43
|
+
|
|
44
|
+
// AC-27: each tab has id="tab-{id}"
|
|
45
|
+
await expect(overviewTab.getAttribute("id")).toBe("tab-overview");
|
|
46
|
+
await expect(specsTab.getAttribute("id")).toBe("tab-specs");
|
|
47
|
+
|
|
48
|
+
// AC-28: each tab has aria-controls="panel-{id}"
|
|
49
|
+
await expect(overviewTab.getAttribute("aria-controls")).toBe("panel-overview");
|
|
50
|
+
|
|
51
|
+
// AC-31: panels have role="tabpanel", id, aria-labelledby
|
|
52
|
+
const overviewPanel = canvasElement.querySelector("#panel-overview") as HTMLElement;
|
|
53
|
+
await expect(overviewPanel).not.toBeNull();
|
|
54
|
+
await expect(overviewPanel.getAttribute("role")).toBe("tabpanel");
|
|
55
|
+
await expect(overviewPanel.getAttribute("aria-labelledby")).toBe("tab-overview");
|
|
56
|
+
|
|
57
|
+
// AC-32: active panel is visible (no hidden attribute)
|
|
58
|
+
await expect(overviewPanel.hasAttribute("hidden")).toBe(false);
|
|
59
|
+
await expect(within(overviewPanel).getByText(/Overview panel content/)).toBeVisible();
|
|
60
|
+
|
|
61
|
+
// AC-33: inactive panels have hidden attribute
|
|
62
|
+
const specsPanel = canvasElement.querySelector("#panel-specs") as HTMLElement;
|
|
63
|
+
await expect(specsPanel.hasAttribute("hidden")).toBe(true);
|
|
64
|
+
const notesPanel = canvasElement.querySelector("#panel-notes") as HTMLElement;
|
|
65
|
+
await expect(notesPanel.hasAttribute("hidden")).toBe(true);
|
|
66
|
+
}}>
|
|
67
|
+
{#snippet children()}
|
|
68
|
+
{#snippet overviewPanel()}
|
|
69
|
+
<p>Overview panel content — introducing the system.</p>
|
|
70
|
+
{/snippet}
|
|
71
|
+
{#snippet specsPanel()}
|
|
72
|
+
<p>Technical specifications go here.</p>
|
|
73
|
+
{/snippet}
|
|
74
|
+
{#snippet notesPanel()}
|
|
75
|
+
<p>Design notes and rationale.</p>
|
|
76
|
+
{/snippet}
|
|
77
|
+
<Tabs tabs={[
|
|
78
|
+
{ id: 'overview', label: 'Overview', panel: overviewPanel },
|
|
79
|
+
{ id: 'specs', label: 'Specs', panel: specsPanel },
|
|
80
|
+
{ id: 'notes', label: 'Notes', panel: notesPanel },
|
|
81
|
+
]} />
|
|
82
|
+
{/snippet}
|
|
83
|
+
</Story>
|
|
84
|
+
|
|
85
|
+
<!-- AC-48: "Pill variant" story -->
|
|
86
|
+
<Story name="Pill variant"
|
|
87
|
+
play={async ({ canvasElement }) => {
|
|
88
|
+
// AC-24: root has tabs--pill class
|
|
89
|
+
const root = canvasElement.querySelector(".tabs") as HTMLElement;
|
|
90
|
+
await expect(root).not.toBeNull();
|
|
91
|
+
await expect(root).toHaveClass("tabs--pill");
|
|
92
|
+
|
|
93
|
+
// AC-40: active tab background resolves to var(--amber)
|
|
94
|
+
const activeTab = within(canvasElement).getByRole("tab", { name: /overview/i });
|
|
95
|
+
await expect(activeTab).toHaveAttribute("aria-selected", "true");
|
|
96
|
+
const amberBg = resolveTokenColor("--amber");
|
|
97
|
+
await expect(getComputedStyle(activeTab).backgroundColor).toBe(amberBg);
|
|
98
|
+
|
|
99
|
+
// AC-41: active tab color resolves to var(--bg)
|
|
100
|
+
const bgColor = resolveTokenColor("--bg");
|
|
101
|
+
await expect(getComputedStyle(activeTab).color).toBe(bgColor);
|
|
102
|
+
}}>
|
|
103
|
+
{#snippet children()}
|
|
104
|
+
{#snippet overviewPanel()}
|
|
105
|
+
<p>Overview content.</p>
|
|
106
|
+
{/snippet}
|
|
107
|
+
{#snippet specsPanel()}
|
|
108
|
+
<p>Specs content.</p>
|
|
109
|
+
{/snippet}
|
|
110
|
+
{#snippet notesPanel()}
|
|
111
|
+
<p>Notes content.</p>
|
|
112
|
+
{/snippet}
|
|
113
|
+
<Tabs variant="pill" tabs={[
|
|
114
|
+
{ id: 'overview', label: 'Overview', panel: overviewPanel },
|
|
115
|
+
{ id: 'specs', label: 'Specs', panel: specsPanel },
|
|
116
|
+
{ id: 'notes', label: 'Notes', panel: notesPanel },
|
|
117
|
+
]} />
|
|
118
|
+
{/snippet}
|
|
119
|
+
</Story>
|
|
120
|
+
|
|
121
|
+
<!-- AC-47: "Tab switch interaction" story -->
|
|
122
|
+
<Story name="Tab switch interaction"
|
|
123
|
+
play={async ({ canvasElement, userEvent }) => {
|
|
124
|
+
const canvas = within(canvasElement);
|
|
125
|
+
|
|
126
|
+
// Initial state: Overview active
|
|
127
|
+
const overviewTab = canvas.getByRole("tab", { name: /overview/i });
|
|
128
|
+
await expect(overviewTab).toHaveAttribute("aria-selected", "true");
|
|
129
|
+
|
|
130
|
+
const overviewPanel = canvasElement.querySelector("#panel-overview") as HTMLElement;
|
|
131
|
+
await expect(overviewPanel.hasAttribute("hidden")).toBe(false);
|
|
132
|
+
|
|
133
|
+
// AC-34: click Specs tab
|
|
134
|
+
const specsTab = canvas.getByRole("tab", { name: /specs/i });
|
|
135
|
+
await userEvent.click(specsTab);
|
|
136
|
+
|
|
137
|
+
// AC-34: Specs tab becomes aria-selected="true"
|
|
138
|
+
await expect(specsTab).toHaveAttribute("aria-selected", "true");
|
|
139
|
+
|
|
140
|
+
// AC-34: Overview tab loses aria-selected
|
|
141
|
+
await expect(overviewTab).toHaveAttribute("aria-selected", "false");
|
|
142
|
+
|
|
143
|
+
// AC-35: Specs panel becomes visible (no hidden attribute)
|
|
144
|
+
const specsPanel = canvasElement.querySelector("#panel-specs") as HTMLElement;
|
|
145
|
+
await expect(specsPanel.hasAttribute("hidden")).toBe(false);
|
|
146
|
+
await expect(within(specsPanel).getByText(/Technical specifications/)).toBeVisible();
|
|
147
|
+
|
|
148
|
+
// AC-35: Overview panel gains hidden attribute
|
|
149
|
+
await expect(overviewPanel.hasAttribute("hidden")).toBe(true);
|
|
150
|
+
}}>
|
|
151
|
+
{#snippet children()}
|
|
152
|
+
{#snippet overviewPanel()}
|
|
153
|
+
<p>This is the overview panel content.</p>
|
|
154
|
+
{/snippet}
|
|
155
|
+
{#snippet specsPanel()}
|
|
156
|
+
<p>Technical specifications go here.</p>
|
|
157
|
+
{/snippet}
|
|
158
|
+
{#snippet notesPanel()}
|
|
159
|
+
<p>Design notes and rationale.</p>
|
|
160
|
+
{/snippet}
|
|
161
|
+
<Tabs tabs={[
|
|
162
|
+
{ id: 'overview', label: 'Overview', panel: overviewPanel },
|
|
163
|
+
{ id: 'specs', label: 'Specs', panel: specsPanel },
|
|
164
|
+
{ id: 'notes', label: 'Notes', panel: notesPanel },
|
|
165
|
+
]} />
|
|
166
|
+
{/snippet}
|
|
167
|
+
</Story>
|
|
168
|
+
|
|
169
|
+
<!-- AC-37: "Second tab initially active" story -->
|
|
170
|
+
<Story name="Second tab initially active"
|
|
171
|
+
play={async ({ canvasElement }) => {
|
|
172
|
+
const canvas = within(canvasElement);
|
|
173
|
+
|
|
174
|
+
// AC-37: second tab has aria-selected="true" on initial render
|
|
175
|
+
const specsTab = canvas.getByRole("tab", { name: /specs/i });
|
|
176
|
+
await expect(specsTab).toHaveAttribute("aria-selected", "true");
|
|
177
|
+
|
|
178
|
+
const overviewTab = canvas.getByRole("tab", { name: /overview/i });
|
|
179
|
+
await expect(overviewTab).toHaveAttribute("aria-selected", "false");
|
|
180
|
+
|
|
181
|
+
// AC-37: second panel is visible, first is hidden
|
|
182
|
+
const specsPanel = canvasElement.querySelector("#panel-specs") as HTMLElement;
|
|
183
|
+
await expect(specsPanel.hasAttribute("hidden")).toBe(false);
|
|
184
|
+
|
|
185
|
+
const overviewPanel = canvasElement.querySelector("#panel-overview") as HTMLElement;
|
|
186
|
+
await expect(overviewPanel.hasAttribute("hidden")).toBe(true);
|
|
187
|
+
}}>
|
|
188
|
+
{#snippet children()}
|
|
189
|
+
{#snippet overviewPanel()}
|
|
190
|
+
<p>Overview content.</p>
|
|
191
|
+
{/snippet}
|
|
192
|
+
{#snippet specsPanel()}
|
|
193
|
+
<p>Specs panel — initially active.</p>
|
|
194
|
+
{/snippet}
|
|
195
|
+
{#snippet notesPanel()}
|
|
196
|
+
<p>Notes content.</p>
|
|
197
|
+
{/snippet}
|
|
198
|
+
<Tabs active="specs" tabs={[
|
|
199
|
+
{ id: 'overview', label: 'Overview', panel: overviewPanel },
|
|
200
|
+
{ id: 'specs', label: 'Specs', panel: specsPanel },
|
|
201
|
+
{ id: 'notes', label: 'Notes', panel: notesPanel },
|
|
202
|
+
]} />
|
|
203
|
+
{/snippet}
|
|
204
|
+
</Story>
|
|
205
|
+
|
|
206
|
+
<!-- AC-38 / AC-39: Underline active tab amber border-bottom, inactive transparent -->
|
|
207
|
+
<Story name="Underline active border"
|
|
208
|
+
play={async ({ canvasElement }) => {
|
|
209
|
+
const canvas = within(canvasElement);
|
|
210
|
+
|
|
211
|
+
// AC-38: active tab border-bottom-color resolves to var(--amber)
|
|
212
|
+
const activeTab = canvas.getByRole("tab", { name: /overview/i }) as HTMLElement;
|
|
213
|
+
await expect(activeTab).toHaveAttribute("aria-selected", "true");
|
|
214
|
+
const amberColor = resolveTokenFgColor("--amber");
|
|
215
|
+
await expect(getComputedStyle(activeTab).borderBottomColor).toBe(amberColor);
|
|
216
|
+
|
|
217
|
+
// AC-39: inactive tab border-bottom-color is transparent
|
|
218
|
+
const inactiveTab = canvas.getByRole("tab", { name: /specs/i }) as HTMLElement;
|
|
219
|
+
await expect(getComputedStyle(inactiveTab).borderBottomColor).toBe("rgba(0, 0, 0, 0)");
|
|
220
|
+
}}>
|
|
221
|
+
{#snippet children()}
|
|
222
|
+
{#snippet overviewPanel()}
|
|
223
|
+
<p>Overview content.</p>
|
|
224
|
+
{/snippet}
|
|
225
|
+
{#snippet specsPanel()}
|
|
226
|
+
<p>Specs content.</p>
|
|
227
|
+
{/snippet}
|
|
228
|
+
<Tabs tabs={[
|
|
229
|
+
{ id: 'overview', label: 'Overview', panel: overviewPanel },
|
|
230
|
+
{ id: 'specs', label: 'Specs', panel: specsPanel },
|
|
231
|
+
]} />
|
|
232
|
+
{/snippet}
|
|
233
|
+
</Story>
|
|
234
|
+
|
|
235
|
+
<!-- AC-42 / AC-43: Tab labels use uppercase mono font -->
|
|
236
|
+
<Story name="Tab label typography"
|
|
237
|
+
play={async ({ canvasElement }) => {
|
|
238
|
+
const canvas = within(canvasElement);
|
|
239
|
+
const tab = canvas.getByRole("tab", { name: /overview/i }) as HTMLElement;
|
|
240
|
+
|
|
241
|
+
// AC-42: text-transform: uppercase
|
|
242
|
+
await expect(getComputedStyle(tab).textTransform).toBe("uppercase");
|
|
243
|
+
|
|
244
|
+
// AC-43: font-family includes mono/JetBrains
|
|
245
|
+
const fontFamily = getComputedStyle(tab).fontFamily.toLowerCase();
|
|
246
|
+
await expect(fontFamily.includes("jetbrains") || fontFamily.includes("mono")).toBe(true);
|
|
247
|
+
}}>
|
|
248
|
+
{#snippet children()}
|
|
249
|
+
{#snippet overviewPanel()}
|
|
250
|
+
<p>Content.</p>
|
|
251
|
+
{/snippet}
|
|
252
|
+
{#snippet specsPanel()}
|
|
253
|
+
<p>Content.</p>
|
|
254
|
+
{/snippet}
|
|
255
|
+
<Tabs tabs={[
|
|
256
|
+
{ id: 'overview', label: 'Overview', panel: overviewPanel },
|
|
257
|
+
{ id: 'specs', label: 'Specs', panel: specsPanel },
|
|
258
|
+
]} />
|
|
259
|
+
{/snippet}
|
|
260
|
+
</Story>
|
|
261
|
+
|
|
262
|
+
<!-- B15: Keyboard Navigation story — exercises the ARIA Tabs keyboard pattern.
|
|
263
|
+
This story MUST FAIL until Tabs.svelte implements:
|
|
264
|
+
- onkeydown handler on each tab <button> for ArrowLeft / ArrowRight / Home / End
|
|
265
|
+
- automatic activation (arrow key moves focus AND activates the tab)
|
|
266
|
+
-->
|
|
267
|
+
<Story name="Keyboard Navigation"
|
|
268
|
+
play={async ({ canvasElement, userEvent }) => {
|
|
269
|
+
const canvas = within(canvasElement);
|
|
270
|
+
|
|
271
|
+
// Step 1: click Overview tab to ensure keyboard focus is on it
|
|
272
|
+
const overviewTab = canvas.getByRole("tab", { name: /overview/i });
|
|
273
|
+
const specsTab = canvas.getByRole("tab", { name: /specs/i });
|
|
274
|
+
const notesTab = canvas.getByRole("tab", { name: /notes/i });
|
|
275
|
+
await userEvent.click(overviewTab);
|
|
276
|
+
await expect(overviewTab).toHaveAttribute("aria-selected", "true");
|
|
277
|
+
|
|
278
|
+
// Step 2: ArrowRight → Specs tab becomes active
|
|
279
|
+
// AC-23, AC-30, AC-35 (panel switching on keyboard nav)
|
|
280
|
+
await userEvent.keyboard("{ArrowRight}");
|
|
281
|
+
await expect(specsTab).toHaveAttribute("aria-selected", "true");
|
|
282
|
+
await expect(overviewTab).toHaveAttribute("aria-selected", "false");
|
|
283
|
+
// Panel visibility follows keyboard activation
|
|
284
|
+
const overviewPanel = canvasElement.querySelector("#panel-overview") as HTMLElement;
|
|
285
|
+
const specsPanel = canvasElement.querySelector("#panel-specs") as HTMLElement;
|
|
286
|
+
await expect(specsPanel.hasAttribute("hidden")).toBe(false);
|
|
287
|
+
await expect(overviewPanel.hasAttribute("hidden")).toBe(true);
|
|
288
|
+
|
|
289
|
+
// Step 3: ArrowRight → Notes tab becomes active
|
|
290
|
+
await userEvent.keyboard("{ArrowRight}");
|
|
291
|
+
await expect(notesTab).toHaveAttribute("aria-selected", "true");
|
|
292
|
+
await expect(specsTab).toHaveAttribute("aria-selected", "false");
|
|
293
|
+
|
|
294
|
+
// Step 4: ArrowRight wraps to first tab (Overview)
|
|
295
|
+
// AC-24
|
|
296
|
+
await userEvent.keyboard("{ArrowRight}");
|
|
297
|
+
await expect(overviewTab).toHaveAttribute("aria-selected", "true");
|
|
298
|
+
await expect(notesTab).toHaveAttribute("aria-selected", "false");
|
|
299
|
+
|
|
300
|
+
// Step 5: End → Notes tab (last)
|
|
301
|
+
// AC-29
|
|
302
|
+
await userEvent.keyboard("{End}");
|
|
303
|
+
await expect(notesTab).toHaveAttribute("aria-selected", "true");
|
|
304
|
+
await expect(overviewTab).toHaveAttribute("aria-selected", "false");
|
|
305
|
+
|
|
306
|
+
// Step 6: Home → Overview tab (first)
|
|
307
|
+
// AC-28
|
|
308
|
+
await userEvent.keyboard("{Home}");
|
|
309
|
+
await expect(overviewTab).toHaveAttribute("aria-selected", "true");
|
|
310
|
+
await expect(notesTab).toHaveAttribute("aria-selected", "false");
|
|
311
|
+
|
|
312
|
+
// Step 7: ArrowLeft wraps from first to last (Notes)
|
|
313
|
+
// AC-27
|
|
314
|
+
await userEvent.keyboard("{ArrowLeft}");
|
|
315
|
+
await expect(notesTab).toHaveAttribute("aria-selected", "true");
|
|
316
|
+
await expect(overviewTab).toHaveAttribute("aria-selected", "false");
|
|
317
|
+
|
|
318
|
+
// AC-26: ArrowLeft non-wrap — from Notes (last) to Specs (prev)
|
|
319
|
+
await userEvent.keyboard("{ArrowLeft}");
|
|
320
|
+
await expect(specsTab).toHaveAttribute("aria-selected", "true");
|
|
321
|
+
await expect(notesTab).toHaveAttribute("aria-selected", "false");
|
|
322
|
+
|
|
323
|
+
// AC-31: assert exactly one tab has aria-selected="true" at this point
|
|
324
|
+
const allTabs = canvas.getAllByRole("tab");
|
|
325
|
+
const selectedTabs = allTabs.filter(t => t.getAttribute("aria-selected") === "true");
|
|
326
|
+
await expect(selectedTabs.length).toBe(1);
|
|
327
|
+
}}>
|
|
328
|
+
{#snippet children()}
|
|
329
|
+
{#snippet overviewPanel()}
|
|
330
|
+
<p>Overview panel</p>
|
|
331
|
+
{/snippet}
|
|
332
|
+
{#snippet specsPanel()}
|
|
333
|
+
<p>Specs panel</p>
|
|
334
|
+
{/snippet}
|
|
335
|
+
{#snippet notesPanel()}
|
|
336
|
+
<p>Notes panel</p>
|
|
337
|
+
{/snippet}
|
|
338
|
+
<Tabs tabs={[
|
|
339
|
+
{ id: 'overview', label: 'Overview', panel: overviewPanel },
|
|
340
|
+
{ id: 'specs', label: 'Specs', panel: specsPanel },
|
|
341
|
+
{ id: 'notes', label: 'Notes', panel: notesPanel },
|
|
342
|
+
]} />
|
|
343
|
+
{/snippet}
|
|
344
|
+
</Story>
|
|
345
|
+
|
|
346
|
+
<!-- AC-32: single-tab edge case — arrow keys stay on the only tab, no error -->
|
|
347
|
+
<Story name="Single Tab"
|
|
348
|
+
play={async ({ canvasElement, userEvent }) => {
|
|
349
|
+
const canvas = within(canvasElement);
|
|
350
|
+
const tab = canvas.getByRole("tab");
|
|
351
|
+
await userEvent.click(tab);
|
|
352
|
+
await expect(tab).toHaveAttribute("aria-selected", "true");
|
|
353
|
+
await userEvent.keyboard("{ArrowRight}");
|
|
354
|
+
await expect(tab).toHaveAttribute("aria-selected", "true");
|
|
355
|
+
await userEvent.keyboard("{ArrowLeft}");
|
|
356
|
+
await expect(tab).toHaveAttribute("aria-selected", "true");
|
|
357
|
+
await userEvent.keyboard("{Home}");
|
|
358
|
+
await expect(tab).toHaveAttribute("aria-selected", "true");
|
|
359
|
+
await userEvent.keyboard("{End}");
|
|
360
|
+
await expect(tab).toHaveAttribute("aria-selected", "true");
|
|
361
|
+
}}>
|
|
362
|
+
{#snippet children()}
|
|
363
|
+
{#snippet onlyPanel()}
|
|
364
|
+
<p>Only panel</p>
|
|
365
|
+
{/snippet}
|
|
366
|
+
<Tabs tabs={[{ id: 'only', label: 'Only', panel: onlyPanel }]} />
|
|
367
|
+
{/snippet}
|
|
368
|
+
</Story>
|
|
369
|
+
|
|
370
|
+
<!-- AC-44: Tabs forwards ...rest attributes to root div -->
|
|
371
|
+
<Story name="Attribute forwarding"
|
|
372
|
+
play={async ({ canvasElement }) => {
|
|
373
|
+
const root = canvasElement.querySelector(".tabs") as HTMLElement;
|
|
374
|
+
await expect(root).not.toBeNull();
|
|
375
|
+
await expect(root.getAttribute("id")).toBe("my-tabs");
|
|
376
|
+
await expect(root.getAttribute("aria-label")).toBe("Product information");
|
|
377
|
+
}}>
|
|
378
|
+
{#snippet children()}
|
|
379
|
+
{#snippet panelA()}
|
|
380
|
+
<p>Content A.</p>
|
|
381
|
+
{/snippet}
|
|
382
|
+
<Tabs id="my-tabs" aria-label="Product information" tabs={[
|
|
383
|
+
{ id: 'a', label: 'Tab A', panel: panelA },
|
|
384
|
+
]} />
|
|
385
|
+
{/snippet}
|
|
386
|
+
</Story>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import Tabs from "./Tabs.svelte";
|
|
2
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
3
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
4
|
+
$$bindings?: Bindings;
|
|
5
|
+
} & Exports;
|
|
6
|
+
(internal: unknown, props: {
|
|
7
|
+
$$events?: Events;
|
|
8
|
+
$$slots?: Slots;
|
|
9
|
+
}): Exports & {
|
|
10
|
+
$set?: any;
|
|
11
|
+
$on?: any;
|
|
12
|
+
};
|
|
13
|
+
z_$$bindings?: Bindings;
|
|
14
|
+
}
|
|
15
|
+
declare const Tabs: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
16
|
+
[evt: string]: CustomEvent<any>;
|
|
17
|
+
}, {}, {}, string>;
|
|
18
|
+
type Tabs = InstanceType<typeof Tabs>;
|
|
19
|
+
export default Tabs;
|