@eifi1/ui-kit 0.21.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.
Files changed (320) hide show
  1. package/README.md +42 -25
  2. package/dist/chart.d.ts +19 -14
  3. package/dist/components/account-chips.d.ts +10 -3
  4. package/dist/components/account-chips.js +5 -2
  5. package/dist/components/account-chips.js.map +1 -1
  6. package/dist/components/account-settings.d.ts +33 -2
  7. package/dist/components/account-settings.js +43 -21
  8. package/dist/components/account-settings.js.map +1 -1
  9. package/dist/components/amount-input.d.ts +53 -14
  10. package/dist/components/amount-input.js +17 -28
  11. package/dist/components/amount-input.js.map +1 -1
  12. package/dist/components/autocomplete.d.ts +29 -0
  13. package/dist/components/autocomplete.js +134 -104
  14. package/dist/components/autocomplete.js.map +1 -1
  15. package/dist/components/button-group.d.ts +19 -14
  16. package/dist/components/calculator.d.ts +19 -14
  17. package/dist/components/checkbox-group.d.ts +104 -0
  18. package/dist/components/checkbox-group.js +111 -0
  19. package/dist/components/checkbox-group.js.map +1 -0
  20. package/dist/components/checkbox.d.ts +19 -0
  21. package/dist/components/checkbox.js +30 -7
  22. package/dist/components/checkbox.js.map +1 -1
  23. package/dist/components/chip.d.ts +18 -0
  24. package/dist/components/chip.js +47 -10
  25. package/dist/components/chip.js.map +1 -1
  26. package/dist/components/column-mapper.d.ts +182 -0
  27. package/dist/components/column-mapper.js +375 -0
  28. package/dist/components/column-mapper.js.map +1 -0
  29. package/dist/components/combobox-core.d.ts +39 -2
  30. package/dist/components/combobox-core.js +29 -6
  31. package/dist/components/combobox-core.js.map +1 -1
  32. package/dist/components/combobox.d.ts +91 -2
  33. package/dist/components/combobox.js +454 -308
  34. package/dist/components/combobox.js.map +1 -1
  35. package/dist/components/confirm-dialog.d.ts +69 -161
  36. package/dist/components/confirm-dialog.js +7 -8
  37. package/dist/components/confirm-dialog.js.map +1 -1
  38. package/dist/components/copy-button.d.ts +16 -11
  39. package/dist/components/country-select.d.ts +167 -0
  40. package/dist/components/country-select.js +293 -0
  41. package/dist/components/country-select.js.map +1 -0
  42. package/dist/components/currency-select.d.ts +17 -1
  43. package/dist/components/currency-select.js +56 -23
  44. package/dist/components/currency-select.js.map +1 -1
  45. package/dist/components/danger-confirm.d.ts +69 -166
  46. package/dist/components/danger-confirm.js +144 -50
  47. package/dist/components/danger-confirm.js.map +1 -1
  48. package/dist/components/data-table-cells.d.ts +1 -2
  49. package/dist/components/data-table-filter-popover.d.ts +1 -2
  50. package/dist/components/data-table-filters.d.ts +1 -2
  51. package/dist/components/data-table-labels.d.ts +4 -97
  52. package/dist/components/data-table-labels.js +9 -1
  53. package/dist/components/data-table-labels.js.map +1 -1
  54. package/dist/components/data-table-pagination.d.ts +3 -1
  55. package/dist/components/data-table.d.ts +1 -2
  56. package/dist/components/data-table.js +98 -3
  57. package/dist/components/data-table.js.map +1 -1
  58. package/dist/components/date-picker.d.ts +66 -5
  59. package/dist/components/date-picker.js +378 -66
  60. package/dist/components/date-picker.js.map +1 -1
  61. package/dist/components/entity-combobox.d.ts +42 -1
  62. package/dist/components/entity-combobox.js +154 -113
  63. package/dist/components/entity-combobox.js.map +1 -1
  64. package/dist/components/facing-pair.d.ts +19 -14
  65. package/dist/components/field-parts.d.ts +224 -0
  66. package/dist/components/field-parts.js +156 -0
  67. package/dist/components/field-parts.js.map +1 -0
  68. package/dist/components/field-strip.d.ts +105 -0
  69. package/dist/components/field-strip.js +46 -0
  70. package/dist/components/field-strip.js.map +1 -0
  71. package/dist/components/file-button.d.ts +19 -14
  72. package/dist/components/file-button.js +13 -3
  73. package/dist/components/file-button.js.map +1 -1
  74. package/dist/components/file-dropzone.d.ts +41 -15
  75. package/dist/components/file-dropzone.js +57 -34
  76. package/dist/components/file-dropzone.js.map +1 -1
  77. package/dist/components/form-actions.d.ts +18 -13
  78. package/dist/components/form-actions.js +69 -5
  79. package/dist/components/form-actions.js.map +1 -1
  80. package/dist/components/iban-input.d.ts +69 -0
  81. package/dist/components/iban-input.js +125 -0
  82. package/dist/components/iban-input.js.map +1 -0
  83. package/dist/components/icon-picker.d.ts +21 -2
  84. package/dist/components/icon-picker.js +19 -4
  85. package/dist/components/icon-picker.js.map +1 -1
  86. package/dist/components/inline-edit-field.d.ts +167 -0
  87. package/dist/components/inline-edit-field.js +293 -0
  88. package/dist/components/inline-edit-field.js.map +1 -0
  89. package/dist/components/language-select.d.ts +130 -0
  90. package/dist/components/language-select.js +18 -0
  91. package/dist/components/language-select.js.map +1 -0
  92. package/dist/components/line-items.d.ts +25 -3
  93. package/dist/components/line-items.js +55 -46
  94. package/dist/components/line-items.js.map +1 -1
  95. package/dist/components/list.d.ts +25 -0
  96. package/dist/components/list.js +16 -5
  97. package/dist/components/list.js.map +1 -1
  98. package/dist/components/money-field.d.ts +32 -14
  99. package/dist/components/money-field.js.map +1 -1
  100. package/dist/components/month-picker.d.ts +52 -4
  101. package/dist/components/month-picker.js +291 -86
  102. package/dist/components/month-picker.js.map +1 -1
  103. package/dist/components/multi-entity-combobox.d.ts +26 -1
  104. package/dist/components/multi-entity-combobox.js +143 -102
  105. package/dist/components/multi-entity-combobox.js.map +1 -1
  106. package/dist/components/multi-select.d.ts +8 -1
  107. package/dist/components/multi-select.js +9 -4
  108. package/dist/components/multi-select.js.map +1 -1
  109. package/dist/components/number-field.d.ts +34 -19
  110. package/dist/components/number-field.js +6 -20
  111. package/dist/components/number-field.js.map +1 -1
  112. package/dist/components/number-input.d.ts +79 -19
  113. package/dist/components/number-input.js +90 -38
  114. package/dist/components/number-input.js.map +1 -1
  115. package/dist/components/numpad-sheet.d.ts +19 -14
  116. package/dist/components/one-time-code-input.d.ts +100 -0
  117. package/dist/components/one-time-code-input.js +137 -0
  118. package/dist/components/one-time-code-input.js.map +1 -0
  119. package/dist/components/phone-input.d.ts +69 -0
  120. package/dist/components/phone-input.js +171 -0
  121. package/dist/components/phone-input.js.map +1 -0
  122. package/dist/components/reauth-dialog.d.ts +2 -2
  123. package/dist/components/reauth-dialog.js +6 -8
  124. package/dist/components/reauth-dialog.js.map +1 -1
  125. package/dist/components/series-chart.d.ts +19 -14
  126. package/dist/components/settings-fields.d.ts +19 -14
  127. package/dist/components/share-card.d.ts +16 -11
  128. package/dist/components/sign-chip.d.ts +66 -0
  129. package/dist/components/sign-chip.js +39 -0
  130. package/dist/components/sign-chip.js.map +1 -0
  131. package/dist/components/swatch-picker.d.ts +43 -2
  132. package/dist/components/swatch-picker.js +80 -32
  133. package/dist/components/swatch-picker.js.map +1 -1
  134. package/dist/components/switch.d.ts +19 -0
  135. package/dist/components/switch.js +24 -6
  136. package/dist/components/switch.js.map +1 -1
  137. package/dist/components/text-link.d.ts +18 -13
  138. package/dist/components/tile-radio.d.ts +88 -13
  139. package/dist/components/tile-radio.js +28 -11
  140. package/dist/components/tile-radio.js.map +1 -1
  141. package/dist/components/time-input.d.ts +24 -15
  142. package/dist/components/time-input.js.map +1 -1
  143. package/dist/components/toggle-group.d.ts +47 -1
  144. package/dist/components/toggle-group.js +110 -69
  145. package/dist/components/toggle-group.js.map +1 -1
  146. package/dist/components/trigger-aria.d.ts +3 -0
  147. package/dist/components/trigger-aria.js +3 -1
  148. package/dist/components/trigger-aria.js.map +1 -1
  149. package/dist/components/ui.d.ts +16 -11
  150. package/dist/components/ui.js +284 -88
  151. package/dist/components/ui.js.map +1 -1
  152. package/dist/components/use-table-state.d.ts +1 -2
  153. package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
  154. package/dist/data-table.d.ts +1 -2
  155. package/dist/data-table.js +2 -1
  156. package/dist/data-table.js.map +1 -1
  157. package/dist/feedback/feedback-attachment.d.ts +1 -1
  158. package/dist/feedback/feedback-attachment.js +348 -41
  159. package/dist/feedback/feedback-attachment.js.map +1 -1
  160. package/dist/feedback/feedback-dialog.d.ts +1 -1
  161. package/dist/feedback/feedback-dialog.js.map +1 -1
  162. package/dist/feedback/feedback-inbox.d.ts +3 -2
  163. package/dist/feedback/feedback-inbox.js.map +1 -1
  164. package/dist/feedback/feedback-thread.d.ts +68 -253
  165. package/dist/feedback/feedback-thread.js +46 -10
  166. package/dist/feedback/feedback-thread.js.map +1 -1
  167. package/dist/{kit-labels-D1J2_jyD.d.ts → feedback-BxeQVzwq.d.ts} +1206 -20
  168. package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-fGAzZPf0.d.ts} +178 -11
  169. package/dist/feedback.d.ts +66 -2
  170. package/dist/hooks/use-file-drop.d.ts +19 -14
  171. package/dist/i18n/defaults.d.ts +19 -14
  172. package/dist/i18n/defaults.js +19 -2
  173. package/dist/i18n/defaults.js.map +1 -1
  174. package/dist/i18n/german.d.ts +19 -14
  175. package/dist/i18n/german.js +84 -5
  176. package/dist/i18n/german.js.map +1 -1
  177. package/dist/i18n/kit-labels.d.ts +15 -10
  178. package/dist/i18n/kit-labels.js +8 -2
  179. package/dist/i18n/kit-labels.js.map +1 -1
  180. package/dist/i18n/languages.d.ts +19 -14
  181. package/dist/i18n/locales/de-CH.d.ts +19 -14
  182. package/dist/i18n/locales/en.d.ts +19 -14
  183. package/dist/i18n/locales/en.js +16 -0
  184. package/dist/i18n/locales/en.js.map +1 -1
  185. package/dist/i18n/locales/es.d.ts +19 -14
  186. package/dist/i18n/locales/es.js +83 -5
  187. package/dist/i18n/locales/es.js.map +1 -1
  188. package/dist/i18n/locales/fr.d.ts +19 -14
  189. package/dist/i18n/locales/fr.js +82 -5
  190. package/dist/i18n/locales/fr.js.map +1 -1
  191. package/dist/i18n/locales/hu.d.ts +19 -14
  192. package/dist/i18n/locales/hu.js +85 -5
  193. package/dist/i18n/locales/hu.js.map +1 -1
  194. package/dist/i18n/locales/it.d.ts +19 -14
  195. package/dist/i18n/locales/it.js +84 -5
  196. package/dist/i18n/locales/it.js.map +1 -1
  197. package/dist/i18n/locales/zh.d.ts +19 -14
  198. package/dist/i18n/locales/zh.js +80 -5
  199. package/dist/i18n/locales/zh.js.map +1 -1
  200. package/dist/i18n/review.d.ts +19 -14
  201. package/dist/i18n/review.js +27 -1
  202. package/dist/i18n/review.js.map +1 -1
  203. package/dist/index.d.ts +21 -10
  204. package/dist/index.js +73 -2
  205. package/dist/index.js.map +1 -1
  206. package/dist/lib/column-mapping.d.ts +81 -0
  207. package/dist/lib/column-mapping.js +108 -0
  208. package/dist/lib/column-mapping.js.map +1 -0
  209. package/dist/lib/countries.d.ts +43 -0
  210. package/dist/lib/countries.js +39 -0
  211. package/dist/lib/countries.js.map +1 -0
  212. package/dist/lib/decimal-marks.d.ts +46 -0
  213. package/dist/lib/decimal-marks.js +40 -0
  214. package/dist/lib/decimal-marks.js.map +1 -0
  215. package/dist/lib/iban.d.ts +98 -0
  216. package/dist/lib/iban.js +154 -0
  217. package/dist/lib/iban.js.map +1 -0
  218. package/dist/lib/isin.d.ts +30 -0
  219. package/dist/lib/isin.js +36 -0
  220. package/dist/lib/isin.js.map +1 -0
  221. package/dist/lib/phone.d.ts +69 -0
  222. package/dist/lib/phone.js +121 -0
  223. package/dist/lib/phone.js.map +1 -0
  224. package/dist/lib/table-text.d.ts +109 -1
  225. package/dist/lib/table-text.js +122 -1
  226. package/dist/lib/table-text.js.map +1 -1
  227. package/dist/rhf/fields.d.ts +233 -18
  228. package/dist/rhf/fields.js +456 -3
  229. package/dist/rhf/fields.js.map +1 -1
  230. package/dist/rhf/form.d.ts +19 -14
  231. package/dist/rhf.d.ts +22 -15
  232. package/dist/rhf.js.map +1 -1
  233. package/dist/shell/app-shell.d.ts +18 -13
  234. package/dist/shell/top-bar-brand.d.ts +19 -14
  235. package/dist/shell/topbar-action-menu.d.ts +36 -3
  236. package/dist/shell/topbar-action-menu.js +74 -33
  237. package/dist/shell/topbar-action-menu.js.map +1 -1
  238. package/dist/shell.d.ts +18 -13
  239. package/dist/table-text.d.ts +1 -1
  240. package/dist/tour/tour.js +20 -24
  241. package/dist/tour/tour.js.map +1 -1
  242. package/dist/wizard/stepper-nav.d.ts +49 -16
  243. package/dist/wizard/stepper-nav.js +4 -0
  244. package/dist/wizard/stepper-nav.js.map +1 -1
  245. package/dist/wizard.d.ts +19 -14
  246. package/package.json +1 -1
  247. package/src/components/account-chips.tsx +17 -3
  248. package/src/components/account-settings.tsx +92 -26
  249. package/src/components/amount-input.tsx +68 -56
  250. package/src/components/autocomplete.tsx +198 -114
  251. package/src/components/checkbox-group.tsx +229 -0
  252. package/src/components/checkbox.tsx +72 -8
  253. package/src/components/chip.tsx +80 -10
  254. package/src/components/column-mapper.tsx +666 -0
  255. package/src/components/combobox-core.tsx +82 -9
  256. package/src/components/combobox.tsx +661 -340
  257. package/src/components/confirm-dialog.tsx +12 -8
  258. package/src/components/country-select.tsx +556 -0
  259. package/src/components/currency-select.tsx +109 -33
  260. package/src/components/danger-confirm.tsx +429 -82
  261. package/src/components/data-table-labels.ts +16 -1
  262. package/src/components/data-table.tsx +175 -6
  263. package/src/components/date-picker.tsx +595 -71
  264. package/src/components/entity-combobox.tsx +235 -124
  265. package/src/components/field-parts.tsx +403 -0
  266. package/src/components/field-strip.tsx +149 -0
  267. package/src/components/file-button.tsx +36 -3
  268. package/src/components/file-dropzone.tsx +106 -38
  269. package/src/components/form-actions.tsx +147 -6
  270. package/src/components/iban-input.tsx +231 -0
  271. package/src/components/icon-picker.tsx +47 -6
  272. package/src/components/inline-edit-field.tsx +533 -0
  273. package/src/components/language-select.tsx +78 -0
  274. package/src/components/line-items.tsx +35 -2
  275. package/src/components/list.tsx +55 -6
  276. package/src/components/money-field.tsx +13 -0
  277. package/src/components/month-picker.tsx +400 -63
  278. package/src/components/multi-entity-combobox.tsx +207 -120
  279. package/src/components/multi-select.tsx +80 -58
  280. package/src/components/number-field.tsx +28 -46
  281. package/src/components/number-input.tsx +210 -43
  282. package/src/components/one-time-code-input.tsx +288 -0
  283. package/src/components/phone-input.tsx +299 -0
  284. package/src/components/reauth-dialog.tsx +17 -18
  285. package/src/components/sign-chip.tsx +103 -0
  286. package/src/components/swatch-picker.tsx +143 -31
  287. package/src/components/switch.tsx +58 -7
  288. package/src/components/tile-radio.tsx +145 -29
  289. package/src/components/time-input.tsx +8 -1
  290. package/src/components/toggle-group.tsx +206 -73
  291. package/src/components/trigger-aria.ts +5 -0
  292. package/src/components/ui.tsx +479 -138
  293. package/src/data-table.ts +2 -1
  294. package/src/feedback/feedback-attachment.tsx +597 -46
  295. package/src/feedback/feedback-dialog.tsx +9 -3
  296. package/src/feedback/feedback-inbox.tsx +3 -2
  297. package/src/feedback/feedback-thread.tsx +113 -9
  298. package/src/i18n/defaults.ts +18 -1
  299. package/src/i18n/german.ts +89 -0
  300. package/src/i18n/kit-labels.tsx +82 -2
  301. package/src/i18n/locales/en.ts +36 -4
  302. package/src/i18n/locales/es.ts +87 -0
  303. package/src/i18n/locales/fr.ts +87 -0
  304. package/src/i18n/locales/hu.ts +83 -0
  305. package/src/i18n/locales/it.ts +88 -0
  306. package/src/i18n/locales/zh.ts +76 -0
  307. package/src/i18n/review.ts +26 -0
  308. package/src/index.ts +71 -3
  309. package/src/lib/column-mapping.ts +234 -0
  310. package/src/lib/countries.ts +91 -0
  311. package/src/lib/decimal-marks.ts +99 -0
  312. package/src/lib/iban.ts +171 -0
  313. package/src/lib/isin.ts +62 -0
  314. package/src/lib/phone.ts +211 -0
  315. package/src/lib/table-text.ts +271 -0
  316. package/src/rhf/fields.tsx +860 -9
  317. package/src/rhf.ts +3 -1
  318. package/src/shell/topbar-action-menu.tsx +134 -38
  319. package/src/tour/tour.tsx +66 -28
  320. package/src/wizard/stepper-nav.tsx +35 -1
