@charcuterie/ui 3.0.0 → 3.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 +10 -0
- package/dist/Badge/Badge.d.ts +33 -3
- package/dist/Badge/Badge.d.ts.map +1 -1
- package/dist/Badge/Badge.js +20 -2
- package/dist/Badge/Badge.js.map +1 -1
- package/dist/Board/Board.d.ts +119 -0
- package/dist/Board/Board.d.ts.map +1 -0
- package/dist/Board/Board.js +165 -0
- package/dist/Board/Board.js.map +1 -0
- package/dist/Board/BoardCard.d.ts +120 -0
- package/dist/Board/BoardCard.d.ts.map +1 -0
- package/dist/Board/BoardCard.js +77 -0
- package/dist/Board/BoardCard.js.map +1 -0
- package/dist/Board/BoardLaneList.d.ts +103 -0
- package/dist/Board/BoardLaneList.d.ts.map +1 -0
- package/dist/Board/BoardLaneList.js +69 -0
- package/dist/Board/BoardLaneList.js.map +1 -0
- package/dist/Board/boardMove.d.ts +116 -0
- package/dist/Board/boardMove.d.ts.map +1 -0
- package/dist/Board/boardMove.js +116 -0
- package/dist/Board/boardMove.js.map +1 -0
- package/dist/Board/useBoardDrag.d.ts +44 -0
- package/dist/Board/useBoardDrag.d.ts.map +1 -0
- package/dist/Board/useBoardDrag.js +237 -0
- package/dist/Board/useBoardDrag.js.map +1 -0
- package/dist/Checkbox/Checkbox.d.ts.map +1 -1
- package/dist/Checkbox/Checkbox.js +15 -4
- package/dist/Checkbox/Checkbox.js.map +1 -1
- package/dist/DataTable/DataTable.d.ts +159 -0
- package/dist/DataTable/DataTable.d.ts.map +1 -0
- package/dist/DataTable/DataTable.js +197 -0
- package/dist/DataTable/DataTable.js.map +1 -0
- package/dist/DatePicker/DateGrid.d.ts +79 -0
- package/dist/DatePicker/DateGrid.d.ts.map +1 -0
- package/dist/DatePicker/DateGrid.js +220 -0
- package/dist/DatePicker/DateGrid.js.map +1 -0
- package/dist/DatePicker/DatePicker.d.ts +138 -0
- package/dist/DatePicker/DatePicker.d.ts.map +1 -0
- package/dist/DatePicker/DatePicker.js +384 -0
- package/dist/DatePicker/DatePicker.js.map +1 -0
- package/dist/DatePicker/datePresets.d.ts +29 -0
- package/dist/DatePicker/datePresets.d.ts.map +1 -0
- package/dist/DatePicker/datePresets.js +28 -0
- package/dist/DatePicker/datePresets.js.map +1 -0
- package/dist/DatePicker/parseDateInput.d.ts +106 -0
- package/dist/DatePicker/parseDateInput.d.ts.map +1 -0
- package/dist/DatePicker/parseDateInput.js +419 -0
- package/dist/DatePicker/parseDateInput.js.map +1 -0
- package/dist/DatePicker/plainDate.d.ts +167 -0
- package/dist/DatePicker/plainDate.d.ts.map +1 -0
- package/dist/DatePicker/plainDate.js +272 -0
- package/dist/DatePicker/plainDate.js.map +1 -0
- package/dist/MarkdownEditor/MarkdownEditor.d.ts +147 -0
- package/dist/MarkdownEditor/MarkdownEditor.d.ts.map +1 -0
- package/dist/MarkdownEditor/MarkdownEditor.js +492 -0
- package/dist/MarkdownEditor/MarkdownEditor.js.map +1 -0
- package/dist/MarkdownEditor/markdownCommands.d.ts +202 -0
- package/dist/MarkdownEditor/markdownCommands.d.ts.map +1 -0
- package/dist/MarkdownEditor/markdownCommands.js +379 -0
- package/dist/MarkdownEditor/markdownCommands.js.map +1 -0
- package/dist/MarkdownEditor/markdownSpans.d.ts +92 -0
- package/dist/MarkdownEditor/markdownSpans.d.ts.map +1 -0
- package/dist/MarkdownEditor/markdownSpans.js +303 -0
- package/dist/MarkdownEditor/markdownSpans.js.map +1 -0
- package/dist/categoricalStyles.d.ts +53 -0
- package/dist/categoricalStyles.d.ts.map +1 -0
- package/dist/categoricalStyles.js +195 -0
- package/dist/categoricalStyles.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/Badge/Badge.mdx +46 -0
- package/src/Badge/Badge.stories.tsx +98 -4
- package/src/Badge/Badge.test.tsx +157 -2
- package/src/Badge/Badge.tsx +49 -3
- package/src/Board/Board.mdx +199 -0
- package/src/Board/Board.stories.tsx +707 -0
- package/src/Board/Board.test.tsx +310 -0
- package/src/Board/Board.tsx +333 -0
- package/src/Board/BoardCard.tsx +290 -0
- package/src/Board/BoardLaneList.tsx +275 -0
- package/src/Board/boardMove.test.ts +213 -0
- package/src/Board/boardMove.ts +225 -0
- package/src/Board/useBoardDrag.ts +413 -0
- package/src/Checkbox/Checkbox.tsx +34 -3
- package/src/DataTable/DataTable.mdx +123 -0
- package/src/DataTable/DataTable.stories.tsx +411 -0
- package/src/DataTable/DataTable.test.tsx +386 -0
- package/src/DataTable/DataTable.tsx +497 -0
- package/src/DatePicker/DateGrid.tsx +467 -0
- package/src/DatePicker/DatePicker.mdx +170 -0
- package/src/DatePicker/DatePicker.stories.tsx +268 -0
- package/src/DatePicker/DatePicker.test.tsx +455 -0
- package/src/DatePicker/DatePicker.tsx +852 -0
- package/src/DatePicker/datePresets.ts +34 -0
- package/src/DatePicker/parseDateInput.test.ts +187 -0
- package/src/DatePicker/parseDateInput.ts +629 -0
- package/src/DatePicker/plainDate.test.ts +375 -0
- package/src/DatePicker/plainDate.ts +465 -0
- package/src/MarkdownEditor/MarkdownEditor.mdx +189 -0
- package/src/MarkdownEditor/MarkdownEditor.stories.tsx +267 -0
- package/src/MarkdownEditor/MarkdownEditor.test.tsx +627 -0
- package/src/MarkdownEditor/MarkdownEditor.tsx +862 -0
- package/src/MarkdownEditor/markdownCommands.test.ts +384 -0
- package/src/MarkdownEditor/markdownCommands.ts +566 -0
- package/src/MarkdownEditor/markdownSpans.test.ts +217 -0
- package/src/MarkdownEditor/markdownSpans.ts +458 -0
- package/src/argTypes.storyHelpers.ts +39 -0
- package/src/categoricalStyles.ts +244 -0
- package/src/index.ts +89 -0
- package/src/sourceRules.test.ts +52 -1
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
2
|
+
import type { ReactNode } from "react"
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
|
|
5
|
+
import { Badge } from "../Badge/Badge.tsx"
|
|
6
|
+
import {
|
|
7
|
+
ContainerBoard,
|
|
8
|
+
StoryCell,
|
|
9
|
+
StoryGrid,
|
|
10
|
+
StorySection,
|
|
11
|
+
} from "../board.storyHelpers.tsx"
|
|
12
|
+
import { EmptyState } from "../EmptyState/EmptyState.tsx"
|
|
13
|
+
import { TextLink } from "../TextLink/TextLink.tsx"
|
|
14
|
+
import type {
|
|
15
|
+
DataTableColumn,
|
|
16
|
+
DataTableSort,
|
|
17
|
+
} from "./DataTable.tsx"
|
|
18
|
+
import { DataTable } from "./DataTable.tsx"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Invented rows, and deliberately so: a screenshot is opaque to
|
|
22
|
+
* every grep, so real data in a fixture is real data nobody can
|
|
23
|
+
* find again later.
|
|
24
|
+
*/
|
|
25
|
+
type Task = {
|
|
26
|
+
assignee: string
|
|
27
|
+
id: string
|
|
28
|
+
priority: "high" | "low" | "medium"
|
|
29
|
+
status: "blocked" | "done" | "todo"
|
|
30
|
+
title: string
|
|
31
|
+
updatedAt: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const TASKS: readonly Task[] = [
|
|
35
|
+
{
|
|
36
|
+
assignee: "Wren",
|
|
37
|
+
id: "task-1",
|
|
38
|
+
priority: "high",
|
|
39
|
+
status: "todo",
|
|
40
|
+
title: "Re-point the garden wall",
|
|
41
|
+
updatedAt: "2026-08-18",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
assignee: "Kestrel",
|
|
45
|
+
id: "task-2",
|
|
46
|
+
priority: "medium",
|
|
47
|
+
status: "blocked",
|
|
48
|
+
title: "Order the replacement extractor fan filter",
|
|
49
|
+
updatedAt: "2026-08-14",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
assignee: "Wren",
|
|
53
|
+
id: "task-3",
|
|
54
|
+
priority: "low",
|
|
55
|
+
status: "done",
|
|
56
|
+
title: "Label the spare keys",
|
|
57
|
+
updatedAt: "2026-08-11",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
assignee: "Marlowe",
|
|
61
|
+
id: "task-4",
|
|
62
|
+
priority: "high",
|
|
63
|
+
status: "todo",
|
|
64
|
+
title:
|
|
65
|
+
"Rebuild the seed-tray shelving now the propagator has moved",
|
|
66
|
+
updatedAt: "2026-08-19",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
assignee: "Kestrel",
|
|
70
|
+
id: "task-5",
|
|
71
|
+
priority: "medium",
|
|
72
|
+
status: "todo",
|
|
73
|
+
title: "Book the chimney sweep",
|
|
74
|
+
updatedAt: "2026-08-09",
|
|
75
|
+
},
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
const STATUS_INTENT = {
|
|
79
|
+
blocked: "danger",
|
|
80
|
+
done: "success",
|
|
81
|
+
todo: "neutral",
|
|
82
|
+
} as const
|
|
83
|
+
|
|
84
|
+
const PRIORITY_ORDER = {
|
|
85
|
+
high: 0,
|
|
86
|
+
low: 2,
|
|
87
|
+
medium: 1,
|
|
88
|
+
} as const
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The columns, as a consumer writes them. `renderCell` returns a
|
|
92
|
+
* node, so the title cell is a real `TextLink` and the status cell
|
|
93
|
+
* is a `Badge` — the table composes what the package already has
|
|
94
|
+
* rather than growing a `type: "badge"` column kind that could only
|
|
95
|
+
* ever render one of them.
|
|
96
|
+
*/
|
|
97
|
+
/**
|
|
98
|
+
* The widths are themselves container queries — `cq-lg:w-32`, not
|
|
99
|
+
* `w-32`. A fixed width is a promise about space the container may
|
|
100
|
+
* not have: four columns pinned at `w-32` inside a 34rem table leave
|
|
101
|
+
* the title 48px, and the title is the column anyone reads.
|
|
102
|
+
*/
|
|
103
|
+
const COLUMNS: readonly DataTableColumn<Task>[] = [
|
|
104
|
+
{
|
|
105
|
+
header: "Task",
|
|
106
|
+
isSortable: true,
|
|
107
|
+
key: "title",
|
|
108
|
+
renderCell: (task) => (
|
|
109
|
+
<TextLink href={`#${task.id}`}>{task.title}</TextLink>
|
|
110
|
+
),
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
className: "cq-lg:w-32",
|
|
114
|
+
header: "Status",
|
|
115
|
+
isSortable: true,
|
|
116
|
+
key: "status",
|
|
117
|
+
renderCell: (task) => (
|
|
118
|
+
<Badge
|
|
119
|
+
appearance="soft"
|
|
120
|
+
intent={STATUS_INTENT[task.status]}
|
|
121
|
+
>
|
|
122
|
+
{task.status}
|
|
123
|
+
</Badge>
|
|
124
|
+
),
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
className: "cq-lg:w-28",
|
|
128
|
+
header: "Priority",
|
|
129
|
+
isSortable: true,
|
|
130
|
+
key: "priority",
|
|
131
|
+
renderCell: (task) => task.priority,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
className: "cq-lg:w-32",
|
|
135
|
+
header: "Assignee",
|
|
136
|
+
key: "assignee",
|
|
137
|
+
renderCell: (task) => task.assignee,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
className: "cq-lg:w-32",
|
|
141
|
+
header: "Updated",
|
|
142
|
+
isSortable: true,
|
|
143
|
+
key: "updatedAt",
|
|
144
|
+
renderCell: (task) => task.updatedAt,
|
|
145
|
+
},
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Comparing two values is the consumer's, and this is what that
|
|
150
|
+
* looks like: three of these five columns do not sort
|
|
151
|
+
* alphabetically. `priority` is an enum whose order is
|
|
152
|
+
* high → medium → low and whose alphabetical order is
|
|
153
|
+
* high → low → medium, which is the case a table that sorted itself
|
|
154
|
+
* would get quietly wrong.
|
|
155
|
+
*/
|
|
156
|
+
const getSortedTasks = (
|
|
157
|
+
tasks: readonly Task[],
|
|
158
|
+
sort: DataTableSort,
|
|
159
|
+
): Task[] => {
|
|
160
|
+
const direction = sort.direction === "ascending" ? 1 : -1
|
|
161
|
+
|
|
162
|
+
return [...tasks].sort((one, two) => {
|
|
163
|
+
if (sort.columnKey === "priority") {
|
|
164
|
+
return (
|
|
165
|
+
(PRIORITY_ORDER[one.priority] -
|
|
166
|
+
PRIORITY_ORDER[two.priority]) *
|
|
167
|
+
direction
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const key =
|
|
172
|
+
sort.columnKey === "title"
|
|
173
|
+
? "title"
|
|
174
|
+
: sort.columnKey === "status"
|
|
175
|
+
? "status"
|
|
176
|
+
: sort.columnKey === "assignee"
|
|
177
|
+
? "assignee"
|
|
178
|
+
: "updatedAt"
|
|
179
|
+
|
|
180
|
+
return one[key].localeCompare(two[key]) * direction
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The state a consumer owns, in the smallest form that is honest:
|
|
186
|
+
* a sort, a selected set, and the rows re-ordered from both. Every
|
|
187
|
+
* story that needs a working table renders this.
|
|
188
|
+
*/
|
|
189
|
+
const TaskTable = ({
|
|
190
|
+
isSelectable = false,
|
|
191
|
+
initialSort,
|
|
192
|
+
label = "Tasks",
|
|
193
|
+
tasks = TASKS,
|
|
194
|
+
}: {
|
|
195
|
+
initialSort?: DataTableSort
|
|
196
|
+
isSelectable?: boolean
|
|
197
|
+
label?: string
|
|
198
|
+
tasks?: readonly Task[]
|
|
199
|
+
}): ReactNode => {
|
|
200
|
+
const [sort, setSort] = useState<
|
|
201
|
+
DataTableSort | undefined
|
|
202
|
+
>(initialSort)
|
|
203
|
+
|
|
204
|
+
const [selectedRowKeys, setSelectedRowKeys] = useState<
|
|
205
|
+
readonly string[]
|
|
206
|
+
>([])
|
|
207
|
+
|
|
208
|
+
const sortedTasks = sort
|
|
209
|
+
? getSortedTasks(tasks, sort)
|
|
210
|
+
: tasks
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<DataTable
|
|
214
|
+
columns={COLUMNS}
|
|
215
|
+
emptyState={
|
|
216
|
+
<EmptyState
|
|
217
|
+
description="Nothing matches this filter. The columns and the sort are still here."
|
|
218
|
+
heading="No tasks"
|
|
219
|
+
/>
|
|
220
|
+
}
|
|
221
|
+
getRowKey={(task) => task.id}
|
|
222
|
+
label={label}
|
|
223
|
+
onSortChange={setSort}
|
|
224
|
+
rows={sortedTasks}
|
|
225
|
+
selection={
|
|
226
|
+
isSelectable
|
|
227
|
+
? {
|
|
228
|
+
getRowLabel: (task) => task.title,
|
|
229
|
+
onSelectionChange: setSelectedRowKeys,
|
|
230
|
+
// Not "Select all tasks": an agent's
|
|
231
|
+
// `getByRole("columnheader", { name: "Task" })` is a
|
|
232
|
+
// **substring, case-insensitive** match in Playwright,
|
|
233
|
+
// so a header box called "Select all tasks" makes the
|
|
234
|
+
// "Task" column ambiguous to the query the component
|
|
235
|
+
// exists to be drivable by.
|
|
236
|
+
selectAllLabel: "Select every row",
|
|
237
|
+
selectedRowKeys,
|
|
238
|
+
}
|
|
239
|
+
: undefined
|
|
240
|
+
}
|
|
241
|
+
sort={sort}
|
|
242
|
+
/>
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const meta = {
|
|
247
|
+
title: "Components/DataTable",
|
|
248
|
+
component: DataTable,
|
|
249
|
+
parameters: { layout: "padded" },
|
|
250
|
+
args: {
|
|
251
|
+
columns: COLUMNS,
|
|
252
|
+
getRowKey: (task: Task) => task.id,
|
|
253
|
+
isLabelVisible: false,
|
|
254
|
+
label: "Tasks",
|
|
255
|
+
rows: TASKS,
|
|
256
|
+
},
|
|
257
|
+
} satisfies Meta<typeof DataTable<Task>>
|
|
258
|
+
|
|
259
|
+
export default meta
|
|
260
|
+
|
|
261
|
+
type Story = StoryObj<typeof meta>
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Five rows, five columns, four of them sortable. Click a header and
|
|
265
|
+
* the rows re-order — the story sorts them, which is the whole point
|
|
266
|
+
* of the split: the component says *which* column is sorted, the
|
|
267
|
+
* consumer says what that means for a priority enum.
|
|
268
|
+
*/
|
|
269
|
+
export const Default: Story = {
|
|
270
|
+
render: () => (
|
|
271
|
+
<TaskTable
|
|
272
|
+
initialSort={{
|
|
273
|
+
columnKey: "updatedAt",
|
|
274
|
+
direction: "descending",
|
|
275
|
+
}}
|
|
276
|
+
/>
|
|
277
|
+
),
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* The three shapes a caller assembles out of the same props: plain,
|
|
282
|
+
* selectable, and empty. There is no `variant` prop — a table with
|
|
283
|
+
* tick boxes differs from one without by having a `selection`, which
|
|
284
|
+
* is a capability rather than a look.
|
|
285
|
+
*/
|
|
286
|
+
export const AllVariants: Story = {
|
|
287
|
+
render: () => (
|
|
288
|
+
<StorySection title="Same columns, three assemblies.">
|
|
289
|
+
<StoryGrid columns={1}>
|
|
290
|
+
<StoryCell align="stretch" label="plain">
|
|
291
|
+
<TaskTable />
|
|
292
|
+
</StoryCell>
|
|
293
|
+
|
|
294
|
+
<StoryCell
|
|
295
|
+
align="stretch"
|
|
296
|
+
label="selectable — a leading checkbox column"
|
|
297
|
+
>
|
|
298
|
+
<TaskTable isSelectable />
|
|
299
|
+
</StoryCell>
|
|
300
|
+
|
|
301
|
+
<StoryCell
|
|
302
|
+
align="stretch"
|
|
303
|
+
label="empty — the header and the sort survive"
|
|
304
|
+
>
|
|
305
|
+
<TaskTable tasks={[]} />
|
|
306
|
+
</StoryCell>
|
|
307
|
+
</StoryGrid>
|
|
308
|
+
</StorySection>
|
|
309
|
+
),
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Sort has three states per column and exactly one column ever holds
|
|
314
|
+
* a direction. The unsorted columns are not blank in the
|
|
315
|
+
* accessibility tree — they say `aria-sort="none"`, which is
|
|
316
|
+
* "sortable, not currently sorted"; an absent `aria-sort` would mean
|
|
317
|
+
* "cannot be sorted at all".
|
|
318
|
+
*/
|
|
319
|
+
export const AllStates: Story = {
|
|
320
|
+
render: () => (
|
|
321
|
+
<StorySection title="Sorted ascending, sorted descending, and a selected row.">
|
|
322
|
+
<StoryGrid columns={1}>
|
|
323
|
+
<StoryCell
|
|
324
|
+
align="stretch"
|
|
325
|
+
label='Task, aria-sort="ascending"'
|
|
326
|
+
>
|
|
327
|
+
<TaskTable
|
|
328
|
+
initialSort={{
|
|
329
|
+
columnKey: "title",
|
|
330
|
+
direction: "ascending",
|
|
331
|
+
}}
|
|
332
|
+
/>
|
|
333
|
+
</StoryCell>
|
|
334
|
+
|
|
335
|
+
<StoryCell
|
|
336
|
+
align="stretch"
|
|
337
|
+
label='Updated, aria-sort="descending"'
|
|
338
|
+
>
|
|
339
|
+
<TaskTable
|
|
340
|
+
initialSort={{
|
|
341
|
+
columnKey: "updatedAt",
|
|
342
|
+
direction: "descending",
|
|
343
|
+
}}
|
|
344
|
+
/>
|
|
345
|
+
</StoryCell>
|
|
346
|
+
|
|
347
|
+
<StoryCell
|
|
348
|
+
align="stretch"
|
|
349
|
+
label="selection — tick a row, or the header box for all of them"
|
|
350
|
+
>
|
|
351
|
+
<TaskTable isSelectable />
|
|
352
|
+
</StoryCell>
|
|
353
|
+
</StoryGrid>
|
|
354
|
+
</StorySection>
|
|
355
|
+
),
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* The **only honest way to story this component.** Three container
|
|
360
|
+
* widths inside one window: the first two are below `--cq-md`
|
|
361
|
+
* (32rem) and each row is a labelled block with the header row
|
|
362
|
+
* wrapped into a strip of sort controls; the third is above it and
|
|
363
|
+
* is a table.
|
|
364
|
+
*
|
|
365
|
+
* Resizing the browser proves nothing here — what changed is the
|
|
366
|
+
* element's width, and a window can be 4K while a board lane is
|
|
367
|
+
* 300px.
|
|
368
|
+
*/
|
|
369
|
+
export const Responsive: Story = {
|
|
370
|
+
render: () => (
|
|
371
|
+
<ContainerBoard>
|
|
372
|
+
{(width) => (
|
|
373
|
+
<TaskTable
|
|
374
|
+
initialSort={{
|
|
375
|
+
columnKey: "title",
|
|
376
|
+
direction: "ascending",
|
|
377
|
+
}}
|
|
378
|
+
// Three tables in one story would otherwise be three
|
|
379
|
+
// things called "Tasks", and `getByRole("table", { name:
|
|
380
|
+
// "Tasks" })` — the query an agent writes — would match
|
|
381
|
+
// all three. `ContainerBoard`'s function form exists for
|
|
382
|
+
// exactly this.
|
|
383
|
+
label={`Tasks at ${width}`}
|
|
384
|
+
/>
|
|
385
|
+
)}
|
|
386
|
+
</ContainerBoard>
|
|
387
|
+
),
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* The complete keyboard path, and there is no custom key handling in
|
|
392
|
+
* it: `Tab` reaches each sortable header's `<button>` and each row's
|
|
393
|
+
* checkbox in document order, `Enter`/`Space` sorts, `Space` ticks.
|
|
394
|
+
*
|
|
395
|
+
* That is the deliberate half. A table is not a `grid` — it has no
|
|
396
|
+
* roving focus, no arrow-key cell navigation, and no focus trap —
|
|
397
|
+
* so a screen reader's own table-reading mode is what moves through
|
|
398
|
+
* the data, and everything interactive is a real control the browser
|
|
399
|
+
* already knows how to reach.
|
|
400
|
+
*/
|
|
401
|
+
export const Interactive: Story = {
|
|
402
|
+
render: () => (
|
|
403
|
+
<TaskTable
|
|
404
|
+
initialSort={{
|
|
405
|
+
columnKey: "title",
|
|
406
|
+
direction: "ascending",
|
|
407
|
+
}}
|
|
408
|
+
isSelectable
|
|
409
|
+
/>
|
|
410
|
+
),
|
|
411
|
+
}
|