@eifi1/ui-kit 0.20.1 → 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/hover-menu.d.ts +3 -0
- package/dist/components/hover-menu.js +2 -2
- package/dist/components/hover-menu.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/topbar-action-menu.d.ts +13 -1
- package/dist/shell/topbar-action-menu.js +31 -28
- package/dist/shell/topbar-action-menu.js.map +1 -1
- 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/hover-menu.tsx +7 -4
- 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/shell/topbar-action-menu.tsx +53 -29
- package/src/tour/tour.tsx +66 -28
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type ComboOption,
|
|
23
23
|
} from "./combobox-core";
|
|
24
24
|
import { DEFAULT_COMBOBOX_LABELS, useKitLabels } from "../i18n/kit-labels";
|
|
25
|
+
import { EndHintRow, FieldCaption, LABEL_IN_ROW, StaticLabelRow, useFieldHint } from "./field-parts";
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* `value`/`onChange` are the TEXT's, and `onSelect` is "a suggestion was taken" —
|
|
@@ -104,6 +105,14 @@ export interface AutocompleteProps<V extends string | number = string>
|
|
|
104
105
|
* its `aria-describedby`, implies `invalid`. Not a failed lookup — that is
|
|
105
106
|
* `loadErrorLabel`, shown in the list. */
|
|
106
107
|
error?: ReactNode;
|
|
108
|
+
/**
|
|
109
|
+
* Standing advice — {@link Combobox}'s `hint`, so this field reads like every other
|
|
110
|
+
* (keksdose K4): plain TEXT is a caption under the field on the input's
|
|
111
|
+
* `aria-describedby`, before any error; a {@link FieldHint} "?" rides the label line,
|
|
112
|
+
* or with no label sits at the field's end edge, outside the box. Not the list's
|
|
113
|
+
* `status` line, which talks about the lookup, not the field.
|
|
114
|
+
*/
|
|
115
|
+
hint?: ReactNode;
|
|
107
116
|
/**
|
|
108
117
|
* The caller's own line in the list, replacing the automatic one (loading / failed
|
|
109
118
|
* / no results). For what only the caller knows: "offline", "the address service
|
|
@@ -135,6 +144,7 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
135
144
|
icon,
|
|
136
145
|
invalid,
|
|
137
146
|
error,
|
|
147
|
+
hint,
|
|
138
148
|
status,
|
|
139
149
|
emptyLabel,
|
|
140
150
|
loadErrorLabel,
|
|
@@ -166,7 +176,8 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
166
176
|
noResults: emptyLabel,
|
|
167
177
|
loadError: loadErrorLabel,
|
|
168
178
|
});
|
|
169
|
-
const
|
|
179
|
+
const hintParts = useFieldHint(hint, ariaDescribedBy);
|
|
180
|
+
const field = useComboboxFieldError(error, invalid, hintParts.describedBy);
|
|
170
181
|
|
|
171
182
|
/** The user's intent: the field is focused and the list has not been dismissed.
|
|
172
183
|
* Whether anything SHOWS is decided below, from what there is to show. */
|
|
@@ -241,124 +252,136 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
241
252
|
<div className={cn("relative", className)}>
|
|
242
253
|
{/* A real <label for> (lenkbank), and still `aria-labelledby`'s target, so the
|
|
243
254
|
name — and the listbox's, which borrows it — is exactly what it was. */}
|
|
244
|
-
{hasLabel && (
|
|
255
|
+
{hasLabel && hintParts.labelHint === undefined && (
|
|
245
256
|
<ComboboxFieldLabel id={labelId} htmlFor={fieldId}>
|
|
246
257
|
{label}
|
|
247
258
|
</ComboboxFieldLabel>
|
|
248
259
|
)}
|
|
249
|
-
<
|
|
250
|
-
{
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
onFocus
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
onBlur
|
|
297
|
-
|
|
298
|
-
}}
|
|
299
|
-
onChange={(e) => {
|
|
300
|
-
onChange(e.target.value);
|
|
301
|
-
setOpen(true);
|
|
302
|
-
setActive(-1);
|
|
303
|
-
}}
|
|
304
|
-
onKeyDown={(e) => {
|
|
305
|
-
// An Escape that closes the OPEN list is the list's, and is consumed before
|
|
306
|
-
// the caller sees it. A caller whose Escape means "close the panel" (keksdose's
|
|
307
|
-
// address search) otherwise closed the whole panel when the user only meant
|
|
308
|
-
// to dismiss the suggestions — the opposite of what the docs promised.
|
|
309
|
-
if (e.key === "Escape" && expanded) {
|
|
310
|
-
e.preventDefault();
|
|
311
|
-
e.stopPropagation();
|
|
260
|
+
<EndHintRow hint={hasLabel ? undefined : hintParts.labelHint}>
|
|
261
|
+
<div ref={fieldRef} className="relative">
|
|
262
|
+
{icon && (
|
|
263
|
+
<span
|
|
264
|
+
aria-hidden
|
|
265
|
+
className={cn(
|
|
266
|
+
"pointer-events-none absolute flex text-[var(--text-muted)]",
|
|
267
|
+
small ? "start-2 [&>svg]:size-3.5" : "start-2.5 [&>svg]:size-4",
|
|
268
|
+
// Labelled, the icon sits on the VALUE's line — the one line box below
|
|
269
|
+
// FIELD_FLOATING_PAD's `pt-4` and the 1px border — not on the middle of
|
|
270
|
+
// the box. Centred on the box it rose into the top strip and sat on the
|
|
271
|
+
// label's first letters, which float at the same start inset. On the
|
|
272
|
+
// value line it is beside what it describes, and the label keeps the
|
|
273
|
+
// start edge every other label in the form's column shares.
|
|
274
|
+
hasLabel
|
|
275
|
+
? "top-[calc(1rem+1px)] h-5 items-center"
|
|
276
|
+
: "top-1/2 -translate-y-1/2",
|
|
277
|
+
)}
|
|
278
|
+
>
|
|
279
|
+
{icon}
|
|
280
|
+
</span>
|
|
281
|
+
)}
|
|
282
|
+
<input
|
|
283
|
+
{...rest}
|
|
284
|
+
ref={ref}
|
|
285
|
+
id={fieldId}
|
|
286
|
+
size={nativeSize}
|
|
287
|
+
type="text"
|
|
288
|
+
value={value}
|
|
289
|
+
disabled={disabled}
|
|
290
|
+
role="combobox"
|
|
291
|
+
aria-autocomplete="list"
|
|
292
|
+
aria-expanded={expanded}
|
|
293
|
+
// Required by the role, and set while closed too, as on the siblings.
|
|
294
|
+
aria-controls={listboxId}
|
|
295
|
+
aria-activedescendant={activeId}
|
|
296
|
+
aria-label={ariaLabel}
|
|
297
|
+
aria-labelledby={labelledBy}
|
|
298
|
+
aria-invalid={field.isInvalid || rest["aria-invalid"] || undefined}
|
|
299
|
+
aria-describedby={field.describedBy}
|
|
300
|
+
autoComplete="off"
|
|
301
|
+
onFocus={(e) => {
|
|
302
|
+
onFocus?.(e);
|
|
303
|
+
// A seeded field (keksdose's `initialQuery`) is looked up as it is
|
|
304
|
+
// focused; an empty one asks nothing, because of `minChars`.
|
|
305
|
+
setOpen(true);
|
|
306
|
+
}}
|
|
307
|
+
onBlur={(e) => {
|
|
308
|
+
onBlur?.(e);
|
|
312
309
|
close();
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (
|
|
325
|
-
// From "nothing highlighted", Up goes to the bottom, as the APG has it.
|
|
326
|
-
else setActive((i) => stepEnabled(results, i < 0 ? results.length : i, -1));
|
|
327
|
-
} else if (e.key === "Enter") {
|
|
328
|
-
// No row highlighted: the text is the answer, and a form's own submit
|
|
329
|
-
// is left alone.
|
|
330
|
-
if (expanded && activeId) {
|
|
310
|
+
}}
|
|
311
|
+
onChange={(e) => {
|
|
312
|
+
onChange(e.target.value);
|
|
313
|
+
setOpen(true);
|
|
314
|
+
setActive(-1);
|
|
315
|
+
}}
|
|
316
|
+
onKeyDown={(e) => {
|
|
317
|
+
// An Escape that closes the OPEN list is the list's, and is consumed before
|
|
318
|
+
// the caller sees it. A caller whose Escape means "close the panel" (keksdose's
|
|
319
|
+
// address search) otherwise closed the whole panel when the user only meant
|
|
320
|
+
// to dismiss the suggestions — the opposite of what the docs promised.
|
|
321
|
+
if (e.key === "Escape" && expanded) {
|
|
331
322
|
e.preventDefault();
|
|
332
|
-
|
|
323
|
+
e.stopPropagation();
|
|
324
|
+
close();
|
|
325
|
+
return;
|
|
333
326
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
327
|
+
onKeyDown?.(e);
|
|
328
|
+
if (e.defaultPrevented) return;
|
|
329
|
+
// Disabled rows are passed over; at either end the highlight stays put.
|
|
330
|
+
if (e.key === "ArrowDown") {
|
|
331
|
+
e.preventDefault();
|
|
332
|
+
if (!open) setOpen(true);
|
|
333
|
+
else setActive((i) => stepEnabled(results, i, 1));
|
|
334
|
+
} else if (e.key === "ArrowUp") {
|
|
335
|
+
e.preventDefault();
|
|
336
|
+
if (!open) setOpen(true);
|
|
337
|
+
// From "nothing highlighted", Up goes to the bottom, as the APG has it.
|
|
338
|
+
else setActive((i) => stepEnabled(results, i < 0 ? results.length : i, -1));
|
|
339
|
+
} else if (e.key === "Enter") {
|
|
340
|
+
// No row highlighted: the text is the answer, and a form's own submit
|
|
341
|
+
// is left alone.
|
|
342
|
+
if (expanded && activeId) {
|
|
343
|
+
e.preventDefault();
|
|
344
|
+
take(results[active]);
|
|
345
|
+
}
|
|
346
|
+
} else if (e.key === "Tab") {
|
|
347
|
+
close();
|
|
348
|
+
}
|
|
349
|
+
}}
|
|
353
350
|
className={cn(
|
|
354
|
-
|
|
355
|
-
|
|
351
|
+
FIELD_BASE,
|
|
352
|
+
hasLabel && FIELD_FLOATING_PAD,
|
|
353
|
+
// Select's `SELECT_SM` box: a fixed 28px, so the caller's line height
|
|
354
|
+
// cannot grow it.
|
|
355
|
+
small && "h-7 py-0 pe-2 ps-2 text-xs",
|
|
356
|
+
icon ? (small ? "ps-7" : "ps-8") : undefined,
|
|
357
|
+
busy && (small ? "pe-7" : "pe-9"),
|
|
358
|
+
field.isInvalid && FIELD_INVALID,
|
|
359
|
+
inputClassName,
|
|
356
360
|
)}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
361
|
+
/>
|
|
362
|
+
{busy && live && (
|
|
363
|
+
// Decorative: the live region below already says "Loading…".
|
|
364
|
+
<span
|
|
365
|
+
className={cn(
|
|
366
|
+
"pointer-events-none absolute top-1/2 flex -translate-y-1/2",
|
|
367
|
+
small ? "end-2" : "end-2.5",
|
|
368
|
+
)}
|
|
369
|
+
>
|
|
370
|
+
<Spinner label={null} className={small ? "h-3.5 w-3.5" : "h-4 w-4"} />
|
|
371
|
+
</span>
|
|
372
|
+
)}
|
|
373
|
+
</div>
|
|
374
|
+
</EndHintRow>
|
|
375
|
+
{/* With a "?" the label shares the top strip with it, after the field so the "?"
|
|
376
|
+
follows the control in the tab order (as on Input and Select). */}
|
|
377
|
+
{hasLabel && hintParts.labelHint !== undefined && (
|
|
378
|
+
<StaticLabelRow hint={hintParts.labelHint}>
|
|
379
|
+
<ComboboxFieldLabel id={labelId} htmlFor={fieldId} className={LABEL_IN_ROW}>
|
|
380
|
+
{label}
|
|
381
|
+
</ComboboxFieldLabel>
|
|
382
|
+
</StaticLabelRow>
|
|
383
|
+
)}
|
|
384
|
+
<FieldCaption parts={hintParts} />
|
|
362
385
|
{field.errorEl}
|
|
363
386
|
{/* The live region, kept mounted so a change IS an announcement: focus stays
|
|
364
387
|
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
|
+
}
|