@@ -5,12 +5,25 @@ import { cn } from "../lib/cn";
5
5
  import { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID } from "./ui";
6
6
  import {
7
7
  ComboboxPanel,
8
+ endHintRowProps,
9
+ offersCreate,
8
10
  useComboboxCore,
9
11
  useComboboxFieldError,
10
12
  type ComboClearValue,
11
13
  type ComboOption,
12
14
  } from "./combobox-core";
13
15
  import { DEFAULT_COMBOBOX_LABELS, DEFAULT_COMMON_LABELS, useKitLabels } from "../i18n/kit-labels";
16
+ import {
17
+ EndHintRow,
18
+ FieldCaption,
19
+ LABEL_IN_ROW,
20
+ LockedReason,
21
+ StaticLabelRow,
22
+ useFieldHint,
23
+ useLockReason,
24
+ } from "./field-parts";
25
+ import { mergeDescribedBy } from "./choice-parts";
26
+ import { useCommitReason } from "./write-lock";
14
27
 
15
28
  export type { ComboClearValue, ComboOption } from "./combobox-core";
16
29
 
@@ -45,6 +58,10 @@ export interface EntityComboboxProps<V extends string | number, C extends ComboC
45
58
  placeholder?: string;
46
59
  searchPlaceholder?: string;
