@charcuterie/ui 3.30.0 → 3.32.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 +14 -0
- package/dist/ActionTiles/ActionTiles.d.ts +115 -0
- package/dist/ActionTiles/ActionTiles.d.ts.map +1 -0
- package/dist/ActionTiles/ActionTiles.js +115 -0
- package/dist/ActionTiles/ActionTiles.js.map +1 -0
- package/dist/RadioGroup/RadioGroup.d.ts +10 -0
- package/dist/RadioGroup/RadioGroup.d.ts.map +1 -1
- package/dist/RadioGroup/RadioGroup.js +12 -39
- package/dist/RadioGroup/RadioGroup.js.map +1 -1
- package/dist/RadioGroup/RadioGroupOption.d.ts.map +1 -1
- package/dist/RadioGroup/RadioGroupOption.js +3 -33
- package/dist/RadioGroup/RadioGroupOption.js.map +1 -1
- package/dist/Tabs/PanelTabs.d.ts +56 -0
- package/dist/Tabs/PanelTabs.d.ts.map +1 -0
- package/dist/Tabs/PanelTabs.js +163 -0
- package/dist/Tabs/PanelTabs.js.map +1 -0
- package/dist/Tabs/RoutedTabs.d.ts +19 -0
- package/dist/Tabs/RoutedTabs.d.ts.map +1 -0
- package/dist/Tabs/RoutedTabs.js +28 -0
- package/dist/Tabs/RoutedTabs.js.map +1 -0
- package/dist/Tabs/TabLink.d.ts +34 -0
- package/dist/Tabs/TabLink.d.ts.map +1 -0
- package/dist/Tabs/TabLink.js +66 -0
- package/dist/Tabs/TabLink.js.map +1 -0
- package/dist/Tabs/TabTrigger.d.ts +1 -1
- package/dist/Tabs/TabTrigger.d.ts.map +1 -1
- package/dist/Tabs/TabTrigger.js +6 -30
- package/dist/Tabs/TabTrigger.js.map +1 -1
- package/dist/Tabs/Tabs.d.ts +80 -64
- package/dist/Tabs/Tabs.d.ts.map +1 -1
- package/dist/Tabs/Tabs.js +53 -182
- package/dist/Tabs/Tabs.js.map +1 -1
- package/dist/Tabs/tabItems.d.ts +36 -0
- package/dist/Tabs/tabItems.d.ts.map +1 -0
- package/dist/Tabs/tabItems.js +2 -0
- package/dist/Tabs/tabItems.js.map +1 -0
- package/dist/Tabs/tabStyles.d.ts +41 -0
- package/dist/Tabs/tabStyles.d.ts.map +1 -0
- package/dist/Tabs/tabStyles.js +79 -0
- package/dist/Tabs/tabStyles.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/tileStyles.d.ts +83 -0
- package/dist/tileStyles.d.ts.map +1 -0
- package/dist/tileStyles.js +98 -0
- package/dist/tileStyles.js.map +1 -0
- package/package.json +1 -1
- package/src/ActionTiles/ActionTiles.mdx +131 -0
- package/src/ActionTiles/ActionTiles.stories.tsx +311 -0
- package/src/ActionTiles/ActionTiles.test.tsx +292 -0
- package/src/ActionTiles/ActionTiles.tsx +292 -0
- package/src/RadioGroup/RadioGroup.mdx +7 -0
- package/src/RadioGroup/RadioGroup.tsx +16 -42
- package/src/RadioGroup/RadioGroupOption.tsx +8 -38
- package/src/Tabs/PanelTabs.tsx +260 -0
- package/src/Tabs/RoutedTabs.tsx +58 -0
- package/src/Tabs/TabLink.tsx +132 -0
- package/src/Tabs/TabTrigger.tsx +10 -39
- package/src/Tabs/Tabs.mdx +70 -0
- package/src/Tabs/Tabs.stories.tsx +133 -1
- package/src/Tabs/Tabs.test.tsx +169 -0
- package/src/Tabs/Tabs.tsx +94 -296
- package/src/Tabs/tabItems.ts +40 -0
- package/src/Tabs/tabStyles.ts +107 -0
- package/src/index.ts +13 -3
- package/src/sourceRules.test.ts +9 -1
- package/src/tileStyles.ts +118 -0
|
@@ -8,8 +8,8 @@ import {
|
|
|
8
8
|
StoryGrid,
|
|
9
9
|
} from "../board.storyHelpers.tsx"
|
|
10
10
|
import { ProgressBar } from "../ProgressBar/ProgressBar.tsx"
|
|
11
|
-
import type { TabItem } from "./Tabs.tsx"
|
|
12
11
|
import { Tabs } from "./Tabs.tsx"
|
|
12
|
+
import type { TabItem, TabLinkItem } from "./tabItems.ts"
|
|
13
13
|
|
|
14
14
|
const BAY_TABS: TabItem[] = [
|
|
15
15
|
{
|
|
@@ -185,3 +185,135 @@ export const Manual: Story = {
|
|
|
185
185
|
tabs: BAY_TABS,
|
|
186
186
|
},
|
|
187
187
|
}
|
|
188
|
+
|
|
189
|
+
const BAY_ROUTE_TABS: TabLinkItem[] = [
|
|
190
|
+
{
|
|
191
|
+
href: "/bays/3",
|
|
192
|
+
label: "Overview",
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
href: "/bays/3/titles",
|
|
196
|
+
label: "Titles",
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
href: "/bays/3/tracks",
|
|
200
|
+
label: "Tracks",
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
href: "/bays/3/log",
|
|
204
|
+
label: "Log",
|
|
205
|
+
},
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* A tab that is a **place**. Every trigger is a real `<a href>`, so
|
|
210
|
+
* middle-click, ctrl-click, "open in a new tab" and "copy link
|
|
211
|
+
* address" all work — and reloading the page comes back to the same
|
|
212
|
+
* section, which is the whole reason to route one.
|
|
213
|
+
*
|
|
214
|
+
* The bar is a named `<nav>`, not a `tablist`: `role="tab"` on an
|
|
215
|
+
* anchor overrides the link role, so a screen reader would announce
|
|
216
|
+
* a disclosure and then the address would change instead. The
|
|
217
|
+
* *paint* is shared with the panel bar above through
|
|
218
|
+
* `toTabTriggerClass`, which is what this mode was added to fix.
|
|
219
|
+
*
|
|
220
|
+
* The sections themselves are rendered by the router — an
|
|
221
|
+
* `<Outlet />` under the bar — so this component draws no panels
|
|
222
|
+
* here at all.
|
|
223
|
+
*/
|
|
224
|
+
export const Routed: Story = {
|
|
225
|
+
args: { label: "Bay 3", tabs: BAY_TABS },
|
|
226
|
+
render: () => (
|
|
227
|
+
<Tabs
|
|
228
|
+
activeHref="/bays/3/log"
|
|
229
|
+
label="Bay 3 sections"
|
|
230
|
+
tabs={BAY_ROUTE_TABS}
|
|
231
|
+
/>
|
|
232
|
+
),
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Routed and panel bars side by side — the comparison this mode
|
|
237
|
+
* exists for. They are one shape doing one job, and before `href`
|
|
238
|
+
* existed the routed one had to be built out of `Nav`, whose current
|
|
239
|
+
* item is a filled pill.
|
|
240
|
+
*
|
|
241
|
+
* The third cell is the matching rule, which is `Nav`'s
|
|
242
|
+
* `resolveActiveKey` rather than a second implementation: the
|
|
243
|
+
* deepest matching path wins, so `/tasks` stands down while the
|
|
244
|
+
* reader is on `/tasks` *and* nothing lights up twice.
|
|
245
|
+
*/
|
|
246
|
+
export const RoutedAllVariants: Story = {
|
|
247
|
+
args: { label: "Bay 3", tabs: BAY_TABS },
|
|
248
|
+
render: () => (
|
|
249
|
+
<StoryGrid columns={2}>
|
|
250
|
+
<StoryCell label="routed · horizontal">
|
|
251
|
+
<Tabs
|
|
252
|
+
activeHref="/bays/3/titles"
|
|
253
|
+
label="Routed horizontal"
|
|
254
|
+
tabs={BAY_ROUTE_TABS}
|
|
255
|
+
/>
|
|
256
|
+
</StoryCell>
|
|
257
|
+
|
|
258
|
+
<StoryCell label="panel · horizontal">
|
|
259
|
+
<Tabs label="Panel horizontal" tabs={BAY_TABS} />
|
|
260
|
+
</StoryCell>
|
|
261
|
+
|
|
262
|
+
<StoryCell label="routed · vertical">
|
|
263
|
+
<Tabs
|
|
264
|
+
activeHref="/bays/3/tracks"
|
|
265
|
+
label="Routed vertical"
|
|
266
|
+
orientation="vertical"
|
|
267
|
+
tabs={BAY_ROUTE_TABS}
|
|
268
|
+
/>
|
|
269
|
+
</StoryCell>
|
|
270
|
+
|
|
271
|
+
<StoryCell label="routed · a child route is still the section">
|
|
272
|
+
<Tabs
|
|
273
|
+
activeHref="/bays/3/titles/4"
|
|
274
|
+
label="Routed on a child route"
|
|
275
|
+
tabs={BAY_ROUTE_TABS}
|
|
276
|
+
/>
|
|
277
|
+
</StoryCell>
|
|
278
|
+
</StoryGrid>
|
|
279
|
+
),
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const ROUTED_STATE_TABS: TabLinkItem[] = [
|
|
283
|
+
...BAY_ROUTE_TABS,
|
|
284
|
+
{
|
|
285
|
+
href: "/bays/3/reports",
|
|
286
|
+
isDisabled: true,
|
|
287
|
+
label: "Reports",
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
href: "https://mkdocs.octen.dev/workspace/docket/",
|
|
291
|
+
isExternal: true,
|
|
292
|
+
label: "Handbook",
|
|
293
|
+
},
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* A disabled routed tab is **not a link at all** — a `<span>` with
|
|
298
|
+
* `aria-disabled`, because an anchor has no `disabled` attribute and
|
|
299
|
+
* `aria-disabled` alone still navigates on Enter. That is the
|
|
300
|
+
* version of this that looks right and is not.
|
|
301
|
+
*
|
|
302
|
+
* An external tab is a plain anchor with `target="_blank"` and
|
|
303
|
+
* `rel="noopener noreferrer"`, and it is **never current** — no
|
|
304
|
+
* address inside this app can be a place outside it.
|
|
305
|
+
*
|
|
306
|
+
* `activeHref` here is a screen the bar does not contain, which is
|
|
307
|
+
* the third state: **nothing** is current. A bar that guessed the
|
|
308
|
+
* closest tab instead would be lying on every modal route.
|
|
309
|
+
*/
|
|
310
|
+
export const RoutedAllStates: Story = {
|
|
311
|
+
args: { label: "Bay 3", tabs: BAY_TABS },
|
|
312
|
+
render: () => (
|
|
313
|
+
<Tabs
|
|
314
|
+
activeHref="/settings"
|
|
315
|
+
label="Bay 3 states"
|
|
316
|
+
tabs={ROUTED_STATE_TABS}
|
|
317
|
+
/>
|
|
318
|
+
),
|
|
319
|
+
}
|
package/src/Tabs/Tabs.test.tsx
CHANGED
|
@@ -13,6 +13,9 @@ const {
|
|
|
13
13
|
Default,
|
|
14
14
|
Manual,
|
|
15
15
|
Responsive,
|
|
16
|
+
Routed,
|
|
17
|
+
RoutedAllStates,
|
|
18
|
+
RoutedAllVariants,
|
|
16
19
|
} = composeStories(stories)
|
|
17
20
|
|
|
18
21
|
test("selecting a tab wires the panel back to it", async () => {
|
|
@@ -281,3 +284,169 @@ test("automatic activation selects as focus moves, and wraps", async () => {
|
|
|
281
284
|
expect(tabs[0]).toHaveAttribute("aria-selected", "true")
|
|
282
285
|
})
|
|
283
286
|
})
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* The routed bar is a **navigation**, not a tablist, and this is the
|
|
290
|
+
* assertion that says so. `role="tab"` on an anchor overrides the
|
|
291
|
+
* link role: a screen reader announces a disclosure, the reader
|
|
292
|
+
* waits for a panel, and the address changes under them instead.
|
|
293
|
+
*
|
|
294
|
+
* A screenshot cannot tell the two apart — the paint is deliberately
|
|
295
|
+
* identical — so nothing but this catches a regression to it.
|
|
296
|
+
*/
|
|
297
|
+
test("a routed tab is a link in a nav, never a tab in a tablist", async () => {
|
|
298
|
+
const { canvas, canvasElement } = await mountStory(Routed)
|
|
299
|
+
|
|
300
|
+
const nav = expectAgentDrivable(canvas, {
|
|
301
|
+
name: "Bay 3 sections",
|
|
302
|
+
role: "navigation",
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
await expect(
|
|
306
|
+
canvas.queryByRole("tablist"),
|
|
307
|
+
).not.toBeInTheDocument()
|
|
308
|
+
|
|
309
|
+
await expect(
|
|
310
|
+
canvas.queryByRole("tab"),
|
|
311
|
+
).not.toBeInTheDocument()
|
|
312
|
+
|
|
313
|
+
// No panels either: the sections are the router's, rendered
|
|
314
|
+
// through an `<Outlet />` this component cannot see.
|
|
315
|
+
await expect(
|
|
316
|
+
canvas.queryByRole("tabpanel"),
|
|
317
|
+
).not.toBeInTheDocument()
|
|
318
|
+
|
|
319
|
+
const links = Array.from(
|
|
320
|
+
nav.querySelectorAll<HTMLAnchorElement>("a[href]"),
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
await expect(links).toHaveLength(4)
|
|
324
|
+
|
|
325
|
+
await expectNoAxeViolations(canvasElement)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
test("exactly one routed tab is current, and it is the deepest match", async () => {
|
|
329
|
+
const { canvas } = await mountStory(RoutedAllVariants)
|
|
330
|
+
|
|
331
|
+
// `/tasks` and the project root both match `/…/tasks`, and only
|
|
332
|
+
// the deeper one may light up — a bar with two current items has
|
|
333
|
+
// stopped saying anything. The rule is `resolveActiveKey`, shared
|
|
334
|
+
// with `Nav` rather than reimplemented here.
|
|
335
|
+
const nav = expectAgentDrivable(canvas, {
|
|
336
|
+
name: "Routed horizontal",
|
|
337
|
+
role: "navigation",
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
const current = nav.querySelectorAll(
|
|
341
|
+
'[aria-current="page"]',
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
await expect(current).toHaveLength(1)
|
|
345
|
+
|
|
346
|
+
await expect(current[0]).toHaveTextContent("Titles")
|
|
347
|
+
|
|
348
|
+
// A child route is still inside the section it belongs to.
|
|
349
|
+
const child = expectAgentDrivable(canvas, {
|
|
350
|
+
name: "Routed on a child route",
|
|
351
|
+
role: "navigation",
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
const childCurrent = child.querySelectorAll(
|
|
355
|
+
'[aria-current="page"]',
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
await expect(childCurrent).toHaveLength(1)
|
|
359
|
+
|
|
360
|
+
await expect(childCurrent[0]).toHaveTextContent("Titles")
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* A disabled anchor is the defect this guards. `<a>` has no
|
|
365
|
+
* `disabled` attribute, so the tempting version — a real `href` plus
|
|
366
|
+
* `aria-disabled` — greys out and still navigates on Enter and on a
|
|
367
|
+
* middle click.
|
|
368
|
+
*/
|
|
369
|
+
test("a disabled routed tab has no href, and an external one is never current", async () => {
|
|
370
|
+
const { canvas, canvasElement } =
|
|
371
|
+
await mountStory(RoutedAllStates)
|
|
372
|
+
|
|
373
|
+
const nav = expectAgentDrivable(canvas, {
|
|
374
|
+
name: "Bay 3 states",
|
|
375
|
+
role: "navigation",
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
const reports = canvas.getByText("Reports")
|
|
379
|
+
|
|
380
|
+
await expect(reports).toHaveAttribute(
|
|
381
|
+
"aria-disabled",
|
|
382
|
+
"true",
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
await expect(reports.closest("a")).toBeNull()
|
|
386
|
+
|
|
387
|
+
const handbook = expectAgentDrivable(canvas, {
|
|
388
|
+
name: /Handbook/u,
|
|
389
|
+
role: "link",
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
await expect(handbook).toHaveAttribute("target", "_blank")
|
|
393
|
+
|
|
394
|
+
await expect(handbook).toHaveAttribute(
|
|
395
|
+
"rel",
|
|
396
|
+
"noopener noreferrer",
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
// Never current: no address inside this app can be a place
|
|
400
|
+
// outside it, and a plain string compare would match one.
|
|
401
|
+
await expect(handbook).not.toHaveAttribute("aria-current")
|
|
402
|
+
|
|
403
|
+
// `activeHref` points at `/overview`, which no tab owns — so the
|
|
404
|
+
// bar honestly marks nothing rather than guessing the closest.
|
|
405
|
+
await expect(
|
|
406
|
+
nav.querySelectorAll('[aria-current="page"]'),
|
|
407
|
+
).toHaveLength(0)
|
|
408
|
+
|
|
409
|
+
await expectNoAxeViolations(canvasElement)
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* The reason both modes live in one component: a routed tab and a
|
|
414
|
+
* panel tab are one shape doing one job, and Docket shipped them on
|
|
415
|
+
* adjacent screens wearing different paint because the routed one
|
|
416
|
+
* had to be built out of `Nav`.
|
|
417
|
+
*/
|
|
418
|
+
test("a routed tab and a panel tab are painted identically", async () => {
|
|
419
|
+
const { canvas } = await mountStory(RoutedAllVariants)
|
|
420
|
+
|
|
421
|
+
const routedCurrent = expectAgentDrivable(canvas, {
|
|
422
|
+
name: "Routed horizontal",
|
|
423
|
+
role: "navigation",
|
|
424
|
+
}).querySelector('[aria-current="page"]')
|
|
425
|
+
|
|
426
|
+
const panelSelected = expectAgentDrivable(canvas, {
|
|
427
|
+
name: "Panel horizontal",
|
|
428
|
+
role: "tablist",
|
|
429
|
+
}).querySelector('[aria-selected="true"]')
|
|
430
|
+
|
|
431
|
+
const routedStyle = globalThis.getComputedStyle(
|
|
432
|
+
routedCurrent as HTMLElement,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
const panelStyle = globalThis.getComputedStyle(
|
|
436
|
+
panelSelected as HTMLElement,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
for (const property of [
|
|
440
|
+
"borderBottomColor",
|
|
441
|
+
"borderBottomWidth",
|
|
442
|
+
"color",
|
|
443
|
+
"fontSize",
|
|
444
|
+
"fontWeight",
|
|
445
|
+
"paddingInlineStart",
|
|
446
|
+
"paddingBlockStart",
|
|
447
|
+
] as const) {
|
|
448
|
+
await expect(routedStyle[property]).toBe(
|
|
449
|
+
panelStyle[property],
|
|
450
|
+
)
|
|
451
|
+
}
|
|
452
|
+
})
|