@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
|
@@ -22,6 +22,16 @@ import {
|
|
|
22
22
|
type ComboOption,
|
|
23
23
|
} from "./combobox-core";
|
|
24
24
|
import { DEFAULT_COMBOBOX_LABELS, useKitLabels } from "../i18n/kit-labels";
|
|
25
|
+
import {
|
|
26
|
+
EndHintRow,
|
|
27
|
+
FieldCaption,
|
|
28
|
+
LABEL_IN_ROW,
|
|
29
|
+
LockedReason,
|
|
30
|
+
StaticLabelRow,
|
|
31
|
+
useFieldHint,
|
|
32
|
+
useLockReason,
|
|
33
|
+
} from "./field-parts";
|
|
34
|
+
import { mergeDescribedBy } from "./choice-parts";
|
|
25
35
|
|
|
26
36
|
/**
|
|
27
37
|
* `value`/`onChange` are the TEXT's, and `onSelect` is "a suggestion was taken" —
|
|
@@ -104,6 +114,14 @@ export interface AutocompleteProps<V extends string | number = string>
|
|
|
104
114
|
* its `aria-describedby`, implies `invalid`. Not a failed lookup — that is
|
|
105
115
|
* `loadErrorLabel`, shown in the list. */
|
|
106
116
|
error?: ReactNode;
|
|
117
|
+
/**
|
|
118
|
+
* Standing advice — {@link Combobox}'s `hint`, so this field reads like every other
|
|
119
|
+
* (keksdose K4): plain TEXT is a caption under the field on the input's
|
|
120
|
+
* `aria-describedby`, before any error; a {@link FieldHint} "?" rides the label line,
|
|
121
|
+
* or with no label sits at the field's end edge, outside the box. Not the list's
|
|
122
|
+
* `status` line, which talks about the lookup, not the field.
|
|
123
|
+
*/
|
|
124
|
+
hint?: ReactNode;
|
|
107
125
|
/**
|
|
108
126
|
* The caller's own line in the list, replacing the automatic one (loading / failed
|
|
109
127
|
* / no results). For what only the caller knows: "offline", "the address service
|
|
@@ -117,6 +135,27 @@ export interface AutocompleteProps<V extends string | number = string>
|
|
|
117
135
|
loadErrorLabel?: string;
|
|
118
136
|
/** Classes for the `<input>` itself; `className` styles the wrapper. */
|
|
119
137
|
inputClassName?: string;
|
|
138
|
+
/**
|
|
139
|
+
* Why the field cannot be changed — {@link Button}'s `disabledReason`, the Combobox
|
|
140
|
+
* family's write lock (keksdose G2, 0.23), for a field whose pick SAVES: an address
|
|
141
|
+
* search whose taken row writes the address, a lookup that links a record.
|
|
142
|
+
*
|
|
143
|
+
* With a reason the input stays focusable and `aria-disabled`, is `readOnly` (the
|
|
144
|
+
* settled grey FIELD_BASE gives a `[readonly]` field), opens no list and asks no
|
|
145
|
+
* `loadOptions` — whatever `open` says — and nothing reaches `onChange`, `onSelect` or
|
|
146
|
+
* `onOpenChange(true)`. Enter is swallowed, so a locked field does not submit the form
|
|
147
|
+
* around it, and a caller's `onKeyDown` does not hear it; every other key still
|
|
148
|
+
* reaches the caller (an Escape that closes its panel) and the caret. The reason is in
|
|
149
|
+
* the kit {@link Tooltip} and on the input's `aria-describedby`. Wins over `disabled`.
|
|
150
|
+
*/
|
|
151
|
+
disabledReason?: ReactNode;
|
|
152
|
+
/**
|
|
153
|
+
* This field COMMITS. Under a locked {@link WriteLockProvider} it is locked the
|
|
154
|
+
* `disabledReason` way with the lock's reason (which wins over its own). A search
|
|
155
|
+
* whose result is only draft state — the form's Save writes it — leaves this off and
|
|
156
|
+
* stays live under the lock. No provider, or an unlocked one: no effect.
|
|
157
|
+
*/
|
|
158
|
+
commit?: boolean;
|
|
120
159
|
}
|
|
121
160
|
|
|
122
161
|
function AutocompleteInner<V extends string | number = string>(
|
|
@@ -135,6 +174,7 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
135
174
|
icon,
|
|
136
175
|
invalid,
|
|
137
176
|
error,
|
|
177
|
+
hint,
|
|
138
178
|
status,
|
|
139
179
|
emptyLabel,
|
|
140
180
|
loadErrorLabel,
|
|
@@ -145,6 +185,8 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
145
185
|
inputClassName,
|
|
146
186
|
id,
|
|
147
187
|
disabled,
|
|
188
|
+
disabledReason,
|
|
189
|
+
commit,
|
|
148
190
|
onKeyDown,
|
|
149
191
|
onFocus,
|
|
150
192
|
onBlur,
|
|
@@ -166,7 +208,8 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
166
208
|
noResults: emptyLabel,
|
|
167
209
|
loadError: loadErrorLabel,
|
|
168
210
|
});
|
|
169
|
-
const
|
|
211
|
+
const hintParts = useFieldHint(hint, ariaDescribedBy);
|
|
212
|
+
const field = useComboboxFieldError(error, invalid, hintParts.describedBy);
|
|
170
213
|
|
|
171
214
|
/** The user's intent: the field is focused and the list has not been dismissed.
|
|
172
215
|
* Whether anything SHOWS is decided below, from what there is to show. */
|
|
@@ -180,7 +223,11 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
180
223
|
if (next !== open) onOpenChange?.(next);
|
|
181
224
|
};
|
|
182
225
|
const [active, setActive] = useState(-1);
|
|
183
|
-
|
|
226
|
+
// The lock: focusable, `readOnly`, no list — see `disabledReason`. Folded into `live`,
|
|
227
|
+
// so a locked field asks nothing and shows nothing even with `open` held true.
|
|
228
|
+
const lock = useLockReason(commit, disabledReason);
|
|
229
|
+
const locked = lock.locked;
|
|
230
|
+
const live = open && !disabled && !locked;
|
|
184
231
|
const { results, busy, failed, tooShort } = useOptionSource<V>({
|
|
185
232
|
options,
|
|
186
233
|
loadOptions,
|
|
@@ -219,7 +266,7 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
219
266
|
setActive(-1);
|
|
220
267
|
};
|
|
221
268
|
const take = (o: ComboOption<V>) => {
|
|
222
|
-
if (o.disabled) return;
|
|
269
|
+
if (o.disabled || locked) return;
|
|
223
270
|
if (fillOnSelect) onChange(o.label);
|
|
224
271
|
onSelect?.(o);
|
|
225
272
|
close();
|
|
@@ -234,6 +281,16 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
234
281
|
// label still names the field.
|
|
235
282
|
const labelledBy =
|
|
236
283
|
ariaLabel === undefined ? (ariaLabelledBy ?? (hasLabel ? labelId : undefined)) : undefined;
|
|
284
|
+
// While locked the field sits in the reason's Tooltip — in a fragment with a hidden
|
|
285
|
+
// copy of the reason, Button's anatomy; `block` so a full-width field stays full width.
|
|
286
|
+
const withLock = (box: ReactNode) =>
|
|
287
|
+
locked ? (
|
|
288
|
+
<LockedReason lock={lock} className="block">
|
|
289
|
+
{box}
|
|
290
|
+
</LockedReason>
|
|
291
|
+
) : (
|
|
292
|
+
box
|
|
293
|
+
);
|
|
237
294
|
|
|
238
295
|
return (
|
|
239
296
|
// `relative`: the floating label and the live region's `sr-only` both need a
|
|
@@ -241,124 +298,151 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
241
298
|
<div className={cn("relative", className)}>
|
|
242
299
|
{/* A real <label for> (lenkbank), and still `aria-labelledby`'s target, so the
|
|
243
300
|
name — and the listbox's, which borrows it — is exactly what it was. */}
|
|
244
|
-
{hasLabel && (
|
|
301
|
+
{hasLabel && hintParts.labelHint === undefined && (
|
|
245
302
|
<ComboboxFieldLabel id={labelId} htmlFor={fieldId}>
|
|
246
303
|
{label}
|
|
247
304
|
</ComboboxFieldLabel>
|
|
248
305
|
)}
|
|
249
|
-
<
|
|
250
|
-
{
|
|
251
|
-
<
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
306
|
+
<EndHintRow hint={hasLabel ? undefined : hintParts.labelHint}>
|
|
307
|
+
{withLock(
|
|
308
|
+
<div ref={fieldRef} className={cn("relative", locked && "cursor-not-allowed")}>
|
|
309
|
+
{icon && (
|
|
310
|
+
<span
|
|
311
|
+
aria-hidden
|
|
312
|
+
className={cn(
|
|
313
|
+
"pointer-events-none absolute flex text-[var(--text-muted)]",
|
|
314
|
+
small ? "start-2 [&>svg]:size-3.5" : "start-2.5 [&>svg]:size-4",
|
|
315
|
+
// Labelled, the icon sits on the VALUE's line — the one line box below
|
|
316
|
+
// FIELD_FLOATING_PAD's `pt-4` and the 1px border — not on the middle of
|
|
317
|
+
// the box. Centred on the box it rose into the top strip and sat on the
|
|
318
|
+
// label's first letters, which float at the same start inset. On the
|
|
319
|
+
// value line it is beside what it describes, and the label keeps the
|
|
320
|
+
// start edge every other label in the form's column shares.
|
|
321
|
+
hasLabel
|
|
322
|
+
? "top-[calc(1rem+1px)] h-5 items-center"
|
|
323
|
+
: "top-1/2 -translate-y-1/2",
|
|
324
|
+
)}
|
|
325
|
+
>
|
|
326
|
+
{icon}
|
|
327
|
+
</span>
|
|
265
328
|
)}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
e.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
329
|
+
<input
|
|
330
|
+
{...rest}
|
|
331
|
+
ref={ref}
|
|
332
|
+
id={fieldId}
|
|
333
|
+
size={nativeSize}
|
|
334
|
+
type="text"
|
|
335
|
+
value={value}
|
|
336
|
+
// Locked: focusable, `readOnly`, `aria-disabled` — see `disabledReason`.
|
|
337
|
+
disabled={locked ? undefined : disabled}
|
|
338
|
+
readOnly={locked || rest.readOnly}
|
|
339
|
+
aria-disabled={locked || rest["aria-disabled"]}
|
|
340
|
+
role="combobox"
|
|
341
|
+
aria-autocomplete="list"
|
|
342
|
+
aria-expanded={expanded}
|
|
343
|
+
// Required by the role, and set while closed too, as on the siblings.
|
|
344
|
+
aria-controls={listboxId}
|
|
345
|
+
aria-activedescendant={activeId}
|
|
346
|
+
aria-label={ariaLabel}
|
|
347
|
+
aria-labelledby={labelledBy}
|
|
348
|
+
aria-invalid={field.isInvalid || rest["aria-invalid"] || undefined}
|
|
349
|
+
aria-describedby={mergeDescribedBy(field.describedBy, locked && lock.reasonId)}
|
|
350
|
+
autoComplete="off"
|
|
351
|
+
onFocus={(e) => {
|
|
352
|
+
onFocus?.(e);
|
|
353
|
+
// A locked field takes focus to say why, and opens nothing.
|
|
354
|
+
if (locked) return;
|
|
355
|
+
// A seeded field (keksdose's `initialQuery`) is looked up as it is
|
|
356
|
+
// focused; an empty one asks nothing, because of `minChars`.
|
|
357
|
+
setOpen(true);
|
|
358
|
+
}}
|
|
359
|
+
onBlur={(e) => {
|
|
360
|
+
onBlur?.(e);
|
|
361
|
+
close();
|
|
362
|
+
}}
|
|
363
|
+
onChange={(e) => {
|
|
364
|
+
if (locked) return;
|
|
365
|
+
onChange(e.target.value);
|
|
366
|
+
setOpen(true);
|
|
367
|
+
setActive(-1);
|
|
368
|
+
}}
|
|
369
|
+
onKeyDown={(e) => {
|
|
370
|
+
// Locked: Enter is swallowed (no form submit, no caller's "search now");
|
|
371
|
+
// every other key is the caller's and the caret's. No list to drive.
|
|
372
|
+
if (locked) {
|
|
373
|
+
if (e.key === "Enter") e.preventDefault();
|
|
374
|
+
else onKeyDown?.(e);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
// An Escape that closes the OPEN list is the list's, and is consumed before
|
|
378
|
+
// the caller sees it. A caller whose Escape means "close the panel" (keksdose's
|
|
379
|
+
// address search) otherwise closed the whole panel when the user only meant
|
|
380
|
+
// to dismiss the suggestions — the opposite of what the docs promised.
|
|
381
|
+
if (e.key === "Escape" && expanded) {
|
|
382
|
+
e.preventDefault();
|
|
383
|
+
e.stopPropagation();
|
|
384
|
+
close();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
onKeyDown?.(e);
|
|
388
|
+
if (e.defaultPrevented) return;
|
|
389
|
+
// Disabled rows are passed over; at either end the highlight stays put.
|
|
390
|
+
if (e.key === "ArrowDown") {
|
|
391
|
+
e.preventDefault();
|
|
392
|
+
if (!open) setOpen(true);
|
|
393
|
+
else setActive((i) => stepEnabled(results, i, 1));
|
|
394
|
+
} else if (e.key === "ArrowUp") {
|
|
395
|
+
e.preventDefault();
|
|
396
|
+
if (!open) setOpen(true);
|
|
397
|
+
// From "nothing highlighted", Up goes to the bottom, as the APG has it.
|
|
398
|
+
else setActive((i) => stepEnabled(results, i < 0 ? results.length : i, -1));
|
|
399
|
+
} else if (e.key === "Enter") {
|
|
400
|
+
// No row highlighted: the text is the answer, and a form's own submit
|
|
401
|
+
// is left alone.
|
|
402
|
+
if (expanded && activeId) {
|
|
403
|
+
e.preventDefault();
|
|
404
|
+
take(results[active]);
|
|
405
|
+
}
|
|
406
|
+
} else if (e.key === "Tab") {
|
|
407
|
+
close();
|
|
408
|
+
}
|
|
409
|
+
}}
|
|
410
|
+
className={cn(
|
|
411
|
+
FIELD_BASE,
|
|
412
|
+
hasLabel && FIELD_FLOATING_PAD,
|
|
413
|
+
// Select's `SELECT_SM` box: a fixed 28px, so the caller's line height
|
|
414
|
+
// cannot grow it.
|
|
415
|
+
small && "h-7 py-0 pe-2 ps-2 text-xs",
|
|
416
|
+
icon ? (small ? "ps-7" : "ps-8") : undefined,
|
|
417
|
+
busy && (small ? "pe-7" : "pe-9"),
|
|
418
|
+
field.isInvalid && FIELD_INVALID,
|
|
419
|
+
inputClassName,
|
|
420
|
+
)}
|
|
421
|
+
/>
|
|
422
|
+
{busy && live && (
|
|
423
|
+
// Decorative: the live region below already says "Loading…".
|
|
424
|
+
<span
|
|
425
|
+
className={cn(
|
|
426
|
+
"pointer-events-none absolute top-1/2 flex -translate-y-1/2",
|
|
427
|
+
small ? "end-2" : "end-2.5",
|
|
428
|
+
)}
|
|
429
|
+
>
|
|
430
|
+
<Spinner label={null} className={small ? "h-3.5 w-3.5" : "h-4 w-4"} />
|
|
431
|
+
</span>
|
|
356
432
|
)}
|
|
357
|
-
|
|
358
|
-
<Spinner label={null} className={small ? "h-3.5 w-3.5" : "h-4 w-4"} />
|
|
359
|
-
</span>
|
|
433
|
+
</div>,
|
|
360
434
|
)}
|
|
361
|
-
</
|
|
435
|
+
</EndHintRow>
|
|
436
|
+
{/* With a "?" the label shares the top strip with it, after the field so the "?"
|
|
437
|
+
follows the control in the tab order (as on Input and Select). */}
|
|
438
|
+
{hasLabel && hintParts.labelHint !== undefined && (
|
|
439
|
+
<StaticLabelRow hint={hintParts.labelHint}>
|
|
440
|
+
<ComboboxFieldLabel id={labelId} htmlFor={fieldId} className={LABEL_IN_ROW}>
|
|
441
|
+
{label}
|
|
442
|
+
</ComboboxFieldLabel>
|
|
443
|
+
</StaticLabelRow>
|
|
444
|
+
)}
|
|
445
|
+
<FieldCaption parts={hintParts} />
|
|
362
446
|
{field.errorEl}
|
|
363
447
|
{/* The live region, kept mounted so a change IS an announcement: focus stays
|
|
364
448
|
in the field, and without it a reader typed into a geocoder and heard only
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { useId } from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
import { Checkbox } from "./checkbox";
|
|
5
|
+
import { hasMessage, mergeDescribedBy } from "./choice-parts";
|
|
6
|
+
import { useCommitReason } from "./write-lock";
|
|
7
|
+
import { RequiredStarOnLegend } from "./field-parts";
|
|
8
|
+
|
|
9
|
+
/** One box of a {@link CheckboxGroup}. */
|
|
10
|
+
export interface CheckboxGroupOption<T extends string = string> {
|
|
11
|
+
value: T;
|
|
12
|
+
/** The words beside the box — its accessible name. */
|
|
13
|
+
label: ReactNode;
|
|
14
|
+
/** A line under the label ("Imprint, Privacy Policy and Terms only"), attached to the
|
|
15
|
+
* box with `aria-describedby` — {@link Checkbox}'s `description`. */
|
|
16
|
+
hint?: ReactNode;
|
|
17
|
+
/** This box cannot be changed; the others can. */
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Whether the legend is drawn. `"sr-only"` keeps it as the group's name for a screen
|
|
22
|
+
* reader where the dialog's title already says it to the eye. */
|
|
23
|
+
export type CheckboxGroupLegendVisibility = "visible" | "sr-only";
|
|
24
|
+
|
|
25
|
+
export interface CheckboxGroupProps<T extends string = string> {
|
|
26
|
+
/** The question the boxes answer — the fieldset's `<legend>`, which names the group.
|
|
27
|
+
* Without one, pass `aria-label`. */
|
|
28
|
+
legend?: ReactNode;
|
|
29
|
+
/** Default `"visible"`. */
|
|
30
|
+
legendVisibility?: CheckboxGroupLegendVisibility;
|
|
31
|
+
"aria-label"?: string;
|
|
32
|
+
/** One box per option, in this order. */
|
|
33
|
+
options: CheckboxGroupOption<T>[];
|
|
34
|
+
/** The ticked values. Controlled. */
|
|
35
|
+
value: T[];
|
|
36
|
+
/**
|
|
37
|
+
* The new set on every tick. In the ORDER OF `options`, not the order the boxes were
|
|
38
|
+
* ticked, so a list saved from it does not change with the order someone clicked in
|
|
39
|
+
* (ChoiceCardGroup's rule). A value in `value` that no option carries — a grant for a
|
|
40
|
+
* locale the app no longer offers — is kept, after the known ones: a picker that
|
|
41
|
+
* dropped what it could not show would delete it on the next unrelated tick.
|
|
42
|
+
*/
|
|
43
|
+
onChange: (value: T[]) => void;
|
|
44
|
+
/**
|
|
45
|
+
* `"vertical"` (default): one box per line. `"horizontal"`: a wrapping row, for a few
|
|
46
|
+
* short labels. With `columns`, vertical becomes a grid of that many columns from the
|
|
47
|
+
* `sm` breakpoint up and stays one column on a phone — the reviewer dialog's shape.
|
|
48
|
+
*/
|
|
49
|
+
layout?: "vertical" | "horizontal";
|
|
50
|
+
/** Columns for a vertical group, from `sm` up (one on a phone). Default 1. */
|
|
51
|
+
columns?: 1 | 2 | 3 | 4;
|
|
52
|
+
/** Shared by every box — what a plain form submits the values under. */
|
|
53
|
+
name?: string;
|
|
54
|
+
/** Every box: the fieldset's own `disabled` (which disables them all natively), each
|
|
55
|
+
* box faded as a disabled Checkbox is, and the legend with them. */
|
|
56
|
+
disabled?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* At least one box must be ticked. Draws the required star after the legend, and puts
|
|
59
|
+
* the native `required` on every box while NONE is ticked — so a form refuses an
|
|
60
|
+
* empty set — and on none once one is (ChoiceCardGroup's rule: checkboxes cannot say
|
|
61
|
+
* "at least one" natively).
|
|
62
|
+
*/
|
|
63
|
+
required?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Standing advice for the group, under the boxes, attached to the fieldset with
|
|
66
|
+
* `aria-describedby` before the error — the field anatomy every kit field keeps.
|
|
67
|
+
*/
|
|
68
|
+
hint?: ReactNode;
|
|
69
|
+
/** What is wrong with the set, in the caller's words. Under the boxes, on the
|
|
70
|
+
* fieldset's `aria-describedby`, and paints every box invalid. */
|
|
71
|
+
error?: ReactNode;
|
|
72
|
+
/** Paints every box invalid without a message (the message lives elsewhere). */
|
|
73
|
+
invalid?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* The group SAVES on change — each tick is written straight away (a reviewer's
|
|
76
|
+
* per-locale grants). Under a locked {@link WriteLockProvider} every box is locked
|
|
77
|
+
* the focusable way with the lock's reason. See {@link Checkbox}'s `commit`.
|
|
78
|
+
*/
|
|
79
|
+
commit?: boolean;
|
|
80
|
+
/** Why no box can be changed — every box gets it, and it wins over `disabled` (the
|
|
81
|
+
* boxes stay focusable to say it). See {@link Checkbox}'s `disabledReason`. */
|
|
82
|
+
disabledReason?: ReactNode;
|
|
83
|
+
/** The fieldset's id. */
|
|
84
|
+
id?: string;
|
|
85
|
+
/** Classes for the fieldset — the outermost element, where layout belongs. */
|
|
86
|
+
className?: string;
|
|
87
|
+
/** Classes for the box list (the grid or the row). */
|
|
88
|
+
listClassName?: string;
|
|
89
|
+
/** Ids of something else on the page that describes the group; merged before the
|
|
90
|
+
* group's own hint and error. */
|
|
91
|
+
"aria-describedby"?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Literal class strings so Tailwind's scanner sees each one.
|
|
95
|
+
const COLUMNS: Record<NonNullable<CheckboxGroupProps["columns"]>, string> = {
|
|
96
|
+
1: "flex flex-col gap-2",
|
|
97
|
+
2: "grid grid-cols-1 gap-2 sm:grid-cols-2",
|
|
98
|
+
3: "grid grid-cols-1 gap-2 sm:grid-cols-3",
|
|
99
|
+
4: "grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-4",
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A set of kit {@link Checkbox}es answering one question, in a `<fieldset>`.
|
|
104
|
+
*
|
|
105
|
+
* Kurvenschmiede's reviewer dialog (features/admin/reviewer-dialog.tsx) builds this by
|
|
106
|
+
* hand — a fieldset, an `sr-only` legend, a grid of checkboxes, a `toggle(code, on)`
|
|
107
|
+
* that adds or filters a `string[]` — and keksdose's and kastlan's reviewer grants build
|
|
108
|
+
* the same picker for the same per-locale grant. The hand-built ones differ in the parts
|
|
109
|
+
* that are easy to get wrong: whether the legend names the group, whether a hint or an
|
|
110
|
+
* error is attached to anything, what order the saved list comes out in, and whether a
|
|
111
|
+
* value the picker cannot show survives a tick.
|
|
112
|
+
*
|
|
113
|
+
* Every box is a kit {@link Checkbox}, so the native checkbox semantics stay (each box
|
|
114
|
+
* submits under `name` in a plain form). The legend is the group's name; the hint and
|
|
115
|
+
* error describe the fieldset. For a choice of ONE, use radios ({@link ChoiceCardGroup},
|
|
116
|
+
* {@link ToggleGroup}).
|
|
117
|
+
*/
|
|
118
|
+
export function CheckboxGroup<T extends string = string>({
|
|
119
|
+
legend,
|
|
120
|
+
legendVisibility = "visible",
|
|
121
|
+
"aria-label": ariaLabel,
|
|
122
|
+
options,
|
|
123
|
+
value,
|
|
124
|
+
onChange,
|
|
125
|
+
layout = "vertical",
|
|
126
|
+
columns = 1,
|
|
127
|
+
name,
|
|
128
|
+
disabled,
|
|
129
|
+
required,
|
|
130
|
+
hint,
|
|
131
|
+
error,
|
|
132
|
+
invalid,
|
|
133
|
+
commit,
|
|
134
|
+
disabledReason,
|
|
135
|
+
id,
|
|
136
|
+
className,
|
|
137
|
+
listClassName,
|
|
138
|
+
"aria-describedby": ariaDescribedBy,
|
|
139
|
+
}: CheckboxGroupProps<T>) {
|
|
140
|
+
const generated = useId();
|
|
141
|
+
const hintId = `${generated}-hint`;
|
|
142
|
+
const errorId = `${generated}-error`;
|
|
143
|
+
const showHint = hasMessage(hint);
|
|
144
|
+
const showError = hasMessage(error);
|
|
145
|
+
const isInvalid = Boolean(invalid) || showError;
|
|
146
|
+
const ticked = new Set(value);
|
|
147
|
+
// Resolved here rather than per box: a reason has to keep the fieldset from being
|
|
148
|
+
// natively `disabled` too, or it would take every box out of the tab order — the
|
|
149
|
+
// thing a reason exists to prevent.
|
|
150
|
+
const reason = useCommitReason(commit, disabledReason);
|
|
151
|
+
const locked = hasMessage(reason);
|
|
152
|
+
|
|
153
|
+
const toggle = (v: T, on: boolean) => {
|
|
154
|
+
const next = new Set(value);
|
|
155
|
+
if (on) next.add(v);
|
|
156
|
+
else next.delete(v);
|
|
157
|
+
const known = new Set(options.map((o) => o.value));
|
|
158
|
+
onChange([
|
|
159
|
+
...options.map((o) => o.value).filter((x) => next.has(x)),
|
|
160
|
+
// Values no option carries, kept in their own order — see `onChange`.
|
|
161
|
+
...value.filter((x) => !known.has(x)),
|
|
162
|
+
]);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<fieldset
|
|
167
|
+
id={id}
|
|
168
|
+
aria-label={ariaLabel}
|
|
169
|
+
aria-describedby={mergeDescribedBy(ariaDescribedBy, showHint && hintId, showError && errorId)}
|
|
170
|
+
disabled={disabled && !locked}
|
|
171
|
+
// `relative` holds an `sr-only` legend (sr-only-containment.test); `min-w-0`
|
|
172
|
+
// undoes a fieldset's `min-width: min-content`, which would otherwise push a long
|
|
173
|
+
// label past a phone's edge instead of wrapping it.
|
|
174
|
+
className={cn("relative m-0 min-w-0 border-0 p-0", className)}
|
|
175
|
+
>
|
|
176
|
+
{legend !== undefined && (
|
|
177
|
+
<legend
|
|
178
|
+
className={
|
|
179
|
+
legendVisibility === "sr-only"
|
|
180
|
+
? "sr-only"
|
|
181
|
+
: cn("mb-2 p-0 text-sm font-medium text-[var(--text-primary)]", (disabled || locked) && "opacity-60")
|
|
182
|
+
}
|
|
183
|
+
>
|
|
184
|
+
{legend}
|
|
185
|
+
{required && (
|
|
186
|
+
<span aria-hidden className="ms-0.5 text-[var(--danger)]">
|
|
187
|
+
*
|
|
188
|
+
</span>
|
|
189
|
+
)}
|
|
190
|
+
</legend>
|
|
191
|
+
)}
|
|
192
|
+
<RequiredStarOnLegend.Provider value={true}>
|
|
193
|
+
<div
|
|
194
|
+
className={cn(
|
|
195
|
+
layout === "horizontal" ? "flex flex-wrap gap-x-4 gap-y-2" : COLUMNS[columns],
|
|
196
|
+
listClassName,
|
|
197
|
+
)}
|
|
198
|
+
>
|
|
199
|
+
{options.map((o) => (
|
|
200
|
+
<Checkbox
|
|
201
|
+
key={o.value}
|
|
202
|
+
name={name}
|
|
203
|
+
value={o.value}
|
|
204
|
+
label={o.label}
|
|
205
|
+
description={o.hint}
|
|
206
|
+
checked={ticked.has(o.value)}
|
|
207
|
+
onCheckedChange={(on) => toggle(o.value, on)}
|
|
208
|
+
disabled={disabled || o.disabled}
|
|
209
|
+
invalid={isInvalid || undefined}
|
|
210
|
+
required={required && value.length === 0 ? true : undefined}
|
|
211
|
+
disabledReason={reason}
|
|
212
|
+
/>
|
|
213
|
+
))}
|
|
214
|
+
</div>
|
|
215
|
+
</RequiredStarOnLegend.Provider>
|
|
216
|
+
{showHint && (
|
|
217
|
+
// A `div`, not a `p`: a hint may be a node with a link in it.
|
|
218
|
+
<div id={hintId} className="mt-1.5 text-[11px] leading-tight text-[var(--text-muted)]">
|
|
219
|
+
{hint}
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
{showError && (
|
|
223
|
+
<p id={errorId} className="mt-1 text-[11px] leading-tight text-[var(--danger)]">
|
|
224
|
+
{error}
|
|
225
|
+
</p>
|
|
226
|
+
)}
|
|
227
|
+
</fieldset>
|
|
228
|
+
);
|
|
229
|
+
}
|