47
60
  emptyLabel?: string;
61
+ /** Offer a clear "×" in place of the chevron while something is chosen; it emits
62
+ * `clearValue`. A pointer shortcut inside the trigger, out of the tab order — the
63
+ * keyboard's way is Delete or Backspace on the CLOSED trigger (0.23, as
64
+ * {@link CountrySelect}). Not offered while `disabled` or locked. */
48
65
  clearable?: boolean;
49
66
  clearLabel?: string;
50
67
  /** The phone sheet's close button. Its own prop rather than a reuse of
@@ -53,14 +70,51 @@ export interface EntityComboboxProps<V extends string | number, C extends ComboC
53
70
  * so it is the one control here that MUST be in the reader's language. */
54
71
  closeLabel?: string;
55
72
  disabled?: boolean;
73
+ /**
74
+ * Why the choice cannot be changed — {@link Button}'s `disabledReason`, for a picker
75
+ * that SAVES on change (keksdose G2: the statement's account, where picking IS the
76
+ * write). The trigger stays focusable and `aria-disabled`, the list does not open,
77
+ * the clear "×" is gone and `onChange` never fires; the reason is in the kit
78
+ * {@link Tooltip} and on the trigger's `aria-describedby`. Wins over `disabled`.
79
+ * {@link CountrySelect}'s lock, which is built on the same core.
80
+ */
81
+ disabledReason?: ReactNode;
82
+ /**
83
+ * This picker COMMITS — choosing saves. Under a locked {@link WriteLockProvider} it is
84
+ * locked the `disabledReason` way with the lock's reason (which wins over its own).
85
+ * A picker inside a form with its own Save stays editable under the lock: leave this
86
+ * off there and put `commit` on the Save. No provider, or an unlocked one: no effect.
87
+ */
88
+ commit?: boolean;
56
89
  /** When set, a "create" row appears for a non-empty query with no exact match. */
