@elabs-ai/components-ui 4.0.0 → 4.2.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 (339) hide show
  1. package/README.md +10 -8
  2. package/dist/chunk-FCH4ZN6G.js +57 -0
  3. package/dist/chunk-FCH4ZN6G.js.map +1 -0
  4. package/dist/form.d.ts +28 -0
  5. package/dist/form.js +123 -0
  6. package/dist/form.js.map +1 -0
  7. package/dist/index.d.ts +2597 -188
  8. package/dist/index.js +12855 -3578
  9. package/dist/index.js.map +1 -1
  10. package/dist/lib/cn.js +10 -1
  11. package/dist/lib/cn.js.map +1 -1
  12. package/package.json +21 -7
  13. package/src/__contract__/attribution-panel.contract.test.tsx +49 -0
  14. package/src/__contract__/badge.contract.test.tsx +49 -0
  15. package/src/__contract__/bounded-number.contract.test.tsx +49 -0
  16. package/src/__contract__/button.contract.test.tsx +49 -0
  17. package/src/__contract__/color-picker.contract.test.tsx +49 -0
  18. package/src/__contract__/combobox.contract.test.tsx +49 -0
  19. package/src/__contract__/command-trigger.contract.test.tsx +49 -0
  20. package/src/__contract__/copyable-value.contract.test.tsx +49 -0
  21. package/src/__contract__/date-picker.contract.test.tsx +49 -0
  22. package/src/__contract__/date-range-picker.contract.test.tsx +49 -0
  23. package/src/__contract__/empty-state.contract.test.tsx +49 -0
  24. package/src/__contract__/error-state.contract.test.tsx +49 -0
  25. package/src/__contract__/icon-button.contract.test.tsx +49 -0
  26. package/src/__contract__/input.contract.test.tsx +49 -0
  27. package/src/__contract__/key-value-editor.contract.test.tsx +49 -0
  28. package/src/__contract__/list-editor.contract.test.tsx +49 -0
  29. package/src/__contract__/loading-state.contract.test.tsx +49 -0
  30. package/src/__contract__/metric-card.contract.test.tsx +49 -0
  31. package/src/__contract__/model-picker.contract.test.tsx +49 -0
  32. package/src/__contract__/nav-notifications.contract.test.tsx +49 -0
  33. package/src/__contract__/number-input.contract.test.tsx +49 -0
  34. package/src/__contract__/rating.contract.test.tsx +49 -0
  35. package/src/__contract__/schema-form.contract.test.tsx +49 -0
  36. package/src/__contract__/section-header.contract.test.tsx +49 -0
  37. package/src/__contract__/segmented-field.contract.test.tsx +49 -0
  38. package/src/__contract__/slider-number.contract.test.tsx +49 -0
  39. package/src/__contract__/status-badge.contract.test.tsx +49 -0
  40. package/src/__contract__/tag-input.contract.test.tsx +49 -0
  41. package/src/__contract__/text.contract.test.tsx +49 -0
  42. package/src/__contract__/textarea.contract.test.tsx +49 -0
  43. package/src/__contract__/timeline.contract.test.tsx +49 -0
  44. package/src/__contract__/toggle.contract.test.tsx +49 -0
  45. package/src/__contract__/transfer.contract.test.tsx +49 -0
  46. package/src/__contract__/tree-select.contract.test.tsx +49 -0
  47. package/src/__contract__/tree.contract.test.tsx +49 -0
  48. package/src/__contract__/virtual-select.contract.test.tsx +49 -0
  49. package/src/__contract__/workspace-picker.contract.test.tsx +49 -0
  50. package/src/_peer-deps.test.ts +59 -0
  51. package/src/blocks-comparison-table.stories.tsx +2 -2
  52. package/src/components/accordion/accordion.tsx +13 -4
  53. package/src/components/advanced-group/advanced-group.tsx +1 -1
  54. package/src/components/alert/alert.tsx +15 -4
  55. package/src/components/alert-dialog/alert-dialog.test.tsx +32 -0
  56. package/src/components/alert-dialog/alert-dialog.tsx +28 -6
  57. package/src/components/app-shell/app-shell.stories.tsx +44 -2
  58. package/src/components/app-shell/app-shell.test.tsx +99 -0
  59. package/src/components/app-shell/app-shell.tsx +56 -4
  60. package/src/components/app-sidebar/app-sidebar.stories.tsx +9 -1
  61. package/src/components/attribution-panel/attribution-panel.tsx +3 -1
  62. package/src/components/attribution-panel/attributions.generated.ts +83 -79
  63. package/src/components/avatar/avatar.tsx +4 -1
  64. package/src/components/badge/badge.stories.tsx +8 -0
  65. package/src/components/badge/badge.test.tsx +8 -0
  66. package/src/components/badge/badge.tsx +15 -5
  67. package/src/components/bento-grid/bento-grid.test.tsx +24 -3
  68. package/src/components/bento-grid/bento-grid.tsx +11 -1
  69. package/src/components/bounded-number/bounded-number.test.tsx +1 -1
  70. package/src/components/bounded-number/bounded-number.tsx +78 -75
  71. package/src/components/breadcrumb/breadcrumb.stories.tsx +38 -0
  72. package/src/components/breadcrumb/breadcrumb.test.tsx +24 -1
  73. package/src/components/breadcrumb/breadcrumb.tsx +10 -4
  74. package/src/components/button/button.stories.tsx +116 -8
  75. package/src/components/button/button.tsx +18 -12
  76. package/src/components/button-group/button-group.tsx +1 -1
  77. package/src/components/calendar/calendar.tsx +3 -2
  78. package/src/components/card/card.stories.tsx +11 -6
  79. package/src/components/card/card.test.tsx +24 -2
  80. package/src/components/card/card.tsx +48 -7
  81. package/src/components/carousel/carousel.test.tsx +169 -0
  82. package/src/components/carousel/carousel.tsx +93 -32
  83. package/src/components/change-review/change-review.stories.tsx +145 -1
  84. package/src/components/change-review/change-review.test.tsx +143 -0
  85. package/src/components/change-review/change-review.tsx +214 -24
  86. package/src/components/checkbox/checkbox.tsx +2 -1
  87. package/src/components/collapsible-panel/use-collapsible-panel.ts +0 -3
  88. package/src/components/color-picker/color-picker.tsx +22 -15
  89. package/src/components/combobox/combobox.stories.tsx +10 -0
  90. package/src/components/combobox/combobox.test.tsx +54 -0
  91. package/src/components/combobox/combobox.tsx +25 -11
  92. package/src/components/command/command.stories.tsx +16 -6
  93. package/src/components/command/command.test.tsx +88 -0
  94. package/src/components/command/command.tsx +38 -13
  95. package/src/components/command/index.ts +1 -0
  96. package/src/components/command-trigger/command-trigger.stories.tsx +39 -0
  97. package/src/components/command-trigger/command-trigger.test.tsx +18 -0
  98. package/src/components/command-trigger/command-trigger.tsx +70 -0
  99. package/src/components/command-trigger/index.ts +1 -0
  100. package/src/components/context-menu/context-menu.tsx +16 -7
  101. package/src/components/context-rail/context-rail.stories.tsx +285 -0
  102. package/src/components/context-rail/context-rail.test.tsx +263 -0
  103. package/src/components/context-rail/context-rail.tsx +678 -0
  104. package/src/components/context-rail/index.ts +1 -0
  105. package/src/components/copyable-value/copyable-value.stories.tsx +10 -0
  106. package/src/components/copyable-value/copyable-value.test.tsx +28 -1
  107. package/src/components/copyable-value/copyable-value.tsx +13 -2
  108. package/src/components/date-picker/date-picker.stories.tsx +9 -0
  109. package/src/components/date-picker/date-picker.test.tsx +52 -0
  110. package/src/components/date-picker/date-picker.tsx +13 -15
  111. package/src/components/date-range-picker/date-range-picker.stories.tsx +9 -0
  112. package/src/components/date-range-picker/date-range-picker.test.tsx +76 -1
  113. package/src/components/date-range-picker/date-range-picker.tsx +39 -28
  114. package/src/components/descriptions/descriptions.tsx +1 -1
  115. package/src/components/dialog/dialog.test.tsx +8 -3
  116. package/src/components/dialog/dialog.tsx +14 -6
  117. package/src/components/drawer/drawer.tsx +23 -6
  118. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +1 -1
  119. package/src/components/dropdown-menu/dropdown-menu.tsx +4 -4
  120. package/src/components/empty-state/empty-state.test.tsx +26 -0
  121. package/src/components/empty-state/empty-state.tsx +6 -1
  122. package/src/components/expand-dialog/expand-dialog.tsx +1 -2
  123. package/src/components/field/field-context.ts +59 -0
  124. package/src/components/field/field.stories.tsx +142 -0
  125. package/src/components/field/field.test.tsx +415 -0
  126. package/src/components/field/field.tsx +342 -0
  127. package/src/components/field/index.ts +9 -0
  128. package/src/components/field-row/field-row.stories.tsx +85 -1
  129. package/src/components/field-row/field-row.test.tsx +145 -0
  130. package/src/components/field-row/field-row.tsx +50 -6
  131. package/src/components/file-upload/file-upload.stories.tsx +16 -0
  132. package/src/components/file-upload/file-upload.test.tsx +90 -12
  133. package/src/components/file-upload/file-upload.tsx +164 -31
  134. package/src/components/form/form.tsx +8 -3
  135. package/src/components/hover-card/hover-card.tsx +1 -0
  136. package/src/components/icon-button/icon-button.test.tsx +1 -1
  137. package/src/components/icon-button/icon-button.tsx +3 -3
  138. package/src/components/input/input.stories.tsx +121 -0
  139. package/src/components/input/input.test.tsx +13 -0
  140. package/src/components/input/input.tsx +8 -4
  141. package/src/components/input-group/input-group.stories.tsx +74 -0
  142. package/src/components/input-group/input-group.tsx +18 -9
  143. package/src/components/input-otp/input-otp.tsx +8 -2
  144. package/src/components/keyboard-shortcuts/index.ts +6 -0
  145. package/src/components/keyboard-shortcuts/keyboard-shortcuts.stories.tsx +177 -0
  146. package/src/components/keyboard-shortcuts/keyboard-shortcuts.test.tsx +73 -0
  147. package/src/components/keyboard-shortcuts/keyboard-shortcuts.tsx +203 -0
  148. package/src/components/label/label.tsx +1 -1
  149. package/src/components/link-preview/link-preview.tsx +1 -1
  150. package/src/components/list-editor/list-editor.test.tsx +24 -0
  151. package/src/components/list-editor/list-editor.tsx +126 -52
  152. package/src/components/locale-provider/index.ts +6 -1
  153. package/src/components/locale-provider/locale-keys.test.ts +134 -0
  154. package/src/components/locale-provider/locale-provider.stories.tsx +89 -1
  155. package/src/components/locale-provider/locale-provider.test.tsx +257 -1
  156. package/src/components/locale-provider/locale-provider.tsx +63 -8
  157. package/src/components/locale-provider/messages.ts +822 -2
  158. package/src/components/match-highlight/match-highlight.stories.tsx +1 -1
  159. package/src/components/mention-input/mention-input-mirror.stories.tsx +9 -1
  160. package/src/components/mention-input/mention-input.stories.tsx +3 -3
  161. package/src/components/mention-input/mention-input.test.tsx +51 -1
  162. package/src/components/mention-input/mention-input.tsx +66 -46
  163. package/src/components/mention-input/mention-value.ts +8 -2
  164. package/src/components/menubar/menubar.tsx +16 -6
  165. package/src/components/metric-card/metric-card.stories.tsx +5 -3
  166. package/src/components/metric-card/metric-card.tsx +21 -2
  167. package/src/components/model-picker/model-picker.stories.tsx +63 -3
  168. package/src/components/model-picker/model-picker.test.tsx +63 -0
  169. package/src/components/model-picker/model-picker.tsx +62 -36
  170. package/src/components/nav-main/nav-main.tsx +8 -4
  171. package/src/components/nav-notifications/nav-notifications.stories.tsx +110 -0
  172. package/src/components/nav-notifications/nav-notifications.test.tsx +11 -0
  173. package/src/components/nav-notifications/nav-notifications.tsx +67 -9
  174. package/src/components/nav-user/nav-user.tsx +5 -5
  175. package/src/components/navigation-menu/navigation-menu.stories.tsx +22 -0
  176. package/src/components/navigation-menu/navigation-menu.test.tsx +174 -0
  177. package/src/components/navigation-menu/navigation-menu.tsx +204 -15
  178. package/src/components/number-input/number-input.stories.tsx +7 -3
  179. package/src/components/number-input/number-input.test.tsx +47 -0
  180. package/src/components/number-input/number-input.tsx +65 -24
  181. package/src/components/page-shell/page-shell.stories.tsx +163 -0
  182. package/src/components/page-shell/page-shell.test.tsx +229 -0
  183. package/src/components/page-shell/page-shell.tsx +108 -25
  184. package/src/components/pagination/pagination.test.tsx +109 -0
  185. package/src/components/pagination/pagination.tsx +27 -19
  186. package/src/components/popover/popover.tsx +1 -0
  187. package/src/components/progress/progress.tsx +2 -0
  188. package/src/components/radio-group/radio-group.tsx +1 -1
  189. package/src/components/rating/rating.tsx +3 -1
  190. package/src/components/resizable/resizable.stories.tsx +114 -2
  191. package/src/components/resizable/resizable.test.tsx +76 -0
  192. package/src/components/resizable/resizable.tsx +32 -6
  193. package/src/components/revision-timeline/revision-timeline.stories.tsx +8 -1
  194. package/src/components/revision-timeline/revision-timeline.test.tsx +26 -0
  195. package/src/components/revision-timeline/revision-timeline.tsx +53 -5
  196. package/src/components/schema-form/from-json-schema.test.ts +390 -0
  197. package/src/components/schema-form/from-json-schema.ts +375 -0
  198. package/src/components/schema-form/index.ts +81 -0
  199. package/src/components/schema-form/schema-form-spec.ts +698 -0
  200. package/src/components/schema-form/schema-form-store.ts +506 -0
  201. package/src/components/schema-form/schema-form.stories.tsx +566 -0
  202. package/src/components/schema-form/schema-form.test.tsx +1021 -0
  203. package/src/components/schema-form/schema-form.tsx +1381 -0
  204. package/src/components/section-header/section-header.stories.tsx +37 -0
  205. package/src/components/section-header/section-header.tsx +14 -3
  206. package/src/components/select/select.tsx +13 -7
  207. package/src/components/separator/separator.tsx +1 -0
  208. package/src/components/sheet/sheet.test.tsx +39 -0
  209. package/src/components/sheet/sheet.tsx +40 -9
  210. package/src/components/side-dock/index.ts +1 -0
  211. package/src/components/side-dock/side-dock.stories.tsx +137 -0
  212. package/src/components/side-dock/side-dock.test.tsx +299 -0
  213. package/src/components/side-dock/side-dock.tsx +515 -0
  214. package/src/components/sidebar/index.ts +1 -0
  215. package/src/components/sidebar/sidebar-frame.test.tsx +162 -0
  216. package/src/components/sidebar/sidebar.stories.tsx +421 -1
  217. package/src/components/sidebar/sidebar.test.tsx +71 -2
  218. package/src/components/sidebar/sidebar.tsx +264 -38
  219. package/src/components/skeleton/skeleton.tsx +1 -0
  220. package/src/components/skip-link/index.ts +1 -0
  221. package/src/components/skip-link/skip-link.stories.tsx +106 -0
  222. package/src/components/skip-link/skip-link.test.tsx +20 -0
  223. package/src/components/skip-link/skip-link.tsx +34 -0
  224. package/src/components/slider/slider.tsx +1 -1
  225. package/src/components/slider-number/slider-number.stories.tsx +4 -1
  226. package/src/components/slider-number/slider-number.test.tsx +1 -1
  227. package/src/components/sonner/sonner.tsx +2 -0
  228. package/src/components/spinner/spinner.test.tsx +18 -0
  229. package/src/components/spinner/spinner.tsx +14 -4
  230. package/src/components/split-panel/index.ts +6 -1
  231. package/src/components/split-panel/split-panel.stories.tsx +18 -2
  232. package/src/components/split-panel/split-panel.test.tsx +20 -0
  233. package/src/components/split-panel/split-panel.tsx +28 -8
  234. package/src/components/state-panel/state-panel.stories.tsx +52 -0
  235. package/src/components/state-panel/state-panel.test.tsx +125 -0
  236. package/src/components/state-panel/state-panel.tsx +110 -24
  237. package/src/components/status-badge/status-badge.stories.tsx +8 -0
  238. package/src/components/switch/switch.tsx +3 -1
  239. package/src/components/table/table.stories.tsx +64 -0
  240. package/src/components/table/table.test.tsx +149 -1
  241. package/src/components/table/table.tsx +71 -2
  242. package/src/components/tabs/index.ts +10 -1
  243. package/src/components/tabs/tabs.stories.tsx +48 -0
  244. package/src/components/tabs/tabs.test.tsx +67 -0
  245. package/src/components/tabs/tabs.tsx +176 -72
  246. package/src/components/tag-input/tag-input.tsx +6 -3
  247. package/src/components/team-switcher/team-switcher.tsx +35 -6
  248. package/src/components/textarea/textarea.tsx +2 -2
  249. package/src/components/theme-switcher/theme-switcher.stories.tsx +76 -0
  250. package/src/components/theme-switcher/theme-switcher.test.tsx +182 -0
  251. package/src/components/theme-switcher/theme-switcher.tsx +140 -12
  252. package/src/components/theme-switcher/use-theme-transition.ts +2 -2
  253. package/src/components/timeline/timeline.stories.tsx +27 -0
  254. package/src/components/timeline/timeline.tsx +10 -3
  255. package/src/components/toggle/toggle.tsx +7 -2
  256. package/src/components/toggle-group/toggle-group.tsx +2 -0
  257. package/src/components/toolbar/toolbar.stories.tsx +10 -2
  258. package/src/components/tooltip/tooltip.tsx +2 -1
  259. package/src/components/top-nav/top-nav.tsx +1 -1
  260. package/src/components/transfer/transfer.tsx +35 -30
  261. package/src/components/tree/tree.stories.tsx +2 -2
  262. package/src/components/tree/tree.test.tsx +41 -0
  263. package/src/components/tree/tree.tsx +91 -31
  264. package/src/components/tree/use-tree-keyboard.ts +37 -14
  265. package/src/components/tree-select/tree-select.stories.tsx +9 -0
  266. package/src/components/tree-select/tree-select.test.tsx +5 -0
  267. package/src/components/tree-select/tree-select.tsx +11 -14
  268. package/src/components/typography/prose.test.tsx +3 -2
  269. package/src/components/typography/prose.tsx +3 -3
  270. package/src/components/typography/typography.stories.tsx +228 -0
  271. package/src/components/view-toolbar/view-toolbar.stories.tsx +41 -1
  272. package/src/components/view-toolbar/view-toolbar.test.tsx +33 -0
  273. package/src/components/view-toolbar/view-toolbar.tsx +37 -4
  274. package/src/components/virtual-select/virtual-select.stories.tsx +9 -0
  275. package/src/components/virtual-select/virtual-select.test.tsx +29 -3
  276. package/src/components/virtual-select/virtual-select.tsx +56 -18
  277. package/src/components/wizard/wizard.test.tsx +9 -0
  278. package/src/components/wizard/wizard.tsx +13 -5
  279. package/src/components/workspace-picker/index.ts +3 -0
  280. package/src/components/workspace-picker/workspace-picker-state.ts +50 -0
  281. package/src/components/workspace-picker/workspace-picker.stories.tsx +126 -0
  282. package/src/components/workspace-picker/workspace-picker.test.tsx +207 -0
  283. package/src/components/workspace-picker/workspace-picker.tsx +172 -0
  284. package/src/illustrations/empty-list-illustration.tsx +26 -0
  285. package/src/illustrations/error-illustration.tsx +60 -0
  286. package/src/illustrations/first-run-illustration.tsx +27 -0
  287. package/src/illustrations/illustration-base.tsx +108 -0
  288. package/src/illustrations/illustrations.stories.tsx +94 -0
  289. package/src/illustrations/illustrations.test.tsx +98 -0
  290. package/src/illustrations/index.ts +16 -0
  291. package/src/illustrations/no-access-illustration.tsx +45 -0
  292. package/src/illustrations/no-results-illustration.tsx +46 -0
  293. package/src/illustrations/offline-illustration.tsx +37 -0
  294. package/src/illustrations/success-illustration.tsx +40 -0
  295. package/src/index.ts +80 -2
  296. package/src/lib/agent-event-model.test.ts +12 -0
  297. package/src/lib/agent-event-model.ts +42 -0
  298. package/src/lib/approval-option.test.ts +17 -0
  299. package/src/lib/approval-option.ts +53 -0
  300. package/src/lib/check-result.ts +33 -0
  301. package/src/lib/cn.ts +9 -0
  302. package/src/lib/csv.ts +48 -0
  303. package/src/lib/diff-line.test.ts +36 -0
  304. package/src/lib/diff-line.ts +63 -0
  305. package/src/lib/diff-rows.test.ts +76 -0
  306. package/src/lib/diff-rows.ts +94 -0
  307. package/src/lib/format-duration.test.ts +22 -0
  308. package/src/lib/format-duration.ts +30 -0
  309. package/src/lib/has-renderable-content.test.ts +49 -0
  310. package/src/lib/has-renderable-content.ts +32 -0
  311. package/src/lib/merge-refs.ts +2 -0
  312. package/src/lib/operating-mode.test.ts +36 -0
  313. package/src/lib/operating-mode.ts +81 -0
  314. package/src/lib/optional-peer.ts +59 -0
  315. package/src/lib/session-launch.ts +37 -0
  316. package/src/lib/slash-command.test.ts +44 -0
  317. package/src/lib/slash-command.ts +45 -0
  318. package/src/lib/trigger-query.test.ts +94 -0
  319. package/src/lib/trigger-query.ts +88 -0
  320. package/src/lib/use-controllable-state.ts +47 -0
  321. package/src/lib/use-mobile.test.tsx +47 -0
  322. package/src/lib/use-mobile.ts +34 -12
  323. package/src/motion.stories.tsx +6 -0
  324. package/src/templates-object-detail-hub.stories.tsx +2 -2
  325. package/src/templates-screen-states.stories.tsx +5 -5
  326. package/src/templates-settings.stories.tsx +1 -1
  327. package/src/blocks/sidebar-02/app-sidebar.tsx +0 -160
  328. package/src/blocks/sidebar-02/logo.tsx +0 -18
  329. package/src/blocks/sidebar-02/nav-main.tsx +0 -13
  330. package/src/blocks/sidebar-02/nav-notifications.tsx +0 -7
  331. package/src/blocks/sidebar-02/sidebar-02.stories.tsx +0 -23
  332. package/src/blocks/sidebar-02/team-switcher.tsx +0 -7
  333. package/src/blocks/sidebar-04/app-sidebar.tsx +0 -236
  334. package/src/blocks/sidebar-04/mail-context.tsx +0 -29
  335. package/src/blocks/sidebar-04/nav-user.tsx +0 -6
  336. package/src/blocks/sidebar-04/sidebar-04.stories.tsx +0 -150
  337. package/src/blocks/sidebar-05/app-sidebar.tsx +0 -375
  338. package/src/blocks/sidebar-05/sidebar-05.stories.tsx +0 -18
  339. package/src/blocks/sidebar-05/team-switcher.tsx +0 -6
