@eifi1/ui-kit 0.11.0 → 0.12.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 +57 -32
- package/dist/chart.d.ts +3 -1
- package/dist/chart.js +2 -0
- package/dist/chart.js.map +1 -1
- package/dist/components/account-settings-labels.d.ts +105 -0
- package/dist/components/account-settings-labels.js +81 -0
- package/dist/components/account-settings-labels.js.map +1 -0
- package/dist/components/account-settings.d.ts +36 -52
- package/dist/components/account-settings.js +44 -8
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +24 -4
- package/dist/components/amount-input.js +5 -2
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.d.ts +68 -0
- package/dist/components/authed-image.js +117 -0
- package/dist/components/authed-image.js.map +1 -0
- package/dist/components/breadcrumbs.d.ts +2 -1
- package/dist/components/breadcrumbs.js +5 -2
- package/dist/components/breadcrumbs.js.map +1 -1
- package/dist/components/button-group.d.ts +51 -2
- package/dist/components/button-group.js +5 -1
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +15 -4
- package/dist/components/calendar-heatmap.d.ts +41 -3
- package/dist/components/calendar-heatmap.js +48 -7
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/chip.d.ts +2 -1
- package/dist/components/chip.js +6 -3
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +35 -3
- package/dist/components/choice-card.js +83 -25
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/combobox-core.js +2 -0
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +2 -2
- package/dist/components/combobox.js +31 -9
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +51 -70
- package/dist/components/copy-button.js +1 -1
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +63 -0
- package/dist/components/data-table-cells.js +64 -0
- package/dist/components/data-table-cells.js.map +1 -0
- 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-filters.js +31 -1
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +13 -0
- package/dist/components/data-table-labels.js +8 -1
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +102 -12
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +3 -1
- package/dist/components/date-picker.js +15 -2
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.d.ts +23 -3
- package/dist/components/description-list.js +23 -3
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +1 -0
- package/dist/components/entity-combobox.d.ts +1 -1
- package/dist/components/entity-combobox.js +18 -3
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.d.ts +92 -0
- package/dist/components/error-boundary.js +111 -0
- package/dist/components/error-boundary.js.map +1 -0
- package/dist/components/field.d.ts +30 -4
- package/dist/components/field.js +40 -17
- package/dist/components/field.js.map +1 -1
- package/dist/components/file-button.d.ts +50 -208
- package/dist/components/file-dropzone.d.ts +49 -2
- package/dist/components/floating-panel.d.ts +13 -4
- package/dist/components/floating-panel.js +7 -3
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +53 -0
- package/dist/components/form-actions.js +98 -0
- package/dist/components/form-actions.js.map +1 -0
- package/dist/components/image-grid.d.ts +67 -0
- package/dist/components/image-grid.js +112 -0
- package/dist/components/image-grid.js.map +1 -0
- package/dist/components/lightbox.d.ts +104 -0
- package/dist/components/lightbox.js +229 -0
- package/dist/components/lightbox.js.map +1 -0
- package/dist/components/line-items.d.ts +114 -0
- package/dist/components/line-items.js +238 -0
- package/dist/components/line-items.js.map +1 -0
- package/dist/components/list.d.ts +23 -2
- package/dist/components/list.js +32 -13
- package/dist/components/list.js.map +1 -1
- package/dist/components/loading-state.d.ts +24 -0
- package/dist/components/loading-state.js +32 -0
- package/dist/components/loading-state.js.map +1 -0
- package/dist/components/menu-item.d.ts +2 -1
- package/dist/components/menu-item.js +5 -1
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +13 -1
- package/dist/components/mini-calendar.js +46 -2
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +20 -1
- package/dist/components/modal.js +26 -2
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-picker.d.ts +21 -1
- package/dist/components/month-picker.js +139 -26
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +1 -1
- package/dist/components/multi-entity-combobox.js +19 -3
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/nav-pills.d.ts +2 -1
- package/dist/components/nav-pills.js +5 -1
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/number-field.d.ts +24 -5
- package/dist/components/number-field.js +5 -7
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +25 -5
- package/dist/components/number-input.js +7 -1
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +15 -4
- package/dist/components/passkeys-setting.d.ts +67 -0
- package/dist/components/passkeys-setting.js +214 -0
- package/dist/components/passkeys-setting.js.map +1 -0
- package/dist/components/picker-sheet.js +2 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart-labels.d.ts +20 -0
- package/dist/components/pie-chart-labels.js +12 -0
- package/dist/components/pie-chart-labels.js.map +1 -0
- package/dist/components/pie-chart.d.ts +101 -0
- package/dist/components/pie-chart.js +283 -0
- package/dist/components/pie-chart.js.map +1 -0
- package/dist/components/progress-bar.d.ts +44 -3
- package/dist/components/progress-bar.js +58 -22
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/qr-code.d.ts +34 -0
- package/dist/components/qr-code.js +57 -0
- package/dist/components/qr-code.js.map +1 -0
- package/dist/components/search-field.d.ts +15 -3
- package/dist/components/search-field.js +5 -1
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/settings-fields.d.ts +49 -1
- package/dist/components/signed-amount.d.ts +121 -0
- package/dist/components/signed-amount.js +152 -0
- package/dist/components/signed-amount.js.map +1 -0
- package/dist/components/skeleton.d.ts +10 -3
- package/dist/components/skeleton.js +12 -2
- package/dist/components/skeleton.js.map +1 -1
- package/dist/components/stat-tile.d.ts +2 -1
- package/dist/components/stat-tile.js +5 -2
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/table.d.ts +39 -5
- package/dist/components/table.js +45 -11
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +176 -0
- package/dist/components/text-link.js +109 -0
- package/dist/components/text-link.js.map +1 -0
- package/dist/components/time-input.d.ts +49 -1
- package/dist/components/toggle-group.d.ts +13 -0
- package/dist/components/toggle-group.js +35 -6
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +36 -3
- package/dist/components/toggle-legend.js +49 -0
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/ui.d.ts +51 -779
- package/dist/components/ui.js +215 -23
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +3 -52
- package/dist/components/use-table-state.js +128 -30
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
- package/dist/data-table.d.ts +2 -1
- package/dist/data-table.js +5 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +26 -3
- package/dist/feedback/feedback-dialog.js +16 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +133 -0
- package/dist/feedback/feedback-thread.js +237 -0
- package/dist/feedback/feedback-thread.js.map +1 -0
- package/dist/feedback.d.ts +2 -1
- package/dist/feedback.js +1 -0
- package/dist/feedback.js.map +1 -1
- package/dist/hooks/use-authed-src.d.ts +71 -0
- package/dist/hooks/use-authed-src.js +63 -0
- package/dist/hooks/use-authed-src.js.map +1 -0
- package/dist/hooks/use-file-drop.d.ts +49 -2
- package/dist/hooks/use-hotkey.d.ts +59 -0
- package/dist/hooks/use-hotkey.js +105 -0
- package/dist/hooks/use-hotkey.js.map +1 -0
- package/dist/hooks/use-search-param-state.d.ts +83 -0
- package/dist/hooks/use-search-param-state.js +94 -0
- package/dist/hooks/use-search-param-state.js.map +1 -0
- package/dist/i18n/defaults.d.ts +15 -4
- package/dist/i18n/defaults.js +31 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +46 -312
- package/dist/i18n/kit-labels.js +20 -5
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +15 -4
- package/dist/i18n/locales/de-CH.d.ts +15 -4
- package/dist/i18n/locales/de-informal.d.ts +15 -4
- package/dist/i18n/locales/de.d.ts +15 -4
- package/dist/i18n/locales/de.js +156 -3
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +15 -4
- package/dist/i18n/locales/es.js +153 -3
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +15 -4
- package/dist/i18n/locales/fr.js +153 -3
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +15 -4
- package/dist/i18n/locales/hu.js +153 -3
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +15 -4
- package/dist/i18n/locales/it.js +153 -3
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +15 -4
- package/dist/i18n/locales/zh.js +153 -3
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +37 -17
- package/dist/index.js +40 -1
- package/dist/index.js.map +1 -1
- package/dist/kit-labels-v3biUF1L.d.ts +1645 -0
- package/dist/lib/format.d.ts +144 -0
- package/dist/lib/format.js +149 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/qr-encode.d.ts +44 -0
- package/dist/lib/qr-encode.js +338 -0
- package/dist/lib/qr-encode.js.map +1 -0
- package/dist/rhf/fields.d.ts +241 -0
- package/dist/rhf/fields.js +601 -0
- package/dist/rhf/fields.js.map +1 -0
- package/dist/rhf/form.d.ts +49 -1
- package/dist/rhf/line-items.d.ts +31 -0
- package/dist/rhf/line-items.js +59 -0
- package/dist/rhf/line-items.js.map +1 -0
- package/dist/rhf.d.ts +61 -1
- package/dist/rhf.js +2 -0
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +99 -2
- package/dist/shell/app-shell.js +58 -3
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/auth-layout.d.ts +74 -0
- package/dist/shell/auth-layout.js +97 -0
- package/dist/shell/auth-layout.js.map +1 -0
- package/dist/shell/top-bar-brand.d.ts +81 -0
- package/dist/shell/top-bar-brand.js +47 -0
- package/dist/shell/top-bar-brand.js.map +1 -0
- package/dist/shell/topbar-action-menu.d.ts +12 -1
- package/dist/shell/topbar-action-menu.js +16 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +56 -1
- package/dist/shell.js +2 -0
- package/dist/shell.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +48 -1
- package/dist/wizard/wizard-context.d.ts +24 -1
- package/dist/wizard/wizard-context.js +19 -2
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.d.ts +20 -5
- package/dist/wizard/wizard-step.js +16 -3
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard.d.ts +50 -3
- package/package.json +1 -1
- package/src/chart.ts +4 -0
- package/src/components/account-settings-labels.ts +190 -0
- package/src/components/account-settings.tsx +110 -63
- package/src/components/amount-input.tsx +13 -1
- package/src/components/authed-image.tsx +190 -0
- package/src/components/breadcrumbs.tsx +8 -2
- package/src/components/button-group.tsx +7 -2
- package/src/components/calendar-heatmap.tsx +105 -10
- package/src/components/chip.tsx +8 -4
- package/src/components/choice-card.tsx +136 -25
- package/src/components/combobox-core.tsx +5 -0
- package/src/components/combobox.tsx +31 -9
- package/src/components/copy-button.tsx +3 -2
- package/src/components/data-table-cells.tsx +119 -0
- package/src/components/data-table-filters.ts +77 -0
- package/src/components/data-table-labels.ts +23 -0
- package/src/components/data-table.tsx +224 -13
- package/src/components/date-picker.tsx +19 -3
- package/src/components/description-list.tsx +52 -3
- package/src/components/entity-combobox.tsx +18 -3
- package/src/components/error-boundary.tsx +216 -0
- package/src/components/field.tsx +56 -5
- package/src/components/floating-panel.tsx +20 -7
- package/src/components/form-actions.tsx +191 -0
- package/src/components/image-grid.tsx +181 -0
- package/src/components/lightbox.tsx +374 -0
- package/src/components/line-items.tsx +385 -0
- package/src/components/list.tsx +65 -16
- package/src/components/loading-state.tsx +47 -0
- package/src/components/menu-item.tsx +7 -2
- package/src/components/mini-calendar.tsx +72 -3
- package/src/components/modal.tsx +61 -3
- package/src/components/month-picker.tsx +181 -26
- package/src/components/multi-entity-combobox.tsx +19 -3
- package/src/components/nav-pills.tsx +8 -1
- package/src/components/number-field.tsx +15 -10
- package/src/components/number-input.tsx +16 -1
- package/src/components/passkeys-setting.tsx +331 -0
- package/src/components/picker-sheet.tsx +7 -1
- package/src/components/pie-chart-labels.ts +32 -0
- package/src/components/pie-chart.tsx +479 -0
- package/src/components/progress-bar.tsx +129 -19
- package/src/components/qr-code.tsx +83 -0
- package/src/components/search-field.tsx +20 -4
- package/src/components/signed-amount.tsx +286 -0
- package/src/components/skeleton.tsx +21 -3
- package/src/components/stat-tile.tsx +8 -2
- package/src/components/table.tsx +102 -13
- package/src/components/text-link.tsx +248 -0
- package/src/components/toggle-group.tsx +58 -10
- package/src/components/toggle-legend.tsx +87 -2
- package/src/components/ui.tsx +426 -36
- package/src/components/use-table-state.ts +265 -33
- package/src/data-table.ts +10 -0
- package/src/feedback/feedback-dialog.tsx +42 -3
- package/src/feedback/feedback-thread.tsx +434 -0
- package/src/feedback.ts +1 -0
- package/src/hooks/use-authed-src.ts +161 -0
- package/src/hooks/use-hotkey.ts +172 -0
- package/src/hooks/use-search-param-state.ts +187 -0
- package/src/i18n/defaults.ts +30 -0
- package/src/i18n/kit-labels.tsx +102 -8
- package/src/i18n/locales/de.ts +153 -0
- package/src/i18n/locales/es.ts +150 -0
- package/src/i18n/locales/fr.ts +153 -0
- package/src/i18n/locales/hu.ts +151 -0
- package/src/i18n/locales/it.ts +151 -0
- package/src/i18n/locales/zh.ts +150 -0
- package/src/index.ts +59 -0
- package/src/lib/format.ts +325 -0
- package/src/lib/qr-encode.ts +420 -0
- package/src/rhf/fields.tsx +1054 -0
- package/src/rhf/line-items.tsx +130 -0
- package/src/rhf.ts +6 -0
- package/src/shell/app-shell.tsx +147 -2
- package/src/shell/auth-layout.tsx +190 -0
- package/src/shell/top-bar-brand.tsx +76 -0
- package/src/shell/topbar-action-menu.tsx +21 -0
- package/src/shell.ts +2 -0
- package/src/wizard/wizard-context.tsx +41 -1
- package/src/wizard/wizard-step.tsx +43 -6
- package/tokens.css +12 -0
|
@@ -175,6 +175,18 @@ export interface MiniCalendarProps extends Omit<ComponentPropsWithoutRef<"div">,
|
|
|
175
175
|
* it is what names the grid.
|
|
176
176
|
*/
|
|
177
177
|
hideNavigation?: boolean;
|
|
178
|
+
/**
|
|
179
|
+
* Fill the leading and trailing cells with the neighbouring months' days, muted —
|
|
180
|
+
* the wall-calendar look, where the first row does not start on a hole.
|
|
181
|
+
*
|
|
182
|
+
* They are real days, not decoration: each is named with its whole date, honours
|
|
183
|
+
* `min`/`max`, and a click selects it and moves the grid to its month (focus goes
|
|
184
|
+
* with it). They are NOT stops of the roving tabindex — the keyboard already reaches
|
|
185
|
+
* them by arrowing off the month's edge, which moves the grid the same way — and
|
|
186
|
+
* `renderDay` is not called for them, so a day's events are drawn once, in its own
|
|
187
|
+
* month. Default `false`: the cells stay empty.
|
|
188
|
+
*/
|
|
189
|
+
showOutsideDays?: boolean;
|
|
178
190
|
/** Extra classes for the calendar's root. */
|
|
179
191
|
className?: string;
|
|
180
192
|
}
|
|
@@ -256,6 +268,7 @@ export function MiniCalendar({
|
|
|
256
268
|
month,
|
|
257
269
|
onMonthChange,
|
|
258
270
|
hideNavigation,
|
|
271
|
+
showOutsideDays = false,
|
|
259
272
|
className,
|
|
260
273
|
...rest
|
|
261
274
|
}: MiniCalendarProps) {
|
|
@@ -335,12 +348,19 @@ export function MiniCalendar({
|
|
|
335
348
|
|
|
336
349
|
const firstOfMonth = new Date(view.year, view.month, 1);
|
|
337
350
|
const lastOfMonth = new Date(view.year, view.month + 1, 0);
|
|
338
|
-
// Leading blanks up to the first of the month, counted from the week's first day
|
|
351
|
+
// Leading blanks up to the first of the month, counted from the week's first day —
|
|
352
|
+
// or the previous month's last days, with `showOutsideDays` (and the next month's
|
|
353
|
+
// first ones at the end). `new Date(y, m, 0)` is the day before the 1st, so the
|
|
354
|
+
// offsets below roll into the neighbouring month by themselves.
|
|
339
355
|
const startWeekday = weekdayIndex(firstOfMonth, weekStart);
|
|
340
356
|
const cells: (Date | null)[] = [];
|
|
341
|
-
for (let i = 0; i < startWeekday; i++)
|
|
357
|
+
for (let i = 0; i < startWeekday; i++) {
|
|
358
|
+
cells.push(showOutsideDays ? new Date(view.year, view.month, i - startWeekday + 1) : null);
|
|
359
|
+
}
|
|
342
360
|
for (let d = 1; d <= lastOfMonth.getDate(); d++) cells.push(new Date(view.year, view.month, d));
|
|
343
|
-
|
|
361
|
+
for (let extra = 1; cells.length % 7 !== 0; extra++) {
|
|
362
|
+
cells.push(showOutsideDays ? new Date(view.year, view.month + 1, extra) : null);
|
|
363
|
+
}
|
|
344
364
|
const weeks: (Date | null)[][] = [];
|
|
345
365
|
for (let i = 0; i < cells.length; i += 7) weeks.push(cells.slice(i, i + 7));
|
|
346
366
|
|
|
@@ -588,6 +608,55 @@ export function MiniCalendar({
|
|
|
588
608
|
}
|
|
589
609
|
const iso = toLocalIso(d);
|
|
590
610
|
const disabled = isDisabled(d);
|
|
611
|
+
// A neighbouring month's day (`showOutsideDays`): a muted, selectable
|
|
612
|
+
// cell outside the tab order — see the prop.
|
|
613
|
+
if (d.getMonth() !== view.month) {
|
|
614
|
+
return (
|
|
615
|
+
<div
|
|
616
|
+
key={di}
|
|
617
|
+
role="none"
|
|
618
|
+
className={cn(
|
|
619
|
+
"relative flex",
|
|
620
|
+
lg ? cn("min-w-0", LG_CELL_HEIGHT) : "h-8 items-center justify-center",
|
|
621
|
+
)}
|
|
622
|
+
>
|
|
623
|
+
<button
|
|
624
|
+
type="button"
|
|
625
|
+
role="gridcell"
|
|
626
|
+
data-iso={iso}
|
|
627
|
+
data-outside=""
|
|
628
|
+
tabIndex={-1}
|
|
629
|
+
aria-label={labels.day(formatDay(d))}
|
|
630
|
+
aria-selected={!disabled && inRange(d)}
|
|
631
|
+
aria-disabled={disabled || undefined}
|
|
632
|
+
onClick={() => {
|
|
633
|
+
if (disabled) return;
|
|
634
|
+
// Focus follows into the month the click moves the grid to:
|
|
635
|
+
// this button is gone after the next commit.
|
|
636
|
+
pendingFocus.current = iso;
|
|
637
|
+
handleClick(d);
|
|
638
|
+
}}
|
|
639
|
+
onKeyDown={onDayKeyDown}
|
|
640
|
+
className={cn(
|
|
641
|
+
"text-xs tabular-nums text-[var(--text-placeholder)] transition-colors",
|
|
642
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]",
|
|
643
|
+
disabled ? "cursor-not-allowed opacity-60" : "hover:bg-[var(--bg-hover)]",
|
|
644
|
+
lg
|
|
645
|
+
? cn("flex w-full min-w-0 items-start bg-[var(--bg-surface-2)] p-1.5", LG_CELL_HEIGHT)
|
|
646
|
+
: "size-8 rounded-full",
|
|
647
|
+
)}
|
|
648
|
+
>
|
|
649
|
+
{lg ? (
|
|
650
|
+
<span className="inline-flex size-6 items-center justify-center">
|
|
651
|
+
{dayNumber.format(d.getDate())}
|
|
652
|
+
</span>
|
|
653
|
+
) : (
|
|
654
|
+
dayNumber.format(d.getDate())
|
|
655
|
+
)}
|
|
656
|
+
</button>
|
|
657
|
+
</div>
|
|
658
|
+
);
|
|
659
|
+
}
|
|
591
660
|
const isStart = !disabled && fromDate && sameYmd(d, fromDate);
|
|
592
661
|
const isEnd = !disabled && toDate && sameYmd(d, toDate);
|
|
593
662
|
const isToday = sameYmd(d, today);
|
package/src/components/modal.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import { cn } from "../lib/cn";
|
|
|
11
11
|
import { useFocusTrap } from "../hooks/use-focus-trap";
|
|
12
12
|
import { useBodyScrollLock } from "../hooks/use-body-scroll-lock";
|
|
13
13
|
import { useOverlayHistory } from "../hooks/use-overlay-history";
|
|
14
|
+
import { useDialogParam, type DialogParam } from "../hooks/use-search-param-state";
|
|
14
15
|
import { OVERLAY_EXIT_MS, prefersReducedMotion, useCloseTransition } from "../hooks/use-close-transition";
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -196,6 +197,50 @@ export interface ModalProps extends Omit<ComponentPropsWithoutRef<"div">, "role"
|
|
|
196
197
|
* suppressed there — there is nothing beside it to see.
|
|
197
198
|
*/
|
|
198
199
|
draggable?: boolean;
|
|
200
|
+
/**
|
|
201
|
+
* Bind the open state to a URL search param (0.12.0): `"edit"` is open while
|
|
202
|
+
* `?edit` is in the query, `{ key: "dialog", value: "create-lease" }` while
|
|
203
|
+
* `?dialog=create-lease` is. The URL is then the only state — `open` is ignored —
|
|
204
|
+
* and the dialog opens from anywhere that sets the param: {@link useDialogParam}'s
|
|
205
|
+
* setter, a `<Link>`, a shared link, a reload.
|
|
206
|
+
*
|
|
207
|
+
* kastlan's `useDialogParam` did this beside the dialog, so its open state could be
|
|
208
|
+
* deep-linked and captured in a feedback report. Here it also owns the history:
|
|
209
|
+
* opening through `useDialogParam` pushes an entry, so the Back gesture closes the
|
|
210
|
+
* dialog by leaving it, and a close (Escape, the backdrop, the X) goes back over
|
|
211
|
+
* that entry rather than stacking a second one. The dialog's own Back handling
|
|
212
|
+
* ({@link useOverlayHistory}'s throwaway entry) stays off in this mode — the param's
|
|
213
|
+
* entry already is one, and two would take two Back presses to close one dialog.
|
|
214
|
+
*
|
|
215
|
+
* `onClose` still runs, after the param is cleared. Requires a react-router Router.
|
|
216
|
+
*/
|
|
217
|
+
urlParam?: DialogParam;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The URL-bound dialog: the param is the open state, and closing clears it. A
|
|
222
|
+
* component of its own so the router hook is only called by a `Modal` that asked for
|
|
223
|
+
* it — a plain `Modal` must keep working with no Router above it.
|
|
224
|
+
*/
|
|
225
|
+
function UrlParamModal({ urlParam, onClose, ...rest }: ModalProps & { urlParam: DialogParam }) {
|
|
226
|
+
const [open, setOpen] = useDialogParam(urlParam);
|
|
227
|
+
// Closed from here the moment the dismissal is decided. Going back is a traversal
|
|
228
|
+
// the browser lands a task later, and between the two the param still says "open":
|
|
229
|
+
// without this the panel, already lowered, would pop back up for that task.
|
|
230
|
+
const [leaving, setLeaving] = useState(false);
|
|
231
|
+
if (leaving && !open) setLeaving(false);
|
|
232
|
+
return (
|
|
233
|
+
<ModalPanel
|
|
234
|
+
{...rest}
|
|
235
|
+
open={open && !leaving}
|
|
236
|
+
ownHistoryEntry={false}
|
|
237
|
+
onClose={() => {
|
|
238
|
+
setLeaving(true);
|
|
239
|
+
setOpen(false);
|
|
240
|
+
onClose();
|
|
241
|
+
}}
|
|
242
|
+
/>
|
|
243
|
+
);
|
|
199
244
|
}
|
|
200
245
|
|
|
201
246
|
/**
|
|
@@ -206,7 +251,15 @@ export interface ModalProps extends Omit<ComponentPropsWithoutRef<"div">, "role"
|
|
|
206
251
|
* into the panel on open and back to the trigger on close, a Tab focus trap, and
|
|
207
252
|
* `role="dialog"`/`aria-modal`.
|
|
208
253
|
*/
|
|
209
|
-
export function Modal({
|
|
254
|
+
export function Modal(props: ModalProps) {
|
|
255
|
+
return props.urlParam !== undefined ? (
|
|
256
|
+
<UrlParamModal {...props} urlParam={props.urlParam} />
|
|
257
|
+
) : (
|
|
258
|
+
<ModalPanel {...props} />
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function ModalPanel({
|
|
210
263
|
onClose,
|
|
211
264
|
open,
|
|
212
265
|
children,
|
|
@@ -218,8 +271,13 @@ export function Modal({
|
|
|
218
271
|
draggable,
|
|
219
272
|
style,
|
|
220
273
|
role = "dialog",
|
|
274
|
+
urlParam: _urlParam,
|
|
275
|
+
ownHistoryEntry = true,
|
|
221
276
|
...rest
|
|
222
|
-
}: ModalProps
|
|
277
|
+
}: ModalProps & {
|
|
278
|
+
/** False when the URL already holds an entry for this dialog — see `urlParam`. */
|
|
279
|
+
ownHistoryEntry?: boolean;
|
|
280
|
+
}) {
|
|
223
281
|
const panelRef = useRef<HTMLDivElement>(null);
|
|
224
282
|
const drag = useDragOffset(Boolean(draggable));
|
|
225
283
|
// Without `open`, mounted means open — the constant `true` every hook below used to
|
|
@@ -262,7 +320,7 @@ export function Modal({
|
|
|
262
320
|
// this the only way out of a dialog is finding its close button — and Back, the
|
|
263
321
|
// gesture everyone reaches for, navigated the page underneath instead (#172).
|
|
264
322
|
// Held only while open: a closed `open={false}` dialog must not own a history entry.
|
|
265
|
-
useOverlayHistory(active, requestClose);
|
|
323
|
+
useOverlayHistory(active && ownHistoryEntry, requestClose);
|
|
266
324
|
|
|
267
325
|
// Through the shared hook and not by hand: this component's own save/restore copy
|
|
268
326
|
// was one half of the pair that left the page permanently unscrollable when a dialog
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
2
2
|
import type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, RefObject } from "react";
|
|
3
|
-
import { Calendar, ChevronLeft, ChevronRight } from "lucide-react";
|
|
3
|
+
import { Calendar, ChevronDown, ChevronLeft, ChevronRight } from "lucide-react";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
import { dirOf, horizontalStep, type Direction } from "../lib/direction";
|
|
6
6
|
import { monthKey, pad } from "../lib/dates";
|
|
7
|
-
import { FieldLabel, FIELD_FLOATING_PAD, FIELD_INVALID, FIELD_TRIGGER } from "./ui";
|
|
7
|
+
import { Button, FieldLabel, FIELD_FLOATING_PAD, FIELD_INVALID, FIELD_TRIGGER, IconButton } from "./ui";
|
|
8
8
|
import { Popover } from "./popover";
|
|
9
9
|
import { splitTriggerAria } from "./trigger-aria";
|
|
10
10
|
import type { TriggerAria } from "./trigger-aria";
|
|
@@ -27,6 +27,11 @@ export interface MonthPickerLabels {
|
|
|
27
27
|
* "Aug" read out on its own does not say which year the grid is on.
|
|
28
28
|
*/
|
|
29
29
|
month: (monthYear: string) => string;
|
|
30
|
+
/** `variant="stepper"`: the icon-only arrows that step the value by one month. */
|
|
31
|
+
previousMonth: string;
|
|
32
|
+
nextMonth: string;
|
|
33
|
+
/** `variant="stepper"`: the button that jumps to the current month. Visible text. */
|
|
34
|
+
today: string;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
/** The English starting point every override is merged onto. Exported (unlike the
|
|
@@ -37,6 +42,9 @@ export const DEFAULT_MONTH_PICKER_LABELS: MonthPickerLabels = {
|
|
|
37
42
|
nextYear: "Next year",
|
|
38
43
|
panel: "Choose a month",
|
|
39
44
|
month: (monthYear) => monthYear,
|
|
45
|
+
previousMonth: "Previous month",
|
|
46
|
+
nextMonth: "Next month",
|
|
47
|
+
today: "Today",
|
|
40
48
|
};
|
|
41
49
|
|
|
42
50
|
/** Merge caller overrides onto the English defaults. */
|
|
@@ -95,6 +103,21 @@ export interface MonthPickerProps extends Omit<ComponentPropsWithoutRef<"div">,
|
|
|
95
103
|
/** Extra classes for the trigger button — e.g. a compact `h-9 w-auto` trigger
|
|
96
104
|
* sitting between two icon buttons in a toolbar. */
|
|
97
105
|
triggerClassName?: string;
|
|
106
|
+
/**
|
|
107
|
+
* `"field"` (default): the field-shaped trigger, for a form.
|
|
108
|
+
*
|
|
109
|
+
* `"stepper"`: the header of a month PAGE — the month as a heading that opens the
|
|
110
|
+
* grid, then Today and ‹ › stepping the value by one month. What a calendar page
|
|
111
|
+
* assembled by hand around the field (overriding its trigger to look like a
|
|
112
|
+
* heading). `label`, `placeholder`, `invalid` and `triggerClassName` are the field's
|
|
113
|
+
* and are not drawn here; with no `value` the arrows step from the current month.
|
|
114
|
+
*/
|
|
115
|
+
variant?: "field" | "stepper";
|
|
116
|
+
/** `variant="stepper"`: the heading level the month is set in (`<h2>` by default), or
|
|
117
|
+
* `false` for no heading element — the page already has one for the month. */
|
|
118
|
+
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6 | false;
|
|
119
|
+
/** `variant="stepper"`: draw the Today button. Default `true`. */
|
|
120
|
+
showToday?: boolean;
|
|
98
121
|
}
|
|
99
122
|
|
|
100
123
|
// ── Month keys ────────────────────────────────────────────────────────────
|
|
@@ -140,6 +163,74 @@ const MONTH_NAME_FORMAT: Intl.DateTimeFormatOptions = { month: "long", year: "nu
|
|
|
140
163
|
|
|
141
164
|
// ── Trigger ───────────────────────────────────────────────────────────────
|
|
142
165
|
|
|
166
|
+
/** Hand focus back to the trigger when the panel closes with focus still inside it. */
|
|
167
|
+
function useFocusBackOnClose(open: boolean, triggerRef: RefObject<HTMLButtonElement | null>) {
|
|
168
|
+
const wasOpen = useRef(false);
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
const justClosed = wasOpen.current && !open;
|
|
171
|
+
wasOpen.current = open;
|
|
172
|
+
if (!justClosed) return;
|
|
173
|
+
// Only when focus went down with the panel. An outside click has already put it
|
|
174
|
+
// somewhere real, and dragging it back here would be worse than leaving it.
|
|
175
|
+
const active = document.activeElement;
|
|
176
|
+
if (!active || active === document.body) triggerRef.current?.focus();
|
|
177
|
+
}, [open, triggerRef]);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The stepper's trigger: the month as a heading-sized button that opens the grid.
|
|
182
|
+
*
|
|
183
|
+
* Its own text is its name ("September 2026"), so it needs no label of its own, and
|
|
184
|
+
* it is `aria-live` for the reason `MiniCalendar`'s caption is: pressing ‹ or › leaves
|
|
185
|
+
* focus on the arrow, and the heading is the only thing that says where the arrow
|
|
186
|
+
* went. A plain button with `aria-haspopup`, not the field's `combobox` — it does not
|
|
187
|
+
* look or behave like a field, and a heading reading "combobox" is a surprise.
|
|
188
|
+
*/
|
|
189
|
+
function MonthHeadingTrigger({
|
|
190
|
+
open,
|
|
191
|
+
toggle,
|
|
192
|
+
triggerRef,
|
|
193
|
+
text,
|
|
194
|
+
panelId,
|
|
195
|
+
disabled,
|
|
196
|
+
aria,
|
|
197
|
+
}: {
|
|
198
|
+
open: boolean;
|
|
199
|
+
toggle: () => void;
|
|
200
|
+
triggerRef: RefObject<HTMLButtonElement | null>;
|
|
201
|
+
text: string;
|
|
202
|
+
panelId: string;
|
|
203
|
+
disabled?: boolean;
|
|
204
|
+
aria: TriggerAria;
|
|
205
|
+
}) {
|
|
206
|
+
useFocusBackOnClose(open, triggerRef);
|
|
207
|
+
return (
|
|
208
|
+
<button
|
|
209
|
+
ref={triggerRef}
|
|
210
|
+
type="button"
|
|
211
|
+
disabled={disabled}
|
|
212
|
+
onClick={toggle}
|
|
213
|
+
aria-haspopup="dialog"
|
|
214
|
+
aria-controls={panelId}
|
|
215
|
+
aria-expanded={open}
|
|
216
|
+
id={aria.id}
|
|
217
|
+
aria-label={aria["aria-label"]}
|
|
218
|
+
aria-labelledby={aria["aria-labelledby"]}
|
|
219
|
+
aria-describedby={aria["aria-describedby"]}
|
|
220
|
+
className={cn(
|
|
221
|
+
"-ms-1.5 inline-flex h-9 min-w-0 items-center gap-1 rounded-md px-1.5 text-lg font-semibold text-[var(--text-primary)] transition-colors",
|
|
222
|
+
"hover:bg-[var(--bg-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]",
|
|
223
|
+
disabled && "cursor-not-allowed opacity-50",
|
|
224
|
+
)}
|
|
225
|
+
>
|
|
226
|
+
<span aria-live="polite" className="truncate">
|
|
227
|
+
{text || "\u00a0"}
|
|
228
|
+
</span>
|
|
229
|
+
<ChevronDown aria-hidden className="size-4 shrink-0 text-[var(--text-muted)]" />
|
|
230
|
+
</button>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
143
234
|
/**
|
|
144
235
|
* The field-shaped button that opens the grid. The same shape as `DatePicker`'s
|
|
145
236
|
* trigger, for the same reasons written down there: `role="combobox"` so it may carry
|
|
@@ -176,16 +267,7 @@ function MonthFieldTrigger({
|
|
|
176
267
|
invalid?: boolean;
|
|
177
268
|
className?: string;
|
|
178
269
|
}) {
|
|
179
|
-
|
|
180
|
-
useEffect(() => {
|
|
181
|
-
const justClosed = wasOpen.current && !open;
|
|
182
|
-
wasOpen.current = open;
|
|
183
|
-
if (!justClosed) return;
|
|
184
|
-
// Only when focus went down with the panel. An outside click has already put it
|
|
185
|
-
// somewhere real, and dragging it back here would be worse than leaving it.
|
|
186
|
-
const active = document.activeElement;
|
|
187
|
-
if (!active || active === document.body) triggerRef.current?.focus();
|
|
188
|
-
}, [open, triggerRef]);
|
|
270
|
+
useFocusBackOnClose(open, triggerRef);
|
|
189
271
|
|
|
190
272
|
return (
|
|
191
273
|
<button
|
|
@@ -475,6 +557,9 @@ export function MonthPicker({
|
|
|
475
557
|
labels: labelsProp,
|
|
476
558
|
className,
|
|
477
559
|
triggerClassName,
|
|
560
|
+
variant = "field",
|
|
561
|
+
headingLevel = 2,
|
|
562
|
+
showToday = true,
|
|
478
563
|
...rest
|
|
479
564
|
}: MonthPickerProps) {
|
|
480
565
|
const labels = useMonthPickerLabels(labelsProp);
|
|
@@ -506,6 +591,89 @@ export function MonthPicker({
|
|
|
506
591
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
507
592
|
const [dir, setDir] = useState<Direction>("ltr");
|
|
508
593
|
|
|
594
|
+
const grid = (close: () => void) => (
|
|
595
|
+
<MonthGrid
|
|
596
|
+
value={selected}
|
|
597
|
+
locale={locale}
|
|
598
|
+
minKey={minKey}
|
|
599
|
+
maxKey={maxKey}
|
|
600
|
+
currentKey={currentKey}
|
|
601
|
+
labels={labels}
|
|
602
|
+
onPick={(key) => {
|
|
603
|
+
onChange(key);
|
|
604
|
+
close();
|
|
605
|
+
}}
|
|
606
|
+
/>
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
if (variant === "stepper") {
|
|
610
|
+
// The arrows step from the value, or from "now" when there is none; each is dead
|
|
611
|
+
// once the month it leads to is out of bounds, and Today once "now" is.
|
|
612
|
+
const base = selected ?? parseMonth(currentKey)!;
|
|
613
|
+
const prevKey = toKey(shiftMonth(base, -1));
|
|
614
|
+
const nextKey = toKey(shiftMonth(base, 1));
|
|
615
|
+
const outside = (key: string) => Boolean((minKey && key < minKey) || (maxKey && key > maxKey));
|
|
616
|
+
const Heading = headingLevel ? (`h${headingLevel}` as const) : null;
|
|
617
|
+
const trigger = (
|
|
618
|
+
<Popover
|
|
619
|
+
width={256}
|
|
620
|
+
panelId={panelId}
|
|
621
|
+
labels={{ panel: labels.panel }}
|
|
622
|
+
dir={dir}
|
|
623
|
+
trigger={({ open, toggle, ref }) => (
|
|
624
|
+
<MonthHeadingTrigger
|
|
625
|
+
open={open}
|
|
626
|
+
toggle={() => {
|
|
627
|
+
setDir(dirOf(rootRef.current));
|
|
628
|
+
toggle();
|
|
629
|
+
}}
|
|
630
|
+
triggerRef={ref}
|
|
631
|
+
text={triggerText}
|
|
632
|
+
panelId={panelId}
|
|
633
|
+
disabled={disabled}
|
|
634
|
+
aria={aria}
|
|
635
|
+
/>
|
|
636
|
+
)}
|
|
637
|
+
>
|
|
638
|
+
{grid}
|
|
639
|
+
</Popover>
|
|
640
|
+
);
|
|
641
|
+
return (
|
|
642
|
+
<div {...wrapperRest} ref={rootRef} className={cn("flex min-w-0 items-center gap-2", className)}>
|
|
643
|
+
{Heading ? <Heading className="m-0 min-w-0 text-lg font-semibold">{trigger}</Heading> : trigger}
|
|
644
|
+
<div className="ms-auto flex shrink-0 items-center gap-1">
|
|
645
|
+
{showToday && (
|
|
646
|
+
<Button
|
|
647
|
+
variant="secondary"
|
|
648
|
+
size="sm"
|
|
649
|
+
disabled={disabled || outside(currentKey)}
|
|
650
|
+
onClick={() => {
|
|
651
|
+
if (currentKey !== value) onChange(currentKey);
|
|
652
|
+
}}
|
|
653
|
+
>
|
|
654
|
+
{labels.today}
|
|
655
|
+
</Button>
|
|
656
|
+
)}
|
|
657
|
+
<IconButton
|
|
658
|
+
aria-label={labels.previousMonth}
|
|
659
|
+
disabled={disabled || outside(prevKey)}
|
|
660
|
+
onClick={() => onChange(prevKey)}
|
|
661
|
+
>
|
|
662
|
+
{/* Mirrored in RTL, where "previous" points the other way. */}
|
|
663
|
+
<ChevronLeft className="rtl:-scale-x-100" />
|
|
664
|
+
</IconButton>
|
|
665
|
+
<IconButton
|
|
666
|
+
aria-label={labels.nextMonth}
|
|
667
|
+
disabled={disabled || outside(nextKey)}
|
|
668
|
+
onClick={() => onChange(nextKey)}
|
|
669
|
+
>
|
|
670
|
+
<ChevronRight className="rtl:-scale-x-100" />
|
|
671
|
+
</IconButton>
|
|
672
|
+
</div>
|
|
673
|
+
</div>
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
509
677
|
return (
|
|
510
678
|
<div {...wrapperRest} ref={rootRef} className={cn("relative", className)}>
|
|
511
679
|
{label !== undefined && <FieldLabel>{label}</FieldLabel>}
|
|
@@ -550,20 +718,7 @@ export function MonthPicker({
|
|
|
550
718
|
/>
|
|
551
719
|
)}
|
|
552
720
|
>
|
|
553
|
-
{
|
|
554
|
-
<MonthGrid
|
|
555
|
-
value={selected}
|
|
556
|
-
locale={locale}
|
|
557
|
-
minKey={minKey}
|
|
558
|
-
maxKey={maxKey}
|
|
559
|
-
currentKey={currentKey}
|
|
560
|
-
labels={labels}
|
|
561
|
-
onPick={(key) => {
|
|
562
|
-
onChange(key);
|
|
563
|
-
close();
|
|
564
|
-
}}
|
|
565
|
-
/>
|
|
566
|
-
)}
|
|
721
|
+
{grid}
|
|
567
722
|
</Popover>
|
|
568
723
|
<Calendar
|
|
569
724
|
aria-hidden
|
|
@@ -94,6 +94,15 @@ export function MultiEntityCombobox<V extends string | number>({
|
|
|
94
94
|
debounceMs,
|
|
95
95
|
loadErrorLabel,
|
|
96
96
|
"aria-label": ariaLabel,
|
|
97
|
+
// The control's own wiring, taken off `rest` so it lands on the TRIGGER rather than
|
|
98
|
+
// the wrapper: `Field`'s render-prop spreads `{ id, aria-describedby, aria-invalid,
|
|
99
|
+
// aria-required }`, and on the role-less wrapper div the label's `htmlFor` named
|
|
100
|
+
// nothing and the hint and required state described nothing — kastlan's
|
|
101
|
+
// communication-page To/Cc/Bcc and lease-tenants-step.tsx read as unnamed pickers.
|
|
102
|
+
id,
|
|
103
|
+
"aria-describedby": ariaDescribedBy,
|
|
104
|
+
"aria-invalid": ariaInvalid,
|
|
105
|
+
"aria-required": ariaRequired,
|
|
97
106
|
...rest
|
|
98
107
|
}: MultiEntityComboboxProps<V>) {
|
|
99
108
|
const core = useComboboxCore<V>({
|
|
@@ -104,7 +113,11 @@ export function MultiEntityCombobox<V extends string | number>({
|
|
|
104
113
|
minChars,
|
|
105
114
|
debounceMs,
|
|
106
115
|
});
|
|
107
|
-
const field = useComboboxFieldError(
|
|
116
|
+
const field = useComboboxFieldError(
|
|
117
|
+
error,
|
|
118
|
+
invalid || ariaInvalid === true || ariaInvalid === "true",
|
|
119
|
+
ariaDescribedBy,
|
|
120
|
+
);
|
|
108
121
|
// The props are the per-instance overrides, the provider the app-wide ones; a
|
|
109
122
|
// prop left `undefined` falls through to the provider rather than masking it.
|
|
110
123
|
const labels = useKitLabels("combobox", DEFAULT_COMBOBOX_LABELS, {
|
|
@@ -154,13 +167,15 @@ export function MultiEntityCombobox<V extends string | number>({
|
|
|
154
167
|
};
|
|
155
168
|
|
|
156
169
|
return (
|
|
157
|
-
// `rest` dresses the wrapper, which has no role; the accessible NAME
|
|
158
|
-
//
|
|
170
|
+
// `rest` dresses the wrapper, which has no role; the accessible NAME — and the
|
|
171
|
+
// id, description, invalid and required state above — go on the trigger, which
|
|
172
|
+
// has one. Spread FIRST so the trigger's ARIA and the
|
|
159
173
|
// handlers that open the panel cannot be clobbered from outside.
|
|
160
174
|
<div {...rest} className={cn("relative", className)}>
|
|
161
175
|
{label !== undefined && <FieldLabel>{label}</FieldLabel>}
|
|
162
176
|
<button
|
|
163
177
|
ref={core.triggerRef}
|
|
178
|
+
id={id}
|
|
164
179
|
type="button"
|
|
165
180
|
// A combobox, not a button. The distinction is not pedantry: this control
|
|
166
181
|
// carried `aria-invalid`, which `button` does not support, so a required
|
|
@@ -188,6 +203,7 @@ export function MultiEntityCombobox<V extends string | number>({
|
|
|
188
203
|
disabled={disabled}
|
|
189
204
|
aria-invalid={field.isInvalid || undefined}
|
|
190
205
|
aria-describedby={field.describedBy}
|
|
206
|
+
aria-required={ariaRequired}
|
|
191
207
|
onClick={() => !disabled && setOpen((o) => !o)}
|
|
192
208
|
// Down/Up opens the list from the closed trigger, per the APG. Enter and
|
|
193
209
|
// Space already do it through the button's own click.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ComponentPropsWithoutRef, ComponentType, MouseEvent, ReactElement, ReactNode } from "react";
|
|
2
2
|
import { cn } from "../lib/cn";
|
|
3
|
+
import { useKitLink } from "../i18n/kit-labels";
|
|
4
|
+
import { pickLinkRenderer, RenderedKitLink } from "./text-link";
|
|
3
5
|
|
|
4
6
|
export interface NavPillItem<T extends string> {
|
|
5
7
|
value: T;
|
|
@@ -33,7 +35,8 @@ export interface NavPillsProps<T extends string> extends Omit<ComponentPropsWith
|
|
|
33
35
|
/** Called with the pill's value on a click — of a button, and of a link too (before
|
|
34
36
|
* the router follows it), for a caller that keeps the choice in state as well. */
|
|
35
37
|
onSelect?: (value: T) => void;
|
|
36
|
-
/** Your router's link. Default `<
|
|
38
|
+
/** Your router's link. Default: the `<UiKitProvider linkComponent>` (for an in-app
|
|
39
|
+
* `href`), then `<a>`. The API `Breadcrumbs`, `Chip` and `ListItem` share. */
|
|
37
40
|
renderLink?: (props: NavPillLinkProps) => ReactElement;
|
|
38
41
|
/** What `aria-current` says on the current pill. Default `page` where the items
|
|
39
42
|
* are links and `true` where they are buttons (keksdose's swipe-surface picker
|
|
@@ -84,6 +87,7 @@ export function NavPills<T extends string>({
|
|
|
84
87
|
className,
|
|
85
88
|
...rest
|
|
86
89
|
}: NavPillsProps<T>) {
|
|
90
|
+
const kitLink = useKitLink();
|
|
87
91
|
const Root = landmark ? "nav" : "div";
|
|
88
92
|
return (
|
|
89
93
|
<Root {...rest} role={landmark ? undefined : "group"} className={cn("min-w-0", className)}>
|
|
@@ -113,8 +117,11 @@ export function NavPills<T extends string>({
|
|
|
113
117
|
"aria-current": isCurrent ? (currentType ?? "page") : undefined,
|
|
114
118
|
onClick: () => onSelect?.(item.value),
|
|
115
119
|
};
|
|
120
|
+
const kit = pickLinkRenderer<NavPillLinkProps>(undefined, kitLink, item.href);
|
|
116
121
|
pill = renderLink ? (
|
|
117
122
|
renderLink(props)
|
|
123
|
+
) : kit ? (
|
|
124
|
+
<RenderedKitLink render={kit} props={props} />
|
|
118
125
|
) : (
|
|
119
126
|
<a href={props.href} className={props.className} aria-current={props["aria-current"]} onClick={props.onClick}>
|
|
120
127
|
{props.children}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useId, useState } from "react";
|
|
2
2
|
import type { ComponentProps, ReactNode } from "react";
|
|
3
3
|
import { NumberInput } from "./number-input";
|
|
4
4
|
import { evaluateExpression, formatResult } from "../lib/calc";
|
|
@@ -62,6 +62,14 @@ export interface NumberFieldProps
|
|
|
62
62
|
* injects `id` / `aria-describedby` / `aria-invalid`) can wrap this directly. */
|
|
63
63
|
"aria-describedby"?: string;
|
|
64
64
|
"aria-invalid"?: boolean | "true" | "false";
|
|
65
|
+
/**
|
|
66
|
+
* Set on the `<input>`. Declared, with `id` (from NumberInput) and the two above, so
|
|
67
|
+
* `Field`'s render-prop spread — `{(ids) => <NumberField {...ids} … />}`, kastlan's
|
|
68
|
+
* wizard steps — is typed and reaches the input rather than being dropped: before
|
|
69
|
+
* this, `aria-required` was not in the type and a required number was announced as
|
|
70
|
+
* optional.
|
|
71
|
+
*/
|
|
72
|
+
"aria-required"?: boolean | "true" | "false";
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
/** The locale's decimal mark, as far as this field can type it: "," or ".". A mark
|
|
@@ -163,6 +171,7 @@ export function NumberField({
|
|
|
163
171
|
id,
|
|
164
172
|
"aria-describedby": ariaDescribedBy,
|
|
165
173
|
"aria-invalid": ariaInvalid,
|
|
174
|
+
"aria-required": ariaRequired,
|
|
166
175
|
...rest
|
|
167
176
|
}: NumberFieldProps) {
|
|
168
177
|
const locale = useKitLocale(localeProp);
|
|
@@ -250,15 +259,9 @@ export function NumberField({
|
|
|
250
259
|
const hasError = error !== undefined && error !== null && error !== false && error !== "";
|
|
251
260
|
const describedBy = [ariaDescribedBy, hasError ? errorId : undefined].filter(Boolean).join(" ");
|
|
252
261
|
|
|
253
|
-
// NumberInput
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
useEffect(() => {
|
|
257
|
-
const input = document.getElementById(fieldId);
|
|
258
|
-
if (!input) return;
|
|
259
|
-
if (describedBy) input.setAttribute("aria-describedby", describedBy);
|
|
260
|
-
else input.removeAttribute("aria-describedby");
|
|
261
|
-
}, [fieldId, describedBy]);
|
|
262
|
+
// Straight onto NumberInput's <input>, which declares both now. They used to be set
|
|
263
|
+
// on the DOM node from an effect, because NumberInput had no props for them.
|
|
264
|
+
const required = ariaRequired === true || ariaRequired === "true";
|
|
262
265
|
|
|
263
266
|
const labelWithUnit =
|
|
264
267
|
unit !== undefined && unitPlacement === "label"
|
|
@@ -277,6 +280,8 @@ export function NumberField({
|
|
|
277
280
|
<NumberInput
|
|
278
281
|
{...rest}
|
|
279
282
|
id={fieldId}
|
|
283
|
+
aria-describedby={describedBy || undefined}
|
|
284
|
+
aria-required={required || undefined}
|
|
280
285
|
label={labelWithUnit}
|
|
281
286
|
value={draft}
|
|
282
287
|
onChange={onText}
|
|
@@ -57,6 +57,15 @@ interface NumberInputProps {
|
|
|
57
57
|
hint?: ReactNode;
|
|
58
58
|
/** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */
|
|
59
59
|
invalid?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Set on the `<input>`, with `id` above. Declared so `Field`'s render-prop spread —
|
|
62
|
+
* `{(ids) => <NumberInput {...ids} … />}` — is typed and reaches the input: the hint
|
|
63
|
+
* and error are described, a required number is announced as required, and
|
|
64
|
+
* `aria-invalid` paints {@link FIELD_INVALID} the way `invalid` does.
|
|
65
|
+
*/
|
|
66
|
+
"aria-describedby"?: string;
|
|
67
|
+
"aria-invalid"?: boolean | "true" | "false";
|
|
68
|
+
"aria-required"?: boolean | "true" | "false";
|
|
60
69
|
/**
|
|
61
70
|
* A static unit shown at the field's right edge — "%", "kg", "km/h".
|
|
62
71
|
*
|
|
@@ -156,7 +165,10 @@ export function NumberInput({
|
|
|
156
165
|
calculator = true,
|
|
157
166
|
variant = "field",
|
|
158
167
|
hint,
|
|
159
|
-
invalid,
|
|
168
|
+
invalid: invalidProp,
|
|
169
|
+
"aria-describedby": ariaDescribedBy,
|
|
170
|
+
"aria-invalid": ariaInvalid,
|
|
171
|
+
"aria-required": ariaRequired,
|
|
160
172
|
suffix,
|
|
161
173
|
step,
|
|
162
174
|
min,
|
|
@@ -164,6 +176,7 @@ export function NumberInput({
|
|
|
164
176
|
}: NumberInputProps) {
|
|
165
177
|
const generatedId = useId();
|
|
166
178
|
const fieldId = id ?? generatedId;
|
|
179
|
+
const invalid = Boolean(invalidProp) || ariaInvalid === true || ariaInvalid === "true";
|
|
167
180
|
const labelled = label !== undefined;
|
|
168
181
|
// On phones we suppress the OS keyboard (inputMode="none" below) for our own
|
|
169
182
|
// calculator numpad, so the desktop popover trigger is hidden (feedback #334).
|
|
@@ -234,6 +247,8 @@ export function NumberInput({
|
|
|
234
247
|
}}
|
|
235
248
|
onKeyDown={onKeyDown}
|
|
236
249
|
aria-invalid={invalid || undefined}
|
|
250
|
+
aria-describedby={ariaDescribedBy}
|
|
251
|
+
aria-required={ariaRequired}
|
|
237
252
|
// The end padding sits last so it always wins over an inputClassName that
|
|
238
253
|
// sets its own px. Both trailing controls can be on at once, so the room
|
|
239
254
|
// they need is reserved together rather than by whichever happens to render:
|