57
90
  onCreate?: (query: string) => void;
58
91
  createLabel?: (query: string) => string;
92
+ /**
93
+ * Offer the create row with NOTHING typed too, worded as this — keksdose G9's
94
+ * "Create cash account", which mints a record under a default name and so needs no
95
+ * query. `onCreate` then receives `""`. Left out, the row needs a query, as before.
96
+ * See {@link InlineEntityCombobox}'s `createEmptyLabel`.
97
+ */
98
+ createEmptyLabel?: string;
99
+ /**
100
+ * The create row WRITES (it mints the record on the server) while picking does not.
101
+ * Under a locked {@link WriteLockProvider} the row stays in the list but cannot be
102
+ * taken, with the lock's reason as its second line — the picker itself stays live.
103
+ * keksdose dev#496: on the read-only demo choosing is draft state, minting is not.
104
+ */
105
+ createCommit?: boolean;
59
106
  /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */
60
107
  invalid?: boolean;
61
108
  /** What is wrong with the value, as {@link Input}'s `error`: rendered under the
62
109
  * field, on the trigger's `aria-describedby`, and implies `invalid`. */
63
110
  error?: ReactNode;
111
+ /**
112
+ * Standing advice — {@link Combobox}'s `hint`, so the pickers read like every other
113
+ * field (keksdose K4): plain TEXT is a caption under the field on the trigger's
114
+ * `aria-describedby`, before any error; a {@link FieldHint} "?" rides the label line,
115
+ * or with no label sits at the trigger's end edge, outside the box.
116
+ */
117
+ hint?: ReactNode;
64
118
  /** Narrow `options` client-side by the query. Default `true`; `false` shows them
65
119
  * as given (a server-ranked list). */