@@ -1,22 +1,167 @@
1
- import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
1
+ "use client";
2
+
3
+ import {
4
+ createContext,
5
+ forwardRef,
6
+ useContext,
7
+ useId,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ type ComponentPropsWithoutRef,
12
+ type ElementRef,
13
+ type PointerEvent as ReactPointerEvent,
14
+ } from "react";
2
15
  import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
3
16
  import { cva } from "class-variance-authority";
4
17
  import { ChevronDown } from "lucide-react";
5
18
  import { cn } from "../../lib/cn";
6
19
 
20
+ /**
21
+ * Residual of #54 (issue #85, fix round 1 → round 2): a pointer move onto a
22
+ * trigger arms `@radix-ui/react-navigation-menu`'s root-level hover-intent
23
+ * open timer (`openTimerRef`, default `delayDuration` 200ms — confirmed
24
+ * unchanged through `@radix-ui/react-navigation-menu@1.2.22`, the current
25
+ * latest stable). Neither the click-open path (root `onItemSelect`) nor any
26
+ * dismiss path (Escape, outside-click, focus-out — all funnelled through one
27
+ * `ROOT_CONTENT_DISMISS` handler → `onItemDismiss`) clears that timer, so it
28
+ * fires later and silently reopens the panel even after an explicit
29
+ * dismissal.
30
+ *
31
+ * Round 1 ran Radix's own `onTriggerLeave` cancellation path (a synthetic
32
+ * `pointerout`) on every click. That clears the stray open timer, but
33
+ * `onTriggerLeave` does two things, not one — it ALSO unconditionally arms
34
+ * Radix's 150ms close timer (`startCloseTimer`/`closeTimerRef`), which
35
+ * nothing then cancels for a click-opened (or keyboard/touch-opened) menu,
36
+ * since it never gets a real pointerenter on its content. Every click-opened
37
+ * menu silently closed itself ~150ms later. No supported, finer-grained Radix
38
+ * API exists to clear only the open timer — the 6 context callbacks
39
+ * (`onTriggerEnter`/`onTriggerLeave`/`onContentEnter`/`onContentLeave`/
40
+ * `onItemSelect`/`onItemDismiss`) are the only reachable surface, and none of
41
+ * the internal refs (`openTimerRef`, `closeTimerRef`, `wasClickCloseRef`,
42
+ * `wasEscapeCloseRef`) are exported.
43
+ *
44
+ * Round 2 fix: stop reaching into Radix's internal timers via synthetic
45
+ * events entirely. `NavigationMenu` takes over `value`/`onValueChange` as a
46
+ * controlled proxy (transparent to a consumer's own controlled/uncontrolled
47
+ * usage) and rejects, ONCE, a reopen request for an item that was just
48
+ * dismissed (value → "") while a REAL mouse pointer was still resting on its
49
+ * trigger — exactly the #85 scenario (dismissed inside the hover-intent
50
+ * window, pointer never left). The guard is a one-shot, pointer-scoped
51
+ * suppression per item (`suppressReopenForRef` in `NavigationMenu`, armed by
52
+ * `NavigationMenuItemValueContext` + a small guard context), cleared the
53
+ * instant ANY of the following happens, so it can never block a genuine
54
+ * interaction: (a) the real pointer leaves that trigger (a fresh hover-intent
55
+ * cycle is legitimate — Radix's own `wasEscapeCloseRef`/`wasClickCloseRef`
56
+ * guards reset on the same real pointerenter), or (b) any click on that
57
+ * trigger (`noteExplicitActivation`, which runs synchronously before Radix's
58
+ * own `onItemSelect` in the same click dispatch, per
59
+ * `composeEventHandlers(props.onClick, () => context.onItemSelect(...))` in
60
+ * `@radix-ui/react-navigation-menu`'s Trigger). A pure keyboard sequence never
61
+ * touches this guard at all (no pointer event ever marks the trigger as
62
+ * "resting"), so it cannot regress keyboard or touch entry — see the 3
63
+ * scenarios locked in `navigation-menu.test.tsx` (pointer dismiss stays
64
+ * dismissed, click-open survives past the close-timer delay with no pointer
65
+ * movement, keyboard open/Escape/no-reopen).
66
+ *
67
+ * `NavigationMenuItem` is wrapped (was a bare re-export) purely so its
68
+ * resolved item value — the same value Radix's OWN auto-`useId()` fallback
69
+ * would otherwise compute internally and never expose — is available to
70
+ * `NavigationMenuTrigger` without inventing a new public prop or reaching
71
+ * into Radix's non-exported item context.
72
+ *
73
+ * Tracked upstream: https://github.com/radix-ui/primitives/issues (file and
74
+ * link the specific issue here once opened — non-blocking, parallel to this
75
+ * interim fix; confirmed still present in 1.2.22, the latest stable release
76
+ * as of this fix).
77
+ */
78
+ type NavigationMenuGuard = {
79
+ notePointerPresence(itemValue: string, present: boolean): void;
80
+ noteExplicitActivation(itemValue: string): void;
81
+ /** One-shot: returns true (and clears the flag) iff this reopen should be swallowed. */
82
+ consumeSuppressionFlag(itemValue: string): boolean;
83
+ };
84
+
85
+ const NavigationMenuGuardContext = createContext<NavigationMenuGuard | null>(null);
86
+ const NavigationMenuItemValueContext = createContext<string | undefined>(undefined);
87
+
7
88
  export const NavigationMenu = forwardRef<
