@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
|
@@ -3,6 +3,9 @@ import type { ComponentPropsWithoutRef, KeyboardEvent, ReactElement, ReactNode }
|
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
import { horizontalStep } from "../lib/direction";
|
|
5
5
|
import { FIELD_INVALID, FloatingField, Label } from "./ui";
|
|
6
|
+
import { Tooltip } from "./tooltip";
|
|
7
|
+
import { useCommitReason } from "./write-lock";
|
|
8
|
+
import { hasContent, LabelStrip } from "./field-parts";
|
|
6
9
|
|
|
7
10
|
export interface ToggleOption<T extends string> {
|
|
8
11
|
value: T;
|
|
@@ -59,6 +62,9 @@ export interface ToggleGroupBaseProps<T extends string>
|
|
|
59
62
|
* `sm`: 12px options with `px-2 py-1` — the compact group keksdose's rule editor
|
|
60
63
|
* (rule-editor:204) writes as `optionClassName="px-2 py-1 text-xs"` beside a small
|
|
61
64
|
* caption. `md` (default) is the size every other group has.
|
|
65
|
+
*
|
|
66
|
+
* Honoured inside the field chrome and the strip too (keksdose K1): the 12px type
|
|
67
|
+
* stays, and the field is still 42px — see `CHROME_PAD`.
|
|
62
68
|
*/
|
|
63
69
|
size?: "sm" | "md";
|
|
64
70
|
/**
|
|
@@ -84,11 +90,32 @@ export interface ToggleGroupBaseProps<T extends string>
|
|
|
84
90
|
* cap pair sits in a `Field` column between two labelled-above inputs, where the
|
|
85
91
|
* chrome's inner label would be the only one of its kind).
|
|
86
92
|
*
|
|
93
|
+
* `"strip"`: the 11px static label (and `hint`) in a strip OVER the bare group, for a
|
|
94
|
+
* group that stands in a row of fields but is not one (keksdose K2 — the transaction
|
|
95
|
+
* row's cleared-status picker, live #288 / #431 / dev#468). No field border: the group
|
|
96
|
+
* keeps its own box, and its segments drop their vertical padding so a 16px strip
|
|
97
|
+
* plus the 26px group is exactly a labelled {@link Input}'s 42px, the label on the
|
|
98
|
+
* same line as theirs. `error` and `caption` go under the group, as in the other two.
|
|
99
|
+
*
|
|
87
100
|
* Inside a `Field`, pass no `label` at all and spread the render-prop instead —
|
|
88
101
|
* `{(ids, { labelId }) => <ToggleGroup {...ids} aria-labelledby={labelId} … />}` —
|
|
89
102
|
* so the Field's label names the group and its hint and error describe it.
|
|
90
103
|
*/
|
|
91
|
-
labelPlacement?: "field" | "above";
|
|
104
|
+
labelPlacement?: "field" | "above" | "strip";
|
|
105
|
+
/**
|
|
106
|
+
* Classes for the field chrome's own box — the bordered, padded box round the
|
|
107
|
+
* segments — merged last, so they win. `className` styles the wrapper the chrome sits
|
|
108
|
+
* in; this reaches the box itself. Only with `label` in the `"field"` placement.
|
|
109
|
+
*
|
|
110
|
+
* For a field JOINED flush to a neighbour (keksdose K1): reports-filters.tsx joins its
|
|
111
|
+
* "group by" field to the range select on its start edge from `md` up, and squares the
|
|
112
|
+
* shared corners by hand chrome today — here that is
|
|
113
|
+
* `chromeClassName="md:rounded-s-none"` with `className="md:-ms-px"` (the wrapper
|
|
114
|
+
* overlaps the neighbour by the one border they share). A class rather than a
|
|
115
|
+
* `joined="start"` because every such join keksdose has is responsive: on a phone the
|
|
116
|
+
* two stack and both keep their corners.
|
|
117
|
+
*/
|
|
118
|
+
chromeClassName?: string;
|
|
92
119
|
/** A {@link FieldHint} on the label line, as on a labelled {@link Select}. Only with
|
|
93
120
|
* `label`. */
|
|
94
121
|
hint?: ReactNode;
|
|
@@ -96,8 +123,60 @@ export interface ToggleGroupBaseProps<T extends string>
|
|
|
96
123
|
* `--danger`, marks the group `aria-invalid` and describes it with the message, as
|
|
97
124
|
* {@link Select}'s `error` does. Only with `label`. */
|
|
98
125
|
error?: ReactNode;
|
|
126
|
+
/**
|
|
127
|
+
* Why the group cannot be changed right now — {@link Button}'s `disabledReason`, for
|
|
128
|
+
* a group that saves itself on change (keksdose K3: the last `SaveGuard` sites are
|
|
129
|
+
* self-committing controls like this one, and SaveGuard's native `disabled` took
|
|
130
|
+
* them out of the tab order so the reason never reached a keyboard).
|
|
131
|
+
*
|
|
132
|
+
* The group stays reachable — its segments are `aria-disabled`, not `disabled`, and
|
|
133
|
+
* a radio group's arrow keys still walk them — but nothing reaches `onChange`, and
|
|
134
|
+
* the reason shows in the kit {@link Tooltip} over the segments on hover or focus and
|
|
135
|
+
* describes each segment. The chosen option keeps its fill, as under `disabled`.
|
|
136
|
+
* Wins over `disabled`, as on Button: passing both keeps the group reachable.
|
|
137
|
+
*/
|
|
138
|
+
disabledReason?: ReactNode;
|
|
139
|
+
/**
|
|
140
|
+
* This group COMMITS — choosing an option saves it. Under a locked
|
|
141
|
+
* {@link WriteLockProvider} it takes the `disabledReason` path with the lock's
|
|
142
|
+
* reason (which wins over a `disabledReason` of its own): *"everything renders; only
|
|
143
|
+
* the commit is locked"* (keksdose dev#496/#497), and a control that is its own commit
|
|
144
|
+
* must render refused, never live-and-discarding. No provider, or an unlocked one:
|
|
145
|
+
* no effect.
|
|
146
|
+
*/
|
|
147
|
+
commit?: boolean;
|
|
99
148
|
}
|
|
100
149
|
|
|
150
|
+
/**
|
|
151
|
+
* The field chrome's vertical geometry — keksdose K1, the kit reintroducing feedback
|
|
152
|
+
* #117 that keksdose's hand-built chrome had fixed.
|
|
153
|
+
*
|
|
154
|
+
* The label is the field's static label: `top-1` and 11px in a `leading-tight` line, so
|
|
155
|
+
* its line box runs from 4px to 17.75px. Until 0.22 the chrome was `pt-4` over a 20px
|
|
156
|
+
* (`leading-5`) segment, so the ACTIVE segment's fill — painted from the top of the
|
|
157
|
+
* segment — started at 17px (1px border + 16px), 0.75px INSIDE the label's line box:
|
|
158
|
+
* the highlight touched the word above it, exactly what #117's rework had measured and
|
|
159
|
+
* cured with `pt-5`.
|
|
160
|
+
*
|
|
161
|
+
* Now the strip is `pt-5` and the fill starts at 21px, 3.25px clear, and the segment
|
|
162
|
+
* gives the 4px back so the field stays a labelled Select's 42px: `md` sets its 14px
|
|
163
|
+
* text in an 18px line with a 2px moat under it (the bare group's own `p-0.5` moat),
|
|
164
|
+
* `sm` sets 12px text in a 16px line over `pb-1` — keksdose's measured chrome
|
|
165
|
+
* (price-level-picker, reports-filters: `pt-5 pb-1`, `text-xs md:py-0`), byte for byte.
|
|
166
|
+
* Before, `size="sm"` inside the chrome was overridden to the md 20px line.
|
|
167
|
+
*
|
|
168
|
+
* md: 1 + 20 + 18 + 2 + 1 = 42 sm: 1 + 20 + 16 + 4 + 1 = 42
|
|
169
|
+
*/
|
|
170
|
+
const CHROME_PAD: Record<"sm" | "md", string> = { md: "pt-5 pb-0.5", sm: "pt-5 pb-1" };
|
|
171
|
+
const CHROME_SEGMENT: Record<"sm" | "md", string> = { md: "py-0 leading-[18px]", sm: "py-0 leading-4" };
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The strip placement's segments: a 26px group (1 + 2 + 20 + 2 + 1) under the 16px
|
|
175
|
+
* strip — `md`'s 20px text-sm line with no padding, `sm`'s 16px line with 2px either
|
|
176
|
+
* side. See `LABEL_STRIP_PAD` for why 16.
|
|
177
|
+
*/
|
|
178
|
+
const STRIP_SEGMENT: Record<"sm" | "md", string> = { md: "py-0", sm: "py-0.5" };
|
|
179
|
+
|
|
101
180
|
/**
|
|
102
181
|
* A line under the group saying what the CHOSEN option means — lenkbank's ToggleField
|
|
103
182
|
* `hint` (features/gear/common.tsx:97): not help behind a "?" but a caption, and one
|
|
@@ -189,18 +268,30 @@ export function ToggleGroup<T extends string>(props: ToggleGroupProps<T>): React
|
|
|
189
268
|
size = "md",
|
|
190
269
|
label,
|
|
191
270
|
labelPlacement = "field",
|
|
271
|
+
chromeClassName,
|
|
192
272
|
hint,
|
|
193
273
|
error,
|
|
274
|
+
disabledReason: ownDisabledReason,
|
|
275
|
+
commit,
|
|
194
276
|
"aria-label": ariaLabelAttr,
|
|
195
277
|
...restWithMode
|
|
196
278
|
} = props;
|
|
197
279
|
const labelId = useId();
|
|
198
280
|
const errorId = useId();
|
|
199
|
-
const
|
|
200
|
-
//
|
|
281
|
+
const reasonId = useId();
|
|
282
|
+
// The lock's reason under a locked provider (with `commit`), else the group's own.
|
|
283
|
+
const disabledReason = useCommitReason(commit, ownDisabledReason);
|
|
284
|
+
const locked = hasContent(disabledReason);
|
|
285
|
+
// Locked looks disabled — the dimmed group, the not-allowed cursor, no hover offer —
|
|
286
|
+
// but stays focusable; `disabled` alone also takes the segments out of the tab order.
|
|
287
|
+
const dimmed = disabled || locked;
|
|
288
|
+
const labelled = hasContent(label);
|
|
289
|
+
// `field` is the chrome; a label placed above or in a strip keeps the bare group's
|
|
290
|
+
// own box.
|
|
201
291
|
const field = labelled && labelPlacement === "field";
|
|
202
292
|
const above = labelled && labelPlacement === "above";
|
|
203
|
-
const
|
|
293
|
+
const strip = labelled && labelPlacement === "strip";
|
|
294
|
+
const hasError = labelled && hasContent(error);
|
|
204
295
|
// Taken off the rest so neither reaches the DOM; `props` keeps them paired, which is
|
|
205
296
|
// what lets the `onChange` below be called with `null` only in the mode that allows it.
|
|
206
297
|
const { allowEmpty: _allowEmpty, onChange: _onChange, caption, semantics: _semantics, ...rest } = restWithMode;
|
|
@@ -210,8 +301,11 @@ export function ToggleGroup<T extends string>(props: ToggleGroupProps<T>): React
|
|
|
210
301
|
const captionId = useId();
|
|
211
302
|
const captionIsLive = typeof caption === "function";
|
|
212
303
|
const captionNode = captionIsLive ? (caption as (v: T | null) => ReactNode)(value) : caption;
|
|
213
|
-
const hasCaption = captionNode
|
|
304
|
+
const hasCaption = hasContent(captionNode);
|
|
214
305
|
const choose = (next: T) => {
|
|
306
|
+
// A locked group swallows the change: it saves on change, and a lock that let the
|
|
307
|
+
// value move and then discarded it would show a state that was never stored.
|
|
308
|
+
if (locked) return;
|
|
215
309
|
if (props.allowEmpty) props.onChange(next === value ? null : next);
|
|
216
310
|
else props.onChange(next);
|
|
217
311
|
};
|
|
@@ -236,10 +330,79 @@ export function ToggleGroup<T extends string>(props: ToggleGroupProps<T>): React
|
|
|
236
330
|
if (next === null) return;
|
|
237
331
|
e.preventDefault();
|
|
238
332
|
next = next < 0 ? last : next > last ? 0 : next;
|
|
333
|
+
// The segments' own parent: the group, or under a lock the tooltip's row inside it.
|
|
239
334
|
const buttons = e.currentTarget.parentElement?.querySelectorAll<HTMLButtonElement>(":scope > button");
|
|
240
335
|
buttons?.[next]?.focus();
|
|
336
|
+
// Locked: the focus walks (each segment says why on arrival), the choice does not.
|
|
241
337
|
choose(options[next].value);
|
|
242
338
|
};
|
|
339
|
+
const segments = options.map((opt, index) => {
|
|
340
|
+
const active = opt.value === value;
|
|
341
|
+
return (
|
|
342
|
+
<button
|
|
343
|
+
key={opt.value}
|
|
344
|
+
type="button"
|
|
345
|
+
role={pressed ? undefined : "radio"}
|
|
346
|
+
aria-checked={pressed ? undefined : active}
|
|
347
|
+
aria-pressed={pressed ? active : undefined}
|
|
348
|
+
// A reason wins over `disabled` (as on Button): reachable, and refusing.
|
|
349
|
+
disabled={disabled && !locked}
|
|
350
|
+
aria-disabled={locked || undefined}
|
|
351
|
+
// On each segment rather than the group: the segment is what takes focus, and
|
|
352
|
+
// a group's description is not read reliably when focus lands inside it.
|
|
353
|
+
aria-describedby={locked ? reasonId : undefined}
|
|
354
|
+
tabIndex={pressed ? undefined : opt.value === tabStop ? 0 : -1}
|
|
355
|
+
onKeyDown={pressed ? undefined : (e) => onRadioKey(e, index)}
|
|
356
|
+
onClick={() => choose(opt.value)}
|
|
357
|
+
className={cn(
|
|
358
|
+
// `truncate` (which carries whitespace-nowrap) rather than letting a
|
|
359
|
+
// label wrap: a segmented control sizes its whole row to the tallest
|
|
360
|
+
// option, so one two-word option — Keksdose feedback #147's "Where I
|
|
361
|
+
// am" — silently doubles the height of every segment beside it.
|
|
362
|
+
//
|
|
363
|
+
// `basis-auto` is what keeps that ellipsis a LAST resort rather than
|
|
364
|
+
// the normal state (Keksdose dev#475). With flex-1's `basis-0`, a
|
|
365
|
+
// shrink-to-fit group (`w-auto`) still resolves to the sum of the
|
|
366
|
+
// labels' widths — and then splits it EQUALLY, so the short option
|
|
367
|
+
// got 66px it did not need and "Where I am" got 66 of the 81 it did:
|
|
368
|
+
// truncated at 1778px of free screen. Basing each segment on its own
|
|
369
|
+
// content and sharing only the LEFTOVER space keeps a full-width
|
|
370
|
+
// group's segments near-equal and an auto-width group's exact.
|
|
371
|
+
//
|
|
372
|
+
// `focus-visible` + `ring-inset`, not `focus` + an outset ring. A ring
|
|
373
|
+
// is a box-shadow that spreads OUTWARD, so on a flush group it painted
|
|
374
|
+
// 2px of ring over both neighbours and over the container's own border
|
|
375
|
+
// — and on a phone it appeared on every TAP, because a tap focuses the
|
|
376
|
+
// button. That is the other half of what live #268's rework saw
|
|
377
|
+
// overlaying the selected segment's boundary. Inset keeps the ring
|
|
378
|
+
// inside the segment it belongs to; focus-visible keeps it for the
|
|
379
|
+
// keyboard, which is the only input that needs it.
|
|
380
|
+
overflow === "wrap" ? "whitespace-nowrap" : "min-w-0 truncate",
|
|
381
|
+
"flex-1 basis-auto rounded px-3 py-1.5 text-sm font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--border-strong)]",
|
|
382
|
+
size === "sm" && "px-2 py-1 text-xs",
|
|
383
|
+
// In the field chrome: no vertical padding, and the line that makes the
|
|
384
|
+
// field 42px under a strip that clears the label — see CHROME_PAD. `sm`
|
|
385
|
+
// keeps its 12px type here (it used to be overridden to the md line).
|
|
386
|
+
field && CHROME_SEGMENT[size],
|
|
387
|
+
// In the strip placement: a 26px group under a 16px strip — STRIP_SEGMENT.
|
|
388
|
+
strip && STRIP_SEGMENT[size],
|
|
389
|
+
active
|
|
390
|
+
? "bg-[var(--bg-inverse)] text-[var(--text-inverse)]"
|
|
391
|
+
: "text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]",
|
|
392
|
+
// No hover fill on a group that cannot be changed — a segment that
|
|
393
|
+
// lights up under the pointer is an offer, and there is none here. Not on
|
|
394
|
+
// the chosen segment: its fill IS its state, and its inverse text would
|
|
395
|
+
// vanish on a transparent hover.
|
|
396
|
+
dimmed && "cursor-not-allowed",
|
|
397
|
+
dimmed && !active && "hover:bg-transparent dark:hover:bg-transparent",
|
|
398
|
+
optionClassName,
|
|
399
|
+
opt.className,
|
|
400
|
+
)}
|
|
401
|
+
>
|
|
402
|
+
{opt.label}
|
|
403
|
+
</button>
|
|
404
|
+
);
|
|
405
|
+
});
|
|
243
406
|
const group = (
|
|
244
407
|
<div
|
|
245
408
|
// The audit's named example of a closed prop list (§"Public API design"): the
|
|
@@ -262,10 +425,10 @@ export function ToggleGroup<T extends string>(props: ToggleGroupProps<T>): React
|
|
|
262
425
|
aria-describedby={
|
|
263
426
|
[rest["aria-describedby"], hasCaption && captionId, hasError && errorId].filter(Boolean).join(" ") || undefined
|
|
264
427
|
}
|
|
265
|
-
// `aria-disabled` on the group as well as `disabled` on each button: a
|
|
266
|
-
// group is what the user is being refused, and a screen reader announcing
|
|
428
|
+
// `aria-disabled` on the group as well as on (or `disabled` on) each button: a
|
|
429
|
+
// radio group is what the user is being refused, and a screen reader announcing
|
|
267
430
|
// three separately-disabled radios does not say that.
|
|
268
|
-
aria-disabled={
|
|
431
|
+
aria-disabled={dimmed || undefined}
|
|
269
432
|
className={cn(
|
|
270
433
|
// `gap-0.5` — the same 2px as the container's own padding, so EVERY segment
|
|
271
434
|
// sits in a uniform 2px moat and no two fills ever touch. Flush segments were
|
|
@@ -284,75 +447,34 @@ export function ToggleGroup<T extends string>(props: ToggleGroupProps<T>): React
|
|
|
284
447
|
// The whole group fades, the way every other disabled control in this
|
|
285
448
|
// package does; `cursor-not-allowed` is on the buttons, which is what a
|
|
286
449
|
// pointer is actually over.
|
|
287
|
-
|
|
450
|
+
dimmed && "opacity-60",
|
|
288
451
|
// The bare group (no chrome to paint) wears the invalid border itself.
|
|
289
|
-
!field && (outsideInvalid || (above && hasError)) && FIELD_INVALID,
|
|
452
|
+
!field && (outsideInvalid || ((above || strip) && hasError)) && FIELD_INVALID,
|
|
290
453
|
// Inside the field's chrome the group is only a row of segments: no border, no
|
|
291
454
|
// surface, no padding of its own, and the field (not the group) is what dims.
|
|
292
455
|
field && "border-0 bg-transparent p-0 shadow-none opacity-100",
|
|
293
456
|
!labelled && className,
|
|
294
457
|
)}
|
|
295
458
|
>
|
|
296
|
-
{
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
// `basis-auto` is what keeps that ellipsis a LAST resort rather than
|
|
316
|
-
// the normal state (Keksdose dev#475). With flex-1's `basis-0`, a
|
|
317
|
-
// shrink-to-fit group (`w-auto`) still resolves to the sum of the
|
|
318
|
-
// labels' widths — and then splits it EQUALLY, so the short option
|
|
319
|
-
// got 66px it did not need and "Where I am" got 66 of the 81 it did:
|
|
320
|
-
// truncated at 1778px of free screen. Basing each segment on its own
|
|
321
|
-
// content and sharing only the LEFTOVER space keeps a full-width
|
|
322
|
-
// group's segments near-equal and an auto-width group's exact.
|
|
323
|
-
//
|
|
324
|
-
// `focus-visible` + `ring-inset`, not `focus` + an outset ring. A ring
|
|
325
|
-
// is a box-shadow that spreads OUTWARD, so on a flush group it painted
|
|
326
|
-
// 2px of ring over both neighbours and over the container's own border
|
|
327
|
-
// — and on a phone it appeared on every TAP, because a tap focuses the
|
|
328
|
-
// button. That is the other half of what live #268's rework saw
|
|
329
|
-
// overlaying the selected segment's boundary. Inset keeps the ring
|
|
330
|
-
// inside the segment it belongs to; focus-visible keeps it for the
|
|
331
|
-
// keyboard, which is the only input that needs it.
|
|
332
|
-
overflow === "wrap" ? "whitespace-nowrap" : "min-w-0 truncate",
|
|
333
|
-
"flex-1 basis-auto rounded px-3 py-1.5 text-sm font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--border-strong)]",
|
|
334
|
-
size === "sm" && "px-2 py-1 text-xs",
|
|
335
|
-
// In a field: no vertical padding and a 20px line — a `text-sm` line, the
|
|
336
|
-
// same line a labelled Select holds under its label strip — so the field's
|
|
337
|
-
// own `pt-4 pb-1` decides the height, as it does for the select.
|
|
338
|
-
field && "py-0 leading-5",
|
|
339
|
-
active
|
|
340
|
-
? "bg-[var(--bg-inverse)] text-[var(--text-inverse)]"
|
|
341
|
-
: "text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]",
|
|
342
|
-
// No hover fill on a group that cannot be changed — a segment that
|
|
343
|
-
// lights up under the pointer is an offer, and there is none here. Not on
|
|
344
|
-
// the chosen segment: its fill IS its state, and its inverse text would
|
|
345
|
-
// vanish on a transparent hover.
|
|
346
|
-
disabled && "cursor-not-allowed",
|
|
347
|
-
disabled && !active && "hover:bg-transparent dark:hover:bg-transparent",
|
|
348
|
-
optionClassName,
|
|
349
|
-
opt.className,
|
|
350
|
-
)}
|
|
351
|
-
>
|
|
352
|
-
{opt.label}
|
|
353
|
-
</button>
|
|
354
|
-
);
|
|
355
|
-
})}
|
|
459
|
+
{locked ? (
|
|
460
|
+
// The reason's bubble wraps the SEGMENTS, inside the group, not the group: a
|
|
461
|
+
// wrapper outside it would become the flex or grid item in the caller's row in
|
|
462
|
+
// the group's place, and a `w-full` / `md:w-auto` on the group would size the
|
|
463
|
+
// wrong box. This row inherits the group's gap and wrap, so the segments lay out
|
|
464
|
+
// exactly as they do unlocked, and `:scope > button` above still finds them. A
|
|
465
|
+
// fragment child, so the Tooltip describes nothing itself — each segment points
|
|
466
|
+
// at the hidden copy, the way a locked Button does.
|
|
467
|
+
<Tooltip label={disabledReason} className="flex min-w-0 flex-1 gap-[inherit] [flex-wrap:inherit]">
|
|
468
|
+
<>
|
|
469
|
+
{segments}
|
|
470
|
+
<span id={reasonId} hidden>
|
|
471
|
+
{disabledReason}
|
|
472
|
+
</span>
|
|
473
|
+
</>
|
|
474
|
+
</Tooltip>
|
|
475
|
+
) : (
|
|
476
|
+
segments
|
|
477
|
+
)}
|
|
356
478
|
</div>
|
|
357
479
|
);
|
|
358
480
|
// Rendered whenever the caption is — `aria-live` must be on the element BEFORE its
|
|
@@ -384,7 +506,7 @@ export function ToggleGroup<T extends string>(props: ToggleGroupProps<T>): React
|
|
|
384
506
|
by `aria-labelledby` on the group; the element keeps the Field look. */}
|
|
385
507
|
<Label
|
|
386
508
|
id={labelId}
|
|
387
|
-
disabled={
|
|
509
|
+
disabled={dimmed}
|
|
388
510
|
data-error={hasError || undefined}
|
|
389
511
|
className="data-[error=true]:text-[var(--danger)]"
|
|
390
512
|
>
|
|
@@ -400,6 +522,15 @@ export function ToggleGroup<T extends string>(props: ToggleGroupProps<T>): React
|
|
|
400
522
|
</div>
|
|
401
523
|
);
|
|
402
524
|
}
|
|
525
|
+
if (strip) {
|
|
526
|
+
return (
|
|
527
|
+
<LabelStrip labelId={labelId} label={label} hint={hint} disabled={dimmed} pad="field" className={className}>
|
|
528
|
+
{group}
|
|
529
|
+
{captionEl}
|
|
530
|
+
{errorEl}
|
|
531
|
+
</LabelStrip>
|
|
532
|
+
);
|
|
533
|
+
}
|
|
403
534
|
if (!field) {
|
|
404
535
|
if (!captionEl) return group;
|
|
405
536
|
// The group keeps its `className`, as without a caption; the wrapper only stacks.
|
|
@@ -422,9 +553,11 @@ export function ToggleGroup<T extends string>(props: ToggleGroupProps<T>): React
|
|
|
422
553
|
>
|
|
423
554
|
<div
|
|
424
555
|
className={cn(
|
|
425
|
-
"flex flex-1 flex-col justify-center rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-1
|
|
426
|
-
|
|
556
|
+
"flex flex-1 flex-col justify-center rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-1 shadow-sm",
|
|
557
|
+
CHROME_PAD[size],
|
|
558
|
+
dimmed && "bg-[var(--bg-surface-2)] opacity-60",
|
|
427
559
|
hasError && FIELD_INVALID,
|
|
560
|
+
chromeClassName,
|
|
428
561
|
)}
|
|
429
562
|
>
|
|
430
563
|
{group}
|
|
@@ -14,6 +14,9 @@ export interface TriggerAria {
|
|
|
14
14
|
"aria-labelledby"?: string;
|
|
15
15
|
"aria-describedby"?: string;
|
|
16
16
|
"aria-invalid"?: boolean | "true" | "false" | "grammar" | "spelling";
|
|
17
|
+
/** 0.23.0: a form library's `required` (RhfMonthPicker) is announced on the trigger,
|
|
18
|
+
* not lost on the wrapper. */
|
|
19
|
+
"aria-required"?: boolean | "true" | "false";
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
/** Split a field's props into what its trigger takes and what its wrapper takes. */
|
|
@@ -26,6 +29,7 @@ export function splitTriggerAria<T extends TriggerAria>(
|
|
|
26
29
|
"aria-labelledby": labelledBy,
|
|
27
30
|
"aria-describedby": describedBy,
|
|
28
31
|
"aria-invalid": invalid,
|
|
32
|
+
"aria-required": required,
|
|
29
33
|
...rest
|
|
30
34
|
} = props;
|
|
31
35
|
return [
|
|
@@ -35,6 +39,7 @@ export function splitTriggerAria<T extends TriggerAria>(
|
|
|
35
39
|
"aria-labelledby": labelledBy,
|
|
36
40
|
"aria-describedby": describedBy,
|
|
37
41
|
"aria-invalid": invalid,
|
|
42
|
+
"aria-required": required,
|
|
38
43
|
},
|
|
39
44
|
rest,
|
|
40
45
|
];
|