66
120
  filter?: boolean;
@@ -80,41 +134,49 @@ export interface EntityComboboxProps<V extends string | number, C extends ComboC
80
134
  * the shared field/anchor/dismiss/search primitives (no cmdk/Radix). For picking
81
135
  * several entities use {@link MultiEntityCombobox}.
82
136
  */
83
- export function EntityCombobox<V extends string | number, C extends ComboClearValue = null>({
84
- value,
85
- onChange,
86
- clearValue = null as C,
87
- options,
88
- loadOptions,
89
- loading,
90
- label,
91
- placeholder,
92
- searchPlaceholder,
93
- emptyLabel,
94
- clearable,
95
- clearLabel,
96
- closeLabel,
97
- disabled,
98
- onCreate,
99
- createLabel,
100
- className,
101
- invalid,
102
- error,
103
- filter,
104
- minChars,
105
- debounceMs,
106
- loadErrorLabel,
107
- "aria-label": ariaLabel,
108
- // The control's own wiring, taken off `rest` so it lands on the TRIGGER rather than
109
- // the wrapper — see MultiEntityCombobox: `Field`'s render-prop spreads `{ id,
110
- // aria-describedby, aria-invalid, aria-required }`, and on the role-less wrapper
111
- // div the label's `htmlFor` named nothing and the hint described nothing.
112
- id,
113
- "aria-describedby": ariaDescribedBy,
114
- "aria-invalid": ariaInvalid,
115
- "aria-required": ariaRequired,
116
- ...rest
117
- }: EntityComboboxProps<V, C>) {
137
+ export function EntityCombobox<V extends string | number, C extends ComboClearValue = null>(
138
+ props: EntityComboboxProps<V, C>,
139
+ ) {
140
+ const {
141
+ value,
142
+ onChange,
143
+ clearValue = null as C,
144
+ options,
145
+ loadOptions,
146
+ loading,
147
+ label,
148
+ placeholder,
149
+ searchPlaceholder,
150
+ emptyLabel,
151
+ clearable,
152
+ clearLabel,
153
+ closeLabel,
154
+ disabled,
155
+ disabledReason,
156
+ commit,
157
+ onCreate,
158
+ createLabel,
159
+ createEmptyLabel,
160
+ createCommit,
161
+ className,
162
+ invalid,
163
+ error,
164
+ hint,
165
+ filter,
166
+ minChars,
167
+ debounceMs,
168
+ loadErrorLabel,
169
+ "aria-label": ariaLabel,
170
+ // The control's own wiring, taken off `rest` so it lands on the TRIGGER rather than
171
+ // the wrapper — see MultiEntityCombobox: `Field`'s render-prop spreads `{ id,
172
+ // aria-describedby, aria-invalid, aria-required }`, and on the role-less wrapper
173
+ // div the label's `htmlFor` named nothing and the hint described nothing.
174
+ id,
175
+ "aria-describedby": ariaDescribedBy,
176
+ "aria-invalid": ariaInvalid,
177
+ "aria-required": ariaRequired,
178
+ ...rest
179
+ } = props;
118
180
  const core = useComboboxCore<V>({
119
181
  options,
120
182
  loadOptions,
@@ -123,10 +185,11 @@ export function EntityCombobox<V extends string | number, C extends ComboClearVa
123
185
  minChars,
124
186
  debounceMs,
125
187
  });
188
+ const hintParts = useFieldHint(hint, ariaDescribedBy);
126
189
  const field = useComboboxFieldError(
127
190
  error,
128
191
  invalid || ariaInvalid === true || ariaInvalid === "true",
129
- ariaDescribedBy,
192
+ hintParts.describedBy,
130
193
  );
131
194
  // The props are the per-instance overrides, the provider the app-wide ones; a
132
195
  // prop left `undefined` falls through to the provider rather than masking it.
@@ -138,6 +201,16 @@ export function EntityCombobox<V extends string | number, C extends ComboClearVa
138
201
  });
