@eifi1/ui-kit 0.8.0 → 0.9.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 +4 -0
- package/dist/components/amount-input.d.ts +2 -0
- package/dist/components/calculator.d.ts +2 -0
- package/dist/components/chip.d.ts +27 -5
- package/dist/components/chip.js +6 -2
- package/dist/components/chip.js.map +1 -1
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +122 -91
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +69 -3
- package/dist/components/date-picker.js +152 -65
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/disclosure.d.ts +11 -2
- package/dist/components/disclosure.js +10 -4
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/floating-panel.d.ts +29 -2
- package/dist/components/floating-panel.js +16 -2
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/full-bleed-dialog.js +3 -1
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/number-field.d.ts +2 -0
- package/dist/components/number-input.d.ts +2 -0
- package/dist/components/numpad-sheet.d.ts +2 -0
- package/dist/components/progress-bar.d.ts +16 -2
- package/dist/components/progress-bar.js +4 -2
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/treemap.js +4 -1
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/ui.d.ts +44 -14
- package/dist/components/ui.js +28 -22
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/{data-table-filters-CF1PXqjQ.d.ts → data-table-filters-Dh9uF_S-.d.ts} +33 -2
- package/dist/data-table.d.ts +1 -1
- package/dist/hooks/use-overlay-history.js +84 -4
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/i18n/defaults.d.ts +2 -0
- package/dist/i18n/defaults.js +2 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +3 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +2 -0
- package/dist/i18n/locales/de-CH.d.ts +2 -0
- package/dist/i18n/locales/de-informal.d.ts +2 -0
- package/dist/i18n/locales/de.d.ts +2 -0
- package/dist/i18n/locales/de.js +10 -0
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +2 -0
- package/dist/i18n/locales/es.js +10 -0
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +2 -0
- package/dist/i18n/locales/fr.js +10 -0
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +2 -0
- package/dist/i18n/locales/hu.js +10 -0
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +2 -0
- package/dist/i18n/locales/it.js +10 -0
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +2 -0
- package/dist/i18n/locales/zh.js +10 -0
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/search/command-palette.d.ts +49 -1
- package/dist/search/command-palette.js +132 -29
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.d.ts +141 -0
- package/dist/search/global-search.js +277 -0
- package/dist/search/global-search.js.map +1 -0
- package/dist/search/search-index.d.ts +105 -0
- package/dist/search/search-index.js +142 -0
- package/dist/search/search-index.js.map +1 -0
- package/dist/search.d.ts +2 -0
- package/dist/search.js +2 -0
- package/dist/search.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +10 -1
- package/dist/wizard/stepper-nav.js +2 -1
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/package.json +12 -3
- package/src/components/chip.tsx +36 -6
- package/src/components/data-table.tsx +81 -5
- package/src/components/date-picker.tsx +328 -117
- package/src/components/disclosure.tsx +24 -8
- package/src/components/floating-panel.tsx +46 -2
- package/src/components/full-bleed-dialog.tsx +7 -2
- package/src/components/progress-bar.tsx +27 -3
- package/src/components/treemap.tsx +3 -0
- package/src/components/ui.tsx +95 -37
- package/src/hooks/use-overlay-history.ts +181 -4
- package/src/i18n/defaults.ts +2 -0
- package/src/i18n/kit-labels.tsx +2 -0
- package/src/i18n/locales/de.ts +10 -0
- package/src/i18n/locales/es.ts +10 -0
- package/src/i18n/locales/fr.ts +10 -0
- package/src/i18n/locales/hu.ts +10 -0
- package/src/i18n/locales/it.ts +10 -0
- package/src/i18n/locales/zh.ts +10 -0
- package/src/index.ts +4 -0
- package/src/search/command-palette.tsx +200 -27
- package/src/search/global-search.tsx +484 -0
- package/src/search/search-index.ts +295 -0
- package/src/search.ts +2 -0
- package/src/wizard/stepper-nav.tsx +11 -1
|
@@ -6,13 +6,16 @@ import { dirOf, type Direction } from "../lib/direction";
|
|
|
6
6
|
import { addDaysIso, parseIsoDate } from "../lib/dates";
|
|
7
7
|
import {
|
|
8
8
|
DEFAULT_DATE_PICKER_LABELS,
|
|
9
|
+
DEFAULT_PICKER_SHEET_LABELS,
|
|
9
10
|
useKitLabels,
|
|
10
11
|
useKitLocale,
|
|
11
12
|
type DatePickerLabels,
|
|
12
13
|
} from "../i18n/kit-labels";
|
|
13
14
|
import { splitTriggerAria } from "./trigger-aria";
|
|
14
15
|
import type { TriggerAria } from "./trigger-aria";
|
|
15
|
-
import { Button, FieldLabel, FIELD_BASE, FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID } from "./ui";
|
|
16
|
+
import { Button, FieldLabel, FIELD_BASE, FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID, PHONE_QUERY } from "./ui";
|
|
17
|
+
import { FullBleedDialog } from "./full-bleed-dialog";
|
|
18
|
+
import { useMediaQuery } from "../hooks/use-media-query";
|
|
16
19
|
import { MiniCalendar, type MiniCalendarProps } from "./mini-calendar";
|
|
17
20
|
import { Popover } from "./popover";
|
|
18
21
|
import { Tooltip } from "./tooltip";
|
|
@@ -66,6 +69,51 @@ function formatDate(iso: string, locale: string | undefined, options?: Intl.Date
|
|
|
66
69
|
return parseIsoDate(iso)?.toLocaleDateString(locale, options) ?? "";
|
|
67
70
|
}
|
|
68
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Everything the kit's own trigger button carries, handed to a custom one
|
|
74
|
+
* ({@link DateRangePickerProps.renderTrigger}) to spread onto its `<button>`. Spread
|
|
75
|
+
* it WHOLE: the ref is how focus comes back when the panel closes, the id pair and
|
|
76
|
+
* `aria-labelledby` are the 0.5.1 naming (an external `<label htmlFor>`, a form
|
|
77
|
+
* library's `aria-describedby`), and `role`/`aria-haspopup`/`aria-controls`/
|
|
78
|
+
* `aria-expanded` are what tell a screen reader this opens a calendar and whether it
|
|
79
|
+
* is open. `className` is the kit's field look (42px with a label, `pe-9` for the
|
|
80
|
+
* glyph the field draws at its end) — merge yours after it with `cn`.
|
|
81
|
+
*/
|
|
82
|
+
export interface DateTriggerAttributes {
|
|
83
|
+
ref: RefObject<HTMLButtonElement | null>;
|
|
84
|
+
type: "button";
|
|
85
|
+
id?: string;
|
|
86
|
+
role: "combobox";
|
|
87
|
+
"aria-haspopup": "dialog";
|
|
88
|
+
"aria-controls": string;
|
|
89
|
+
"aria-expanded": boolean;
|
|
90
|
+
"aria-labelledby"?: string;
|
|
91
|
+
"aria-label"?: string;
|
|
92
|
+
"aria-describedby"?: string;
|
|
93
|
+
"aria-invalid"?: true;
|
|
94
|
+
disabled?: boolean;
|
|
95
|
+
onClick: () => void;
|
|
96
|
+
className: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** A custom trigger, as `DateField` calls it; each picker adapts it to its own props. */
|
|
100
|
+
type TriggerRenderer = (attrs: DateTriggerAttributes, valueId: string) => ReactNode;
|
|
101
|
+
|
|
102
|
+
/** A caller's trigger, as an element of its own: the attributes carry the trigger's
|
|
103
|
+
* ref, and a ref is handed to a child as a prop, not passed to a function in the
|
|
104
|
+
* middle of the parent's render. */
|
|
105
|
+
function CustomTrigger({
|
|
106
|
+
render,
|
|
107
|
+
attrs,
|
|
108
|
+
valueId,
|
|
109
|
+
}: {
|
|
110
|
+
render: TriggerRenderer;
|
|
111
|
+
attrs: DateTriggerAttributes;
|
|
112
|
+
valueId: string;
|
|
113
|
+
}) {
|
|
114
|
+
return render(attrs, valueId);
|
|
115
|
+
}
|
|
116
|
+
|
|
69
117
|
/**
|
|
70
118
|
* The field-shaped button that opens the calendar.
|
|
71
119
|
*
|
|
@@ -86,9 +134,12 @@ function DateFieldTrigger({
|
|
|
86
134
|
invalid,
|
|
87
135
|
panelId,
|
|
88
136
|
aria,
|
|
137
|
+
renderTrigger,
|
|
89
138
|
}: {
|
|
90
139
|
/** The caller's naming/description attributes, routed here from the field. */
|
|
91
140
|
aria: TriggerAria;
|
|
141
|
+
/** Draw the button yourself, with these attributes — see {@link DateTriggerAttributes}. */
|
|
142
|
+
renderTrigger?: TriggerRenderer;
|
|
92
143
|
open: boolean;
|
|
93
144
|
toggle: () => void;
|
|
94
145
|
triggerRef: RefObject<HTMLButtonElement | null>;
|
|
@@ -123,47 +174,55 @@ function DateFieldTrigger({
|
|
|
123
174
|
// is this trigger, every time, however the panel was opened.
|
|
124
175
|
}, [open, triggerRef]);
|
|
125
176
|
|
|
177
|
+
// One attribute object, worn either by the kit's own button below or by a caller's
|
|
178
|
+
// (`renderTrigger`) — so a custom trigger cannot quietly fall out of step with the
|
|
179
|
+
// naming and state this one announces.
|
|
180
|
+
const attrs: DateTriggerAttributes = {
|
|
181
|
+
ref: triggerRef,
|
|
182
|
+
type: "button",
|
|
183
|
+
disabled,
|
|
184
|
+
onClick: toggle,
|
|
185
|
+
// The name has to carry the VALUE. A bare `aria-label={label}` — which is what
|
|
186
|
+
// this was — replaces the button's text outright, so the one field in the form
|
|
187
|
+
// whose entire job is to show a date announced "Due date" and stopped there,
|
|
188
|
+
// and a screen-reader user could not read back what they had picked without
|
|
189
|
+
// opening the calendar and hunting for the selected day.
|
|
190
|
+
//
|
|
191
|
+
// Two ids rather than one composed string (`common.fieldValue`): "label: value"
|
|
192
|
+
// is a sentence, and a name reference is spoken as the two texts in order with
|
|
193
|
+
// no punctuation to translate at all. The hidden twin of the FieldLabel lives
|
|
194
|
+
// in DateField, next to the label it copies.
|
|
195
|
+
id: aria.id,
|
|
196
|
+
"aria-labelledby": aria["aria-label"] && !aria["aria-labelledby"] ? undefined : labelledBy,
|
|
197
|
+
"aria-label": aria["aria-label"],
|
|
198
|
+
"aria-describedby": aria["aria-describedby"],
|
|
199
|
+
// `role="combobox"` on a button that opens a calendar is the APG date-picker
|
|
200
|
+
// shape, and it is what makes the next two lines legal: `button` supports
|
|
201
|
+
// neither `aria-expanded` nor `aria-invalid`, so the previous markup set an
|
|
202
|
+
// invalid state that announced nothing (ESLint's `role-supports-aria-props`).
|
|
203
|
+
//
|
|
204
|
+
// `aria-haspopup="dialog"` is now honest — it was not when this comment first
|
|
205
|
+
// said so. `Popover` gained a real `role="dialog"` in the same wave, so the
|
|
206
|
+
// trigger's promise and the panel's role finally agree. The phone sheet is a
|
|
207
|
+
// `role="dialog"` too, and carries the same id.
|
|
208
|
+
role: "combobox",
|
|
209
|
+
"aria-haspopup": "dialog",
|
|
210
|
+
"aria-controls": panelId,
|
|
211
|
+
"aria-expanded": open,
|
|
212
|
+
"aria-invalid":
|
|
213
|
+
invalid || aria["aria-invalid"] === true || aria["aria-invalid"] === "true" || undefined,
|
|
214
|
+
className: cn(
|
|
215
|
+
FIELD_TRIGGER,
|
|
216
|
+
"pe-9",
|
|
217
|
+
padded && FIELD_FLOATING_PAD,
|
|
218
|
+
disabled && "cursor-not-allowed opacity-50",
|
|
219
|
+
invalid && FIELD_INVALID,
|
|
220
|
+
),
|
|
221
|
+
};
|
|
222
|
+
if (renderTrigger) return <CustomTrigger render={renderTrigger} attrs={attrs} valueId={valueId} />;
|
|
223
|
+
|
|
126
224
|
return (
|
|
127
|
-
<button
|
|
128
|
-
ref={triggerRef}
|
|
129
|
-
type="button"
|
|
130
|
-
disabled={disabled}
|
|
131
|
-
onClick={toggle}
|
|
132
|
-
// The name has to carry the VALUE. A bare `aria-label={label}` — which is what
|
|
133
|
-
// this was — replaces the button's text outright, so the one field in the form
|
|
134
|
-
// whose entire job is to show a date announced "Due date" and stopped there,
|
|
135
|
-
// and a screen-reader user could not read back what they had picked without
|
|
136
|
-
// opening the calendar and hunting for the selected day.
|
|
137
|
-
//
|
|
138
|
-
// Two ids rather than one composed string (`common.fieldValue`): "label: value"
|
|
139
|
-
// is a sentence, and a name reference is spoken as the two texts in order with
|
|
140
|
-
// no punctuation to translate at all. The hidden twin of the FieldLabel lives
|
|
141
|
-
// in DateField, next to the label it copies.
|
|
142
|
-
id={aria.id}
|
|
143
|
-
aria-labelledby={aria["aria-label"] && !aria["aria-labelledby"] ? undefined : labelledBy}
|
|
144
|
-
aria-label={aria["aria-label"]}
|
|
145
|
-
aria-describedby={aria["aria-describedby"]}
|
|
146
|
-
// `role="combobox"` on a button that opens a calendar is the APG date-picker
|
|
147
|
-
// shape, and it is what makes the next two lines legal: `button` supports
|
|
148
|
-
// neither `aria-expanded` nor `aria-invalid`, so the previous markup set an
|
|
149
|
-
// invalid state that announced nothing (ESLint's `role-supports-aria-props`).
|
|
150
|
-
//
|
|
151
|
-
// `aria-haspopup="dialog"` is now honest — it was not when this comment first
|
|
152
|
-
// said so. `Popover` gained a real `role="dialog"` in the same wave, so the
|
|
153
|
-
// trigger's promise and the panel's role finally agree.
|
|
154
|
-
role="combobox"
|
|
155
|
-
aria-haspopup="dialog"
|
|
156
|
-
aria-controls={panelId}
|
|
157
|
-
aria-expanded={open}
|
|
158
|
-
aria-invalid={invalid || aria["aria-invalid"] === true || aria["aria-invalid"] === "true" || undefined}
|
|
159
|
-
className={cn(
|
|
160
|
-
FIELD_TRIGGER,
|
|
161
|
-
"pe-9",
|
|
162
|
-
padded && FIELD_FLOATING_PAD,
|
|
163
|
-
disabled && "cursor-not-allowed opacity-50",
|
|
164
|
-
invalid && FIELD_INVALID,
|
|
165
|
-
)}
|
|
166
|
-
>
|
|
225
|
+
<button {...attrs}>
|
|
167
226
|
<span id={valueId} className={cn("truncate", !hasValue && "text-[var(--text-placeholder)]")}>
|
|
168
227
|
{/* `|| " "` (a NON-BREAKING space) — triggerText is "" when there is no value and no
|
|
169
228
|
placeholder was passed. An empty span has no line box, so the
|
|
@@ -184,6 +243,24 @@ function DateFieldTrigger({
|
|
|
184
243
|
);
|
|
185
244
|
}
|
|
186
245
|
|
|
246
|
+
/**
|
|
247
|
+
* How a panel's body and its action row are put on screen — the popover stacks them,
|
|
248
|
+
* the phone sheet pins the actions in its footer. Handed to the panel rather than
|
|
249
|
+
* applied around it because the actions belong to the panel's own draft state
|
|
250
|
+
* (`RangePanel`), while the frame they sit in belongs to the field.
|
|
251
|
+
*/
|
|
252
|
+
type PanelFrame = (body: ReactNode, actions?: ReactNode) => ReactNode;
|
|
253
|
+
|
|
254
|
+
const popoverFrame: PanelFrame = (body, actions) =>
|
|
255
|
+
actions ? (
|
|
256
|
+
<div className="flex flex-col gap-2">
|
|
257
|
+
{body}
|
|
258
|
+
<div className="flex items-center justify-end gap-2 border-t border-[var(--border)] pt-2">{actions}</div>
|
|
259
|
+
</div>
|
|
260
|
+
) : (
|
|
261
|
+
body
|
|
262
|
+
);
|
|
263
|
+
|
|
187
264
|
/** A field-styled trigger that opens a portalled MiniCalendar. Shared by the
|
|
188
265
|
* single- and range-date pickers below. */
|
|
189
266
|
function DateField({
|
|
@@ -198,6 +275,9 @@ function DateField({
|
|
|
198
275
|
width,
|
|
199
276
|
onClear,
|
|
200
277
|
invalid,
|
|
278
|
+
sheet = false,
|
|
279
|
+
sheetBackCloses = true,
|
|
280
|
+
renderTrigger,
|
|
201
281
|
children,
|
|
202
282
|
...rest
|
|
203
283
|
}: Omit<ComponentPropsWithoutRef<"div">, "children"> & {
|
|
@@ -215,7 +295,13 @@ function DateField({
|
|
|
215
295
|
/** Popover panel width; omit for the default (a bare calendar). */
|
|
216
296
|
width?: number;
|
|
217
297
|
onClear: () => void;
|
|
218
|
-
|
|
298
|
+
/** Open as a full-screen sheet instead of the popover — the picker decides when
|
|
299
|
+
* (below {@link PHONE_QUERY}). */
|
|
300
|
+
sheet?: boolean;
|
|
301
|
+
/** The sheet's `FullBleedDialog backCloses`. */
|
|
302
|
+
sheetBackCloses?: boolean;
|
|
303
|
+
renderTrigger?: TriggerRenderer;
|
|
304
|
+
children: (close: () => void, frame: PanelFrame) => ReactNode;
|
|
219
305
|
}) {
|
|
220
306
|
const showClear = Boolean(clearable && hasValue && !disabled);
|
|
221
307
|
const [aria, wrapperRest] = splitTriggerAria(rest);
|
|
@@ -233,6 +319,83 @@ function DateField({
|
|
|
233
319
|
// its own interactive "?" hint in it, and naming a button with that reads the hint
|
|
234
320
|
// out as part of the field's name.
|
|
235
321
|
const named = typeof label === "string";
|
|
322
|
+
// The sheet's open flag. Only the phone branch owns one: on desktop `Popover` owns
|
|
323
|
+
// it, so the two can never disagree about what is on screen.
|
|
324
|
+
const [sheetOpen, setSheetOpen] = useState(false);
|
|
325
|
+
const sheetTriggerRef = useRef<HTMLButtonElement>(null);
|
|
326
|
+
const closeSheet = () => setSheetOpen(false);
|
|
327
|
+
const sheetLabels = useKitLabels("pickerSheet", DEFAULT_PICKER_SHEET_LABELS);
|
|
328
|
+
const sheetFrame: PanelFrame = (body, actions) => (
|
|
329
|
+
<FullBleedDialog
|
|
330
|
+
open
|
|
331
|
+
onClose={closeSheet}
|
|
332
|
+
// The same id the popover panel wears, so the trigger's `aria-controls` holds in
|
|
333
|
+
// both presentations; named the way the popover is.
|
|
334
|
+
id={panelId}
|
|
335
|
+
aria-label={panelLabel}
|
|
336
|
+
header={panelLabel}
|
|
337
|
+
// `pickerSheet.close`: the one "close this full-screen picker" string the kit
|
|
338
|
+
// already translates, rather than a second key saying the same thing.
|
|
339
|
+
closeLabel={sheetLabels.close}
|
|
340
|
+
backCloses={sheetBackCloses}
|
|
341
|
+
dir={dir}
|
|
342
|
+
// Portalled to <body>, the sheet is not inside the field's wrapper, so a
|
|
343
|
+
// document-level "outside click" listener — a Popover or dropdown this field
|
|
344
|
+
// sits in — would read a tap on a day as outside and unmount the cell before
|
|
345
|
+
// its click (Keksdose dev#477, the fix `PickerSheet` carries).
|
|
346
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
347
|
+
footer={actions}
|
|
348
|
+
>
|
|
349
|
+
{body}
|
|
350
|
+
</FullBleedDialog>
|
|
351
|
+
);
|
|
352
|
+
const fieldTrigger = ({
|
|
353
|
+
open,
|
|
354
|
+
toggle,
|
|
355
|
+
ref,
|
|
356
|
+
}: {
|
|
357
|
+
open: boolean;
|
|
358
|
+
toggle: () => void;
|
|
359
|
+
ref: RefObject<HTMLButtonElement | null>;
|
|
360
|
+
}) => (
|
|
361
|
+
<DateFieldTrigger
|
|
362
|
+
open={open}
|
|
363
|
+
toggle={() => {
|
|
364
|
+
setDir(dirOf(rootRef.current));
|
|
365
|
+
toggle();
|
|
366
|
+
}}
|
|
367
|
+
triggerRef={ref}
|
|
368
|
+
triggerText={triggerText}
|
|
369
|
+
hasValue={hasValue}
|
|
370
|
+
// ALWAYS a name reference, never `undefined`. A `<button>` takes its
|
|
371
|
+
// accessible name from its contents, so an unlabelled trigger used to
|
|
372
|
+
// announce its own date text for free. `role="combobox"` does not —
|
|
373
|
+
// content is excluded from name computation for that role — so leaving
|
|
374
|
+
// this undefined made an unlabelled date field announce nothing at all.
|
|
375
|
+
// Named: "label, value". Unnamed: the value alone, which is what the
|
|
376
|
+
// button was saying before.
|
|
377
|
+
//
|
|
378
|
+
// A caller's own reference comes first. And when the caller gives the field
|
|
379
|
+
// an `id` (so an external `<label htmlFor>` can point at it), the trigger
|
|
380
|
+
// lists ITSELF first: a self-reference in `aria-labelledby` is resolved
|
|
381
|
+
// from the element's native label (accname 2B → 2D), so "Due date" from
|
|
382
|
+
// that <label> is spoken before the value, instead of being overridden.
|
|
383
|
+
labelledBy={[
|
|
384
|
+
aria["aria-labelledby"],
|
|
385
|
+
named ? labelId : !aria["aria-labelledby"] && aria.id ? aria.id : undefined,
|
|
386
|
+
valueId,
|
|
387
|
+
]
|
|
388
|
+
.filter(Boolean)
|
|
389
|
+
.join(" ")}
|
|
390
|
+
aria={aria}
|
|
391
|
+
panelId={panelId}
|
|
392
|
+
valueId={valueId}
|
|
393
|
+
padded={label !== undefined}
|
|
394
|
+
disabled={disabled}
|
|
395
|
+
invalid={invalid}
|
|
396
|
+
renderTrigger={renderTrigger}
|
|
397
|
+
/>
|
|
398
|
+
);
|
|
236
399
|
return (
|
|
237
400
|
// The caller's attributes land here, on the field's own box — the trigger inside is
|
|
238
401
|
// named by `aria-labelledby` and must keep the id pair it is given.
|
|
@@ -250,54 +413,26 @@ function DateField({
|
|
|
250
413
|
{label}
|
|
251
414
|
</span>
|
|
252
415
|
)}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
// content is excluded from name computation for that role — so leaving
|
|
274
|
-
// this undefined made an unlabelled date field announce nothing at all.
|
|
275
|
-
// Named: "label, value". Unnamed: the value alone, which is what the
|
|
276
|
-
// button was saying before.
|
|
277
|
-
//
|
|
278
|
-
// A caller's own reference comes first. And when the caller gives the field
|
|
279
|
-
// an `id` (so an external `<label htmlFor>` can point at it), the trigger
|
|
280
|
-
// lists ITSELF first: a self-reference in `aria-labelledby` is resolved
|
|
281
|
-
// from the element's native label (accname 2B → 2D), so "Due date" from
|
|
282
|
-
// that <label> is spoken before the value, instead of being overridden.
|
|
283
|
-
labelledBy={[
|
|
284
|
-
aria["aria-labelledby"],
|
|
285
|
-
named ? labelId : !aria["aria-labelledby"] && aria.id ? aria.id : undefined,
|
|
286
|
-
valueId,
|
|
287
|
-
]
|
|
288
|
-
.filter(Boolean)
|
|
289
|
-
.join(" ")}
|
|
290
|
-
aria={aria}
|
|
291
|
-
panelId={panelId}
|
|
292
|
-
valueId={valueId}
|
|
293
|
-
padded={label !== undefined}
|
|
294
|
-
disabled={disabled}
|
|
295
|
-
invalid={invalid}
|
|
296
|
-
/>
|
|
297
|
-
)}
|
|
298
|
-
>
|
|
299
|
-
{(close) => children(close)}
|
|
300
|
-
</Popover>
|
|
416
|
+
{sheet ? (
|
|
417
|
+
<>
|
|
418
|
+
{fieldTrigger({ open: sheetOpen, toggle: () => setSheetOpen((v) => !v), ref: sheetTriggerRef })}
|
|
419
|
+
{/* Mounted only while open, like the popover's children: the panel's draft is
|
|
420
|
+
born with it, so every open starts again from the committed value. */}
|
|
421
|
+
{sheetOpen && children(closeSheet, sheetFrame)}
|
|
422
|
+
</>
|
|
423
|
+
) : (
|
|
424
|
+
<Popover
|
|
425
|
+
width={width}
|
|
426
|
+
panelId={panelId}
|
|
427
|
+
// Named for what it is. Unnamed, it fell back to `popover.panel`, and a date
|
|
428
|
+
// field announced its calendar as "Popover" — in English, in every language.
|
|
429
|
+
labels={{ panel: panelLabel }}
|
|
430
|
+
dir={dir}
|
|
431
|
+
trigger={fieldTrigger}
|
|
432
|
+
>
|
|
433
|
+
{(close) => children(close, popoverFrame)}
|
|
434
|
+
</Popover>
|
|
435
|
+
)}
|
|
301
436
|
{showClear ? (
|
|
302
437
|
<button
|
|
303
438
|
type="button"
|
|
@@ -612,6 +747,45 @@ export interface DateRangePickerProps extends DatePickerBaseProps {
|
|
|
612
747
|
preset?: string | null;
|
|
613
748
|
/** See {@link DateRangeCommit}. Default `"immediate"`. */
|
|
614
749
|
commit?: DateRangeCommit;
|
|
750
|
+
/**
|
|
751
|
+
* Draw the trigger yourself. keksdose's report range field
|
|
752
|
+
* (reports/report-range-field.tsx, "Why this is app-level") is why: its trigger
|
|
753
|
+
* NAMES the active preset ("Last 3 months", the window as a muted suffix) where the
|
|
754
|
+
* kit's shows two dates, sits inside a Tooltip, and is squared off on one side so the
|
|
755
|
+
* granularity control can join it flush at the field's 42px.
|
|
756
|
+
*
|
|
757
|
+
* Spread `props.triggerProps` onto a `<button>` whole and put `props.valueProps` on
|
|
758
|
+
* the element that shows the value — see {@link DateTriggerAttributes} for what each
|
|
759
|
+
* part is for. The field around it (label, hidden name twin, clear/calendar glyph,
|
|
760
|
+
* popover or phone sheet) is unchanged.
|
|
761
|
+
*/
|
|
762
|
+
renderTrigger?: (props: DateRangeTriggerRenderProps) => ReactNode;
|
|
763
|
+
/**
|
|
764
|
+
* Below {@link PHONE_QUERY} the panel opens as a full-screen sheet (`FullBleedDialog`)
|
|
765
|
+
* with presets in a grid above the calendar and Apply pinned in the footer, instead
|
|
766
|
+
* of the 440px popover that hung off a 360px screen (keksdose's report range field,
|
|
767
|
+
* which hand-rolled exactly this). This is its `backCloses`: on by default, so Back
|
|
768
|
+
* dismisses the sheet; off for a caller whose commit rewrites the URL with
|
|
769
|
+
* `replaceState` in the same tick — keksdose's `?preset/from/to` — where the router
|
|
770
|
+
* would overwrite the sheet's history marker.
|
|
771
|
+
*/
|
|
772
|
+
sheetBackCloses?: boolean;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/** What {@link DateRangePickerProps.renderTrigger} is called with. */
|
|
776
|
+
export interface DateRangeTriggerRenderProps {
|
|
777
|
+
/** Spread onto the `<button>`, whole. Merge your class after `triggerProps.className`. */
|
|
778
|
+
triggerProps: DateTriggerAttributes;
|
|
779
|
+
/** Put on the element showing the value: the trigger is named "label, value" by
|
|
780
|
+
* reference to this id. Without it the name is the label alone. */
|
|
781
|
+
valueProps: { id: string };
|
|
782
|
+
open: boolean;
|
|
783
|
+
from: string;
|
|
784
|
+
to: string;
|
|
785
|
+
/** The preset the column marks for the committed range, if any — the one to name. */
|
|
786
|
+
preset: DateRangePickerPreset | undefined;
|
|
787
|
+
/** What the kit's own trigger would show: the formatted range, or the placeholder. */
|
|
788
|
+
text: string;
|
|
615
789
|
}
|
|
616
790
|
|
|
617
791
|
interface RangeDraft {
|
|
@@ -648,11 +822,15 @@ function PresetColumn({
|
|
|
648
822
|
marked,
|
|
649
823
|
label,
|
|
650
824
|
onPick,
|
|
825
|
+
sheet,
|
|
651
826
|
}: {
|
|
652
827
|
presets: readonly DateRangePickerPreset[];
|
|
653
828
|
marked: number;
|
|
654
829
|
label: string;
|
|
655
830
|
onPick: (p: DateRangePickerPreset) => void;
|
|
831
|
+
/** The phone sheet's shape: a two-column grid ABOVE the calendar with 44px rows —
|
|
832
|
+
* a 128px column beside a month grid leaves the grid ~200px on a 360px screen. */
|
|
833
|
+
sheet?: boolean;
|
|
656
834
|
}) {
|
|
657
835
|
return (
|
|
658
836
|
// `border-e`/`pe`, not `-r`: the preset column is on the START side, and the rule
|
|
@@ -660,7 +838,11 @@ function PresetColumn({
|
|
|
660
838
|
<div
|
|
661
839
|
role="group"
|
|
662
840
|
aria-label={label}
|
|
663
|
-
className=
|
|
841
|
+
className={
|
|
842
|
+
sheet
|
|
843
|
+
? "grid grid-cols-2 gap-0.5"
|
|
844
|
+
: "flex w-32 shrink-0 flex-col gap-0.5 border-e border-[var(--border)] pe-2"
|
|
845
|
+
}
|
|
664
846
|
>
|
|
665
847
|
{presets.map((p, i) => {
|
|
666
848
|
const selected = i === marked;
|
|
@@ -672,7 +854,9 @@ function PresetColumn({
|
|
|
672
854
|
data-preset={p.id}
|
|
673
855
|
onClick={() => onPick(p)}
|
|
674
856
|
className={cn(
|
|
675
|
-
"rounded px-2
|
|
857
|
+
"rounded px-2 text-start",
|
|
858
|
+
// 44px on a phone: the kit's touch-target size (`SHEET_ROW_CLASS`).
|
|
859
|
+
sheet ? "min-h-11 py-2 text-sm" : "py-1.5 text-xs",
|
|
676
860
|
selected
|
|
677
861
|
? "bg-[var(--bg-active)] font-medium text-[var(--text-primary)]"
|
|
678
862
|
: "text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]",
|
|
@@ -700,6 +884,8 @@ function RangePanel({
|
|
|
700
884
|
controlled,
|
|
701
885
|
commit,
|
|
702
886
|
close,
|
|
887
|
+
frame,
|
|
888
|
+
sheet,
|
|
703
889
|
commitRange,
|
|
704
890
|
calendarProps,
|
|
705
891
|
labels,
|
|
@@ -711,6 +897,9 @@ function RangePanel({
|
|
|
711
897
|
controlled: boolean;
|
|
712
898
|
commit: DateRangeCommit;
|
|
713
899
|
close: () => void;
|
|
900
|
+
/** Where the body and the Apply row go — see `PanelFrame`. */
|
|
901
|
+
frame: PanelFrame;
|
|
902
|
+
sheet: boolean;
|
|
714
903
|
commitRange: (from: string, to: string, presetId: string | undefined) => void;
|
|
715
904
|
calendarProps: Pick<MiniCalendarProps, "locale" | "min" | "max" | "labels">;
|
|
716
905
|
labels: DatePickerLabels;
|
|
@@ -748,8 +937,9 @@ function RangePanel({
|
|
|
748
937
|
|
|
749
938
|
const body =
|
|
750
939
|
presets && presets.length > 0 ? (
|
|
751
|
-
<div className="flex gap-3">
|
|
940
|
+
<div className={sheet ? "flex flex-col gap-3" : "flex gap-3"}>
|
|
752
941
|
<PresetColumn
|
|
942
|
+
sheet={sheet}
|
|
753
943
|
presets={presets}
|
|
754
944
|
marked={marked}
|
|
755
945
|
label={labels.presets}
|
|
@@ -770,29 +960,30 @@ function RangePanel({
|
|
|
770
960
|
calendar
|
|
771
961
|
);
|
|
772
962
|
|
|
773
|
-
if (!drafting) return body;
|
|
774
|
-
return (
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
963
|
+
if (!drafting) return frame(body);
|
|
964
|
+
return frame(
|
|
965
|
+
body,
|
|
966
|
+
<>
|
|
967
|
+
{/* `flex-1` in the sheet: two thumb-wide halves of the footer, not two small
|
|
968
|
+
buttons in its corner. */}
|
|
969
|
+
<Button type="button" variant="ghost" className={sheet ? "flex-1" : undefined} onClick={close}>
|
|
970
|
+
{labels.cancel}
|
|
971
|
+
</Button>
|
|
972
|
+
<Button
|
|
973
|
+
type="button"
|
|
974
|
+
className={sheet ? "flex-1" : undefined}
|
|
975
|
+
// A half-made `(from, "")` would commit a window with no end — a blank
|
|
976
|
+
// report, one click into a two-click gesture.
|
|
977
|
+
disabled={!complete}
|
|
978
|
+
onClick={() => {
|
|
979
|
+
if (!complete) return;
|
|
980
|
+
commitRange(draft.from, draft.to, draft.presetId);
|
|
981
|
+
close();
|
|
982
|
+
}}
|
|
983
|
+
>
|
|
984
|
+
{labels.apply}
|
|
985
|
+
</Button>
|
|
986
|
+
</>,
|
|
796
987
|
);
|
|
797
988
|
}
|
|
798
989
|
|
|
@@ -813,6 +1004,8 @@ export function DateRangePicker({
|
|
|
813
1004
|
presets,
|
|
814
1005
|
preset,
|
|
815
1006
|
commit = "immediate",
|
|
1007
|
+
renderTrigger,
|
|
1008
|
+
sheetBackCloses,
|
|
816
1009
|
calendarLabels,
|
|
817
1010
|
label,
|
|
818
1011
|
clearable,
|
|
@@ -837,6 +1030,7 @@ export function DateRangePicker({
|
|
|
837
1030
|
: `${a}${separator}…`
|
|
838
1031
|
: (placeholder ?? "");
|
|
839
1032
|
const hasPresets = Boolean(presets && presets.length > 0);
|
|
1033
|
+
const sheet = useMediaQuery(PHONE_QUERY, false);
|
|
840
1034
|
const commitRange = (f: string, t: string, presetId: string | undefined) => {
|
|
841
1035
|
setOwnPreset(presetId);
|
|
842
1036
|
// Two arguments when no preset is involved — exactly the 0.7 call, so a caller's
|
|
@@ -858,8 +1052,23 @@ export function DateRangePicker({
|
|
|
858
1052
|
hasValue={Boolean(from || to)}
|
|
859
1053
|
triggerText={triggerText}
|
|
860
1054
|
onClear={() => commitRange("", "", undefined)}
|
|
1055
|
+
sheet={sheet}
|
|
1056
|
+
sheetBackCloses={sheetBackCloses}
|
|
1057
|
+
renderTrigger={
|
|
1058
|
+
renderTrigger &&
|
|
1059
|
+
((triggerProps, valueId) =>
|
|
1060
|
+
renderTrigger({
|
|
1061
|
+
triggerProps,
|
|
1062
|
+
valueProps: { id: valueId },
|
|
1063
|
+
open: triggerProps["aria-expanded"],
|
|
1064
|
+
from,
|
|
1065
|
+
to,
|
|
1066
|
+
preset: presets?.[markedPreset(presets, from, to, activeId, controlled)],
|
|
1067
|
+
text: triggerText,
|
|
1068
|
+
}))
|
|
1069
|
+
}
|
|
861
1070
|
>
|
|
862
|
-
{(close) => (
|
|
1071
|
+
{(close, frame) => (
|
|
863
1072
|
<RangePanel
|
|
864
1073
|
from={from}
|
|
865
1074
|
to={to}
|
|
@@ -868,6 +1077,8 @@ export function DateRangePicker({
|
|
|
868
1077
|
controlled={controlled}
|
|
869
1078
|
commit={commit}
|
|
870
1079
|
close={close}
|
|
1080
|
+
frame={frame}
|
|
1081
|
+
sheet={sheet}
|
|
871
1082
|
commitRange={commitRange}
|
|
872
1083
|
calendarProps={{ locale, min, max, labels: calendarLabels }}
|
|
873
1084
|
labels={text}
|