@eifi1/ui-kit 0.21.0 → 0.22.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/dist/chart.d.ts +10 -8
- 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 +27 -8
- package/dist/components/amount-input.js +15 -28
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +8 -0
- package/dist/components/autocomplete.js +9 -4
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +10 -8
- package/dist/components/calculator.d.ts +10 -8
- 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/combobox.d.ts +13 -2
- package/dist/components/combobox.js +17 -8
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +66 -161
- package/dist/components/copy-button.d.ts +10 -8
- package/dist/components/country-select.d.ts +124 -0
- package/dist/components/country-select.js +237 -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 +66 -166
- package/dist/components/danger-confirm.js +133 -49
- 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 +25 -5
- package/dist/components/date-picker.js +110 -59
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +8 -1
- package/dist/components/entity-combobox.js +9 -4
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +10 -8
- package/dist/components/field-anatomy.d.ts +95 -0
- package/dist/components/field-anatomy.js +84 -0
- package/dist/components/field-anatomy.js.map +1 -0
- package/dist/components/field-parts.d.ts +114 -0
- package/dist/components/field-parts.js +72 -0
- package/dist/components/field-parts.js.map +1 -0
- package/dist/components/file-button.d.ts +10 -8
- package/dist/components/file-button.js +13 -3
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +32 -9
- package/dist/components/file-dropzone.js +57 -34
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/form-actions.d.ts +10 -8
- package/dist/components/form-actions.js +61 -3
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/iban-input.d.ts +66 -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 +12 -2
- package/dist/components/icon-picker.js +9 -2
- 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 +127 -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 +20 -8
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.d.ts +52 -4
- package/dist/components/month-picker.js +290 -86
- package/dist/components/month-picker.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 +25 -13
- package/dist/components/number-field.js +6 -20
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +70 -13
- package/dist/components/number-input.js +90 -38
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +10 -8
- 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 +66 -0
- package/dist/components/phone-input.js +171 -0
- package/dist/components/phone-input.js.map +1 -0
- package/dist/components/series-chart.d.ts +10 -8
- package/dist/components/settings-fields.d.ts +10 -8
- package/dist/components/share-card.d.ts +8 -6
- 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 +27 -2
- package/dist/components/swatch-picker.js +73 -31
- 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 +10 -8
- package/dist/components/tile-radio.d.ts +68 -13
- package/dist/components/tile-radio.js +3 -3
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +15 -9
- 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/ui.d.ts +10 -8
- package/dist/components/ui.js +280 -78
- 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 +210 -3
- 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 +1 -1
- package/dist/feedback/feedback-thread.d.ts +37 -3
- package/dist/feedback/feedback-thread.js +38 -9
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-NhUm7Zga.d.ts} +70 -5
- package/dist/feedback.d.ts +2 -2
- package/dist/hooks/use-file-drop.d.ts +10 -8
- package/dist/i18n/defaults.d.ts +10 -8
- package/dist/i18n/defaults.js +17 -2
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +10 -8
- package/dist/i18n/german.js +50 -5
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +10 -8
- package/dist/i18n/kit-labels.js +8 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +10 -8
- package/dist/i18n/locales/de-CH.d.ts +10 -8
- package/dist/i18n/locales/en.d.ts +10 -8
- package/dist/i18n/locales/en.js +9 -0
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +10 -8
- package/dist/i18n/locales/es.js +48 -5
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +10 -8
- package/dist/i18n/locales/fr.js +48 -5
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +10 -8
- package/dist/i18n/locales/hu.js +49 -5
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +10 -8
- package/dist/i18n/locales/it.js +49 -5
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +10 -8
- package/dist/i18n/locales/zh.js +48 -5
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +10 -8
- package/dist/i18n/review.js +8 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +17 -10
- package/dist/index.js +57 -2
- package/dist/index.js.map +1 -1
- package/dist/{kit-labels-D1J2_jyD.d.ts → kit-labels-R8mIAc9N.d.ts} +811 -8
- 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/rhf/fields.d.ts +148 -11
- package/dist/rhf/fields.js +332 -3
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +10 -8
- package/dist/rhf.d.ts +13 -9
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +10 -8
- package/dist/shell/top-bar-brand.d.ts +10 -8
- package/dist/shell.d.ts +10 -8
- package/dist/tour/tour.js +20 -24
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +10 -8
- package/dist/wizard.d.ts +10 -8
- 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 +49 -56
- package/src/components/autocomplete.tsx +133 -110
- 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/combobox.tsx +292 -253
- package/src/components/country-select.tsx +443 -0
- package/src/components/currency-select.tsx +109 -33
- package/src/components/danger-confirm.tsx +345 -77
- package/src/components/data-table-labels.ts +16 -1
- package/src/components/data-table.tsx +175 -6
- package/src/components/date-picker.tsx +173 -62
- package/src/components/entity-combobox.tsx +107 -87
- package/src/components/field-anatomy.tsx +190 -0
- package/src/components/field-parts.tsx +184 -0
- package/src/components/file-button.tsx +36 -3
- package/src/components/file-dropzone.tsx +106 -38
- package/src/components/form-actions.tsx +115 -2
- package/src/components/iban-input.tsx +231 -0
- package/src/components/icon-picker.tsx +26 -4
- 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 +10 -0
- package/src/components/month-picker.tsx +399 -63
- 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/sign-chip.tsx +103 -0
- package/src/components/swatch-picker.tsx +118 -30
- package/src/components/switch.tsx +58 -7
- package/src/components/tile-radio.tsx +71 -16
- package/src/components/time-input.tsx +8 -1
- package/src/components/toggle-group.tsx +206 -73
- package/src/components/ui.tsx +472 -105
- package/src/data-table.ts +2 -1
- package/src/feedback/feedback-attachment.tsx +313 -9
- package/src/feedback/feedback-dialog.tsx +2 -0
- package/src/feedback/feedback-thread.tsx +66 -5
- package/src/i18n/defaults.ts +16 -1
- package/src/i18n/german.ts +47 -0
- package/src/i18n/kit-labels.tsx +78 -2
- package/src/i18n/locales/en.ts +10 -0
- package/src/i18n/locales/es.ts +46 -0
- package/src/i18n/locales/fr.ts +45 -0
- package/src/i18n/locales/hu.ts +46 -0
- package/src/i18n/locales/it.ts +47 -0
- package/src/i18n/locales/zh.ts +44 -0
- package/src/i18n/review.ts +7 -0
- package/src/index.ts +53 -3
- 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/rhf/fields.tsx +606 -9
- package/src/rhf.ts +2 -1
- package/src/tour/tour.tsx +66 -28
package/src/rhf/fields.tsx
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
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} />
|
|
13
16
|
* </Form>
|
|
14
17
|
* ```
|
|
15
18
|
*
|
|
@@ -41,19 +44,34 @@
|
|
|
41
44
|
* {@link RhfField} is the shell they are all built on, for a control the kit does not
|
|
42
45
|
* ship (an address autocomplete): it takes a `render` and wires the rest.
|
|
43
46
|
*/
|
|
44
|
-
import { useLayoutEffect, useRef, useState, type ReactElement, type ReactNode } from "react";
|
|
45
|
-
import
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
import { useLayoutEffect, useRef, useState, type FocusEvent, type ReactElement, type ReactNode } from "react";
|
|
48
|
+
import {
|
|
49
|
+
useController,
|
|
50
|
+
type ControllerFieldState,
|
|
51
|
+
type ControllerProps,
|
|
52
|
+
type ControllerRenderProps,
|
|
53
|
+
type FieldPath,
|
|
54
|
+
type FieldValues,
|
|
51
55
|
} from "react-hook-form";
|
|
52
56
|
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useFormField } from "./form";
|
|
53
57
|
import { Input, Select, Textarea, type InputProps, type SelectProps, type TextareaProps } from "../components/ui";
|
|
54
58
|
import { NumberField, type NumberFieldProps } from "../components/number-field";
|
|
55
59
|
import { AmountInput } from "../components/amount-input";
|
|
56
|
-
import {
|
|
60
|
+
import {
|
|
61
|
+
DatePicker,
|
|
62
|
+
DateRangePicker,
|
|
63
|
+
type DatePickerProps,
|
|
64
|
+
type DateRangePickerProps,
|
|
65
|
+
} from "../components/date-picker";
|
|
66
|
+
import { TimeInput, type TimeInputProps } from "../components/time-input";
|
|
67
|
+
import { IbanInput, type IbanInputProps } from "../components/iban-input";
|
|
68
|
+
import { PhoneInput, type PhoneInputProps } from "../components/phone-input";
|
|
69
|
+
import {
|
|
70
|
+
ToggleGroup,
|
|
71
|
+
type ToggleGroupBaseProps,
|
|
72
|
+
type ToggleGroupClearableProps,
|
|
73
|
+
type ToggleGroupRequiredProps,
|
|
74
|
+
} from "../components/toggle-group";
|
|
57
75
|
import { Checkbox, type CheckboxProps } from "../components/checkbox";
|
|
58
76
|
import { EntityCombobox, type EntityComboboxProps } from "../components/entity-combobox";
|
|
59
77
|
import { Combobox, type ComboboxProps } from "../components/combobox";
|
|
@@ -679,7 +697,9 @@ export type RhfDateFieldProps<
|
|
|
679
697
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
680
698
|
TTransformed = TFieldValues,
|
|
681
699
|
> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &
|
|
682
|
-
|
|
700
|
+
// `error`: the form's message is shown by the binding (FormMessage); the picker's own
|
|
701
|
+
// `error` (0.22) would show it a second time.
|
|
702
|
+
Omit<DatePickerProps, "value" | "onChange" | "label" | "invalid" | "disabled" | "className" | "id" | "error"> & {
|
|
683
703
|
/** Classes for the picker. Default `w-full`. */
|
|
684
704
|
inputClassName?: string;
|
|
685
705
|
/** What a cleared date stores. Default `""`, the empty ISO string; `null` for a
|
|
@@ -766,6 +786,417 @@ export function RhfDateField<
|
|
|
766
786
|
);
|
|
767
787
|
}
|
|
768
788
|
|
|
789
|
+
// ── date range ───────────────────────────────────────────────────────────────
|
|
790
|
+
|
|
791
|
+
type OwnDateRangeProps = Omit<
|
|
792
|
+
DateRangePickerProps,
|
|
793
|
+
"from" | "to" | "onChange" | "label" | "invalid" | "disabled" | "className" | "id" | "error"
|
|
794
|
+
>;
|
|
795
|
+
|
|
796
|
+
export type RhfDateRangePickerProps<
|
|
797
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
798
|
+
TFromName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
799
|
+
TToName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
800
|
+
TTransformed = TFieldValues,
|
|
801
|
+
> = Omit<RhfFieldBaseProps<TFieldValues, TFromName, TTransformed>, "name" | "rules"> &
|
|
802
|
+
OwnDateRangeProps & {
|
|
803
|
+
/** The form field holding the range's first day, an ISO `"YYYY-MM-DD"`. */
|
|
804
|
+
fromName: TFromName;
|
|
805
|
+
/** The form field holding the range's last day. */
|
|
806
|
+
toName: TToName;
|
|
807
|
+
/** react-hook-form's `rules` for the start (`required`, a `validate` …). */
|
|
808
|
+
fromRules?: ControllerProps<TFieldValues, TFromName, TTransformed>["rules"];
|
|
809
|
+
/** react-hook-form's `rules` for the end — the place for "not before the start",
|
|
810
|
+
* which can read the start with `getValues`: the start is always written first. */
|
|
811
|
+
toRules?: ControllerProps<TFieldValues, TToName, TTransformed>["rules"];
|
|
812
|
+
/** Classes for the picker. Default `w-full`. */
|
|
813
|
+
inputClassName?: string;
|
|
814
|
+
/** What an empty end stores — a clear, or the half-made range between the first
|
|
815
|
+
* and second click. Default `""`, the empty ISO string; `null` for a nullable
|
|
816
|
+
* schema. */
|
|
817
|
+
emptyValue?: "" | null;
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* The kit's {@link DateRangePicker} over TWO form fields — `fromName` and `toName` —
|
|
822
|
+
* the way a period is stored: as two columns, each its own schema key, not one
|
|
823
|
+
* `{ from, to }` object (kastlan 4).
|
|
824
|
+
*
|
|
825
|
+
* ```tsx
|
|
826
|
+
* <RhfDateRangePicker fromName="periodFrom" toName="periodTo" label="Billing period"
|
|
827
|
+
* presets={presets} clearable />
|
|
828
|
+
* ```
|
|
829
|
+
*
|
|
830
|
+
* Why it is not {@link RhfField}: a `Controller` binds one name, and the range is
|
|
831
|
+
* two. Built by hand — an `RhfField` on the start and a second `useController` for
|
|
832
|
+
* the end, which is what kastlan's period fields needed — it works until the END is
|
|
833
|
+
* the one in error: the shell's label, message and `aria-invalid` all read the
|
|
834
|
+
* start's state alone, so "End before start" (a `refine` with `path: ["periodTo"]`,
|
|
835
|
+
* where such a rule goes) never shows. Here both states are read:
|
|
836
|
+
*
|
|
837
|
+
* - **Either field's error is shown and paints.** The label turns, the trigger is
|
|
838
|
+
* `aria-invalid` and wears the danger border when EITHER field has an error, and
|
|
839
|
+
* both messages are listed under the field (once, if the two say the same thing),
|
|
840
|
+
* each pointed at by the trigger's `aria-describedby`.
|
|
841
|
+
* - **One change writes both.** The picker reports the range whole, so the start and
|
|
842
|
+
* then the end are written on every change — the end too when only the start
|
|
843
|
+
* moved, so a cross-field rule on the end re-runs and an "End before start" clears
|
|
844
|
+
* the moment the start is fixed. A clear (`clearable`'s ×) empties both.
|
|
845
|
+
* - **Focus on error** lands on the trigger whichever of the two is in error.
|
|
846
|
+
* - **Touched** once a range is complete or cleared — not on the first click of a
|
|
847
|
+
* pick, which would mark (and, with `mode: "onTouched"`, validate) an end the user
|
|
848
|
+
* is still choosing.
|
|
849
|
+
*
|
|
850
|
+
* In `commit="immediate"` mode (the picker's default) the first calendar click is
|
|
851
|
+
* reported on its own, so the form holds `(from, emptyValue)` until the second; with
|
|
852
|
+
* `commit="apply"` nothing reaches the form until Apply.
|
|
853
|
+
*
|
|
854
|
+
* The common props (`label`, `hint`, `required`, `disabled`, `excludeWhenDisabled`,
|
|
855
|
+
* `control`, `className`) apply to the pair; `rules` are per field.
|
|
856
|
+
*/
|
|
857
|
+
export function RhfDateRangePicker<
|
|
858
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
859
|
+
TFromName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
860
|
+
TToName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
861
|
+
TTransformed = TFieldValues,
|
|
862
|
+
>({
|
|
863
|
+
fromName,
|
|
864
|
+
toName,
|
|
865
|
+
control,
|
|
866
|
+
fromRules,
|
|
867
|
+
toRules,
|
|
868
|
+
label,
|
|
869
|
+
hint,
|
|
870
|
+
required,
|
|
871
|
+
disabled,
|
|
872
|
+
excludeWhenDisabled,
|
|
873
|
+
className,
|
|
874
|
+
inputClassName,
|
|
875
|
+
emptyValue = "",
|
|
876
|
+
...pickerProps
|
|
877
|
+
}: RhfDateRangePickerProps<TFieldValues, TFromName, TToName, TTransformed>) {
|
|
878
|
+
const rhfDisabled = excludeWhenDisabled ? disabled : undefined;
|
|
879
|
+
// The end, read here; the start is the FormField below, which gives the label, hint
|
|
880
|
+
// and ids their context. This component re-renders on the end's state, and with it
|
|
881
|
+
// the FormField's render.
|
|
882
|
+
const to = useController<TFieldValues, TToName, TTransformed>({
|
|
883
|
+
name: toName,
|
|
884
|
+
control,
|
|
885
|
+
rules: toRules,
|
|
886
|
+
disabled: rhfDisabled,
|
|
887
|
+
});
|
|
888
|
+
return (
|
|
889
|
+
<FormField
|
|
890
|
+
control={control}
|
|
891
|
+
name={fromName}
|
|
892
|
+
rules={fromRules}
|
|
893
|
+
disabled={rhfDisabled}
|
|
894
|
+
render={({ field, fieldState }) => (
|
|
895
|
+
<FormItem className={className}>
|
|
896
|
+
<RangeBody
|
|
897
|
+
from={field as unknown as ControllerRenderProps}
|
|
898
|
+
fromState={fieldState}
|
|
899
|
+
to={to.field as unknown as ControllerRenderProps}
|
|
900
|
+
toState={to.fieldState}
|
|
901
|
+
label={label}
|
|
902
|
+
hint={hint}
|
|
903
|
+
required={required}
|
|
904
|
+
disabled={Boolean(disabled)}
|
|
905
|
+
emptyValue={emptyValue}
|
|
906
|
+
inputClassName={inputClassName}
|
|
907
|
+
pickerProps={pickerProps}
|
|
908
|
+
/>
|
|
909
|
+
</FormItem>
|
|
910
|
+
)}
|
|
911
|
+
/>
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function RangeBody({
|
|
916
|
+
from,
|
|
917
|
+
fromState,
|
|
918
|
+
to,
|
|
919
|
+
toState,
|
|
920
|
+
label,
|
|
921
|
+
hint,
|
|
922
|
+
required,
|
|
923
|
+
disabled,
|
|
924
|
+
emptyValue,
|
|
925
|
+
inputClassName,
|
|
926
|
+
pickerProps,
|
|
927
|
+
}: {
|
|
928
|
+
from: ControllerRenderProps;
|
|
929
|
+
fromState: ControllerFieldState;
|
|
930
|
+
to: ControllerRenderProps;
|
|
931
|
+
toState: ControllerFieldState;
|
|
932
|
+
label: ReactNode;
|
|
933
|
+
hint: ReactNode;
|
|
934
|
+
required?: boolean;
|
|
935
|
+
disabled: boolean;
|
|
936
|
+
emptyValue: "" | null;
|
|
937
|
+
inputClassName?: string;
|
|
938
|
+
pickerProps: OwnDateRangeProps;
|
|
939
|
+
}) {
|
|
940
|
+
const { id, formItemId, formMessageId, describedBy } = useFormField();
|
|
941
|
+
// The trigger forwards no ref; both fields focus it, so `shouldFocusError` lands
|
|
942
|
+
// there whichever of the two is first in error.
|
|
943
|
+
useFocusHandle(from.ref, () => document.getElementById(formItemId));
|
|
944
|
+
useFocusHandle(to.ref, () => document.getElementById(formItemId));
|
|
945
|
+
const invalid = fromState.invalid || toState.invalid;
|
|
946
|
+
const messages = [fromState.error?.message, toState.error?.message].filter(
|
|
947
|
+
(m, i, all): m is string => typeof m === "string" && m !== "" && all.indexOf(m) === i,
|
|
948
|
+
);
|
|
949
|
+
const messageIds = messages.map((_, i) => `${formMessageId}-${i}`);
|
|
950
|
+
const fromValue: unknown = from.value;
|
|
951
|
+
const toValue: unknown = to.value;
|
|
952
|
+
return (
|
|
953
|
+
<>
|
|
954
|
+
{hasContent(label) && (
|
|
955
|
+
// FormLabel colours from the START's error alone (its context is that field);
|
|
956
|
+
// the pair's state says it here.
|
|
957
|
+
<FormLabel id={`${id}-label`} required={required} data-error={invalid || undefined}>
|
|
958
|
+
{label}
|
|
959
|
+
</FormLabel>
|
|
960
|
+
)}
|
|
961
|
+
<DateRangePicker
|
|
962
|
+
{...pickerProps}
|
|
963
|
+
id={formItemId}
|
|
964
|
+
// Merged, never replaced — a caller's own reference first, as FormControl does.
|
|
965
|
+
aria-describedby={
|
|
966
|
+
[pickerProps["aria-describedby"], describedBy, ...messageIds].filter(Boolean).join(" ") || undefined
|
|
967
|
+
}
|
|
968
|
+
aria-invalid={invalid || undefined}
|
|
969
|
+
aria-required={required || undefined}
|
|
970
|
+
from={typeof fromValue === "string" ? fromValue : ""}
|
|
971
|
+
to={typeof toValue === "string" ? toValue : ""}
|
|
972
|
+
onChange={(f, t) => {
|
|
973
|
+
from.onChange(f === "" ? emptyValue : f);
|
|
974
|
+
to.onChange(t === "" ? emptyValue : t);
|
|
975
|
+
if (t !== "" || f === "") {
|
|
976
|
+
from.onBlur();
|
|
977
|
+
to.onBlur();
|
|
978
|
+
}
|
|
979
|
+
}}
|
|
980
|
+
disabled={disabled || from.disabled || to.disabled}
|
|
981
|
+
invalid={invalid}
|
|
982
|
+
className={inputClassName ?? "w-full"}
|
|
983
|
+
/>
|
|
984
|
+
{hasContent(hint) && <FormDescription>{hint}</FormDescription>}
|
|
985
|
+
{messages.map((message, i) => (
|
|
986
|
+
// FormMessage's look and its rule: no `role="alert"` — read when focus reaches
|
|
987
|
+
// the trigger, which names these in its description.
|
|
988
|
+
<p
|
|
989
|
+
key={message}
|
|
990
|
+
id={messageIds[i]}
|
|
991
|
+
data-slot="form-message"
|
|
992
|
+
className="text-[11px] leading-tight text-[var(--danger)]"
|
|
993
|
+
>
|
|
994
|
+
{message}
|
|
995
|
+
</p>
|
|
996
|
+
))}
|
|
997
|
+
</>
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// ── time ─────────────────────────────────────────────────────────────────────
|
|
1002
|
+
|
|
1003
|
+
export type RhfTimeInputProps<
|
|
1004
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
1005
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
1006
|
+
TTransformed = TFieldValues,
|
|
1007
|
+
> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &
|
|
1008
|
+
Omit<
|
|
1009
|
+
TimeInputProps,
|
|
1010
|
+
| "name"
|
|
1011
|
+
| "value"
|
|
1012
|
+
| "defaultValue"
|
|
1013
|
+
| "onChange"
|
|
1014
|
+
| "onValueChange"
|
|
1015
|
+
| "onBlur"
|
|
1016
|
+
| "label"
|
|
1017
|
+
| "error"
|
|
1018
|
+
| "invalid"
|
|
1019
|
+
| "className"
|
|
1020
|
+
| "disabled"
|
|
1021
|
+
| "required"
|
|
1022
|
+
> & {
|
|
1023
|
+
/** What a cleared time stores. Default `""`; `null` for a nullable schema. */
|
|
1024
|
+
emptyValue?: "" | null;
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* The kit's {@link TimeInput}: a time of day as `"HH:mm"` (`"HH:mm:ss"` when `step`
|
|
1029
|
+
* asks for seconds), normalised before it reaches the form, so the stored value
|
|
1030
|
+
* compares and sorts as a string. kastlan 4: the owner meeting's time was an
|
|
1031
|
+
* {@link RhfField} with a hand-wired `TimeInput` in its render — `ref`, `name`, the
|
|
1032
|
+
* `?? ""`, `onValueChange`, `onBlur`, `invalid` — the seven lines every bound field
|
|
1033
|
+
* folds into one.
|
|
1034
|
+
*
|
|
1035
|
+
* `min` / `max` paint an out-of-window time (see TimeInput) but do not fail the form:
|
|
1036
|
+
* validation stays the schema's or `rules`'. `rules={{ validate: (v) =>
|
|
1037
|
+
* isTimeInRange(v, min, max) || "…" }}` says it with the same window.
|
|
1038
|
+
*/
|
|
1039
|
+
export function RhfTimeInput<
|
|
1040
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
1041
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
1042
|
+
TTransformed = TFieldValues,
|
|
1043
|
+
>({
|
|
1044
|
+
name,
|
|
1045
|
+
control,
|
|
1046
|
+
rules,
|
|
1047
|
+
label,
|
|
1048
|
+
hint,
|
|
1049
|
+
required,
|
|
1050
|
+
disabled,
|
|
1051
|
+
excludeWhenDisabled,
|
|
1052
|
+
className,
|
|
1053
|
+
emptyValue = "",
|
|
1054
|
+
...timeProps
|
|
1055
|
+
}: RhfTimeInputProps<TFieldValues, TName, TTransformed>) {
|
|
1056
|
+
return (
|
|
1057
|
+
<RhfField
|
|
1058
|
+
{...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
|
|
1059
|
+
render={({ field, invalid }) => {
|
|
1060
|
+
const value: unknown = field.value;
|
|
1061
|
+
return (
|
|
1062
|
+
<TimeInput
|
|
1063
|
+
aria-required={required || undefined}
|
|
1064
|
+
{...timeProps}
|
|
1065
|
+
name={field.name}
|
|
1066
|
+
ref={field.ref}
|
|
1067
|
+
value={typeof value === "string" ? value : ""}
|
|
1068
|
+
onValueChange={(v) => field.onChange(v === "" ? emptyValue : v)}
|
|
1069
|
+
onBlur={field.onBlur}
|
|
1070
|
+
disabled={field.disabled}
|
|
1071
|
+
invalid={invalid}
|
|
1072
|
+
/>
|
|
1073
|
+
);
|
|
1074
|
+
}}
|
|
1075
|
+
/>
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// ── IBAN and phone (0.22) ────────────────────────────────────────────────────
|
|
1080
|
+
|
|
1081
|
+
type OwnTextishProps<P> = Omit<
|
|
1082
|
+
P,
|
|
1083
|
+
| "name"
|
|
1084
|
+
| "value"
|
|
1085
|
+
| "defaultValue"
|
|
1086
|
+
| "onChange"
|
|
1087
|
+
| "onValueChange"
|
|
1088
|
+
| "onBlur"
|
|
1089
|
+
| "label"
|
|
1090
|
+
| "error"
|
|
1091
|
+
| "invalid"
|
|
1092
|
+
| "className"
|
|
1093
|
+
| "disabled"
|
|
1094
|
+
| "required"
|
|
1095
|
+
>;
|
|
1096
|
+
|
|
1097
|
+
export type RhfIbanInputProps<
|
|
1098
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
1099
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
1100
|
+
TTransformed = TFieldValues,
|
|
1101
|
+
> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> & OwnTextishProps<IbanInputProps>;
|
|
1102
|
+
|
|
1103
|
+
/**
|
|
1104
|
+
* {@link IbanInput} bound to one field, which stores the COMPACT upper-case IBAN (what
|
|
1105
|
+
* kastlan's server checks and its QR bill prints). The field's own checksum and
|
|
1106
|
+
* `kind` messages show while the form has nothing to say; a form error (the schema's
|
|
1107
|
+
* or `rules`') replaces them, as a caller's `error` does on the bare field. kastlan
|
|
1108
|
+
* asked for the field; it binds everything through react-hook-form.
|
|
1109
|
+
*/
|
|
1110
|
+
export function RhfIbanInput<
|
|
1111
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
1112
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
1113
|
+
TTransformed = TFieldValues,
|
|
1114
|
+
>({
|
|
1115
|
+
name,
|
|
1116
|
+
control,
|
|
1117
|
+
rules,
|
|
1118
|
+
label,
|
|
1119
|
+
hint,
|
|
1120
|
+
required,
|
|
1121
|
+
disabled,
|
|
1122
|
+
excludeWhenDisabled,
|
|
1123
|
+
className,
|
|
1124
|
+
...ibanProps
|
|
1125
|
+
}: RhfIbanInputProps<TFieldValues, TName, TTransformed>) {
|
|
1126
|
+
return (
|
|
1127
|
+
<RhfField
|
|
1128
|
+
{...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
|
|
1129
|
+
render={({ field, invalid }) => {
|
|
1130
|
+
const value: unknown = field.value;
|
|
1131
|
+
return (
|
|
1132
|
+
<IbanInput
|
|
1133
|
+
aria-required={required || undefined}
|
|
1134
|
+
{...ibanProps}
|
|
1135
|
+
name={field.name}
|
|
1136
|
+
ref={field.ref}
|
|
1137
|
+
value={typeof value === "string" ? value : ""}
|
|
1138
|
+
onValueChange={field.onChange}
|
|
1139
|
+
onBlur={field.onBlur}
|
|
1140
|
+
disabled={field.disabled}
|
|
1141
|
+
invalid={invalid}
|
|
1142
|
+
/>
|
|
1143
|
+
);
|
|
1144
|
+
}}
|
|
1145
|
+
/>
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
export type RhfPhoneInputProps<
|
|
1150
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
1151
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
1152
|
+
TTransformed = TFieldValues,
|
|
1153
|
+
> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> & OwnTextishProps<PhoneInputProps>;
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* {@link PhoneInput} bound to one field, which stores E.164 when the number reads as
|
|
1157
|
+
* one and the typed text unchanged otherwise — kastlan's contacts keep their old free
|
|
1158
|
+
* text until someone edits it. Validation (required, a pattern for E.164 only) is the
|
|
1159
|
+
* schema's or `rules`'.
|
|
1160
|
+
*/
|
|
1161
|
+
export function RhfPhoneInput<
|
|
1162
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
1163
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
1164
|
+
TTransformed = TFieldValues,
|
|
1165
|
+
>({
|
|
1166
|
+
name,
|
|
1167
|
+
control,
|
|
1168
|
+
rules,
|
|
1169
|
+
label,
|
|
1170
|
+
hint,
|
|
1171
|
+
required,
|
|
1172
|
+
disabled,
|
|
1173
|
+
excludeWhenDisabled,
|
|
1174
|
+
className,
|
|
1175
|
+
...phoneProps
|
|
1176
|
+
}: RhfPhoneInputProps<TFieldValues, TName, TTransformed>) {
|
|
1177
|
+
return (
|
|
1178
|
+
<RhfField
|
|
1179
|
+
{...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
|
|
1180
|
+
render={({ field, invalid }) => {
|
|
1181
|
+
const value: unknown = field.value;
|
|
1182
|
+
return (
|
|
1183
|
+
<PhoneInput
|
|
1184
|
+
aria-required={required || undefined}
|
|
1185
|
+
{...phoneProps}
|
|
1186
|
+
name={field.name}
|
|
1187
|
+
ref={field.ref}
|
|
1188
|
+
value={typeof value === "string" ? value : ""}
|
|
1189
|
+
onValueChange={field.onChange}
|
|
1190
|
+
onBlur={field.onBlur}
|
|
1191
|
+
disabled={field.disabled}
|
|
1192
|
+
invalid={invalid}
|
|
1193
|
+
/>
|
|
1194
|
+
);
|
|
1195
|
+
}}
|
|
1196
|
+
/>
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
769
1200
|
// ── select ───────────────────────────────────────────────────────────────────
|
|
770
1201
|
|
|
771
1202
|
export interface RhfSelectOption {
|
|
@@ -900,6 +1331,172 @@ export function RhfCheckbox<
|
|
|
900
1331
|
);
|
|
901
1332
|
}
|
|
902
1333
|
|
|
1334
|
+
// ── toggle group ─────────────────────────────────────────────────────────────
|
|
1335
|
+
|
|
1336
|
+
/** The two shapes a {@link ToggleGroup} comes in, carried over: `allowEmpty` picks
|
|
1337
|
+
* whether a press on the chosen option clears it, and types the caption to match. */
|
|
1338
|
+
type RhfToggleGroupMode<T extends string> =
|
|
1339
|
+
| {
|
|
1340
|
+
/** One option is always the answer (the default): a press on the chosen
|
|
1341
|
+
* option re-sends it. */
|
|
1342
|
+
allowEmpty?: false;
|
|
1343
|
+
semantics?: ToggleGroupRequiredProps<T>["semantics"];
|
|
1344
|
+
caption?: ToggleGroupRequiredProps<T>["caption"];
|
|
1345
|
+
emptyValue?: undefined;
|
|
1346
|
+
}
|
|
1347
|
+
| {
|
|
1348
|
+
/** A press on the chosen option clears it, and the form stores `emptyValue`. */
|
|
1349
|
+
allowEmpty: true;
|
|
1350
|
+
semantics?: ToggleGroupClearableProps<T>["semantics"];
|
|
1351
|
+
caption?: ToggleGroupClearableProps<T>["caption"];
|
|
1352
|
+
/** What a cleared group stores. Default `null`; `""` for a schema that spells
|
|
1353
|
+
* "no choice" as an empty string. */
|
|
1354
|
+
emptyValue?: null | "";
|
|
1355
|
+
};
|
|
1356
|
+
|
|
1357
|
+
export type RhfToggleGroupProps<
|
|
1358
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
1359
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
1360
|
+
T extends string = string,
|
|
1361
|
+
TTransformed = TFieldValues,
|
|
1362
|
+
> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &
|
|
1363
|
+
Pick<
|
|
1364
|
+
ToggleGroupBaseProps<T>,
|
|
1365
|
+
"options" | "optionClassName" | "overflow" | "size" | "aria-label" | "commit" | "disabledReason"
|
|
1366
|
+
> & {
|
|
1367
|
+
/** Classes for the group itself — `className` is the item's box. */
|
|
1368
|
+
groupClassName?: string;
|
|
1369
|
+
} & RhfToggleGroupMode<T>;
|
|
1370
|
+
|
|
1371
|
+
/**
|
|
1372
|
+
* The kit's {@link ToggleGroup} bound to one field: a choice between a few answers
|
|
1373
|
+
* ("Monthly / Quarterly / Yearly"), stored as the chosen option's `value`. kastlan 4
|
|
1374
|
+
* asked for it with {@link RhfTimeInput} and {@link RhfDateRangePicker}: the controls
|
|
1375
|
+
* its forms still had to wire by hand through {@link RhfField}'s render.
|
|
1376
|
+
*
|
|
1377
|
+
* The label is the form's {@link FormLabel} above the bare group, as on every other
|
|
1378
|
+
* bound field — the `labelPlacement="above"` shape, not the group's own field chrome —
|
|
1379
|
+
* and it names the group (`aria-labelledby`); the hint and the message describe it,
|
|
1380
|
+
* and an error paints the group's border. Focus on error goes to the option a
|
|
1381
|
+
* keyboard user would land on (the chosen one, else the first); touched is set when
|
|
1382
|
+
* focus leaves the group, not when it moves between options.
|
|
1383
|
+
*
|
|
1384
|
+
* Required by default, as the group is: a press on the chosen option keeps it. With
|
|
1385
|
+
* `allowEmpty`, a second press clears the choice and the form stores `emptyValue`
|
|
1386
|
+
* (`null` by default) — the options then become toggle buttons, see ToggleGroup. A
|
|
1387
|
+
* value that matches no option (an empty default) leaves nothing pressed; that the
|
|
1388
|
+
* field still needs answering is the schema's or `rules`' to say.
|
|
1389
|
+
*/
|
|
1390
|
+
export function RhfToggleGroup<
|
|
1391
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
1392
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
1393
|
+
T extends string = string,
|
|
1394
|
+
TTransformed = TFieldValues,
|
|
1395
|
+
>({
|
|
1396
|
+
name,
|
|
1397
|
+
control,
|
|
1398
|
+
rules,
|
|
1399
|
+
label,
|
|
1400
|
+
hint,
|
|
1401
|
+
required,
|
|
1402
|
+
disabled,
|
|
1403
|
+
excludeWhenDisabled,
|
|
1404
|
+
className,
|
|
1405
|
+
...groupProps
|
|
1406
|
+
}: RhfToggleGroupProps<TFieldValues, TName, T, TTransformed>) {
|
|
1407
|
+
const labelled = hasContent(label);
|
|
1408
|
+
return (
|
|
1409
|
+
<RhfField
|
|
1410
|
+
{...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
|
|
1411
|
+
asControl={false}
|
|
1412
|
+
render={({ field, invalid, id, labelId, describedBy }) => (
|
|
1413
|
+
<ToggleControl<T>
|
|
1414
|
+
field={field as unknown as ControllerRenderProps}
|
|
1415
|
+
invalid={invalid}
|
|
1416
|
+
required={required}
|
|
1417
|
+
id={id}
|
|
1418
|
+
labelledBy={labelled ? labelId : undefined}
|
|
1419
|
+
describedBy={describedBy}
|
|
1420
|
+
groupProps={groupProps}
|
|
1421
|
+
/>
|
|
1422
|
+
)}
|
|
1423
|
+
/>
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
function ToggleControl<T extends string>({
|
|
1428
|
+
field,
|
|
1429
|
+
invalid,
|
|
1430
|
+
required,
|
|
1431
|
+
id,
|
|
1432
|
+
labelledBy,
|
|
1433
|
+
describedBy,
|
|
1434
|
+
groupProps,
|
|
1435
|
+
}: {
|
|
1436
|
+
field: ControllerRenderProps;
|
|
1437
|
+
invalid: boolean;
|
|
1438
|
+
required?: boolean;
|
|
1439
|
+
id: string;
|
|
1440
|
+
labelledBy: string | undefined;
|
|
1441
|
+
describedBy: string | undefined;
|
|
1442
|
+
groupProps: Omit<RhfToggleGroupProps<FieldValues, string, T>, keyof RhfFieldBaseProps>;
|
|
1443
|
+
}) {
|
|
1444
|
+
const { groupClassName, emptyValue, allowEmpty, semantics, caption, ...rest } = groupProps;
|
|
1445
|
+
// ToggleGroup forwards no ref. Its options are buttons: the radio shape's one tab
|
|
1446
|
+
// stop (`tabindex="0"`), or in the pressed shape the pressed one — else the first.
|
|
1447
|
+
useFocusHandle(field.ref, () => {
|
|
1448
|
+
const group = document.getElementById(id);
|
|
1449
|
+
return (
|
|
1450
|
+
group?.querySelector<HTMLElement>('button[tabindex="0"], button[aria-pressed="true"]') ??
|
|
1451
|
+
group?.querySelector<HTMLElement>("button")
|
|
1452
|
+
);
|
|
1453
|
+
});
|
|
1454
|
+
// React's onBlur is focusout: it fires on every move between options as well, and
|
|
1455
|
+
// only leaving the group is a blur of the FIELD.
|
|
1456
|
+
const onBlur = (e: FocusEvent<HTMLDivElement>) => {
|
|
1457
|
+
if (!e.currentTarget.contains(e.relatedTarget as Node | null)) field.onBlur();
|
|
1458
|
+
};
|
|
1459
|
+
const raw: unknown = field.value;
|
|
1460
|
+
const value = raw === null || raw === undefined || raw === "" ? null : (String(raw) as T);
|
|
1461
|
+
const common = {
|
|
1462
|
+
...rest,
|
|
1463
|
+
id,
|
|
1464
|
+
className: groupClassName,
|
|
1465
|
+
"aria-labelledby": labelledBy,
|
|
1466
|
+
"aria-describedby": describedBy,
|
|
1467
|
+
"aria-invalid": invalid || undefined,
|
|
1468
|
+
disabled: field.disabled,
|
|
1469
|
+
onBlur,
|
|
1470
|
+
};
|
|
1471
|
+
// The props' union pairs `allowEmpty` with its caption's type; taken apart above it no
|
|
1472
|
+
// longer narrows, so each branch says which half it is.
|
|
1473
|
+
if (allowEmpty) {
|
|
1474
|
+
return (
|
|
1475
|
+
<ToggleGroup<T>
|
|
1476
|
+
{...common}
|
|
1477
|
+
allowEmpty
|
|
1478
|
+
caption={caption as ToggleGroupClearableProps<T>["caption"]}
|
|
1479
|
+
value={value}
|
|
1480
|
+
onChange={(v) => field.onChange(v ?? emptyValue ?? null)}
|
|
1481
|
+
/>
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
return (
|
|
1485
|
+
<ToggleGroup<T>
|
|
1486
|
+
{...common}
|
|
1487
|
+
allowEmpty={false}
|
|
1488
|
+
semantics={semantics}
|
|
1489
|
+
caption={caption as ToggleGroupRequiredProps<T>["caption"]}
|
|
1490
|
+
// A radiogroup takes `aria-required`; a group of toggle buttons does not.
|
|
1491
|
+
aria-required={required && semantics !== "pressed" ? true : undefined}
|
|
1492
|
+
// An empty default matches no option: nothing pressed, the first option the tab
|
|
1493
|
+
// stop.
|
|
1494
|
+
value={(value ?? "") as T}
|
|
1495
|
+
onChange={(v) => field.onChange(v)}
|
|
1496
|
+
/>
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
903
1500
|
// ── comboboxes ───────────────────────────────────────────────────────────────
|
|
904
1501
|
|
|
905
1502
|
export type RhfComboboxProps<
|
package/src/rhf.ts
CHANGED
|
@@ -10,7 +10,8 @@ export * from "./rhf/form";
|
|
|
10
10
|
// The react-hook-form bridge to the wizard's Next gate (useWizard lives in /wizard).
|
|
11
11
|
export * from "./rhf/use-rhf-wizard-step";
|
|
12
12
|
// Bound fields — one line per field (RhfTextField, RhfNumberField, RhfIntegerField, RhfMoneyField,
|
|
13
|
-
// RhfDateField, RhfTextarea, RhfSelect, RhfCheckbox, RhfCombobox, RhfTextCombobox
|
|
13
|
+
// RhfDateField, RhfTextarea, RhfSelect, RhfCheckbox, RhfCombobox, RhfTextCombobox; 0.22: RhfTimeInput,
|
|
14
|
+
// RhfDateRangePicker, RhfToggleGroup, RhfIbanInput, RhfPhoneInput)
|
|
14
15
|
// and RhfField, the shell they are built on.
|
|
15
16
|
export * from "./rhf/fields";
|
|
16
17
|
// LineItems over useFieldArray.
|