8
89
  ElementRef<typeof NavigationMenuPrimitive.Root>,
9
90
  ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
10
- >(function NavigationMenu({ className, children, ...props }, ref) {
91
+ >(function NavigationMenu(
92
+ { className, children, value: valueProp, defaultValue, onValueChange, ...props },
93
+ ref,
94
+ ) {
95
+ const isControlled = valueProp !== undefined;
96
+ const [internalValue, setInternalValue] = useState(defaultValue ?? "");
97
+ const currentValue = isControlled ? (valueProp as string) : internalValue;
98
+ const currentValueRef = useRef(currentValue);
99
+ currentValueRef.current = currentValue;
100
+
101
+ // itemValue currently under a resting, real (mouse) pointer — at most one
102
+ // at a time in practice, since a real pointer can only be inside one
103
+ // trigger's bounds.
104
+ const pointerPresentForRef = useRef<string | null>(null);
105
+ // One-shot: the itemValue whose next reopen request should be swallowed.
106
+ const suppressReopenForRef = useRef<string | null>(null);
107
+
108
+ const guard = useMemo<NavigationMenuGuard>(
109
+ () => ({
110
+ notePointerPresence(itemValue, present) {
111
+ if (present) {
112
+ pointerPresentForRef.current = itemValue;
113
+ return;
114
+ }
115
+ if (pointerPresentForRef.current === itemValue) pointerPresentForRef.current = null;
116
+ // A genuine leave ends the suspicious window this guard exists for —
117
+ // any future reopen must come from a fresh interaction (a new
118
+ // hover-intent cycle after re-entering, or a click), both legitimate.
119
+ if (suppressReopenForRef.current === itemValue) suppressReopenForRef.current = null;
120
+ },
121
+ noteExplicitActivation(itemValue) {
122
+ if (suppressReopenForRef.current === itemValue) suppressReopenForRef.current = null;
123
+ },
124
+ consumeSuppressionFlag(itemValue) {
125
+ if (suppressReopenForRef.current === itemValue) {
126
+ suppressReopenForRef.current = null;
127
+ return true;
128
+ }
129
+ return false;
130
+ },
131
+ }),
132
+ [],
133
+ );
134
+
135
+ const handleValueChange = (next: string) => {
136
+ const previous = currentValueRef.current;
137
+ if (next === "") {
138
+ if (previous !== "" && pointerPresentForRef.current === previous) {
139
+ // Dismissed while a real pointer still rests on its trigger, inside
140
+ // the hover-intent window — the #85 defect. Arm the one-shot guard
141
+ // against exactly this item's next reopen request.
142
+ suppressReopenForRef.current = previous;
143
+ }
144
+ } else if (guard.consumeSuppressionFlag(next)) {
145
+ return; // Swallow the stray reopen — stay on `previous` (closed).
146
+ }
147
+ if (!isControlled) setInternalValue(next);
148
+ onValueChange?.(next);
149
+ };
150
+
11
151
  return (
12
- <NavigationMenuPrimitive.Root
13
- ref={ref}
14
- className={cn("relative z-10 flex max-w-max flex-1 items-center justify-center", className)}
15
- {...props}
16
- >
17
- {children}
18
- <NavigationMenuViewport />
19
- </NavigationMenuPrimitive.Root>
152
+ <NavigationMenuGuardContext.Provider value={guard}>
153
+ <NavigationMenuPrimitive.Root
154
+ ref={ref}
155
+ data-slot="navigation-menu"
156
+ value={currentValue}
157
+ onValueChange={handleValueChange}
158
+ className={cn("relative z-10 flex max-w-max flex-1 items-center justify-center", className)}
159
+ {...props}
160
+ >
161
+ {children}
162
+ <NavigationMenuViewport />
163
+ </NavigationMenuPrimitive.Root>
164
+ </NavigationMenuGuardContext.Provider>
20
165
  );
21
166
  });
22
167
 
@@ -27,26 +172,68 @@ export const NavigationMenuList = forwardRef<
27
172
  return (
28
173
  <NavigationMenuPrimitive.List
29
174
  ref={ref}
175
+ data-slot="navigation-menu-list"
30
176
  className={cn("group flex flex-1 list-none items-center justify-center gap-1", className)}
31
177
  {...props}
32
178
  />
33
179
  );
34
180
  });
