@eifi1/ui-kit 0.21.0 → 0.23.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 +42 -25
- package/dist/chart.d.ts +19 -14
- package/dist/components/account-chips.d.ts +10 -3
- package/dist/components/account-chips.js +5 -2
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/account-settings.d.ts +33 -2
- package/dist/components/account-settings.js +43 -21
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +53 -14
- package/dist/components/amount-input.js +17 -28
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +29 -0
- package/dist/components/autocomplete.js +134 -104
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +19 -14
- package/dist/components/calculator.d.ts +19 -14
- package/dist/components/checkbox-group.d.ts +104 -0
- package/dist/components/checkbox-group.js +111 -0
- package/dist/components/checkbox-group.js.map +1 -0
- package/dist/components/checkbox.d.ts +19 -0
- package/dist/components/checkbox.js +30 -7
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/chip.d.ts +18 -0
- package/dist/components/chip.js +47 -10
- package/dist/components/chip.js.map +1 -1
- package/dist/components/column-mapper.d.ts +182 -0
- package/dist/components/column-mapper.js +375 -0
- package/dist/components/column-mapper.js.map +1 -0
- package/dist/components/combobox-core.d.ts +39 -2
- package/dist/components/combobox-core.js +29 -6
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +91 -2
- package/dist/components/combobox.js +454 -308
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +69 -161
- package/dist/components/confirm-dialog.js +7 -8
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.d.ts +16 -11
- package/dist/components/country-select.d.ts +167 -0
- package/dist/components/country-select.js +293 -0
- package/dist/components/country-select.js.map +1 -0
- package/dist/components/currency-select.d.ts +17 -1
- package/dist/components/currency-select.js +56 -23
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +69 -166
- package/dist/components/danger-confirm.js +144 -50
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +1 -2
- package/dist/components/data-table-filter-popover.d.ts +1 -2
- package/dist/components/data-table-filters.d.ts +1 -2
- package/dist/components/data-table-labels.d.ts +4 -97
- package/dist/components/data-table-labels.js +9 -1
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-pagination.d.ts +3 -1
- package/dist/components/data-table.d.ts +1 -2
- package/dist/components/data-table.js +98 -3
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +66 -5
- package/dist/components/date-picker.js +378 -66
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +42 -1
- package/dist/components/entity-combobox.js +154 -113
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +19 -14
- package/dist/components/field-parts.d.ts +224 -0
- package/dist/components/field-parts.js +156 -0
- package/dist/components/field-parts.js.map +1 -0
- package/dist/components/field-strip.d.ts +105 -0
- package/dist/components/field-strip.js +46 -0
- package/dist/components/field-strip.js.map +1 -0
- package/dist/components/file-button.d.ts +19 -14
- package/dist/components/file-button.js +13 -3
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +41 -15
- package/dist/components/file-dropzone.js +57 -34
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/form-actions.d.ts +18 -13
- package/dist/components/form-actions.js +69 -5
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/iban-input.d.ts +69 -0
- package/dist/components/iban-input.js +125 -0
- package/dist/components/iban-input.js.map +1 -0
- package/dist/components/icon-picker.d.ts +21 -2
- package/dist/components/icon-picker.js +19 -4
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/inline-edit-field.d.ts +167 -0
- package/dist/components/inline-edit-field.js +293 -0
- package/dist/components/inline-edit-field.js.map +1 -0
- package/dist/components/language-select.d.ts +130 -0
- package/dist/components/language-select.js +18 -0
- package/dist/components/language-select.js.map +1 -0
- package/dist/components/line-items.d.ts +25 -3
- package/dist/components/line-items.js +55 -46
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.d.ts +25 -0
- package/dist/components/list.js +16 -5
- package/dist/components/list.js.map +1 -1
- package/dist/components/money-field.d.ts +32 -14
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.d.ts +52 -4
- package/dist/components/month-picker.js +291 -86
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +26 -1
- package/dist/components/multi-entity-combobox.js +143 -102
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +8 -1
- package/dist/components/multi-select.js +9 -4
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +34 -19
- package/dist/components/number-field.js +6 -20
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +79 -19
- package/dist/components/number-input.js +90 -38
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +19 -14
- package/dist/components/one-time-code-input.d.ts +100 -0
- package/dist/components/one-time-code-input.js +137 -0
- package/dist/components/one-time-code-input.js.map +1 -0
- package/dist/components/phone-input.d.ts +69 -0
- package/dist/components/phone-input.js +171 -0
- package/dist/components/phone-input.js.map +1 -0
- package/dist/components/reauth-dialog.d.ts +2 -2
- package/dist/components/reauth-dialog.js +6 -8
- package/dist/components/reauth-dialog.js.map +1 -1
- package/dist/components/series-chart.d.ts +19 -14
- package/dist/components/settings-fields.d.ts +19 -14
- package/dist/components/share-card.d.ts +16 -11
- package/dist/components/sign-chip.d.ts +66 -0
- package/dist/components/sign-chip.js +39 -0
- package/dist/components/sign-chip.js.map +1 -0
- package/dist/components/swatch-picker.d.ts +43 -2
- package/dist/components/swatch-picker.js +80 -32
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/switch.d.ts +19 -0
- package/dist/components/switch.js +24 -6
- package/dist/components/switch.js.map +1 -1
- package/dist/components/text-link.d.ts +18 -13
- package/dist/components/tile-radio.d.ts +88 -13
- package/dist/components/tile-radio.js +28 -11
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +24 -15
- package/dist/components/time-input.js.map +1 -1
- package/dist/components/toggle-group.d.ts +47 -1
- package/dist/components/toggle-group.js +110 -69
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/trigger-aria.d.ts +3 -0
- package/dist/components/trigger-aria.js +3 -1
- package/dist/components/trigger-aria.js.map +1 -1
- package/dist/components/ui.d.ts +16 -11
- package/dist/components/ui.js +284 -88
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -2
- package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
- package/dist/data-table.d.ts +1 -2
- package/dist/data-table.js +2 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +1 -1
- package/dist/feedback/feedback-attachment.js +348 -41
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +1 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +3 -2
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +68 -253
- package/dist/feedback/feedback-thread.js +46 -10
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/{kit-labels-D1J2_jyD.d.ts → feedback-BxeQVzwq.d.ts} +1206 -20
- package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-fGAzZPf0.d.ts} +178 -11
- package/dist/feedback.d.ts +66 -2
- package/dist/hooks/use-file-drop.d.ts +19 -14
- package/dist/i18n/defaults.d.ts +19 -14
- package/dist/i18n/defaults.js +19 -2
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +19 -14
- package/dist/i18n/german.js +84 -5
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +15 -10
- package/dist/i18n/kit-labels.js +8 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +19 -14
- package/dist/i18n/locales/de-CH.d.ts +19 -14
- package/dist/i18n/locales/en.d.ts +19 -14
- package/dist/i18n/locales/en.js +16 -0
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +19 -14
- package/dist/i18n/locales/es.js +83 -5
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +19 -14
- package/dist/i18n/locales/fr.js +82 -5
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +19 -14
- package/dist/i18n/locales/hu.js +85 -5
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +19 -14
- package/dist/i18n/locales/it.js +84 -5
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +19 -14
- package/dist/i18n/locales/zh.js +80 -5
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +19 -14
- package/dist/i18n/review.js +27 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +21 -10
- package/dist/index.js +73 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/column-mapping.d.ts +81 -0
- package/dist/lib/column-mapping.js +108 -0
- package/dist/lib/column-mapping.js.map +1 -0
- package/dist/lib/countries.d.ts +43 -0
- package/dist/lib/countries.js +39 -0
- package/dist/lib/countries.js.map +1 -0
- package/dist/lib/decimal-marks.d.ts +46 -0
- package/dist/lib/decimal-marks.js +40 -0
- package/dist/lib/decimal-marks.js.map +1 -0
- package/dist/lib/iban.d.ts +98 -0
- package/dist/lib/iban.js +154 -0
- package/dist/lib/iban.js.map +1 -0
- package/dist/lib/isin.d.ts +30 -0
- package/dist/lib/isin.js +36 -0
- package/dist/lib/isin.js.map +1 -0
- package/dist/lib/phone.d.ts +69 -0
- package/dist/lib/phone.js +121 -0
- package/dist/lib/phone.js.map +1 -0
- package/dist/lib/table-text.d.ts +109 -1
- package/dist/lib/table-text.js +122 -1
- package/dist/lib/table-text.js.map +1 -1
- package/dist/rhf/fields.d.ts +233 -18
- package/dist/rhf/fields.js +456 -3
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +19 -14
- package/dist/rhf.d.ts +22 -15
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +18 -13
- package/dist/shell/top-bar-brand.d.ts +19 -14
- package/dist/shell/topbar-action-menu.d.ts +36 -3
- package/dist/shell/topbar-action-menu.js +74 -33
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +18 -13
- package/dist/table-text.d.ts +1 -1
- package/dist/tour/tour.js +20 -24
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +49 -16
- package/dist/wizard/stepper-nav.js +4 -0
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard.d.ts +19 -14
- package/package.json +1 -1
- package/src/components/account-chips.tsx +17 -3
- package/src/components/account-settings.tsx +92 -26
- package/src/components/amount-input.tsx +68 -56
- package/src/components/autocomplete.tsx +198 -114
- package/src/components/checkbox-group.tsx +229 -0
- package/src/components/checkbox.tsx +72 -8
- package/src/components/chip.tsx +80 -10
- package/src/components/column-mapper.tsx +666 -0
- package/src/components/combobox-core.tsx +82 -9
- package/src/components/combobox.tsx +661 -340
- package/src/components/confirm-dialog.tsx +12 -8
- package/src/components/country-select.tsx +556 -0
- package/src/components/currency-select.tsx +109 -33
- package/src/components/danger-confirm.tsx +429 -82
- package/src/components/data-table-labels.ts +16 -1
- package/src/components/data-table.tsx +175 -6
- package/src/components/date-picker.tsx +595 -71
- package/src/components/entity-combobox.tsx +235 -124
- package/src/components/field-parts.tsx +403 -0
- package/src/components/field-strip.tsx +149 -0
- package/src/components/file-button.tsx +36 -3
- package/src/components/file-dropzone.tsx +106 -38
- package/src/components/form-actions.tsx +147 -6
- package/src/components/iban-input.tsx +231 -0
- package/src/components/icon-picker.tsx +47 -6
- package/src/components/inline-edit-field.tsx +533 -0
- package/src/components/language-select.tsx +78 -0
- package/src/components/line-items.tsx +35 -2
- package/src/components/list.tsx +55 -6
- package/src/components/money-field.tsx +13 -0
- package/src/components/month-picker.tsx +400 -63
- package/src/components/multi-entity-combobox.tsx +207 -120
- package/src/components/multi-select.tsx +80 -58
- package/src/components/number-field.tsx +28 -46
- package/src/components/number-input.tsx +210 -43
- package/src/components/one-time-code-input.tsx +288 -0
- package/src/components/phone-input.tsx +299 -0
- package/src/components/reauth-dialog.tsx +17 -18
- package/src/components/sign-chip.tsx +103 -0
- package/src/components/swatch-picker.tsx +143 -31
- package/src/components/switch.tsx +58 -7
- package/src/components/tile-radio.tsx +145 -29
- package/src/components/time-input.tsx +8 -1
- package/src/components/toggle-group.tsx +206 -73
- package/src/components/trigger-aria.ts +5 -0
- package/src/components/ui.tsx +479 -138
- package/src/data-table.ts +2 -1
- package/src/feedback/feedback-attachment.tsx +597 -46
- package/src/feedback/feedback-dialog.tsx +9 -3
- package/src/feedback/feedback-inbox.tsx +3 -2
- package/src/feedback/feedback-thread.tsx +113 -9
- package/src/i18n/defaults.ts +18 -1
- package/src/i18n/german.ts +89 -0
- package/src/i18n/kit-labels.tsx +82 -2
- package/src/i18n/locales/en.ts +36 -4
- package/src/i18n/locales/es.ts +87 -0
- package/src/i18n/locales/fr.ts +87 -0
- package/src/i18n/locales/hu.ts +83 -0
- package/src/i18n/locales/it.ts +88 -0
- package/src/i18n/locales/zh.ts +76 -0
- package/src/i18n/review.ts +26 -0
- package/src/index.ts +71 -3
- package/src/lib/column-mapping.ts +234 -0
- package/src/lib/countries.ts +91 -0
- package/src/lib/decimal-marks.ts +99 -0
- package/src/lib/iban.ts +171 -0
- package/src/lib/isin.ts +62 -0
- package/src/lib/phone.ts +211 -0
- package/src/lib/table-text.ts +271 -0
- package/src/rhf/fields.tsx +860 -9
- package/src/rhf.ts +3 -1
- package/src/shell/topbar-action-menu.tsx +134 -38
- package/src/tour/tour.tsx +66 -28
- package/src/wizard/stepper-nav.tsx +35 -1
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
2
|
-
import type { ComponentPropsWithoutRef, ComponentType, ReactNode, RefObject } from "react";
|
|
3
|
-
import { Calendar, CalendarClock, ChevronLeft, ChevronRight, X } from "lucide-react";
|
|
2
|
+
import type { ComponentPropsWithoutRef, ComponentType, KeyboardEvent, ReactNode, RefObject } from "react";
|
|
3
|
+
import { Calendar, CalendarClock, ChevronDown, ChevronLeft, ChevronRight, X } from "lucide-react";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
|
-
import { dirOf, type Direction } from "../lib/direction";
|
|
6
|
-
import { addDaysIso, parseIsoDate } from "../lib/dates";
|
|
5
|
+
import { dirOf, horizontalStep, type Direction } from "../lib/direction";
|
|
6
|
+
import { addDaysIso, monthKey, pad, parseIsoDate } from "../lib/dates";
|
|
7
7
|
import {
|
|
8
8
|
DEFAULT_DATE_PICKER_LABELS,
|
|
9
9
|
DEFAULT_PICKER_SHEET_LABELS,
|
|
10
|
+
useKitDateFormatter,
|
|
10
11
|
useKitLabels,
|
|
11
12
|
useKitLocale,
|
|
12
13
|
type DatePickerLabels,
|
|
14
|
+
type KitDateFormatContext,
|
|
13
15
|
} from "../i18n/kit-labels";
|
|
14
16
|
import { splitTriggerAria } from "./trigger-aria";
|
|
15
17
|
import type { TriggerAria } from "./trigger-aria";
|
|
16
|
-
import { Button,
|
|
18
|
+
import { Button, FIELD_BASE, FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID, PHONE_QUERY } from "./ui";
|
|
19
|
+
import { FieldBox, FieldLabelLine, useFieldMessages } from "./field-parts";
|
|
17
20
|
import { FullBleedDialog } from "./full-bleed-dialog";
|
|
18
21
|
import { useMediaQuery } from "../hooks/use-media-query";
|
|
19
|
-
import { MiniCalendar, type MiniCalendarProps } from "./mini-calendar";
|
|
22
|
+
import { DEFAULT_MINI_CALENDAR_LABELS, MiniCalendar, type MiniCalendarProps } from "./mini-calendar";
|
|
23
|
+
import { DEFAULT_MONTH_PICKER_LABELS } from "./month-picker";
|
|
20
24
|
import { Popover } from "./popover";
|
|
21
25
|
import { Tooltip } from "./tooltip";
|
|
22
26
|
|
|
@@ -47,19 +51,80 @@ interface DatePickerBaseProps extends Omit<ComponentPropsWithoutRef<"div">, "onC
|
|
|
47
51
|
disabled?: boolean;
|
|
48
52
|
/** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */
|
|
49
53
|
invalid?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Standing advice for the field (keksdose K4, "uniform field anatomy"), as on a
|
|
56
|
+
* {@link Select}: plain TEXT is a caption under the field, attached with
|
|
57
|
+
* `aria-describedby`; a {@link FieldHint} rides the label line (or, unlabelled, sits
|
|
58
|
+
* at the field's end, outside the box).
|
|
59
|
+
*/
|
|
60
|
+
hint?: ReactNode;
|
|
61
|
+
/**
|
|
62
|
+
* What is wrong with the date, in the caller's words ("The due date is before the
|
|
63
|
+
* invoice date") — {@link Input}'s `error` (keksdose K4: DateField errors had no slot).
|
|
64
|
+
* Rendered under the field (under the whole row, with `step` / `today`), attached to
|
|
65
|
+
* the trigger with `aria-describedby` — merged with any the caller passed, after the
|
|
66
|
+
* caption — and implies `invalid`. Passing the key at all keeps the field's box while
|
|
67
|
+
* there is no message, so the trigger is never remounted as one comes and goes.
|
|
68
|
+
*/
|
|
69
|
+
error?: ReactNode;
|
|
50
70
|
className?: string;
|
|
51
|
-
/** Intl options for the trigger's formatted date (default: locale short date).
|
|
71
|
+
/** Intl options for the trigger's formatted date (default: locale short date). Win
|
|
72
|
+
* over the provider's `formatDate`. */
|
|
52
73
|
formatOptions?: Intl.DateTimeFormatOptions;
|
|
53
|
-
/** Render the trigger's text for an ISO date yourself; wins over `formatOptions
|
|
74
|
+
/** Render the trigger's text for an ISO date yourself; wins over `formatOptions` and
|
|
75
|
+
* over the provider's `formatDate`.
|
|
54
76
|
* For a host whose date rendering is more than one `Intl` call can say — Keksdose
|
|
55
77
|
* puts the weekday's name in the UI language beside digits ordered by a separate
|
|
56
|
-
* format preference, two locales in one string. `locale` still drives the calendar.
|
|
78
|
+
* format preference, two locales in one string. `locale` still drives the calendar.
|
|
79
|
+
* Set that rendering once with `<UiKitProvider formatDate>` (keksdose K12) and this
|
|
80
|
+
* is for the one field that differs. */
|
|
57
81
|
formatValue?: (iso: string) => string;
|
|
58
82
|
/** Passed through to the {@link MiniCalendar} this opens. Its month arrows are
|
|
59
83
|
* icon-only, so their `aria-label` is the only name they have — and the package
|
|
60
84
|
* ships English defaults, which a translating host has to be able to replace.
|
|
61
85
|
* Usually unnecessary now: the calendar reads `miniCalendar` from the provider. */
|
|
62
86
|
calendarLabels?: MiniCalendarProps["labels"];
|
|
87
|
+
/**
|
|
88
|
+
* Make the panel's caption ("July 2026") a button that opens the year's twelve
|
|
89
|
+
* months, with ‹ › stepping a year; picking a month returns to its days. Far dates
|
|
90
|
+
* stop being a march of month arrows.
|
|
91
|
+
*
|
|
92
|
+
* kastlan is why: its service-charge period runs July to June, and with one month on
|
|
93
|
+
* screen and ‹ › the only way across, the end of the period was eleven clicks past
|
|
94
|
+
* the start — so kastlan kept two date fields instead of `RhfDateRangePicker`. With
|
|
95
|
+
* the jump the whole period is open, caption, Jul, 1, caption, next year, Jun, 30.
|
|
96
|
+
*
|
|
97
|
+
* What it adds, for the keyboard and a screen reader:
|
|
98
|
+
* - the caption is a disclosure button (`aria-expanded`). Its name is the month it
|
|
99
|
+
* shows, so speech input can say what it sees; it is `aria-live`, as the
|
|
100
|
+
* calendar's caption was, so paging months still says where you landed; and it is
|
|
101
|
+
* described by `monthPicker.panel` ("Choose a month").
|
|
102
|
+
* - the month grid is `MonthPicker`'s grid: one tab stop, arrows walk the months
|
|
103
|
+
* (across the year's edge), Home/End the row, PageUp/PageDown a year, Enter or a
|
|
104
|
+
* click picks. Focus opens on the month on show and comes back on its days — the
|
|
105
|
+
* selected day if it is in that month, else the 1st. Months outside `min`/`max`
|
|
106
|
+
* are `aria-disabled`, and a year arrow is once the whole year beyond is.
|
|
107
|
+
* - Escape in the month grid goes back to the days without closing the panel (focus
|
|
108
|
+
* on the caption); in the days, Escape closes the panel as before.
|
|
109
|
+
*
|
|
110
|
+
* No new strings: the month arrows keep `miniCalendar.previousMonth` / `nextMonth`
|
|
111
|
+
* (`calendarLabels`), and the month grid speaks with `MonthPicker`'s words —
|
|
112
|
+
* `monthPicker.previousYear` / `nextYear` / `month` / `panel` — because it is that
|
|
113
|
+
* grid, and a host that translated it once should not have to again.
|
|
114
|
+
*
|
|
115
|
+
* Off by default: the caption becomes one more stop in the panel's tab order, which a
|
|
116
|
+
* 0.x minor does not spring on a form. Works in the popover and in
|
|
117
|
+
* `DateRangePicker`'s phone sheet alike, beside presets and in `commit="apply"`
|
|
118
|
+
* (a preset picked while the months are open goes back to the days, on its start).
|
|
119
|
+
*
|
|
120
|
+
* Why not a two-month panel (`months={2}`) instead, or beside it: for a twelve-month
|
|
121
|
+
* period it still pages six times where this takes one year step, and two
|
|
122
|
+
* `MiniCalendar`s side by side each follow the value on their own — picking the end
|
|
123
|
+
* in the right-hand month would pull the left one onto it — so the calendar would
|
|
124
|
+
* have to learn to share a month first. Not worth that for a saving of one arrow on a
|
|
125
|
+
* range across a single month boundary.
|
|
126
|
+
*/
|
|
127
|
+
monthJump?: boolean;
|
|
63
128
|
}
|
|
64
129
|
|
|
65
130
|
/** Format an ISO date for the trigger. `lib/dates`' `formatIsoDate` insists on a
|
|
@@ -69,6 +134,30 @@ function formatDate(iso: string, locale: string | undefined, options?: Intl.Date
|
|
|
69
134
|
return parseIsoDate(iso)?.toLocaleDateString(locale, options) ?? "";
|
|
70
135
|
}
|
|
71
136
|
|
|
137
|
+
/**
|
|
138
|
+
* The trigger's date renderer, in the kit's order of precedence: the picker's own
|
|
139
|
+
* `formatValue`, then its `formatOptions` (an explicit `Intl` look for this field),
|
|
140
|
+
* then the provider's `formatDate` (keksdose K12), then the locale's short date. A
|
|
141
|
+
* provider formatter that answers `""` falls through to the default for that date.
|
|
142
|
+
*/
|
|
143
|
+
function useTriggerFormat(
|
|
144
|
+
source: KitDateFormatContext["source"],
|
|
145
|
+
locale: string | undefined,
|
|
146
|
+
formatOptions: Intl.DateTimeFormatOptions | undefined,
|
|
147
|
+
formatValue: ((iso: string) => string) | undefined,
|
|
148
|
+
): (iso: string) => string {
|
|
149
|
+
const fromProvider = useKitDateFormatter();
|
|
150
|
+
if (formatValue) return formatValue;
|
|
151
|
+
return (iso) => {
|
|
152
|
+
if (formatOptions === undefined && fromProvider) {
|
|
153
|
+
// A single date stands alone, where the weekday helps; a range's two ends do not.
|
|
154
|
+
const text = fromProvider(iso, { unit: "day", source, locale, weekday: source === "datePicker" });
|
|
155
|
+
if (text) return text;
|
|
156
|
+
}
|
|
157
|
+
return formatDate(iso, locale, formatOptions);
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
72
161
|
/**
|
|
73
162
|
* Everything the kit's own trigger button carries, handed to a custom one
|
|
74
163
|
* ({@link DateRangePickerProps.renderTrigger}) to spread onto its `<button>`. Spread
|
|
@@ -285,6 +374,7 @@ function DateField({
|
|
|
285
374
|
sheet = false,
|
|
286
375
|
sheetBackCloses = true,
|
|
287
376
|
renderTrigger,
|
|
377
|
+
labelHint,
|
|
288
378
|
children,
|
|
289
379
|
// The trigger's, not the wrapper's: `Field`'s render-prop spreads it with the id
|
|
290
380
|
// pair, and a required date read as optional while it sat on the role-less div.
|
|
@@ -295,6 +385,8 @@ function DateField({
|
|
|
295
385
|
triggerText: string;
|
|
296
386
|
hasValue: boolean;
|
|
297
387
|
label?: ReactNode;
|
|
388
|
+
/** A `FieldHint` for the label line, beside the label. */
|
|
389
|
+
labelHint?: ReactNode;
|
|
298
390
|
disabled?: boolean;
|
|
299
391
|
clearable?: boolean;
|
|
300
392
|
/** Already resolved by the picker (prop > provider > English). */
|
|
@@ -412,7 +504,7 @@ function DateField({
|
|
|
412
504
|
// The caller's attributes land here, on the field's own box — the trigger inside is
|
|
413
505
|
// named by `aria-labelledby` and must keep the id pair it is given.
|
|
414
506
|
<div {...wrapperRest} ref={rootRef} className={cn("relative", className)}>
|
|
415
|
-
{label !== undefined && <
|
|
507
|
+
{label !== undefined && <FieldLabelLine label={label} hint={labelHint} />}
|
|
416
508
|
{/* The visible FieldLabel is a plain span, not a `<label htmlFor>`, so it names
|
|
417
509
|
nothing on its own — this hidden twin is what the trigger is named by.
|
|
418
510
|
`aria-hidden` because a name reference reads a hidden element deliberately
|
|
@@ -511,6 +603,374 @@ function StepButton({
|
|
|
511
603
|
);
|
|
512
604
|
}
|
|
513
605
|
|
|
606
|
+
// ── Month jump (`monthJump`, kastlan) ─────────────────────────────────────
|
|
607
|
+
|
|
608
|
+
/** A "YYYY-MM" key as a count of months, and back: month arithmetic with no `Date`
|
|
609
|
+
* rollover to step around. */
|
|
610
|
+
const monthIndex = (key: string) => Number(key.slice(0, 4)) * 12 + Number(key.slice(5, 7)) - 1;
|
|
611
|
+
const keyOfIndex = (index: number) =>
|
|
612
|
+
`${String(Math.floor(index / 12)).padStart(4, "0")}-${pad((index % 12) + 1)}`;
|
|
613
|
+
const shiftKey = (key: string, delta: number) => keyOfIndex(monthIndex(key) + delta);
|
|
614
|
+
/** The 1st of a "YYYY-MM" month, for `Intl` to name. */
|
|
615
|
+
const firstOfKey = (key: string) => new Date(Number(key.slice(0, 4)), Number(key.slice(5, 7)) - 1, 1);
|
|
616
|
+
|
|
617
|
+
/** Upper-case the first letter only, as `MonthPicker` does: "julio de 2026" heads a
|
|
618
|
+
* panel as "Julio de 2026" — not "Julio De 2026", which CSS `capitalize` would write. */
|
|
619
|
+
const upperFirst = (text: string, locale: string | undefined) =>
|
|
620
|
+
text.charAt(0).toLocaleUpperCase(locale) + text.slice(1);
|
|
621
|
+
|
|
622
|
+
/** Months per row of the month grid — `MonthPicker`'s 4×3. */
|
|
623
|
+
const MONTH_COLUMNS = 3;
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* The month `MiniCalendar` opens on — the selection, or today, pulled inside the
|
|
627
|
+
* bounds — worked out the way it does, so the caption drawn over a calendar that was
|
|
628
|
+
* given no `month` names the month that calendar is showing.
|
|
629
|
+
*/
|
|
630
|
+
function openingMonth(from: string, to: string, lo: Date | null, hi: Date | null): string {
|
|
631
|
+
const start = parseIsoDate(from) ?? parseIsoDate(to) ?? new Date();
|
|
632
|
+
if (lo && start < lo) return monthKey(lo);
|
|
633
|
+
if (hi && start > hi) return monthKey(hi);
|
|
634
|
+
return monthKey(start);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
type PanelCalendarProps = Pick<
|
|
638
|
+
MiniCalendarProps,
|
|
639
|
+
"mode" | "from" | "to" | "locale" | "min" | "max" | "labels" | "onSelect" | "focusOnOpen"
|
|
640
|
+
> & {
|
|
641
|
+
/** See {@link DatePickerBaseProps.monthJump}. */
|
|
642
|
+
monthJump?: boolean;
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
/** The panel's calendar: the plain {@link MiniCalendar}, exactly as before — or, with
|
|
646
|
+
* `monthJump`, the same calendar under a caption that opens a month grid. */
|
|
647
|
+
function PanelCalendar({ monthJump, ...calendar }: PanelCalendarProps) {
|
|
648
|
+
return monthJump ? <JumpCalendar {...calendar} /> : <MiniCalendar {...calendar} />;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** How the day grid is mounted: `key` remounts it, `month` is the month it follows
|
|
652
|
+
* (left out until the panel first moves, so it opens exactly where it always has), and
|
|
653
|
+
* `focus` is its `focusOnOpen`. */
|
|
654
|
+
interface DayGridMount {
|
|
655
|
+
key: number;
|
|
656
|
+
month?: string;
|
|
657
|
+
focus: boolean;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* {@link MiniCalendar} with its navigation drawn here — ‹ caption › — so the caption can
|
|
662
|
+
* be a button that swaps the days for a month grid ({@link DatePickerBaseProps.monthJump}).
|
|
663
|
+
*
|
|
664
|
+
* Built on the calendar's own `hideNavigation` / `month` / `onMonthChange`, not a fork
|
|
665
|
+
* of it: the days, their keys, the range band and every announcement stay the
|
|
666
|
+
* calendar's. What this keeps is `shown`, the month on screen, for the caption — fed by
|
|
667
|
+
* the arrows here, by the calendar's `onMonthChange` (PageUp, arrowing off the edge),
|
|
668
|
+
* and by the value, whose changes the calendar follows WITHOUT reporting them (a preset
|
|
669
|
+
* in `commit="apply"` moves the grid to its start). That last one is mirrored below with
|
|
670
|
+
* the calendar's own rule, and when it lands in another month the day grid is remounted
|
|
671
|
+
* there rather than steered by `month`: a `month` that changes in the same render as the
|
|
672
|
+
* value would win over the value's day, and the grid's tab stop would land on the
|
|
673
|
+
* month's same-numbered day instead of the preset's start.
|
|
674
|
+
*
|
|
675
|
+
* The month grid is `MonthPicker`'s, redrawn here because that one is internal to its
|
|
676
|
+
* file; same roles, keys and words (`monthPicker` labels).
|
|
677
|
+
*/
|
|
678
|
+
function JumpCalendar({
|
|
679
|
+
from = "",
|
|
680
|
+
to = "",
|
|
681
|
+
mode = "range",
|
|
682
|
+
locale,
|
|
683
|
+
min,
|
|
684
|
+
max,
|
|
685
|
+
labels,
|
|
686
|
+
onSelect,
|
|
687
|
+
focusOnOpen,
|
|
688
|
+
}: Omit<PanelCalendarProps, "monthJump">) {
|
|
689
|
+
// The month arrows say what they said when the calendar drew them; the month grid
|
|
690
|
+
// says what `MonthPicker`'s says.
|
|
691
|
+
const calendarText = useKitLabels("miniCalendar", DEFAULT_MINI_CALENDAR_LABELS, labels);
|
|
692
|
+
const monthText = useKitLabels("monthPicker", DEFAULT_MONTH_PICKER_LABELS);
|
|
693
|
+
const id = useId();
|
|
694
|
+
const captionId = `${id}-caption`;
|
|
695
|
+
const hintId = `${id}-hint`;
|
|
696
|
+
const captionRef = useRef<HTMLButtonElement>(null);
|
|
697
|
+
const monthsRef = useRef<HTMLDivElement>(null);
|
|
698
|
+
|
|
699
|
+
const lo = min ? parseIsoDate(min) : null;
|
|
700
|
+
const hi = max ? parseIsoDate(max) : null;
|
|
701
|
+
const minKey = lo ? monthKey(lo) : null;
|
|
702
|
+
const maxKey = hi ? monthKey(hi) : null;
|
|
703
|
+
const outOfBounds = (key: string) => Boolean((minKey && key < minKey) || (maxKey && key > maxKey));
|
|
704
|
+
const clampKey = (key: string) => (minKey && key < minKey ? minKey : maxKey && key > maxKey ? maxKey : key);
|
|
705
|
+
|
|
706
|
+
const [view, setView] = useState<"days" | "months">("days");
|
|
707
|
+
const [shown, setShown] = useState(() => openingMonth(from, to, lo, hi));
|
|
708
|
+
const [dayGrid, setDayGrid] = useState<DayGridMount>(() => ({ key: 0, focus: Boolean(focusOnOpen) }));
|
|
709
|
+
// The month holding the month grid's tab stop — and with it the year on show.
|
|
710
|
+
const [active, setActive] = useState(shown);
|
|
711
|
+
|
|
712
|
+
// The value moved from outside the day grid (a preset, a reset): follow the end that
|
|
713
|
+
// changed, by the calendar's own rule, and remount the days there — see above. Also
|
|
714
|
+
// what takes an open month grid back to the days when a preset is picked beside it.
|
|
715
|
+
const [lastValue, setLastValue] = useState({ from, to });
|
|
716
|
+
if (lastValue.from !== from || lastValue.to !== to) {
|
|
717
|
+
const changed = lastValue.from !== from ? from : to;
|
|
718
|
+
setLastValue({ from, to });
|
|
719
|
+
const next = parseIsoDate(changed) ?? parseIsoDate(from) ?? parseIsoDate(to);
|
|
720
|
+
const key = next ? monthKey(next) : null;
|
|
721
|
+
if (key && (key !== shown || view === "months")) {
|
|
722
|
+
setShown(key);
|
|
723
|
+
setView("days");
|
|
724
|
+
setDayGrid((d) => ({ key: d.key + 1, month: key, focus: false }));
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// A month cell that has to take focus once it exists: opening the grid, and arrowing
|
|
729
|
+
// across a year's edge, which relabels the same twelve buttons.
|
|
730
|
+
const pendingFocus = useRef<string | null>(null);
|
|
731
|
+
useEffect(() => {
|
|
732
|
+
const key = pendingFocus.current;
|
|
733
|
+
if (!key) return;
|
|
734
|
+
pendingFocus.current = null;
|
|
735
|
+
monthsRef.current?.querySelector<HTMLElement>(`[data-month="${key}"]`)?.focus();
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
const showMonth = (key: string) => {
|
|
739
|
+
setShown(key);
|
|
740
|
+
// Follows by `month`: the calendar keeps the day of the month (clamped), and the
|
|
741
|
+
// focus stays on the arrow being pressed.
|
|
742
|
+
setDayGrid((d) => ({ ...d, month: key }));
|
|
743
|
+
};
|
|
744
|
+
const openMonths = () => {
|
|
745
|
+
const start = clampKey(shown);
|
|
746
|
+
pendingFocus.current = start;
|
|
747
|
+
setActive(start);
|
|
748
|
+
setView("months");
|
|
749
|
+
};
|
|
750
|
+
const closeMonths = (focusCaption: boolean) => {
|
|
751
|
+
setView("days");
|
|
752
|
+
setDayGrid((d) => ({ ...d, month: shown, focus: false }));
|
|
753
|
+
if (focusCaption) captionRef.current?.focus();
|
|
754
|
+
};
|
|
755
|
+
const pick = (key: string) => {
|
|
756
|
+
setShown(key);
|
|
757
|
+
setView("days");
|
|
758
|
+
// Picked to go there: the focus goes with it, onto the days.
|
|
759
|
+
setDayGrid((d) => ({ key: d.key + 1, month: key, focus: true }));
|
|
760
|
+
};
|
|
761
|
+
const moveActive = (key: string, focus: boolean) => {
|
|
762
|
+
if (focus) pendingFocus.current = key;
|
|
763
|
+
setActive(key);
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
const inDays = view === "days";
|
|
767
|
+
// Escape in the month grid is "back", not "close": the month grid is a step INSIDE the
|
|
768
|
+
// panel, and losing the half-made range to a key meant to undo that step would be the
|
|
769
|
+
// worse surprise. Stopped here, so neither the popover nor the phone sheet sees it.
|
|
770
|
+
const escapeToDays = (e: KeyboardEvent<HTMLElement>) => {
|
|
771
|
+
if (e.key !== "Escape" || inDays) return;
|
|
772
|
+
e.preventDefault();
|
|
773
|
+
e.stopPropagation();
|
|
774
|
+
closeMonths(true);
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
const onMonthKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {
|
|
778
|
+
if (e.key === "Escape") return escapeToDays(e);
|
|
779
|
+
// Left and right are visual: the grid runs the other way in a right-to-left panel.
|
|
780
|
+
const step = horizontalStep(e.key, e.currentTarget);
|
|
781
|
+
const col = (monthIndex(active) % 12) % MONTH_COLUMNS;
|
|
782
|
+
let delta: number;
|
|
783
|
+
switch (step ? "horizontal" : e.key) {
|
|
784
|
+
case "horizontal":
|
|
785
|
+
delta = step;
|
|
786
|
+
break;
|
|
787
|
+
case "ArrowUp":
|
|
788
|
+
delta = -MONTH_COLUMNS;
|
|
789
|
+
break;
|
|
790
|
+
case "ArrowDown":
|
|
791
|
+
delta = MONTH_COLUMNS;
|
|
792
|
+
break;
|
|
793
|
+
case "Home":
|
|
794
|
+
delta = -col;
|
|
795
|
+
break;
|
|
796
|
+
case "End":
|
|
797
|
+
delta = MONTH_COLUMNS - 1 - col;
|
|
798
|
+
break;
|
|
799
|
+
case "PageUp":
|
|
800
|
+
delta = -12;
|
|
801
|
+
break;
|
|
802
|
+
case "PageDown":
|
|
803
|
+
delta = 12;
|
|
804
|
+
break;
|
|
805
|
+
default:
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
// Before the no-op check: these keys also scroll the panel.
|
|
809
|
+
e.preventDefault();
|
|
810
|
+
if (delta !== 0) moveActive(shiftKey(active, delta), true);
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
const year = Number(active.slice(0, 4));
|
|
814
|
+
// A year arrow is dead once every month of the year it leads to is out of bounds.
|
|
815
|
+
const prevYearBlocked = Boolean(minKey && year - 1 < Number(minKey.slice(0, 4)));
|
|
816
|
+
const nextYearBlocked = Boolean(maxKey && year + 1 > Number(maxKey.slice(0, 4)));
|
|
817
|
+
const prevBlocked = !inDays && prevYearBlocked;
|
|
818
|
+
const nextBlocked = !inDays && nextYearBlocked;
|
|
819
|
+
|
|
820
|
+
const longName = useMemo(() => new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" }), [locale]);
|
|
821
|
+
const shortNames = useMemo(() => {
|
|
822
|
+
const fmt = new Intl.DateTimeFormat(locale, { month: "short" });
|
|
823
|
+
return Array.from({ length: 12 }, (_, i) => upperFirst(fmt.format(new Date(2000, i, 1)), locale));
|
|
824
|
+
}, [locale]);
|
|
825
|
+
const caption = inDays
|
|
826
|
+
? upperFirst(longName.format(firstOfKey(shown)), locale)
|
|
827
|
+
: new Intl.DateTimeFormat(locale, { year: "numeric" }).format(new Date(year, 0, 1));
|
|
828
|
+
|
|
829
|
+
// The selection, as months: both ends, and the months between (a range's band).
|
|
830
|
+
const fromDate = parseIsoDate(from);
|
|
831
|
+
const toDate = parseIsoDate(to);
|
|
832
|
+
const fromKey = fromDate ? monthKey(fromDate) : null;
|
|
833
|
+
const toKey = toDate ? monthKey(toDate) : null;
|
|
834
|
+
const low = fromKey ?? toKey;
|
|
835
|
+
const high = toKey ?? fromKey;
|
|
836
|
+
const currentKey = monthKey(new Date());
|
|
837
|
+
|
|
838
|
+
// Aria-disabled rather than disabled, like the day cells: a focused button that
|
|
839
|
+
// turns `disabled` drops the focus to <body>, mid-panel.
|
|
840
|
+
const arrow =
|
|
841
|
+
"rounded p-1 text-[var(--text-muted)] hover:bg-[var(--bg-hover)] aria-disabled:cursor-not-allowed aria-disabled:opacity-30 aria-disabled:hover:bg-transparent";
|
|
842
|
+
|
|
843
|
+
return (
|
|
844
|
+
<div className="select-none">
|
|
845
|
+
{/* The calendar's own row, redrawn: the same arrows in the same places. Each
|
|
846
|
+
button stays mounted across the two views and changes what it steps, so a
|
|
847
|
+
press never pulls the element out from under the pointer or the focus. */}
|
|
848
|
+
<div className="flex items-center justify-between gap-1 pb-1">
|
|
849
|
+
<button
|
|
850
|
+
type="button"
|
|
851
|
+
aria-label={inDays ? calendarText.previousMonth : monthText.previousYear}
|
|
852
|
+
aria-disabled={prevBlocked || undefined}
|
|
853
|
+
onClick={() => {
|
|
854
|
+
if (inDays) showMonth(shiftKey(shown, -1));
|
|
855
|
+
else if (!prevBlocked) moveActive(shiftKey(active, -12), false);
|
|
856
|
+
}}
|
|
857
|
+
onKeyDown={escapeToDays}
|
|
858
|
+
className={arrow}
|
|
859
|
+
>
|
|
860
|
+
{/* Mirrored in RTL: "previous" points to the reading START, which is right. */}
|
|
861
|
+
<ChevronLeft className="size-4 rtl:-scale-x-100" aria-hidden />
|
|
862
|
+
</button>
|
|
863
|
+
<button
|
|
864
|
+
ref={captionRef}
|
|
865
|
+
type="button"
|
|
866
|
+
aria-expanded={!inDays}
|
|
867
|
+
aria-describedby={inDays ? hintId : undefined}
|
|
868
|
+
onClick={inDays ? openMonths : () => closeMonths(false)}
|
|
869
|
+
onKeyDown={escapeToDays}
|
|
870
|
+
className="inline-flex min-w-0 items-center gap-1 rounded px-1.5 py-0.5 text-xs font-medium text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]"
|
|
871
|
+
>
|
|
872
|
+
{/* The name is the text, so it says what is on screen; `aria-live` because
|
|
873
|
+
pressing an arrow leaves focus ON the arrow, and this is the only thing
|
|
874
|
+
that says where it went. It also names the month grid. */}
|
|
875
|
+
<span id={captionId} aria-live="polite" className="truncate">
|
|
876
|
+
{caption}
|
|
877
|
+
</span>
|
|
878
|
+
<ChevronDown
|
|
879
|
+
aria-hidden
|
|
880
|
+
className={cn("size-3.5 shrink-0 text-[var(--text-muted)]", !inDays && "rotate-180")}
|
|
881
|
+
/>
|
|
882
|
+
</button>
|
|
883
|
+
<button
|
|
884
|
+
type="button"
|
|
885
|
+
aria-label={inDays ? calendarText.nextMonth : monthText.nextYear}
|
|
886
|
+
aria-disabled={nextBlocked || undefined}
|
|
887
|
+
onClick={() => {
|
|
888
|
+
if (inDays) showMonth(shiftKey(shown, 1));
|
|
889
|
+
else if (!nextBlocked) moveActive(shiftKey(active, 12), false);
|
|
890
|
+
}}
|
|
891
|
+
onKeyDown={escapeToDays}
|
|
892
|
+
className={arrow}
|
|
893
|
+
>
|
|
894
|
+
<ChevronRight className="size-4 rtl:-scale-x-100" aria-hidden />
|
|
895
|
+
</button>
|
|
896
|
+
</div>
|
|
897
|
+
{/* The caption's description — read through the reference, never on its own. */}
|
|
898
|
+
<span id={hintId} hidden>
|
|
899
|
+
{monthText.panel}
|
|
900
|
+
</span>
|
|
901
|
+
{inDays ? (
|
|
902
|
+
<MiniCalendar
|
|
903
|
+
key={dayGrid.key}
|
|
904
|
+
mode={mode}
|
|
905
|
+
from={from}
|
|
906
|
+
to={to}
|
|
907
|
+
locale={locale}
|
|
908
|
+
min={min}
|
|
909
|
+
max={max}
|
|
910
|
+
labels={labels}
|
|
911
|
+
onSelect={onSelect}
|
|
912
|
+
focusOnOpen={dayGrid.focus}
|
|
913
|
+
hideNavigation
|
|
914
|
+
month={dayGrid.month}
|
|
915
|
+
onMonthChange={showMonth}
|
|
916
|
+
/>
|
|
917
|
+
) : (
|
|
918
|
+
<div ref={monthsRef} role="grid" aria-labelledby={captionId} className="grid gap-1">
|
|
919
|
+
{Array.from({ length: 12 / MONTH_COLUMNS }, (_, row) => (
|
|
920
|
+
<div key={row} role="row" className="grid grid-cols-3 gap-1">
|
|
921
|
+
{Array.from({ length: MONTH_COLUMNS }, (_, c) => {
|
|
922
|
+
const index = row * MONTH_COLUMNS + c;
|
|
923
|
+
const key = keyOfIndex(year * 12 + index);
|
|
924
|
+
const disabled = outOfBounds(key);
|
|
925
|
+
const selected = low !== null && high !== null && key >= low && key <= high;
|
|
926
|
+
const end = key === fromKey || key === toKey;
|
|
927
|
+
const current = key === currentKey;
|
|
928
|
+
return (
|
|
929
|
+
<button
|
|
930
|
+
key={c}
|
|
931
|
+
type="button"
|
|
932
|
+
role="gridcell"
|
|
933
|
+
data-month={key}
|
|
934
|
+
tabIndex={key === active ? 0 : -1}
|
|
935
|
+
aria-label={monthText.month(longName.format(firstOfKey(key)))}
|
|
936
|
+
aria-selected={selected}
|
|
937
|
+
aria-disabled={disabled || undefined}
|
|
938
|
+
aria-current={current ? "date" : undefined}
|
|
939
|
+
onClick={() => {
|
|
940
|
+
// The guard `disabled` would have been: out-of-bounds months stay
|
|
941
|
+
// focusable, so the roving tab stop never falls into a hole.
|
|
942
|
+
if (!disabled) pick(key);
|
|
943
|
+
}}
|
|
944
|
+
onKeyDown={onMonthKeyDown}
|
|
945
|
+
className={cn(
|
|
946
|
+
// 44px rows: the touch-target size, and four of them stand as tall
|
|
947
|
+
// as a month of days, so the panel does not jump as the views swap.
|
|
948
|
+
"h-11 rounded px-1 text-sm tabular-nums transition-colors",
|
|
949
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset",
|
|
950
|
+
// A --brand ring on a --brand fill exists only in the DOM.
|
|
951
|
+
end && !disabled ? "focus-visible:ring-[var(--brand-contrast)]" : "focus-visible:ring-[var(--brand)]",
|
|
952
|
+
disabled
|
|
953
|
+
? "cursor-not-allowed text-[var(--text-placeholder)]"
|
|
954
|
+
: end
|
|
955
|
+
? "bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)]"
|
|
956
|
+
: selected
|
|
957
|
+
? "bg-[var(--brand-bg)] font-medium text-[var(--brand-muted)] hover:bg-[var(--brand-bg-hover)]"
|
|
958
|
+
: "text-[var(--text-primary)] hover:bg-[var(--bg-hover)]",
|
|
959
|
+
current && !selected && !disabled && "ring-1 ring-inset ring-[var(--border-strong)]",
|
|
960
|
+
)}
|
|
961
|
+
>
|
|
962
|
+
{shortNames[index]}
|
|
963
|
+
</button>
|
|
964
|
+
);
|
|
965
|
+
})}
|
|
966
|
+
</div>
|
|
967
|
+
))}
|
|
968
|
+
</div>
|
|
969
|
+
)}
|
|
970
|
+
</div>
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
|
|
514
974
|
export interface DatePickerProps extends DatePickerBaseProps {
|
|
515
975
|
/** ISO "YYYY-MM-DD", or "" for empty. */
|
|
516
976
|
value: string;
|
|
@@ -568,35 +1028,62 @@ function useDatePickerLabels(
|
|
|
568
1028
|
return useKitLabels("datePicker", DEFAULT_DATE_PICKER_LABELS, fromProps);
|
|
569
1029
|
}
|
|
570
1030
|
|
|
1031
|
+
/**
|
|
1032
|
+
* The caller's `hint` when it has no label line to ride: at the end of the field,
|
|
1033
|
+
* outside the box, as an unlabelled {@link Select} places its FieldHint (0.15.5 P8).
|
|
1034
|
+
*/
|
|
1035
|
+
function withEndHint(field: ReactNode, hint: ReactNode): ReactNode {
|
|
1036
|
+
if (hint === undefined) return field;
|
|
1037
|
+
return (
|
|
1038
|
+
<div className="flex items-center gap-1.5">
|
|
1039
|
+
<div className="min-w-0 flex-1">{field}</div>
|
|
1040
|
+
<span className="flex shrink-0 items-center">{hint}</span>
|
|
1041
|
+
</div>
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
571
1045
|
/** Single-date picker: a field showing the formatted date, opening a calendar. */
|
|
572
|
-
export function DatePicker({
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
1046
|
+
export function DatePicker(props: DatePickerProps) {
|
|
1047
|
+
const {
|
|
1048
|
+
value,
|
|
1049
|
+
onChange,
|
|
1050
|
+
locale: localeProp,
|
|
1051
|
+
placeholder,
|
|
1052
|
+
min,
|
|
1053
|
+
max,
|
|
1054
|
+
formatOptions,
|
|
1055
|
+
formatValue,
|
|
1056
|
+
step,
|
|
1057
|
+
stepLabels,
|
|
1058
|
+
today,
|
|
1059
|
+
todayLabel,
|
|
1060
|
+
className,
|
|
1061
|
+
calendarLabels,
|
|
1062
|
+
monthJump,
|
|
1063
|
+
label,
|
|
1064
|
+
clearable,
|
|
1065
|
+
clearLabel,
|
|
1066
|
+
disabled,
|
|
1067
|
+
invalid,
|
|
1068
|
+
hint,
|
|
1069
|
+
error,
|
|
1070
|
+
// Named so it reaches the field (and so its trigger) when a step/today row is the
|
|
1071
|
+
// root and takes the rest of the caller's props.
|
|
1072
|
+
"aria-required": ariaRequired,
|
|
1073
|
+
...rest
|
|
1074
|
+
} = props;
|
|
597
1075
|
const locale = useKitLocale(localeProp);
|
|
598
1076
|
const text = useDatePickerLabels(clearLabel, stepLabels, todayLabel);
|
|
599
|
-
const render =
|
|
1077
|
+
const render = useTriggerFormat("datePicker", locale, formatOptions, formatValue);
|
|
1078
|
+
// keksdose K4: the caption and the error under the field, attached to the TRIGGER
|
|
1079
|
+
// (the element a screen reader is on) after any description of the caller's own.
|
|
1080
|
+
const messages = useFieldMessages({
|
|
1081
|
+
hint,
|
|
1082
|
+
error,
|
|
1083
|
+
invalid,
|
|
1084
|
+
describedBy: rest["aria-describedby"],
|
|
1085
|
+
ariaInvalid: rest["aria-invalid"],
|
|
1086
|
+
});
|
|
600
1087
|
// With a step or a today button beside it the field is no longer the outermost
|
|
601
1088
|
// element — the flex row at the bottom is, and a caller's attributes belong on
|
|
602
1089
|
// whichever of the two is actually the root. Naming the shared props rather than
|
|
@@ -608,20 +1095,23 @@ export function DatePicker({
|
|
|
608
1095
|
// With step/today buttons the flex row is the root and takes the caller's props —
|
|
609
1096
|
// except the ones that name the field, which still belong on its trigger.
|
|
610
1097
|
{...(wrapped ? triggerAria : rest)}
|
|
1098
|
+
aria-describedby={messages.describedBy}
|
|
611
1099
|
aria-required={ariaRequired}
|
|
612
1100
|
label={label}
|
|
1101
|
+
labelHint={messages.labelHint}
|
|
613
1102
|
clearable={clearable}
|
|
614
1103
|
clearLabel={text.clear}
|
|
615
1104
|
panelLabel={text.panel}
|
|
616
1105
|
disabled={disabled}
|
|
617
|
-
invalid={
|
|
1106
|
+
invalid={messages.isInvalid}
|
|
618
1107
|
className={step ? "min-w-0 flex-1" : className}
|
|
619
1108
|
hasValue={Boolean(value)}
|
|
620
1109
|
triggerText={value ? render(value) : (placeholder ?? "")}
|
|
621
1110
|
onClear={() => onChange("")}
|
|
622
1111
|
>
|
|
623
1112
|
{(close) => (
|
|
624
|
-
<
|
|
1113
|
+
<PanelCalendar
|
|
1114
|
+
monthJump={monthJump}
|
|
625
1115
|
mode="single"
|
|
626
1116
|
focusOnOpen
|
|
627
1117
|
from={value}
|
|
@@ -638,7 +1128,16 @@ export function DatePicker({
|
|
|
638
1128
|
)}
|
|
639
1129
|
</DateField>
|
|
640
1130
|
);
|
|
641
|
-
|
|
1131
|
+
// Passed at all — even as `undefined` — the field keeps its box; see FieldBox.
|
|
1132
|
+
const reserve = "error" in props || "hint" in props;
|
|
1133
|
+
const endHint = label === undefined ? messages.labelHint : undefined;
|
|
1134
|
+
if (!wrapped) {
|
|
1135
|
+
return (
|
|
1136
|
+
<FieldBox below={messages.below} reserve={reserve}>
|
|
1137
|
+
{withEndHint(field, endHint)}
|
|
1138
|
+
</FieldBox>
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
642
1141
|
|
|
643
1142
|
// An empty field has nothing to step from, so both buttons are dead until a date
|
|
644
1143
|
// is picked. Bounds are compared as strings: "YYYY-MM-DD" sorts chronologically.
|
|
@@ -649,7 +1148,7 @@ export function DatePicker({
|
|
|
649
1148
|
if (!next || disabled) return true;
|
|
650
1149
|
return outOfBounds(next);
|
|
651
1150
|
};
|
|
652
|
-
|
|
1151
|
+
const row = (
|
|
653
1152
|
// ONE joined control, not three boxes in a row: the buttons and the field share
|
|
654
1153
|
// their borders (each piece overlaps the previous by a pixel) and only the outer
|
|
655
1154
|
// ends are rounded — the button-group shape, so ‹ date › reads as one field with
|
|
@@ -701,6 +1200,13 @@ export function DatePicker({
|
|
|
701
1200
|
)}
|
|
702
1201
|
</div>
|
|
703
1202
|
);
|
|
1203
|
+
// Under the whole row, not under the field between the buttons: the message is about
|
|
1204
|
+
// the date, and the steppers are part of how it is entered.
|
|
1205
|
+
return (
|
|
1206
|
+
<FieldBox below={messages.below} reserve={reserve}>
|
|
1207
|
+
{withEndHint(row, endHint)}
|
|
1208
|
+
</FieldBox>
|
|
1209
|
+
);
|
|
704
1210
|
}
|
|
705
1211
|
|
|
706
1212
|
export interface DateRangePickerPreset {
|
|
@@ -939,7 +1445,7 @@ function RangePanel({
|
|
|
939
1445
|
frame: PanelFrame;
|
|
940
1446
|
sheet: boolean;
|
|
941
1447
|
commitRange: (from: string, to: string, presetId: string | undefined) => void;
|
|
942
|
-
calendarProps: Pick<
|
|
1448
|
+
calendarProps: Pick<PanelCalendarProps, "locale" | "min" | "max" | "labels" | "monthJump">;
|
|
943
1449
|
labels: DatePickerLabels;
|
|
944
1450
|
renderDraftSummary?: DateRangePickerProps["renderDraftSummary"];
|
|
945
1451
|
}) {
|
|
@@ -962,7 +1468,7 @@ function RangePanel({
|
|
|
962
1468
|
: null;
|
|
963
1469
|
|
|
964
1470
|
const picker = (
|
|
965
|
-
<
|
|
1471
|
+
<PanelCalendar
|
|
966
1472
|
{...calendarProps}
|
|
967
1473
|
focusOnOpen
|
|
968
1474
|
from={shown.from}
|
|
@@ -1052,34 +1558,45 @@ function RangePanel({
|
|
|
1052
1558
|
/** Two-date range picker: click a start then an end; closes once both are set.
|
|
1053
1559
|
* With `presets`, a column of named ranges is shown beside the calendar; with
|
|
1054
1560
|
* `commit="apply"` the panel drafts and commits on Apply (see {@link DateRangeCommit}). */
|
|
1055
|
-
export function DateRangePicker({
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1561
|
+
export function DateRangePicker(props: DateRangePickerProps) {
|
|
1562
|
+
const {
|
|
1563
|
+
from,
|
|
1564
|
+
to,
|
|
1565
|
+
onChange,
|
|
1566
|
+
locale: localeProp,
|
|
1567
|
+
placeholder,
|
|
1568
|
+
min,
|
|
1569
|
+
max,
|
|
1570
|
+
formatOptions,
|
|
1571
|
+
formatValue,
|
|
1572
|
+
separator = " – ",
|
|
1573
|
+
presets,
|
|
1574
|
+
preset,
|
|
1575
|
+
commit = "immediate",
|
|
1576
|
+
renderTrigger,
|
|
1577
|
+
sheetBackCloses,
|
|
1578
|
+
renderDraftSummary,
|
|
1579
|
+
calendarLabels,
|
|
1580
|
+
monthJump,
|
|
1581
|
+
label,
|
|
1582
|
+
clearable,
|
|
1583
|
+
clearLabel,
|
|
1584
|
+
disabled,
|
|
1585
|
+
invalid,
|
|
1586
|
+
hint,
|
|
1587
|
+
error,
|
|
1588
|
+
...rest
|
|
1589
|
+
} = props;
|
|
1080
1590
|
const locale = useKitLocale(localeProp);
|
|
1081
1591
|
const text = useDatePickerLabels(clearLabel, undefined, undefined);
|
|
1082
|
-
const render =
|
|
1592
|
+
const render = useTriggerFormat("dateRangePicker", locale, formatOptions, formatValue);
|
|
1593
|
+
const messages = useFieldMessages({
|
|
1594
|
+
hint,
|
|
1595
|
+
error,
|
|
1596
|
+
invalid,
|
|
1597
|
+
describedBy: rest["aria-describedby"],
|
|
1598
|
+
ariaInvalid: rest["aria-invalid"],
|
|
1599
|
+
});
|
|
1083
1600
|
// The preset this picker committed itself, for a caller that does not control
|
|
1084
1601
|
// `preset`. Cleared by a hand-picked range or a clear.
|
|
1085
1602
|
const [ownPreset, setOwnPreset] = useState<string | undefined>(undefined);
|
|
@@ -1101,15 +1618,17 @@ export function DateRangePicker({
|
|
|
1101
1618
|
if (presetId === undefined) onChange(f, t);
|
|
1102
1619
|
else onChange(f, t, presetId);
|
|
1103
1620
|
};
|
|
1104
|
-
|
|
1621
|
+
const field = (
|
|
1105
1622
|
<DateField
|
|
1106
1623
|
{...rest}
|
|
1624
|
+
aria-describedby={messages.describedBy}
|
|
1107
1625
|
label={label}
|
|
1626
|
+
labelHint={messages.labelHint}
|
|
1108
1627
|
clearable={clearable}
|
|
1109
1628
|
clearLabel={text.clear}
|
|
1110
1629
|
panelLabel={text.rangePanel}
|
|
1111
1630
|
disabled={disabled}
|
|
1112
|
-
invalid={
|
|
1631
|
+
invalid={messages.isInvalid}
|
|
1113
1632
|
// Widen so the preset column sits beside the calendar (default otherwise).
|
|
1114
1633
|
width={hasPresets ? 440 : undefined}
|
|
1115
1634
|
hasValue={Boolean(from || to)}
|
|
@@ -1143,11 +1662,16 @@ export function DateRangePicker({
|
|
|
1143
1662
|
frame={frame}
|
|
1144
1663
|
sheet={sheet}
|
|
1145
1664
|
commitRange={commitRange}
|
|
1146
|
-
calendarProps={{ locale, min, max, labels: calendarLabels }}
|
|
1665
|
+
calendarProps={{ locale, min, max, labels: calendarLabels, monthJump }}
|
|
1147
1666
|
labels={text}
|
|
1148
1667
|
renderDraftSummary={renderDraftSummary}
|
|
1149
1668
|
/>
|
|
1150
1669
|
)}
|
|
1151
1670
|
</DateField>
|
|
1152
1671
|
);
|
|
1672
|
+
return (
|
|
1673
|
+
<FieldBox below={messages.below} reserve={"error" in props || "hint" in props}>
|
|
1674
|
+
{withEndHint(field, label === undefined ? messages.labelHint : undefined)}
|
|
1675
|
+
</FieldBox>
|
|
1676
|
+
);
|
|
1153
1677
|
}
|