@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.
- 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/Dialog/Dialog.d.ts +12 -2
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +2 -2
- package/dist/Dialog/Dialog.js.map +1 -1
- 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/Modal/Modal.d.ts +6 -2
- package/dist/Modal/Modal.d.ts.map +1 -1
- package/dist/Modal/Modal.js +1 -1
- package/dist/Modal/Modal.js.map +1 -1
- package/dist/Overlay/OverlayPanel.d.ts +13 -2
- package/dist/Overlay/OverlayPanel.d.ts.map +1 -1
- package/dist/Overlay/OverlayPanel.js +5 -1
- package/dist/Overlay/OverlayPanel.js.map +1 -1
- 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/Dialog/Dialog.stories.tsx +48 -0
- package/src/Dialog/Dialog.test.tsx +30 -0
- package/src/Dialog/Dialog.tsx +13 -1
- 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/Modal/Modal.tsx +7 -1
- package/src/Overlay/OverlayPanel.tsx +17 -1
- 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,268 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
2
|
+
import type { ReactNode } from "react"
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
|
|
5
|
+
import { controlSizeArgType } from "../argTypes.storyHelpers.ts"
|
|
6
|
+
import {
|
|
7
|
+
ContainerBoard,
|
|
8
|
+
StoryCell,
|
|
9
|
+
StoryGrid,
|
|
10
|
+
StorySection,
|
|
11
|
+
} from "../board.storyHelpers.tsx"
|
|
12
|
+
import { Field } from "../Field/Field.tsx"
|
|
13
|
+
import { DatePicker } from "./DatePicker.tsx"
|
|
14
|
+
import type { DateRange } from "./plainDate.ts"
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Wednesday, 19 August 2026 — fixed, and passed to every story.
|
|
18
|
+
*
|
|
19
|
+
* A date component demoed against the real clock is a set of
|
|
20
|
+
* screenshots that stop matching the day after they are taken, and a
|
|
21
|
+
* "Today" preset that lands somewhere different every morning.
|
|
22
|
+
*/
|
|
23
|
+
const TODAY = "2026-08-19"
|
|
24
|
+
|
|
25
|
+
const meta = {
|
|
26
|
+
title: "Components/DatePicker",
|
|
27
|
+
component: DatePicker,
|
|
28
|
+
args: {
|
|
29
|
+
label: "Due date",
|
|
30
|
+
// Pinned, so a demo (and every screenshot of it) reads the same
|
|
31
|
+
// regardless of what locale the machine rendering it is set to.
|
|
32
|
+
// `AllVariants` carries the one that deliberately differs.
|
|
33
|
+
locale: "en-US",
|
|
34
|
+
size: "md",
|
|
35
|
+
today: TODAY,
|
|
36
|
+
},
|
|
37
|
+
argTypes: {
|
|
38
|
+
size: controlSizeArgType,
|
|
39
|
+
},
|
|
40
|
+
parameters: { layout: "padded" },
|
|
41
|
+
} satisfies Meta<typeof DatePicker>
|
|
42
|
+
|
|
43
|
+
export default meta
|
|
44
|
+
|
|
45
|
+
type Story = StoryObj<typeof meta>
|
|
46
|
+
|
|
47
|
+
const describeValue = (
|
|
48
|
+
value: DateRange | null | string,
|
|
49
|
+
) => {
|
|
50
|
+
if (value === null) {
|
|
51
|
+
return "—"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (typeof value === "string") {
|
|
55
|
+
return value
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return `${value.start ?? "—"} to ${value.end ?? "—"}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const DatePickerHarness = ({
|
|
62
|
+
isRange,
|
|
63
|
+
locale = "en-US",
|
|
64
|
+
...pickerProps
|
|
65
|
+
}: {
|
|
66
|
+
isDisabled?: boolean
|
|
67
|
+
isRange?: boolean
|
|
68
|
+
label: string
|
|
69
|
+
locale?: string
|
|
70
|
+
maxValue?: string
|
|
71
|
+
minValue?: string
|
|
72
|
+
presets?: readonly { days: number; label: string }[]
|
|
73
|
+
value?: DateRange | string
|
|
74
|
+
}): ReactNode => {
|
|
75
|
+
const [chosen, setChosen] = useState<
|
|
76
|
+
DateRange | null | string
|
|
77
|
+
>(pickerProps.value ?? null)
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className="flex flex-col gap-1">
|
|
81
|
+
<DatePicker
|
|
82
|
+
{...pickerProps}
|
|
83
|
+
isRange={isRange}
|
|
84
|
+
locale={locale}
|
|
85
|
+
onChange={setChosen}
|
|
86
|
+
today={TODAY}
|
|
87
|
+
/>
|
|
88
|
+
|
|
89
|
+
<p className="text-content-muted text-xs">
|
|
90
|
+
Chosen: {describeValue(chosen)}
|
|
91
|
+
</p>
|
|
92
|
+
</div>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The field, closed. Click it or press ArrowDown to open the
|
|
98
|
+
* calendar; or just type — `tomorrow`, `next fri`, `+14d`, `8/19`,
|
|
99
|
+
* `19 aug`, `2026-08-19` and a bare `19` all resolve, and the line
|
|
100
|
+
* underneath shows what they resolved to before anything commits.
|
|
101
|
+
*/
|
|
102
|
+
export const Default: Story = {
|
|
103
|
+
args: { label: "Due date" },
|
|
104
|
+
render: () => <DatePickerHarness label="Due date" />,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Single and range, a bounded field, a locale that writes the day
|
|
109
|
+
* first, and the shortcut row switched off.
|
|
110
|
+
*
|
|
111
|
+
* `isRange` is a **mode**, not a sibling component — the same call
|
|
112
|
+
* `Combobox` made about `isMultiple`, and for the same reason: a
|
|
113
|
+
* range picker has no ARIA role of its own to be named after.
|
|
114
|
+
*/
|
|
115
|
+
export const AllVariants: Story = {
|
|
116
|
+
args: { label: "Due date" },
|
|
117
|
+
render: () => (
|
|
118
|
+
<StoryGrid columns={2}>
|
|
119
|
+
<StoryCell align="stretch" label="single (default)">
|
|
120
|
+
<DatePickerHarness label="Due date" />
|
|
121
|
+
</StoryCell>
|
|
122
|
+
|
|
123
|
+
<StoryCell align="stretch" label="isRange">
|
|
124
|
+
<DatePickerHarness isRange label="Phase" />
|
|
125
|
+
</StoryCell>
|
|
126
|
+
|
|
127
|
+
<StoryCell
|
|
128
|
+
align="stretch"
|
|
129
|
+
label="minValue / maxValue"
|
|
130
|
+
>
|
|
131
|
+
<DatePickerHarness
|
|
132
|
+
label="Sprint day"
|
|
133
|
+
maxValue="2026-08-28"
|
|
134
|
+
minValue="2026-08-17"
|
|
135
|
+
/>
|
|
136
|
+
</StoryCell>
|
|
137
|
+
|
|
138
|
+
<StoryCell align="stretch" label='locale="en-GB"'>
|
|
139
|
+
<DatePickerHarness
|
|
140
|
+
label="Scheduled"
|
|
141
|
+
locale="en-GB"
|
|
142
|
+
/>
|
|
143
|
+
</StoryCell>
|
|
144
|
+
|
|
145
|
+
<StoryCell align="stretch" label="presets={[]}">
|
|
146
|
+
<DatePickerHarness label="Review on" presets={[]} />
|
|
147
|
+
</StoryCell>
|
|
148
|
+
|
|
149
|
+
<StoryCell align="stretch" label="inside a Field">
|
|
150
|
+
<Field
|
|
151
|
+
description="Anything typeable: tomorrow, next fri, +14d."
|
|
152
|
+
label="Follow up"
|
|
153
|
+
>
|
|
154
|
+
<DatePicker
|
|
155
|
+
label="Follow up"
|
|
156
|
+
locale="en-US"
|
|
157
|
+
today={TODAY}
|
|
158
|
+
/>
|
|
159
|
+
</Field>
|
|
160
|
+
</StoryCell>
|
|
161
|
+
</StoryGrid>
|
|
162
|
+
),
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Empty, filled, disabled, bounded, and — the state that matters
|
|
167
|
+
* most — **refused**. A value the parser cannot read leaves the text
|
|
168
|
+
* exactly as typed, marks the field invalid, and says why. It does
|
|
169
|
+
* not clear the field and it does not quietly pick a nearby date.
|
|
170
|
+
*/
|
|
171
|
+
export const AllStates: Story = {
|
|
172
|
+
args: { label: "Due date" },
|
|
173
|
+
render: () => (
|
|
174
|
+
<StorySection title="States">
|
|
175
|
+
<StoryGrid columns={2}>
|
|
176
|
+
<StoryCell align="stretch" label="empty">
|
|
177
|
+
<DatePickerHarness label="Empty" />
|
|
178
|
+
</StoryCell>
|
|
179
|
+
|
|
180
|
+
<StoryCell align="stretch" label="filled">
|
|
181
|
+
<DatePickerHarness
|
|
182
|
+
label="Filled"
|
|
183
|
+
value="2026-08-19"
|
|
184
|
+
/>
|
|
185
|
+
</StoryCell>
|
|
186
|
+
|
|
187
|
+
<StoryCell align="stretch" label="range filled">
|
|
188
|
+
<DatePickerHarness
|
|
189
|
+
isRange
|
|
190
|
+
label="Phase"
|
|
191
|
+
value={{
|
|
192
|
+
end: "2026-09-04",
|
|
193
|
+
start: "2026-08-24",
|
|
194
|
+
}}
|
|
195
|
+
/>
|
|
196
|
+
</StoryCell>
|
|
197
|
+
|
|
198
|
+
<StoryCell align="stretch" label="isDisabled">
|
|
199
|
+
<DatePickerHarness
|
|
200
|
+
isDisabled
|
|
201
|
+
label="Locked"
|
|
202
|
+
value="2026-08-19"
|
|
203
|
+
/>
|
|
204
|
+
</StoryCell>
|
|
205
|
+
|
|
206
|
+
<StoryCell
|
|
207
|
+
align="stretch"
|
|
208
|
+
label="error (type “ju 19”)"
|
|
209
|
+
>
|
|
210
|
+
<Field
|
|
211
|
+
error="Pick a date the parser can read."
|
|
212
|
+
label="Refused"
|
|
213
|
+
>
|
|
214
|
+
<DatePicker
|
|
215
|
+
label="Refused"
|
|
216
|
+
locale="en-US"
|
|
217
|
+
today={TODAY}
|
|
218
|
+
/>
|
|
219
|
+
</Field>
|
|
220
|
+
</StoryCell>
|
|
221
|
+
</StoryGrid>
|
|
222
|
+
</StorySection>
|
|
223
|
+
),
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The **field** at three container widths. The panel is the other
|
|
228
|
+
* half of the story and it is a container of its own: portalled and
|
|
229
|
+
* clamped to the space floating-ui found, so at a narrow viewport —
|
|
230
|
+
* or on a desktop zoomed in, which is the same measurement — the
|
|
231
|
+
* weekday headers drop to one letter, the cells shrink, and a range
|
|
232
|
+
* picker's two months stack instead of sitting side by side.
|
|
233
|
+
*/
|
|
234
|
+
export const Responsive: Story = {
|
|
235
|
+
args: { label: "Due date" },
|
|
236
|
+
render: () => (
|
|
237
|
+
<ContainerBoard>
|
|
238
|
+
{(width) => (
|
|
239
|
+
<DatePicker
|
|
240
|
+
label={`Due ${width}`}
|
|
241
|
+
locale="en-US"
|
|
242
|
+
presets={[]}
|
|
243
|
+
today={TODAY}
|
|
244
|
+
/>
|
|
245
|
+
)}
|
|
246
|
+
</ContainerBoard>
|
|
247
|
+
),
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The complete keyboard path, with the value echoed underneath.
|
|
252
|
+
*
|
|
253
|
+
* Tab to the field. Type `next fri` and watch the line resolve it —
|
|
254
|
+
* nothing has committed yet. ArrowDown moves into the grid; arrows
|
|
255
|
+
* move by day, up/down by week, PageUp/PageDown by month,
|
|
256
|
+
* Shift+PageUp/PageDown by year, Home/End to the ends of the week.
|
|
257
|
+
* Enter selects and closes; Escape closes and puts the caret back.
|
|
258
|
+
*/
|
|
259
|
+
export const Interactive: Story = {
|
|
260
|
+
args: { label: "Due date" },
|
|
261
|
+
render: () => (
|
|
262
|
+
<div className="flex flex-col gap-4">
|
|
263
|
+
<DatePickerHarness label="Due date" />
|
|
264
|
+
|
|
265
|
+
<DatePickerHarness isRange label="Phase" />
|
|
266
|
+
</div>
|
|
267
|
+
),
|
|
268
|
+
}
|
|
@@ -0,0 +1,455 @@
|
|
|
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
|
+
PHONE,
|
|
11
|
+
setViewport,
|
|
12
|
+
} from "../viewport.testHelpers.ts"
|
|
13
|
+
import * as stories from "./DatePicker.stories.tsx"
|
|
14
|
+
|
|
15
|
+
const { AllVariants, Default, Interactive } =
|
|
16
|
+
composeStories(stories)
|
|
17
|
+
|
|
18
|
+
const openDefault = async () => {
|
|
19
|
+
const mounted = await mountStory(Default)
|
|
20
|
+
|
|
21
|
+
const input = expectAgentDrivable(mounted.canvas, {
|
|
22
|
+
name: "Due date",
|
|
23
|
+
role: "combobox",
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
await userEvent.click(input)
|
|
27
|
+
|
|
28
|
+
await waitFor(() => {
|
|
29
|
+
expect(
|
|
30
|
+
mounted.body.getByRole("dialog", {
|
|
31
|
+
name: "Due date calendar",
|
|
32
|
+
}),
|
|
33
|
+
).toBeInTheDocument()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
return { ...mounted, input }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test("the field is a combobox that opens a named calendar dialog over a grid", async () => {
|
|
40
|
+
const { body, input } = await openDefault()
|
|
41
|
+
|
|
42
|
+
await expect(input).toHaveAttribute(
|
|
43
|
+
"aria-haspopup",
|
|
44
|
+
"dialog",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
await expect(input).toHaveAttribute(
|
|
48
|
+
"aria-expanded",
|
|
49
|
+
"true",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const dialog = body.getByRole("dialog", {
|
|
53
|
+
name: "Due date calendar",
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
await expect(input).toHaveAttribute(
|
|
57
|
+
"aria-controls",
|
|
58
|
+
dialog.id,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
// The grid is named by the month it draws, so two of them on
|
|
62
|
+
// screen (range mode) stay separately addressable.
|
|
63
|
+
expectAgentDrivable(body, {
|
|
64
|
+
name: "August 2026",
|
|
65
|
+
role: "grid",
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// Every day's accessible name is the whole date, not "19".
|
|
69
|
+
expectAgentDrivable(body, {
|
|
70
|
+
name: "Wednesday, August 19, 2026",
|
|
71
|
+
role: "gridcell",
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
await expectNoAxeViolations(dialog)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test("today is marked, and exactly one cell is in the tab order", async () => {
|
|
78
|
+
const { body } = await openDefault()
|
|
79
|
+
|
|
80
|
+
const today = body.getByRole("gridcell", {
|
|
81
|
+
name: "Wednesday, August 19, 2026",
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
await expect(today).toHaveAttribute(
|
|
85
|
+
"aria-current",
|
|
86
|
+
"date",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
// One grid on screen, so every gridcell in the document is one of
|
|
90
|
+
// this month's.
|
|
91
|
+
const tabStops = body
|
|
92
|
+
.getAllByRole("gridcell")
|
|
93
|
+
.filter((cell: HTMLElement) => cell.tabIndex >= 0)
|
|
94
|
+
|
|
95
|
+
// A roving-tabindex grid needs exactly one: zero strands the
|
|
96
|
+
// widget, several put 31 days into the tab order.
|
|
97
|
+
await expect(tabStops).toHaveLength(1)
|
|
98
|
+
|
|
99
|
+
await expect(tabStops[0]).toBe(today)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test("a typed relative date is echoed and does NOT commit until Enter", async () => {
|
|
103
|
+
const { canvas, input } = await openDefault()
|
|
104
|
+
|
|
105
|
+
await userEvent.type(input, "next fri")
|
|
106
|
+
|
|
107
|
+
// The whole anti-silent-guess mechanism: the resolution is on
|
|
108
|
+
// screen, in a live region, before anything is stored.
|
|
109
|
+
await waitFor(() => {
|
|
110
|
+
expect(
|
|
111
|
+
canvas.getByText("Friday, August 21, 2026"),
|
|
112
|
+
).toBeInTheDocument()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
await expect(
|
|
116
|
+
canvas.getByText("Chosen: —"),
|
|
117
|
+
).toBeInTheDocument()
|
|
118
|
+
|
|
119
|
+
await userEvent.keyboard("{Enter}")
|
|
120
|
+
|
|
121
|
+
await waitFor(() => {
|
|
122
|
+
expect(
|
|
123
|
+
canvas.getByText("Chosen: 2026-08-21"),
|
|
124
|
+
).toBeInTheDocument()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Committed as a calendar date, and rendered back in the locale's
|
|
128
|
+
// own medium form.
|
|
129
|
+
await expect(input).toHaveValue("Aug 21, 2026")
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test("an ambiguous value is refused by name, and the typed text survives", async () => {
|
|
133
|
+
const { canvas, input } = await openDefault()
|
|
134
|
+
|
|
135
|
+
await userEvent.type(input, "ju 19")
|
|
136
|
+
|
|
137
|
+
await waitFor(() => {
|
|
138
|
+
expect(
|
|
139
|
+
canvas.getByText(/could be June or July/),
|
|
140
|
+
).toBeInTheDocument()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
await expect(input).toHaveAttribute(
|
|
144
|
+
"aria-invalid",
|
|
145
|
+
"true",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
await userEvent.keyboard("{Enter}")
|
|
149
|
+
|
|
150
|
+
// Nothing committed, and the text was neither cleared nor
|
|
151
|
+
// rewritten to some nearby date the user did not ask for.
|
|
152
|
+
await expect(
|
|
153
|
+
canvas.getByText("Chosen: —"),
|
|
154
|
+
).toBeInTheDocument()
|
|
155
|
+
|
|
156
|
+
await expect(input).toHaveValue("ju 19")
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test("ArrowDown enters the grid, and the arrows move by day, week and month", async () => {
|
|
160
|
+
const { body, input } = await openDefault()
|
|
161
|
+
|
|
162
|
+
await userEvent.click(input)
|
|
163
|
+
|
|
164
|
+
await userEvent.keyboard("{ArrowDown}")
|
|
165
|
+
|
|
166
|
+
const wednesday = body.getByRole("gridcell", {
|
|
167
|
+
name: "Wednesday, August 19, 2026",
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
await waitFor(() => {
|
|
171
|
+
expect(wednesday).toHaveFocus()
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
await userEvent.keyboard("{ArrowRight}")
|
|
175
|
+
|
|
176
|
+
await waitFor(() => {
|
|
177
|
+
expect(
|
|
178
|
+
body.getByRole("gridcell", {
|
|
179
|
+
name: "Thursday, August 20, 2026",
|
|
180
|
+
}),
|
|
181
|
+
).toHaveFocus()
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// Down is a week, not a row of whatever happens to be rendered.
|
|
185
|
+
await userEvent.keyboard("{ArrowDown}")
|
|
186
|
+
|
|
187
|
+
await waitFor(() => {
|
|
188
|
+
expect(
|
|
189
|
+
body.getByRole("gridcell", {
|
|
190
|
+
name: "Thursday, August 27, 2026",
|
|
191
|
+
}),
|
|
192
|
+
).toHaveFocus()
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
// PageDown pages the month, and the grid redraws around a day it
|
|
196
|
+
// had not rendered.
|
|
197
|
+
await userEvent.keyboard("{PageDown}")
|
|
198
|
+
|
|
199
|
+
await waitFor(() => {
|
|
200
|
+
expect(
|
|
201
|
+
body.getByRole("grid", { name: "September 2026" }),
|
|
202
|
+
).toBeInTheDocument()
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
await expect(
|
|
206
|
+
body.getByRole("gridcell", {
|
|
207
|
+
name: "Sunday, September 27, 2026",
|
|
208
|
+
}),
|
|
209
|
+
).toHaveFocus()
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test("Enter on a cell selects and closes; Escape puts the caret back", async () => {
|
|
213
|
+
const { body, canvas, input } = await openDefault()
|
|
214
|
+
|
|
215
|
+
await userEvent.click(input)
|
|
216
|
+
|
|
217
|
+
await userEvent.keyboard("{ArrowDown}{ArrowRight}{Enter}")
|
|
218
|
+
|
|
219
|
+
await waitFor(() => {
|
|
220
|
+
expect(
|
|
221
|
+
canvas.getByText("Chosen: 2026-08-20"),
|
|
222
|
+
).toBeInTheDocument()
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
await expect(body.queryByRole("dialog")).toBeNull()
|
|
226
|
+
|
|
227
|
+
// Reopen, then leave without choosing.
|
|
228
|
+
await userEvent.click(input)
|
|
229
|
+
|
|
230
|
+
await waitFor(() => {
|
|
231
|
+
expect(body.getByRole("dialog")).toBeInTheDocument()
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
await userEvent.keyboard("{Escape}")
|
|
235
|
+
|
|
236
|
+
await waitFor(() => {
|
|
237
|
+
expect(body.queryByRole("dialog")).toBeNull()
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
await expect(input).toHaveFocus()
|
|
241
|
+
|
|
242
|
+
await expect(
|
|
243
|
+
canvas.getByText("Chosen: 2026-08-20"),
|
|
244
|
+
).toBeInTheDocument()
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test("a preset commits the date it names, against the injected today", async () => {
|
|
248
|
+
const { body, canvas, input } = await openDefault()
|
|
249
|
+
|
|
250
|
+
await userEvent.click(input)
|
|
251
|
+
|
|
252
|
+
await userEvent.click(
|
|
253
|
+
body.getByRole("button", { name: "Next week" }),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
await waitFor(() => {
|
|
257
|
+
expect(
|
|
258
|
+
canvas.getByText("Chosen: 2026-08-26"),
|
|
259
|
+
).toBeInTheDocument()
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
test("range mode picks a start then an end, and swaps a backwards pair", async () => {
|
|
264
|
+
await setViewport(DESKTOP)
|
|
265
|
+
|
|
266
|
+
const { body, canvas } = await mountStory(Interactive)
|
|
267
|
+
|
|
268
|
+
const start = expectAgentDrivable(canvas, {
|
|
269
|
+
name: "Phase start",
|
|
270
|
+
role: "combobox",
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
await userEvent.click(start)
|
|
274
|
+
|
|
275
|
+
await waitFor(() => {
|
|
276
|
+
expect(
|
|
277
|
+
body.getByRole("dialog", { name: "Phase calendar" }),
|
|
278
|
+
).toBeInTheDocument()
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
// Two months, side by side at desktop width.
|
|
282
|
+
await expect(
|
|
283
|
+
body.getByRole("grid", { name: "August 2026" }),
|
|
284
|
+
).toBeInTheDocument()
|
|
285
|
+
|
|
286
|
+
await expect(
|
|
287
|
+
body.getByRole("grid", { name: "September 2026" }),
|
|
288
|
+
).toBeInTheDocument()
|
|
289
|
+
|
|
290
|
+
await userEvent.click(
|
|
291
|
+
body.getByRole("gridcell", {
|
|
292
|
+
name: "Monday, August 24, 2026",
|
|
293
|
+
}),
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
// The panel stays open for the second half of the range.
|
|
297
|
+
await expect(
|
|
298
|
+
body.getByRole("dialog", { name: "Phase calendar" }),
|
|
299
|
+
).toBeInTheDocument()
|
|
300
|
+
|
|
301
|
+
// Pick an END that precedes the start. Throwing it away would
|
|
302
|
+
// discard the day the user just clicked; the pair is swapped.
|
|
303
|
+
await userEvent.click(
|
|
304
|
+
body.getByRole("gridcell", {
|
|
305
|
+
name: "Thursday, August 20, 2026",
|
|
306
|
+
}),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
await waitFor(() => {
|
|
310
|
+
expect(
|
|
311
|
+
canvas.getByText("Chosen: 2026-08-20 to 2026-08-24"),
|
|
312
|
+
).toBeInTheDocument()
|
|
313
|
+
})
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
test("minValue and maxValue disable the days outside them", async () => {
|
|
317
|
+
const { body, canvas } = await mountStory(AllVariants)
|
|
318
|
+
|
|
319
|
+
await userEvent.click(
|
|
320
|
+
expectAgentDrivable(canvas, {
|
|
321
|
+
name: "Sprint day",
|
|
322
|
+
role: "combobox",
|
|
323
|
+
}),
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
await waitFor(() => {
|
|
327
|
+
expect(
|
|
328
|
+
body.getByRole("dialog", {
|
|
329
|
+
name: "Sprint day calendar",
|
|
330
|
+
}),
|
|
331
|
+
).toBeInTheDocument()
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
await expect(
|
|
335
|
+
body.getByRole("gridcell", {
|
|
336
|
+
name: "Sunday, August 16, 2026",
|
|
337
|
+
}),
|
|
338
|
+
).toHaveAttribute("aria-disabled", "true")
|
|
339
|
+
|
|
340
|
+
await expect(
|
|
341
|
+
body.getByRole("gridcell", {
|
|
342
|
+
name: "Monday, August 17, 2026",
|
|
343
|
+
}),
|
|
344
|
+
).not.toHaveAttribute("aria-disabled")
|
|
345
|
+
|
|
346
|
+
await expect(
|
|
347
|
+
body.getByRole("gridcell", {
|
|
348
|
+
name: "Saturday, August 29, 2026",
|
|
349
|
+
}),
|
|
350
|
+
).toHaveAttribute("aria-disabled", "true")
|
|
351
|
+
|
|
352
|
+
// A disabled day cannot be committed by clicking it.
|
|
353
|
+
await userEvent.click(
|
|
354
|
+
body.getByRole("gridcell", {
|
|
355
|
+
name: "Sunday, August 16, 2026",
|
|
356
|
+
}),
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
await expect(
|
|
360
|
+
body.getByRole("dialog", {
|
|
361
|
+
name: "Sprint day calendar",
|
|
362
|
+
}),
|
|
363
|
+
).toBeInTheDocument()
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
test("Narrow View: the range panel fits the viewport and stacks its months", async () => {
|
|
367
|
+
await setViewport(PHONE)
|
|
368
|
+
|
|
369
|
+
const { body, canvas } = await mountStory(Interactive)
|
|
370
|
+
|
|
371
|
+
await userEvent.click(
|
|
372
|
+
expectAgentDrivable(canvas, {
|
|
373
|
+
name: "Phase start",
|
|
374
|
+
role: "combobox",
|
|
375
|
+
}),
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
const dialog = await waitFor(() =>
|
|
379
|
+
body.getByRole("dialog", { name: "Phase calendar" }),
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
const panel = dialog.getBoundingClientRect()
|
|
383
|
+
|
|
384
|
+
// The panel asks for 41rem and gets whatever floating-ui found.
|
|
385
|
+
// Nothing may hang off the side of a phone.
|
|
386
|
+
await expect(panel.width).toBeLessThanOrEqual(PHONE.width)
|
|
387
|
+
|
|
388
|
+
await expect(panel.left).toBeGreaterThanOrEqual(0)
|
|
389
|
+
|
|
390
|
+
await expect(panel.right).toBeLessThanOrEqual(PHONE.width)
|
|
391
|
+
|
|
392
|
+
const august = body
|
|
393
|
+
.getByRole("grid", { name: "August 2026" })
|
|
394
|
+
.getBoundingClientRect()
|
|
395
|
+
|
|
396
|
+
const september = body
|
|
397
|
+
.getByRole("grid", { name: "September 2026" })
|
|
398
|
+
.getBoundingClientRect()
|
|
399
|
+
|
|
400
|
+
// Stacked, not side by side — the container query read the panel's
|
|
401
|
+
// own inline size, which is what a media query could not have told
|
|
402
|
+
// it once the panel is portalled and clamped.
|
|
403
|
+
await expect(september.top).toBeGreaterThanOrEqual(
|
|
404
|
+
august.bottom,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
await expectNoAxeViolations(dialog)
|
|
408
|
+
|
|
409
|
+
await setViewport(DESKTOP)
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
test("a container narrower than --cq-xs drops the weekday headers to one letter", async () => {
|
|
413
|
+
// The owner's actual case: not a phone, but a browser zoomed in
|
|
414
|
+
// far enough that the layout viewport is tiny. The panel is
|
|
415
|
+
// clamped to the space available and the calendar reads *that*.
|
|
416
|
+
await setViewport({ height: 700, width: 260 })
|
|
417
|
+
|
|
418
|
+
const { body, canvas } = await mountStory(Default)
|
|
419
|
+
|
|
420
|
+
await userEvent.click(
|
|
421
|
+
expectAgentDrivable(canvas, {
|
|
422
|
+
name: "Due date",
|
|
423
|
+
role: "combobox",
|
|
424
|
+
}),
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
const grid = await waitFor(() =>
|
|
428
|
+
body.getByRole("grid", { name: "August 2026" }),
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
await expect(
|
|
432
|
+
grid.getBoundingClientRect().width,
|
|
433
|
+
).toBeLessThan(256)
|
|
434
|
+
|
|
435
|
+
const [firstHeader] = body.getAllByRole("columnheader")
|
|
436
|
+
|
|
437
|
+
const [narrowSpan, shortSpan] = Array.from(
|
|
438
|
+
firstHeader?.querySelectorAll("span") ?? [],
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
await expect(
|
|
442
|
+
globalThis.getComputedStyle(narrowSpan as Element)
|
|
443
|
+
.display,
|
|
444
|
+
).not.toBe("none")
|
|
445
|
+
|
|
446
|
+
await expect(
|
|
447
|
+
globalThis.getComputedStyle(shortSpan as Element)
|
|
448
|
+
.display,
|
|
449
|
+
).toBe("none")
|
|
450
|
+
|
|
451
|
+
// The column still announces its full name either way.
|
|
452
|
+
await expect(firstHeader).toHaveTextContent("Sunday")
|
|
453
|
+
|
|
454
|
+
await setViewport(DESKTOP)
|
|
455
|
+
})
|