@eifi1/ui-kit 0.22.0 → 0.24.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 +8 -7
- package/dist/components/amount-input.d.ts +34 -7
- package/dist/components/amount-input.js +14 -10
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +26 -0
- package/dist/components/autocomplete.js +164 -137
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +8 -7
- package/dist/components/calculator.d.ts +8 -7
- package/dist/components/column-mapper.d.ts +235 -0
- package/dist/components/column-mapper.js +411 -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 +113 -3
- package/dist/components/combobox.js +456 -313
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +8 -7
- package/dist/components/confirm-dialog.js +7 -8
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.d.ts +5 -4
- package/dist/components/country-select.d.ts +50 -7
- package/dist/components/country-select.js +65 -9
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +8 -7
- package/dist/components/danger-confirm.js +64 -7
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/date-picker.d.ts +41 -0
- package/dist/components/date-picker.js +269 -8
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +48 -2
- package/dist/components/entity-combobox.js +159 -114
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +8 -7
- package/dist/components/field-parts.d.ts +116 -6
- package/dist/components/field-parts.js +84 -0
- package/dist/components/field-parts.js.map +1 -1
- 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 +8 -7
- package/dist/components/file-dropzone.d.ts +8 -7
- package/dist/components/form-actions.d.ts +8 -7
- package/dist/components/form-actions.js +23 -3
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/iban-input.d.ts +8 -7
- package/dist/components/icon-picker.d.ts +10 -1
- package/dist/components/icon-picker.js +12 -4
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/language-select.d.ts +8 -7
- package/dist/components/money-field.d.ts +11 -7
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.js +2 -1
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +31 -2
- package/dist/components/multi-entity-combobox.js +152 -103
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/number-field.d.ts +8 -7
- package/dist/components/number-input.d.ts +8 -7
- package/dist/components/numpad-sheet.d.ts +8 -7
- package/dist/components/phone-input.d.ts +8 -7
- 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 +8 -7
- package/dist/components/settings-fields.d.ts +8 -7
- package/dist/components/share-card.d.ts +8 -7
- package/dist/components/swatch-picker.d.ts +17 -1
- package/dist/components/swatch-picker.js +10 -4
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/text-link.d.ts +8 -7
- package/dist/components/tile-radio.d.ts +20 -0
- package/dist/components/tile-radio.js +25 -8
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +8 -7
- package/dist/components/toggle-group.js +2 -2
- 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 +5 -4
- package/dist/components/ui.js +60 -28
- package/dist/components/ui.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +66 -1
- package/dist/feedback/feedback-attachment.js +185 -65
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +66 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +67 -252
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +66 -287
- package/dist/feedback/feedback-thread.js +16 -3
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/{kit-labels-R8mIAc9N.d.ts → feedback-DOwPu-Il.d.ts} +1273 -30
- package/dist/feedback.d.ts +65 -3
- package/dist/hooks/use-file-drop.d.ts +8 -7
- package/dist/i18n/defaults.d.ts +8 -7
- package/dist/i18n/defaults.js +3 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +8 -7
- package/dist/i18n/german.js +36 -2
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +5 -4
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +8 -7
- package/dist/i18n/locales/de-CH.d.ts +8 -7
- package/dist/i18n/locales/en.d.ts +8 -7
- package/dist/i18n/locales/en.js +7 -0
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +8 -7
- package/dist/i18n/locales/es.js +37 -2
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +8 -7
- package/dist/i18n/locales/fr.js +36 -2
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +8 -7
- package/dist/i18n/locales/hu.js +38 -2
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +8 -7
- package/dist/i18n/locales/it.js +37 -2
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +8 -7
- package/dist/i18n/locales/zh.js +34 -2
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +8 -7
- package/dist/i18n/review.js +20 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.js +16 -0
- 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/strip-fade.d.ts +2 -2
- package/dist/lib/strip-fade.js.map +1 -1
- 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 +142 -13
- package/dist/rhf/fields.js +185 -3
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +8 -7
- package/dist/rhf.d.ts +9 -8
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +8 -7
- package/dist/shell/top-bar-brand.d.ts +8 -7
- 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 +8 -7
- package/dist/table-text.d.ts +1 -1
- package/dist/wizard/stepper-nav.d.ts +38 -9
- package/dist/wizard/stepper-nav.js +4 -0
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard.d.ts +8 -7
- package/package.json +1 -1
- package/src/components/amount-input.tsx +50 -14
- package/src/components/autocomplete.tsx +188 -116
- package/src/components/column-mapper.tsx +768 -0
- package/src/components/combobox-core.tsx +82 -9
- package/src/components/combobox.tsx +655 -335
- package/src/components/confirm-dialog.tsx +12 -8
- package/src/components/country-select.tsx +135 -22
- package/src/components/danger-confirm.tsx +274 -24
- package/src/components/date-picker.tsx +423 -10
- package/src/components/entity-combobox.tsx +243 -129
- package/src/components/field-parts.tsx +224 -5
- package/src/components/field-strip.tsx +149 -0
- package/src/components/form-actions.tsx +52 -5
- package/src/components/icon-picker.tsx +23 -4
- package/src/components/money-field.tsx +3 -0
- package/src/components/month-picker.tsx +2 -1
- package/src/components/multi-entity-combobox.tsx +221 -121
- package/src/components/reauth-dialog.tsx +17 -18
- package/src/components/swatch-picker.tsx +29 -5
- package/src/components/tile-radio.tsx +74 -13
- package/src/components/toggle-group.tsx +2 -2
- package/src/components/trigger-aria.ts +5 -0
- package/src/components/ui.tsx +114 -33
- package/src/feedback/feedback-attachment.tsx +372 -72
- package/src/feedback/feedback-dialog.tsx +7 -3
- package/src/feedback/feedback-inbox.tsx +3 -2
- package/src/feedback/feedback-thread.tsx +112 -9
- package/src/i18n/defaults.ts +2 -0
- package/src/i18n/german.ts +42 -0
- package/src/i18n/kit-labels.tsx +4 -0
- package/src/i18n/locales/en.ts +27 -5
- package/src/i18n/locales/es.ts +41 -0
- package/src/i18n/locales/fr.ts +42 -0
- package/src/i18n/locales/hu.ts +37 -0
- package/src/i18n/locales/it.ts +41 -0
- package/src/i18n/locales/zh.ts +32 -0
- package/src/i18n/review.ts +19 -0
- package/src/index.ts +18 -0
- package/src/lib/column-mapping.ts +234 -0
- package/src/lib/strip-fade.ts +2 -2
- package/src/lib/table-text.ts +271 -0
- package/src/rhf/fields.tsx +421 -6
- package/src/rhf.ts +2 -1
- package/src/shell/topbar-action-menu.tsx +134 -38
- package/src/wizard/stepper-nav.tsx +35 -1
- package/dist/components/field-anatomy.d.ts +0 -95
- package/dist/components/field-anatomy.js +0 -84
- package/dist/components/field-anatomy.js.map +0 -1
- package/dist/feedback-attachment-NhUm7Zga.d.ts +0 -367
- package/src/components/field-anatomy.tsx +0 -190
package/dist/wizard.d.ts
CHANGED
|
@@ -6,7 +6,10 @@ export { WizardStep, WizardStepProps } from './wizard/wizard-step.js';
|
|
|
6
6
|
export { WizardSummary } from './wizard/wizard-summary.js';
|
|
7
7
|
export { StepperNav } from './wizard/stepper-nav.js';
|
|
8
8
|
import 'react';
|
|
9
|
-
import './
|
|
9
|
+
import './feedback-DOwPu-Il.js';
|
|
10
|
+
import './components/tooltip.js';
|
|
11
|
+
import './hooks/use-anchored-rect.js';
|
|
12
|
+
import './lib/clipping.js';
|
|
10
13
|
import './data-table-labels-B7OdnM0S.js';
|
|
11
14
|
import './components/data-table-sort.js';
|
|
12
15
|
import './components/swipeable-row.js';
|
|
@@ -16,9 +19,6 @@ import './components/popover.js';
|
|
|
16
19
|
import './components/chip.js';
|
|
17
20
|
import 'lucide-react';
|
|
18
21
|
import './components/field-sync.js';
|
|
19
|
-
import './components/tooltip.js';
|
|
20
|
-
import './hooks/use-anchored-rect.js';
|
|
21
|
-
import './lib/clipping.js';
|
|
22
22
|
import './tour/tour.js';
|
|
23
23
|
import './search/command-palette.js';
|
|
24
24
|
import './search/global-search.js';
|
|
@@ -38,9 +38,6 @@ import './components/dialog-frame.js';
|
|
|
38
38
|
import './components/modal.js';
|
|
39
39
|
import './hooks/use-search-param-state.js';
|
|
40
40
|
import './components/measured-grid.js';
|
|
41
|
-
import './feedback-attachment-NhUm7Zga.js';
|
|
42
|
-
import './feedback/feedback-thread.js';
|
|
43
|
-
import './feedback/feedback-inbox.js';
|
|
44
41
|
import './components/account-settings-labels.js';
|
|
45
42
|
import './components/floating-panel.js';
|
|
46
43
|
import './components/bulk-action-bar.js';
|
|
@@ -70,3 +67,7 @@ import './components/inline-edit-field.js';
|
|
|
70
67
|
import './lib/iban.js';
|
|
71
68
|
import './lib/phone.js';
|
|
72
69
|
import './components/sign-chip.js';
|
|
70
|
+
import './components/column-mapper.js';
|
|
71
|
+
import './lib/column-mapping.js';
|
|
72
|
+
import './lib/table-text.js';
|
|
73
|
+
import './components/field-parts.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eifi1/ui-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "App-agnostic shared design system: domain-free React components, theme and tokens.",
|
|
6
6
|
"_sideEffects": "The barrel (src/index.ts) re-exports EVERY component, including components/chart, which statically imports recharts. Without this field a bundler must assume each of those modules has side effects, so it cannot drop the unused ones: importing a Button from '@eifi1/ui-kit' dragged all 433KB of recharts into the consumer's ENTRY chunk, and Keksdose was preloading it on every page load, login included. Nothing in src/ imports for side effects (no bare imports, no CSS imports from JS, no module-level global writes) — the token stylesheet is its own './tokens.css' export — so this is safe. CSS is still listed so any future stylesheet survives. Re-checked when `flag-icons` left `dependencies` and tsup started emitting a `\"use client\"` banner: neither touches this. The flag stylesheet was never imported from JS in the first place (that was the finding), and a directive prologue is a string literal every bundler lifts out before it decides whether a module is inert.",
|
|
@@ -55,7 +55,33 @@ interface AmountInputProps {
|
|
|
55
55
|
* a phone keeps its numpad, exactly as {@link NumberInput}'s `calculator={false}`.
|
|
56
56
|
*/
|
|
57
57
|
calculator?: boolean;
|
|
58
|
+
/** The field's WRAPPER — width, a margin, a flex share. The `<input>` is
|
|
59
|
+
* `inputClassName`'s. */
|
|
58
60
|
className?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Classes for the `<input>` itself — {@link NumberInput}'s and {@link Input}'s prop of
|
|
63
|
+
* the same name, merged the same way: after everything the field sets for LOOKS — the
|
|
64
|
+
* base classes, the plain end padding, the display shape's centring and the `tone`
|
|
65
|
+
* colour — so it can restyle the figure (`text-end` for a money column whose decimal
|
|
66
|
+
* points line up, `font-medium`, a tighter `py-1` or a symmetric `px-2` in a table
|
|
67
|
+
* cell, `text-transparent` while a figure is being re-fetched), and before what the
|
|
68
|
+
* field needs to WORK: the end padding the calculator and the currency chip reserve,
|
|
69
|
+
* and the invalid border. Neither of those can be taken away by accident — a caller's
|
|
70
|
+
* `px-2` keeps its start edge and the digits still stop short of the chip, and a
|
|
71
|
+
* borderless cell's `border-transparent` still turns red when the value is wrong.
|
|
72
|
+
*
|
|
73
|
+
* keksdose G7: the invoice's VAT cell right-aligns its figure, and with only
|
|
74
|
+
* `className` (the wrapper) it reached the element through `[&_input]:text-end` — a
|
|
75
|
+
* descendant selector that depends on the field's inner markup, which the kit is free
|
|
76
|
+
* to change, and that a reader of the call site has to decode. {@link MoneyField}
|
|
77
|
+
* passes it through, and `RhfMoneyField` takes it too.
|
|
78
|
+
*
|
|
79
|
+
* keksdose 0.24: until then it sat BEFORE the plain end padding (`pe-3`) and the tone,
|
|
80
|
+
* so the budget's assigned cell could not make its padding symmetric (`px-2` came out
|
|
81
|
+
* `px-2 pe-3`) and the holdings price could not hide an outflow-toned figure while
|
|
82
|
+
* re-fetching — both were back on `[&_input]:` selectors on the wrapper.
|
|
83
|
+
*/
|
|
84
|
+
inputClassName?: string;
|
|
59
85
|
id?: string;
|
|
60
86
|
ariaLabel?: string;
|
|
61
87
|
/**
|
|
@@ -291,7 +317,7 @@ function isResultOf(previous: string, text: string, settle: (text: string) => st
|
|
|
291
317
|
}
|
|
292
318
|
|
|
293
319
|
export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
294
|
-
({ value, onChange, currency, onCurrencyChange, placeholder, label, disabled, invalid: invalidProp, error, calculator = true, className, id, ariaLabel, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, hint, autoFocus, tone = "neutral", negative = false, onNegativeChange, variant = "field", align = "start", labels, currencyNames, digits: digitsProp, min, max, onCommit }, ref) => {
|
|
320
|
+
({ value, onChange, currency, onCurrencyChange, placeholder, label, disabled, invalid: invalidProp, error, calculator = true, className, inputClassName, id, ariaLabel, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, hint, autoFocus, tone = "neutral", negative = false, onNegativeChange, variant = "field", align = "start", labels, currencyNames, digits: digitsProp, min, max, onCommit }, ref) => {
|
|
295
321
|
const generatedId = useId();
|
|
296
322
|
const fieldId = id ?? generatedId;
|
|
297
323
|
const hintId = useId();
|
|
@@ -320,6 +346,17 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
320
346
|
// fighting itself through responsive overrides. `useMediaQuery` reads
|
|
321
347
|
// synchronously on mount, so there is no field→figure flash.
|
|
322
348
|
const asDisplay = variant === "display" && isMobile;
|
|
349
|
+
// Room for the trailing controls — the calculator and the currency chip — or
|
|
350
|
+
// nothing when neither trails. showCalc is always false on a phone, so the display
|
|
351
|
+
// shape only ever has to clear the currency chip — and it clears it by the chip's
|
|
352
|
+
// actual width (a text-sm code plus a chevron, ~58px) rather than the 5rem the
|
|
353
|
+
// boxed shape reserves. On a 375px screen those 16px are the difference between a
|
|
354
|
+
// readable figure and a clipped one (feedback #430 rework).
|
|
355
|
+
const trailingRoom = asDisplay
|
|
356
|
+
? currency ? (editable ? "pe-16" : "pe-12") : undefined
|
|
357
|
+
: showCalc
|
|
358
|
+
? currency ? (editable ? "pe-24" : "pe-16") : "pe-10"
|
|
359
|
+
: currency ? (editable ? "pe-20" : "pe-14") : undefined;
|
|
323
360
|
const innerRef = useRef<HTMLInputElement>(null);
|
|
324
361
|
const setRefs = useCallback(
|
|
325
362
|
(el: HTMLInputElement | null) => {
|
|
@@ -504,25 +541,24 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
504
541
|
// (feedback #314). FLOATING_INPUT_CLASS already bundles the peer +
|
|
505
542
|
// transparent-placeholder bits.
|
|
506
543
|
asDisplay ? DISPLAY_INPUT_CLASS : label !== undefined ? FLOATING_INPUT_CLASS : FIELD_BASE,
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
|
|
510
|
-
// chevron, ~58px) rather than the 5rem the boxed shape reserves. On a
|
|
511
|
-
// 375px screen those 16px are the difference between a readable figure
|
|
512
|
-
// and a clipped one (feedback #430 rework).
|
|
513
|
-
asDisplay
|
|
514
|
-
? currency ? (editable ? "pe-16" : "pe-12") : "pe-0"
|
|
515
|
-
: showCalc
|
|
516
|
-
? currency ? (editable ? "pe-24" : "pe-16") : "pe-10"
|
|
517
|
-
: currency ? (editable ? "pe-20" : "pe-14") : "pe-3",
|
|
544
|
+
// The plain end padding, with nothing trailing: cosmetic, so the caller's
|
|
545
|
+
// to change (keksdose 0.24 — a symmetric `px-2` in a budget cell).
|
|
546
|
+
!trailingRoom && (asDisplay ? "pe-0" : "pe-3"),
|
|
518
547
|
// Centred display shape: centre in what is LEFT of the currency chip.
|
|
519
548
|
// `text-center` alone would centre the figure in the whole box, i.e.
|
|
520
|
-
// partly underneath the chip; the padding
|
|
549
|
+
// partly underneath the chip; the padding below is what takes the chip
|
|
521
550
|
// out of the centring, and nothing is added at the start because a
|
|
522
551
|
// mirrored reservation would spend the width twice (see `align`).
|
|
523
552
|
asDisplay && align === "center" && "text-center",
|
|
524
|
-
//
|
|
553
|
+
// Over the base class's own text colour.
|
|
525
554
|
TONE_CLASS[tone],
|
|
555
|
+
// The caller's, over all of the above — base, plain padding, alignment and
|
|
556
|
+
// tone — and under only what the field needs to work (see `inputClassName`).
|
|
557
|
+
inputClassName,
|
|
558
|
+
// Room for the trailing controls, after the caller's so a `px-2` cannot run
|
|
559
|
+
// the digits under the chip (NumberInput's order, which reserves its unit
|
|
560
|
+
// and calculator the same way).
|
|
561
|
+
trailingRoom,
|
|
526
562
|
invalid && FIELD_INVALID,
|
|
527
563
|
)}
|
|
528
564
|
aria-invalid={invalid || undefined}
|
|
@@ -14,6 +14,7 @@ import { useAnchorDir } from "./use-anchor-dir";
|
|
|
14
14
|
import {
|
|
15
15
|
ComboboxFieldLabel,
|
|
16
16
|
DISABLED_ROW_CLASS,
|
|
17
|
+
endHintRowProps,
|
|
17
18
|
isOptionEnabled,
|
|
18
19
|
stepEnabled,
|
|
19
20
|
useActiveOptionScroll,
|
|
@@ -22,7 +23,16 @@ import {
|
|
|
22
23
|
type ComboOption,
|
|
23
24
|
} from "./combobox-core";
|
|
24
25
|
import { DEFAULT_COMBOBOX_LABELS, useKitLabels } from "../i18n/kit-labels";
|
|
25
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
EndHintRow,
|
|
28
|
+
FieldCaption,
|
|
29
|
+
LABEL_IN_ROW,
|
|
30
|
+
LockedReason,
|
|
31
|
+
StaticLabelRow,
|
|
32
|
+
useFieldHint,
|
|
33
|
+
useLockReason,
|
|
34
|
+
} from "./field-parts";
|
|
35
|
+
import { mergeDescribedBy } from "./choice-parts";
|
|
26
36
|
|
|
27
37
|
/**
|
|
28
38
|
* `value`/`onChange` are the TEXT's, and `onSelect` is "a suggestion was taken" —
|
|
@@ -111,6 +121,11 @@ export interface AutocompleteProps<V extends string | number = string>
|
|
|
111
121
|
* `aria-describedby`, before any error; a {@link FieldHint} "?" rides the label line,
|
|
112
122
|
* or with no label sits at the field's end edge, outside the box. Not the list's
|
|
113
123
|
* `status` line, which talks about the lookup, not the field.
|
|
124
|
+
*
|
|
125
|
+
* Pass it — even as `undefined` — on a field whose hint comes and goes (0.24): the
|
|
126
|
+
* unlabelled field then keeps its end-hint row whether or not a "?" is in it, so the
|
|
127
|
+
* `<input>` is never re-parented, and focus and caret survive the toggle. The rule
|
|
128
|
+
* the rest of the family has had since 0.23 (and the fields since 0.22).
|
|
114
129
|
*/
|
|
115
130
|
hint?: ReactNode;
|
|
116
131
|
/**
|
|
@@ -126,10 +141,34 @@ export interface AutocompleteProps<V extends string | number = string>
|
|
|
126
141
|
loadErrorLabel?: string;
|
|
127
142
|
/** Classes for the `<input>` itself; `className` styles the wrapper. */
|
|
128
143
|
inputClassName?: string;
|
|
144
|
+
/**
|
|
145
|
+
* Why the field cannot be changed — {@link Button}'s `disabledReason`, the Combobox
|
|
146
|
+
* family's write lock (keksdose G2, 0.23), for a field whose pick SAVES: an address
|
|
147
|
+
* search whose taken row writes the address, a lookup that links a record.
|
|
148
|
+
*
|
|
149
|
+
* With a reason the input stays focusable and `aria-disabled`, is `readOnly` (the
|
|
150
|
+
* settled grey FIELD_BASE gives a `[readonly]` field), opens no list and asks no
|
|
151
|
+
* `loadOptions` — whatever `open` says — and nothing reaches `onChange`, `onSelect` or
|
|
152
|
+
* `onOpenChange(true)`. Enter is swallowed, so a locked field does not submit the form
|
|
153
|
+
* around it, and a caller's `onKeyDown` does not hear it; every other key still
|
|
154
|
+
* reaches the caller (an Escape that closes its panel) and the caret. The reason is in
|
|
155
|
+
* the kit {@link Tooltip} and on the input's `aria-describedby`. Wins over `disabled`.
|
|
156
|
+
*/
|
|
157
|
+
disabledReason?: ReactNode;
|
|
158
|
+
/**
|
|
159
|
+
* This field COMMITS. Under a locked {@link WriteLockProvider} it is locked the
|
|
160
|
+
* `disabledReason` way with the lock's reason (which wins over its own). A search
|
|
161
|
+
* whose result is only draft state — the form's Save writes it — leaves this off and
|
|
162
|
+
* stays live under the lock. No provider, or an unlocked one: no effect.
|
|
163
|
+
*/
|
|
164
|
+
commit?: boolean;
|
|
129
165
|
}
|
|
130
166
|
|
|
131
167
|
function AutocompleteInner<V extends string | number = string>(
|
|
132
|
-
|
|
168
|
+
props: AutocompleteProps<V>,
|
|
169
|
+
ref: ForwardedRef<HTMLInputElement>,
|
|
170
|
+
) {
|
|
171
|
+
const {
|
|
133
172
|
value,
|
|
134
173
|
onChange,
|
|
135
174
|
options,
|
|
@@ -155,6 +194,8 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
155
194
|
inputClassName,
|
|
156
195
|
id,
|
|
157
196
|
disabled,
|
|
197
|
+
disabledReason,
|
|
198
|
+
commit,
|
|
158
199
|
onKeyDown,
|
|
159
200
|
onFocus,
|
|
160
201
|
onBlur,
|
|
@@ -162,9 +203,7 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
162
203
|
"aria-labelledby": ariaLabelledBy,
|
|
163
204
|
"aria-describedby": ariaDescribedBy,
|
|
164
205
|
...rest
|
|
165
|
-
}
|
|
166
|
-
ref: ForwardedRef<HTMLInputElement>,
|
|
167
|
-
) {
|
|
206
|
+
} = props;
|
|
168
207
|
const generated = useId();
|
|
169
208
|
const fieldId = id ?? generated;
|
|
170
209
|
// Off the GENERATED id: a caller's `id` is theirs to collide with.
|
|
@@ -191,7 +230,11 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
191
230
|
if (next !== open) onOpenChange?.(next);
|
|
192
231
|
};
|
|
193
232
|
const [active, setActive] = useState(-1);
|
|
194
|
-
|
|
233
|
+
// The lock: focusable, `readOnly`, no list — see `disabledReason`. Folded into `live`,
|
|
234
|
+
// so a locked field asks nothing and shows nothing even with `open` held true.
|
|
235
|
+
const lock = useLockReason(commit, disabledReason);
|
|
236
|
+
const locked = lock.locked;
|
|
237
|
+
const live = open && !disabled && !locked;
|
|
195
238
|
const { results, busy, failed, tooShort } = useOptionSource<V>({
|
|
196
239
|
options,
|
|
197
240
|
loadOptions,
|
|
@@ -230,7 +273,7 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
230
273
|
setActive(-1);
|
|
231
274
|
};
|
|
232
275
|
const take = (o: ComboOption<V>) => {
|
|
233
|
-
if (o.disabled) return;
|
|
276
|
+
if (o.disabled || locked) return;
|
|
234
277
|
if (fillOnSelect) onChange(o.label);
|
|
235
278
|
onSelect?.(o);
|
|
236
279
|
close();
|
|
@@ -245,6 +288,16 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
245
288
|
// label still names the field.
|
|
246
289
|
const labelledBy =
|
|
247
290
|
ariaLabel === undefined ? (ariaLabelledBy ?? (hasLabel ? labelId : undefined)) : undefined;
|
|
291
|
+
// While locked the field sits in the reason's Tooltip — in a fragment with a hidden
|
|
292
|
+
// copy of the reason, Button's anatomy; `block` so a full-width field stays full width.
|
|
293
|
+
const withLock = (box: ReactNode) =>
|
|
294
|
+
locked ? (
|
|
295
|
+
<LockedReason lock={lock} className="block">
|
|
296
|
+
{box}
|
|
297
|
+
</LockedReason>
|
|
298
|
+
) : (
|
|
299
|
+
box
|
|
300
|
+
);
|
|
248
301
|
|
|
249
302
|
return (
|
|
250
303
|
// `relative`: the floating label and the live region's `sr-only` both need a
|
|
@@ -257,120 +310,139 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
257
310
|
{label}
|
|
258
311
|
</ComboboxFieldLabel>
|
|
259
312
|
)}
|
|
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
|
-
|
|
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
|
-
// 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) {
|
|
322
|
-
e.preventDefault();
|
|
323
|
-
e.stopPropagation();
|
|
313
|
+
{/* `"hint" in props`: the family's 0.22 rule (see `endHintRowProps`) — passed at
|
|
314
|
+
all, even as `undefined`, a hint keeps the unlabelled field's end-hint row, so
|
|
315
|
+
a "?" coming and going never re-parents the <input> under the caret (0.24, the
|
|
316
|
+
last of the family to get it; the siblings had it in 0.23). */}
|
|
317
|
+
<EndHintRow {...endHintRowProps("hint" in props, hasLabel, hintParts.labelHint)}>
|
|
318
|
+
{withLock(
|
|
319
|
+
<div ref={fieldRef} className={cn("relative", locked && "cursor-not-allowed")}>
|
|
320
|
+
{icon && (
|
|
321
|
+
<span
|
|
322
|
+
aria-hidden
|
|
323
|
+
className={cn(
|
|
324
|
+
"pointer-events-none absolute flex text-[var(--text-muted)]",
|
|
325
|
+
small ? "start-2 [&>svg]:size-3.5" : "start-2.5 [&>svg]:size-4",
|
|
326
|
+
// Labelled, the icon sits on the VALUE's line — the one line box below
|
|
327
|
+
// FIELD_FLOATING_PAD's `pt-4` and the 1px border — not on the middle of
|
|
328
|
+
// the box. Centred on the box it rose into the top strip and sat on the
|
|
329
|
+
// label's first letters, which float at the same start inset. On the
|
|
330
|
+
// value line it is beside what it describes, and the label keeps the
|
|
331
|
+
// start edge every other label in the form's column shares.
|
|
332
|
+
hasLabel
|
|
333
|
+
? "top-[calc(1rem+1px)] h-5 items-center"
|
|
334
|
+
: "top-1/2 -translate-y-1/2",
|
|
335
|
+
)}
|
|
336
|
+
>
|
|
337
|
+
{icon}
|
|
338
|
+
</span>
|
|
339
|
+
)}
|
|
340
|
+
<input
|
|
341
|
+
{...rest}
|
|
342
|
+
ref={ref}
|
|
343
|
+
id={fieldId}
|
|
344
|
+
size={nativeSize}
|
|
345
|
+
type="text"
|
|
346
|
+
value={value}
|
|
347
|
+
// Locked: focusable, `readOnly`, `aria-disabled` — see `disabledReason`.
|
|
348
|
+
disabled={locked ? undefined : disabled}
|
|
349
|
+
readOnly={locked || rest.readOnly}
|
|
350
|
+
aria-disabled={locked || rest["aria-disabled"]}
|
|
351
|
+
role="combobox"
|
|
352
|
+
aria-autocomplete="list"
|
|
353
|
+
aria-expanded={expanded}
|
|
354
|
+
// Required by the role, and set while closed too, as on the siblings.
|
|
355
|
+
aria-controls={listboxId}
|
|
356
|
+
aria-activedescendant={activeId}
|
|
357
|
+
aria-label={ariaLabel}
|
|
358
|
+
aria-labelledby={labelledBy}
|
|
359
|
+
aria-invalid={field.isInvalid || rest["aria-invalid"] || undefined}
|
|
360
|
+
aria-describedby={mergeDescribedBy(field.describedBy, locked && lock.reasonId)}
|
|
361
|
+
autoComplete="off"
|
|
362
|
+
onFocus={(e) => {
|
|
363
|
+
onFocus?.(e);
|
|
364
|
+
// A locked field takes focus to say why, and opens nothing.
|
|
365
|
+
if (locked) return;
|
|
366
|
+
// A seeded field (keksdose's `initialQuery`) is looked up as it is
|
|
367
|
+
// focused; an empty one asks nothing, because of `minChars`.
|
|
368
|
+
setOpen(true);
|
|
369
|
+
}}
|
|
370
|
+
onBlur={(e) => {
|
|
371
|
+
onBlur?.(e);
|
|
324
372
|
close();
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
|
|
373
|
+
}}
|
|
374
|
+
onChange={(e) => {
|
|
375
|
+
if (locked) return;
|
|
376
|
+
onChange(e.target.value);
|
|
377
|
+
setOpen(true);
|
|
378
|
+
setActive(-1);
|
|
379
|
+
}}
|
|
380
|
+
onKeyDown={(e) => {
|
|
381
|
+
// Locked: Enter is swallowed (no form submit, no caller's "search now");
|
|
382
|
+
// every other key is the caller's and the caret's. No list to drive.
|
|
383
|
+
if (locked) {
|
|
384
|
+
if (e.key === "Enter") e.preventDefault();
|
|
385
|
+
else onKeyDown?.(e);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
// An Escape that closes the OPEN list is the list's, and is consumed before
|
|
389
|
+
// the caller sees it. A caller whose Escape means "close the panel" (keksdose's
|
|
390
|
+
// address search) otherwise closed the whole panel when the user only meant
|
|
391
|
+
// to dismiss the suggestions — the opposite of what the docs promised.
|
|
392
|
+
if (e.key === "Escape" && expanded) {
|
|
343
393
|
e.preventDefault();
|
|
344
|
-
|
|
394
|
+
e.stopPropagation();
|
|
395
|
+
close();
|
|
396
|
+
return;
|
|
345
397
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
398
|
+
onKeyDown?.(e);
|
|
399
|
+
if (e.defaultPrevented) return;
|
|
400
|
+
// Disabled rows are passed over; at either end the highlight stays put.
|
|
401
|
+
if (e.key === "ArrowDown") {
|
|
402
|
+
e.preventDefault();
|
|
403
|
+
if (!open) setOpen(true);
|
|
404
|
+
else setActive((i) => stepEnabled(results, i, 1));
|
|
405
|
+
} else if (e.key === "ArrowUp") {
|
|
406
|
+
e.preventDefault();
|
|
407
|
+
if (!open) setOpen(true);
|
|
408
|
+
// From "nothing highlighted", Up goes to the bottom, as the APG has it.
|
|
409
|
+
else setActive((i) => stepEnabled(results, i < 0 ? results.length : i, -1));
|
|
410
|
+
} else if (e.key === "Enter") {
|
|
411
|
+
// No row highlighted: the text is the answer, and a form's own submit
|
|
412
|
+
// is left alone.
|
|
413
|
+
if (expanded && activeId) {
|
|
414
|
+
e.preventDefault();
|
|
415
|
+
take(results[active]);
|
|
416
|
+
}
|
|
417
|
+
} else if (e.key === "Tab") {
|
|
418
|
+
close();
|
|
419
|
+
}
|
|
420
|
+
}}
|
|
365
421
|
className={cn(
|
|
366
|
-
|
|
367
|
-
|
|
422
|
+
FIELD_BASE,
|
|
423
|
+
hasLabel && FIELD_FLOATING_PAD,
|
|
424
|
+
// Select's `SELECT_SM` box: a fixed 28px, so the caller's line height
|
|
425
|
+
// cannot grow it.
|
|
426
|
+
small && "h-7 py-0 pe-2 ps-2 text-xs",
|
|
427
|
+
icon ? (small ? "ps-7" : "ps-8") : undefined,
|
|
428
|
+
busy && (small ? "pe-7" : "pe-9"),
|
|
429
|
+
field.isInvalid && FIELD_INVALID,
|
|
430
|
+
inputClassName,
|
|
368
431
|
)}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
432
|
+
/>
|
|
433
|
+
{busy && live && (
|
|
434
|
+
// Decorative: the live region below already says "Loading…".
|
|
435
|
+
<span
|
|
436
|
+
className={cn(
|
|
437
|
+
"pointer-events-none absolute top-1/2 flex -translate-y-1/2",
|
|
438
|
+
small ? "end-2" : "end-2.5",
|
|
439
|
+
)}
|
|
440
|
+
>
|
|
441
|
+
<Spinner label={null} className={small ? "h-3.5 w-3.5" : "h-4 w-4"} />
|
|
442
|
+
</span>
|
|
443
|
+
)}
|
|
444
|
+
</div>,
|
|
445
|
+
)}
|
|
374
446
|
</EndHintRow>
|
|
375
447
|
{/* With a "?" the label shares the top strip with it, after the field so the "?"
|
|
376
448
|
follows the control in the tab order (as on Input and Select). */}
|