@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
@@ -4,6 +4,8 @@ import { cn } from "../lib/cn";
4
4
  import { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID } from "./ui";
5
5
  import {
6
6
  ComboboxPanel,
7
+ endHintRowProps,
8
+ offersCreate,
7
9
  useComboboxCore,
8
10
  useComboboxFieldError,
9
11
  type ComboOption,
@@ -14,6 +16,17 @@ import {
14
16
  useKitLabels,
15
17
  useKitLocale,
16
18
  } from "../i18n/kit-labels";
19
+ import {
20
+ EndHintRow,
21
+ FieldCaption,
22
+ LABEL_IN_ROW,
23
+ LockedReason,
24
+ StaticLabelRow,
25
+ useFieldHint,
26
+ useLockReason,
27
+ } from "./field-parts";
28
+ import { mergeDescribedBy } from "./choice-parts";
29
+ import { useCommitReason } from "./write-lock";
17
30
 
18
31
  /** `onChange` is the kit's — "a selection was made", carrying values — rather
19
32
  * than the div's form event, so the DOM's spelling of it is omitted. */
@@ -35,6 +48,9 @@ export interface MultiEntityComboboxProps<V extends string | number>
35
48
  /** Trigger summary when 1+ selected (default: the labels as a locale-formatted
36
49
  * list, or `combobox.selectedCount` when a label can't be resolved). */
37
50
  itemLabel?: (count: number) => string;
51
+ /** Offer a clear "×" while anything is chosen; it emits `[]`. A pointer shortcut out
52
+ * of the tab order — the keyboard's way is Delete or Backspace on the CLOSED trigger,
53
+ * or unticking rows in the open list. Not offered while `disabled` or locked. */
38
54
  clearable?: boolean;
39
55
  clearLabel?: string;
40
56
  /** The phone sheet's close button. Its own prop rather than a reuse of
@@ -43,15 +59,37 @@ export interface MultiEntityComboboxProps<V extends string | number>
43
59
  * so it is the one control here that MUST be in the reader's language. */
44
60
  closeLabel?: string;
45
61
  disabled?: boolean;
62
+ /** Why the selection cannot be changed — {@link EntityCombobox}'s `disabledReason`:
63
+ * focusable and `aria-disabled`, no list, no clear, no `onChange`, the reason in the
64
+ * kit {@link Tooltip}. Wins over `disabled`. */
65
+ disabledReason?: ReactNode;
66
+ /** This picker COMMITS — toggling saves. Locked the `disabledReason` way under a
67
+ * locked {@link WriteLockProvider}. See {@link EntityCombobox}'s `commit`. */
68
+ commit?: boolean;
46
69
  /** When set, a "create" row appears for a non-empty query with no exact match.
47
70
  * The panel stays open (adding the new entity to `value` is the caller's job). */
48
71
  onCreate?: (query: string) => void;
49
72
  createLabel?: (query: string) => string;
73
+ /** Offer the create row with nothing typed too, worded as this. See
74
+ * {@link EntityCombobox}'s `createEmptyLabel`. */
75
+ createEmptyLabel?: string;
76
+ /** The create row is a write while toggling is not: locked on its own under a locked
77
+ * {@link WriteLockProvider}. See {@link EntityCombobox}'s `createCommit`. */
78
+ createCommit?: boolean;
50
79
  /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */
51
80
  invalid?: boolean;
52
81
  /** What is wrong with the value, as {@link Input}'s `error`: rendered under the
53
82
  * field, on the trigger's `aria-describedby`, and implies `invalid`. */
54
83
  error?: ReactNode;
84
+ /**
85
+ * Standing advice — the rest of the Combobox family's `hint` since 0.22, which this
86
+ * one was left without (keksdose K4): plain TEXT is a caption under the field on the
87
+ * trigger's `aria-describedby`, before any error; a {@link FieldHint} "?" rides the
88
+ * label line, or with no label sits at the trigger's end edge, outside the box.
89
+ * Passed at all — even `undefined` — the field keeps one box, so a hint coming and
90
+ * going never rebuilds the trigger under the cursor (the 0.22 rule).
91
+ */
92
+ hint?: ReactNode;
55
93
  /** Narrow `options` client-side by the query. Default `true`; `false` shows them
56
94
  * as given (a server-ranked list). */
57
95
  filter?: boolean;
@@ -69,42 +107,48 @@ export interface MultiEntityComboboxProps<V extends string | number>
69
107
  * with the same async `loadOptions`/`onCreate` support. Rows show a checkbox and
70
108
  * toggle without closing the panel; the trigger summarises the selection.
71
109
  */
72
- export function MultiEntityCombobox<V extends string | number>({
73
- value,
74
- onChange,
75
- options,
76
- loadOptions,
77
- loading,
78
- label,
79
- placeholder,
80
- searchPlaceholder,
81
- emptyLabel,
82
- itemLabel,
83
- clearable,
84
- clearLabel,
85
- closeLabel,
86
- disabled,
87
- onCreate,
88
- createLabel,
89
- className,
90
- invalid,
91
- error,
92
- filter,
93
- minChars,
94
- debounceMs,
95
- loadErrorLabel,
96
- "aria-label": ariaLabel,
97
- // The control's own wiring, taken off `rest` so it lands on the TRIGGER rather than
98
- // the wrapper: `Field`'s render-prop spreads `{ id, aria-describedby, aria-invalid,
99
- // aria-required }`, and on the role-less wrapper div the label's `htmlFor` named
100
- // nothing and the hint and required state described nothing — kastlan's
101
- // communication-page To/Cc/Bcc and lease-tenants-step.tsx read as unnamed pickers.
102
- id,
103
- "aria-describedby": ariaDescribedBy,
104
- "aria-invalid": ariaInvalid,
105
- "aria-required": ariaRequired,
106
- ...rest
107
- }: MultiEntityComboboxProps<V>) {
110
+ export function MultiEntityCombobox<V extends string | number>(props: MultiEntityComboboxProps<V>) {
111
+ const {
112
+ value,
113
+ onChange,
114
+ options,
115
+ loadOptions,
116
+ loading,
117
+ label,
118
+ placeholder,
119
+ searchPlaceholder,
120
+ emptyLabel,
121
+ itemLabel,
122
+ clearable,
123
+ clearLabel,
124
+ closeLabel,
125
+ disabled,
126
+ disabledReason,
127
+ commit,
128
+ onCreate,
129
+ createLabel,
130
+ createEmptyLabel,
131
+ createCommit,
132
+ className,
133
+ invalid,
134
+ error,
135
+ hint,
136
+ filter,
137
+ minChars,
138
+ debounceMs,
139
+ loadErrorLabel,
140
+ "aria-label": ariaLabel,
141
+ // The control's own wiring, taken off `rest` so it lands on the TRIGGER rather than
142
+ // the wrapper: `Field`'s render-prop spreads `{ id, aria-describedby, aria-invalid,
143
+ // aria-required }`, and on the role-less wrapper div the label's `htmlFor` named
144
+ // nothing and the hint and required state described nothing — kastlan's
145
+ // communication-page To/Cc/Bcc and lease-tenants-step.tsx read as unnamed pickers.
146
+ id,
147
+ "aria-describedby": ariaDescribedBy,
148
+ "aria-invalid": ariaInvalid,
149
+ "aria-required": ariaRequired,
150
+ ...rest
151
+ } = props;
108
152
  const core = useComboboxCore<V>({
109
153
  options,
110
154
  loadOptions,
@@ -113,10 +157,11 @@ export function MultiEntityCombobox<V extends string | number>({
113
157
  minChars,
114
158
  debounceMs,
115
159
  });
160
+ const hintParts = useFieldHint(hint, ariaDescribedBy);
116
161
  const field = useComboboxFieldError(
117
162
  error,
118
163
  invalid || ariaInvalid === true || ariaInvalid === "true",
119
- ariaDescribedBy,
164
+ hintParts.describedBy,
120
165
  );
121
166
  // The props are the per-instance overrides, the provider the app-wide ones; a
122
167
  // prop left `undefined` falls through to the provider rather than masking it.
@@ -128,6 +173,11 @@ export function MultiEntityCombobox<V extends string | number>({
128
173
  });
129
174
  const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
130
175
  const { open, results, resolve, setOpen, query, triggerRef } = core;
176
+ // The lock — see EntityCombobox: the trigger stays reachable and says why.
177
+ const lock = useLockReason(commit, disabledReason);
178
+ const inert = lock.locked || Boolean(disabled);
179
+ if (lock.locked && open) setOpen(false);
180
+ const createReason = useCommitReason(createCommit, undefined);
131
181
  // One id per instance, generated here rather than in the core: `aria-controls` on
132
182
  // the trigger has to name the list while the list is still closed, so the id
133
183
  // belongs to whoever renders both ends of it.
@@ -154,12 +204,27 @@ export function MultiEntityCombobox<V extends string | number>({
154
204
  : labels.selectedCount(value.length);
155
205
  })();
156
206
 
207
+ // While locked the trigger sits in the reason's Tooltip, as on EntityCombobox.
208
+ const withLock = (trigger: ReactNode) =>
209
+ lock.locked ? (
210
+ <LockedReason lock={lock} className="block">
211
+ {trigger}
212
+ </LockedReason>
213
+ ) : (
214
+ trigger
215
+ );
216
+
157
217
  const q = query.trim();
158
- const showCreate =
159
- Boolean(onCreate) && q.length > 0 && !results.some((o) => o.label.toLowerCase() === q.toLowerCase());
160
- const showClear = Boolean(clearable && value.length > 0 && !disabled);
218
+ const showCreate = offersCreate(
219
+ onCreate,
220
+ q,
221
+ results.map((o) => o.label),
222
+ createEmptyLabel,
223
+ );
224
+ const showClear = Boolean(clearable && value.length > 0 && !inert);
161
225
 
162
226
  const toggle = (o: ComboOption<V>) => {
227
+ if (lock.locked) return;
163
228
  core.rememberOption(o);
164
229
  if (valueSet.has(o.value)) onChange(value.filter((v) => v !== o.value));
165
230
  else onChange([...value, o.value]);
@@ -172,88 +237,107 @@ export function MultiEntityCombobox<V extends string | number>({
172
237
  // has one. Spread FIRST so the trigger's ARIA and the
173
238
  // handlers that open the panel cannot be clobbered from outside.
174
239
  <div {...rest} className={cn("relative", className)}>
175
- {label !== undefined && <FieldLabel>{label}</FieldLabel>}
176
- <button
177
- ref={triggerRef}
178
- id={id}
179
- type="button"
180
- // A combobox, not a button. The distinction is not pedantry: this control
181
- // carried `aria-invalid`, which `button` does not support, so a required
182
- // field left empty painted a rose border and told a reader nothing at all —
183
- // and ESLint flagged it as exactly that (`role-supports-aria-props`). The
184
- // fix the audit asked for is the role that describes what this IS: a closed
185
- // choice that expands into the list named below. `combobox` supports
186
- // `aria-invalid`, so the border and the announcement finally agree.
187
- role="combobox"
188
- aria-expanded={open}
189
- aria-controls={listboxId}
190
- aria-haspopup="listbox"
191
- // The label is a floating <span>, not a <label for>, so without this the
192
- // trigger's accessible name is whatever value happens to be selected —
193
- // "Checking" with nothing saying it is the account. The label AND the
194
- // value, because `aria-label` replaces the content rather than adding to
195
- // it, and a control that announces only its name has lost the answer.
196
- // A caller's own name wins over the composition: two fields labelled
197
- // "Account" on a transfer form are the from and the to, and only the
198
- // caller knows which is which.
199
- aria-label={
200
- ariaLabel ??
201
- (typeof label === "string" ? common.fieldValue(label, summary) : undefined)
202
- }
203
- disabled={disabled}
204
- aria-invalid={field.isInvalid || undefined}
205
- aria-describedby={field.describedBy}
206
- aria-required={ariaRequired}
207
- onClick={() => !disabled && setOpen((o) => !o)}
208
- // Down/Up opens the list from the closed trigger, per the APG. Enter and
209
- // Space already do it through the button's own click.
210
- onKeyDown={(e) => {
211
- if (disabled) return;
212
- if (e.key === "ArrowDown" || e.key === "ArrowUp") {
213
- e.preventDefault();
214
- setOpen(true);
215
- }
216
- }}
217
- className={cn(
218
- FIELD_TRIGGER,
219
- "pe-9",
220
- label !== undefined && FIELD_FLOATING_PAD,
221
- disabled && "cursor-not-allowed opacity-50",
222
- field.isInvalid && FIELD_INVALID,
223
- )}
224
- >
225
- <span
226
- className={cn(
227
- "min-w-0 truncate",
228
- // Inherit FIELD_BASE's ink for a value, keep the placeholder tone
229
- // (`--text-placeholder`) — the same rule every field on a form row
230
- // follows (Keksdose dev#477).
231
- !value.length && "text-[var(--text-placeholder)]",
232
- )}
233
- >
234
- {summary}
235
- </span>
236
- {showClear ? (
237
- /* eslint-disable-next-line jsx-a11y/click-events-have-key-events -- tabIndex -1 inside
238
- the trigger <button>, so it never holds focus and a key handler here could
239
- never fire. A pointer shortcut; the keyboard path is unticking the options
240
- in the open list. */
241
- <span
242
- role="button"
243
- tabIndex={-1}
244
- aria-label={labels.clear}
245
- onClick={(e) => {
246
- e.stopPropagation();
247
- onChange([]);
240
+ {label !== undefined && hintParts.labelHint === undefined && <FieldLabel>{label}</FieldLabel>}
241
+ <EndHintRow {...endHintRowProps("hint" in props, label !== undefined, hintParts.labelHint)}>
242
+ {withLock(
243
+ <button
244
+ ref={triggerRef}
245
+ id={id}
246
+ type="button"
247
+ // A combobox, not a button. The distinction is not pedantry: this control
248
+ // carried `aria-invalid`, which `button` does not support, so a required
249
+ // field left empty painted a rose border and told a reader nothing at all —
250
+ // and ESLint flagged it as exactly that (`role-supports-aria-props`). The
251
+ // fix the audit asked for is the role that describes what this IS: a closed
252
+ // choice that expands into the list named below. `combobox` supports
253
+ // `aria-invalid`, so the border and the announcement finally agree.
254
+ role="combobox"
255
+ aria-expanded={open}
256
+ aria-controls={listboxId}
257
+ aria-haspopup="listbox"
258
+ // The label is a floating <span>, not a <label for>, so without this the
259
+ // trigger's accessible name is whatever value happens to be selected —
260
+ // "Checking" with nothing saying it is the account. The label AND the
261
+ // value, because `aria-label` replaces the content rather than adding to
262
+ // it, and a control that announces only its name has lost the answer.
263
+ // A caller's own name wins over the composition: two fields labelled
264
+ // "Account" on a transfer form are the from and the to, and only the
265
+ // caller knows which is which.
266
+ aria-label={
267
+ ariaLabel ??
268
+ (typeof label === "string" ? common.fieldValue(label, summary) : undefined)
269
+ }
270
+ // Focusable while locked — see EntityCombobox.
271
+ disabled={lock.locked ? undefined : disabled}
272
+ aria-disabled={lock.locked || undefined}
273
+ aria-invalid={field.isInvalid || undefined}
274
+ aria-describedby={mergeDescribedBy(field.describedBy, lock.locked && lock.reasonId)}
275
+ aria-required={ariaRequired}
276
+ onClick={() => !inert && setOpen((o) => !o)}
277
+ // Down/Up opens the list from the closed trigger, per the APG. Enter and
278
+ // Space already do it through the button's own click.
279
+ onKeyDown={(e) => {
280
+ if (inert) return;
281
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
282
+ e.preventDefault();
283
+ setOpen(true);
284
+ } else if (showClear && !open && (e.key === "Delete" || e.key === "Backspace")) {
285
+ // The keyboard's clear: the "×" is a pointer shortcut out of the tab
286
+ // order, so without this a cleared-by-mouse field was set-only from the
287
+ // keyboard. The closed trigger only — CountrySelect's rule (0.23).
288
+ e.preventDefault();
289
+ onChange([]);
290
+ }
248
291
  }}
249
- className="absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]"
292
+ className={cn(
293
+ FIELD_TRIGGER,
294
+ "pe-9",
295
+ label !== undefined && FIELD_FLOATING_PAD,
296
+ inert && "cursor-not-allowed opacity-50",
297
+ field.isInvalid && FIELD_INVALID,
298
+ )}
250
299
  >
251
- <X className="size-4" />
252
- </span>
253
- ) : (
254
- <FieldChevron />
300
+ <span
301
+ className={cn(
302
+ "min-w-0 truncate",
303
+ // Inherit FIELD_BASE's ink for a value, keep the placeholder tone
304
+ // (`--text-placeholder`) — the same rule every field on a form row
305
+ // follows (Keksdose dev#477).
306
+ !value.length && "text-[var(--text-placeholder)]",
307
+ )}
308
+ >
309
+ {summary}
310
+ </span>
311
+ {showClear ? (
312
+ /* eslint-disable-next-line jsx-a11y/click-events-have-key-events -- tabIndex -1 inside
313
+ the trigger <button>, so it never holds focus and a key handler here could
314
+ never fire. A pointer shortcut; the keyboard paths are Delete on the closed
315
+ trigger and unticking the options in the open list. */
316
+ <span
317
+ role="button"
318
+ tabIndex={-1}
319
+ aria-label={labels.clear}
320
+ onClick={(e) => {
321
+ e.stopPropagation();
322
+ onChange([]);
323
+ }}
324
+ className="absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]"
325
+ >
326
+ <X className="size-4" />
327
+ </span>
328
+ ) : (
329
+ <FieldChevron />
330
+ )}
331
+ </button>,
255
332
  )}
256
- </button>
333
+ </EndHintRow>
334
+ {/* With a "?" the label shares the top strip with it — after the trigger, so the
335
+ "?" follows the control in the tab order, as on EntityCombobox. */}
336
+ {label !== undefined && hintParts.labelHint !== undefined && (
337
+ <StaticLabelRow hint={hintParts.labelHint}>
338
+ <FieldLabel className={LABEL_IN_ROW}>{label}</FieldLabel>
339
+ </StaticLabelRow>
340
+ )}
257
341
  <ComboboxPanel
258
342
  core={core}
259
343
  listboxId={listboxId}
@@ -269,11 +353,14 @@ export function MultiEntityCombobox<V extends string | number>({
269
353
  onChoose={toggle}
270
354
  showCreate={showCreate}
271
355
  onCreate={() => {
356
+ if (lock.locked) return;
272
357
  onCreate?.(q);
273
358
  core.setQuery("");
274
359
  }}
275
- createContent={labels.create(q)}
360
+ createContent={q ? labels.create(q) : createEmptyLabel}
361
+ createDisabledReason={createReason}
276
362
  />
363
+ <FieldCaption parts={hintParts} />
277
364
  {field.errorEl}
278
365
  </div>
279
366
  );
@@ -9,6 +9,7 @@ import { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_INVALID, FIELD_FLOATING_
9
9
  import { cn } from "../lib/cn";
10
10
  import { DropdownPanel, DropdownSearchHeader, useDropdownSearch } from "./dropdown";
11
11
  import { useActiveOptionScroll, useComboboxFieldError } from "./combobox-core";
12
+ import { EndHintRow, FieldCaption, LABEL_IN_ROW, StaticLabelRow, useFieldHint } from "./field-parts";
12
13
  import {
13
14
  DEFAULT_COMMON_LABELS,
14
15
  DEFAULT_MULTI_SELECT_LABELS,
@@ -55,6 +56,13 @@ export interface MultiSelectProps extends Omit<ComponentPropsWithoutRef<"div">,
55
56
  * caller passed) and implies `invalid` — {@link Input}'s `error`, on the same
56
57
  * rules. */
57
58
  error?: ReactNode;
59
+ /**
60
+ * Standing advice for the field — {@link Combobox}'s `hint` (keksdose K4): plain TEXT
61
+ * is a caption under the field on the trigger's `aria-describedby`, before any error;
62
+ * a {@link FieldHint} "?" rides the label line, or with no label sits at the trigger's
63
+ * end edge, outside the box. Not an option's `hint`, which stays on its row.
64
+ */
65
+ hint?: ReactNode;
58
66
  /** The trigger cannot be opened: FIELD_BASE's settled grey, no chevron (as a
59
67
  * disabled {@link Select} drops its own), and a dimmed label. */
60
68
  disabled?: boolean;
@@ -75,6 +83,7 @@ export function MultiSelect({
75
83
  panelClassName,
76
84
  invalid,
77
85
  error,
86
+ hint,
78
87
  disabled,
79
88
  "aria-label": ariaLabel,
80
89
  "aria-describedby": ariaDescribedBy,
@@ -92,7 +101,8 @@ export function MultiSelect({
92
101
  selectedCount: itemLabel,
93
102
  });
94
103
  const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
95
- const field = useComboboxFieldError(error, invalid, ariaDescribedBy);
104
+ const hintParts = useFieldHint(hint, ariaDescribedBy);
105
+ const field = useComboboxFieldError(error, invalid, hintParts.describedBy);
96
106
  // One id per instance: `aria-controls` on the trigger names the list while the
97
107
  // list is still closed.
98
108
  const uid = useId();
@@ -163,64 +173,75 @@ export function MultiSelect({
163
173
  // the thing with a role. Spread FIRST so the ARIA the trigger composes, and the
164
174
  // handlers that open the list, cannot be replaced by accident from outside.
165
175
  <div {...rest} ref={wrapperRef} className={cn("relative", className)}>
166
- {label !== undefined && <FieldLabel className={disabled ? "opacity-50" : undefined}>{label}</FieldLabel>}
167
- <button
168
- ref={triggerRef}
169
- type="button"
170
- disabled={disabled}
171
- // A combobox, not a bare button: this trigger carried `aria-invalid`, which
172
- // `button` does not support, so a required-and-empty filter painted its rose
173
- // border and announced nothing (ESLint's `role-supports-aria-props`, the
174
- // audit's §a11y). The role that describes it is the one that also supports
175
- // the attribute.
176
- role="combobox"
177
- aria-expanded={open}
178
- aria-controls={listboxId}
179
- aria-haspopup="listbox"
180
- // The label is a floating <span>, not a <label for>, so without this the
181
- // trigger announces only its summary — "3 selected", with nothing saying
182
- // three of WHAT. The label AND the summary, because `aria-label` replaces
183
- // the content rather than adding to it; the same composition its two
184
- // siblings use, so a form row of pickers reads the same way throughout.
185
- // A caller's own name wins: two filters labelled "Accounts" on one screen have
186
- // to be told apart, and only the caller knows by what.
187
- aria-label={
188
- ariaLabel ?? (typeof label === "string" ? common.fieldValue(label, selectedSummary) : undefined)
189
- }
190
- onClick={() => {
191
- setOpen((v) => !v);
192
- setActive(0);
193
- }}
194
- // Down/Up opens the list from the closed trigger, per the APG; Enter and
195
- // Space already do through the button's own click.
196
- onKeyDown={(e) => {
197
- if (e.key === "ArrowDown" || e.key === "ArrowUp") {
198
- e.preventDefault();
199
- setOpen(true);
200
- setActive(0);
176
+ {label !== undefined && hintParts.labelHint === undefined && (
177
+ <FieldLabel className={disabled ? "opacity-50" : undefined}>{label}</FieldLabel>
178
+ )}
179
+ <EndHintRow hint={label === undefined ? hintParts.labelHint : undefined}>
180
+ <button
181
+ ref={triggerRef}
182
+ type="button"
183
+ disabled={disabled}
184
+ // A combobox, not a bare button: this trigger carried `aria-invalid`, which
185
+ // `button` does not support, so a required-and-empty filter painted its rose
186
+ // border and announced nothing (ESLint's `role-supports-aria-props`, the
187
+ // audit's §a11y). The role that describes it is the one that also supports
188
+ // the attribute.
189
+ role="combobox"
190
+ aria-expanded={open}
191
+ aria-controls={listboxId}
192
+ aria-haspopup="listbox"
193
+ // The label is a floating <span>, not a <label for>, so without this the
194
+ // trigger announces only its summary — "3 selected", with nothing saying
195
+ // three of WHAT. The label AND the summary, because `aria-label` replaces
196
+ // the content rather than adding to it; the same composition its two
197
+ // siblings use, so a form row of pickers reads the same way throughout.
198
+ // A caller's own name wins: two filters labelled "Accounts" on one screen have
199
+ // to be told apart, and only the caller knows by what.
200
+ aria-label={
201
+ ariaLabel ?? (typeof label === "string" ? common.fieldValue(label, selectedSummary) : undefined)
201
202
  }
202
- }}
203
- aria-invalid={field.isInvalid || undefined}
204
- // On the TRIGGER, where focus lands — the `aria-describedby` a caller put on
205
- // the component is taken off the wrapper for the same reason.
206
- aria-describedby={field.describedBy}
207
- className={cn(
208
- FIELD_TRIGGER,
209
- "pe-9",
210
- label !== undefined && FIELD_FLOATING_PAD,
211
- // FIELD_TRIGGER's hover would still light a trigger nothing can open.
212
- "disabled:hover:bg-[var(--bg-surface-2)]",
213
- // Dimmed like EntityCombobox's and MultiEntityCombobox's disabled triggers;
214
- // only the label was, so a disabled MultiSelect read as live (showcase audit).
215
- "disabled:cursor-not-allowed disabled:opacity-50",
216
- field.isInvalid && FIELD_INVALID,
217
- )}
218
- >
219
- {/* The summary IS the field's value, so it inherits FIELD_BASE's ink rather
220
- than restating a lighter one (Keksdose dev#477). */}
221
- <span className="truncate">{selectedSummary}</span>
222
- {!disabled && <FieldChevron />}
223
- </button>
203
+ onClick={() => {
204
+ setOpen((v) => !v);
205
+ setActive(0);
206
+ }}
207
+ // Down/Up opens the list from the closed trigger, per the APG; Enter and
208
+ // Space already do through the button's own click.
209
+ onKeyDown={(e) => {
210
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
211
+ e.preventDefault();
212
+ setOpen(true);
213
+ setActive(0);
214
+ }
215
+ }}
216
+ aria-invalid={field.isInvalid || undefined}
217
+ // On the TRIGGER, where focus lands — the `aria-describedby` a caller put on
218
+ // the component is taken off the wrapper for the same reason.
219
+ aria-describedby={field.describedBy}
220
+ className={cn(
221
+ FIELD_TRIGGER,
222
+ "pe-9",
223
+ label !== undefined && FIELD_FLOATING_PAD,
224
+ // FIELD_TRIGGER's hover would still light a trigger nothing can open.
225
+ "disabled:hover:bg-[var(--bg-surface-2)]",
226
+ // Dimmed like EntityCombobox's and MultiEntityCombobox's disabled triggers;
227
+ // only the label was, so a disabled MultiSelect read as live (showcase audit).
228
+ "disabled:cursor-not-allowed disabled:opacity-50",
229
+ field.isInvalid && FIELD_INVALID,
230
+ )}
231
+ >
232
+ {/* The summary IS the field's value, so it inherits FIELD_BASE's ink rather
233
+ than restating a lighter one (Keksdose dev#477). */}
234
+ <span className="truncate">{selectedSummary}</span>
235
+ {!disabled && <FieldChevron />}
236
+ </button>
237
+ </EndHintRow>
238
+ {/* With a "?" the label shares the top strip with it — after the trigger, so the
239
+ "?" follows the control in the tab order, as on Input and Select. */}
240
+ {label !== undefined && hintParts.labelHint !== undefined && (
241
+ <StaticLabelRow hint={hintParts.labelHint}>
242
+ <FieldLabel className={cn(LABEL_IN_ROW, disabled && "opacity-50")}>{label}</FieldLabel>
243
+ </StaticLabelRow>
244
+ )}
224
245
  {open && !disabled && (
225
246
  <DropdownPanel
226
247
  // 16rem by default; `panelClassName` is how a caller widens it (Keksdose
@@ -327,6 +348,7 @@ export function MultiSelect({
327
348
  {/* After the panel, not before it: the panel is `absolute` with no `top`, so it
328
349
  opens at its place in the flow — under the message, had the message come
329
350
  first. */}
351
+ <FieldCaption parts={hintParts} />
330
352
  {field.errorEl}
331
353
  </div>
332
354
  );