@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,852 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useUniqueId,
|
|
3
|
+
useVisibility,
|
|
4
|
+
} from "@charcuterie/logic"
|
|
5
|
+
import type { ControlSize } from "@charcuterie/tokens"
|
|
6
|
+
import {
|
|
7
|
+
FloatingFocusManager,
|
|
8
|
+
FloatingPortal,
|
|
9
|
+
} from "@floating-ui/react"
|
|
10
|
+
import type {
|
|
11
|
+
KeyboardEvent as ReactKeyboardEvent,
|
|
12
|
+
ReactNode,
|
|
13
|
+
} from "react"
|
|
14
|
+
import { useRef, useState } from "react"
|
|
15
|
+
|
|
16
|
+
import { Button } from "../Button/Button.tsx"
|
|
17
|
+
import { CONTROL_SIZE_CLASS } from "../controlStyles.ts"
|
|
18
|
+
import { IconButton } from "../IconButton/IconButton.tsx"
|
|
19
|
+
import {
|
|
20
|
+
DISABLED_CLASS,
|
|
21
|
+
FOCUS_RING_CLASS,
|
|
22
|
+
} from "../intentStyles.ts"
|
|
23
|
+
import { PANEL_SURFACE_CLASS } from "../Overlay/overlayPanelClass.ts"
|
|
24
|
+
import { useAnchoredOverlay } from "../Overlay/useAnchoredOverlay.ts"
|
|
25
|
+
import type { SlotProps } from "../slotProps.ts"
|
|
26
|
+
import { mergeSlotProps } from "../slotProps.ts"
|
|
27
|
+
import { toClassName } from "../toClassName.ts"
|
|
28
|
+
import { DateGrid } from "./DateGrid.tsx"
|
|
29
|
+
import type { DatePreset } from "./datePresets.ts"
|
|
30
|
+
import { DEFAULT_DATE_PRESETS } from "./datePresets.ts"
|
|
31
|
+
import type { DateInputKeywords } from "./parseDateInput.ts"
|
|
32
|
+
import { parseDateInput } from "./parseDateInput.ts"
|
|
33
|
+
import type { DateRange, PlainDate } from "./plainDate.ts"
|
|
34
|
+
import {
|
|
35
|
+
addDays,
|
|
36
|
+
addMonths,
|
|
37
|
+
clampPlainDate,
|
|
38
|
+
comparePlainDates,
|
|
39
|
+
formatPlainDate,
|
|
40
|
+
getFirstDayOfWeek,
|
|
41
|
+
getIsWithinRange,
|
|
42
|
+
getLocalPlainDate,
|
|
43
|
+
parseIsoDate,
|
|
44
|
+
toIsoDate,
|
|
45
|
+
} from "./plainDate.ts"
|
|
46
|
+
|
|
47
|
+
export type DatePickerProps = SlotProps & {
|
|
48
|
+
className?: string
|
|
49
|
+
/**
|
|
50
|
+
* Sunday 0 through Saturday 6. Omit and the locale decides, via
|
|
51
|
+
* `Intl.Locale`'s `getWeekInfo` — which is why there is no
|
|
52
|
+
* `isMondayFirst` here.
|
|
53
|
+
*/
|
|
54
|
+
firstDayOfWeek?: number
|
|
55
|
+
isDisabled?: boolean
|
|
56
|
+
/**
|
|
57
|
+
* Start-and-end mode. A **mode, not a sibling component** — see
|
|
58
|
+
* the component docblock, and the `Combobox` `isMultiple`
|
|
59
|
+
* precedent it follows.
|
|
60
|
+
*/
|
|
61
|
+
isRange?: boolean
|
|
62
|
+
/** Override the English relative words (`today`, `next`, `+3d`). */
|
|
63
|
+
keywords?: DateInputKeywords
|
|
64
|
+
/**
|
|
65
|
+
* Required. It names the field for a screen reader **and** names
|
|
66
|
+
* the calendar dialog and its grids, which a `role="dialog"` takes
|
|
67
|
+
* from nothing else. Inside a `Field`, pass the same text as the
|
|
68
|
+
* `Field`'s own `label` — they are the same name and WCAG 2.5.3
|
|
69
|
+
* wants them to agree.
|
|
70
|
+
*/
|
|
71
|
+
label: string
|
|
72
|
+
/** Omit for the platform's resolved locale. */
|
|
73
|
+
locale?: string
|
|
74
|
+
/** ISO `YYYY-MM-DD`. Days after this are unselectable. */
|
|
75
|
+
maxValue?: string
|
|
76
|
+
/** ISO `YYYY-MM-DD`. Days before this are unselectable. */
|
|
77
|
+
minValue?: string
|
|
78
|
+
/**
|
|
79
|
+
* An ISO `YYYY-MM-DD` in single mode, a `{ end, start }` of them in
|
|
80
|
+
* range mode, and `null` when cleared. Never a `Date`, and that is
|
|
81
|
+
* the point — see the docblock.
|
|
82
|
+
*/
|
|
83
|
+
onChange?: (value: DateRange | null | string) => void
|
|
84
|
+
placeholder?: string
|
|
85
|
+
/** Pass `[]` to drop the shortcut row entirely. */
|
|
86
|
+
presets?: readonly DatePreset[]
|
|
87
|
+
size?: ControlSize
|
|
88
|
+
/**
|
|
89
|
+
* Which zone's calendar "today" is read from, when `today` is
|
|
90
|
+
* omitted. Ignored when `today` is supplied — which is what every
|
|
91
|
+
* test and every story does.
|
|
92
|
+
*/
|
|
93
|
+
timeZone?: string
|
|
94
|
+
/**
|
|
95
|
+
* The clock, injected, as an ISO `YYYY-MM-DD`.
|
|
96
|
+
*
|
|
97
|
+
* Every relative form (`tomorrow`, `next fri`, `+14d`), the
|
|
98
|
+
* `aria-current="date"` marker, and the preset row resolve against
|
|
99
|
+
* this. Supplying it is what makes the component testable at all:
|
|
100
|
+
* a date picker tested against the real "today" passes on the day
|
|
101
|
+
* it was written and fails silently afterwards. Omitted, it is
|
|
102
|
+
* read **once**, at mount, from `timeZone` or the device.
|
|
103
|
+
*/
|
|
104
|
+
today?: string
|
|
105
|
+
/** **Initial** only, like every other value prop in this library. */
|
|
106
|
+
value?: DateRange | string
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const CHEVRON_PREVIOUS = (
|
|
110
|
+
<svg
|
|
111
|
+
aria-hidden="true"
|
|
112
|
+
className="size-4"
|
|
113
|
+
fill="none"
|
|
114
|
+
focusable={false}
|
|
115
|
+
stroke="currentColor"
|
|
116
|
+
strokeLinecap="round"
|
|
117
|
+
strokeLinejoin="round"
|
|
118
|
+
strokeWidth={1.75}
|
|
119
|
+
viewBox="0 0 24 24"
|
|
120
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
121
|
+
>
|
|
122
|
+
<path d="m15 18-6-6 6-6" />
|
|
123
|
+
</svg>
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const CHEVRON_NEXT = (
|
|
127
|
+
<svg
|
|
128
|
+
aria-hidden="true"
|
|
129
|
+
className="size-4"
|
|
130
|
+
fill="none"
|
|
131
|
+
focusable={false}
|
|
132
|
+
stroke="currentColor"
|
|
133
|
+
strokeLinecap="round"
|
|
134
|
+
strokeLinejoin="round"
|
|
135
|
+
strokeWidth={1.75}
|
|
136
|
+
viewBox="0 0 24 24"
|
|
137
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
138
|
+
>
|
|
139
|
+
<path d="m9 18 6-6-6-6" />
|
|
140
|
+
</svg>
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
const INPUT_CLASS =
|
|
144
|
+
"w-full min-w-0 rounded-md border border-border-default bg-surface-raised text-content-primary transition-colors duration-(--duration-fast) ease-standard placeholder:text-content-muted hover:border-border-strong aria-invalid:border-intent-danger-border"
|
|
145
|
+
|
|
146
|
+
type Endpoint = "end" | "start"
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* A date field: a text input you can actually type into, and a
|
|
150
|
+
* calendar dialog hanging off it.
|
|
151
|
+
*
|
|
152
|
+
* ### The value is a calendar date, never an instant
|
|
153
|
+
*
|
|
154
|
+
* `onChange` reports an ISO `YYYY-MM-DD` string — the same format
|
|
155
|
+
* `<input type="date">` uses — and never a `Date`. A `Date` is a
|
|
156
|
+
* count of milliseconds, so reading one back always goes through the
|
|
157
|
+
* browser's timezone, and a due date picked at 23:30 in Denver comes
|
|
158
|
+
* back as the previous day in London. `plainDate.ts` has the full
|
|
159
|
+
* argument and the arithmetic; `plainDate.test.ts` re-runs it under
|
|
160
|
+
* UTC+14 and UTC-11 and asserts nothing moves.
|
|
161
|
+
*
|
|
162
|
+
* ### Typing is the feature
|
|
163
|
+
*
|
|
164
|
+
* The grid is the fallback, not the interface. `tomorrow`,
|
|
165
|
+
* `next fri`, `+14d`, `19 aug`, `8/19`, `2026-08-19` and a bare `19`
|
|
166
|
+
* all resolve, in the active locale, against an injected `today` —
|
|
167
|
+
* and **the resolution is echoed underneath the field, in a live
|
|
168
|
+
* region, before anything commits.** Nothing here guesses in
|
|
169
|
+
* silence: an ambiguous month prefix comes back as "'ju' could be
|
|
170
|
+
* June or July", not as June. `parseDateInput.ts` is the grammar and
|
|
171
|
+
* the reasoning.
|
|
172
|
+
*
|
|
173
|
+
* ### Range is a mode, not a sibling
|
|
174
|
+
*
|
|
175
|
+
* `isRange` follows
|
|
176
|
+
* [Combobox's `isMultiple`](../../../docs/decisions/2026-08-05-combobox-multi-select-stays-ismultiple-not-a-separate-component.md)
|
|
177
|
+
* for the same three reasons, which hold even harder here:
|
|
178
|
+
*
|
|
179
|
+
* - **No distinct role.** The family is named by ARIA role. A range
|
|
180
|
+
* picker is the same `role="dialog"` over the same `role="grid"`;
|
|
181
|
+
* there is no `daterange` role to name a `DateRangePicker` after,
|
|
182
|
+
* so the name would describe a *presentation*.
|
|
183
|
+
* - **The delta is small and the shared part is everything.** The
|
|
184
|
+
* parser, the locale plumbing, the min/max clamp, the keyboard
|
|
185
|
+
* grid, the panel, the presets and the echo line are identical.
|
|
186
|
+
* What changes is a second input, a second month, and a band
|
|
187
|
+
* drawn between two days.
|
|
188
|
+
* - **`Combobox` already set the precedent for the value shape.**
|
|
189
|
+
* Its `selectedValue` is `readonly string[] | string`; this one is
|
|
190
|
+
* `DateRange | string`. A discriminated props union would type
|
|
191
|
+
* better and was rejected for that consistency, and because
|
|
192
|
+
* Storybook's docgen renders a union props type as an untyped
|
|
193
|
+
* object control.
|
|
194
|
+
*
|
|
195
|
+
* ### The panel is a container, and it is queried as one
|
|
196
|
+
*
|
|
197
|
+
* `@container` sits on the panel, and everything inside sizes to the
|
|
198
|
+
* panel's own inline size rather than to the window: one-letter
|
|
199
|
+
* weekday headers and 32px cells below `--cq-xs`, two-letter and
|
|
200
|
+
* 36px above it, 40px above `--cq-sm`, and in range mode the two
|
|
201
|
+
* months stack below `--cq-sm` and sit side by side above it. That
|
|
202
|
+
* is not the same question a media query asks — the owner browses
|
|
203
|
+
* zoomed in, where a 390px phone reports a 195px layout viewport and
|
|
204
|
+
* a desktop window at 200% has no room either. The `size` middleware
|
|
205
|
+
* clamps the panel to the space actually available, and the
|
|
206
|
+
* container query reads what it got.
|
|
207
|
+
*
|
|
208
|
+
* ### It owns its own open state, and its own value
|
|
209
|
+
*
|
|
210
|
+
* Assembled, like [`Picker`](../Picker/Picker.tsx): the
|
|
211
|
+
* `useVisibility` is in here, because a field that needs the caller
|
|
212
|
+
* to wire `isVisible` is a field the caller has to assemble. And
|
|
213
|
+
* `value` **seeds** rather than controls, matching `Select`,
|
|
214
|
+
* `SegmentedControl` and `Combobox` — one owner for the value, which
|
|
215
|
+
* is the whole of the state layer's argument.
|
|
216
|
+
*/
|
|
217
|
+
export const DatePicker = ({
|
|
218
|
+
className,
|
|
219
|
+
firstDayOfWeek,
|
|
220
|
+
isDisabled = false,
|
|
221
|
+
isRange = false,
|
|
222
|
+
keywords,
|
|
223
|
+
label,
|
|
224
|
+
locale,
|
|
225
|
+
maxValue,
|
|
226
|
+
minValue,
|
|
227
|
+
onChange,
|
|
228
|
+
placeholder = "e.g. tomorrow, 8/19, +14d",
|
|
229
|
+
presets = DEFAULT_DATE_PRESETS,
|
|
230
|
+
size = "md",
|
|
231
|
+
timeZone,
|
|
232
|
+
today,
|
|
233
|
+
value,
|
|
234
|
+
...receivedSlotProps
|
|
235
|
+
}: DatePickerProps): ReactNode => {
|
|
236
|
+
const baseId = useUniqueId()
|
|
237
|
+
|
|
238
|
+
const anchorElement = useRef<HTMLDivElement>(null)
|
|
239
|
+
|
|
240
|
+
const inputElements = useRef(
|
|
241
|
+
new Map<Endpoint, HTMLInputElement>(),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
const { hide, isVisible, show } = useVisibility()
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Read once, lazily, and never again.
|
|
248
|
+
*
|
|
249
|
+
* A component that re-read the clock on every render would change
|
|
250
|
+
* what `tomorrow` means underneath a user typing it at midnight,
|
|
251
|
+
* and would make every test a race. `today` as a prop is the
|
|
252
|
+
* supported way to control it, and every story and test passes it.
|
|
253
|
+
*/
|
|
254
|
+
const [resolvedToday] = useState(
|
|
255
|
+
() =>
|
|
256
|
+
parseIsoDate(today) ??
|
|
257
|
+
getLocalPlainDate(new Date(), timeZone),
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
const todayDate = parseIsoDate(today) ?? resolvedToday
|
|
261
|
+
|
|
262
|
+
const minimum = parseIsoDate(minValue) ?? undefined
|
|
263
|
+
|
|
264
|
+
const maximum = parseIsoDate(maxValue) ?? undefined
|
|
265
|
+
|
|
266
|
+
const resolvedFirstDayOfWeek =
|
|
267
|
+
firstDayOfWeek ?? getFirstDayOfWeek(locale)
|
|
268
|
+
|
|
269
|
+
const seededRange: DateRange =
|
|
270
|
+
typeof value === "string"
|
|
271
|
+
? { end: null, start: value }
|
|
272
|
+
: (value ?? { end: null, start: null })
|
|
273
|
+
|
|
274
|
+
const [range, setRange] = useState<DateRange>(seededRange)
|
|
275
|
+
|
|
276
|
+
const toDisplayText = (isoDate: null | string) => {
|
|
277
|
+
const date = parseIsoDate(isoDate)
|
|
278
|
+
|
|
279
|
+
return date === null
|
|
280
|
+
? ""
|
|
281
|
+
: formatPlainDate(date, {
|
|
282
|
+
dateStyle: "medium",
|
|
283
|
+
locale,
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const [texts, setTexts] = useState({
|
|
288
|
+
end: toDisplayText(seededRange.end),
|
|
289
|
+
start: toDisplayText(seededRange.start),
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
const [activeEndpoint, setActiveEndpoint] =
|
|
293
|
+
useState<Endpoint>("start")
|
|
294
|
+
|
|
295
|
+
const [isGridFocused, setIsGridFocused] = useState(false)
|
|
296
|
+
|
|
297
|
+
const [hoveredDate, setHoveredDate] =
|
|
298
|
+
useState<null | PlainDate>(null)
|
|
299
|
+
|
|
300
|
+
const [focusedDate, setFocusedDate] = useState<PlainDate>(
|
|
301
|
+
() =>
|
|
302
|
+
clampPlainDate(
|
|
303
|
+
parseIsoDate(seededRange.start) ?? todayDate,
|
|
304
|
+
minimum,
|
|
305
|
+
maximum,
|
|
306
|
+
),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
const [visibleMonth, setVisibleMonth] =
|
|
310
|
+
useState<PlainDate>(
|
|
311
|
+
() => parseIsoDate(seededRange.start) ?? todayDate,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
const parsed = parseDateInput(texts[activeEndpoint], {
|
|
315
|
+
keywords,
|
|
316
|
+
locale,
|
|
317
|
+
today: todayDate,
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
const echoId = `${baseId}-echo`
|
|
321
|
+
|
|
322
|
+
const dismiss = () => {
|
|
323
|
+
hide()
|
|
324
|
+
|
|
325
|
+
setIsGridFocused(false)
|
|
326
|
+
|
|
327
|
+
setHoveredDate(null)
|
|
328
|
+
|
|
329
|
+
inputElements.current.get(activeEndpoint)?.focus()
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const {
|
|
333
|
+
context,
|
|
334
|
+
floatingStyles,
|
|
335
|
+
getFloatingProps,
|
|
336
|
+
setFloating,
|
|
337
|
+
} = useAnchoredOverlay({
|
|
338
|
+
anchorRef: anchorElement,
|
|
339
|
+
isVisible,
|
|
340
|
+
maxHeightPx: 520,
|
|
341
|
+
maxWidthPx: isRange ? 656 : 320,
|
|
342
|
+
offsetValue: 4,
|
|
343
|
+
onDismiss: () => {
|
|
344
|
+
dismiss()
|
|
345
|
+
},
|
|
346
|
+
role: "dialog",
|
|
347
|
+
trigger: undefined,
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
const { id: panelId, ...floatingProps } =
|
|
351
|
+
getFloatingProps() as Record<string, unknown> & {
|
|
352
|
+
id: string
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const report = (nextRange: DateRange) => {
|
|
356
|
+
setRange(nextRange)
|
|
357
|
+
|
|
358
|
+
if (isRange) {
|
|
359
|
+
onChange?.(
|
|
360
|
+
nextRange.end === null && nextRange.start === null
|
|
361
|
+
? null
|
|
362
|
+
: nextRange,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
return
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
onChange?.(nextRange.start)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const writeEndpoint = (
|
|
372
|
+
endpoint: Endpoint,
|
|
373
|
+
date: null | PlainDate,
|
|
374
|
+
) => {
|
|
375
|
+
const isoDate = date === null ? null : toIsoDate(date)
|
|
376
|
+
|
|
377
|
+
setTexts((previous) => ({
|
|
378
|
+
...previous,
|
|
379
|
+
[endpoint]:
|
|
380
|
+
date === null ? "" : toDisplayText(isoDate),
|
|
381
|
+
}))
|
|
382
|
+
|
|
383
|
+
const nextRange = isRange
|
|
384
|
+
? { ...range, [endpoint]: isoDate }
|
|
385
|
+
: { end: null, start: isoDate }
|
|
386
|
+
|
|
387
|
+
// A range whose end precedes its start is not a range anybody
|
|
388
|
+
// typed on purpose, and rejecting it would throw away the day
|
|
389
|
+
// they just picked. Swapping keeps both dates and matches every
|
|
390
|
+
// range control in the wild.
|
|
391
|
+
const isBackwards =
|
|
392
|
+
isRange &&
|
|
393
|
+
nextRange.end !== null &&
|
|
394
|
+
nextRange.start !== null &&
|
|
395
|
+
nextRange.end < nextRange.start
|
|
396
|
+
|
|
397
|
+
report(
|
|
398
|
+
isBackwards
|
|
399
|
+
? { end: nextRange.start, start: nextRange.end }
|
|
400
|
+
: nextRange,
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const commitText = (endpoint: Endpoint) => {
|
|
405
|
+
const result = parseDateInput(texts[endpoint], {
|
|
406
|
+
keywords,
|
|
407
|
+
locale,
|
|
408
|
+
today: todayDate,
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
if (result.kind === "empty") {
|
|
412
|
+
writeEndpoint(endpoint, null)
|
|
413
|
+
|
|
414
|
+
return
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (result.kind === "unparsed") {
|
|
418
|
+
// The text stays exactly as typed. Clearing it would throw
|
|
419
|
+
// away what the user has to edit, and rewriting it to the last
|
|
420
|
+
// good value would silently discard the thing they meant.
|
|
421
|
+
return
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
writeEndpoint(
|
|
425
|
+
endpoint,
|
|
426
|
+
clampPlainDate(result.date, minimum, maximum),
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const handleTextChange = (
|
|
431
|
+
endpoint: Endpoint,
|
|
432
|
+
nextText: string,
|
|
433
|
+
) => {
|
|
434
|
+
setTexts((previous) => ({
|
|
435
|
+
...previous,
|
|
436
|
+
[endpoint]: nextText,
|
|
437
|
+
}))
|
|
438
|
+
|
|
439
|
+
const result = parseDateInput(nextText, {
|
|
440
|
+
keywords,
|
|
441
|
+
locale,
|
|
442
|
+
today: todayDate,
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
// Live preview: the grid follows what has been typed so far
|
|
446
|
+
// without committing it, which is what makes `next fri` legible
|
|
447
|
+
// rather than an act of faith.
|
|
448
|
+
if (result.kind === "date") {
|
|
449
|
+
setFocusedDate(result.date)
|
|
450
|
+
|
|
451
|
+
setVisibleMonth(result.date)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const chooseDate = (date: PlainDate) => {
|
|
456
|
+
setFocusedDate(date)
|
|
457
|
+
|
|
458
|
+
if (!isRange) {
|
|
459
|
+
writeEndpoint("start", date)
|
|
460
|
+
|
|
461
|
+
dismiss()
|
|
462
|
+
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const isStartingOver =
|
|
467
|
+
range.start === null || range.end !== null
|
|
468
|
+
|
|
469
|
+
if (isStartingOver) {
|
|
470
|
+
setTexts((previous) => ({ ...previous, end: "" }))
|
|
471
|
+
|
|
472
|
+
setRange({ end: null, start: toIsoDate(date) })
|
|
473
|
+
|
|
474
|
+
setTexts((previous) => ({
|
|
475
|
+
...previous,
|
|
476
|
+
start: toDisplayText(toIsoDate(date)),
|
|
477
|
+
}))
|
|
478
|
+
|
|
479
|
+
setActiveEndpoint("end")
|
|
480
|
+
|
|
481
|
+
return
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
writeEndpoint("end", date)
|
|
485
|
+
|
|
486
|
+
dismiss()
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const handleInputKeyDown = (
|
|
490
|
+
keyEvent: ReactKeyboardEvent<HTMLInputElement>,
|
|
491
|
+
endpoint: Endpoint,
|
|
492
|
+
) => {
|
|
493
|
+
if (keyEvent.key === "ArrowDown") {
|
|
494
|
+
keyEvent.preventDefault()
|
|
495
|
+
|
|
496
|
+
show()
|
|
497
|
+
|
|
498
|
+
setIsGridFocused(true)
|
|
499
|
+
|
|
500
|
+
return
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (keyEvent.key === "Enter") {
|
|
504
|
+
keyEvent.preventDefault()
|
|
505
|
+
|
|
506
|
+
commitText(endpoint)
|
|
507
|
+
|
|
508
|
+
if (isVisible) {
|
|
509
|
+
dismiss()
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (keyEvent.key === "Escape" && isVisible) {
|
|
516
|
+
keyEvent.preventDefault()
|
|
517
|
+
|
|
518
|
+
dismiss()
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const selectedDates = [
|
|
523
|
+
parseIsoDate(range.start),
|
|
524
|
+
parseIsoDate(range.end),
|
|
525
|
+
].filter((one) => one !== null)
|
|
526
|
+
|
|
527
|
+
const rangeStartDate = parseIsoDate(range.start)
|
|
528
|
+
|
|
529
|
+
const previewEnd = isRange
|
|
530
|
+
? (parseIsoDate(range.end) ??
|
|
531
|
+
(rangeStartDate === null
|
|
532
|
+
? null
|
|
533
|
+
: (hoveredDate ??
|
|
534
|
+
(comparePlainDates(focusedDate, rangeStartDate) >
|
|
535
|
+
0
|
|
536
|
+
? focusedDate
|
|
537
|
+
: null))))
|
|
538
|
+
: null
|
|
539
|
+
|
|
540
|
+
const secondMonth = addMonths(visibleMonth, 1)
|
|
541
|
+
|
|
542
|
+
const monthLabel = (month: PlainDate) =>
|
|
543
|
+
formatPlainDate(month, {
|
|
544
|
+
locale,
|
|
545
|
+
month: "long",
|
|
546
|
+
year: "numeric",
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
const echoText =
|
|
550
|
+
parsed.kind === "date"
|
|
551
|
+
? formatPlainDate(parsed.date, {
|
|
552
|
+
dateStyle: "full",
|
|
553
|
+
locale,
|
|
554
|
+
})
|
|
555
|
+
: parsed.kind === "unparsed"
|
|
556
|
+
? parsed.reason
|
|
557
|
+
: ""
|
|
558
|
+
|
|
559
|
+
const isInvalid = parsed.kind === "unparsed"
|
|
560
|
+
|
|
561
|
+
const renderInput = (endpoint: Endpoint) => {
|
|
562
|
+
const ownProps = {
|
|
563
|
+
"aria-controls": isVisible ? panelId : undefined,
|
|
564
|
+
"aria-describedby": echoId,
|
|
565
|
+
"aria-expanded": isVisible,
|
|
566
|
+
"aria-haspopup": "dialog" as const,
|
|
567
|
+
"aria-invalid":
|
|
568
|
+
activeEndpoint === endpoint && isInvalid
|
|
569
|
+
? true
|
|
570
|
+
: undefined,
|
|
571
|
+
"aria-label": isRange
|
|
572
|
+
? `${label} ${endpoint}`
|
|
573
|
+
: label,
|
|
574
|
+
autoComplete: "off",
|
|
575
|
+
className: toClassName(
|
|
576
|
+
INPUT_CLASS,
|
|
577
|
+
CONTROL_SIZE_CLASS[size],
|
|
578
|
+
FOCUS_RING_CLASS,
|
|
579
|
+
DISABLED_CLASS,
|
|
580
|
+
),
|
|
581
|
+
disabled: isDisabled,
|
|
582
|
+
id: `${baseId}-${endpoint}`,
|
|
583
|
+
// A date is digits far more often than it is words, and a
|
|
584
|
+
// numeric-leaning soft keyboard with letters still available is
|
|
585
|
+
// what lets `8/19` and `next fri` both be typed on a phone.
|
|
586
|
+
inputMode: "text" as const,
|
|
587
|
+
onChange: (changeEvent: {
|
|
588
|
+
currentTarget: { value: string }
|
|
589
|
+
}) => {
|
|
590
|
+
handleTextChange(
|
|
591
|
+
endpoint,
|
|
592
|
+
changeEvent.currentTarget.value,
|
|
593
|
+
)
|
|
594
|
+
},
|
|
595
|
+
onClick: () => {
|
|
596
|
+
setActiveEndpoint(endpoint)
|
|
597
|
+
|
|
598
|
+
show()
|
|
599
|
+
},
|
|
600
|
+
onFocus: () => {
|
|
601
|
+
setActiveEndpoint(endpoint)
|
|
602
|
+
},
|
|
603
|
+
onKeyDown: (
|
|
604
|
+
keyEvent: ReactKeyboardEvent<HTMLInputElement>,
|
|
605
|
+
) => {
|
|
606
|
+
handleInputKeyDown(keyEvent, endpoint)
|
|
607
|
+
},
|
|
608
|
+
placeholder,
|
|
609
|
+
ref: (element: HTMLInputElement | null) => {
|
|
610
|
+
if (element) {
|
|
611
|
+
inputElements.current.set(endpoint, element)
|
|
612
|
+
} else {
|
|
613
|
+
inputElements.current.delete(endpoint)
|
|
614
|
+
}
|
|
615
|
+
},
|
|
616
|
+
role: "combobox" as const,
|
|
617
|
+
type: "text" as const,
|
|
618
|
+
value: texts[endpoint],
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return (
|
|
622
|
+
<input
|
|
623
|
+
// The `Field` above clones `id`, `aria-describedby`,
|
|
624
|
+
// `aria-invalid` and `required` onto this component; they
|
|
625
|
+
// belong on the input, not on the wrapper, and only on the
|
|
626
|
+
// first one — a `<label for>` names one control.
|
|
627
|
+
{...(endpoint === "start"
|
|
628
|
+
? mergeSlotProps(receivedSlotProps, ownProps)
|
|
629
|
+
: ownProps)}
|
|
630
|
+
key={endpoint}
|
|
631
|
+
/>
|
|
632
|
+
)
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const renderGrid = (month: PlainDate) => (
|
|
636
|
+
<DateGrid
|
|
637
|
+
firstDayOfWeek={resolvedFirstDayOfWeek}
|
|
638
|
+
focusedDate={focusedDate}
|
|
639
|
+
gridMonth={month}
|
|
640
|
+
isGridFocused={isGridFocused}
|
|
641
|
+
isRange={isRange}
|
|
642
|
+
key={`${month.year}-${month.month}`}
|
|
643
|
+
label={monthLabel(month)}
|
|
644
|
+
locale={locale}
|
|
645
|
+
maximum={maximum}
|
|
646
|
+
minimum={minimum}
|
|
647
|
+
onFocusDate={(date) => {
|
|
648
|
+
setFocusedDate(date)
|
|
649
|
+
|
|
650
|
+
setVisibleMonth(date)
|
|
651
|
+
}}
|
|
652
|
+
onHoverDate={setHoveredDate}
|
|
653
|
+
onSelectDate={chooseDate}
|
|
654
|
+
previewEnd={previewEnd}
|
|
655
|
+
rangeStart={rangeStartDate}
|
|
656
|
+
selectedDates={selectedDates}
|
|
657
|
+
today={todayDate}
|
|
658
|
+
/>
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
return (
|
|
662
|
+
// The overlay anchors to THIS box, not to the input row, so the echo
|
|
663
|
+
// line below stays visible while the calendar is open. It is the line
|
|
664
|
+
// that says what a typed value resolved to; a panel covering it turns
|
|
665
|
+
// the one anti-guess mechanism off at exactly the moment it matters.
|
|
666
|
+
<div
|
|
667
|
+
className={toClassName(
|
|
668
|
+
"flex flex-col gap-1",
|
|
669
|
+
className,
|
|
670
|
+
)}
|
|
671
|
+
ref={anchorElement}
|
|
672
|
+
>
|
|
673
|
+
<div className="flex items-center gap-2">
|
|
674
|
+
{renderInput("start")}
|
|
675
|
+
|
|
676
|
+
{isRange ? (
|
|
677
|
+
<>
|
|
678
|
+
<span
|
|
679
|
+
aria-hidden="true"
|
|
680
|
+
className="shrink-0 text-content-secondary text-sm"
|
|
681
|
+
>
|
|
682
|
+
to
|
|
683
|
+
</span>
|
|
684
|
+
|
|
685
|
+
{renderInput("end")}
|
|
686
|
+
</>
|
|
687
|
+
) : null}
|
|
688
|
+
</div>
|
|
689
|
+
|
|
690
|
+
{/*
|
|
691
|
+
The anti-silent-guess mechanism, and the reason this component
|
|
692
|
+
is allowed to accept `8/19` at all. It is a live region so the
|
|
693
|
+
resolution is announced while typing, and it is wired into the
|
|
694
|
+
input's `aria-describedby` so it is read again on focus. A
|
|
695
|
+
parser this permissive without this line would be a field that
|
|
696
|
+
quietly files things in the wrong month.
|
|
697
|
+
*/}
|
|
698
|
+
<p
|
|
699
|
+
aria-live="polite"
|
|
700
|
+
className={toClassName(
|
|
701
|
+
"min-h-5 text-sm",
|
|
702
|
+
isInvalid
|
|
703
|
+
? "text-intent-danger-content"
|
|
704
|
+
: "text-content-secondary",
|
|
705
|
+
)}
|
|
706
|
+
id={echoId}
|
|
707
|
+
role="status"
|
|
708
|
+
>
|
|
709
|
+
{echoText}
|
|
710
|
+
</p>
|
|
711
|
+
|
|
712
|
+
{isVisible ? (
|
|
713
|
+
<FloatingPortal>
|
|
714
|
+
<FloatingFocusManager
|
|
715
|
+
context={context}
|
|
716
|
+
// Focus stays where the user is typing until they ask for
|
|
717
|
+
// the grid with ArrowDown or a click. A calendar that
|
|
718
|
+
// steals the caret on open is a calendar you cannot type
|
|
719
|
+
// into, which is the whole point of this field.
|
|
720
|
+
disabled
|
|
721
|
+
modal={false}
|
|
722
|
+
>
|
|
723
|
+
<div
|
|
724
|
+
{...floatingProps}
|
|
725
|
+
aria-label={`${label} calendar`}
|
|
726
|
+
className={toClassName(
|
|
727
|
+
PANEL_SURFACE_CLASS,
|
|
728
|
+
// `overflow-hidden`, because `maxHeightPx` reaches the
|
|
729
|
+
// panel as a `max-height` from the `size` middleware and
|
|
730
|
+
// nothing else clips: on a phone the preset row simply
|
|
731
|
+
// spilled out past the panel's own rounded edge and sat
|
|
732
|
+
// on the page behind it. The *months* are what scrolls
|
|
733
|
+
// (below), so the month nav and the shortcut row stay
|
|
734
|
+
// put — a preset you have to scroll to find is not a
|
|
735
|
+
// shortcut, and it is the fastest control here on a
|
|
736
|
+
// phone.
|
|
737
|
+
"@container z-[var(--layer-modal)] flex flex-col gap-3 overflow-hidden p-3 text-content-primary",
|
|
738
|
+
isRange ? "w-[41rem]" : "w-80",
|
|
739
|
+
)}
|
|
740
|
+
id={panelId}
|
|
741
|
+
ref={setFloating}
|
|
742
|
+
role="dialog"
|
|
743
|
+
style={floatingStyles}
|
|
744
|
+
>
|
|
745
|
+
<div className="flex items-center justify-between gap-2">
|
|
746
|
+
<IconButton
|
|
747
|
+
appearance="ghost"
|
|
748
|
+
label="Previous month"
|
|
749
|
+
onClick={() => {
|
|
750
|
+
setVisibleMonth(
|
|
751
|
+
addMonths(visibleMonth, -1),
|
|
752
|
+
)
|
|
753
|
+
}}
|
|
754
|
+
size="sm"
|
|
755
|
+
type="button"
|
|
756
|
+
>
|
|
757
|
+
{CHEVRON_PREVIOUS}
|
|
758
|
+
</IconButton>
|
|
759
|
+
|
|
760
|
+
<span
|
|
761
|
+
// Announced on change, so paging with the buttons
|
|
762
|
+
// says which month you landed on rather than
|
|
763
|
+
// silently redrawing 35 cells.
|
|
764
|
+
aria-live="polite"
|
|
765
|
+
className="font-medium text-sm"
|
|
766
|
+
>
|
|
767
|
+
{isRange
|
|
768
|
+
? `${monthLabel(visibleMonth)} – ${monthLabel(secondMonth)}`
|
|
769
|
+
: monthLabel(visibleMonth)}
|
|
770
|
+
</span>
|
|
771
|
+
|
|
772
|
+
<IconButton
|
|
773
|
+
appearance="ghost"
|
|
774
|
+
label="Next month"
|
|
775
|
+
onClick={() => {
|
|
776
|
+
setVisibleMonth(
|
|
777
|
+
addMonths(visibleMonth, 1),
|
|
778
|
+
)
|
|
779
|
+
}}
|
|
780
|
+
size="sm"
|
|
781
|
+
type="button"
|
|
782
|
+
>
|
|
783
|
+
{CHEVRON_NEXT}
|
|
784
|
+
</IconButton>
|
|
785
|
+
</div>
|
|
786
|
+
|
|
787
|
+
<div className="charcuterie-scrollbar flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto cq-sm:flex-row">
|
|
788
|
+
{renderGrid(visibleMonth)}
|
|
789
|
+
|
|
790
|
+
{isRange ? renderGrid(secondMonth) : null}
|
|
791
|
+
</div>
|
|
792
|
+
|
|
793
|
+
{presets.length > 0 ||
|
|
794
|
+
range.start !== null ? (
|
|
795
|
+
<div className="flex flex-wrap items-center gap-1 border-border-subtle border-t pt-2">
|
|
796
|
+
{presets.map((preset) => {
|
|
797
|
+
const presetDate = addDays(
|
|
798
|
+
todayDate,
|
|
799
|
+
preset.days,
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
return (
|
|
803
|
+
<Button
|
|
804
|
+
appearance="ghost"
|
|
805
|
+
isDisabled={
|
|
806
|
+
!getIsWithinRange(
|
|
807
|
+
presetDate,
|
|
808
|
+
minimum,
|
|
809
|
+
maximum,
|
|
810
|
+
)
|
|
811
|
+
}
|
|
812
|
+
key={preset.label}
|
|
813
|
+
onClick={() => {
|
|
814
|
+
setVisibleMonth(presetDate)
|
|
815
|
+
|
|
816
|
+
chooseDate(presetDate)
|
|
817
|
+
}}
|
|
818
|
+
size="sm"
|
|
819
|
+
type="button"
|
|
820
|
+
>
|
|
821
|
+
{preset.label}
|
|
822
|
+
</Button>
|
|
823
|
+
)
|
|
824
|
+
})}
|
|
825
|
+
|
|
826
|
+
{range.start === null ? null : (
|
|
827
|
+
<Button
|
|
828
|
+
appearance="ghost"
|
|
829
|
+
className="ms-auto"
|
|
830
|
+
intent="danger"
|
|
831
|
+
onClick={() => {
|
|
832
|
+
setTexts({ end: "", start: "" })
|
|
833
|
+
|
|
834
|
+
report({ end: null, start: null })
|
|
835
|
+
|
|
836
|
+
setActiveEndpoint("start")
|
|
837
|
+
}}
|
|
838
|
+
size="sm"
|
|
839
|
+
type="button"
|
|
840
|
+
>
|
|
841
|
+
Clear
|
|
842
|
+
</Button>
|
|
843
|
+
)}
|
|
844
|
+
</div>
|
|
845
|
+
) : null}
|
|
846
|
+
</div>
|
|
847
|
+
</FloatingFocusManager>
|
|
848
|
+
</FloatingPortal>
|
|
849
|
+
) : null}
|
|
850
|
+
</div>
|
|
851
|
+
)
|
|
852
|
+
}
|