139
202
  const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
140
203
  const { open, results, resolve, setOpen, rememberOption, query, triggerRef } = core;
204
+ // The lock: this picker's own reason, or the provider's under `commit`.
205
+ const lock = useLockReason(commit, disabledReason);
206
+ const inert = lock.locked || Boolean(disabled);
207
+ // A lock arriving while the list is up closes it — adjusted while
208
+ // rendering, as the core adjusts its own query, so no frame offers a pick that would
209
+ // be thrown away.
210
+ if (lock.locked && open) setOpen(false);
211
+ // The create row's own lock, for a picker whose choosing is draft and whose create
212
+ // is the write (`createCommit`).
213
+ const createReason = useCommitReason(createCommit, undefined);
141
214
  // One id per instance, generated here rather than in the core: `aria-controls` on
142
215
  // the trigger has to name the list while the list is still closed, so the id
143
216
  // belongs to whoever renders both ends of it.
@@ -147,13 +220,28 @@ export function EntityCombobox<V extends string | number, C extends ComboClearVa
147
220
  const chosen: V | null = value == null || value === clearValue ? null : (value as V);
148
221
  const selectedOption = chosen == null ? null : resolve(chosen);
149
222
  const q = query.trim();
150
- const showCreate =
151
- Boolean(onCreate) && q.length > 0 && !results.some((o) => o.label.toLowerCase() === q.toLowerCase());
152
- const showClear = Boolean(clearable && chosen != null && !disabled);
223
+ const showCreate = offersCreate(
224
+ onCreate,
225
+ q,
226
+ results.map((o) => o.label),
227
+ createEmptyLabel,
228
+ );
229
+ const showClear = Boolean(clearable && chosen != null && !inert);
153
230
  /** What the closed control is showing — the second half of its accessible name. */
154
231
  const triggerText = selectedOption?.label ?? placeholder ?? "";
232
+ // While locked the trigger sits in the reason's Tooltip — in a fragment with a hidden
233
+ // copy of the reason, Button's anatomy — `block` so a full-width field stays full width.
234
+ const withLock = (trigger: ReactNode) =>
235
+ lock.locked ? (
236
+ <LockedReason lock={lock} className="block">
237
+ {trigger}
238
+ </LockedReason>
239
+ ) : (
240
+ trigger
241
+ );
155
242
 
