@charcuterie/ui 3.0.0 → 3.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.
Files changed (130) hide show
  1. package/README.md +10 -0
  2. package/dist/Badge/Badge.d.ts +33 -3
  3. package/dist/Badge/Badge.d.ts.map +1 -1
  4. package/dist/Badge/Badge.js +20 -2
  5. package/dist/Badge/Badge.js.map +1 -1
  6. package/dist/Board/Board.d.ts +119 -0
  7. package/dist/Board/Board.d.ts.map +1 -0
  8. package/dist/Board/Board.js +165 -0
  9. package/dist/Board/Board.js.map +1 -0
  10. package/dist/Board/BoardCard.d.ts +120 -0
  11. package/dist/Board/BoardCard.d.ts.map +1 -0
  12. package/dist/Board/BoardCard.js +77 -0
  13. package/dist/Board/BoardCard.js.map +1 -0
  14. package/dist/Board/BoardLaneList.d.ts +103 -0
  15. package/dist/Board/BoardLaneList.d.ts.map +1 -0
  16. package/dist/Board/BoardLaneList.js +69 -0
  17. package/dist/Board/BoardLaneList.js.map +1 -0
  18. package/dist/Board/boardMove.d.ts +116 -0
  19. package/dist/Board/boardMove.d.ts.map +1 -0
  20. package/dist/Board/boardMove.js +116 -0
  21. package/dist/Board/boardMove.js.map +1 -0
  22. package/dist/Board/useBoardDrag.d.ts +44 -0
  23. package/dist/Board/useBoardDrag.d.ts.map +1 -0
  24. package/dist/Board/useBoardDrag.js +237 -0
  25. package/dist/Board/useBoardDrag.js.map +1 -0
  26. package/dist/Checkbox/Checkbox.d.ts.map +1 -1
  27. package/dist/Checkbox/Checkbox.js +15 -4
  28. package/dist/Checkbox/Checkbox.js.map +1 -1
  29. package/dist/DataTable/DataTable.d.ts +159 -0
  30. package/dist/DataTable/DataTable.d.ts.map +1 -0
  31. package/dist/DataTable/DataTable.js +197 -0
  32. package/dist/DataTable/DataTable.js.map +1 -0
  33. package/dist/DatePicker/DateGrid.d.ts +79 -0
  34. package/dist/DatePicker/DateGrid.d.ts.map +1 -0
  35. package/dist/DatePicker/DateGrid.js +220 -0
  36. package/dist/DatePicker/DateGrid.js.map +1 -0
  37. package/dist/DatePicker/DatePicker.d.ts +138 -0
  38. package/dist/DatePicker/DatePicker.d.ts.map +1 -0
  39. package/dist/DatePicker/DatePicker.js +384 -0
  40. package/dist/DatePicker/DatePicker.js.map +1 -0
  41. package/dist/DatePicker/datePresets.d.ts +29 -0
  42. package/dist/DatePicker/datePresets.d.ts.map +1 -0
  43. package/dist/DatePicker/datePresets.js +28 -0
  44. package/dist/DatePicker/datePresets.js.map +1 -0
  45. package/dist/DatePicker/parseDateInput.d.ts +106 -0
  46. package/dist/DatePicker/parseDateInput.d.ts.map +1 -0
  47. package/dist/DatePicker/parseDateInput.js +419 -0
  48. package/dist/DatePicker/parseDateInput.js.map +1 -0
  49. package/dist/DatePicker/plainDate.d.ts +167 -0
  50. package/dist/DatePicker/plainDate.d.ts.map +1 -0
  51. package/dist/DatePicker/plainDate.js +272 -0
  52. package/dist/DatePicker/plainDate.js.map +1 -0
  53. package/dist/Dialog/Dialog.d.ts +12 -2
  54. package/dist/Dialog/Dialog.d.ts.map +1 -1
  55. package/dist/Dialog/Dialog.js +2 -2
  56. package/dist/Dialog/Dialog.js.map +1 -1
  57. package/dist/MarkdownEditor/MarkdownEditor.d.ts +147 -0
  58. package/dist/MarkdownEditor/MarkdownEditor.d.ts.map +1 -0
  59. package/dist/MarkdownEditor/MarkdownEditor.js +492 -0
  60. package/dist/MarkdownEditor/MarkdownEditor.js.map +1 -0
  61. package/dist/MarkdownEditor/markdownCommands.d.ts +202 -0
  62. package/dist/MarkdownEditor/markdownCommands.d.ts.map +1 -0
  63. package/dist/MarkdownEditor/markdownCommands.js +379 -0
  64. package/dist/MarkdownEditor/markdownCommands.js.map +1 -0
  65. package/dist/MarkdownEditor/markdownSpans.d.ts +92 -0
  66. package/dist/MarkdownEditor/markdownSpans.d.ts.map +1 -0
  67. package/dist/MarkdownEditor/markdownSpans.js +303 -0
  68. package/dist/MarkdownEditor/markdownSpans.js.map +1 -0
  69. package/dist/Modal/Modal.d.ts +6 -2
  70. package/dist/Modal/Modal.d.ts.map +1 -1
  71. package/dist/Modal/Modal.js +1 -1
  72. package/dist/Modal/Modal.js.map +1 -1
  73. package/dist/Overlay/OverlayPanel.d.ts +13 -2
  74. package/dist/Overlay/OverlayPanel.d.ts.map +1 -1
  75. package/dist/Overlay/OverlayPanel.js +5 -1
  76. package/dist/Overlay/OverlayPanel.js.map +1 -1
  77. package/dist/categoricalStyles.d.ts +53 -0
  78. package/dist/categoricalStyles.d.ts.map +1 -0
  79. package/dist/categoricalStyles.js +195 -0
  80. package/dist/categoricalStyles.js.map +1 -0
  81. package/dist/index.d.ts +22 -0
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +11 -0
  84. package/dist/index.js.map +1 -1
  85. package/package.json +2 -2
  86. package/src/Badge/Badge.mdx +46 -0
  87. package/src/Badge/Badge.stories.tsx +98 -4
  88. package/src/Badge/Badge.test.tsx +157 -2
  89. package/src/Badge/Badge.tsx +49 -3
  90. package/src/Board/Board.mdx +199 -0
  91. package/src/Board/Board.stories.tsx +707 -0
  92. package/src/Board/Board.test.tsx +310 -0
  93. package/src/Board/Board.tsx +333 -0
  94. package/src/Board/BoardCard.tsx +290 -0
  95. package/src/Board/BoardLaneList.tsx +275 -0
  96. package/src/Board/boardMove.test.ts +213 -0
  97. package/src/Board/boardMove.ts +225 -0
  98. package/src/Board/useBoardDrag.ts +413 -0
  99. package/src/Checkbox/Checkbox.tsx +34 -3
  100. package/src/DataTable/DataTable.mdx +123 -0
  101. package/src/DataTable/DataTable.stories.tsx +411 -0
  102. package/src/DataTable/DataTable.test.tsx +386 -0
  103. package/src/DataTable/DataTable.tsx +497 -0
  104. package/src/DatePicker/DateGrid.tsx +467 -0
  105. package/src/DatePicker/DatePicker.mdx +170 -0
  106. package/src/DatePicker/DatePicker.stories.tsx +268 -0
  107. package/src/DatePicker/DatePicker.test.tsx +455 -0
  108. package/src/DatePicker/DatePicker.tsx +852 -0
  109. package/src/DatePicker/datePresets.ts +34 -0
  110. package/src/DatePicker/parseDateInput.test.ts +187 -0
  111. package/src/DatePicker/parseDateInput.ts +629 -0
  112. package/src/DatePicker/plainDate.test.ts +375 -0
  113. package/src/DatePicker/plainDate.ts +465 -0
  114. package/src/Dialog/Dialog.stories.tsx +48 -0
  115. package/src/Dialog/Dialog.test.tsx +30 -0
  116. package/src/Dialog/Dialog.tsx +13 -1
  117. package/src/MarkdownEditor/MarkdownEditor.mdx +189 -0
  118. package/src/MarkdownEditor/MarkdownEditor.stories.tsx +267 -0
  119. package/src/MarkdownEditor/MarkdownEditor.test.tsx +627 -0
  120. package/src/MarkdownEditor/MarkdownEditor.tsx +862 -0
  121. package/src/MarkdownEditor/markdownCommands.test.ts +384 -0
  122. package/src/MarkdownEditor/markdownCommands.ts +566 -0
  123. package/src/MarkdownEditor/markdownSpans.test.ts +217 -0
  124. package/src/MarkdownEditor/markdownSpans.ts +458 -0
  125. package/src/Modal/Modal.tsx +7 -1
  126. package/src/Overlay/OverlayPanel.tsx +17 -1
  127. package/src/argTypes.storyHelpers.ts +39 -0
  128. package/src/categoricalStyles.ts +244 -0
  129. package/src/index.ts +89 -0
  130. package/src/sourceRules.test.ts +52 -1
