@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.
Files changed (208) hide show
  1. package/README.md +42 -25
  2. package/dist/chart.d.ts +8 -7
  3. package/dist/components/amount-input.d.ts +34 -7
  4. package/dist/components/amount-input.js +14 -10
  5. package/dist/components/amount-input.js.map +1 -1
  6. package/dist/components/autocomplete.d.ts +26 -0
  7. package/dist/components/autocomplete.js +164 -137
  8. package/dist/components/autocomplete.js.map +1 -1
  9. package/dist/components/button-group.d.ts +8 -7
  10. package/dist/components/calculator.d.ts +8 -7
  11. package/dist/components/column-mapper.d.ts +235 -0
  12. package/dist/components/column-mapper.js +411 -0
  13. package/dist/components/column-mapper.js.map +1 -0
  14. package/dist/components/combobox-core.d.ts +39 -2
  15. package/dist/components/combobox-core.js +29 -6
  16. package/dist/components/combobox-core.js.map +1 -1
  17. package/dist/components/combobox.d.ts +113 -3
  18. package/dist/components/combobox.js +456 -313
  19. package/dist/components/combobox.js.map +1 -1
  20. package/dist/components/confirm-dialog.d.ts +8 -7
  21. package/dist/components/confirm-dialog.js +7 -8
  22. package/dist/components/confirm-dialog.js.map +1 -1
  23. package/dist/components/copy-button.d.ts +5 -4
  24. package/dist/components/country-select.d.ts +50 -7
  25. package/dist/components/country-select.js +65 -9
  26. package/dist/components/country-select.js.map +1 -1
  27. package/dist/components/danger-confirm.d.ts +8 -7
  28. package/dist/components/danger-confirm.js +64 -7
  29. package/dist/components/danger-confirm.js.map +1 -1
  30. package/dist/components/date-picker.d.ts +41 -0
  31. package/dist/components/date-picker.js +269 -8
  32. package/dist/components/date-picker.js.map +1 -1
  33. package/dist/components/entity-combobox.d.ts +48 -2
  34. package/dist/components/entity-combobox.js +159 -114
  35. package/dist/components/entity-combobox.js.map +1 -1
  36. package/dist/components/facing-pair.d.ts +8 -7
  37. package/dist/components/field-parts.d.ts +116 -6
  38. package/dist/components/field-parts.js +84 -0
  39. package/dist/components/field-parts.js.map +1 -1
  40. package/dist/components/field-strip.d.ts +105 -0
  41. package/dist/components/field-strip.js +46 -0
  42. package/dist/components/field-strip.js.map +1 -0
  43. package/dist/components/file-button.d.ts +8 -7
  44. package/dist/components/file-dropzone.d.ts +8 -7
  45. package/dist/components/form-actions.d.ts +8 -7
  46. package/dist/components/form-actions.js +23 -3
  47. package/dist/components/form-actions.js.map +1 -1
  48. package/dist/components/iban-input.d.ts +8 -7
  49. package/dist/components/icon-picker.d.ts +10 -1
  50. package/dist/components/icon-picker.js +12 -4
  51. package/dist/components/icon-picker.js.map +1 -1
  52. package/dist/components/language-select.d.ts +8 -7
  53. package/dist/components/money-field.d.ts +11 -7
  54. package/dist/components/money-field.js.map +1 -1
  55. package/dist/components/month-picker.js +2 -1
  56. package/dist/components/month-picker.js.map +1 -1
  57. package/dist/components/multi-entity-combobox.d.ts +31 -2
  58. package/dist/components/multi-entity-combobox.js +152 -103
  59. package/dist/components/multi-entity-combobox.js.map +1 -1
  60. package/dist/components/number-field.d.ts +8 -7
  61. package/dist/components/number-input.d.ts +8 -7
  62. package/dist/components/numpad-sheet.d.ts +8 -7
  63. package/dist/components/phone-input.d.ts +8 -7
  64. package/dist/components/reauth-dialog.d.ts +2 -2
  65. package/dist/components/reauth-dialog.js +6 -8
  66. package/dist/components/reauth-dialog.js.map +1 -1
  67. package/dist/components/series-chart.d.ts +8 -7
  68. package/dist/components/settings-fields.d.ts +8 -7
  69. package/dist/components/share-card.d.ts +8 -7
  70. package/dist/components/swatch-picker.d.ts +17 -1
  71. package/dist/components/swatch-picker.js +10 -4
  72. package/dist/components/swatch-picker.js.map +1 -1
  73. package/dist/components/text-link.d.ts +8 -7
  74. package/dist/components/tile-radio.d.ts +20 -0
  75. package/dist/components/tile-radio.js +25 -8
  76. package/dist/components/tile-radio.js.map +1 -1
  77. package/dist/components/time-input.d.ts +8 -7
  78. package/dist/components/toggle-group.js +2 -2
  79. package/dist/components/toggle-group.js.map +1 -1
  80. package/dist/components/trigger-aria.d.ts +3 -0
  81. package/dist/components/trigger-aria.js +3 -1
  82. package/dist/components/trigger-aria.js.map +1 -1
  83. package/dist/components/ui.d.ts +5 -4
  84. package/dist/components/ui.js +60 -28
  85. package/dist/components/ui.js.map +1 -1
  86. package/dist/feedback/feedback-attachment.d.ts +66 -1
  87. package/dist/feedback/feedback-attachment.js +185 -65
  88. package/dist/feedback/feedback-attachment.js.map +1 -1
  89. package/dist/feedback/feedback-dialog.d.ts +66 -1
  90. package/dist/feedback/feedback-dialog.js.map +1 -1
  91. package/dist/feedback/feedback-inbox.d.ts +67 -252
  92. package/dist/feedback/feedback-inbox.js.map +1 -1
  93. package/dist/feedback/feedback-thread.d.ts +66 -287
  94. package/dist/feedback/feedback-thread.js +16 -3
  95. package/dist/feedback/feedback-thread.js.map +1 -1
  96. package/dist/{kit-labels-R8mIAc9N.d.ts → feedback-DOwPu-Il.d.ts} +1273 -30
  97. package/dist/feedback.d.ts +65 -3
  98. package/dist/hooks/use-file-drop.d.ts +8 -7
  99. package/dist/i18n/defaults.d.ts +8 -7
  100. package/dist/i18n/defaults.js +3 -1
  101. package/dist/i18n/defaults.js.map +1 -1
  102. package/dist/i18n/german.d.ts +8 -7
  103. package/dist/i18n/german.js +36 -2
  104. package/dist/i18n/german.js.map +1 -1
  105. package/dist/i18n/kit-labels.d.ts +5 -4
  106. package/dist/i18n/kit-labels.js.map +1 -1
  107. package/dist/i18n/languages.d.ts +8 -7
  108. package/dist/i18n/locales/de-CH.d.ts +8 -7
  109. package/dist/i18n/locales/en.d.ts +8 -7
  110. package/dist/i18n/locales/en.js +7 -0
  111. package/dist/i18n/locales/en.js.map +1 -1
  112. package/dist/i18n/locales/es.d.ts +8 -7
  113. package/dist/i18n/locales/es.js +37 -2
  114. package/dist/i18n/locales/es.js.map +1 -1
  115. package/dist/i18n/locales/fr.d.ts +8 -7
  116. package/dist/i18n/locales/fr.js +36 -2
  117. package/dist/i18n/locales/fr.js.map +1 -1
  118. package/dist/i18n/locales/hu.d.ts +8 -7
  119. package/dist/i18n/locales/hu.js +38 -2
  120. package/dist/i18n/locales/hu.js.map +1 -1
  121. package/dist/i18n/locales/it.d.ts +8 -7
  122. package/dist/i18n/locales/it.js +37 -2
  123. package/dist/i18n/locales/it.js.map +1 -1
  124. package/dist/i18n/locales/zh.d.ts +8 -7
  125. package/dist/i18n/locales/zh.js +34 -2
  126. package/dist/i18n/locales/zh.js.map +1 -1
  127. package/dist/i18n/review.d.ts +8 -7
  128. package/dist/i18n/review.js +20 -1
  129. package/dist/i18n/review.js.map +1 -1
  130. package/dist/index.d.ts +6 -4
  131. package/dist/index.js +16 -0
  132. package/dist/index.js.map +1 -1
  133. package/dist/lib/column-mapping.d.ts +81 -0
  134. package/dist/lib/column-mapping.js +108 -0
  135. package/dist/lib/column-mapping.js.map +1 -0
  136. package/dist/lib/strip-fade.d.ts +2 -2
  137. package/dist/lib/strip-fade.js.map +1 -1
  138. package/dist/lib/table-text.d.ts +109 -1
  139. package/dist/lib/table-text.js +122 -1
  140. package/dist/lib/table-text.js.map +1 -1
  141. package/dist/rhf/fields.d.ts +142 -13
  142. package/dist/rhf/fields.js +185 -3
  143. package/dist/rhf/fields.js.map +1 -1
  144. package/dist/rhf/form.d.ts +8 -7
  145. package/dist/rhf.d.ts +9 -8
  146. package/dist/rhf.js.map +1 -1
  147. package/dist/shell/app-shell.d.ts +8 -7
  148. package/dist/shell/top-bar-brand.d.ts +8 -7
  149. package/dist/shell/topbar-action-menu.d.ts +36 -3
  150. package/dist/shell/topbar-action-menu.js +74 -33
  151. package/dist/shell/topbar-action-menu.js.map +1 -1
  152. package/dist/shell.d.ts +8 -7
  153. package/dist/table-text.d.ts +1 -1
  154. package/dist/wizard/stepper-nav.d.ts +38 -9
  155. package/dist/wizard/stepper-nav.js +4 -0
  156. package/dist/wizard/stepper-nav.js.map +1 -1
  157. package/dist/wizard.d.ts +8 -7
  158. package/package.json +1 -1
  159. package/src/components/amount-input.tsx +50 -14
  160. package/src/components/autocomplete.tsx +188 -116
  161. package/src/components/column-mapper.tsx +768 -0
  162. package/src/components/combobox-core.tsx +82 -9
  163. package/src/components/combobox.tsx +655 -335
  164. package/src/components/confirm-dialog.tsx +12 -8
  165. package/src/components/country-select.tsx +135 -22
  166. package/src/components/danger-confirm.tsx +274 -24
  167. package/src/components/date-picker.tsx +423 -10
  168. package/src/components/entity-combobox.tsx +243 -129
  169. package/src/components/field-parts.tsx +224 -5
  170. package/src/components/field-strip.tsx +149 -0
  171. package/src/components/form-actions.tsx +52 -5
  172. package/src/components/icon-picker.tsx +23 -4
  173. package/src/components/money-field.tsx +3 -0
  174. package/src/components/month-picker.tsx +2 -1
  175. package/src/components/multi-entity-combobox.tsx +221 -121
  176. package/src/components/reauth-dialog.tsx +17 -18
  177. package/src/components/swatch-picker.tsx +29 -5
  178. package/src/components/tile-radio.tsx +74 -13
  179. package/src/components/toggle-group.tsx +2 -2
  180. package/src/components/trigger-aria.ts +5 -0
  181. package/src/components/ui.tsx +114 -33
  182. package/src/feedback/feedback-attachment.tsx +372 -72
  183. package/src/feedback/feedback-dialog.tsx +7 -3
  184. package/src/feedback/feedback-inbox.tsx +3 -2
  185. package/src/feedback/feedback-thread.tsx +112 -9
  186. package/src/i18n/defaults.ts +2 -0
  187. package/src/i18n/german.ts +42 -0
  188. package/src/i18n/kit-labels.tsx +4 -0
  189. package/src/i18n/locales/en.ts +27 -5
  190. package/src/i18n/locales/es.ts +41 -0
  191. package/src/i18n/locales/fr.ts +42 -0
  192. package/src/i18n/locales/hu.ts +37 -0
  193. package/src/i18n/locales/it.ts +41 -0
  194. package/src/i18n/locales/zh.ts +32 -0
  195. package/src/i18n/review.ts +19 -0
  196. package/src/index.ts +18 -0
  197. package/src/lib/column-mapping.ts +234 -0
  198. package/src/lib/strip-fade.ts +2 -2
  199. package/src/lib/table-text.ts +271 -0
  200. package/src/rhf/fields.tsx +421 -6
  201. package/src/rhf.ts +2 -1
  202. package/src/shell/topbar-action-menu.tsx +134 -38
  203. package/src/wizard/stepper-nav.tsx +35 -1
  204. package/dist/components/field-anatomy.d.ts +0 -95
  205. package/dist/components/field-anatomy.js +0 -84
  206. package/dist/components/field-anatomy.js.map +0 -1
  207. package/dist/feedback-attachment-NhUm7Zga.d.ts +0 -367
  208. 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, Input } from "./ui";
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
- <Input
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
- autoComplete="off"
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 { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from "../i18n/kit-labels";
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
- onChange: (code: string) => void;
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` (the
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
- const emptyText = placeholder ?? (label === undefined ? text.country : "");
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
- ariaLabel ??
313
- (selectedName !== undefined
314
- ? common.fieldValue(fieldName, selectedName)
315
- : placeholder
316
- ? common.fieldValue(fieldName, placeholder)
317
- : fieldName);
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
- onChange(o.value);
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={triggerRef}
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 className={cn("truncate", selectedName === undefined && "text-[var(--text-placeholder)]")}>
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
- <FieldChevron />
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