@eifi1/ui-kit 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -25
- package/dist/chart.d.ts +11 -8
- package/dist/components/amount-input.d.ts +28 -8
- package/dist/components/amount-input.js +3 -1
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +21 -0
- package/dist/components/autocomplete.js +128 -103
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +11 -8
- package/dist/components/calculator.d.ts +11 -8
- package/dist/components/column-mapper.d.ts +182 -0
- package/dist/components/column-mapper.js +375 -0
- package/dist/components/column-mapper.js.map +1 -0
- package/dist/components/combobox-core.d.ts +39 -2
- package/dist/components/combobox-core.js +29 -6
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +80 -2
- package/dist/components/combobox.js +448 -311
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +11 -8
- package/dist/components/confirm-dialog.js +7 -8
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.d.ts +8 -5
- 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 +11 -8
- package/dist/components/danger-confirm.js +13 -3
- 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 +35 -1
- package/dist/components/entity-combobox.js +148 -112
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +11 -8
- 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 +11 -8
- package/dist/components/file-dropzone.d.ts +11 -8
- package/dist/components/form-actions.d.ts +10 -7
- package/dist/components/form-actions.js +8 -2
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/iban-input.d.ts +11 -8
- 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 +11 -8
- package/dist/components/money-field.d.ts +14 -8
- 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 +26 -1
- package/dist/components/multi-entity-combobox.js +143 -102
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/number-field.d.ts +11 -8
- package/dist/components/number-input.d.ts +11 -8
- package/dist/components/numpad-sheet.d.ts +11 -8
- package/dist/components/phone-input.d.ts +11 -8
- 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 +11 -8
- package/dist/components/settings-fields.d.ts +11 -8
- package/dist/components/share-card.d.ts +10 -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 +10 -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 +11 -8
- 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 +8 -5
- package/dist/components/ui.js +5 -11
- package/dist/components/ui.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +1 -1
- package/dist/feedback/feedback-attachment.js +156 -56
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +1 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +3 -2
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +68 -287
- package/dist/feedback/feedback-thread.js +8 -1
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/{kit-labels-R8mIAc9N.d.ts → feedback-BxeQVzwq.d.ts} +405 -22
- package/dist/{feedback-attachment-NhUm7Zga.d.ts → feedback-attachment-fGAzZPf0.d.ts} +112 -10
- package/dist/feedback.d.ts +66 -2
- package/dist/hooks/use-file-drop.d.ts +11 -8
- package/dist/i18n/defaults.d.ts +11 -8
- package/dist/i18n/defaults.js +3 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +11 -8
- package/dist/i18n/german.js +36 -2
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +7 -4
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +11 -8
- package/dist/i18n/locales/de-CH.d.ts +11 -8
- package/dist/i18n/locales/en.d.ts +11 -8
- package/dist/i18n/locales/en.js +7 -0
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +11 -8
- package/dist/i18n/locales/es.js +37 -2
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +11 -8
- package/dist/i18n/locales/fr.js +36 -2
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +11 -8
- package/dist/i18n/locales/hu.js +38 -2
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +11 -8
- package/dist/i18n/locales/it.js +37 -2
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +11 -8
- package/dist/i18n/locales/zh.js +34 -2
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +11 -8
- package/dist/i18n/review.js +20 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +7 -3
- 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/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 +89 -11
- package/dist/rhf/fields.js +124 -0
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +11 -8
- package/dist/rhf.d.ts +12 -9
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +10 -7
- package/dist/shell/top-bar-brand.d.ts +11 -8
- 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 +10 -7
- package/dist/table-text.d.ts +1 -1
- package/dist/wizard/stepper-nav.d.ts +41 -10
- package/dist/wizard/stepper-nav.js +4 -0
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard.d.ts +11 -8
- package/package.json +1 -1
- package/src/components/amount-input.tsx +20 -1
- package/src/components/autocomplete.tsx +172 -111
- package/src/components/column-mapper.tsx +666 -0
- package/src/components/combobox-core.tsx +82 -9
- package/src/components/combobox.tsx +614 -332
- package/src/components/confirm-dialog.tsx +12 -8
- package/src/components/country-select.tsx +135 -22
- package/src/components/danger-confirm.tsx +91 -12
- package/src/components/date-picker.tsx +423 -10
- package/src/components/entity-combobox.tsx +217 -126
- package/src/components/field-parts.tsx +224 -5
- package/src/components/field-strip.tsx +149 -0
- package/src/components/form-actions.tsx +32 -4
- 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 +207 -120
- 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 +7 -33
- package/src/feedback/feedback-attachment.tsx +308 -61
- package/src/feedback/feedback-dialog.tsx +7 -3
- package/src/feedback/feedback-inbox.tsx +3 -2
- package/src/feedback/feedback-thread.tsx +47 -4
- 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/table-text.ts +271 -0
- package/src/rhf/fields.tsx +254 -0
- 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/src/components/field-anatomy.tsx +0 -190
|
@@ -2,10 +2,10 @@ import { createContext, useCallback, useContext, useEffect, useRef, useState } f
|
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
3
|
|
|
4
4
|
import { useKitLabels } from "../i18n/kit-labels";
|
|
5
|
-
import { typedMatches } from "./danger-confirm";
|
|
5
|
+
import { TypedConfirmField, typedMatches } from "./danger-confirm";
|
|
6
6
|
import type { TypedMatch } from "./danger-confirm";
|
|
7
7
|
import { DialogFrame } from "./dialog-frame";
|
|
8
|
-
import { Button
|
|
8
|
+
import { Button } from "./ui";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* The `confirmDialog` namespace of `<UiKitProvider labels>`: the two buttons' fallback
|
|
@@ -300,15 +300,19 @@ function ConfirmDialogView({
|
|
|
300
300
|
confirmRef.current?.click();
|
|
301
301
|
}}
|
|
302
302
|
>
|
|
303
|
-
|
|
303
|
+
{/* The kit's one typed-confirmation field (0.23), DangerConfirm's too: the
|
|
304
|
+
keyboard told to leave the text alone, `data-matched` on the input. The
|
|
305
|
+
label is this namespace's (`confirmDialog.typed`), and `placeholder=""`
|
|
306
|
+
keeps out the `dangerConfirm.phrasePlaceholder` a provider may set for
|
|
307
|
+
the tile — this field never had one. */}
|
|
308
|
+
<TypedConfirmField
|
|
304
309
|
ref={typedRef}
|
|
310
|
+
target={target}
|
|
311
|
+
match={request.typedMatch ?? "caseless"}
|
|
305
312
|
label={typedLabel}
|
|
313
|
+
placeholder=""
|
|
306
314
|
value={typed}
|
|
307
|
-
|
|
308
|
-
autoCapitalize="off"
|
|
309
|
-
autoCorrect="off"
|
|
310
|
-
spellCheck={false}
|
|
311
|
-
onChange={(e) => setTyped(e.target.value)}
|
|
315
|
+
onValueChange={setTyped}
|
|
312
316
|
/>
|
|
313
317
|
</form>
|
|
314
318
|
)}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { useId, useMemo } from "react";
|
|
2
|
-
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
1
|
+
import { useCallback, useId, useMemo } from "react";
|
|
2
|
+
import type { ComponentPropsWithoutRef, ReactNode, Ref } from "react";
|
|
3
|
+
import { X } from "lucide-react";
|
|
3
4
|
import { cn } from "../lib/cn";
|
|
4
5
|
import { COUNTRY_CODES, countryName, normalizeCountryCode } from "../lib/countries";
|
|
5
6
|
import { FieldChevron, FIELD_FLOATING_PAD, FIELD_INVALID, FIELD_TRIGGER, FloatingField } from "./ui";
|
|
@@ -11,9 +12,14 @@ import {
|
|
|
11
12
|
useComboboxFieldError,
|
|
12
13
|
type ComboOption,
|
|
13
14
|
} from "./combobox-core";
|
|
14
|
-
import { hasMessage, mergeDescribedBy } from "./choice-parts";
|
|
15
|
+
import { assignRef, hasMessage, mergeDescribedBy } from "./choice-parts";
|
|
15
16
|
import { useCommitReason } from "./write-lock";
|
|
16
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_COMBOBOX_LABELS,
|
|
19
|
+
DEFAULT_COMMON_LABELS,
|
|
20
|
+
useKitLabels,
|
|
21
|
+
useKitLocale,
|
|
22
|
+
} from "../i18n/kit-labels";
|
|
17
23
|
|
|
18
24
|
// ── Labels ────────────────────────────────────────────────────────────────────
|
|
19
25
|
|
|
@@ -44,15 +50,46 @@ export const DEFAULT_COUNTRY_SELECT_LABELS: CountrySelectLabels = {
|
|
|
44
50
|
* `onChange` is the kit's — "a country was picked", carrying its code — rather than
|
|
45
51
|
* the div's form event. `id` and the `aria-*` wiring below go to the TRIGGER, which is
|
|
46
52
|
* the control a reader meets; everything else in `rest` dresses the wrapper.
|
|
53
|
+
*
|
|
54
|
+
* `Clearable` follows `clearable`, the way {@link EntityCombobox}'s `C` follows its
|
|
55
|
+
* `clearValue`: it is inferred from the prop, so `onChange` is `(code: string) => void`
|
|
56
|
+
* for the field as it always was (`onChange={setCountry}` over a `string` state still
|
|
57
|
+
* type-checks), and `(code: string | null) => void` once `clearable` is set — the
|
|
58
|
+
* `null` of a clear cannot reach a handler that was never told about it.
|
|
47
59
|
*/
|
|
48
|
-
export interface CountrySelectProps
|
|
60
|
+
export interface CountrySelectProps<Clearable extends boolean = false>
|
|
49
61
|
extends Omit<ComponentPropsWithoutRef<"div">, "onChange" | "defaultValue"> {
|
|
50
62
|
/** The chosen country as an ISO 3166-1 alpha-2 code ("CH"), in either case;
|
|
51
63
|
* `null`, `undefined` or `""` when none is. A code outside the list still shows
|
|
52
64
|
* its name — the list restricts what can be PICKED, not what can be shown. */
|
|
53
65
|
value: string | null | undefined;
|
|
54
|
-
/** A country was picked: its alpha-2 code, always upper-case.
|
|
55
|
-
|
|
66
|
+
/** A country was picked: its alpha-2 code, always upper-case. With `clearable`,
|
|
67
|
+
* also `null`: the "×" (or Delete on the trigger) emptied the field. */
|
|
68
|
+
onChange: (code: Clearable extends true ? string | null : string) => void;
|
|
69
|
+
/**
|
|
70
|
+
* Offer a clear "×" in place of the chevron while a country is chosen; it hands
|
|
71
|
+
* `onChange` `null` (the kit's later list: an optional country — a contact's
|
|
72
|
+
* nationality, a bank filter's "any country" — could be set but never unset again).
|
|
73
|
+
*
|
|
74
|
+
* The combobox family's clear, as {@link EntityCombobox} draws it: a pointer
|
|
75
|
+
* shortcut inside the trigger, out of the tab order so it is not a second stop
|
|
76
|
+
* between this field and the next, named by `clearLabel` (default: the
|
|
77
|
+
* `combobox.clear` label every picker in the family shares). The keyboard way is
|
|
78
|
+
* Delete or Backspace on the closed trigger. Not offered while `disabled` or locked
|
|
79
|
+
* (`commit` / `disabledReason`): a clear is a change like any other.
|
|
80
|
+
*/
|
|
81
|
+
clearable?: Clearable;
|
|
82
|
+
/** The clear "×"'s accessible name. Wins over `combobox.clear` from the
|
|
83
|
+
* {@link UiKitProvider}, as {@link EntityCombobox}'s `clearLabel` does. */
|
|
84
|
+
clearLabel?: string;
|
|
85
|
+
/**
|
|
86
|
+
* The TRIGGER — the focusable `<button>` a reader meets — as a React 19 ref prop,
|
|
87
|
+
* the way {@link MoneyField} takes its input's (kastlan: react-hook-form's
|
|
88
|
+
* focus-on-error calls `focus()` on whatever `field.ref` was handed, and without a
|
|
89
|
+
* ref the first field in error on an address form was never focused). See
|
|
90
|
+
* `RhfCountrySelect`, which passes it.
|
|
91
|
+
*/
|
|
92
|
+
ref?: Ref<HTMLButtonElement>;
|
|
56
93
|
/**
|
|
57
94
|
* Offer only these codes (any case; duplicates and malformed entries dropped).
|
|
58
95
|
* Default: every ISO 3166-1 country, {@link COUNTRY_CODES}. Sorted by name either
|
|
@@ -92,8 +129,8 @@ export interface CountrySelectProps
|
|
|
92
129
|
error?: ReactNode;
|
|
93
130
|
/** Required and unanswered — {@link FIELD_INVALID}, as on every field. */
|
|
94
131
|
invalid?: boolean;
|
|
95
|
-
/** The empty trigger's text. Default: nothing when the field has a `label`
|
|
96
|
-
* label already says what goes here), else `labels.country`. */
|
|
132
|
+
/** The empty trigger's text. Default: nothing when the field has a `label` or an
|
|
133
|
+
* `aria-labelledby` (the label already says what goes here), else `labels.country`. */
|
|
97
134
|
placeholder?: string;
|
|
98
135
|
/** As on the other pickers: no focus, no list, the settled look. */
|
|
99
136
|
disabled?: boolean;
|
|
@@ -117,6 +154,15 @@ export interface CountrySelectProps
|
|
|
117
154
|
/** Per-instance overrides of {@link CountrySelectLabels}, over the provider's
|
|
118
155
|
* `countrySelect` namespace, over English. */
|
|
119
156
|
labels?: Partial<CountrySelectLabels>;
|
|
157
|
+
/**
|
|
158
|
+
* Name the trigger by an element of the caller's — a form's own label above the
|
|
159
|
+
* field, which is how `RhfCountrySelect` names it. The trigger is then named by
|
|
160
|
+
* that element AND the chosen country (two references, as {@link DatePicker}'s
|
|
161
|
+
* trigger is named, so "Country Switzerland" needs no punctuation to translate),
|
|
162
|
+
* instead of the composed "label: country". An `aria-label` still wins. Before
|
|
163
|
+
* 0.23 this landed on the wrapper `<div>`, which has no role to be named.
|
|
164
|
+
*/
|
|
165
|
+
"aria-labelledby"?: string;
|
|
120
166
|
}
|
|
121
167
|
|
|
122
168
|
/** Fold a name for search: no case, no accents — "osterreich" finds Österreich, "cote"
|
|
@@ -192,8 +238,11 @@ function collator(locale: string): Intl.Collator {
|
|
|
192
238
|
*
|
|
193
239
|
* Field anatomy as on every field: floating `label`, `hint`, `error`, `invalid`; the
|
|
194
240
|
* trigger is named "label: value" as the other trigger pickers are.
|
|
241
|
+
*
|
|
242
|
+
* Since 0.23 it takes a `ref` to the trigger (react-hook-form's focus-on-error, kastlan)
|
|
243
|
+
* and `clearable`, which hands `onChange` `null`.
|
|
195
244
|
*/
|
|
196
|
-
export function CountrySelect({
|
|
245
|
+
export function CountrySelect<Clearable extends boolean = false>({
|
|
197
246
|
value,
|
|
198
247
|
onChange,
|
|
199
248
|
countries,
|
|
@@ -207,18 +256,25 @@ export function CountrySelect({
|
|
|
207
256
|
disabled,
|
|
208
257
|
disabledReason: ownReason,
|
|
209
258
|
commit,
|
|
259
|
+
clearable,
|
|
260
|
+
clearLabel,
|
|
210
261
|
locale: localeProp,
|
|
211
262
|
labels: labelsProp,
|
|
212
263
|
className,
|
|
213
264
|
id,
|
|
265
|
+
ref,
|
|
214
266
|
"aria-label": ariaLabel,
|
|
267
|
+
"aria-labelledby": ariaLabelledBy,
|
|
215
268
|
"aria-describedby": ariaDescribedBy,
|
|
216
269
|
"aria-invalid": ariaInvalid,
|
|
217
270
|
"aria-required": ariaRequired,
|
|
218
271
|
...rest
|
|
219
|
-
}: CountrySelectProps) {
|
|
272
|
+
}: CountrySelectProps<Clearable>) {
|
|
220
273
|
const text = useKitLabels("countrySelect", DEFAULT_COUNTRY_SELECT_LABELS, labelsProp);
|
|
221
274
|
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
275
|
+
// The clear's name is the family's (`combobox.clear`), as on EntityCombobox; a prop
|
|
276
|
+
// left `undefined` falls through to the provider rather than masking it.
|
|
277
|
+
const comboText = useKitLabels("combobox", DEFAULT_COMBOBOX_LABELS, { clear: clearLabel });
|
|
222
278
|
// English without a locale — see `countryName`.
|
|
223
279
|
const locale = useKitLocale(localeProp) ?? "en";
|
|
224
280
|
|
|
@@ -268,6 +324,15 @@ export function CountrySelect({
|
|
|
268
324
|
);
|
|
269
325
|
const core = useComboboxCore<string>({ options: allOptions, filter: false });
|
|
270
326
|
const { open, setOpen, query, triggerRef, closeToTrigger } = core;
|
|
327
|
+
// The core anchors the panel to the trigger and hands focus back to it; the caller's
|
|
328
|
+
// `ref` wants the same element. One callback feeds both.
|
|
329
|
+
const setTrigger = useCallback(
|
|
330
|
+
(el: HTMLButtonElement | null) => {
|
|
331
|
+
triggerRef.current = el;
|
|
332
|
+
assignRef(ref, el);
|
|
333
|
+
},
|
|
334
|
+
[ref, triggerRef],
|
|
335
|
+
);
|
|
271
336
|
|
|
272
337
|
const q = fold(query.trim());
|
|
273
338
|
const results = useMemo(() => {
|
|
@@ -306,19 +371,40 @@ export function CountrySelect({
|
|
|
306
371
|
|
|
307
372
|
const code = normalizeCountryCode(value);
|
|
308
373
|
const selectedName = code ? countryName(code, locale) : undefined;
|
|
309
|
-
|
|
374
|
+
// A field named by a label of the caller's (`aria-labelledby`) is a labelled field too:
|
|
375
|
+
// the label already says what goes here.
|
|
376
|
+
const emptyText = placeholder ?? (label === undefined && !ariaLabelledBy ? text.country : "");
|
|
310
377
|
const fieldName = typeof label === "string" ? label : text.country;
|
|
378
|
+
const valueId = `${generated}-value`;
|
|
379
|
+
// Named by the caller's element (a form's label) — then the shown text is the second
|
|
380
|
+
// reference, as the composed name below has the value second; but only when it IS a
|
|
381
|
+
// value or the caller's placeholder, never the `country` fallback word.
|
|
382
|
+
const labelledBy =
|
|
383
|
+
ariaLabel === undefined && ariaLabelledBy
|
|
384
|
+
? mergeDescribedBy(ariaLabelledBy, (selectedName !== undefined || Boolean(placeholder)) && valueId)
|
|
385
|
+
: undefined;
|
|
311
386
|
const name =
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
387
|
+
labelledBy !== undefined
|
|
388
|
+
? undefined
|
|
389
|
+
: (ariaLabel ??
|
|
390
|
+
(selectedName !== undefined
|
|
391
|
+
? common.fieldValue(fieldName, selectedName)
|
|
392
|
+
: placeholder
|
|
393
|
+
? common.fieldValue(fieldName, placeholder)
|
|
394
|
+
: fieldName));
|
|
395
|
+
|
|
396
|
+
const showClear = Boolean(clearable && code && !inert);
|
|
397
|
+
// `onChange` widened to take the clear's `null` — its type says so once `clearable`
|
|
398
|
+
// is set, and the clear is only ever offered then.
|
|
399
|
+
const emit = onChange as (code: string | null) => void;
|
|
400
|
+
const clear = () => {
|
|
401
|
+
if (!showClear) return;
|
|
402
|
+
emit(null);
|
|
403
|
+
};
|
|
318
404
|
|
|
319
405
|
const choose = (o: ComboOption<string>) => {
|
|
320
406
|
if (inert) return;
|
|
321
|
-
|
|
407
|
+
emit(o.value);
|
|
322
408
|
// Back to the trigger: the panel holding focus is about to unmount, and a keyboard
|
|
323
409
|
// user who has answered this field should stand on it, ready to Tab on.
|
|
324
410
|
closeToTrigger();
|
|
@@ -326,7 +412,7 @@ export function CountrySelect({
|
|
|
326
412
|
|
|
327
413
|
const trigger = (
|
|
328
414
|
<button
|
|
329
|
-
ref={
|
|
415
|
+
ref={setTrigger}
|
|
330
416
|
id={triggerId}
|
|
331
417
|
type="button"
|
|
332
418
|
// A closed choice that expands into the list below — the role every trigger
|
|
@@ -337,6 +423,7 @@ export function CountrySelect({
|
|
|
337
423
|
aria-controls={listboxId}
|
|
338
424
|
aria-expanded={open}
|
|
339
425
|
aria-label={name}
|
|
426
|
+
aria-labelledby={labelledBy}
|
|
340
427
|
aria-invalid={field.isInvalid || undefined}
|
|
341
428
|
aria-required={ariaRequired}
|
|
342
429
|
aria-describedby={mergeDescribedBy(field.describedBy, locked && reasonId)}
|
|
@@ -358,6 +445,10 @@ export function CountrySelect({
|
|
|
358
445
|
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
359
446
|
e.preventDefault();
|
|
360
447
|
setOpen(true);
|
|
448
|
+
} else if (showClear && !open && (e.key === "Delete" || e.key === "Backspace")) {
|
|
449
|
+
// The keyboard's clear: the "×" is out of the tab order (see `clearable`).
|
|
450
|
+
e.preventDefault();
|
|
451
|
+
clear();
|
|
361
452
|
}
|
|
362
453
|
}}
|
|
363
454
|
className={cn(
|
|
@@ -373,11 +464,33 @@ export function CountrySelect({
|
|
|
373
464
|
{flags && code && <CurrencyFlag country={code.toLowerCase()} />}
|
|
374
465
|
{/* A long name ("Saint Vincent and the Grenadines") truncates rather than
|
|
375
466
|
widening the field: the kit is checked at 390px. */}
|
|
376
|
-
<span
|
|
467
|
+
<span
|
|
468
|
+
id={valueId}
|
|
469
|
+
className={cn("truncate", selectedName === undefined && "text-[var(--text-placeholder)]")}
|
|
470
|
+
>
|
|
377
471
|
{selectedName ?? emptyText}
|
|
378
472
|
</span>
|
|
379
473
|
</span>
|
|
380
|
-
|
|
474
|
+
{showClear ? (
|
|
475
|
+
/* eslint-disable-next-line jsx-a11y/click-events-have-key-events -- tabIndex -1 inside
|
|
476
|
+
the trigger <button>, so it never holds focus and a key handler here could never
|
|
477
|
+
fire; the keys go to the trigger, where Delete and Backspace clear. A pointer
|
|
478
|
+
shortcut, as on EntityCombobox. */
|
|
479
|
+
<span
|
|
480
|
+
role="button"
|
|
481
|
+
tabIndex={-1}
|
|
482
|
+
aria-label={comboText.clear}
|
|
483
|
+
onClick={(e) => {
|
|
484
|
+
e.stopPropagation();
|
|
485
|
+
clear();
|
|
486
|
+
}}
|
|
487
|
+
className="absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]"
|
|
488
|
+
>
|
|
489
|
+
<X aria-hidden className="size-4" />
|
|
490
|
+
</span>
|
|
491
|
+
) : (
|
|
492
|
+
<FieldChevron />
|
|
493
|
+
)}
|
|
381
494
|
</button>
|
|
382
495
|
);
|
|
383
496
|
|
|
@@ -324,6 +324,18 @@ export interface DangerConfirmLabels {
|
|
|
324
324
|
/** 0.22: the checkbox `requireAcknowledge={true}` shows — "I have read what this does
|
|
325
325
|
* and want to continue" (keksdose's `admin.users.confirm_ack`). */
|
|
326
326
|
acknowledge: string;
|
|
327
|
+
/**
|
|
328
|
+
* 0.23: why the armed confirm is held while the typed phrase does not match yet — in
|
|
329
|
+
* the confirm's tooltip and its description (keksdose G4a). A FUNCTION of the phrase
|
|
330
|
+
* by default ("Type “DELETE” to confirm"), or a finished STRING, as `phrase`.
|
|
331
|
+
*/
|
|
332
|
+
needsPhrase: string | ((phrase: string) => string);
|
|
333
|
+
/** 0.23: why the armed confirm is held while the `requireAcknowledge` box is unticked
|
|
334
|
+
* — "Tick the box to confirm". */
|
|
335
|
+
needsAcknowledge: string;
|
|
336
|
+
/** 0.23: why the armed confirm is held while the `requirePassword` field is empty —
|
|
337
|
+
* "Enter your password to confirm". */
|
|
338
|
+
needsPassword: string;
|
|
327
339
|
}
|
|
328
340
|
|
|
329
341
|
/** `satisfies` rather than a type annotation, so `DEFAULT_DANGER_CONFIRM_LABELS.phrase`
|
|
@@ -336,8 +348,33 @@ export const DEFAULT_DANGER_CONFIRM_LABELS = {
|
|
|
336
348
|
password: "Password",
|
|
337
349
|
phrase: (phrase: string) => `Type “${phrase}” to confirm`,
|
|
338
350
|
acknowledge: "I have read what this does and want to continue.",
|
|
351
|
+
needsPhrase: (phrase: string) => `Type “${phrase}” to confirm`,
|
|
352
|
+
needsAcknowledge: "Tick the box to confirm",
|
|
353
|
+
needsPassword: "Enter your password to confirm",
|
|
339
354
|
} satisfies DangerConfirmLabels;
|
|
340
355
|
|
|
356
|
+
/**
|
|
357
|
+
* What the guards were answered with — the second argument of
|
|
358
|
+
* {@link DangerConfirmProps.onConfirm} (0.23, keksdose G4b). A key is there only when its
|
|
359
|
+
* guard was asked for, so the object can go into a request body as it is.
|
|
360
|
+
*/
|
|
361
|
+
export interface DangerConfirmValues {
|
|
362
|
+
/**
|
|
363
|
+
* The text typed into the `phrase` field — as the match rule compared it: trimmed
|
|
364
|
+
* under `phraseMatch` `"trim"` and `"caseless"` (the spaces a phone keyboard adds are
|
|
365
|
+
* not part of it), exactly as typed under `"exact"`. The case is the user's, even
|
|
366
|
+
* under `"caseless"`: a server that re-checks applies its own rule to it. Absent
|
|
367
|
+
* without a `phrase`.
|
|
368
|
+
*/
|
|
369
|
+
typed?: string;
|
|
370
|
+
/** The entered password — the same value as the first argument. Absent without
|
|
371
|
+
* `requirePassword`. */
|
|
372
|
+
password?: string;
|
|
373
|
+
/** `true` when the `requireAcknowledge` box was shown — it was ticked, since the
|
|
374
|
+
* confirm is held until it is. Absent without one. */
|
|
375
|
+
acknowledged?: true;
|
|
376
|
+
}
|
|
377
|
+
|
|
341
378
|
/**
|
|
342
379
|
* One line of {@link DangerConfirmProps.consequences}: what the action will do. A plain
|
|
343
380
|
* string, or this, for a key of its own and the `severe` mark.
|
|
@@ -360,12 +397,25 @@ export interface DangerConfirmProps extends Omit<ComponentPropsWithoutRef<"div">
|
|
|
360
397
|
* (fields wiped) when it resolves, still armed when it rejects — so the user can
|
|
361
398
|
* correct a wrong password and retry. The rejection is not swallowed for you to
|
|
362
399
|
* miss: handle it (and show why) in the caller, as with any mutation.
|
|
400
|
+
*
|
|
401
|
+
* 0.23: the second argument is what every guard was answered with — the typed phrase,
|
|
402
|
+
* the password, the tick ({@link DangerConfirmValues}). keksdose G4b: its admin
|
|
403
|
+
* actions send the address the admin TYPED, and the server re-checks it against the
|
|
404
|
+
* account (`assert_confirmed`), so a client that put the wrong row's id in the request
|
|
405
|
+
* is refused rather than obeyed; the tile kept the text to itself.
|
|
406
|
+
*
|
|
407
|
+
* onConfirm={(_password, { typed }) =>
|
|
408
|
+
* reset.mutateAsync({ id, confirm: { acknowledged: true, confirm_email: typed ?? null } })}
|
|
409
|
+
*
|
|
410
|
+
* A second argument rather than one object in place of the password: every
|
|
411
|
+
* `(password) => …` and `() => …` handler, and a `mutation.mutate` passed as it is,
|
|
412
|
+
* keeps working unchanged, and a handler that needs one guard's answer names it.
|
|
363
413
|
*/
|
|
364
|
-
onConfirm: (password
|
|
365
|
-
/** Show a password field; confirm
|
|
414
|
+
onConfirm: (password: string | undefined, values: DangerConfirmValues) => void | Promise<unknown>;
|
|
415
|
+
/** Show a password field; confirm is held until it is filled. */
|
|
366
416
|
requirePassword?: boolean;
|
|
367
417
|
/**
|
|
368
|
-
* Show an "I understand" checkbox; confirm
|
|
418
|
+
* Show an "I understand" checkbox; confirm is held until it is ticked
|
|
369
419
|
* (keksdose K7). `true` words it with `labels.acknowledge`; any other node IS the
|
|
370
420
|
* checkbox's label ("I understand Anna will be signed out everywhere").
|
|
371
421
|
*
|
|
@@ -386,8 +436,8 @@ export interface DangerConfirmProps extends Omit<ComponentPropsWithoutRef<"div">
|
|
|
386
436
|
* who reads the same paragraph on every row stops reading it (keksdose dev#488).
|
|
387
437
|
*/
|
|
388
438
|
consequences?: readonly (string | DangerConsequence)[];
|
|
389
|
-
/** Show a "type <phrase> to confirm" field; confirm
|
|
390
|
-
*
|
|
439
|
+
/** Show a "type <phrase> to confirm" field; confirm is held until the field matches
|
|
440
|
+
* (case-sensitive; surrounding spaces ignored unless `phraseMatch="exact"`). */
|
|
391
441
|
phrase?: string;
|
|
392
442
|
/**
|
|
393
443
|
* How the typed text is compared with `phrase`. `"trim"` (default) ignores spaces
|
|
@@ -446,8 +496,17 @@ export interface DangerConfirmProps extends Omit<ComponentPropsWithoutRef<"div">
|
|
|
446
496
|
/**
|
|
447
497
|
* An "arm → confirm" tile for destructive actions: one button, which expands into a
|
|
448
498
|
* warning, an optional list of consequences, an optional "I understand" tick, an
|
|
449
|
-
* optional type-to-confirm field, an optional password field and a confirm that
|
|
450
|
-
*
|
|
499
|
+
* optional type-to-confirm field, an optional password field and a confirm that is held
|
|
500
|
+
* until every guard is satisfied.
|
|
501
|
+
*
|
|
502
|
+
* A held confirm SAYS which guard is still open (0.23, keksdose G4a): it is
|
|
503
|
+
* `aria-disabled` rather than `disabled` — still focusable, so a keyboard user can land
|
|
504
|
+
* on it — with the reason in the kit {@link Tooltip} and its description ("Type
|
|
505
|
+
* “DELETE” to confirm", "Tick the box to confirm", "Enter your password to confirm";
|
|
506
|
+
* `labels.needs*`), the first open guard in the order they are drawn. FormActions'
|
|
507
|
+
* `submitDisabledReason` does the same for a form's Save. Pressing it, or Enter in a
|
|
508
|
+
* field, does nothing. A lock's reason wins over a guard's; while the action runs the
|
|
509
|
+
* confirm is plainly disabled, its spinner saying why.
|
|
451
510
|
*
|
|
452
511
|
* Keksdose hand-rolled it three times (load demo data, wipe everything, reset a
|
|
453
512
|
* budget) and then as `shared/components/danger-confirm.tsx`; the only app-specific
|
|
@@ -549,14 +608,33 @@ export function DangerConfirm({
|
|
|
549
608
|
const phraseOk = phrase === undefined || typedMatches(typed, phrase, phraseMatch);
|
|
550
609
|
const acknowledgeOk = !asksAcknowledge || acknowledged;
|
|
551
610
|
const canConfirm = passwordOk && phraseOk && acknowledgeOk && !busy;
|
|
611
|
+
// The first guard still open, in the order the fields are drawn — the one the user
|
|
612
|
+
// meets next, so the sentence points at it.
|
|
613
|
+
const guardReason: string | undefined = !acknowledgeOk
|
|
614
|
+
? labels.needsAcknowledge
|
|
615
|
+
: !phraseOk
|
|
616
|
+
? typeof labels.needsPhrase === "function"
|
|
617
|
+
? labels.needsPhrase(phrase ?? "")
|
|
618
|
+
: labels.needsPhrase
|
|
619
|
+
: !passwordOk
|
|
620
|
+
? labels.needsPassword
|
|
621
|
+
: undefined;
|
|
622
|
+
// The lock first (no guard can lift it); none while busy, when the guards were met and
|
|
623
|
+
// the spinner is the state.
|
|
624
|
+
const confirmReason: ReactNode = locked ? lockedReason : busy ? undefined : guardReason;
|
|
552
625
|
|
|
553
626
|
const submit = (e: FormEvent) => {
|
|
554
627
|
e.preventDefault();
|
|
555
628
|
// `locked` too: Enter in a field submits the form without the button's say.
|
|
556
629
|
if (!canConfirm || locked) return;
|
|
630
|
+
const values: DangerConfirmValues = {
|
|
631
|
+
...(phrase !== undefined && { typed: phraseMatch === "exact" ? typed : typed.trim() }),
|
|
632
|
+
...(requirePassword && { password }),
|
|
633
|
+
...(asksAcknowledge && { acknowledged: true as const }),
|
|
634
|
+
};
|
|
557
635
|
// Disarms when it resolves; a rejection leaves it armed, fields kept: the caller
|
|
558
636
|
// shows why, the user retries.
|
|
559
|
-
run(onConfirm(
|
|
637
|
+
run(onConfirm(values.password, values), () => setArmed(false));
|
|
560
638
|
};
|
|
561
639
|
|
|
562
640
|
// The first field takes focus on arm: the tick, then the phrase, then the password —
|
|
@@ -676,10 +754,11 @@ export function DangerConfirm({
|
|
|
676
754
|
type="submit"
|
|
677
755
|
variant={tone === "warning" ? "primary" : "danger"}
|
|
678
756
|
disabled={!canConfirm}
|
|
679
|
-
//
|
|
680
|
-
// the way Button does — focusable, `aria-disabled`, the
|
|
681
|
-
// tooltip and description
|
|
682
|
-
|
|
757
|
+
// Held by a guard, or armed and then locked (or rendered armed under a lock):
|
|
758
|
+
// the confirm says why the way Button does — focusable, `aria-disabled`, the
|
|
759
|
+
// reason in its tooltip and description, a press swallowed (the submit with
|
|
760
|
+
// it) — and the fields stay as typed. Only busy is the native `disabled`.
|
|
761
|
+
disabledReason={confirmReason}
|
|
683
762
|
aria-busy={busy || undefined}
|
|
684
763
|
>
|
|
685
764
|
{busy && <Spinner label={null} className="h-4 w-4" />}
|