@charcuterie/ui 4.0.0 → 4.2.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 +3 -2
- package/dist/ActionTiles/ActionTiles.d.ts +66 -8
- package/dist/ActionTiles/ActionTiles.d.ts.map +1 -1
- package/dist/ActionTiles/ActionTiles.js +88 -11
- package/dist/ActionTiles/ActionTiles.js.map +1 -1
- package/dist/PortraitTiles/PortraitAvatar.d.ts +21 -0
- package/dist/PortraitTiles/PortraitAvatar.d.ts.map +1 -0
- package/dist/PortraitTiles/PortraitAvatar.js +29 -0
- package/dist/PortraitTiles/PortraitAvatar.js.map +1 -0
- package/dist/PortraitTiles/PortraitTiles.d.ts +162 -0
- package/dist/PortraitTiles/PortraitTiles.d.ts.map +1 -0
- package/dist/PortraitTiles/PortraitTiles.js +263 -0
- package/dist/PortraitTiles/PortraitTiles.js.map +1 -0
- package/dist/PortraitTiles/portraitColour.d.ts +49 -0
- package/dist/PortraitTiles/portraitColour.d.ts.map +1 -0
- package/dist/PortraitTiles/portraitColour.js +55 -0
- package/dist/PortraitTiles/portraitColour.js.map +1 -0
- package/dist/RouterLink/UnstyledLink.d.ts +17 -0
- package/dist/RouterLink/UnstyledLink.d.ts.map +1 -0
- package/dist/RouterLink/UnstyledLink.js +24 -0
- package/dist/RouterLink/UnstyledLink.js.map +1 -0
- package/dist/categoricalStyles.d.ts +27 -0
- package/dist/categoricalStyles.d.ts.map +1 -1
- package/dist/categoricalStyles.js +49 -0
- package/dist/categoricalStyles.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/tileStyles.d.ts +15 -0
- package/dist/tileStyles.d.ts.map +1 -1
- package/dist/tileStyles.js +26 -1
- package/dist/tileStyles.js.map +1 -1
- package/package.json +2 -2
- package/src/ActionTiles/ActionTiles.mdx +82 -8
- package/src/ActionTiles/ActionTiles.stories.tsx +107 -2
- package/src/ActionTiles/ActionTiles.test.tsx +205 -5
- package/src/ActionTiles/ActionTiles.tsx +274 -110
- package/src/PortraitTiles/PortraitAvatar.tsx +49 -0
- package/src/PortraitTiles/PortraitTiles.mdx +153 -0
- package/src/PortraitTiles/PortraitTiles.stories.tsx +403 -0
- package/src/PortraitTiles/PortraitTiles.test.tsx +399 -0
- package/src/PortraitTiles/PortraitTiles.tsx +577 -0
- package/src/PortraitTiles/portraitColour.test.ts +39 -0
- package/src/PortraitTiles/portraitColour.ts +67 -0
- package/src/RadioGroup/RadioGroup.stories.tsx +39 -0
- package/src/RouterLink/RouterLink.mdx +25 -4
- package/src/RouterLink/UnstyledLink.stories.tsx +57 -0
- package/src/RouterLink/UnstyledLink.test.tsx +40 -0
- package/src/RouterLink/UnstyledLink.tsx +34 -0
- package/src/categoricalStyles.ts +57 -0
- package/src/index.ts +11 -0
- package/src/sourceRules.test.ts +17 -4
- package/src/tileStyles.ts +30 -1
|
@@ -0,0 +1,399 @@
|
|
|
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 "./PortraitTiles.stories.tsx"
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
AllStates,
|
|
12
|
+
AllVariants,
|
|
13
|
+
DataColours,
|
|
14
|
+
Default,
|
|
15
|
+
Interactive,
|
|
16
|
+
NamedHues,
|
|
17
|
+
Responsive,
|
|
18
|
+
WithPictures,
|
|
19
|
+
} = composeStories(stories)
|
|
20
|
+
|
|
21
|
+
test("it is a named group of pressable portraits", async () => {
|
|
22
|
+
const { canvas, canvasElement } =
|
|
23
|
+
await mountStory(Default)
|
|
24
|
+
|
|
25
|
+
const group = expectAgentDrivable(canvas, {
|
|
26
|
+
name: "Who's shopping?",
|
|
27
|
+
role: "group",
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
await expect(
|
|
31
|
+
group.querySelectorAll("button"),
|
|
32
|
+
).toHaveLength(4)
|
|
33
|
+
|
|
34
|
+
// Nothing here is selected, and nothing claims to be — the same
|
|
35
|
+
// call `ActionTiles` makes. A picker that keeps a value is
|
|
36
|
+
// `RadioGroup itemShape="tile"`.
|
|
37
|
+
await expect(
|
|
38
|
+
group.querySelector(
|
|
39
|
+
"[aria-checked], [aria-pressed], [role='radio']",
|
|
40
|
+
),
|
|
41
|
+
).toBeNull()
|
|
42
|
+
|
|
43
|
+
await expectNoAxeViolations(canvasElement)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The number and its unit are both inside the tile, so both are
|
|
48
|
+
* computed into the accessible name. "1,240" on its own tells a
|
|
49
|
+
* screen-reader user nothing at all, and "Avery" alone loses the
|
|
50
|
+
* thing the portrait was being chosen by.
|
|
51
|
+
*/
|
|
52
|
+
test("the name, the number and its unit are one accessible name", async () => {
|
|
53
|
+
const { canvas } = await mountStory(Default)
|
|
54
|
+
|
|
55
|
+
expectAgentDrivable(canvas, {
|
|
56
|
+
name: "Avery 1,240 points",
|
|
57
|
+
role: "button",
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The avatar is decoration: the name it sits beside is inside the
|
|
63
|
+
* same tile, so an announced picture is the label read twice. This
|
|
64
|
+
* holds for the initials fallback and for a real `<img>`, which is
|
|
65
|
+
* why both are checked.
|
|
66
|
+
*/
|
|
67
|
+
test("the face stays out of the accessible name", async () => {
|
|
68
|
+
const { canvas, canvasElement } =
|
|
69
|
+
await mountStory(WithPictures)
|
|
70
|
+
|
|
71
|
+
const group = expectAgentDrivable(canvas, {
|
|
72
|
+
name: "Who's shopping?",
|
|
73
|
+
role: "group",
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
for (const image of group.querySelectorAll("img")) {
|
|
77
|
+
await expect(image).toHaveAttribute("alt", "")
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Named by its words alone. An initial that joined the name would
|
|
81
|
+
// read "A Avery 1,240 points".
|
|
82
|
+
expectAgentDrivable(canvas, {
|
|
83
|
+
name: "Casey 2,015 points",
|
|
84
|
+
role: "button",
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
await expectNoAxeViolations(canvasElement)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A 404 is the case this shape cannot absorb: a torn hole where a
|
|
92
|
+
* face was, beside three that loaded. `onError` is the only signal a
|
|
93
|
+
* browser gives, and the fallback is the one the caller already
|
|
94
|
+
* supplied for "no picture".
|
|
95
|
+
*
|
|
96
|
+
* The story points a portrait at a file that does not exist, so this
|
|
97
|
+
* is the real failure rather than a mocked one.
|
|
98
|
+
*/
|
|
99
|
+
test("a picture that fails to load falls back to the initials", async () => {
|
|
100
|
+
const { canvas } = await mountStory(WithPictures)
|
|
101
|
+
|
|
102
|
+
const group = expectAgentDrivable(canvas, {
|
|
103
|
+
name: "Who's shopping?",
|
|
104
|
+
role: "group",
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
await waitFor(async () => {
|
|
108
|
+
const broken = Array.from(
|
|
109
|
+
group.querySelectorAll<HTMLImageElement>("img"),
|
|
110
|
+
).find((one) =>
|
|
111
|
+
one.src.includes("charcuterie-missing-portrait"),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
// Gone from the DOM entirely, replaced by the letter — not left
|
|
115
|
+
// in place with a broken-image glyph in it.
|
|
116
|
+
await expect(broken).toBeUndefined()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const bailey = expectAgentDrivable(canvas, {
|
|
120
|
+
name: "Bailey 860 points",
|
|
121
|
+
role: "button",
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
await expect(bailey.textContent).toContain("B")
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* `layout="auto"` reads the container and never the window. Every
|
|
129
|
+
* panel in the board is the same window, so a difference between
|
|
130
|
+
* them can only have come from the container.
|
|
131
|
+
*/
|
|
132
|
+
test("the arrangement comes from the container, not the window", async () => {
|
|
133
|
+
const { canvas } = await mountStory(Responsive)
|
|
134
|
+
|
|
135
|
+
const getTileDirection = (name: string) => {
|
|
136
|
+
const tile = expectAgentDrivable(canvas, {
|
|
137
|
+
name,
|
|
138
|
+
role: "group",
|
|
139
|
+
}).querySelector<HTMLElement>("button")
|
|
140
|
+
|
|
141
|
+
if (!tile) {
|
|
142
|
+
throw new Error(`${name} drew no portrait`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return getComputedStyle(tile).flexDirection
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Below `cq-sm` there is no room to stack: a face beside a name is
|
|
149
|
+
// what fits in one narrow column.
|
|
150
|
+
await expect(
|
|
151
|
+
getTileDirection("Who's shopping at 15rem"),
|
|
152
|
+
).toBe("row")
|
|
153
|
+
|
|
154
|
+
await expect(
|
|
155
|
+
getTileDirection("Who's shopping at 34rem"),
|
|
156
|
+
).toBe("column")
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test("a fixed layout skips the query", async () => {
|
|
160
|
+
const { canvas, canvasElement } =
|
|
161
|
+
await mountStory(AllVariants)
|
|
162
|
+
|
|
163
|
+
const getTileDirection = (name: string) => {
|
|
164
|
+
const tile = expectAgentDrivable(canvas, {
|
|
165
|
+
name,
|
|
166
|
+
role: "group",
|
|
167
|
+
}).querySelector<HTMLElement>("button")
|
|
168
|
+
|
|
169
|
+
if (!tile) {
|
|
170
|
+
throw new Error(`${name} drew no portrait`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return getComputedStyle(tile).flexDirection
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Both cells are the same width. Only the prop differs, which is
|
|
177
|
+
// the point of having one.
|
|
178
|
+
await expect(
|
|
179
|
+
getTileDirection("Who's shopping, as rows"),
|
|
180
|
+
).toBe("row")
|
|
181
|
+
|
|
182
|
+
await expect(
|
|
183
|
+
getTileDirection("Who's shopping, as columns"),
|
|
184
|
+
).toBe("column")
|
|
185
|
+
|
|
186
|
+
await expectNoAxeViolations(canvasElement)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The hue has to reach the paint, and the assertion is a computed
|
|
191
|
+
* colour rather than a class name. A `--color-categorical-*` that
|
|
192
|
+
* had never existed resolves to nothing, paints transparent, and
|
|
193
|
+
* satisfies every check that the element rendered — which is exactly
|
|
194
|
+
* how a token name that did not exist shipped in Docket.
|
|
195
|
+
*/
|
|
196
|
+
test("each portrait wears a different hue, and a named one holds it", async () => {
|
|
197
|
+
const { canvas } = await mountStory(Default)
|
|
198
|
+
|
|
199
|
+
const faces = Array.from(
|
|
200
|
+
expectAgentDrivable(canvas, {
|
|
201
|
+
name: "Who's shopping?",
|
|
202
|
+
role: "group",
|
|
203
|
+
}).querySelectorAll<HTMLElement>(
|
|
204
|
+
"button > *:first-child",
|
|
205
|
+
),
|
|
206
|
+
).map((face) => getComputedStyle(face).backgroundColor)
|
|
207
|
+
|
|
208
|
+
await expect(faces).toHaveLength(4)
|
|
209
|
+
|
|
210
|
+
for (const face of faces) {
|
|
211
|
+
await expect(face).not.toBe("rgba(0, 0, 0, 0)")
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
await expect(new Set(faces).size).toBe(4)
|
|
215
|
+
|
|
216
|
+
const { canvas: pinnedCanvas } =
|
|
217
|
+
await mountStory(NamedHues)
|
|
218
|
+
|
|
219
|
+
const pinned = Array.from(
|
|
220
|
+
expectAgentDrivable(pinnedCanvas, {
|
|
221
|
+
name: "Who's shopping, with pinned colours",
|
|
222
|
+
role: "group",
|
|
223
|
+
}).querySelectorAll<HTMLElement>(
|
|
224
|
+
"button > *:first-child",
|
|
225
|
+
),
|
|
226
|
+
).map((face) => getComputedStyle(face).backgroundColor)
|
|
227
|
+
|
|
228
|
+
// Avery is position 0 in both sets. Naming `categorical: 3` has to
|
|
229
|
+
// change its colour, or the prop does nothing and every assertion
|
|
230
|
+
// above still passes.
|
|
231
|
+
await expect(pinned[0]).not.toBe(faces[0])
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The second colour arm, which no token covers: the hue arrives from
|
|
236
|
+
* data, lands in an inline custom property, and three separate
|
|
237
|
+
* classes read it back out. Every step of that is invisible to
|
|
238
|
+
* typecheck, to lint and to axe — the CSS either generated or it did
|
|
239
|
+
* not, and a portrait with no fill looks like a picture that failed
|
|
240
|
+
* to load.
|
|
241
|
+
*/
|
|
242
|
+
test("a data colour reaches the fill exactly, and the initials flip to suit it", async () => {
|
|
243
|
+
const { canvas, canvasElement } =
|
|
244
|
+
await mountStory(DataColours)
|
|
245
|
+
|
|
246
|
+
const faces = Array.from(
|
|
247
|
+
expectAgentDrivable(canvas, {
|
|
248
|
+
name: "Who's shopping, coloured by their cards",
|
|
249
|
+
role: "group",
|
|
250
|
+
}).querySelectorAll<HTMLElement>(
|
|
251
|
+
"button > *:first-child",
|
|
252
|
+
),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
await expect(faces).toHaveLength(3)
|
|
256
|
+
|
|
257
|
+
// Exactly the stated colour. A palette hue would be some other
|
|
258
|
+
// triplet entirely, which is the whole point of the arm.
|
|
259
|
+
await expect(
|
|
260
|
+
faces.map(
|
|
261
|
+
(face) => getComputedStyle(face).backgroundColor,
|
|
262
|
+
),
|
|
263
|
+
).toEqual([
|
|
264
|
+
"rgb(143, 211, 244)",
|
|
265
|
+
"rgb(217, 210, 197)",
|
|
266
|
+
"rgb(59, 74, 107)",
|
|
267
|
+
])
|
|
268
|
+
|
|
269
|
+
// The first two fills are pale and the third is dark, so the
|
|
270
|
+
// letters have to go the other way on the third. Nothing in the
|
|
271
|
+
// toolchain would have said otherwise.
|
|
272
|
+
await expect(
|
|
273
|
+
faces.map((face) => getComputedStyle(face).color),
|
|
274
|
+
).toEqual([
|
|
275
|
+
"rgb(0, 0, 0)",
|
|
276
|
+
"rgb(0, 0, 0)",
|
|
277
|
+
"rgb(255, 255, 255)",
|
|
278
|
+
])
|
|
279
|
+
|
|
280
|
+
await expectNoAxeViolations(canvasElement)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The number is the hue mixed toward the scheme's own text colour,
|
|
285
|
+
* not the hue itself — a `#C0C4CC` number on a near-white surface is
|
|
286
|
+
* the readability failure this arm gives up the palette's guarantee
|
|
287
|
+
* to allow.
|
|
288
|
+
*/
|
|
289
|
+
test("the number is the data colour pulled toward readable, not the raw colour", async () => {
|
|
290
|
+
const { canvas } = await mountStory(DataColours)
|
|
291
|
+
|
|
292
|
+
const bailey = Array.from(
|
|
293
|
+
expectAgentDrivable(canvas, {
|
|
294
|
+
name: "Who's shopping, coloured by their cards",
|
|
295
|
+
role: "group",
|
|
296
|
+
}).querySelectorAll<HTMLButtonElement>("button"),
|
|
297
|
+
).find((one) => one.textContent?.includes("Bailey"))
|
|
298
|
+
|
|
299
|
+
const stat = Array.from(
|
|
300
|
+
bailey?.querySelectorAll<HTMLElement>("span") ?? [],
|
|
301
|
+
).find((one) => one.textContent === "860")
|
|
302
|
+
|
|
303
|
+
await expect(stat).toBeDefined()
|
|
304
|
+
|
|
305
|
+
const statColour = getComputedStyle(
|
|
306
|
+
stat as HTMLElement,
|
|
307
|
+
).color
|
|
308
|
+
|
|
309
|
+
await expect(statColour).not.toBe("rgb(217, 210, 197)")
|
|
310
|
+
|
|
311
|
+
await expect(statColour).not.toBe("rgba(0, 0, 0, 0)")
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
test("every portrait is its own tab stop, and Enter and Space press it", async () => {
|
|
315
|
+
const { canvas } = await mountStory(Interactive)
|
|
316
|
+
|
|
317
|
+
const tiles = Array.from(
|
|
318
|
+
expectAgentDrivable(canvas, {
|
|
319
|
+
name: "Who's shopping?",
|
|
320
|
+
role: "group",
|
|
321
|
+
}).querySelectorAll<HTMLButtonElement>("button"),
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
await expect(
|
|
325
|
+
tiles.filter((one) => one.tabIndex === 0),
|
|
326
|
+
).toHaveLength(2)
|
|
327
|
+
|
|
328
|
+
tiles[0]?.focus()
|
|
329
|
+
|
|
330
|
+
await userEvent.tab()
|
|
331
|
+
|
|
332
|
+
await waitFor(() => {
|
|
333
|
+
expect(tiles[1]).toHaveFocus()
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
await userEvent.keyboard("{Enter}")
|
|
337
|
+
|
|
338
|
+
await userEvent.keyboard(" ")
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
test("a disabled portrait cannot be pressed", async () => {
|
|
342
|
+
const { canvas, canvasElement } =
|
|
343
|
+
await mountStory(AllStates)
|
|
344
|
+
|
|
345
|
+
const bailey = Array.from(
|
|
346
|
+
expectAgentDrivable(canvas, {
|
|
347
|
+
name: "Who's shopping, with Bailey unavailable",
|
|
348
|
+
role: "group",
|
|
349
|
+
}).querySelectorAll<HTMLButtonElement>("button"),
|
|
350
|
+
).find((one) => one.textContent?.includes("Bailey"))
|
|
351
|
+
|
|
352
|
+
await expect(bailey).toBeDisabled()
|
|
353
|
+
|
|
354
|
+
await expectNoAxeViolations(canvasElement)
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
test("an href portrait is a real anchor", async () => {
|
|
358
|
+
const { canvas, canvasElement } =
|
|
359
|
+
await mountStory(AllStates)
|
|
360
|
+
|
|
361
|
+
const avery = Array.from(
|
|
362
|
+
expectAgentDrivable(canvas, {
|
|
363
|
+
name: "Who's shopping, as links",
|
|
364
|
+
role: "group",
|
|
365
|
+
}).querySelectorAll<HTMLAnchorElement>("a"),
|
|
366
|
+
).find((one) => one.textContent?.includes("Avery"))
|
|
367
|
+
|
|
368
|
+
// A real `<a href>`, so middle-click, ctrl-click, "open in a new
|
|
369
|
+
// tab" and "copy link address" come from the element rather than
|
|
370
|
+
// from the paint.
|
|
371
|
+
await expect(avery).toHaveAttribute("href", "/avery")
|
|
372
|
+
|
|
373
|
+
await expectNoAxeViolations(canvasElement)
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* The box is shared with the other tile shapes through
|
|
378
|
+
* `tileStyles.ts`. Two tile sets on one page have to be the same
|
|
379
|
+
* card, and "identical" spelled as two copies of one string is a
|
|
380
|
+
* promise that survives exactly one edit.
|
|
381
|
+
*/
|
|
382
|
+
test("a portrait is the same box as an action tile", async () => {
|
|
383
|
+
const { canvas } = await mountStory(Default)
|
|
384
|
+
|
|
385
|
+
const portrait = expectAgentDrivable(canvas, {
|
|
386
|
+
name: "Avery 1,240 points",
|
|
387
|
+
role: "button",
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
const style = getComputedStyle(portrait)
|
|
391
|
+
|
|
392
|
+
await expect(style.borderTopWidth).toBe("1px")
|
|
393
|
+
|
|
394
|
+
await expect(style.borderTopLeftRadius).not.toBe("0px")
|
|
395
|
+
|
|
396
|
+
await expect(
|
|
397
|
+
Number.parseFloat(style.paddingTop),
|
|
398
|
+
).toBeGreaterThan(0)
|
|
399
|
+
})
|