@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
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { useId, useMemo, type ComponentPropsWithoutRef, type ReactNode } from "react";
|
|
1
|
+
import { useCallback, useId, useMemo, type ComponentPropsWithoutRef, type ReactNode, type Ref } from "react";
|
|
2
2
|
import { X } from "lucide-react";
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
import { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID } from "./ui";
|
|
5
5
|
import {
|
|
6
6
|
ComboboxPanel,
|
|
7
|
+
endHintRowProps,
|
|
8
|
+
offersCreate,
|
|
7
9
|
useComboboxCore,
|
|
8
10
|
useComboboxFieldError,
|
|
9
11
|
type ComboOption,
|
|
@@ -14,6 +16,17 @@ import {
|
|
|
14
16
|
useKitLabels,
|
|
15
17
|
useKitLocale,
|
|
16
18
|
} from "../i18n/kit-labels";
|
|
19
|
+
import {
|
|
20
|
+
EndHintRow,
|
|
21
|
+
FieldCaption,
|
|
22
|
+
LABEL_IN_ROW,
|
|
23
|
+
LockedReason,
|
|
24
|
+
StaticLabelRow,
|
|
25
|
+
useFieldHint,
|
|
26
|
+
useLockReason,
|
|
27
|
+
} from "./field-parts";
|
|
28
|
+
import { assignRef, mergeDescribedBy } from "./choice-parts";
|
|
29
|
+
import { useCommitReason } from "./write-lock";
|
|
17
30
|
|
|
18
31
|
/** `onChange` is the kit's — "a selection was made", carrying values — rather
|
|
19
32
|
* than the div's form event, so the DOM's spelling of it is omitted. */
|
|
@@ -35,6 +48,9 @@ export interface MultiEntityComboboxProps<V extends string | number>
|
|
|
35
48
|
/** Trigger summary when 1+ selected (default: the labels as a locale-formatted
|
|
36
49
|
* list, or `combobox.selectedCount` when a label can't be resolved). */
|
|
37
50
|
itemLabel?: (count: number) => string;
|
|
51
|
+
/** Offer a clear "×" while anything is chosen; it emits `[]`. A pointer shortcut out
|
|
52
|
+
* of the tab order — the keyboard's way is Delete or Backspace on the CLOSED trigger,
|
|
53
|
+
* or unticking rows in the open list. Not offered while `disabled` or locked. */
|
|
38
54
|
clearable?: boolean;
|
|
39
55
|
clearLabel?: string;
|
|
40
56
|
/** The phone sheet's close button. Its own prop rather than a reuse of
|
|
@@ -43,15 +59,37 @@ export interface MultiEntityComboboxProps<V extends string | number>
|
|
|
43
59
|
* so it is the one control here that MUST be in the reader's language. */
|
|
44
60
|
closeLabel?: string;
|
|
45
61
|
disabled?: boolean;
|
|
62
|
+
/** Why the selection cannot be changed — {@link EntityCombobox}'s `disabledReason`:
|
|
63
|
+
* focusable and `aria-disabled`, no list, no clear, no `onChange`, the reason in the
|
|
64
|
+
* kit {@link Tooltip}. Wins over `disabled`. */
|
|
65
|
+
disabledReason?: ReactNode;
|
|
66
|
+
/** This picker COMMITS — toggling saves. Locked the `disabledReason` way under a
|
|
67
|
+
* locked {@link WriteLockProvider}. See {@link EntityCombobox}'s `commit`. */
|
|
68
|
+
commit?: boolean;
|
|
46
69
|
/** When set, a "create" row appears for a non-empty query with no exact match.
|
|
47
70
|
* The panel stays open (adding the new entity to `value` is the caller's job). */
|
|
48
71
|
onCreate?: (query: string) => void;
|
|
49
72
|
createLabel?: (query: string) => string;
|
|
73
|
+
/** Offer the create row with nothing typed too, worded as this. See
|
|
74
|
+
* {@link EntityCombobox}'s `createEmptyLabel`. */
|
|
75
|
+
createEmptyLabel?: string;
|
|
76
|
+
/** The create row is a write while toggling is not: locked on its own under a locked
|
|
77
|
+
* {@link WriteLockProvider}. See {@link EntityCombobox}'s `createCommit`. */
|
|
78
|
+
createCommit?: boolean;
|
|
50
79
|
/** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */
|
|
51
80
|
invalid?: boolean;
|
|
52
81
|
/** What is wrong with the value, as {@link Input}'s `error`: rendered under the
|
|
53
82
|
* field, on the trigger's `aria-describedby`, and implies `invalid`. */
|
|
54
83
|
error?: ReactNode;
|
|
84
|
+
/**
|
|
85
|
+
* Standing advice — the rest of the Combobox family's `hint` since 0.22, which this
|
|
86
|
+
* one was left without (keksdose K4): plain TEXT is a caption under the field on the
|
|
87
|
+
* trigger's `aria-describedby`, before any error; a {@link FieldHint} "?" rides the
|
|
88
|
+
* label line, or with no label sits at the trigger's end edge, outside the box.
|
|
89
|
+
* Passed at all — even `undefined` — the field keeps one box, so a hint coming and
|
|
90
|
+
* going never rebuilds the trigger under the cursor (the 0.22 rule).
|
|
91
|
+
*/
|
|
92
|
+
hint?: ReactNode;
|
|
55
93
|
/** Narrow `options` client-side by the query. Default `true`; `false` shows them
|
|
56
94
|
* as given (a server-ranked list). */
|
|
57
95
|
filter?: boolean;
|
|
@@ -62,6 +100,10 @@ export interface MultiEntityComboboxProps<V extends string | number>
|
|
|
62
100
|
debounceMs?: number;
|
|
63
101
|
/** Shown when `loadOptions` rejects. Default: `combobox.loadError`. */
|
|
64
102
|
loadErrorLabel?: string;
|
|
103
|
+
/** The TRIGGER — the focusable `<button>` — as a React 19 ref prop, as on
|
|
104
|
+
* {@link EntityCombobox} and {@link CountrySelect} (0.24, kastlan: react-hook-form's
|
|
105
|
+
* focus-on-error focuses whatever `field.ref` is handed). Not the wrapper. */
|
|
106
|
+
ref?: Ref<HTMLButtonElement>;
|
|
65
107
|
}
|
|
66
108
|
|
|
67
109
|
/**
|
|
@@ -69,42 +111,49 @@ export interface MultiEntityComboboxProps<V extends string | number>
|
|
|
69
111
|
* with the same async `loadOptions`/`onCreate` support. Rows show a checkbox and
|
|
70
112
|
* toggle without closing the panel; the trigger summarises the selection.
|
|
71
113
|
*/
|
|
72
|
-
export function MultiEntityCombobox<V extends string | number>({
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
114
|
+
export function MultiEntityCombobox<V extends string | number>(props: MultiEntityComboboxProps<V>) {
|
|
115
|
+
const {
|
|
116
|
+
value,
|
|
117
|
+
onChange,
|
|
118
|
+
options,
|
|
119
|
+
loadOptions,
|
|
120
|
+
loading,
|
|
121
|
+
label,
|
|
122
|
+
placeholder,
|
|
123
|
+
searchPlaceholder,
|
|
124
|
+
emptyLabel,
|
|
125
|
+
itemLabel,
|
|
126
|
+
clearable,
|
|
127
|
+
clearLabel,
|
|
128
|
+
closeLabel,
|
|
129
|
+
disabled,
|
|
130
|
+
disabledReason,
|
|
131
|
+
commit,
|
|
132
|
+
onCreate,
|
|
133
|
+
createLabel,
|
|
134
|
+
createEmptyLabel,
|
|
135
|
+
createCommit,
|
|
136
|
+
className,
|
|
137
|
+
invalid,
|
|
138
|
+
error,
|
|
139
|
+
hint,
|
|
140
|
+
filter,
|
|
141
|
+
minChars,
|
|
142
|
+
debounceMs,
|
|
143
|
+
loadErrorLabel,
|
|
144
|
+
ref,
|
|
145
|
+
"aria-label": ariaLabel,
|
|
146
|
+
// The control's own wiring, taken off `rest` so it lands on the TRIGGER rather than
|
|
147
|
+
// the wrapper: `Field`'s render-prop spreads `{ id, aria-describedby, aria-invalid,
|
|
148
|
+
// aria-required }`, and on the role-less wrapper div the label's `htmlFor` named
|
|
149
|
+
// nothing and the hint and required state described nothing — kastlan's
|
|
150
|
+
// communication-page To/Cc/Bcc and lease-tenants-step.tsx read as unnamed pickers.
|
|
151
|
+
id,
|
|
152
|
+
"aria-describedby": ariaDescribedBy,
|
|
153
|
+
"aria-invalid": ariaInvalid,
|
|
154
|
+
"aria-required": ariaRequired,
|
|
155
|
+
...rest
|
|
156
|
+
} = props;
|
|
108
157
|
const core = useComboboxCore<V>({
|
|
109
158
|
options,
|
|
110
159
|
loadOptions,
|
|
@@ -113,10 +162,11 @@ export function MultiEntityCombobox<V extends string | number>({
|
|
|
113
162
|
minChars,
|
|
114
163
|
debounceMs,
|
|
115
164
|
});
|
|
165
|
+
const hintParts = useFieldHint(hint, ariaDescribedBy);
|
|
116
166
|
const field = useComboboxFieldError(
|
|
117
167
|
error,
|
|
118
168
|
invalid || ariaInvalid === true || ariaInvalid === "true",
|
|
119
|
-
|
|
169
|
+
hintParts.describedBy,
|
|
120
170
|
);
|
|
121
171
|
// The props are the per-instance overrides, the provider the app-wide ones; a
|
|
122
172
|
// prop left `undefined` falls through to the provider rather than masking it.
|
|
@@ -128,6 +178,19 @@ export function MultiEntityCombobox<V extends string | number>({
|
|
|
128
178
|
});
|
|
129
179
|
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
130
180
|
const { open, results, resolve, setOpen, query, triggerRef } = core;
|
|
181
|
+
// The core's trigger ref and the caller's, fed by one callback — see EntityCombobox.
|
|
182
|
+
const setTrigger = useCallback(
|
|
183
|
+
(el: HTMLButtonElement | null) => {
|
|
184
|
+
triggerRef.current = el;
|
|
185
|
+
assignRef(ref, el);
|
|
186
|
+
},
|
|
187
|
+
[ref, triggerRef],
|
|
188
|
+
);
|
|
189
|
+
// The lock — see EntityCombobox: the trigger stays reachable and says why.
|
|
190
|
+
const lock = useLockReason(commit, disabledReason);
|
|
191
|
+
const inert = lock.locked || Boolean(disabled);
|
|
192
|
+
if (lock.locked && open) setOpen(false);
|
|
193
|
+
const createReason = useCommitReason(createCommit, undefined);
|
|
131
194
|
// One id per instance, generated here rather than in the core: `aria-controls` on
|
|
132
195
|
// the trigger has to name the list while the list is still closed, so the id
|
|
133
196
|
// belongs to whoever renders both ends of it.
|
|
@@ -154,12 +217,27 @@ export function MultiEntityCombobox<V extends string | number>({
|
|
|
154
217
|
: labels.selectedCount(value.length);
|
|
155
218
|
})();
|
|
156
219
|
|
|
220
|
+
// While locked the trigger sits in the reason's Tooltip, as on EntityCombobox.
|
|
221
|
+
const withLock = (trigger: ReactNode) =>
|
|
222
|
+
lock.locked ? (
|
|
223
|
+
<LockedReason lock={lock} className="block">
|
|
224
|
+
{trigger}
|
|
225
|
+
</LockedReason>
|
|
226
|
+
) : (
|
|
227
|
+
trigger
|
|
228
|
+
);
|
|
229
|
+
|
|
157
230
|
const q = query.trim();
|
|
158
|
-
const showCreate =
|
|
159
|
-
|
|
160
|
-
|
|
231
|
+
const showCreate = offersCreate(
|
|
232
|
+
onCreate,
|
|
233
|
+
q,
|
|
234
|
+
results.map((o) => o.label),
|
|
235
|
+
createEmptyLabel,
|
|
236
|
+
);
|
|
237
|
+
const showClear = Boolean(clearable && value.length > 0 && !inert);
|
|
161
238
|
|
|
162
239
|
const toggle = (o: ComboOption<V>) => {
|
|
240
|
+
if (lock.locked) return;
|
|
163
241
|
core.rememberOption(o);
|
|
164
242
|
if (valueSet.has(o.value)) onChange(value.filter((v) => v !== o.value));
|
|
165
243
|
else onChange([...value, o.value]);
|
|
@@ -172,88 +250,107 @@ export function MultiEntityCombobox<V extends string | number>({
|
|
|
172
250
|
// has one. Spread FIRST so the trigger's ARIA and the
|
|
173
251
|
// handlers that open the panel cannot be clobbered from outside.
|
|
174
252
|
<div {...rest} className={cn("relative", className)}>
|
|
175
|
-
{label !== undefined && <FieldLabel>{label}</FieldLabel>}
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
className={cn(
|
|
227
|
-
"min-w-0 truncate",
|
|
228
|
-
// Inherit FIELD_BASE's ink for a value, keep the placeholder tone
|
|
229
|
-
// (`--text-placeholder`) — the same rule every field on a form row
|
|
230
|
-
// follows (Keksdose dev#477).
|
|
231
|
-
!value.length && "text-[var(--text-placeholder)]",
|
|
232
|
-
)}
|
|
233
|
-
>
|
|
234
|
-
{summary}
|
|
235
|
-
</span>
|
|
236
|
-
{showClear ? (
|
|
237
|
-
/* eslint-disable-next-line jsx-a11y/click-events-have-key-events -- tabIndex -1 inside
|
|
238
|
-
the trigger <button>, so it never holds focus and a key handler here could
|
|
239
|
-
never fire. A pointer shortcut; the keyboard path is unticking the options
|
|
240
|
-
in the open list. */
|
|
241
|
-
<span
|
|
242
|
-
role="button"
|
|
243
|
-
tabIndex={-1}
|
|
244
|
-
aria-label={labels.clear}
|
|
245
|
-
onClick={(e) => {
|
|
246
|
-
e.stopPropagation();
|
|
247
|
-
onChange([]);
|
|
253
|
+
{label !== undefined && hintParts.labelHint === undefined && <FieldLabel>{label}</FieldLabel>}
|
|
254
|
+
<EndHintRow {...endHintRowProps("hint" in props, label !== undefined, hintParts.labelHint)}>
|
|
255
|
+
{withLock(
|
|
256
|
+
<button
|
|
257
|
+
ref={setTrigger}
|
|
258
|
+
id={id}
|
|
259
|
+
type="button"
|
|
260
|
+
// A combobox, not a button. The distinction is not pedantry: this control
|
|
261
|
+
// carried `aria-invalid`, which `button` does not support, so a required
|
|
262
|
+
// field left empty painted a rose border and told a reader nothing at all —
|
|
263
|
+
// and ESLint flagged it as exactly that (`role-supports-aria-props`). The
|
|
264
|
+
// fix the audit asked for is the role that describes what this IS: a closed
|
|
265
|
+
// choice that expands into the list named below. `combobox` supports
|
|
266
|
+
// `aria-invalid`, so the border and the announcement finally agree.
|
|
267
|
+
role="combobox"
|
|
268
|
+
aria-expanded={open}
|
|
269
|
+
aria-controls={listboxId}
|
|
270
|
+
aria-haspopup="listbox"
|
|
271
|
+
// The label is a floating <span>, not a <label for>, so without this the
|
|
272
|
+
// trigger's accessible name is whatever value happens to be selected —
|
|
273
|
+
// "Checking" with nothing saying it is the account. The label AND the
|
|
274
|
+
// value, because `aria-label` replaces the content rather than adding to
|
|
275
|
+
// it, and a control that announces only its name has lost the answer.
|
|
276
|
+
// A caller's own name wins over the composition: two fields labelled
|
|
277
|
+
// "Account" on a transfer form are the from and the to, and only the
|
|
278
|
+
// caller knows which is which.
|
|
279
|
+
aria-label={
|
|
280
|
+
ariaLabel ??
|
|
281
|
+
(typeof label === "string" ? common.fieldValue(label, summary) : undefined)
|
|
282
|
+
}
|
|
283
|
+
// Focusable while locked — see EntityCombobox.
|
|
284
|
+
disabled={lock.locked ? undefined : disabled}
|
|
285
|
+
aria-disabled={lock.locked || undefined}
|
|
286
|
+
aria-invalid={field.isInvalid || undefined}
|
|
287
|
+
aria-describedby={mergeDescribedBy(field.describedBy, lock.locked && lock.reasonId)}
|
|
288
|
+
aria-required={ariaRequired}
|
|
289
|
+
onClick={() => !inert && setOpen((o) => !o)}
|
|
290
|
+
// Down/Up opens the list from the closed trigger, per the APG. Enter and
|
|
291
|
+
// Space already do it through the button's own click.
|
|
292
|
+
onKeyDown={(e) => {
|
|
293
|
+
if (inert) return;
|
|
294
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
295
|
+
e.preventDefault();
|
|
296
|
+
setOpen(true);
|
|
297
|
+
} else if (showClear && !open && (e.key === "Delete" || e.key === "Backspace")) {
|
|
298
|
+
// The keyboard's clear: the "×" is a pointer shortcut out of the tab
|
|
299
|
+
// order, so without this a cleared-by-mouse field was set-only from the
|
|
300
|
+
// keyboard. The closed trigger only — CountrySelect's rule (0.23).
|
|
301
|
+
e.preventDefault();
|
|
302
|
+
onChange([]);
|
|
303
|
+
}
|
|
248
304
|
}}
|
|
249
|
-
className=
|
|
305
|
+
className={cn(
|
|
306
|
+
FIELD_TRIGGER,
|
|
307
|
+
"pe-9",
|
|
308
|
+
label !== undefined && FIELD_FLOATING_PAD,
|
|
309
|
+
inert && "cursor-not-allowed opacity-50",
|
|
310
|
+
field.isInvalid && FIELD_INVALID,
|
|
311
|
+
)}
|
|
250
312
|
>
|
|
251
|
-
<
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
313
|
+
<span
|
|
314
|
+
className={cn(
|
|
315
|
+
"min-w-0 truncate",
|
|
316
|
+
// Inherit FIELD_BASE's ink for a value, keep the placeholder tone
|
|
317
|
+
// (`--text-placeholder`) — the same rule every field on a form row
|
|
318
|
+
// follows (Keksdose dev#477).
|
|
319
|
+
!value.length && "text-[var(--text-placeholder)]",
|
|
320
|
+
)}
|
|
321
|
+
>
|
|
322
|
+
{summary}
|
|
323
|
+
</span>
|
|
324
|
+
{showClear ? (
|
|
325
|
+
/* eslint-disable-next-line jsx-a11y/click-events-have-key-events -- tabIndex -1 inside
|
|
326
|
+
the trigger <button>, so it never holds focus and a key handler here could
|
|
327
|
+
never fire. A pointer shortcut; the keyboard paths are Delete on the closed
|
|
328
|
+
trigger and unticking the options in the open list. */
|
|
329
|
+
<span
|
|
330
|
+
role="button"
|
|
331
|
+
tabIndex={-1}
|
|
332
|
+
aria-label={labels.clear}
|
|
333
|
+
onClick={(e) => {
|
|
334
|
+
e.stopPropagation();
|
|
335
|
+
onChange([]);
|
|
336
|
+
}}
|
|
337
|
+
className="absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]"
|
|
338
|
+
>
|
|
339
|
+
<X className="size-4" />
|
|
340
|
+
</span>
|
|
341
|
+
) : (
|
|
342
|
+
<FieldChevron />
|
|
343
|
+
)}
|
|
344
|
+
</button>,
|
|
255
345
|
)}
|
|
256
|
-
</
|
|
346
|
+
</EndHintRow>
|
|
347
|
+
{/* With a "?" the label shares the top strip with it — after the trigger, so the
|
|
348
|
+
"?" follows the control in the tab order, as on EntityCombobox. */}
|
|
349
|
+
{label !== undefined && hintParts.labelHint !== undefined && (
|
|
350
|
+
<StaticLabelRow hint={hintParts.labelHint}>
|
|
351
|
+
<FieldLabel className={LABEL_IN_ROW}>{label}</FieldLabel>
|
|
352
|
+
</StaticLabelRow>
|
|
353
|
+
)}
|
|
257
354
|
<ComboboxPanel
|
|
258
355
|
core={core}
|
|
259
356
|
listboxId={listboxId}
|
|
@@ -269,11 +366,14 @@ export function MultiEntityCombobox<V extends string | number>({
|
|
|
269
366
|
onChoose={toggle}
|
|
270
367
|
showCreate={showCreate}
|
|
271
368
|
onCreate={() => {
|
|
369
|
+
if (lock.locked) return;
|
|
272
370
|
onCreate?.(q);
|
|
273
371
|
core.setQuery("");
|
|
274
372
|
}}
|
|
275
|
-
createContent={labels.create(q)}
|
|
373
|
+
createContent={q ? labels.create(q) : createEmptyLabel}
|
|
374
|
+
createDisabledReason={createReason}
|
|
276
375
|
/>
|
|
376
|
+
<FieldCaption parts={hintParts} />
|
|
277
377
|
{field.errorEl}
|
|
278
378
|
</div>
|
|
279
379
|
);
|
|
@@ -2,10 +2,10 @@ import { useContext, useEffect, useId, useRef, useState } from "react";
|
|
|
2
2
|
import type { KeyboardEvent, ReactNode } from "react";
|
|
3
3
|
|
|
4
4
|
import { useKitLabels } from "../i18n/kit-labels";
|
|
5
|
-
import { usePromisePending } from "./danger-confirm";
|
|
5
|
+
import { CurrentPasswordInput, usePromisePending } from "./danger-confirm";
|
|
6
6
|
import { DialogFrame } from "./dialog-frame";
|
|
7
7
|
import { ModalCloseContext } from "./modal";
|
|
8
|
-
import { Button,
|
|
8
|
+
import { Button, Spinner } from "./ui";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Every string the dialog renders — the `reauthDialog` namespace of
|
|
@@ -85,8 +85,8 @@ export interface ReauthDialogProps {
|
|
|
85
85
|
* `requirePassword` tile. Both are the same three facts — the field is the CURRENT
|
|
86
86
|
* password (the password manager fills it, never generates one), a wrong one stays in
|
|
87
87
|
* the dialog with its error, and the APP verifies it — so the promise handling is
|
|
88
|
-
* `DangerConfirm`'s own, shared, the field is its `CurrentPasswordInput`
|
|
89
|
-
*
|
|
88
|
+
* `DangerConfirm`'s own, shared, the field is its `CurrentPasswordInput` with an
|
|
89
|
+
* `error`, and this is the modal form of them.
|
|
90
90
|
*
|
|
91
91
|
* A component rather than a `useReauth()` promise: the step that follows the check
|
|
92
92
|
* (Kurvenschmiede's WebAuthn ceremony) needs what the server answered, and the error
|
|
@@ -201,24 +201,23 @@ export function ReauthDialog({
|
|
|
201
201
|
submit();
|
|
202
202
|
}}
|
|
203
203
|
>
|
|
204
|
-
{/*
|
|
205
|
-
password
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
204
|
+
{/* The kit's CurrentPasswordInput (0.23; it drew the same field by hand until
|
|
205
|
+
then): the password manager fills it with the CURRENT password and never
|
|
206
|
+
offers to generate one; `busy` makes it `readOnly`, not `disabled`, because
|
|
207
|
+
disabling the field that has focus drops focus to <body>. Labelled by this
|
|
208
|
+
namespace's `password` ("Current password"), not `dangerConfirm.password`.
|
|
209
|
+
The `error` is Input's own: it paints the field, sits under it and is its
|
|
210
|
+
`aria-describedby`; passing the prop on every render keeps the field's box
|
|
211
|
+
steady, so the message going away on the first keystroke of a retry does not
|
|
212
|
+
remount the <input> under the typist. */}
|
|
213
|
+
<CurrentPasswordInput
|
|
213
214
|
ref={inputRef}
|
|
214
|
-
type="password"
|
|
215
|
-
autoComplete="current-password"
|
|
216
215
|
label={labels.password}
|
|
217
216
|
value={password}
|
|
218
|
-
|
|
217
|
+
busy={busy}
|
|
219
218
|
error={shownError}
|
|
220
|
-
|
|
221
|
-
setPassword(
|
|
219
|
+
onValueChange={(next) => {
|
|
220
|
+
setPassword(next);
|
|
222
221
|
setEdited(true);
|
|
223
222
|
}}
|
|
224
223
|
/>
|
|
@@ -6,7 +6,7 @@ import { TILE_SIZE, TileRadioGroup } from "./tile-radio";
|
|
|
6
6
|
import type { TileItem, TileSize } from "./tile-radio";
|
|
7
7
|
import { Tooltip } from "./tooltip";
|
|
8
8
|
import { useCommitReason } from "./write-lock";
|
|
9
|
-
import { hasContent, LabelStrip } from "./field-
|
|
9
|
+
import { hasContent, LabelStrip } from "./field-parts";
|
|
10
10
|
|
|
11
11
|
export interface SwatchPickerLabels {
|
|
12
12
|
/** The "no colour" tile's name, when `allowNone` is set. */
|
|
@@ -38,7 +38,23 @@ export interface SwatchOption<T extends string> {
|
|
|
38
38
|
/** A remark read after the name and shown under it in the bubble ("used by
|
|
39
39
|
* Groceries"). The tile wears a dot while it has one. */
|
|
40
40
|
note?: string;
|
|
41
|
+
/** Out of the choice and out of reach — skipped by the arrow keys and Tab. With a
|
|
42
|
+
* `disabledReason` the tile stays reachable instead and says why. */
|
|
41
43
|
disabled?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Why THIS colour cannot be chosen right now — the whole picker's `disabledReason`,
|
|
46
|
+
* one tile at a time (the kit's later list after keksdose K3: a colour another
|
|
47
|
+
* category already wears, a flag only an owner may set). {@link Button}'s rule: a tile
|
|
48
|
+
* that is merely `disabled` is a dead square a keyboard never lands on, so nobody
|
|
49
|
+
* learns why it is dimmed.
|
|
50
|
+
*
|
|
51
|
+
* The tile looks disabled but is `aria-disabled`, not `disabled`: Tab and the arrow
|
|
52
|
+
* keys still reach it, its bubble shows the reason under its name, and the reason
|
|
53
|
+
* describes it. Choosing it — a click, Space or Enter, or an arrow under automatic
|
|
54
|
+
* activation — does nothing: the focus moves onto it and the checked colour stays
|
|
55
|
+
* where it was, as under the whole-picker lock. Wins over `disabled`.
|
|
56
|
+
*/
|
|
57
|
+
disabledReason?: ReactNode;
|
|
42
58
|
}
|
|
43
59
|
|
|
44
60
|
/**
|
|
@@ -142,16 +158,24 @@ export function SwatchPicker<T extends string>({
|
|
|
142
158
|
const byValue = useMemo(() => new Map(options.map((o) => [o.value, o])), [options]);
|
|
143
159
|
const items: TileItem<T>[] = [
|
|
144
160
|
...(allowNone ? [{ key: null, label: text.none }] : []),
|
|
145
|
-
...options.map((o) => ({
|
|
161
|
+
...options.map((o) => ({
|
|
162
|
+
key: o.value,
|
|
163
|
+
label: o.label,
|
|
164
|
+
note: o.note,
|
|
165
|
+
disabled: o.disabled,
|
|
166
|
+
disabledReason: o.disabledReason,
|
|
167
|
+
})),
|
|
146
168
|
];
|
|
147
169
|
const tiles = (
|
|
148
170
|
<TileRadioGroup
|
|
149
171
|
items={items}
|
|
150
172
|
checked={mixed ? undefined : value}
|
|
151
173
|
// A locked picker saves on change, so the change is swallowed — the arrow keys
|
|
152
|
-
// still move the focus (and, automatic, would choose), and nothing is chosen.
|
|
174
|
+
// still move the focus (and, automatic, would choose), and nothing is chosen. A
|
|
175
|
+
// locked TILE is refused by the tiles themselves; asked again here so a reason
|
|
176
|
+
// can never become a change, whatever the tiles do.
|
|
153
177
|
onSelect={(next) => {
|
|
154
|
-
if (!locked) onChange(next);
|
|
178
|
+
if (!locked && !hasContent(next === null ? undefined : byValue.get(next)?.disabledReason)) onChange(next);
|
|
155
179
|
}}
|
|
156
180
|
activation={activation}
|
|
157
181
|
// A reason wins over `disabled`: the tiles stay in the tab order.
|
|
@@ -235,7 +259,7 @@ export function SwatchPicker<T extends string>({
|
|
|
235
259
|
);
|
|
236
260
|
if (!labelled) return group;
|
|
237
261
|
return (
|
|
238
|
-
<LabelStrip labelId={labelId} label={label} hint={hint} disabled={disabled || locked} pad="
|
|
262
|
+
<LabelStrip labelId={labelId} label={label} hint={hint} disabled={disabled || locked} pad="clear" className={className}>
|
|
239
263
|
{group}
|
|
240
264
|
</LabelStrip>
|
|
241
265
|
);
|