35
181
 
36
- export const NavigationMenuItem = NavigationMenuPrimitive.Item;
182
+ export const NavigationMenuItem = forwardRef<
183
+ ElementRef<typeof NavigationMenuPrimitive.Item>,
184
+ ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Item>
185
+ >(function NavigationMenuItem({ value: valueProp, ...props }, ref) {
186
+ const autoValue = useId();
187
+ const value = valueProp ?? autoValue;
188
+ return (
189
+ <NavigationMenuItemValueContext.Provider value={value}>
190
+ <NavigationMenuPrimitive.Item
191
+ ref={ref}
192
+ data-slot="navigation-menu-item"
193
+ value={value}
194
+ {...props}
195
+ />
196
+ </NavigationMenuItemValueContext.Provider>
197
+ );
198
+ });
37
199
 
38
200
  export const navigationMenuTriggerStyle = cva(
39
- "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:bg-accent",
201
+ "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-body font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:bg-accent",
40
202
  );
41
203
 
42
204
  export const NavigationMenuTrigger = forwardRef<
43
205
  ElementRef<typeof NavigationMenuPrimitive.Trigger>,
44
206
  ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
45
- >(function NavigationMenuTrigger({ className, children, ...props }, ref) {
207
+ >(function NavigationMenuTrigger(
208
+ { className, children, onClick, onPointerEnter, onPointerLeave, ...props },
209
+ ref,
210
+ ) {
211
+ const itemValue = useContext(NavigationMenuItemValueContext);
212
+ const guard = useContext(NavigationMenuGuardContext);
213
+
214
+ const notePointer = (event: ReactPointerEvent<HTMLButtonElement>, present: boolean) => {
215
+ if (itemValue !== undefined && guard && event.pointerType === "mouse") {
216
+ guard.notePointerPresence(itemValue, present);
217
+ }
218
+ };
219
+
46
220
  return (
47
221
  <NavigationMenuPrimitive.Trigger
48
222
  ref={ref}
223
+ data-slot="navigation-menu-trigger"
49
224
  className={cn(navigationMenuTriggerStyle(), "group", className)}
225
+ onPointerEnter={(event) => {
226
+ onPointerEnter?.(event);
227
+ notePointer(event, true);
228
+ }}
229
+ onPointerLeave={(event) => {
230
+ onPointerLeave?.(event);
231
+ notePointer(event, false);
232
+ }}
233
+ onClick={(event) => {
234
+ if (itemValue !== undefined) guard?.noteExplicitActivation(itemValue);
235
+ onClick?.(event);
236
+ }}
50
237
  {...props}
51
238
  >
52
239
  {children}
@@ -65,8 +252,9 @@ export const NavigationMenuContent = forwardRef<
65
252
  return (
66
253
  <NavigationMenuPrimitive.Content
67
254
  ref={ref}
255
+ data-slot="navigation-menu-content"
68
256
  className={cn(
69
- "left-0 top-0 w-full p-2 md:absolute md:w-auto",
257
+ "start-0 top-0 w-full p-2 md:absolute md:w-auto",
70
258
  "data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out",
71
259
  "data-[motion=from-end]:slide-in-from-right-2 data-[motion=from-start]:slide-in-from-left-2 data-[motion=to-end]:slide-out-to-right-2 data-[motion=to-start]:slide-out-to-left-2",
72
260
  "data-[motion^=from-]:[--tw-ease:var(--ease-entrance)]",
@@ -84,9 +272,10 @@ export const NavigationMenuViewport = forwardRef<
84
272
  ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
85
273
  >(function NavigationMenuViewport({ className, ...props }, ref) {
86
274
  return (
87
- <div className="absolute left-0 top-full flex justify-center">
275
+ <div className="absolute start-0 top-full flex justify-center">
88
276
  <NavigationMenuPrimitive.Viewport
89
277
  ref={ref}
278
+ data-slot="navigation-menu-viewport"
90
279
  className={cn(
91
280
  "relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
92
281
  "origin-top transition-[width,height] duration-base ease-standard data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:zoom-in-90 data-[state=closed]:zoom-out-95",
@@ -76,13 +76,17 @@ type Story = StoryObj<typeof meta>;
76
76
 
77
77
  export const Default: Story = {
78
78
  args: { defaultValue: 5, min: 0, max: 10 },
79
- // Clicks the Increase button and confirms the value increments.
79
+ // Clicks the Increase button and confirms the value increments. The input is a
80
+ // locale-formatted text spinbutton, so the displayed value is a string and the
81
+ // numeric value is announced via aria-valuenow.
80
82
  play: async ({ canvas, userEvent }) => {
81
83
  const input = canvas.getByRole("spinbutton");
82
- await expect(input).toHaveValue(5);
84
+ await expect(input).toHaveValue("5");
85
+ await expect(input).toHaveAttribute("aria-valuenow", "5");
83
86
  const increaseBtn = canvas.getByRole("button", { name: /increase/i });
84
87
  await userEvent.click(increaseBtn);
85
- await expect(input).toHaveValue(6);
88
+ await expect(input).toHaveValue("6");
89
+ await expect(input).toHaveAttribute("aria-valuenow", "6");
86
90
  },
87
91
  };
88
92
 
@@ -1,7 +1,9 @@
1
+ import { createRef } from "react";
1
2
  import { describe, expect, it, vi } from "vitest";
2
3
  import { render, screen } from "@testing-library/react";
3
4
  import userEvent from "@testing-library/user-event";
4
5
  import { NumberInput } from "./number-input";
6
+ import { LocaleProvider } from "../locale-provider";
5
7
 
6
8
  describe("NumberInput", () => {
7
9
  it("renders a spinbutton input", () => {
@@ -93,4 +95,49 @@ describe("NumberInput", () => {
93
95
  await userEvent.keyboard("{ArrowDown}");
94
96
  expect(onValueChange).toHaveBeenLastCalledWith(5);
95
97
  });
98
+
99
+ it("forwards the ref to the underlying <input>, not the wrapper", () => {
100
+ const ref = createRef<HTMLInputElement>();
101
+ render(<NumberInput aria-label="Qty" defaultValue={5} ref={ref} />);
102
+ expect(ref.current).toBeInstanceOf(HTMLInputElement);
103
+ expect(ref.current).toBe(screen.getByRole("spinbutton", { name: "Qty" }));
104
+ });
105
+
106
+ it("does not hard-fix a width — the caller's className is not fighting a default", () => {
107
+ render(<NumberInput aria-label="Qty" defaultValue={5} className="w-24" />);
108
+ const group = screen.getByRole("group");
109
+ expect(group.className).not.toMatch(/\bw-36\b/);
110
+ expect(group.className).toMatch(/\bw-24\b/);
111
+ });
112
+
113
+ it("rounds fp-noisy step increments to the step's precision (0.1 + 0.1 + 0.1)", async () => {
114
+ const onValueChange = vi.fn();
115
+ render(
116
+ <NumberInput aria-label="Amount" defaultValue={0} step={0.1} onValueChange={onValueChange} />,
117
+ );
118
+ const increaseBtn = screen.getByRole("button", { name: "Increase" });
119
+ await userEvent.click(increaseBtn);
120
+ await userEvent.click(increaseBtn);
121
+ await userEvent.click(increaseBtn);
122
+ expect(onValueChange).toHaveBeenLastCalledWith(0.3);
123
+ });
124
+
125
+ it("parses a locale-formatted number using the active locale's separators (de-DE: comma decimal)", async () => {
126
+ const onValueChange = vi.fn();
127
+ render(
128
+ <LocaleProvider locale="de-DE">
129
+ <NumberInput
130
+ aria-label="Betrag"
131
+ defaultValue={0}
132
+ step={0.1}
133
+ onValueChange={onValueChange}
134
+ />
135
+ </LocaleProvider>,
136
+ );
137
+ const input = screen.getByRole("spinbutton", { name: "Betrag" });
138
+ await userEvent.clear(input);
139
+ await userEvent.type(input, "1.234,5");
140
+ await userEvent.tab();
141
+ expect(onValueChange).toHaveBeenLastCalledWith(1234.5);
142
+ });
96
143
  });
@@ -1,8 +1,10 @@
1
+ "use client";
2
+
1
3
  // a2ui.exposed: yes
2
4
  import { forwardRef, useCallback, useRef, useState, type HTMLAttributes } from "react";
3
5
  import { Minus, Plus } from "lucide-react";
4
- import { cn } from "../../lib/cn";
5
6
  import { InputGroup, InputGroupButton, InputGroupInput } from "../input-group";
7
+ import { useLocale } from "../locale-provider";
6
8
 
7
9
  export interface NumberInputProps extends Omit<
8
10
  HTMLAttributes<HTMLDivElement>,
@@ -56,11 +58,37 @@ function clampValue(val: number, min?: number, max?: number): number {
56
58
  return result;
57
59
  }
58
60
 
59
- function formatNum(value: number, formatOptions?: Intl.NumberFormatOptions): string {
60
- if (formatOptions) {
61
- return new Intl.NumberFormat(undefined, formatOptions).format(value);
62
- }
63
- return String(value);
61
+ /** Number of decimal places implied by a step (e.g. 0.1 → 1, 1 → 0). */
62
+ function stepDecimals(step: number): number {
63
+ if (!isFinite(step) || step === 0) return 0;
64
+ const s = String(step);
65
+ const i = s.indexOf(".");
66
+ return i === -1 ? 0 : s.length - i - 1;
67
+ }
68
+
69
+ /** Round `value` to the decimal precision implied by `step`, avoiding fp noise (0.1+0.2). */
70
+ function roundToStep(value: number, step: number): number {
71
+ const decimals = stepDecimals(step);
72
+ if (decimals === 0) return Math.round(value);
73
+ const factor = 10 ** decimals;
74
+ return Math.round(value * factor) / factor;
75
+ }
76
+
77
+ /** The active locale's decimal and group separator characters (e.g. "," / "." in de-DE). */
78
+ function getLocaleSeparators(locale: string): { decimal: string; group: string } {
79
+ const parts = new Intl.NumberFormat(locale).formatToParts(1234.5);
80
+ const decimal = parts.find((p) => p.type === "decimal")?.value ?? ".";
81
+ const group = parts.find((p) => p.type === "group")?.value ?? ",";
82
+ return { decimal, group };
83
+ }
84
+
85
+ /** Parse a locale-formatted numeric string (group + decimal separators) into a number. */
86
+ function parseLocaleNumber(raw: string, locale: string): number {
87
+ const { decimal, group } = getLocaleSeparators(locale);
88
+ let normalized = raw.trim();
89
+ if (group) normalized = normalized.split(group).join("");
90
+ if (decimal !== ".") normalized = normalized.split(decimal).join(".");
91
+ return Number(normalized);
64
92
  }
65
93
 
66
94
  /**
@@ -69,7 +97,7 @@ function formatNum(value: number, formatOptions?: Intl.NumberFormatOptions): str
69
97
  * Controlled via `value`/`onValueChange`; uncontrolled via `defaultValue`.
70
98
  * Keyboard ↑/↓ = ±step; Shift+↑/↓ = ±step×10. Clamps on blur (not per keystroke).
71
99
  */
72
- export const NumberInput = forwardRef<HTMLDivElement, NumberInputProps>(function NumberInput(
100
+ export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(function NumberInput(
73
101
  {
74
102
  value: valueProp,
75
103
  defaultValue,
@@ -94,14 +122,20 @@ export const NumberInput = forwardRef<HTMLDivElement, NumberInputProps>(function
94
122
  },
95
123
  ref,
96
124
  ) {
125
+ const { locale, formatNumber, t } = useLocale();
97
126
  const isControlled = valueProp !== undefined;
98
127
 
128
+ const formatValue = useCallback(
129
+ (n: number) => formatNumber(n, formatOptions),
130
+ [formatNumber, formatOptions],
131
+ );
132
+
99
133
  const [internalValue, setInternalValue] = useState<number | null>(
100
134
  defaultValue !== undefined ? defaultValue : null,
101
135
  );
102
136
  const [inputText, setInputText] = useState<string>(() => {
103
137
  const initial = isControlled ? valueProp : defaultValue;
104
- return initial != null ? formatNum(initial, formatOptions) : "";
138
+ return initial != null ? formatValue(initial) : "";
105
139
  });
106
140
  // Track whether the user is actively editing to avoid overwriting their input
107
141
  const isEditing = useRef(false);
@@ -120,12 +154,12 @@ export const NumberInput = forwardRef<HTMLDivElement, NumberInputProps>(function
120
154
  (delta: number) => {
121
155
  if (disabled || readOnly) return;
122
156
  const base = currentValue ?? 0;
123
- let next = base + delta;
157
+ let next = roundToStep(base + delta, step);
124
158
  if (clamp) next = clampValue(next, min, max);
125
- setInputText(next != null ? formatNum(next, formatOptions) : "");
159
+ setInputText(formatValue(next));
126
160
  commit(next);
127
161
  },
128
- [disabled, readOnly, currentValue, clamp, min, max, formatOptions, commit],
162
+ [disabled, readOnly, currentValue, step, clamp, min, max, formatValue, commit],
129
163
  );
130
164
 
131
165
  const handleKeyDown = useCallback(
@@ -152,22 +186,23 @@ export const NumberInput = forwardRef<HTMLDivElement, NumberInputProps>(function
152
186
  commit(null);
153
187
  return;
154
188
  }
155
- const parsed = parseFloat(raw);
189
+ const parsed = parseLocaleNumber(raw, locale);
156
190
  if (isNaN(parsed)) {
157
191
  // Revert to last known good value
158
- setInputText(currentValue != null ? formatNum(currentValue, formatOptions) : "");
192
+ setInputText(currentValue != null ? formatValue(currentValue) : "");
159
193
  return;
160
194
  }
161
- const clamped = clamp ? clampValue(parsed, min, max) : parsed;
162
- setInputText(formatNum(clamped, formatOptions));
195
+ const rounded = roundToStep(parsed, step);
196
+ const clamped = clamp ? clampValue(rounded, min, max) : rounded;
197
+ setInputText(formatValue(clamped));
163
198
  commit(clamped);
164
- }, [inputText, currentValue, clamp, min, max, formatOptions, commit]);
199
+ }, [inputText, currentValue, clamp, min, max, step, locale, formatValue, commit]);
165
200
 
166
201
  // Keep display text in sync with controlled value changes (only when not editing)
167
202
  const prevControlledValue = useRef(valueProp);
168
203
  if (isControlled && valueProp !== prevControlledValue.current && !isEditing.current) {
169
204
  prevControlledValue.current = valueProp;
170
- const nextText = valueProp != null ? formatNum(valueProp, formatOptions) : "";
205
+ const nextText = valueProp != null ? formatValue(valueProp) : "";
171
206
  if (nextText !== inputText) setInputText(nextText);
172
207
  }
173
208
 
@@ -177,18 +212,24 @@ export const NumberInput = forwardRef<HTMLDivElement, NumberInputProps>(function
177
212
  disabled || readOnly || (max !== undefined && (currentValue ?? 0) >= max);
178
213
 
179
214
  return (
180
- <InputGroup ref={ref} className={cn("w-36", className)} {...props}>
215
+ <InputGroup className={className} {...props}>
181
216
  <InputGroupButton
182
- aria-label="Decrease"
217
+ aria-label={t("ui.numberInput.decrease")}
183
218
  tabIndex={-1}
184
219
  disabled={decrementDisabled}
185
220
  onClick={() => applyStep(-step)}
186
- className="rounded-r-none"
221
+ className="rounded-e-none"
187
222
  >
188
223
  <Minus aria-hidden="true" />
189
224
  </InputGroupButton>
190
225
  <InputGroupInput
191
- type="number"
226
+ ref={ref}
227
+ type="text"
228
+ inputMode="decimal"
229
+ role="spinbutton"
230
+ aria-valuenow={currentValue ?? undefined}
231
+ aria-valuemin={min}
232
+ aria-valuemax={max}
192
233
  id={id}
193
234
  name={name}
194
235
  value={inputText}
@@ -200,17 +241,17 @@ export const NumberInput = forwardRef<HTMLDivElement, NumberInputProps>(function
200
241
  aria-describedby={ariaDescribedby}
201
242
  aria-invalid={ariaInvalid}
202
243
  aria-required={ariaRequired}
203
- className="text-center [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
244
+ className="text-center"
204
245
  onChange={handleChange}
205
246
  onKeyDown={handleKeyDown}
206
247
  onBlur={handleBlur}
207
248
  />
208
249
  <InputGroupButton
209
- aria-label="Increase"
250
+ aria-label={t("ui.numberInput.increase")}
210
251
  tabIndex={-1}
211
252
  disabled={incrementDisabled}
212
253
  onClick={() => applyStep(step)}
213
- className="rounded-l-none"
254
+ className="rounded-s-none"
214
255
  >
215
256
  <Plus aria-hidden="true" />
216
257
  </InputGroupButton>