156
243
  const choose = (o: ComboOption<V>) => {
244
+ if (lock.locked) return;
157
245
  rememberOption(o);
158
246
  onChange(o.value);
159
247
  // Back to the trigger, not to <body>: the panel that held focus is about to
@@ -168,94 +256,114 @@ export function EntityCombobox<V extends string | number, C extends ComboClearVa
168
256
  // has one. Spread FIRST so the trigger's ARIA and the
169
257
  // handlers that open the panel cannot be clobbered from outside.
170
258
  <div {...rest} className={cn("relative", className)}>
171
- {label !== undefined && <FieldLabel>{label}</FieldLabel>}
172
- <button
173
- ref={triggerRef}
174
- id={id}
175
- type="button"
176
- // A combobox, not a button. The distinction is not pedantry: this control
177
- // carried `aria-invalid`, which `button` does not support, so a required
178
- // field left empty painted a rose border and told a reader nothing at all —
179
- // and ESLint flagged it as exactly that (`role-supports-aria-props`). The
180
- // fix the audit asked for is the role that describes what this IS: a closed
181
- // choice that expands into the list named below. `combobox` supports
182
- // `aria-invalid`, so the border and the announcement finally agree.
183
- role="combobox"
184
- aria-expanded={open}
185
- aria-controls={listboxId}
186
- aria-haspopup="listbox"
187
- // The label is a floating <span>, not a <label for>, so without this the
188
- // trigger's accessible name is whatever value happens to be selected —
189
- // "Checking" with nothing saying it is the account. The label AND the
190
- // value, because `aria-label` replaces the content rather than adding to
191
- // it, and a control that announces only its name has lost the answer.
192
- // A caller's own name wins over the composition: two fields labelled
193
- // "Account" on a transfer form are the from and the to, and only the
194
- // caller knows which is which.
195
- aria-label={
196
- ariaLabel ??
197
- (typeof label === "string" ? common.fieldValue(label, triggerText) : undefined)
198
- }
199
- disabled={disabled}
200
- aria-invalid={field.isInvalid || undefined}
201
- aria-describedby={field.describedBy}
202
- aria-required={ariaRequired}
203
- onClick={() => !disabled && setOpen((o) => !o)}
204
- // Down/Up opens the list from the closed trigger, per the APG. Enter and
205
- // Space already do it through the button's own click.
206
- onKeyDown={(e) => {
207
- if (disabled) return;
208
- if (e.key === "ArrowDown" || e.key === "ArrowUp") {
209
- e.preventDefault();
210
- setOpen(true);
211
- }
212
- }}
213
- className={cn(
214
- FIELD_TRIGGER,
215
- "pe-9",
216
- label !== undefined && FIELD_FLOATING_PAD,
217
- disabled && "cursor-not-allowed opacity-50",
218
- field.isInvalid && FIELD_INVALID,
219
- )}
220
- >
221
- <span className="flex min-w-0 items-center gap-2">
222
- {selectedOption?.icon && <span className="shrink-0">{selectedOption.icon}</span>}
223
- <span
259
+ {label !== undefined && hintParts.labelHint === undefined && <FieldLabel>{label}</FieldLabel>}
260
+ <EndHintRow {...endHintRowProps("hint" in props, label !== undefined, hintParts.labelHint)}>
261
+ {withLock(
262
+ <button
263
+ ref={triggerRef}
264
+ id={id}
265
+ type="button"
266
+ // A combobox, not a button. The distinction is not pedantry: this control
267
+ // carried `aria-invalid`, which `button` does not support, so a required
268
+ // field left empty painted a rose border and told a reader nothing at all —
269
+ // and ESLint flagged it as exactly that (`role-supports-aria-props`). The
270
+ // fix the audit asked for is the role that describes what this IS: a closed
271
+ // choice that expands into the list named below. `combobox` supports
272
+ // `aria-invalid`, so the border and the announcement finally agree.
273
+ role="combobox"
274
+ aria-expanded={open}
275
+ aria-controls={listboxId}
276
+ aria-haspopup="listbox"
277
+ // The label is a floating <span>, not a <label for>, so without this the
278
+ // trigger's accessible name is whatever value happens to be selected —
279
+ // "Checking" with nothing saying it is the account. The label AND the
280
+ // value, because `aria-label` replaces the content rather than adding to
281
+ // it, and a control that announces only its name has lost the answer.
282
+ // A caller's own name wins over the composition: two fields labelled
283
+ // "Account" on a transfer form are the from and the to, and only the
284
+ // caller knows which is which.
285
+ aria-label={
286
+ ariaLabel ??
287
+ (typeof label === "string" ? common.fieldValue(label, triggerText) : undefined)
288
+ }
289
+ // The focusable kind of disabled while locked (Button's rule): a native
290
+ // `disabled` would take the field out of the tab order, and the reason with it.
291
+ disabled={lock.locked ? undefined : disabled}
292
+ aria-disabled={lock.locked || undefined}
293
+ aria-invalid={field.isInvalid || undefined}
294
+ aria-describedby={mergeDescribedBy(field.describedBy, lock.locked && lock.reasonId)}
295
+ aria-required={ariaRequired}
296
+ onClick={() => !inert && setOpen((o) => !o)}
297
+ // Down/Up opens the list from the closed trigger, per the APG. Enter and
298
+ // Space already do it through the button's own click.
299
+ onKeyDown={(e) => {
300
+ if (inert) return;
301
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
302
+ e.preventDefault();
303
+ setOpen(true);
304
+ } else if (showClear && !open && (e.key === "Delete" || e.key === "Backspace")) {
305
+ // The keyboard's clear: the "×" is a pointer shortcut out of the tab
306
+ // order, so without this a cleared-by-mouse field was set-only from the
307
+ // keyboard. The closed trigger only — CountrySelect's rule (0.23).
308
+ e.preventDefault();
309
+ onChange(clearValue);
310
+ }
311
+ }}
224
312
  className={cn(
225
- "truncate",
226
- // A chosen value is the field's VALUE, so it is set in the same ink an
227
- // <input>'s value is — FIELD_BASE's own text colour, inherited rather
228
- // than restated (Keksdose dev#477). It used to be one notch lighter
229
- // than the typeahead fields beside it, which is visible when a picker
230
- // and a text field share a form row. Nothing selected keeps the
231
- // placeholder tone (`--text-placeholder`), which every field here
232
- // agrees on.
233
- !selectedOption && "text-[var(--text-placeholder)]",
313
+ FIELD_TRIGGER,
314
+ "pe-9",
315
+ label !== undefined && FIELD_FLOATING_PAD,
316
+ inert && "cursor-not-allowed opacity-50",
317
+ field.isInvalid && FIELD_INVALID,
234
318
  )}
235
319
  >
236
- {selectedOption?.label ?? placeholder ?? ""}
237
- </span>
238
- </span>
239
- {showClear ? (
240
- /* eslint-disable-next-line jsx-a11y/click-events-have-key-events -- tabIndex -1 inside
241
- the trigger <button>, so it never holds focus and a key handler here could
242
- never fire; the keys go to the trigger. A pointer shortcut only. */
243
- <span
244
- role="button"
245
- tabIndex={-1}
246
- aria-label={labels.clear}
247
- onClick={(e) => {
248
- e.stopPropagation();
249
- onChange(clearValue);
250
- }}
251
- className="absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]"
252
- >
253
- <X className="size-4" />
254
- </span>
255
- ) : (
256
- <FieldChevron />
320
+ <span className="flex min-w-0 items-center gap-2">
321
+ {selectedOption?.icon && <span className="shrink-0">{selectedOption.icon}</span>}
322
+ <span
323
+ className={cn(
324
+ "truncate",
325
+ // A chosen value is the field's VALUE, so it is set in the same ink an
326
+ // <input>'s value is — FIELD_BASE's own text colour, inherited rather
327
+ // than restated (Keksdose dev#477). It used to be one notch lighter
328
+ // than the typeahead fields beside it, which is visible when a picker
329
+ // and a text field share a form row. Nothing selected keeps the
330
+ // placeholder tone (`--text-placeholder`), which every field here
331
+ // agrees on.
332
+ !selectedOption && "text-[var(--text-placeholder)]",
333
+ )}
334
+ >
335
+ {selectedOption?.label ?? placeholder ?? ""}
336
+ </span>
337
+ </span>
338
+ {showClear ? (
339
+ /* eslint-disable-next-line jsx-a11y/click-events-have-key-events -- tabIndex -1 inside
340
+ the trigger <button>, so it never holds focus and a key handler here could
341
+ never fire; the keys go to the trigger, where Delete and Backspace clear. */
342
+ <span
343
+ role="button"
344
+ tabIndex={-1}
345
+ aria-label={labels.clear}
346
+ onClick={(e) => {
347
+ e.stopPropagation();
348
+ onChange(clearValue);
349
+ }}
350
+ className="absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]"
351
+ >
352
+ <X className="size-4" />
353
+ </span>
354
+ ) : (
355
+ <FieldChevron />
356
+ )}
357
+ </button>,
257
358
  )}
258
- </button>
359
+ </EndHintRow>
360
+ {/* With a "?" the label shares the top strip with it — after the trigger, so the
361
+ "?" follows the control in the tab order, as on Input and Select. */}
362
+ {label !== undefined && hintParts.labelHint !== undefined && (
363
+ <StaticLabelRow hint={hintParts.labelHint}>
364
+ <FieldLabel className={LABEL_IN_ROW}>{label}</FieldLabel>
365
+ </StaticLabelRow>
366
+ )}
259
367
  <ComboboxPanel
260
368
  core={core}
261
369
  listboxId={listboxId}
@@ -270,11 +378,14 @@ export function EntityCombobox<V extends string | number, C extends ComboClearVa
270
378
  onChoose={choose}
271
379
  showCreate={showCreate}
272
380
  onCreate={() => {
381
+ if (lock.locked) return;
273
382
  onCreate?.(q);
274
383
  core.closeToTrigger();
275
384
  }}
276
- createContent={labels.create(q)}
385
+ createContent={q ? labels.create(q) : createEmptyLabel}
386
+ createDisabledReason={createReason}
277
387
  />
388
+ <FieldCaption parts={hintParts} />
278
389
  {field.errorEl}
279
390
  </div>
280
391
  );