@charcuterie/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/CHANGELOG.md +13 -0
- package/README.md +161 -0
- package/dist/Alert/Alert.d.ts +88 -0
- package/dist/Alert/Alert.d.ts.map +1 -0
- package/dist/Alert/Alert.js +67 -0
- package/dist/Alert/Alert.js.map +1 -0
- package/dist/Badge/Badge.d.ts +54 -0
- package/dist/Badge/Badge.d.ts.map +1 -0
- package/dist/Badge/Badge.js +54 -0
- package/dist/Badge/Badge.js.map +1 -0
- package/dist/Badge/useIsTextClipped.d.ts +22 -0
- package/dist/Badge/useIsTextClipped.d.ts.map +1 -0
- package/dist/Badge/useIsTextClipped.js +57 -0
- package/dist/Badge/useIsTextClipped.js.map +1 -0
- package/dist/Button/Button.d.ts +58 -0
- package/dist/Button/Button.d.ts.map +1 -0
- package/dist/Button/Button.js +37 -0
- package/dist/Button/Button.js.map +1 -0
- package/dist/Card/Card.d.ts +44 -0
- package/dist/Card/Card.d.ts.map +1 -0
- package/dist/Card/Card.js +59 -0
- package/dist/Card/Card.js.map +1 -0
- package/dist/EmptyState/EmptyState.d.ts +42 -0
- package/dist/EmptyState/EmptyState.d.ts.map +1 -0
- package/dist/EmptyState/EmptyState.js +37 -0
- package/dist/EmptyState/EmptyState.js.map +1 -0
- package/dist/IconButton/IconButton.d.ts +34 -0
- package/dist/IconButton/IconButton.d.ts.map +1 -0
- package/dist/IconButton/IconButton.js +23 -0
- package/dist/IconButton/IconButton.js.map +1 -0
- package/dist/LiveStatusIndicator/LiveStatusIndicator.d.ts +37 -0
- package/dist/LiveStatusIndicator/LiveStatusIndicator.d.ts.map +1 -0
- package/dist/LiveStatusIndicator/LiveStatusIndicator.js +39 -0
- package/dist/LiveStatusIndicator/LiveStatusIndicator.js.map +1 -0
- package/dist/MediaTile/MediaTile.d.ts +46 -0
- package/dist/MediaTile/MediaTile.d.ts.map +1 -0
- package/dist/MediaTile/MediaTile.js +79 -0
- package/dist/MediaTile/MediaTile.js.map +1 -0
- package/dist/MediaTile/mediaStatus.d.ts +22 -0
- package/dist/MediaTile/mediaStatus.d.ts.map +1 -0
- package/dist/MediaTile/mediaStatus.js +24 -0
- package/dist/MediaTile/mediaStatus.js.map +1 -0
- package/dist/Modal/Modal.d.ts +66 -0
- package/dist/Modal/Modal.d.ts.map +1 -0
- package/dist/Modal/Modal.js +111 -0
- package/dist/Modal/Modal.js.map +1 -0
- package/dist/Modal/lockScrollBehind.d.ts +22 -0
- package/dist/Modal/lockScrollBehind.d.ts.map +1 -0
- package/dist/Modal/lockScrollBehind.js +48 -0
- package/dist/Modal/lockScrollBehind.js.map +1 -0
- package/dist/Popover/Popover.d.ts +66 -0
- package/dist/Popover/Popover.d.ts.map +1 -0
- package/dist/Popover/Popover.js +88 -0
- package/dist/Popover/Popover.js.map +1 -0
- package/dist/ProgressBar/ProgressBar.d.ts +51 -0
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -0
- package/dist/ProgressBar/ProgressBar.js +50 -0
- package/dist/ProgressBar/ProgressBar.js.map +1 -0
- package/dist/ProgressBar/progressValue.d.ts +44 -0
- package/dist/ProgressBar/progressValue.d.ts.map +1 -0
- package/dist/ProgressBar/progressValue.js +55 -0
- package/dist/ProgressBar/progressValue.js.map +1 -0
- package/dist/SegmentedControl/SegmentedControl.d.ts +59 -0
- package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -0
- package/dist/SegmentedControl/SegmentedControl.js +129 -0
- package/dist/SegmentedControl/SegmentedControl.js.map +1 -0
- package/dist/SegmentedControl/SegmentedOption.d.ts +28 -0
- package/dist/SegmentedControl/SegmentedOption.d.ts.map +1 -0
- package/dist/SegmentedControl/SegmentedOption.js +40 -0
- package/dist/SegmentedControl/SegmentedOption.js.map +1 -0
- package/dist/Skeleton/Skeleton.d.ts +30 -0
- package/dist/Skeleton/Skeleton.d.ts.map +1 -0
- package/dist/Skeleton/Skeleton.js +43 -0
- package/dist/Skeleton/Skeleton.js.map +1 -0
- package/dist/Spinner/Spinner.d.ts +37 -0
- package/dist/Spinner/Spinner.d.ts.map +1 -0
- package/dist/Spinner/Spinner.js +33 -0
- package/dist/Spinner/Spinner.js.map +1 -0
- package/dist/Tabs/TabTrigger.d.ts +32 -0
- package/dist/Tabs/TabTrigger.d.ts.map +1 -0
- package/dist/Tabs/TabTrigger.js +62 -0
- package/dist/Tabs/TabTrigger.js.map +1 -0
- package/dist/Tabs/Tabs.d.ts +95 -0
- package/dist/Tabs/Tabs.d.ts.map +1 -0
- package/dist/Tabs/Tabs.js +195 -0
- package/dist/Tabs/Tabs.js.map +1 -0
- package/dist/VisuallyHidden/VisuallyHidden.d.ts +25 -0
- package/dist/VisuallyHidden/VisuallyHidden.d.ts.map +1 -0
- package/dist/VisuallyHidden/VisuallyHidden.js +23 -0
- package/dist/VisuallyHidden/VisuallyHidden.js.map +1 -0
- package/dist/controlStyles.d.ts +50 -0
- package/dist/controlStyles.d.ts.map +1 -0
- package/dist/controlStyles.js +60 -0
- package/dist/controlStyles.js.map +1 -0
- package/dist/expectNoAxeViolations.testHelpers.d.ts +19 -0
- package/dist/expectNoAxeViolations.testHelpers.d.ts.map +1 -0
- package/dist/expectNoAxeViolations.testHelpers.js +32 -0
- package/dist/expectNoAxeViolations.testHelpers.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/intentStyles.d.ts +76 -0
- package/dist/intentStyles.d.ts.map +1 -0
- package/dist/intentStyles.js +150 -0
- package/dist/intentStyles.js.map +1 -0
- package/dist/mountStory.testHelpers.d.ts +30 -0
- package/dist/mountStory.testHelpers.d.ts.map +1 -0
- package/dist/mountStory.testHelpers.js +19 -0
- package/dist/mountStory.testHelpers.js.map +1 -0
- package/dist/statusIntent.d.ts +33 -0
- package/dist/statusIntent.d.ts.map +1 -0
- package/dist/statusIntent.js +99 -0
- package/dist/statusIntent.js.map +1 -0
- package/dist/styles.css +130 -0
- package/dist/testing/expectAgentDrivable.d.ts +53 -0
- package/dist/testing/expectAgentDrivable.d.ts.map +1 -0
- package/dist/testing/expectAgentDrivable.js +163 -0
- package/dist/testing/expectAgentDrivable.js.map +1 -0
- package/dist/testing/index.d.ts +11 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +10 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/toClassName.d.ts +12 -0
- package/dist/toClassName.d.ts.map +1 -0
- package/dist/toClassName.js +12 -0
- package/dist/toClassName.js.map +1 -0
- package/dist/tokens.d.ts +16 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +16 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +70 -0
- package/src/Alert/Alert.mdx +70 -0
- package/src/Alert/Alert.stories.tsx +218 -0
- package/src/Alert/Alert.test.tsx +112 -0
- package/src/Alert/Alert.tsx +166 -0
- package/src/Badge/Badge.mdx +43 -0
- package/src/Badge/Badge.stories.tsx +229 -0
- package/src/Badge/Badge.test.tsx +153 -0
- package/src/Badge/Badge.tsx +130 -0
- package/src/Badge/useIsTextClipped.ts +77 -0
- package/src/Button/Button.mdx +44 -0
- package/src/Button/Button.stories.tsx +175 -0
- package/src/Button/Button.test.tsx +114 -0
- package/src/Button/Button.tsx +125 -0
- package/src/Card/Card.mdx +53 -0
- package/src/Card/Card.stories.tsx +207 -0
- package/src/Card/Card.test.tsx +56 -0
- package/src/Card/Card.tsx +142 -0
- package/src/EmptyState/EmptyState.mdx +40 -0
- package/src/EmptyState/EmptyState.stories.tsx +183 -0
- package/src/EmptyState/EmptyState.test.tsx +41 -0
- package/src/EmptyState/EmptyState.tsx +114 -0
- package/src/IconButton/IconButton.mdx +34 -0
- package/src/IconButton/IconButton.stories.tsx +191 -0
- package/src/IconButton/IconButton.test.tsx +70 -0
- package/src/IconButton/IconButton.tsx +54 -0
- package/src/LiveStatusIndicator/LiveStatusIndicator.mdx +43 -0
- package/src/LiveStatusIndicator/LiveStatusIndicator.stories.tsx +199 -0
- package/src/LiveStatusIndicator/LiveStatusIndicator.test.tsx +63 -0
- package/src/LiveStatusIndicator/LiveStatusIndicator.tsx +116 -0
- package/src/MediaTile/MediaTile.mdx +51 -0
- package/src/MediaTile/MediaTile.stories.tsx +211 -0
- package/src/MediaTile/MediaTile.test.tsx +41 -0
- package/src/MediaTile/MediaTile.tsx +227 -0
- package/src/MediaTile/mediaStatus.ts +28 -0
- package/src/Modal/Modal.mdx +113 -0
- package/src/Modal/Modal.stories.tsx +272 -0
- package/src/Modal/Modal.test.tsx +248 -0
- package/src/Modal/Modal.tsx +228 -0
- package/src/Modal/lockScrollBehind.ts +59 -0
- package/src/Popover/Popover.mdx +102 -0
- package/src/Popover/Popover.stories.tsx +247 -0
- package/src/Popover/Popover.test.tsx +191 -0
- package/src/Popover/Popover.tsx +164 -0
- package/src/ProgressBar/ProgressBar.mdx +48 -0
- package/src/ProgressBar/ProgressBar.stories.tsx +232 -0
- package/src/ProgressBar/ProgressBar.test.tsx +74 -0
- package/src/ProgressBar/ProgressBar.tsx +172 -0
- package/src/ProgressBar/progressValue.test.ts +101 -0
- package/src/ProgressBar/progressValue.ts +87 -0
- package/src/SegmentedControl/SegmentedControl.mdx +71 -0
- package/src/SegmentedControl/SegmentedControl.stories.tsx +150 -0
- package/src/SegmentedControl/SegmentedControl.test.tsx +208 -0
- package/src/SegmentedControl/SegmentedControl.tsx +219 -0
- package/src/SegmentedControl/SegmentedOption.tsx +94 -0
- package/src/Skeleton/Skeleton.mdx +30 -0
- package/src/Skeleton/Skeleton.stories.tsx +130 -0
- package/src/Skeleton/Skeleton.test.tsx +47 -0
- package/src/Skeleton/Skeleton.tsx +106 -0
- package/src/Spinner/Spinner.mdx +36 -0
- package/src/Spinner/Spinner.stories.tsx +118 -0
- package/src/Spinner/Spinner.test.tsx +38 -0
- package/src/Spinner/Spinner.tsx +92 -0
- package/src/Tabs/TabTrigger.tsx +126 -0
- package/src/Tabs/Tabs.mdx +126 -0
- package/src/Tabs/Tabs.stories.tsx +187 -0
- package/src/Tabs/Tabs.test.tsx +283 -0
- package/src/Tabs/Tabs.tsx +333 -0
- package/src/VisuallyHidden/VisuallyHidden.mdx +29 -0
- package/src/VisuallyHidden/VisuallyHidden.stories.tsx +58 -0
- package/src/VisuallyHidden/VisuallyHidden.test.tsx +31 -0
- package/src/VisuallyHidden/VisuallyHidden.tsx +43 -0
- package/src/argTypes.storyHelpers.ts +115 -0
- package/src/board.storyHelpers.tsx +150 -0
- package/src/controlStyles.ts +79 -0
- package/src/expectNoAxeViolations.testHelpers.ts +41 -0
- package/src/icons.storyHelpers.tsx +116 -0
- package/src/index.ts +132 -0
- package/src/intentStyles.ts +207 -0
- package/src/mdxReferences.test.ts +95 -0
- package/src/mountStory.testHelpers.ts +41 -0
- package/src/sourceRules.test.ts +303 -0
- package/src/statusIntent.test.ts +136 -0
- package/src/statusIntent.ts +137 -0
- package/src/storyControls.test.ts +128 -0
- package/src/styles.css +130 -0
- package/src/tailwindCandidates.test.ts +377 -0
- package/src/testing/expectAgentDrivable.test.tsx +71 -0
- package/src/testing/expectAgentDrivable.ts +273 -0
- package/src/testing/index.ts +17 -0
- package/src/toClassName.ts +13 -0
- package/src/tokens.ts +16 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
2
|
+
|
|
3
|
+
import { toStoryChoice } from "../argTypes.storyHelpers.ts"
|
|
4
|
+
import { Badge } from "../Badge/Badge.tsx"
|
|
5
|
+
import {
|
|
6
|
+
ContainerBoard,
|
|
7
|
+
StoryCell,
|
|
8
|
+
StoryGrid,
|
|
9
|
+
} from "../board.storyHelpers.tsx"
|
|
10
|
+
import { ProgressBar } from "../ProgressBar/ProgressBar.tsx"
|
|
11
|
+
import type { TabItem } from "./Tabs.tsx"
|
|
12
|
+
import { Tabs } from "./Tabs.tsx"
|
|
13
|
+
|
|
14
|
+
const BAY_TABS: TabItem[] = [
|
|
15
|
+
{
|
|
16
|
+
content: (
|
|
17
|
+
<ProgressBar
|
|
18
|
+
isValueShown
|
|
19
|
+
label="Ripping title 4 of 9"
|
|
20
|
+
value={47}
|
|
21
|
+
/>
|
|
22
|
+
),
|
|
23
|
+
key: "progress",
|
|
24
|
+
label: "Progress",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
content: (
|
|
28
|
+
<p className="font-mono text-content-secondary text-xs">
|
|
29
|
+
14:02:11 AACS handshake complete, reading…
|
|
30
|
+
</p>
|
|
31
|
+
),
|
|
32
|
+
key: "log",
|
|
33
|
+
label: "Log",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
content: (
|
|
37
|
+
<p className="text-content-secondary text-sm">
|
|
38
|
+
Blade Runner (1982) · 9 titles · MakeMKV 1.17.8
|
|
39
|
+
</p>
|
|
40
|
+
),
|
|
41
|
+
key: "disc",
|
|
42
|
+
label: "Disc",
|
|
43
|
+
},
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
const meta = {
|
|
47
|
+
title: "Components/Tabs",
|
|
48
|
+
component: Tabs,
|
|
49
|
+
parameters: { layout: "padded" },
|
|
50
|
+
argTypes: {
|
|
51
|
+
activation: toStoryChoice([
|
|
52
|
+
"automatic",
|
|
53
|
+
"manual",
|
|
54
|
+
] as const),
|
|
55
|
+
orientation: toStoryChoice([
|
|
56
|
+
"horizontal",
|
|
57
|
+
"vertical",
|
|
58
|
+
] as const),
|
|
59
|
+
},
|
|
60
|
+
args: {
|
|
61
|
+
activation: "automatic",
|
|
62
|
+
orientation: "horizontal",
|
|
63
|
+
},
|
|
64
|
+
} satisfies Meta<typeof Tabs>
|
|
65
|
+
|
|
66
|
+
export default meta
|
|
67
|
+
|
|
68
|
+
type Story = StoryObj<typeof meta>
|
|
69
|
+
|
|
70
|
+
export const Default: Story = {
|
|
71
|
+
args: { label: "Bay 3", tabs: BAY_TABS },
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const AllVariants: Story = {
|
|
75
|
+
args: { label: "Bay 3", tabs: BAY_TABS },
|
|
76
|
+
render: () => (
|
|
77
|
+
<StoryGrid columns={2}>
|
|
78
|
+
<StoryCell label="horizontal · automatic">
|
|
79
|
+
<Tabs label="Bay 3 horizontal" tabs={BAY_TABS} />
|
|
80
|
+
</StoryCell>
|
|
81
|
+
|
|
82
|
+
<StoryCell label="vertical · automatic">
|
|
83
|
+
<Tabs
|
|
84
|
+
label="Bay 3 vertical"
|
|
85
|
+
orientation="vertical"
|
|
86
|
+
tabs={BAY_TABS}
|
|
87
|
+
/>
|
|
88
|
+
</StoryCell>
|
|
89
|
+
|
|
90
|
+
<StoryCell label="horizontal · manual">
|
|
91
|
+
<Tabs
|
|
92
|
+
activation="manual"
|
|
93
|
+
label="Bay 3 manual"
|
|
94
|
+
tabs={BAY_TABS}
|
|
95
|
+
/>
|
|
96
|
+
</StoryCell>
|
|
97
|
+
|
|
98
|
+
<StoryCell label="starts on the second tab">
|
|
99
|
+
<Tabs
|
|
100
|
+
activeKey="log"
|
|
101
|
+
label="Bay 3 starting on log"
|
|
102
|
+
tabs={BAY_TABS}
|
|
103
|
+
/>
|
|
104
|
+
</StoryCell>
|
|
105
|
+
</StoryGrid>
|
|
106
|
+
),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const STATE_TABS: TabItem[] = [
|
|
110
|
+
...BAY_TABS,
|
|
111
|
+
{
|
|
112
|
+
content: (
|
|
113
|
+
<p className="text-content-secondary text-sm">
|
|
114
|
+
Unreachable — this bay has no verdict yet.
|
|
115
|
+
</p>
|
|
116
|
+
),
|
|
117
|
+
isDisabled: true,
|
|
118
|
+
key: "verdict",
|
|
119
|
+
label: "Verdict",
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
content: (
|
|
123
|
+
<div className="flex gap-2">
|
|
124
|
+
<Badge intent="warning" size="sm">
|
|
125
|
+
1 retry
|
|
126
|
+
</Badge>
|
|
127
|
+
|
|
128
|
+
<Badge intent="info" size="sm">
|
|
129
|
+
AACS
|
|
130
|
+
</Badge>
|
|
131
|
+
</div>
|
|
132
|
+
),
|
|
133
|
+
key: "flags",
|
|
134
|
+
label: "Flags",
|
|
135
|
+
},
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* A disabled tab is out of the *focus* group and still one of the
|
|
140
|
+
* *options* — it owns a panel and an id either way. Registration is
|
|
141
|
+
* membership, so the arrow keys skip it with nothing in
|
|
142
|
+
* `RovingFocus` knowing the word "disabled".
|
|
143
|
+
*/
|
|
144
|
+
export const AllStates: Story = {
|
|
145
|
+
args: { label: "Bay 3", tabs: STATE_TABS },
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* A tab bar **scrolls**, it does not wrap. A wrapped tablist puts
|
|
150
|
+
* tabs on two rows, which breaks both the visual row and the
|
|
151
|
+
* mental model the arrow keys give — "right" stops meaning right.
|
|
152
|
+
*
|
|
153
|
+
* `ContainerBoard` is used here as a width harness rather than as
|
|
154
|
+
* a container-query claim: `Tabs` declares no container, because
|
|
155
|
+
* nothing about it needs to *know* its width. It just has to
|
|
156
|
+
* survive being narrow.
|
|
157
|
+
*/
|
|
158
|
+
export const Responsive: Story = {
|
|
159
|
+
args: { label: "Bay 3", tabs: STATE_TABS },
|
|
160
|
+
render: () => (
|
|
161
|
+
<ContainerBoard>
|
|
162
|
+
{(width) => (
|
|
163
|
+
<Tabs
|
|
164
|
+
label={`Bay 3 at ${width}`}
|
|
165
|
+
tabs={STATE_TABS}
|
|
166
|
+
/>
|
|
167
|
+
)}
|
|
168
|
+
</ContainerBoard>
|
|
169
|
+
),
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The falsification point, and the reason `Tabs` is P0.
|
|
174
|
+
*
|
|
175
|
+
* In `manual`, an arrow key moves focus to a tab that is *not*
|
|
176
|
+
* chosen — a state a three-kind model cannot represent at all,
|
|
177
|
+
* because it had one notion of "the current one" doing both jobs.
|
|
178
|
+
* Arrow through this one and watch the underline stay put until
|
|
179
|
+
* Enter.
|
|
180
|
+
*/
|
|
181
|
+
export const Manual: Story = {
|
|
182
|
+
args: {
|
|
183
|
+
activation: "manual",
|
|
184
|
+
label: "Bay 3 manual",
|
|
185
|
+
tabs: BAY_TABS,
|
|
186
|
+
},
|
|
187
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { composeStories } from "@storybook/react"
|
|
2
|
+
import { expect, userEvent, waitFor } from "storybook/test"
|
|
3
|
+
import { test } from "vitest"
|
|
4
|
+
|
|
5
|
+
import { expectNoAxeViolations } from "../expectNoAxeViolations.testHelpers.ts"
|
|
6
|
+
import { mountStory } from "../mountStory.testHelpers.ts"
|
|
7
|
+
import { expectAgentDrivable } from "../testing/index.ts"
|
|
8
|
+
import * as stories from "./Tabs.stories.tsx"
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
AllStates,
|
|
12
|
+
AllVariants,
|
|
13
|
+
Default,
|
|
14
|
+
Manual,
|
|
15
|
+
Responsive,
|
|
16
|
+
} = composeStories(stories)
|
|
17
|
+
|
|
18
|
+
test("selecting a tab wires the panel back to it", async () => {
|
|
19
|
+
const { canvas, canvasElement } =
|
|
20
|
+
await mountStory(Default)
|
|
21
|
+
|
|
22
|
+
const tablist = expectAgentDrivable(canvas, {
|
|
23
|
+
name: "Bay 3",
|
|
24
|
+
role: "tablist",
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
await expect(tablist).toHaveAttribute(
|
|
28
|
+
"aria-orientation",
|
|
29
|
+
"horizontal",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const log = expectAgentDrivable(canvas, {
|
|
33
|
+
name: "Log",
|
|
34
|
+
role: "tab",
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
await userEvent.click(log)
|
|
38
|
+
|
|
39
|
+
await waitFor(() => {
|
|
40
|
+
expect(log).toHaveAttribute("aria-selected", "true")
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// `aria-controls` really reaches the panel, and the panel names
|
|
44
|
+
// itself back through `aria-labelledby`. Both ids come from one
|
|
45
|
+
// `useUniqueId`, which is why they cannot drift.
|
|
46
|
+
const panel = expectAgentDrivable(canvas, {
|
|
47
|
+
name: "Log",
|
|
48
|
+
role: "tabpanel",
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
await expect(log).toHaveAttribute(
|
|
52
|
+
"aria-controls",
|
|
53
|
+
panel.id,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
await expectNoAxeViolations(canvasElement)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test("activeKey decides the first render and nothing after", async () => {
|
|
60
|
+
const { canvas } = await mountStory(AllVariants)
|
|
61
|
+
|
|
62
|
+
// A controlled prop is the thing this library refuses to have.
|
|
63
|
+
const tablist = expectAgentDrivable(canvas, {
|
|
64
|
+
name: "Bay 3 starting on log",
|
|
65
|
+
role: "tablist",
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const selected = tablist.querySelector(
|
|
69
|
+
'[aria-selected="true"]',
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
await expect(selected).toHaveTextContent("Log")
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test("a vertical tablist says so", async () => {
|
|
76
|
+
const { canvas } = await mountStory(AllVariants)
|
|
77
|
+
|
|
78
|
+
await expect(
|
|
79
|
+
expectAgentDrivable(canvas, {
|
|
80
|
+
name: "Bay 3 vertical",
|
|
81
|
+
role: "tablist",
|
|
82
|
+
}),
|
|
83
|
+
).toHaveAttribute("aria-orientation", "vertical")
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test("the arrow keys skip a disabled tab", async () => {
|
|
87
|
+
const { canvas } = await mountStory(AllStates)
|
|
88
|
+
|
|
89
|
+
const verdict = expectAgentDrivable(canvas, {
|
|
90
|
+
name: "Verdict",
|
|
91
|
+
role: "tab",
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
await expect(verdict).toBeDisabled()
|
|
95
|
+
|
|
96
|
+
// The disabled tab is out of the *focus* group and still one of
|
|
97
|
+
// the *options* — it owns a panel and an id either way.
|
|
98
|
+
// Registration is membership, so the arrow keys skip it with
|
|
99
|
+
// nothing in `RovingFocus` knowing the word "disabled".
|
|
100
|
+
const flags = expectAgentDrivable(canvas, {
|
|
101
|
+
name: "Flags",
|
|
102
|
+
role: "tab",
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
await userEvent.click(
|
|
106
|
+
expectAgentDrivable(canvas, {
|
|
107
|
+
name: "Disc",
|
|
108
|
+
role: "tab",
|
|
109
|
+
}),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
await userEvent.keyboard("{ArrowRight}")
|
|
113
|
+
|
|
114
|
+
await waitFor(() => {
|
|
115
|
+
expect(flags).toHaveFocus()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
await expect(verdict).not.toHaveFocus()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A tab bar **scrolls**, it does not wrap. A wrapped tablist puts
|
|
123
|
+
* tabs on two rows, which breaks both the visual row and the mental
|
|
124
|
+
* model the arrow keys give — "right" stops meaning right.
|
|
125
|
+
*/
|
|
126
|
+
test("a narrow tab bar scrolls rather than wrapping", async () => {
|
|
127
|
+
const { canvas } = await mountStory(Responsive)
|
|
128
|
+
|
|
129
|
+
const narrow = expectAgentDrivable(canvas, {
|
|
130
|
+
name: "Bay 3 at 15rem",
|
|
131
|
+
role: "tablist",
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
await expect(narrow.scrollWidth).toBeGreaterThan(
|
|
135
|
+
narrow.clientWidth,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
// **And it clips what it cannot fit.** The line above says only
|
|
139
|
+
// that the content is wider than the box, which is equally true of
|
|
140
|
+
// a bar painting straight across the panel beside it — which is
|
|
141
|
+
// what this component actually did until 2026-07-30, in this exact
|
|
142
|
+
// story, with this exact assertion passing. Overflowing and
|
|
143
|
+
// scrolling are different things and the test has to say which.
|
|
144
|
+
await expect(
|
|
145
|
+
globalThis.getComputedStyle(narrow).overflowX,
|
|
146
|
+
).not.toBe("visible")
|
|
147
|
+
|
|
148
|
+
// One row, still. `clientHeight` growing past a single tab's
|
|
149
|
+
// height is what wrapping would look like — and, now that this is
|
|
150
|
+
// a scroll container, what a *classic* scrollbar taking layout
|
|
151
|
+
// space would look like too. Both are caught here.
|
|
152
|
+
const wide = expectAgentDrivable(canvas, {
|
|
153
|
+
name: "Bay 3 at 34rem",
|
|
154
|
+
role: "tablist",
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
await expect(narrow.clientHeight).toBe(wide.clientHeight)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test("the focus ring survives the scroll container", async () => {
|
|
161
|
+
const { canvas } = await mountStory(Responsive)
|
|
162
|
+
|
|
163
|
+
const narrow = expectAgentDrivable(canvas, {
|
|
164
|
+
name: "Bay 3 at 15rem",
|
|
165
|
+
role: "tablist",
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// Tabbed to, not `.focus()`ed: the ring is a `:focus-visible`
|
|
169
|
+
// rule, so a programmatic focus leaves `outline-offset` at its
|
|
170
|
+
// `0px` initial value and *any* assertion about it passes. That is
|
|
171
|
+
// how the first version of this test passed against the bug it was
|
|
172
|
+
// written for.
|
|
173
|
+
await userEvent.tab()
|
|
174
|
+
|
|
175
|
+
const tab = document.activeElement as HTMLElement
|
|
176
|
+
|
|
177
|
+
await expect(narrow).toContainElement(tab)
|
|
178
|
+
|
|
179
|
+
// An `overflow` other than `visible` clips its descendants'
|
|
180
|
+
// outlines, and the focus ring *is* an outline. The tab fills the
|
|
181
|
+
// bar's content box, so a positive offset would put the whole ring
|
|
182
|
+
// in the clipped region — visible nowhere, on the one component in
|
|
183
|
+
// the library that scrolls. Drawing it inward is what keeps it on
|
|
184
|
+
// screen, and this is the assertion that fails if the shared
|
|
185
|
+
// `--focus-ring-offset` is ever restored here.
|
|
186
|
+
const { outlineOffset, outlineWidth } =
|
|
187
|
+
globalThis.getComputedStyle(tab)
|
|
188
|
+
|
|
189
|
+
await expect(
|
|
190
|
+
Number.parseFloat(outlineWidth),
|
|
191
|
+
).toBeGreaterThan(0)
|
|
192
|
+
|
|
193
|
+
await expect(
|
|
194
|
+
Number.parseFloat(outlineOffset),
|
|
195
|
+
).toBeLessThanOrEqual(0)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The falsification point that M4 was built to run: `automatic` and
|
|
200
|
+
* `manual` differ only in whether moving focus also *chooses*, and
|
|
201
|
+
* that difference is one line because focus and selection are
|
|
202
|
+
* separate kinds.
|
|
203
|
+
*/
|
|
204
|
+
test("manual activation moves focus without selecting", async () => {
|
|
205
|
+
const { canvas } = await mountStory(Manual)
|
|
206
|
+
|
|
207
|
+
const tablist = expectAgentDrivable(canvas, {
|
|
208
|
+
name: "Bay 3 manual",
|
|
209
|
+
role: "tablist",
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
const tabs = Array.from(
|
|
213
|
+
tablist.querySelectorAll<HTMLButtonElement>(
|
|
214
|
+
'[role="tab"]',
|
|
215
|
+
),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
const [progress, log] = tabs as [
|
|
219
|
+
HTMLButtonElement,
|
|
220
|
+
HTMLButtonElement,
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
// Exactly one tab stop for the whole bar — the roving-tabindex
|
|
224
|
+
// rule, straight from `selectTabIndex`.
|
|
225
|
+
await expect(
|
|
226
|
+
tabs.filter((one) => one.tabIndex === 0),
|
|
227
|
+
).toHaveLength(1)
|
|
228
|
+
|
|
229
|
+
progress.focus()
|
|
230
|
+
|
|
231
|
+
await userEvent.keyboard("{ArrowRight}")
|
|
232
|
+
|
|
233
|
+
await waitFor(() => {
|
|
234
|
+
expect(log).toHaveFocus()
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
// The whole point: focus moved, selection did not.
|
|
238
|
+
await expect(log).toHaveAttribute(
|
|
239
|
+
"aria-selected",
|
|
240
|
+
"false",
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
await expect(progress).toHaveAttribute(
|
|
244
|
+
"aria-selected",
|
|
245
|
+
"true",
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
await userEvent.keyboard("{Enter}")
|
|
249
|
+
|
|
250
|
+
await waitFor(() => {
|
|
251
|
+
expect(log).toHaveAttribute("aria-selected", "true")
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
test("automatic activation selects as focus moves, and wraps", async () => {
|
|
256
|
+
const { canvas } = await mountStory(Default)
|
|
257
|
+
|
|
258
|
+
const tablist = expectAgentDrivable(canvas, {
|
|
259
|
+
name: "Bay 3",
|
|
260
|
+
role: "tablist",
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
const tabs =
|
|
264
|
+
tablist.querySelectorAll<HTMLButtonElement>(
|
|
265
|
+
'[role="tab"]',
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
tabs[0]?.focus()
|
|
269
|
+
|
|
270
|
+
await userEvent.keyboard("{ArrowRight}")
|
|
271
|
+
|
|
272
|
+
await waitFor(() => {
|
|
273
|
+
expect(tabs[1]).toHaveAttribute("aria-selected", "true")
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
// Wrapping is on, because a tab list is one of the patterns ARIA
|
|
277
|
+
// says wraps.
|
|
278
|
+
await userEvent.keyboard("{ArrowRight}{ArrowRight}")
|
|
279
|
+
|
|
280
|
+
await waitFor(() => {
|
|
281
|
+
expect(tabs[0]).toHaveAttribute("aria-selected", "true")
|
|
282
|
+
})
|
|
283
|
+
})
|