@eifi1/ui-kit 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chart.d.ts +10 -8
- package/dist/components/account-chips.d.ts +10 -3
- package/dist/components/account-chips.js +5 -2
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/account-settings.d.ts +33 -2
- package/dist/components/account-settings.js +43 -21
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +27 -8
- package/dist/components/amount-input.js +15 -28
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +8 -0
- package/dist/components/autocomplete.js +9 -4
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +10 -8
- package/dist/components/calculator.d.ts +10 -8
- package/dist/components/checkbox-group.d.ts +104 -0
- package/dist/components/checkbox-group.js +111 -0
- package/dist/components/checkbox-group.js.map +1 -0
- package/dist/components/checkbox.d.ts +19 -0
- package/dist/components/checkbox.js +30 -7
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/chip.d.ts +18 -0
- package/dist/components/chip.js +47 -10
- package/dist/components/chip.js.map +1 -1
- package/dist/components/combobox.d.ts +13 -2
- package/dist/components/combobox.js +17 -8
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +66 -161
- package/dist/components/copy-button.d.ts +10 -8
- package/dist/components/country-select.d.ts +124 -0
- package/dist/components/country-select.js +237 -0
- package/dist/components/country-select.js.map +1 -0
- package/dist/components/currency-select.d.ts +17 -1
- package/dist/components/currency-select.js +56 -23
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +66 -166
- package/dist/components/danger-confirm.js +133 -49
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +1 -2
- package/dist/components/data-table-filter-popover.d.ts +1 -2
- package/dist/components/data-table-filters.d.ts +1 -2
- package/dist/components/data-table-labels.d.ts +4 -97
- package/dist/components/data-table-labels.js +9 -1
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-pagination.d.ts +3 -1
- package/dist/components/data-table.d.ts +1 -2
- package/dist/components/data-table.js +98 -3
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +25 -5
- package/dist/components/date-picker.js +110 -59
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +8 -1
- package/dist/components/entity-combobox.js +9 -4
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +10 -8
- package/dist/components/field-anatomy.d.ts +95 -0
- package/dist/components/field-anatomy.js +84 -0
- package/dist/components/field-anatomy.js.map +1 -0
- package/dist/components/field-parts.d.ts +114 -0
- package/dist/components/field-parts.js +72 -0
- package/dist/components/field-parts.js.map +1 -0
- package/dist/components/file-button.d.ts +10 -8
- package/dist/components/file-button.js +13 -3
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +32 -9
- package/dist/components/file-dropzone.js +57 -34
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/form-actions.d.ts +10 -8
- package/dist/components/form-actions.js +61 -3
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/iban-input.d.ts +66 -0
- package/dist/components/iban-input.js +125 -0
- package/dist/components/iban-input.js.map +1 -0
- package/dist/components/icon-picker.d.ts +12 -2
- package/dist/components/icon-picker.js +9 -2
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/inline-edit-field.d.ts +167 -0
- package/dist/components/inline-edit-field.js +293 -0
- package/dist/components/inline-edit-field.js.map +1 -0
- package/dist/components/language-select.d.ts +127 -0
- package/dist/components/language-select.js +18 -0
- package/dist/components/language-select.js.map +1 -0
- package/dist/components/line-items.d.ts +25 -3
- package/dist/components/line-items.js +55 -46
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.d.ts +25 -0
- package/dist/components/list.js +16 -5
- package/dist/components/list.js.map +1 -1
- package/dist/components/money-field.d.ts +20 -8
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.d.ts +52 -4
- package/dist/components/month-picker.js +290 -86
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-select.d.ts +8 -1
- package/dist/components/multi-select.js +9 -4
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +25 -13
- package/dist/components/number-field.js +6 -20
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +70 -13
- package/dist/components/number-input.js +90 -38
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +10 -8
- package/dist/components/one-time-code-input.d.ts +100 -0
- package/dist/components/one-time-code-input.js +137 -0
- package/dist/components/one-time-code-input.js.map +1 -0
- package/dist/components/phone-input.d.ts +66 -0
- package/dist/components/phone-input.js +171 -0
- package/dist/components/phone-input.js.map +1 -0
- package/dist/components/series-chart.d.ts +10 -8
- package/dist/components/settings-fields.d.ts +10 -8
- package/dist/components/share-card.d.ts +8 -6
- package/dist/components/sign-chip.d.ts +66 -0
- package/dist/components/sign-chip.js +39 -0
- package/dist/components/sign-chip.js.map +1 -0
- package/dist/components/swatch-picker.d.ts +27 -2
- package/dist/components/swatch-picker.js +73 -31
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/switch.d.ts +19 -0
- package/dist/components/switch.js +24 -6
- package/dist/components/switch.js.map +1 -1
- package/dist/components/text-link.d.ts +10 -8
- package/dist/components/tile-radio.d.ts +68 -13
- package/dist/components/tile-radio.js +3 -3
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +15 -9
- package/dist/components/time-input.js.map +1 -1
- package/dist/components/toggle-group.d.ts +47 -1
- package/dist/components/toggle-group.js +110 -69
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/ui.d.ts +10 -8
- package/dist/components/ui.js +280 -78
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -2
- package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
- package/dist/data-table.d.ts +1 -2
- package/dist/data-table.js +2 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +1 -1
- package/dist/feedback/feedback-attachment.js +210 -3
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +1 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +1 -1
- package/dist/feedback/feedback-thread.d.ts +37 -3
- package/dist/feedback/feedback-thread.js +38 -9
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-NhUm7Zga.d.ts} +70 -5
- package/dist/feedback.d.ts +2 -2
- package/dist/hooks/use-file-drop.d.ts +10 -8
- package/dist/i18n/defaults.d.ts +10 -8
- package/dist/i18n/defaults.js +17 -2
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +10 -8
- package/dist/i18n/german.js +50 -5
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +10 -8
- package/dist/i18n/kit-labels.js +8 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +10 -8
- package/dist/i18n/locales/de-CH.d.ts +10 -8
- package/dist/i18n/locales/en.d.ts +10 -8
- package/dist/i18n/locales/en.js +9 -0
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +10 -8
- package/dist/i18n/locales/es.js +48 -5
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +10 -8
- package/dist/i18n/locales/fr.js +48 -5
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +10 -8
- package/dist/i18n/locales/hu.js +49 -5
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +10 -8
- package/dist/i18n/locales/it.js +49 -5
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +10 -8
- package/dist/i18n/locales/zh.js +48 -5
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +10 -8
- package/dist/i18n/review.js +8 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +17 -10
- package/dist/index.js +57 -2
- package/dist/index.js.map +1 -1
- package/dist/{kit-labels-D1J2_jyD.d.ts → kit-labels-R8mIAc9N.d.ts} +811 -8
- package/dist/lib/countries.d.ts +43 -0
- package/dist/lib/countries.js +39 -0
- package/dist/lib/countries.js.map +1 -0
- package/dist/lib/decimal-marks.d.ts +46 -0
- package/dist/lib/decimal-marks.js +40 -0
- package/dist/lib/decimal-marks.js.map +1 -0
- package/dist/lib/iban.d.ts +98 -0
- package/dist/lib/iban.js +154 -0
- package/dist/lib/iban.js.map +1 -0
- package/dist/lib/isin.d.ts +30 -0
- package/dist/lib/isin.js +36 -0
- package/dist/lib/isin.js.map +1 -0
- package/dist/lib/phone.d.ts +69 -0
- package/dist/lib/phone.js +121 -0
- package/dist/lib/phone.js.map +1 -0
- package/dist/rhf/fields.d.ts +148 -11
- package/dist/rhf/fields.js +332 -3
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +10 -8
- package/dist/rhf.d.ts +13 -9
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +10 -8
- package/dist/shell/top-bar-brand.d.ts +10 -8
- package/dist/shell.d.ts +10 -8
- package/dist/tour/tour.js +20 -24
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +10 -8
- package/dist/wizard.d.ts +10 -8
- package/package.json +1 -1
- package/src/components/account-chips.tsx +17 -3
- package/src/components/account-settings.tsx +92 -26
- package/src/components/amount-input.tsx +49 -56
- package/src/components/autocomplete.tsx +133 -110
- package/src/components/checkbox-group.tsx +229 -0
- package/src/components/checkbox.tsx +72 -8
- package/src/components/chip.tsx +80 -10
- package/src/components/combobox.tsx +292 -253
- package/src/components/country-select.tsx +443 -0
- package/src/components/currency-select.tsx +109 -33
- package/src/components/danger-confirm.tsx +345 -77
- package/src/components/data-table-labels.ts +16 -1
- package/src/components/data-table.tsx +175 -6
- package/src/components/date-picker.tsx +173 -62
- package/src/components/entity-combobox.tsx +107 -87
- package/src/components/field-anatomy.tsx +190 -0
- package/src/components/field-parts.tsx +184 -0
- package/src/components/file-button.tsx +36 -3
- package/src/components/file-dropzone.tsx +106 -38
- package/src/components/form-actions.tsx +115 -2
- package/src/components/iban-input.tsx +231 -0
- package/src/components/icon-picker.tsx +26 -4
- package/src/components/inline-edit-field.tsx +533 -0
- package/src/components/language-select.tsx +78 -0
- package/src/components/line-items.tsx +35 -2
- package/src/components/list.tsx +55 -6
- package/src/components/money-field.tsx +10 -0
- package/src/components/month-picker.tsx +399 -63
- package/src/components/multi-select.tsx +80 -58
- package/src/components/number-field.tsx +28 -46
- package/src/components/number-input.tsx +210 -43
- package/src/components/one-time-code-input.tsx +288 -0
- package/src/components/phone-input.tsx +299 -0
- package/src/components/sign-chip.tsx +103 -0
- package/src/components/swatch-picker.tsx +118 -30
- package/src/components/switch.tsx +58 -7
- package/src/components/tile-radio.tsx +71 -16
- package/src/components/time-input.tsx +8 -1
- package/src/components/toggle-group.tsx +206 -73
- package/src/components/ui.tsx +472 -105
- package/src/data-table.ts +2 -1
- package/src/feedback/feedback-attachment.tsx +313 -9
- package/src/feedback/feedback-dialog.tsx +2 -0
- package/src/feedback/feedback-thread.tsx +66 -5
- package/src/i18n/defaults.ts +16 -1
- package/src/i18n/german.ts +47 -0
- package/src/i18n/kit-labels.tsx +78 -2
- package/src/i18n/locales/en.ts +10 -0
- package/src/i18n/locales/es.ts +46 -0
- package/src/i18n/locales/fr.ts +45 -0
- package/src/i18n/locales/hu.ts +46 -0
- package/src/i18n/locales/it.ts +47 -0
- package/src/i18n/locales/zh.ts +44 -0
- package/src/i18n/review.ts +7 -0
- package/src/index.ts +53 -3
- package/src/lib/countries.ts +91 -0
- package/src/lib/decimal-marks.ts +99 -0
- package/src/lib/iban.ts +171 -0
- package/src/lib/isin.ts +62 -0
- package/src/lib/phone.ts +211 -0
- package/src/rhf/fields.tsx +606 -9
- package/src/rhf.ts +2 -1
- package/src/tour/tour.tsx +66 -28
|
@@ -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
|
// One look for both combobox flavors below — the suggestion list and its rows
|
|
27
28
|
// must stay pixel-identical between the free-text and the id-keyed variant (and
|
|
@@ -149,6 +150,15 @@ export interface ComboboxProps
|
|
|
149
150
|
* field, on the `<input>`'s `aria-describedby`, and implies `invalid` (lenkbank's
|
|
150
151
|
* scope fields, which moved here off `<Input list>` + `<datalist>`). */
|
|
151
152
|
error?: ReactNode;
|
|
153
|
+
/**
|
|
154
|
+
* Standing advice for the field — {@link Input}'s `hint`, so the combobox family
|
|
155
|
+
* reads like every other field (keksdose K4: the family had `error` and no `hint`, so
|
|
156
|
+
* the app's pickers carried their captions outside the component, attached to
|
|
157
|
+
* nothing). Plain TEXT is a caption under the field on the input's
|
|
158
|
+
* `aria-describedby`, before any error; a {@link FieldHint} "?" rides the label line,
|
|
159
|
+
* or with no label sits at the field's end edge, outside the box.
|
|
160
|
+
*/
|
|
161
|
+
hint?: ReactNode;
|
|
152
162
|
/** As `<input disabled>`: no focus, no list, the field's settled look. The chevron
|
|
153
163
|
* stops toggling with it — it is a mouse target the input's own `disabled` does
|
|
154
164
|
* not reach. */
|
|
@@ -232,6 +242,7 @@ export function Combobox({
|
|
|
232
242
|
createLabel,
|
|
233
243
|
invalid,
|
|
234
244
|
error,
|
|
245
|
+
hint,
|
|
235
246
|
disabled,
|
|
236
247
|
optionAdornment,
|
|
237
248
|
autoFocus,
|
|
@@ -246,10 +257,11 @@ export function Combobox({
|
|
|
246
257
|
"aria-required": ariaRequired,
|
|
247
258
|
...rest
|
|
248
259
|
}: ComboboxProps) {
|
|
260
|
+
const hintParts = useFieldHint(hint, ariaDescribedBy);
|
|
249
261
|
const field = useComboboxFieldError(
|
|
250
262
|
error,
|
|
251
263
|
invalid || ariaInvalid === true || ariaInvalid === "true",
|
|
252
|
-
|
|
264
|
+
hintParts.describedBy,
|
|
253
265
|
);
|
|
254
266
|
const generated = useId();
|
|
255
267
|
const fieldId = id ?? generated;
|
|
@@ -383,134 +395,147 @@ export function Combobox({
|
|
|
383
395
|
<div {...rest} ref={wrapperRef} className={cn("relative", className)}>
|
|
384
396
|
{/* A real <label for> since lenkbank's tests met a field `getByLabelText` could
|
|
385
397
|
not find: it used to be a <span>, with the text copied onto `aria-label`. The
|
|
386
|
-
name a reader hears is the same words either way.
|
|
387
|
-
|
|
398
|
+
name a reader hears is the same words either way. With a "?" it moves into a
|
|
399
|
+
row after the field, below. */}
|
|
400
|
+
{label !== undefined && hintParts.labelHint === undefined && (
|
|
388
401
|
<ComboboxFieldLabel htmlFor={fieldId} className={disabled ? "opacity-50" : undefined}>
|
|
389
402
|
{label}
|
|
390
403
|
</ComboboxFieldLabel>
|
|
391
404
|
)}
|
|
392
|
-
{
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
<
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
setTyping(false);
|
|
433
|
-
// The sheet carries its own input, so the field behind it must not also
|
|
434
|
-
// pull up the keyboard and scroll the page under the dialog.
|
|
435
|
-
if (isPhone) sheetInputRef.current?.focus();
|
|
436
|
-
}}
|
|
437
|
-
// A CLICK as well as focus — the other half of dev#549. Picking a suggestion
|
|
438
|
-
// closes the list without moving focus (the rows suppress `mousedown` on
|
|
439
|
-
// purpose, so the input never blurred), which means clicking the field again
|
|
440
|
-
// fires no `focus` event at all and the list stayed shut. "Does not open
|
|
441
|
-
// anything" was literally true.
|
|
442
|
-
onClick={() => {
|
|
443
|
-
setOpen(true);
|
|
444
|
-
setTyping(false);
|
|
445
|
-
}}
|
|
446
|
-
// `inputMode="none"` rather than readOnly: the field must not look
|
|
447
|
-
// uneditable (FIELD_BASE greys a read-only field since dev#468) and must
|
|
448
|
-
// still take focus — it just has no keyboard of its own, the same trick
|
|
449
|
-
// the amount field uses for the numpad.
|
|
450
|
-
inputMode={isPhone ? "none" : undefined}
|
|
451
|
-
onChange={(e) => {
|
|
452
|
-
onChange(e.target.value);
|
|
453
|
-
setOpen(true);
|
|
454
|
-
setActive(-1);
|
|
455
|
-
setTyping(true);
|
|
456
|
-
}}
|
|
457
|
-
onKeyDown={(e) => {
|
|
458
|
-
if (e.key === "ArrowDown") {
|
|
459
|
-
e.preventDefault();
|
|
405
|
+
<EndHintRow hint={label === undefined ? hintParts.labelHint : undefined}>
|
|
406
|
+
{/* The chevron centers against this inner wrapper, which hugs the input.
|
|
407
|
+
The outer div can be taller than the input (as a grid item it
|
|
408
|
+
stretches to the row height, e.g. next to the editor's category cell
|
|
409
|
+
with its split button), which used to drag a top-1/2 chevron down to
|
|
410
|
+
the input's bottom edge (feedback #248). */}
|
|
411
|
+
{/* Dimmed as a whole when disabled, the way EntityCombobox dims its trigger.
|
|
412
|
+
FIELD_BASE's grey alone left a disabled picker looking like a filled-in
|
|
413
|
+
one beside the pickers that do dim (the label is not the input's `peer`,
|
|
414
|
+
so it is dimmed by hand above). */}
|
|
415
|
+
<div ref={fieldRef} className={cn("relative", disabled && "cursor-not-allowed opacity-50")}>
|
|
416
|
+
<input
|
|
417
|
+
id={fieldId}
|
|
418
|
+
value={value}
|
|
419
|
+
placeholder={placeholder}
|
|
420
|
+
// The caller's name, over the <label for> above — which is what names the
|
|
421
|
+
// field otherwise (dev#477: a field with neither announces only its text).
|
|
422
|
+
aria-label={ariaLabel}
|
|
423
|
+
role="combobox"
|
|
424
|
+
aria-expanded={open}
|
|
425
|
+
// The list this field is the mouth of. Required by the role, and the half
|
|
426
|
+
// that was missing: the field said it was expanded and never said what it
|
|
427
|
+
// had expanded, so a reader had no way from the box to the options
|
|
428
|
+
// (ESLint's `role-has-required-aria-props`, the audit's §a11y).
|
|
429
|
+
aria-controls={listboxId}
|
|
430
|
+
aria-activedescendant={activeId}
|
|
431
|
+
aria-autocomplete="list"
|
|
432
|
+
aria-invalid={field.isInvalid || undefined}
|
|
433
|
+
aria-describedby={field.describedBy}
|
|
434
|
+
aria-required={ariaRequired}
|
|
435
|
+
autoComplete="off"
|
|
436
|
+
disabled={disabled}
|
|
437
|
+
// eslint-disable-next-line jsx-a11y/no-autofocus -- a documented prop the caller opts into (off by default); the field never takes focus on its own.
|
|
438
|
+
autoFocus={autoFocus}
|
|
439
|
+
onBlur={onBlur}
|
|
440
|
+
onMouseDown={primaryOnly.onMouseDown}
|
|
441
|
+
onFocus={() => {
|
|
442
|
+
// A back/forward mouse button focuses this field on its way to
|
|
443
|
+
// navigating; it is not a request to open anything (live #309 rework).
|
|
444
|
+
if (primaryOnly.fromAuxButton()) return;
|
|
460
445
|
setOpen(true);
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
}
|
|
466
|
-
|
|
446
|
+
setTyping(false);
|
|
447
|
+
// The sheet carries its own input, so the field behind it must not also
|
|
448
|
+
// pull up the keyboard and scroll the page under the dialog.
|
|
449
|
+
if (isPhone) sheetInputRef.current?.focus();
|
|
450
|
+
}}
|
|
451
|
+
// A CLICK as well as focus — the other half of dev#549. Picking a suggestion
|
|
452
|
+
// closes the list without moving focus (the rows suppress `mousedown` on
|
|
453
|
+
// purpose, so the input never blurred), which means clicking the field again
|
|
454
|
+
// fires no `focus` event at all and the list stayed shut. "Does not open
|
|
455
|
+
// anything" was literally true.
|
|
456
|
+
onClick={() => {
|
|
457
|
+
setOpen(true);
|
|
458
|
+
setTyping(false);
|
|
459
|
+
}}
|
|
460
|
+
// `inputMode="none"` rather than readOnly: the field must not look
|
|
461
|
+
// uneditable (FIELD_BASE greys a read-only field since dev#468) and must
|
|
462
|
+
// still take focus — it just has no keyboard of its own, the same trick
|
|
463
|
+
// the amount field uses for the numpad.
|
|
464
|
+
inputMode={isPhone ? "none" : undefined}
|
|
465
|
+
onChange={(e) => {
|
|
466
|
+
onChange(e.target.value);
|
|
467
|
+
setOpen(true);
|
|
468
|
+
setActive(-1);
|
|
469
|
+
setTyping(true);
|
|
470
|
+
}}
|
|
471
|
+
onKeyDown={(e) => {
|
|
472
|
+
if (e.key === "ArrowDown") {
|
|
473
|
+
e.preventDefault();
|
|
474
|
+
setOpen(true);
|
|
475
|
+
setActive((i) => Math.min(i + 1, matches.length - 1));
|
|
476
|
+
} else if (e.key === "ArrowUp") {
|
|
467
477
|
e.preventDefault();
|
|
468
|
-
|
|
469
|
-
} else {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
478
|
+
setActive((i) => Math.max(i - 1, 0));
|
|
479
|
+
} else if (e.key === "Enter") {
|
|
480
|
+
if (open && active >= 0 && active < matches.length) {
|
|
481
|
+
e.preventDefault();
|
|
482
|
+
commit(matches[active]);
|
|
483
|
+
} else {
|
|
484
|
+
// No row highlighted: the typed text is the answer. `preventDefault`
|
|
485
|
+
// only when a caller is taking Enter, so a plain form submit is
|
|
486
|
+
// otherwise left alone.
|
|
487
|
+
if (onSubmit) e.preventDefault();
|
|
488
|
+
setOpen(false);
|
|
489
|
+
onSubmit?.();
|
|
490
|
+
}
|
|
491
|
+
} else if (e.key === "Escape") {
|
|
492
|
+
setOpen(false);
|
|
493
|
+
setActive(-1);
|
|
494
|
+
} else if (e.key === "Tab") {
|
|
495
|
+
// Closes, and lets the browser take the Tab: focus is in THIS input,
|
|
496
|
+
// which is staying, so there is nothing to catch — unlike the pickers
|
|
497
|
+
// whose focus sits inside a portalled panel.
|
|
474
498
|
setOpen(false);
|
|
475
|
-
|
|
499
|
+
setActive(-1);
|
|
476
500
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
//
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
501
|
+
// Home/End are deliberately absent. The APG gives them to the list only
|
|
502
|
+
// where the combobox is not editable; here the text IS the value, a
|
|
503
|
+
// payee name is long enough to want the caret moved to its start, and
|
|
504
|
+
// the desktop list is capped at 8 rows — so jumping it would be worth
|
|
505
|
+
// almost nothing and would cost the one gesture that field is used with.
|
|
506
|
+
}}
|
|
507
|
+
className={cn(
|
|
508
|
+
FIELD_BASE,
|
|
509
|
+
label !== undefined && FIELD_FLOATING_PAD,
|
|
510
|
+
"pe-9",
|
|
511
|
+
field.isInvalid && FIELD_INVALID,
|
|
512
|
+
)}
|
|
513
|
+
/>
|
|
514
|
+
<ChevronDown
|
|
515
|
+
aria-hidden
|
|
516
|
+
onMouseDown={(e) => {
|
|
517
|
+
// Toggle on the chevron without stealing focus from the input.
|
|
518
|
+
e.preventDefault();
|
|
519
|
+
if (!disabled) setOpen((o) => !o);
|
|
520
|
+
}}
|
|
521
|
+
className={cn(
|
|
522
|
+
"absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]",
|
|
523
|
+
// The box above does the dimming; dimming here too would halve it again.
|
|
524
|
+
!disabled && "cursor-pointer",
|
|
525
|
+
)}
|
|
526
|
+
/>
|
|
527
|
+
</div>
|
|
528
|
+
</EndHintRow>
|
|
529
|
+
{/* The label and its "?" share the top strip — after the field in the DOM, so the
|
|
530
|
+
"?" follows the control in the tab order, as it does on Input and Select. */}
|
|
531
|
+
{label !== undefined && hintParts.labelHint !== undefined && (
|
|
532
|
+
<StaticLabelRow hint={hintParts.labelHint}>
|
|
533
|
+
<ComboboxFieldLabel htmlFor={fieldId} className={cn(LABEL_IN_ROW, disabled && "opacity-50")}>
|
|
534
|
+
{label}
|
|
535
|
+
</ComboboxFieldLabel>
|
|
536
|
+
</StaticLabelRow>
|
|
537
|
+
)}
|
|
538
|
+
<FieldCaption parts={hintParts} />
|
|
514
539
|
{field.errorEl}
|
|
515
540
|
{isPhone && (
|
|
516
541
|
<PickerSheet
|
|
@@ -667,6 +692,8 @@ export interface InlineEntityComboboxProps<V extends string | number, C extends
|
|
|
667
692
|
invalid?: boolean;
|
|
668
693
|
/** What is wrong with the value — see {@link Combobox}'s `error`. */
|
|
669
694
|
error?: ReactNode;
|
|
695
|
+
/** Standing advice — see {@link Combobox}'s `hint` (keksdose K4). */
|
|
696
|
+
hint?: ReactNode;
|
|
670
697
|
/** A picked/typed option emits its id; emptying the field, or the "×", emits
|
|
671
698
|
* `clearValue`. */
|
|
672
699
|
onChange: (v: V | C) => void;
|
|
@@ -740,6 +767,7 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
|
|
|
740
767
|
clearLabel,
|
|
741
768
|
invalid,
|
|
742
769
|
error,
|
|
770
|
+
hint,
|
|
743
771
|
"aria-label": ariaLabel,
|
|
744
772
|
// Off `rest` and onto the <input>, which is the combobox a reader meets: `Field`'s
|
|
745
773
|
// render-prop spreads `{ id, aria-describedby, aria-invalid, aria-required }`, and
|
|
@@ -752,10 +780,11 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
|
|
|
752
780
|
// The clear value reads as "nothing selected", whichever one the caller picked —
|
|
753
781
|
// so from here down `value` is the id or `null`, as it always was.
|
|
754
782
|
const value: V | null = rawValue == null || rawValue === clearValue ? null : (rawValue as V);
|
|
783
|
+
const hintParts = useFieldHint(hint, ariaDescribedBy);
|
|
755
784
|
const field = useComboboxFieldError(
|
|
756
785
|
error,
|
|
757
786
|
invalid || ariaInvalid === true || ariaInvalid === "true",
|
|
758
|
-
|
|
787
|
+
hintParts.describedBy,
|
|
759
788
|
);
|
|
760
789
|
const generated = useId();
|
|
761
790
|
const fieldId = id ?? generated;
|
|
@@ -880,149 +909,159 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
|
|
|
880
909
|
// which is the combobox a reader meets. Spread FIRST, so the field's ARIA and the
|
|
881
910
|
// handlers carrying live #309 and dev#549 cannot be replaced from outside.
|
|
882
911
|
<div {...rest} ref={wrapperRef} className={cn("relative", className)}>
|
|
883
|
-
{/* A real <label for> — see {@link Combobox}. */}
|
|
884
|
-
{label !== undefined && (
|
|
912
|
+
{/* A real <label for> — see {@link Combobox}; with a "?", in a row after the field. */}
|
|
913
|
+
{label !== undefined && hintParts.labelHint === undefined && (
|
|
885
914
|
<ComboboxFieldLabel htmlFor={fieldId} className={disabled ? "opacity-50" : undefined}>
|
|
886
915
|
{label}
|
|
887
916
|
</ComboboxFieldLabel>
|
|
888
917
|
)}
|
|
889
|
-
{
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
<
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
// The sheet has its own input; the field behind it must not also pull up
|
|
933
|
-
// the keyboard and scroll the page under the dialog.
|
|
934
|
-
if (isPhone) sheetInputRef.current?.focus();
|
|
935
|
-
}}
|
|
936
|
-
onBlur={() => {
|
|
937
|
-
// On a phone the blur is the SHEET taking focus, not the user leaving the
|
|
938
|
-
// field — reconciling there would close the sheet the instant it opened.
|
|
939
|
-
if (!isPhone) reconcile();
|
|
940
|
-
}}
|
|
941
|
-
onChange={(e) => {
|
|
942
|
-
setText(e.target.value);
|
|
943
|
-
setOpen(true);
|
|
944
|
-
setActive(-1);
|
|
945
|
-
}}
|
|
946
|
-
onKeyDown={(e) => {
|
|
947
|
-
// Disabled rows are passed over; Up from "nothing highlighted" lands on
|
|
948
|
-
// the first takeable row, as it always landed on row 0.
|
|
949
|
-
if (e.key === "ArrowDown") {
|
|
950
|
-
e.preventDefault();
|
|
918
|
+
<EndHintRow hint={label === undefined ? hintParts.labelHint : undefined}>
|
|
919
|
+
{/* Inner wrapper for chevron centering — same reasoning as Combobox above.
|
|
920
|
+
It is also what the portalled list anchors to. */}
|
|
921
|
+
{/* Dimmed as a whole when disabled, the way EntityCombobox dims its trigger.
|
|
922
|
+
FIELD_BASE's grey alone left a disabled picker looking like a filled-in
|
|
923
|
+
one beside the pickers that do dim (the label is not the input's `peer`,
|
|
924
|
+
so it is dimmed by hand above). */}
|
|
925
|
+
<div ref={fieldRef} className={cn("relative", disabled && "cursor-not-allowed opacity-50")}>
|
|
926
|
+
<input
|
|
927
|
+
id={fieldId}
|
|
928
|
+
value={shown}
|
|
929
|
+
placeholder={placeholder}
|
|
930
|
+
// The caller's name, over the <label for> above. Just the label, never
|
|
931
|
+
// "label: value" the way a trigger button has to compose it: an input
|
|
932
|
+
// already exposes its value separately.
|
|
933
|
+
aria-label={ariaLabel}
|
|
934
|
+
role="combobox"
|
|
935
|
+
aria-expanded={open}
|
|
936
|
+
// Required by the role, and the half that was missing: the field said it
|
|
937
|
+
// was expanded and never said what it had expanded (ESLint's
|
|
938
|
+
// `role-has-required-aria-props`, the audit's §a11y).
|
|
939
|
+
aria-controls={listboxId}
|
|
940
|
+
aria-activedescendant={activeId}
|
|
941
|
+
aria-autocomplete="list"
|
|
942
|
+
aria-invalid={field.isInvalid || undefined}
|
|
943
|
+
aria-describedby={field.describedBy}
|
|
944
|
+
aria-required={ariaRequired}
|
|
945
|
+
autoComplete="off"
|
|
946
|
+
disabled={disabled}
|
|
947
|
+
// eslint-disable-next-line jsx-a11y/no-autofocus -- a documented prop the caller opts into (off by default); the field never takes focus on its own.
|
|
948
|
+
autoFocus={autoFocus}
|
|
949
|
+
// `inputMode="none"` rather than readOnly, for the same reason Combobox
|
|
950
|
+
// above gives: the sheet carries the keyboard, and a readOnly field would
|
|
951
|
+
// take FIELD_BASE's settled look on a field that is perfectly editable.
|
|
952
|
+
inputMode={isPhone ? "none" : undefined}
|
|
953
|
+
onMouseDown={primaryOnly.onMouseDown}
|
|
954
|
+
onFocus={(e) => {
|
|
955
|
+
// See {@link usePrimaryPressOnly}: a back/forward button lands here on
|
|
956
|
+
// its way to navigating, and neither the list nor the select-all is
|
|
957
|
+
// anything it asked for (live #309 rework).
|
|
958
|
+
if (primaryOnly.fromAuxButton()) return;
|
|
959
|
+
setText(shown);
|
|
960
|
+
e.currentTarget.select();
|
|
951
961
|
setOpen(true);
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
962
|
+
// The sheet has its own input; the field behind it must not also pull up
|
|
963
|
+
// the keyboard and scroll the page under the dialog.
|
|
964
|
+
if (isPhone) sheetInputRef.current?.focus();
|
|
965
|
+
}}
|
|
966
|
+
onBlur={() => {
|
|
967
|
+
// On a phone the blur is the SHEET taking focus, not the user leaving the
|
|
968
|
+
// field — reconciling there would close the sheet the instant it opened.
|
|
969
|
+
if (!isPhone) reconcile();
|
|
970
|
+
}}
|
|
971
|
+
onChange={(e) => {
|
|
972
|
+
setText(e.target.value);
|
|
973
|
+
setOpen(true);
|
|
974
|
+
setActive(-1);
|
|
975
|
+
}}
|
|
976
|
+
onKeyDown={(e) => {
|
|
977
|
+
// Disabled rows are passed over; Up from "nothing highlighted" lands on
|
|
978
|
+
// the first takeable row, as it always landed on row 0.
|
|
979
|
+
if (e.key === "ArrowDown") {
|
|
980
|
+
e.preventDefault();
|
|
981
|
+
setOpen(true);
|
|
982
|
+
setActive((i) => stepEnabled(matches, i, 1));
|
|
983
|
+
} else if (e.key === "ArrowUp") {
|
|
958
984
|
e.preventDefault();
|
|
959
|
-
|
|
960
|
-
} else {
|
|
985
|
+
setActive((i) => (i < 0 ? stepEnabled(matches, -1, 1) : stepEnabled(matches, i, -1)));
|
|
986
|
+
} else if (e.key === "Enter") {
|
|
987
|
+
if (open && isOptionEnabled(matches[active])) {
|
|
988
|
+
e.preventDefault();
|
|
989
|
+
commit(matches[active]);
|
|
990
|
+
} else {
|
|
991
|
+
reconcile();
|
|
992
|
+
}
|
|
993
|
+
} else if (e.key === "Escape") {
|
|
994
|
+
setText(null);
|
|
995
|
+
close();
|
|
996
|
+
} else if (e.key === "Tab") {
|
|
997
|
+
// Focus is in THIS input and stays there, so the browser's own Tab is
|
|
998
|
+
// left alone; all that is needed is that the list stop covering what
|
|
999
|
+
// the user is tabbing to. `reconcile` rather than `close`, because
|
|
1000
|
+
// leaving the field is exactly when loose text has to be judged.
|
|
961
1001
|
reconcile();
|
|
962
1002
|
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
close();
|
|
966
|
-
} else if (e.key === "Tab") {
|
|
967
|
-
// Focus is in THIS input and stays there, so the browser's own Tab is
|
|
968
|
-
// left alone; all that is needed is that the list stop covering what
|
|
969
|
-
// the user is tabbing to. `reconcile` rather than `close`, because
|
|
970
|
-
// leaving the field is exactly when loose text has to be judged.
|
|
971
|
-
reconcile();
|
|
972
|
-
}
|
|
973
|
-
// Home/End stay with the caret — see the note in {@link Combobox}: this
|
|
974
|
-
// field is editable, and its text is what `reconcile` judges.
|
|
975
|
-
}}
|
|
976
|
-
className={cn(
|
|
977
|
-
FIELD_BASE,
|
|
978
|
-
label !== undefined && FIELD_FLOATING_PAD,
|
|
979
|
-
"pe-9",
|
|
980
|
-
field.isInvalid && FIELD_INVALID,
|
|
981
|
-
)}
|
|
982
|
-
/>
|
|
983
|
-
{showClear ? (
|
|
984
|
-
<button
|
|
985
|
-
type="button"
|
|
986
|
-
// Out of the tab order, like the clear on `EntityCombobox`: the keyboard
|
|
987
|
-
// already clears this field by selecting its text and deleting, and a
|
|
988
|
-
// second stop between every picker and the next field is a worse trade
|
|
989
|
-
// than the one gesture it saves.
|
|
990
|
-
tabIndex={-1}
|
|
991
|
-
aria-label={labels.clear}
|
|
992
|
-
// preventDefault, exactly as the chevron does: without it the press
|
|
993
|
-
// focuses the input, which on a phone opens the sheet over the field the
|
|
994
|
-
// press was clearing.
|
|
995
|
-
onMouseDown={(e) => e.preventDefault()}
|
|
996
|
-
onClick={() => {
|
|
997
|
-
setText(null);
|
|
998
|
-
close();
|
|
999
|
-
onChange(clearValue);
|
|
1000
|
-
}}
|
|
1001
|
-
className={cn(
|
|
1002
|
-
"absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--text-placeholder)]",
|
|
1003
|
-
"hover:text-[var(--text-secondary)]",
|
|
1004
|
-
)}
|
|
1005
|
-
>
|
|
1006
|
-
<X aria-hidden className="size-4" />
|
|
1007
|
-
</button>
|
|
1008
|
-
) : (
|
|
1009
|
-
<ChevronDown
|
|
1010
|
-
aria-hidden
|
|
1011
|
-
onMouseDown={(e) => {
|
|
1012
|
-
// Toggle on the chevron without stealing focus from the input — and
|
|
1013
|
-
// not at all on a disabled field, which the input's own `disabled`
|
|
1014
|
-
// does not stop from here.
|
|
1015
|
-
e.preventDefault();
|
|
1016
|
-
if (!disabled) setOpen((o) => !o);
|
|
1003
|
+
// Home/End stay with the caret — see the note in {@link Combobox}: this
|
|
1004
|
+
// field is editable, and its text is what `reconcile` judges.
|
|
1017
1005
|
}}
|
|
1018
1006
|
className={cn(
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1007
|
+
FIELD_BASE,
|
|
1008
|
+
label !== undefined && FIELD_FLOATING_PAD,
|
|
1009
|
+
"pe-9",
|
|
1010
|
+
field.isInvalid && FIELD_INVALID,
|
|
1022
1011
|
)}
|
|
1023
1012
|
/>
|
|
1024
|
-
|
|
1025
|
-
|
|
1013
|
+
{showClear ? (
|
|
1014
|
+
<button
|
|
1015
|
+
type="button"
|
|
1016
|
+
// Out of the tab order, like the clear on `EntityCombobox`: the keyboard
|
|
1017
|
+
// already clears this field by selecting its text and deleting, and a
|
|
1018
|
+
// second stop between every picker and the next field is a worse trade
|
|
1019
|
+
// than the one gesture it saves.
|
|
1020
|
+
tabIndex={-1}
|
|
1021
|
+
aria-label={labels.clear}
|
|
1022
|
+
// preventDefault, exactly as the chevron does: without it the press
|
|
1023
|
+
// focuses the input, which on a phone opens the sheet over the field the
|
|
1024
|
+
// press was clearing.
|
|
1025
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
1026
|
+
onClick={() => {
|
|
1027
|
+
setText(null);
|
|
1028
|
+
close();
|
|
1029
|
+
onChange(clearValue);
|
|
1030
|
+
}}
|
|
1031
|
+
className={cn(
|
|
1032
|
+
"absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--text-placeholder)]",
|
|
1033
|
+
"hover:text-[var(--text-secondary)]",
|
|
1034
|
+
)}
|
|
1035
|
+
>
|
|
1036
|
+
<X aria-hidden className="size-4" />
|
|
1037
|
+
</button>
|
|
1038
|
+
) : (
|
|
1039
|
+
<ChevronDown
|
|
1040
|
+
aria-hidden
|
|
1041
|
+
onMouseDown={(e) => {
|
|
1042
|
+
// Toggle on the chevron without stealing focus from the input — and
|
|
1043
|
+
// not at all on a disabled field, which the input's own `disabled`
|
|
1044
|
+
// does not stop from here.
|
|
1045
|
+
e.preventDefault();
|
|
1046
|
+
if (!disabled) setOpen((o) => !o);
|
|
1047
|
+
}}
|
|
1048
|
+
className={cn(
|
|
1049
|
+
"absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]",
|
|
1050
|
+
// The box above does the dimming; dimming here too would halve it again.
|
|
1051
|
+
!disabled && "cursor-pointer",
|
|
1052
|
+
)}
|
|
1053
|
+
/>
|
|
1054
|
+
)}
|
|
1055
|
+
</div>
|
|
1056
|
+
</EndHintRow>
|
|
1057
|
+
{label !== undefined && hintParts.labelHint !== undefined && (
|
|
1058
|
+
<StaticLabelRow hint={hintParts.labelHint}>
|
|
1059
|
+
<ComboboxFieldLabel htmlFor={fieldId} className={cn(LABEL_IN_ROW, disabled && "opacity-50")}>
|
|
1060
|
+
{label}
|
|
1061
|
+
</ComboboxFieldLabel>
|
|
1062
|
+
</StaticLabelRow>
|
|
1063
|
+
)}
|
|
1064
|
+
<FieldCaption parts={hintParts} />
|
|
1026
1065
|
{field.errorEl}
|
|
1027
1066
|
{isPhone && (
|
|
1028
1067
|
<PickerSheet
|