@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
package/src/rhf/fields.tsx
CHANGED
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
* <RhfSelect name="type" label="Type" options={TYPES} />
|
|
11
11
|
* <RhfCombobox name="tenantId" label="Tenant" options={tenants} clearable />
|
|
12
12
|
* <RhfCheckbox name="isDefault" label="Default account" />
|
|
13
|
+
* <RhfTimeInput name="meetingTime" label="Time" />
|
|
14
|
+
* <RhfDateRangePicker fromName="periodFrom" toName="periodTo" label="Period" />
|
|
15
|
+
* <RhfToggleGroup name="interval" label="Interval" options={INTERVALS} />
|
|
16
|
+
* <RhfCountrySelect name="country" label="Country" preferred={["CH", "DE"]} />
|
|
17
|
+
* <RhfMonthPicker name="fiscalYear" label="Fiscal year" mode="year" valueAsNumber />
|
|
13
18
|
* </Form>
|
|
14
19
|
* ```
|
|
15
20
|
*
|
|
@@ -41,19 +46,36 @@
|
|
|
41
46
|
* {@link RhfField} is the shell they are all built on, for a control the kit does not
|
|
42
47
|
* ship (an address autocomplete): it takes a `render` and wires the rest.
|
|
43
48
|
*/
|
|
44
|
-
import { useLayoutEffect, useRef, useState, type ReactElement, type ReactNode } from "react";
|
|
45
|
-
import
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
import { useLayoutEffect, useRef, useState, type FocusEvent, type ReactElement, type ReactNode } from "react";
|
|
50
|
+
import {
|
|
51
|
+
useController,
|
|
52
|
+
type ControllerFieldState,
|
|
53
|
+
type ControllerProps,
|
|
54
|
+
type ControllerRenderProps,
|
|
55
|
+
type FieldPath,
|
|
56
|
+
type FieldValues,
|
|
51
57
|
} from "react-hook-form";
|
|
52
58
|
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useFormField } from "./form";
|
|
53
59
|
import { Input, Select, Textarea, type InputProps, type SelectProps, type TextareaProps } from "../components/ui";
|
|
54
60
|
import { NumberField, type NumberFieldProps } from "../components/number-field";
|
|
55
61
|
import { AmountInput } from "../components/amount-input";
|
|
56
|
-
import {
|
|
62
|
+
import {
|
|
63
|
+
DatePicker,
|
|
64
|
+
DateRangePicker,
|
|
65
|
+
type DatePickerProps,
|
|
66
|
+
type DateRangePickerProps,
|
|
67
|
+
} from "../components/date-picker";
|
|
68
|
+
import { TimeInput, type TimeInputProps } from "../components/time-input";
|
|
69
|
+
import { IbanInput, type IbanInputProps } from "../components/iban-input";
|
|
70
|
+
import { PhoneInput, type PhoneInputProps } from "../components/phone-input";
|
|
71
|
+
import { CountrySelect, type CountrySelectProps } from "../components/country-select";
|
|
72
|
+
import { MonthPicker, type MonthPickerProps } from "../components/month-picker";
|
|
73
|
+
import {
|
|
74
|
+
ToggleGroup,
|
|
75
|
+
type ToggleGroupBaseProps,
|
|
76
|
+
type ToggleGroupClearableProps,
|
|
77
|
+
type ToggleGroupRequiredProps,
|
|
78
|
+
} from "../components/toggle-group";
|
|
57
79
|
import { Checkbox, type CheckboxProps } from "../components/checkbox";
|
|
58
80
|
import { EntityCombobox, type EntityComboboxProps } from "../components/entity-combobox";
|
|
59
81
|
import { Combobox, type ComboboxProps } from "../components/combobox";
|
|
@@ -553,6 +575,10 @@ export type RhfMoneyFieldProps<
|
|
|
553
575
|
* with `rules` as well if a clamp needs explaining. */
|
|
554
576
|
min?: number;
|
|
555
577
|
max?: number;
|
|
578
|
+
/** Classes for the `<input>` — `className` is the item's box. {@link RhfTextField},
|
|
579
|
+
* {@link RhfTextarea} and {@link RhfNumberField} take it; since 0.23 AmountInput
|
|
580
|
+
* does too (keksdose G7: `text-end` for a money column). */
|
|
581
|
+
inputClassName?: string;
|
|
556
582
|
};
|
|
557
583
|
|
|
558
584
|
/** The amount a text reads as, or `undefined` for a draft that is not one yet. */
|
|
@@ -679,7 +705,9 @@ export type RhfDateFieldProps<
|
|
|
679
705
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
680
706
|
TTransformed = TFieldValues,
|
|
681
707
|
> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &
|
|
682
|
-
|
|
708
|
+
// `error`: the form's message is shown by the binding (FormMessage); the picker's own
|
|
709
|
+
// `error` (0.22) would show it a second time.
|
|
710
|
+
Omit<DatePickerProps, "value" | "onChange" | "label" | "invalid" | "disabled" | "className" | "id" | "error"> & {
|
|
683
711
|
/** Classes for the picker. Default `w-full`. */
|
|
684
712
|
inputClassName?: string;
|
|
685
713
|
/** What a cleared date stores. Default `""`, the empty ISO string; `null` for a
|
|
@@ -766,6 +794,663 @@ export function RhfDateField<
|
|
|
766
794
|
);
|
|
767
795
|
}
|
|
768
796
|
|
|
797
|
+
// ── date range ───────────────────────────────────────────────────────────────
|
|
798
|
+
|
|
799
|
+
type OwnDateRangeProps = Omit<
|
|
800
|
+
DateRangePickerProps,
|
|
801
|
+
"from" | "to" | "onChange" | "label" | "invalid" | "disabled" | "className" | "id" | "error"
|
|
802
|
+
>;
|
|
803
|
+
|
|
804
|
+
export type RhfDateRangePickerProps<
|
|
805
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
806
|
+
TFromName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
807
|
+
TToName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
808
|
+
TTransformed = TFieldValues,
|
|
809
|
+
> = Omit<RhfFieldBaseProps<TFieldValues, TFromName, TTransformed>, "name" | "rules"> &
|
|
810
|
+
OwnDateRangeProps & {
|
|
811
|
+
/** The form field holding the range's first day, an ISO `"YYYY-MM-DD"`. */
|
|
812
|
+
fromName: TFromName;
|
|
813
|
+
/** The form field holding the range's last day. */
|
|
814
|
+
toName: TToName;
|
|
815
|
+
/** react-hook-form's `rules` for the start (`required`, a `validate` …). */
|
|
816
|
+
fromRules?: ControllerProps<TFieldValues, TFromName, TTransformed>["rules"];
|
|
817
|
+
/** react-hook-form's `rules` for the end — the place for "not before the start",
|
|
818
|
+
* which can read the start with `getValues`: the start is always written first. */
|
|
819
|
+
toRules?: ControllerProps<TFieldValues, TToName, TTransformed>["rules"];
|
|
820
|
+
/** Classes for the picker. Default `w-full`. */
|
|
821
|
+
inputClassName?: string;
|
|
822
|
+
/** What an empty end stores — a clear, or the half-made range between the first
|
|
823
|
+
* and second click. Default `""`, the empty ISO string; `null` for a nullable
|
|
824
|
+
* schema. */
|
|
825
|
+
emptyValue?: "" | null;
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* The kit's {@link DateRangePicker} over TWO form fields — `fromName` and `toName` —
|
|
830
|
+
* the way a period is stored: as two columns, each its own schema key, not one
|
|
831
|
+
* `{ from, to }` object (kastlan 4).
|
|
832
|
+
*
|
|
833
|
+
* ```tsx
|
|
834
|
+
* <RhfDateRangePicker fromName="periodFrom" toName="periodTo" label="Billing period"
|
|
835
|
+
* presets={presets} clearable />
|
|
836
|
+
* ```
|
|
837
|
+
*
|
|
838
|
+
* Why it is not {@link RhfField}: a `Controller` binds one name, and the range is
|
|
839
|
+
* two. Built by hand — an `RhfField` on the start and a second `useController` for
|
|
840
|
+
* the end, which is what kastlan's period fields needed — it works until the END is
|
|
841
|
+
* the one in error: the shell's label, message and `aria-invalid` all read the
|
|
842
|
+
* start's state alone, so "End before start" (a `refine` with `path: ["periodTo"]`,
|
|
843
|
+
* where such a rule goes) never shows. Here both states are read:
|
|
844
|
+
*
|
|
845
|
+
* - **Either field's error is shown and paints.** The label turns, the trigger is
|
|
846
|
+
* `aria-invalid` and wears the danger border when EITHER field has an error, and
|
|
847
|
+
* both messages are listed under the field (once, if the two say the same thing),
|
|
848
|
+
* each pointed at by the trigger's `aria-describedby`.
|
|
849
|
+
* - **One change writes both.** The picker reports the range whole, so the start and
|
|
850
|
+
* then the end are written on every change — the end too when only the start
|
|
851
|
+
* moved, so a cross-field rule on the end re-runs and an "End before start" clears
|
|
852
|
+
* the moment the start is fixed. A clear (`clearable`'s ×) empties both.
|
|
853
|
+
* - **Focus on error** lands on the trigger whichever of the two is in error.
|
|
854
|
+
* - **Touched** once a range is complete or cleared — not on the first click of a
|
|
855
|
+
* pick, which would mark (and, with `mode: "onTouched"`, validate) an end the user
|
|
856
|
+
* is still choosing.
|
|
857
|
+
*
|
|
858
|
+
* In `commit="immediate"` mode (the picker's default) the first calendar click is
|
|
859
|
+
* reported on its own, so the form holds `(from, emptyValue)` until the second; with
|
|
860
|
+
* `commit="apply"` nothing reaches the form until Apply.
|
|
861
|
+
*
|
|
862
|
+
* The common props (`label`, `hint`, `required`, `disabled`, `excludeWhenDisabled`,
|
|
863
|
+
* `control`, `className`) apply to the pair; `rules` are per field.
|
|
864
|
+
*/
|
|
865
|
+
export function RhfDateRangePicker<
|
|
866
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
867
|
+
TFromName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
868
|
+
TToName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
869
|
+
TTransformed = TFieldValues,
|
|
870
|
+
>({
|
|
871
|
+
fromName,
|
|
872
|
+
toName,
|
|
873
|
+
control,
|
|
874
|
+
fromRules,
|
|
875
|
+
toRules,
|
|
876
|
+
label,
|
|
877
|
+
hint,
|
|
878
|
+
required,
|
|
879
|
+
disabled,
|
|
880
|
+
excludeWhenDisabled,
|
|
881
|
+
className,
|
|
882
|
+
inputClassName,
|
|
883
|
+
emptyValue = "",
|
|
884
|
+
...pickerProps
|
|
885
|
+
}: RhfDateRangePickerProps<TFieldValues, TFromName, TToName, TTransformed>) {
|
|
886
|
+
const rhfDisabled = excludeWhenDisabled ? disabled : undefined;
|
|
887
|
+
// The end, read here; the start is the FormField below, which gives the label, hint
|
|
888
|
+
// and ids their context. This component re-renders on the end's state, and with it
|
|
889
|
+
// the FormField's render.
|
|
890
|
+
const to = useController<TFieldValues, TToName, TTransformed>({
|
|
891
|
+
name: toName,
|
|
892
|
+
control,
|
|
893
|
+
rules: toRules,
|
|
894
|
+
disabled: rhfDisabled,
|
|
895
|
+
});
|
|
896
|
+
return (
|
|
897
|
+
<FormField
|
|
898
|
+
control={control}
|
|
899
|
+
name={fromName}
|
|
900
|
+
rules={fromRules}
|
|
901
|
+
disabled={rhfDisabled}
|
|
902
|
+
render={({ field, fieldState }) => (
|
|
903
|
+
<FormItem className={className}>
|
|
904
|
+
<RangeBody
|
|
905
|
+
from={field as unknown as ControllerRenderProps}
|
|
906
|
+
fromState={fieldState}
|
|
907
|
+
to={to.field as unknown as ControllerRenderProps}
|
|
908
|
+
toState={to.fieldState}
|
|
909
|
+
label={label}
|
|
910
|
+
hint={hint}
|
|
911
|
+
required={required}
|
|
912
|
+
disabled={Boolean(disabled)}
|
|
913
|
+
emptyValue={emptyValue}
|
|
914
|
+
inputClassName={inputClassName}
|
|
915
|
+
pickerProps={pickerProps}
|
|
916
|
+
/>
|
|
917
|
+
</FormItem>
|
|
918
|
+
)}
|
|
919
|
+
/>
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function RangeBody({
|
|
924
|
+
from,
|
|
925
|
+
fromState,
|
|
926
|
+
to,
|
|
927
|
+
toState,
|
|
928
|
+
label,
|
|
929
|
+
hint,
|
|
930
|
+
required,
|
|
931
|
+
disabled,
|
|
932
|
+
emptyValue,
|
|
933
|
+
inputClassName,
|
|
934
|
+
pickerProps,
|
|
935
|
+
}: {
|
|
936
|
+
from: ControllerRenderProps;
|
|
937
|
+
fromState: ControllerFieldState;
|
|
938
|
+
to: ControllerRenderProps;
|
|
939
|
+
toState: ControllerFieldState;
|
|
940
|
+
label: ReactNode;
|
|
941
|
+
hint: ReactNode;
|
|
942
|
+
required?: boolean;
|
|
943
|
+
disabled: boolean;
|
|
944
|
+
emptyValue: "" | null;
|
|
945
|
+
inputClassName?: string;
|
|
946
|
+
pickerProps: OwnDateRangeProps;
|
|
947
|
+
}) {
|
|
948
|
+
const { id, formItemId, formMessageId, describedBy } = useFormField();
|
|
949
|
+
// The trigger forwards no ref; both fields focus it, so `shouldFocusError` lands
|
|
950
|
+
// there whichever of the two is first in error.
|
|
951
|
+
useFocusHandle(from.ref, () => document.getElementById(formItemId));
|
|
952
|
+
useFocusHandle(to.ref, () => document.getElementById(formItemId));
|
|
953
|
+
const invalid = fromState.invalid || toState.invalid;
|
|
954
|
+
const messages = [fromState.error?.message, toState.error?.message].filter(
|
|
955
|
+
(m, i, all): m is string => typeof m === "string" && m !== "" && all.indexOf(m) === i,
|
|
956
|
+
);
|
|
957
|
+
const messageIds = messages.map((_, i) => `${formMessageId}-${i}`);
|
|
958
|
+
const fromValue: unknown = from.value;
|
|
959
|
+
const toValue: unknown = to.value;
|
|
960
|
+
return (
|
|
961
|
+
<>
|
|
962
|
+
{hasContent(label) && (
|
|
963
|
+
// FormLabel colours from the START's error alone (its context is that field);
|
|
964
|
+
// the pair's state says it here.
|
|
965
|
+
<FormLabel id={`${id}-label`} required={required} data-error={invalid || undefined}>
|
|
966
|
+
{label}
|
|
967
|
+
</FormLabel>
|
|
968
|
+
)}
|
|
969
|
+
<DateRangePicker
|
|
970
|
+
{...pickerProps}
|
|
971
|
+
id={formItemId}
|
|
972
|
+
// Merged, never replaced — a caller's own reference first, as FormControl does.
|
|
973
|
+
aria-describedby={
|
|
974
|
+
[pickerProps["aria-describedby"], describedBy, ...messageIds].filter(Boolean).join(" ") || undefined
|
|
975
|
+
}
|
|
976
|
+
aria-invalid={invalid || undefined}
|
|
977
|
+
aria-required={required || undefined}
|
|
978
|
+
from={typeof fromValue === "string" ? fromValue : ""}
|
|
979
|
+
to={typeof toValue === "string" ? toValue : ""}
|
|
980
|
+
onChange={(f, t) => {
|
|
981
|
+
from.onChange(f === "" ? emptyValue : f);
|
|
982
|
+
to.onChange(t === "" ? emptyValue : t);
|
|
983
|
+
if (t !== "" || f === "") {
|
|
984
|
+
from.onBlur();
|
|
985
|
+
to.onBlur();
|
|
986
|
+
}
|
|
987
|
+
}}
|
|
988
|
+
disabled={disabled || from.disabled || to.disabled}
|
|
989
|
+
invalid={invalid}
|
|
990
|
+
className={inputClassName ?? "w-full"}
|
|
991
|
+
/>
|
|
992
|
+
{hasContent(hint) && <FormDescription>{hint}</FormDescription>}
|
|
993
|
+
{messages.map((message, i) => (
|
|
994
|
+
// FormMessage's look and its rule: no `role="alert"` — read when focus reaches
|
|
995
|
+
// the trigger, which names these in its description.
|
|
996
|
+
<p
|
|
997
|
+
key={message}
|
|
998
|
+
id={messageIds[i]}
|
|
999
|
+
data-slot="form-message"
|
|
1000
|
+
className="text-[11px] leading-tight text-[var(--danger)]"
|
|
1001
|
+
>
|
|
1002
|
+
{message}
|
|
1003
|
+
</p>
|
|
1004
|
+
))}
|
|
1005
|
+
</>
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// ── time ─────────────────────────────────────────────────────────────────────
|
|
1010
|
+
|
|
1011
|
+
export type RhfTimeInputProps<
|
|
1012
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
1013
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
1014
|
+
TTransformed = TFieldValues,
|
|
1015
|
+
> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &
|
|
1016
|
+
Omit<
|
|
1017
|
+
TimeInputProps,
|
|
1018
|
+
| "name"
|
|
1019
|
+
| "value"
|
|
1020
|
+
| "defaultValue"
|
|
1021
|
+
| "onChange"
|
|
1022
|
+
| "onValueChange"
|
|
1023
|
+
| "onBlur"
|
|
1024
|
+
| "label"
|
|
1025
|
+
| "error"
|
|
1026
|
+
| "invalid"
|
|
1027
|
+
| "className"
|
|
1028
|
+
| "disabled"
|
|
1029
|
+
| "required"
|
|
1030
|
+
> & {
|
|
1031
|
+
/** What a cleared time stores. Default `""`; `null` for a nullable schema. */
|
|
1032
|
+
emptyValue?: "" | null;
|
|
1033
|
+
};
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* The kit's {@link TimeInput}: a time of day as `"HH:mm"` (`"HH:mm:ss"` when `step`
|
|
1037
|
+
* asks for seconds), normalised before it reaches the form, so the stored value
|
|
1038
|
+
* compares and sorts as a string. kastlan 4: the owner meeting's time was an
|
|
1039
|
+
* {@link RhfField} with a hand-wired `TimeInput` in its render — `ref`, `name`, the
|
|
1040
|
+
* `?? ""`, `onValueChange`, `onBlur`, `invalid` — the seven lines every bound field
|
|
1041
|
+
* folds into one.
|
|
1042
|
+
*
|
|
1043
|
+
* `min` / `max` paint an out-of-window time (see TimeInput) but do not fail the form:
|
|
1044
|
+
* validation stays the schema's or `rules`'. `rules={{ validate: (v) =>
|
|
1045
|
+
* isTimeInRange(v, min, max) || "…" }}` says it with the same window.
|
|
1046
|
+
*/
|
|
1047
|
+
export function RhfTimeInput<
|
|
1048
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
1049
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
1050
|
+
TTransformed = TFieldValues,
|
|
1051
|
+
>({
|
|
1052
|
+
name,
|
|
1053
|
+
control,
|
|
1054
|
+
rules,
|
|
1055
|
+
label,
|
|
1056
|
+
hint,
|
|
1057
|
+
required,
|
|
1058
|
+
disabled,
|
|
1059
|
+
excludeWhenDisabled,
|
|
1060
|
+
className,
|
|
1061
|
+
emptyValue = "",
|
|
1062
|
+
...timeProps
|
|
1063
|
+
}: RhfTimeInputProps<TFieldValues, TName, TTransformed>) {
|
|
1064
|
+
return (
|
|
1065
|
+
<RhfField
|
|
1066
|
+
{...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
|
|
1067
|
+
render={({ field, invalid }) => {
|
|
1068
|
+
const value: unknown = field.value;
|
|
1069
|
+
return (
|
|
1070
|
+
<TimeInput
|
|
1071
|
+
aria-required={required || undefined}
|
|
1072
|
+
{...timeProps}
|
|
1073
|
+
name={field.name}
|
|
1074
|
+
ref={field.ref}
|
|
1075
|
+
value={typeof value === "string" ? value : ""}
|
|
1076
|
+
onValueChange={(v) => field.onChange(v === "" ? emptyValue : v)}
|
|
1077
|
+
onBlur={field.onBlur}
|
|
1078
|
+
disabled={field.disabled}
|
|
1079
|
+
invalid={invalid}
|
|
1080
|
+
/>
|
|
1081
|
+
);
|
|
1082
|
+
}}
|
|
1083
|
+
/>
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// ── IBAN and phone (0.22) ────────────────────────────────────────────────────
|
|
1088
|
+
|
|
1089
|
+
type OwnTextishProps<P> = Omit<
|
|
1090
|
+
P,
|
|
1091
|
+
| "name"
|
|
1092
|
+
| "value"
|
|
1093
|
+
| "defaultValue"
|
|
1094
|
+
| "onChange"
|
|
1095
|
+
| "onValueChange"
|
|
1096
|
+
| "onBlur"
|
|
1097
|
+
| "label"
|
|
1098
|
+
| "error"
|
|
1099
|
+
| "invalid"
|
|
1100
|
+
| "className"
|
|
1101
|
+
| "disabled"
|
|
1102
|
+
| "required"
|
|
1103
|
+
>;
|
|
1104
|
+
|
|
1105
|
+
export type RhfIbanInputProps<
|
|
1106
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
1107
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
1108
|
+
TTransformed = TFieldValues,
|
|
1109
|
+
> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> & OwnTextishProps<IbanInputProps>;
|
|
1110
|
+
|
|
1111
|
+
/**
|
|
1112
|
+
* {@link IbanInput} bound to one field, which stores the COMPACT upper-case IBAN (what
|
|
1113
|
+
* kastlan's server checks and its QR bill prints). The field's own checksum and
|
|
1114
|
+
* `kind` messages show while the form has nothing to say; a form error (the schema's
|
|
1115
|
+
* or `rules`') replaces them, as a caller's `error` does on the bare field. kastlan
|
|
1116
|
+
* asked for the field; it binds everything through react-hook-form.
|
|
1117
|
+
*/
|
|
1118
|
+
export function RhfIbanInput<
|
|
1119
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
1120
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
1121
|
+
TTransformed = TFieldValues,
|
|
1122
|
+
>({
|
|
1123
|
+
name,
|
|
1124
|
+
control,
|
|
1125
|
+
rules,
|
|
1126
|
+
label,
|
|
1127
|
+
hint,
|
|
1128
|
+
required,
|
|
1129
|
+
disabled,
|
|
1130
|
+
excludeWhenDisabled,
|
|
1131
|
+
className,
|
|
1132
|
+
...ibanProps
|
|
1133
|
+
}: RhfIbanInputProps<TFieldValues, TName, TTransformed>) {
|
|
1134
|
+
return (
|
|
1135
|
+
<RhfField
|
|
1136
|
+
{...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
|
|
1137
|
+
render={({ field, invalid }) => {
|
|
1138
|
+
const value: unknown = field.value;
|
|
1139
|
+
return (
|
|
1140
|
+
<IbanInput
|
|
1141
|
+
aria-required={required || undefined}
|
|
1142
|
+
{...ibanProps}
|
|
1143
|
+
name={field.name}
|
|
1144
|
+
ref={field.ref}
|
|
1145
|
+
value={typeof value === "string" ? value : ""}
|
|
1146
|
+
onValueChange={field.onChange}
|
|
1147
|
+
onBlur={field.onBlur}
|
|
1148
|
+
disabled={field.disabled}
|
|
1149
|
+
invalid={invalid}
|
|
1150
|
+
/>
|
|
1151
|
+
);
|
|
1152
|
+
}}
|
|
1153
|
+
/>
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
export type RhfPhoneInputProps<
|
|
1158
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
1159
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
1160
|
+
TTransformed = TFieldValues,
|
|
1161
|
+
> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> & OwnTextishProps<PhoneInputProps>;
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* {@link PhoneInput} bound to one field, which stores E.164 when the number reads as
|
|
1165
|
+
* one and the typed text unchanged otherwise — kastlan's contacts keep their old free
|
|
1166
|
+
* text until someone edits it. Validation (required, a pattern for E.164 only) is the
|
|
1167
|
+
* schema's or `rules`'.
|
|
1168
|
+
*/
|
|
1169
|
+
export function RhfPhoneInput<
|
|
1170
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
1171
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
1172
|
+
TTransformed = TFieldValues,
|
|
1173
|
+
>({
|
|
1174
|
+
name,
|
|
1175
|
+
control,
|
|
1176
|
+
rules,
|
|
1177
|
+
label,
|
|
1178
|
+
hint,
|
|
1179
|
+
required,
|
|
1180
|
+
disabled,
|
|
1181
|
+
excludeWhenDisabled,
|
|
1182
|
+
className,
|
|
1183
|
+
...phoneProps
|
|
1184
|
+
}: RhfPhoneInputProps<TFieldValues, TName, TTransformed>) {
|
|
1185
|
+
return (
|
|
1186
|
+
<RhfField
|
|
1187
|
+
{...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
|
|
1188
|
+
render={({ field, invalid }) => {
|
|
1189
|
+
const value: unknown = field.value;
|
|
1190
|
+
return (
|
|
1191
|
+
<PhoneInput
|
|
1192
|
+
aria-required={required || undefined}
|
|
1193
|
+
{...phoneProps}
|
|
1194
|
+
name={field.name}
|
|
1195
|
+
ref={field.ref}
|
|
1196
|
+
value={typeof value === "string" ? value : ""}
|
|
1197
|
+
onValueChange={field.onChange}
|
|
1198
|
+
onBlur={field.onBlur}
|
|
1199
|
+
disabled={field.disabled}
|
|
1200
|
+
invalid={invalid}
|
|
1201
|
+
/>
|
|
1202
|
+
);
|
|
1203
|
+
}}
|
|
1204
|
+
/>
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// ── country and month (0.23) ─────────────────────────────────────────────────
|
|
1209
|
+
|
|
1210
|
+
type OwnCountrySelectProps = Omit<
|
|
1211
|
+
CountrySelectProps<boolean>,
|
|
1212
|
+
| "value"
|
|
1213
|
+
| "onChange"
|
|
1214
|
+
| "onBlur"
|
|
1215
|
+
| "label"
|
|
1216
|
+
| "hint"
|
|
1217
|
+
| "error"
|
|
1218
|
+
| "invalid"
|
|
1219
|
+
| "disabled"
|
|
1220
|
+
| "className"
|
|
1221
|
+
| "id"
|
|
1222
|
+
| "ref"
|
|
1223
|
+
| "clearable"
|
|
1224
|
+
>;
|
|
1225
|
+
|
|
1226
|
+
export type RhfCountrySelectProps<
|
|
1227
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
1228
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
1229
|
+
TTransformed = TFieldValues,
|
|
1230
|
+
> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &
|
|
1231
|
+
OwnCountrySelectProps & {
|
|
1232
|
+
/** Offer the picker's clear "×" (and Delete on the trigger) while a country is
|
|
1233
|
+
* chosen. A clear stores `clearValue`. */
|
|
1234
|
+
clearable?: boolean;
|
|
1235
|
+
/** What a clear stores. Default `null`; `""` for a schema that spells "no
|
|
1236
|
+
* country" as an empty string (a zod `z.string().max(2)`, kastlan's). The picker
|
|
1237
|
+
* reads either back as empty. */
|
|
1238
|
+
clearValue?: ComboClearValue;
|
|
1239
|
+
/** Classes for the picker — `className` is the item's box. */
|
|
1240
|
+
selectClassName?: string;
|
|
1241
|
+
};
|
|
1242
|
+
|
|
1243
|
+
/**
|
|
1244
|
+
* {@link CountrySelect} bound to one field, which stores the ISO 3166-1 alpha-2 code
|
|
1245
|
+
* ("CH", always upper-case), or `clearValue` once cleared (`clearable`). kastlan asked
|
|
1246
|
+
* for it (0.22 adoption): its address form wired a `CountrySelect` through
|
|
1247
|
+
* {@link RhfField}'s render, and with no ref on the picker react-hook-form's
|
|
1248
|
+
* focus-on-error had nothing to focus — the one field on the form a failed submit
|
|
1249
|
+
* could not take you to.
|
|
1250
|
+
*
|
|
1251
|
+
* Bound the way {@link RhfIbanInput} and {@link RhfPhoneInput} are: `field.ref` reaches
|
|
1252
|
+
* the trigger (the picker's `ref`, 0.23), the form's error paints the trigger and is
|
|
1253
|
+
* the shell's message under it, the hint is the shell's description — so neither is
|
|
1254
|
+
* ever handed to the picker, and its box never changes when one comes or goes. The
|
|
1255
|
+
* form's label names the trigger together with the chosen country, and is the phone
|
|
1256
|
+
* sheet's title. `commit` and `disabledReason` are the picker's own: a country that
|
|
1257
|
+
* saves itself stays focusable under a lock and says why.
|
|
1258
|
+
*
|
|
1259
|
+
* ```tsx
|
|
1260
|
+
* <RhfCountrySelect name="country" label="Country" required
|
|
1261
|
+
* preferred={["CH", "LI", "DE", "AT", "FR", "IT"]} rules={{ required: "Choose a country" }} />
|
|
1262
|
+
* ```
|
|
1263
|
+
*/
|
|
1264
|
+
export function RhfCountrySelect<
|
|
1265
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
1266
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
1267
|
+
TTransformed = TFieldValues,
|
|
1268
|
+
>({
|
|
1269
|
+
name,
|
|
1270
|
+
control,
|
|
1271
|
+
rules,
|
|
1272
|
+
label,
|
|
1273
|
+
hint,
|
|
1274
|
+
required,
|
|
1275
|
+
disabled,
|
|
1276
|
+
excludeWhenDisabled,
|
|
1277
|
+
className,
|
|
1278
|
+
clearable,
|
|
1279
|
+
clearValue = null,
|
|
1280
|
+
selectClassName,
|
|
1281
|
+
labels,
|
|
1282
|
+
...countryProps
|
|
1283
|
+
}: RhfCountrySelectProps<TFieldValues, TName, TTransformed>) {
|
|
1284
|
+
const labelled = hasContent(label);
|
|
1285
|
+
// A string label is also what the field IS — the picker's `country` word, which
|
|
1286
|
+
// titles the phone sheet. A caller's own `labels.country` still wins.
|
|
1287
|
+
const ownLabels = typeof label === "string" ? { country: label, ...labels } : labels;
|
|
1288
|
+
return (
|
|
1289
|
+
<RhfField
|
|
1290
|
+
{...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
|
|
1291
|
+
render={({ field, invalid, labelId }) => {
|
|
1292
|
+
const value: unknown = field.value;
|
|
1293
|
+
return (
|
|
1294
|
+
// FormControl clones this element with `id` / `aria-describedby` /
|
|
1295
|
+
// `aria-invalid`, which the picker routes to its trigger.
|
|
1296
|
+
<CountrySelect<boolean>
|
|
1297
|
+
aria-required={required || undefined}
|
|
1298
|
+
// The form's label, then the country (see the picker's `aria-labelledby`).
|
|
1299
|
+
aria-labelledby={labelled ? labelId : undefined}
|
|
1300
|
+
{...countryProps}
|
|
1301
|
+
labels={ownLabels}
|
|
1302
|
+
className={selectClassName}
|
|
1303
|
+
ref={field.ref}
|
|
1304
|
+
value={typeof value === "string" ? value : null}
|
|
1305
|
+
clearable={clearable}
|
|
1306
|
+
onChange={(code) => field.onChange(code ?? clearValue)}
|
|
1307
|
+
// Focus leaving the picker (the wrapper's focusout) marks the field touched.
|
|
1308
|
+
onBlur={field.onBlur}
|
|
1309
|
+
disabled={field.disabled}
|
|
1310
|
+
invalid={invalid}
|
|
1311
|
+
/>
|
|
1312
|
+
);
|
|
1313
|
+
}}
|
|
1314
|
+
/>
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
type OwnMonthPickerProps = Omit<
|
|
1319
|
+
MonthPickerProps,
|
|
1320
|
+
"value" | "onChange" | "onBlur" | "label" | "hint" | "error" | "invalid" | "disabled" | "className" | "id" | "mode"
|
|
1321
|
+
>;
|
|
1322
|
+
|
|
1323
|
+
/** `mode` picks what the form stores, and `valueAsNumber` is only offered for a year. */
|
|
1324
|
+
type RhfMonthPickerMode =
|
|
1325
|
+
| {
|
|
1326
|
+
/** A month, stored as `"YYYY-MM"` (the default). */
|
|
1327
|
+
mode?: "month";
|
|
1328
|
+
valueAsNumber?: undefined;
|
|
1329
|
+
}
|
|
1330
|
+
| {
|
|
1331
|
+
/** A year, stored as `"YYYY"` — or as a number with `valueAsNumber`. */
|
|
1332
|
+
mode: "year";
|
|
1333
|
+
/** Store the year as a number (`2026`) rather than `"2026"` — kastlan's budget
|
|
1334
|
+
* `fiscal_year` is an integer column. A number in the form reads back as its
|
|
1335
|
+
* year. */
|
|
1336
|
+
valueAsNumber?: boolean;
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
export type RhfMonthPickerProps<
|
|
1340
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
1341
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
1342
|
+
TTransformed = TFieldValues,
|
|
1343
|
+
> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &
|
|
1344
|
+
OwnMonthPickerProps & {
|
|
1345
|
+
/** Classes for the picker — `className` is the item's box. As {@link RhfDateField}'s. */
|
|
1346
|
+
inputClassName?: string;
|
|
1347
|
+
} & RhfMonthPickerMode;
|
|
1348
|
+
|
|
1349
|
+
function MonthControl({
|
|
1350
|
+
field,
|
|
1351
|
+
invalid,
|
|
1352
|
+
id,
|
|
1353
|
+
valueAsNumber,
|
|
1354
|
+
mode,
|
|
1355
|
+
inputClassName,
|
|
1356
|
+
pickerProps,
|
|
1357
|
+
required,
|
|
1358
|
+
...aria
|
|
1359
|
+
}: {
|
|
1360
|
+
field: ControllerRenderProps;
|
|
1361
|
+
invalid: boolean;
|
|
1362
|
+
id: string;
|
|
1363
|
+
valueAsNumber?: boolean;
|
|
1364
|
+
mode?: "month" | "year";
|
|
1365
|
+
inputClassName?: string;
|
|
1366
|
+
pickerProps: OwnMonthPickerProps;
|
|
1367
|
+
required?: boolean;
|
|
1368
|
+
"aria-describedby"?: string;
|
|
1369
|
+
"aria-invalid"?: MonthPickerProps["aria-invalid"];
|
|
1370
|
+
}) {
|
|
1371
|
+
// MonthPicker forwards no ref; it routes `id` to its trigger, where the handle finds
|
|
1372
|
+
// it — RhfDateField's way.
|
|
1373
|
+
useFocusHandle(field.ref, () => document.getElementById(id));
|
|
1374
|
+
const value: unknown = field.value;
|
|
1375
|
+
return (
|
|
1376
|
+
<MonthPicker
|
|
1377
|
+
{...pickerProps}
|
|
1378
|
+
{...aria}
|
|
1379
|
+
// MonthPicker routes `aria-required` to its trigger since 0.23.0.
|
|
1380
|
+
aria-required={required || undefined}
|
|
1381
|
+
id={id}
|
|
1382
|
+
mode={mode}
|
|
1383
|
+
value={value === null || value === undefined ? "" : String(value)}
|
|
1384
|
+
onChange={(key) => {
|
|
1385
|
+
field.onChange(mode === "year" && valueAsNumber ? Number(key) : key);
|
|
1386
|
+
// A pick is the whole interaction — there is no text to leave — so it touches.
|
|
1387
|
+
field.onBlur();
|
|
1388
|
+
}}
|
|
1389
|
+
disabled={field.disabled}
|
|
1390
|
+
invalid={invalid}
|
|
1391
|
+
className={inputClassName}
|
|
1392
|
+
/>
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/**
|
|
1397
|
+
* {@link MonthPicker} bound to one field: a month as `"YYYY-MM"`, or with `mode="year"` a
|
|
1398
|
+
* year as `"YYYY"` (`valueAsNumber` stores `2026`). kastlan asked for it with
|
|
1399
|
+
* {@link RhfCountrySelect}: its budget wizard's fiscal year was a `MonthPicker
|
|
1400
|
+
* mode="year"` inside {@link RhfField}'s render — the `String()` in, the `Number()`
|
|
1401
|
+
* out, `invalid` by hand — and a failed submit could not focus it, because the picker
|
|
1402
|
+
* forwards no ref.
|
|
1403
|
+
*
|
|
1404
|
+
* Bound as {@link RhfDateField} is: a focus handle finds the trigger by its id, so
|
|
1405
|
+
* focus-on-error lands there; the form's error paints the trigger and is the shell's
|
|
1406
|
+
* message; the hint is the shell's description; neither is handed to the picker, so
|
|
1407
|
+
* its box never changes. Picking marks the field touched.
|
|
1408
|
+
*
|
|
1409
|
+
* ```tsx
|
|
1410
|
+
* <RhfMonthPicker name="fiscalYear" label="Fiscal year" mode="year" valueAsNumber
|
|
1411
|
+
* min="2020" max="2029" rules={{ required: "Choose a year" }} />
|
|
1412
|
+
* ```
|
|
1413
|
+
*/
|
|
1414
|
+
export function RhfMonthPicker<
|
|
1415
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
1416
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
1417
|
+
TTransformed = TFieldValues,
|
|
1418
|
+
>({
|
|
1419
|
+
name,
|
|
1420
|
+
control,
|
|
1421
|
+
rules,
|
|
1422
|
+
label,
|
|
1423
|
+
hint,
|
|
1424
|
+
required,
|
|
1425
|
+
disabled,
|
|
1426
|
+
excludeWhenDisabled,
|
|
1427
|
+
className,
|
|
1428
|
+
inputClassName,
|
|
1429
|
+
mode,
|
|
1430
|
+
valueAsNumber,
|
|
1431
|
+
...pickerProps
|
|
1432
|
+
}: RhfMonthPickerProps<TFieldValues, TName, TTransformed>) {
|
|
1433
|
+
return (
|
|
1434
|
+
<RhfField
|
|
1435
|
+
{...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
|
|
1436
|
+
render={({ field, invalid, id }) => (
|
|
1437
|
+
// FormControl clones this element with `aria-describedby` / `aria-invalid`,
|
|
1438
|
+
// which MonthControl hands on to the picker's trigger.
|
|
1439
|
+
<MonthControl
|
|
1440
|
+
field={field as unknown as ControllerRenderProps}
|
|
1441
|
+
invalid={invalid}
|
|
1442
|
+
id={id}
|
|
1443
|
+
mode={mode}
|
|
1444
|
+
valueAsNumber={valueAsNumber}
|
|
1445
|
+
inputClassName={inputClassName}
|
|
1446
|
+
pickerProps={pickerProps}
|
|
1447
|
+
required={required}
|
|
1448
|
+
/>
|
|
1449
|
+
)}
|
|
1450
|
+
/>
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
769
1454
|
// ── select ───────────────────────────────────────────────────────────────────
|
|
770
1455
|
|
|
771
1456
|
export interface RhfSelectOption {
|
|
@@ -900,6 +1585,172 @@ export function RhfCheckbox<
|
|
|
900
1585
|
);
|
|
901
1586
|
}
|
|
902
1587
|
|
|
1588
|
+
// ── toggle group ─────────────────────────────────────────────────────────────
|
|
1589
|
+
|
|
1590
|
+
/** The two shapes a {@link ToggleGroup} comes in, carried over: `allowEmpty` picks
|
|
1591
|
+
* whether a press on the chosen option clears it, and types the caption to match. */
|
|
1592
|
+
type RhfToggleGroupMode<T extends string> =
|
|
1593
|
+
| {
|
|
1594
|
+
/** One option is always the answer (the default): a press on the chosen
|
|
1595
|
+
* option re-sends it. */
|
|
1596
|
+
allowEmpty?: false;
|
|
1597
|
+
semantics?: ToggleGroupRequiredProps<T>["semantics"];
|
|
1598
|
+
caption?: ToggleGroupRequiredProps<T>["caption"];
|
|
1599
|
+
emptyValue?: undefined;
|
|
1600
|
+
}
|
|
1601
|
+
| {
|
|
1602
|
+
/** A press on the chosen option clears it, and the form stores `emptyValue`. */
|
|
1603
|
+
allowEmpty: true;
|
|
1604
|
+
semantics?: ToggleGroupClearableProps<T>["semantics"];
|
|
1605
|
+
caption?: ToggleGroupClearableProps<T>["caption"];
|
|
1606
|
+
/** What a cleared group stores. Default `null`; `""` for a schema that spells
|
|
1607
|
+
* "no choice" as an empty string. */
|
|
1608
|
+
emptyValue?: null | "";
|
|
1609
|
+
};
|
|
1610
|
+
|
|
1611
|
+
export type RhfToggleGroupProps<
|
|
1612
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
1613
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
1614
|
+
T extends string = string,
|
|
1615
|
+
TTransformed = TFieldValues,
|
|
1616
|
+
> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &
|
|
1617
|
+
Pick<
|
|
1618
|
+
ToggleGroupBaseProps<T>,
|
|
1619
|
+
"options" | "optionClassName" | "overflow" | "size" | "aria-label" | "commit" | "disabledReason"
|
|
1620
|
+
> & {
|
|
1621
|
+
/** Classes for the group itself — `className` is the item's box. */
|
|
1622
|
+
groupClassName?: string;
|
|
1623
|
+
} & RhfToggleGroupMode<T>;
|
|
1624
|
+
|
|
1625
|
+
/**
|
|
1626
|
+
* The kit's {@link ToggleGroup} bound to one field: a choice between a few answers
|
|
1627
|
+
* ("Monthly / Quarterly / Yearly"), stored as the chosen option's `value`. kastlan 4
|
|
1628
|
+
* asked for it with {@link RhfTimeInput} and {@link RhfDateRangePicker}: the controls
|
|
1629
|
+
* its forms still had to wire by hand through {@link RhfField}'s render.
|
|
1630
|
+
*
|
|
1631
|
+
* The label is the form's {@link FormLabel} above the bare group, as on every other
|
|
1632
|
+
* bound field — the `labelPlacement="above"` shape, not the group's own field chrome —
|
|
1633
|
+
* and it names the group (`aria-labelledby`); the hint and the message describe it,
|
|
1634
|
+
* and an error paints the group's border. Focus on error goes to the option a
|
|
1635
|
+
* keyboard user would land on (the chosen one, else the first); touched is set when
|
|
1636
|
+
* focus leaves the group, not when it moves between options.
|
|
1637
|
+
*
|
|
1638
|
+
* Required by default, as the group is: a press on the chosen option keeps it. With
|
|
1639
|
+
* `allowEmpty`, a second press clears the choice and the form stores `emptyValue`
|
|
1640
|
+
* (`null` by default) — the options then become toggle buttons, see ToggleGroup. A
|
|
1641
|
+
* value that matches no option (an empty default) leaves nothing pressed; that the
|
|
1642
|
+
* field still needs answering is the schema's or `rules`' to say.
|
|
1643
|
+
*/
|
|
1644
|
+
export function RhfToggleGroup<
|
|
1645
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
1646
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
1647
|
+
T extends string = string,
|
|
1648
|
+
TTransformed = TFieldValues,
|
|
1649
|
+
>({
|
|
1650
|
+
name,
|
|
1651
|
+
control,
|
|
1652
|
+
rules,
|
|
1653
|
+
label,
|
|
1654
|
+
hint,
|
|
1655
|
+
required,
|
|
1656
|
+
disabled,
|
|
1657
|
+
excludeWhenDisabled,
|
|
1658
|
+
className,
|
|
1659
|
+
...groupProps
|
|
1660
|
+
}: RhfToggleGroupProps<TFieldValues, TName, T, TTransformed>) {
|
|
1661
|
+
const labelled = hasContent(label);
|
|
1662
|
+
return (
|
|
1663
|
+
<RhfField
|
|
1664
|
+
{...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
|
|
1665
|
+
asControl={false}
|
|
1666
|
+
render={({ field, invalid, id, labelId, describedBy }) => (
|
|
1667
|
+
<ToggleControl<T>
|
|
1668
|
+
field={field as unknown as ControllerRenderProps}
|
|
1669
|
+
invalid={invalid}
|
|
1670
|
+
required={required}
|
|
1671
|
+
id={id}
|
|
1672
|
+
labelledBy={labelled ? labelId : undefined}
|
|
1673
|
+
describedBy={describedBy}
|
|
1674
|
+
groupProps={groupProps}
|
|
1675
|
+
/>
|
|
1676
|
+
)}
|
|
1677
|
+
/>
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
function ToggleControl<T extends string>({
|
|
1682
|
+
field,
|
|
1683
|
+
invalid,
|
|
1684
|
+
required,
|
|
1685
|
+
id,
|
|
1686
|
+
labelledBy,
|
|
1687
|
+
describedBy,
|
|
1688
|
+
groupProps,
|
|
1689
|
+
}: {
|
|
1690
|
+
field: ControllerRenderProps;
|
|
1691
|
+
invalid: boolean;
|
|
1692
|
+
required?: boolean;
|
|
1693
|
+
id: string;
|
|
1694
|
+
labelledBy: string | undefined;
|
|
1695
|
+
describedBy: string | undefined;
|
|
1696
|
+
groupProps: Omit<RhfToggleGroupProps<FieldValues, string, T>, keyof RhfFieldBaseProps>;
|
|
1697
|
+
}) {
|
|
1698
|
+
const { groupClassName, emptyValue, allowEmpty, semantics, caption, ...rest } = groupProps;
|
|
1699
|
+
// ToggleGroup forwards no ref. Its options are buttons: the radio shape's one tab
|
|
1700
|
+
// stop (`tabindex="0"`), or in the pressed shape the pressed one — else the first.
|
|
1701
|
+
useFocusHandle(field.ref, () => {
|
|
1702
|
+
const group = document.getElementById(id);
|
|
1703
|
+
return (
|
|
1704
|
+
group?.querySelector<HTMLElement>('button[tabindex="0"], button[aria-pressed="true"]') ??
|
|
1705
|
+
group?.querySelector<HTMLElement>("button")
|
|
1706
|
+
);
|
|
1707
|
+
});
|
|
1708
|
+
// React's onBlur is focusout: it fires on every move between options as well, and
|
|
1709
|
+
// only leaving the group is a blur of the FIELD.
|
|
1710
|
+
const onBlur = (e: FocusEvent<HTMLDivElement>) => {
|
|
1711
|
+
if (!e.currentTarget.contains(e.relatedTarget as Node | null)) field.onBlur();
|
|
1712
|
+
};
|
|
1713
|
+
const raw: unknown = field.value;
|
|
1714
|
+
const value = raw === null || raw === undefined || raw === "" ? null : (String(raw) as T);
|
|
1715
|
+
const common = {
|
|
1716
|
+
...rest,
|
|
1717
|
+
id,
|
|
1718
|
+
className: groupClassName,
|
|
1719
|
+
"aria-labelledby": labelledBy,
|
|
1720
|
+
"aria-describedby": describedBy,
|
|
1721
|
+
"aria-invalid": invalid || undefined,
|
|
1722
|
+
disabled: field.disabled,
|
|
1723
|
+
onBlur,
|
|
1724
|
+
};
|
|
1725
|
+
// The props' union pairs `allowEmpty` with its caption's type; taken apart above it no
|
|
1726
|
+
// longer narrows, so each branch says which half it is.
|
|
1727
|
+
if (allowEmpty) {
|
|
1728
|
+
return (
|
|
1729
|
+
<ToggleGroup<T>
|
|
1730
|
+
{...common}
|
|
1731
|
+
allowEmpty
|
|
1732
|
+
caption={caption as ToggleGroupClearableProps<T>["caption"]}
|
|
1733
|
+
value={value}
|
|
1734
|
+
onChange={(v) => field.onChange(v ?? emptyValue ?? null)}
|
|
1735
|
+
/>
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
return (
|
|
1739
|
+
<ToggleGroup<T>
|
|
1740
|
+
{...common}
|
|
1741
|
+
allowEmpty={false}
|
|
1742
|
+
semantics={semantics}
|
|
1743
|
+
caption={caption as ToggleGroupRequiredProps<T>["caption"]}
|
|
1744
|
+
// A radiogroup takes `aria-required`; a group of toggle buttons does not.
|
|
1745
|
+
aria-required={required && semantics !== "pressed" ? true : undefined}
|
|
1746
|
+
// An empty default matches no option: nothing pressed, the first option the tab
|
|
1747
|
+
// stop.
|
|
1748
|
+
value={(value ?? "") as T}
|
|
1749
|
+
onChange={(v) => field.onChange(v)}
|
|
1750
|
+
/>
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
903
1754
|
// ── comboboxes ───────────────────────────────────────────────────────────────
|
|
904
1755
|
|
|
905
1756
|
export type RhfComboboxProps<
|