@@ -0,0 +1,386 @@
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 {
9
+ DESKTOP,
10
+ setViewport,
11
+ } from "../viewport.testHelpers.ts"
12
+ import * as stories from "./DataTable.stories.tsx"
13
+
14
+ const { AllVariants, Default, Interactive, Responsive } =
15
+ composeStories(stories)
16
+
17
+ /**
18
+ * The `<caption>` is the table's accessible name. Without one a
19
+ * screen reader announces "table, 6 columns, 5 rows" and nothing a
20
+ * reader could use to decide whether to enter it — and
21
+ * `getByRole("table", { name })`, the query an agent writes, has
22
+ * nothing to match on.
23
+ */
24
+ test("the table is drivable by role and name", async () => {
25
+ const { canvas, canvasElement } =
26
+ await mountStory(Default)
27
+
28
+ const table = expectAgentDrivable(canvas, {
29
+ name: "Tasks",
30
+ role: "table",
31
+ })
32
+
33
+ await expect(table.tagName).toBe("TABLE")
34
+
35
+ await expectNoAxeViolations(canvasElement)
36
+ })
37
+
38
+ /**
39
+ * The composition, asserted rather than assumed: a sortable column
40
+ * is a `SortableTableHeader`, so it is a `<th>` carrying `aria-sort`
41
+ * with a `<button>` inside it, and a non-sortable one has **no**
42
+ * `aria-sort` at all — absent is how the attribute says "this column
43
+ * cannot be sorted", where `"none"` would say the opposite.
44
+ */
45
+ test("sortable columns announce, unsortable ones stay silent", async () => {
46
+ const { canvas } = await mountStory(Default)
47
+
48
+ const updated = expectAgentDrivable(canvas, {
49
+ name: "Updated",
50
+ role: "columnheader",
51
+ })
52
+
53
+ await expect(updated).toHaveAttribute(
54
+ "aria-sort",
55
+ "descending",
56
+ )
57
+
58
+ await expect(
59
+ expectAgentDrivable(canvas, {
60
+ name: "Task",
61
+ role: "columnheader",
62
+ }),
63
+ ).toHaveAttribute("aria-sort", "none")
64
+
65
+ const assignee = expectAgentDrivable(canvas, {
66
+ name: "Assignee",
67
+ role: "columnheader",
68
+ })
69
+
70
+ await expect(assignee).not.toHaveAttribute("aria-sort")
71
+
72
+ await expect(
73
+ canvas.queryAllByRole("button", { name: "Assignee" }),
74
+ ).toHaveLength(0)
75
+ })
76
+
77
+ /**
78
+ * The half the component does *not* own, proved by the half it does:
79
+ * clicking a header changes `aria-sort` **and** the story's rows
80
+ * come back in a different order, because the story sorted them.
81
+ *
82
+ * `priority` is the column that makes the split worth having. Its
83
+ * order is high → medium → low and its alphabetical order is
84
+ * high → low → medium, so a table that sorted its own rows would
85
+ * have to be handed a comparator to get this right anyway.
86
+ */
87
+ test("a sort request re-orders the consumer's rows", async () => {
88
+ const { canvas } = await mountStory(Interactive)
89
+
90
+ await userEvent.click(
91
+ expectAgentDrivable(canvas, {
92
+ name: "Priority",
93
+ role: "button",
94
+ }),
95
+ )
96
+
97
+ await waitFor(() => {
98
+ expect(
99
+ expectAgentDrivable(canvas, {
100
+ name: "Priority",
101
+ role: "columnheader",
102
+ }),
103
+ ).toHaveAttribute("aria-sort", "ascending")
104
+ })
105
+
106
+ const getFirstRowPriority = () =>
107
+ canvas
108
+ .getAllByRole("row")[1]
109
+ ?.textContent?.includes("high")
110
+
111
+ await expect(getFirstRowPriority()).toBe(true)
112
+
113
+ await userEvent.click(
114
+ expectAgentDrivable(canvas, {
115
+ name: "Priority",
116
+ role: "button",
117
+ }),
118
+ )
119
+
120
+ await waitFor(() => {
121
+ expect(
122
+ expectAgentDrivable(canvas, {
123
+ name: "Priority",
124
+ role: "columnheader",
125
+ }),
126
+ ).toHaveAttribute("aria-sort", "descending")
127
+ })
128
+
129
+ await expect(getFirstRowPriority()).toBe(false)
130
+
131
+ // Exactly one column ever holds a direction.
132
+ await expect(
133
+ expectAgentDrivable(canvas, {
134
+ name: "Task",
135
+ role: "columnheader",
136
+ }),
137
+ ).toHaveAttribute("aria-sort", "none")
138
+ })
139
+
140
+ /**
141
+ * A gate, not a nicety. The sort control is reachable with `Tab` in
142
+ * document order and the ring is a **measured** outline — a class
143
+ * assertion would pass on a component whose ring never paints.
144
+ */
145
+ test("a sortable header takes keyboard focus with a visible ring", async () => {
146
+ const { canvas } = await mountStory(Interactive)
147
+
148
+ const sortButton = expectAgentDrivable(canvas, {
149
+ name: "Task",
150
+ role: "button",
151
+ })
152
+
153
+ // The selection column is first in document order, so the first
154
+ // stop is its header box and the second is the first sortable
155
+ // column. Asserting both is the tab order itself.
156
+ await userEvent.tab()
157
+
158
+ await waitFor(() => {
159
+ expect(document.activeElement).toBe(
160
+ expectAgentDrivable(canvas, {
161
+ name: "Select every row",
162
+ role: "checkbox",
163
+ }),
164
+ )
165
+ })
166
+
167
+ await userEvent.tab()
168
+
169
+ await waitFor(() => {
170
+ expect(document.activeElement).toBe(sortButton)
171
+ })
172
+
173
+ const { outlineStyle, outlineWidth } =
174
+ globalThis.getComputedStyle(sortButton)
175
+
176
+ await expect(outlineStyle).toBe("solid")
177
+
178
+ await expect(
179
+ Number.parseFloat(outlineWidth),
180
+ ).toBeGreaterThan(0)
181
+
182
+ await userEvent.keyboard("{Enter}")
183
+
184
+ await waitFor(() => {
185
+ expect(
186
+ expectAgentDrivable(canvas, {
187
+ name: "Task",
188
+ role: "columnheader",
189
+ }),
190
+ ).toHaveAttribute("aria-sort", "descending")
191
+ })
192
+ }, 15000)
193
+
194
+ /**
195
+ * `indeterminate` is a **DOM property with no HTML attribute**, so
196
+ * there is no markup that could have set it — which is the shortest
197
+ * proof that the header box is written to rather than rendered.
198
+ */
199
+ test("selecting some rows puts the header box in its third state", async () => {
200
+ const { canvas, canvasElement } =
201
+ await mountStory(Interactive)
202
+
203
+ const firstRowBox = expectAgentDrivable(canvas, {
204
+ name: "Book the chimney sweep",
205
+ role: "checkbox",
206
+ }) as HTMLInputElement
207
+
208
+ await userEvent.click(firstRowBox)
209
+
210
+ const selectAllBox = expectAgentDrivable(canvas, {
211
+ name: "Select every row",
212
+ role: "checkbox",
213
+ }) as HTMLInputElement
214
+
215
+ await waitFor(() => {
216
+ expect(selectAllBox.indeterminate).toBe(true)
217
+ })
218
+
219
+ await expect(selectAllBox.checked).toBe(false)
220
+
221
+ // …and it is *painted*. `Checkbox` renders an `appearance-none`
222
+ // box, so before `indeterminate:` reached it the property was set,
223
+ // a screen reader announced "mixed", and the box on screen was
224
+ // indistinguishable from empty.
225
+ const mixedMark =
226
+ selectAllBox.parentElement?.querySelector(
227
+ "svg:last-of-type",
228
+ ) as SVGElement
229
+
230
+ await expect(
231
+ globalThis.getComputedStyle(mixedMark).visibility,
232
+ ).toBe("visible")
233
+
234
+ await userEvent.click(selectAllBox)
235
+
236
+ await waitFor(() => {
237
+ expect(selectAllBox.indeterminate).toBe(false)
238
+ })
239
+
240
+ await expect(selectAllBox.checked).toBe(true)
241
+
242
+ for (const box of canvas.getAllByRole("checkbox")) {
243
+ await expect((box as HTMLInputElement).checked).toBe(
244
+ true,
245
+ )
246
+ }
247
+
248
+ await expectNoAxeViolations(canvasElement)
249
+ })
250
+
251
+ /**
252
+ * The falsifiable half of "the checkboxes are written to, not
253
+ * re-mounted": a re-mount would take the focus with it, one row into
254
+ * a keyboard pass down the column. This is the assertion that fails
255
+ * if anyone ever swaps the effect for a state-bearing `key`.
256
+ */
257
+ test("ticking a row keeps the focus on the box that was ticked", async () => {
258
+ const { canvas } = await mountStory(Interactive)
259
+
260
+ const rowBox = expectAgentDrivable(canvas, {
261
+ name: "Label the spare keys",
262
+ role: "checkbox",
263
+ })
264
+
265
+ rowBox.focus()
266
+
267
+ await userEvent.keyboard(" ")
268
+
269
+ await waitFor(() => {
270
+ expect((rowBox as HTMLInputElement).checked).toBe(true)
271
+ })
272
+
273
+ await expect(document.activeElement).toBe(rowBox)
274
+
275
+ // And the row says so to CSS, which is what paints it — not
276
+ // `aria-selected`, which belongs to `grid` rows rather than
277
+ // `table` rows.
278
+ await waitFor(() => {
279
+ expect(rowBox.closest("tr")).toHaveAttribute(
280
+ "data-is-selected",
281
+ "true",
282
+ )
283
+ })
284
+ }, 15000)
285
+
286
+ /**
287
+ * The container query, measured — the claim the whole component
288
+ * rests on.
289
+ *
290
+ * `Responsive` renders the same table into three fixed panels
291
+ * inside **one** window, so nothing about the viewport changes
292
+ * between these two assertions. At 15rem a cell is a `flex` block
293
+ * carrying its own column label; at 34rem — past `--cq-md` (32rem)
294
+ * — the same cell is a `table-cell` and the label is gone, because
295
+ * the header row is back above it.
296
+ *
297
+ * A `@media` query cannot produce this result at all: both tables
298
+ * are in the same window at the same zoom.
299
+ */
300
+ test("the layout follows the container, not the window", async () => {
301
+ // 1440px of window, so the three panels can be the widths they
302
+ // claim — and so the wide table's own container is genuinely the
303
+ // thing being measured. The first panel is 240px inside that same
304
+ // 1440px window, which is the sentence a media query cannot say.
305
+ await setViewport(DESKTOP)
306
+
307
+ const { canvas, canvasElement } =
308
+ await mountStory(Responsive)
309
+
310
+ const narrowTable = expectAgentDrivable(canvas, {
311
+ name: "Tasks at 15rem",
312
+ role: "table",
313
+ })
314
+
315
+ const wideTable = expectAgentDrivable(canvas, {
316
+ name: "Tasks at 34rem",
317
+ role: "table",
318
+ })
319
+
320
+ const getFirstCell = (table: HTMLElement) =>
321
+ table.querySelector("tbody td") as HTMLElement
322
+
323
+ const narrowCell = getFirstCell(narrowTable)
324
+
325
+ const wideCell = getFirstCell(wideTable)
326
+
327
+ await expect(
328
+ globalThis.getComputedStyle(narrowCell).display,
329
+ ).toBe("flex")
330
+
331
+ await expect(
332
+ globalThis.getComputedStyle(wideCell).display,
333
+ ).toBe("table-cell")
334
+
335
+ // The in-cell column label: present and painted when narrow,
336
+ // `display: none` when the header row is doing that job.
337
+ const getLabel = (cell: HTMLElement) =>
338
+ cell.querySelector(
339
+ "[aria-hidden='true']",
340
+ ) as HTMLElement
341
+
342
+ await expect(
343
+ globalThis.getComputedStyle(getFirstCell(narrowTable))
344
+ .display,
345
+ ).toBe("flex")
346
+
347
+ await expect(
348
+ globalThis.getComputedStyle(getLabel(narrowCell))
349
+ .display,
350
+ ).not.toBe("none")
351
+
352
+ await expect(
353
+ globalThis.getComputedStyle(getLabel(wideCell)).display,
354
+ ).toBe("none")
355
+
356
+ // Reflow, not a pan surface: nothing in the narrow panel is
357
+ // wider than the panel.
358
+ const panel = narrowTable.parentElement as HTMLElement
359
+
360
+ await expect(narrowTable.scrollWidth).toBeLessThanOrEqual(
361
+ panel.clientWidth + 1,
362
+ )
363
+
364
+ await expectNoAxeViolations(canvasElement)
365
+ })
366
+
367
+ /**
368
+ * The rows go, the table stays. A filter that matches nothing
369
+ * swapping the whole table for a message takes the sort the reader
370
+ * just set — and every column header — away with it.
371
+ */
372
+ test("an empty table keeps its headers", async () => {
373
+ const { canvas } = await mountStory(AllVariants)
374
+
375
+ const emptyHeading = canvas.getByRole("heading", {
376
+ name: "No tasks",
377
+ })
378
+
379
+ await expect(emptyHeading.closest("table")).not.toBeNull()
380
+
381
+ await expect(
382
+ emptyHeading
383
+ .closest("table")
384
+ ?.querySelectorAll("thead th"),
385
+ ).toHaveLength(5)
386
+ })