@astryxdesign/core 0.5.2-canary.2da7a39 → 0.5.2-canary.2e51213

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 (375) hide show
  1. package/dist/AppShell/AppShell.d.ts +2 -2
  2. package/dist/AppShell/AppShell.d.ts.map +1 -1
  3. package/dist/AppShell/AppShell.js +10 -7
  4. package/dist/Banner/Banner.d.ts +5 -0
  5. package/dist/Banner/Banner.d.ts.map +1 -1
  6. package/dist/Banner/Banner.js +7 -2
  7. package/dist/Breadcrumbs/BreadcrumbItem.d.ts +3 -2
  8. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  9. package/dist/Breadcrumbs/BreadcrumbItem.js +22 -15
  10. package/dist/Carousel/Carousel.d.ts.map +1 -1
  11. package/dist/Carousel/Carousel.js +2 -2
  12. package/dist/Chat/ChatComposer.d.ts +2 -2
  13. package/dist/Chat/ChatComposer.d.ts.map +1 -1
  14. package/dist/Chat/ChatComposer.js +21 -2
  15. package/dist/Chat/ChatToolCalls.d.ts +1 -1
  16. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  17. package/dist/Chat/ChatToolCalls.js +12 -34
  18. package/dist/Chat/useChatNewMessages.js +3 -3
  19. package/dist/CheckboxList/CheckboxListItem.d.ts +5 -3
  20. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  21. package/dist/DateTimeInput/DateTimeInput.d.ts +47 -13
  22. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  23. package/dist/DateTimeInput/DateTimeInput.js +269 -162
  24. package/dist/DateTimeInput/NativeDateSegment.d.ts +24 -0
  25. package/dist/DateTimeInput/NativeDateSegment.d.ts.map +1 -0
  26. package/dist/DateTimeInput/NativeDateSegment.js +235 -0
  27. package/dist/DateTimeInput/NativeTimeSegment.d.ts +30 -0
  28. package/dist/DateTimeInput/NativeTimeSegment.d.ts.map +1 -0
  29. package/dist/DateTimeInput/NativeTimeSegment.js +248 -0
  30. package/dist/DateTimeInput/index.d.ts +1 -1
  31. package/dist/DateTimeInput/index.d.ts.map +1 -1
  32. package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts +95 -0
  33. package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts.map +1 -0
  34. package/dist/DateTimeInput/nativePickerSegmentStyles.js +103 -0
  35. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  36. package/dist/DropdownMenu/DropdownMenu.js +2 -4
  37. package/dist/Field/Field.d.ts.map +1 -1
  38. package/dist/Field/Field.js +10 -4
  39. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  40. package/dist/Field/PanelSearchInput.js +3 -5
  41. package/dist/FieldStatus/FieldStatus.d.ts.map +1 -1
  42. package/dist/FieldStatus/FieldStatus.js +4 -2
  43. package/dist/Layer/useTouchTrigger.d.ts.map +1 -1
  44. package/dist/Layer/useTouchTrigger.js +2 -4
  45. package/dist/Layout/Layout.d.ts +7 -8
  46. package/dist/Layout/Layout.d.ts.map +1 -1
  47. package/dist/Layout/Layout.js +7 -8
  48. package/dist/MultiSelector/MultiSelector.d.ts +11 -1
  49. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  50. package/dist/MultiSelector/MultiSelector.js +89 -45
  51. package/dist/RadioList/RadioListItem.d.ts +30 -5
  52. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  53. package/dist/RadioList/RadioListItem.js +17 -4
  54. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  55. package/dist/Resizable/ResizeHandle.js +102 -44
  56. package/dist/Resizable/index.d.ts +2 -0
  57. package/dist/Resizable/index.d.ts.map +1 -1
  58. package/dist/Resizable/index.js +1 -0
  59. package/dist/Resizable/useResizable.d.ts +89 -8
  60. package/dist/Resizable/useResizable.d.ts.map +1 -1
  61. package/dist/Resizable/useResizable.js +521 -38
  62. package/dist/Resizable/utils.d.ts +35 -0
  63. package/dist/Resizable/utils.d.ts.map +1 -0
  64. package/dist/Resizable/utils.js +27 -0
  65. package/dist/Selector/Selector.d.ts +10 -0
  66. package/dist/Selector/Selector.d.ts.map +1 -1
  67. package/dist/Selector/Selector.js +105 -57
  68. package/dist/Selector/useSelectorPresentation.d.ts +1 -0
  69. package/dist/Selector/useSelectorPresentation.d.ts.map +1 -1
  70. package/dist/Selector/useSelectorPresentation.js +17 -2
  71. package/dist/Slider/Slider.d.ts.map +1 -1
  72. package/dist/Slider/Slider.js +3 -5
  73. package/dist/Spinner/Spinner.d.ts.map +1 -1
  74. package/dist/Spinner/Spinner.js +9 -10
  75. package/dist/Stepper/Step.d.ts +2 -1
  76. package/dist/Stepper/Step.d.ts.map +1 -1
  77. package/dist/Stepper/Step.js +75 -11
  78. package/dist/Stepper/Stepper.d.ts.map +1 -1
  79. package/dist/Stepper/Stepper.js +1 -0
  80. package/dist/TabList/TabList.d.ts.map +1 -1
  81. package/dist/TabList/TabList.js +2 -3
  82. package/dist/Table/index.d.ts +16 -1
  83. package/dist/Table/index.d.ts.map +1 -1
  84. package/dist/Table/index.js +4 -0
  85. package/dist/Table/plugins/columnResize/useTableColumnResize.d.ts.map +1 -1
  86. package/dist/Table/plugins/columnResize/useTableColumnResize.js +8 -9
  87. package/dist/Table/plugins/rowStatus/index.d.ts +1 -1
  88. package/dist/Table/plugins/rowStatus/index.d.ts.map +1 -1
  89. package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts +11 -30
  90. package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts.map +1 -1
  91. package/dist/Table/plugins/rowStatus/useTableRowStatus.js +108 -62
  92. package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
  93. package/dist/Table/plugins/sortable/useTableSortable.js +9 -1
  94. package/dist/Text/useTruncation.d.ts +1 -1
  95. package/dist/Text/useTruncation.d.ts.map +1 -1
  96. package/dist/Text/useTruncation.js +8 -7
  97. package/dist/TimeInput/TimeInput.d.ts +16 -1
  98. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  99. package/dist/TimeInput/TimeInput.js +98 -60
  100. package/dist/TimeInput/index.d.ts +1 -1
  101. package/dist/TimeInput/index.d.ts.map +1 -1
  102. package/dist/Timestamp/Timestamp.d.ts +2 -2
  103. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  104. package/dist/Timestamp/Timestamp.js +4 -145
  105. package/dist/Timestamp/formatRelativeTime.d.ts +20 -0
  106. package/dist/Timestamp/formatRelativeTime.d.ts.map +1 -0
  107. package/dist/Timestamp/formatRelativeTime.js +81 -0
  108. package/dist/Toast/ToastViewport.d.ts.map +1 -1
  109. package/dist/Toast/ToastViewport.js +125 -77
  110. package/dist/Tokenizer/Tokenizer.d.ts +0 -38
  111. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  112. package/dist/Tokenizer/Tokenizer.js +94 -26
  113. package/dist/Tokenizer/useEndLaneReserve.d.ts +31 -0
  114. package/dist/Tokenizer/useEndLaneReserve.d.ts.map +1 -0
  115. package/dist/Tokenizer/useEndLaneReserve.js +136 -0
  116. package/dist/Tooltip/useTooltip.d.ts +1 -1
  117. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  118. package/dist/Tooltip/useTooltip.js +53 -42
  119. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  120. package/dist/Typeahead/BaseTypeahead.js +85 -30
  121. package/dist/Typeahead/Typeahead.d.ts +0 -21
  122. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  123. package/dist/Typeahead/Typeahead.js +108 -67
  124. package/dist/Typeahead/busyIndicatorLane.d.ts +62 -0
  125. package/dist/Typeahead/busyIndicatorLane.d.ts.map +1 -0
  126. package/dist/Typeahead/busyIndicatorLane.js +98 -0
  127. package/dist/astryx.css +25 -7
  128. package/dist/hooks/useFocusReturnVisibility.d.ts.map +1 -1
  129. package/dist/hooks/useFocusReturnVisibility.js +3 -5
  130. package/dist/hooks/useIsomorphicLayoutEffect.d.ts +2 -2
  131. package/dist/hooks/useIsomorphicLayoutEffect.d.ts.map +1 -1
  132. package/dist/hooks/useOverflow.js +11 -11
  133. package/dist/hooks/useScrollOverflow.d.ts.map +1 -1
  134. package/dist/hooks/useScrollOverflow.js +7 -4
  135. package/dist/theme/defineTheme.d.ts +13 -0
  136. package/dist/theme/defineTheme.d.ts.map +1 -1
  137. package/dist/theme/defineTheme.js +7 -0
  138. package/dist/theme/generateThemeRules.d.ts.map +1 -1
  139. package/dist/theme/generateThemeRules.js +1 -1
  140. package/dist/theme/localTokens.d.ts +18 -0
  141. package/dist/theme/localTokens.d.ts.map +1 -0
  142. package/dist/theme/localTokens.js +162 -0
  143. package/dist/utils/interactionModality.d.ts +6 -28
  144. package/dist/utils/interactionModality.d.ts.map +1 -1
  145. package/dist/utils/interactionModality.js +72 -37
  146. package/dist/utils/sharedResizeObserver.d.ts +17 -9
  147. package/dist/utils/sharedResizeObserver.d.ts.map +1 -1
  148. package/dist/utils/sharedResizeObserver.js +54 -12
  149. package/locales/en.json +8 -0
  150. package/locales/pseudo.json +6 -0
  151. package/package.json +7 -2
  152. package/src/AlertDialog/useImperativeAlertDialog.test.tsx +163 -0
  153. package/src/AppShell/AppShell.doc.mjs +4 -3
  154. package/src/AppShell/AppShell.test.tsx +21 -0
  155. package/src/AppShell/AppShell.tsx +12 -7
  156. package/src/Banner/Banner.doc.mjs +5 -1
  157. package/src/Banner/Banner.tsx +12 -1
  158. package/src/BottomSheet/BottomSheet.doc.mjs +31 -0
  159. package/src/BottomSheet/BottomSheet.spec.md +178 -0
  160. package/src/Breadcrumbs/BreadcrumbItem.tsx +18 -9
  161. package/src/Breadcrumbs/Breadcrumbs.doc.mjs +4 -1
  162. package/src/Breadcrumbs/Breadcrumbs.test.tsx +12 -0
  163. package/src/Button/Button.doc.mjs +56 -0
  164. package/src/ButtonGroup/ButtonGroup.doc.mjs +47 -0
  165. package/src/Calendar/hooks/useCalendarConstraints.test.ts +153 -0
  166. package/src/Calendar/hooks/useCalendarDays.test.ts +265 -0
  167. package/src/Calendar/hooks/useCalendarNavigation.test.ts +252 -0
  168. package/src/Carousel/Carousel.test.tsx +83 -0
  169. package/src/Carousel/Carousel.tsx +8 -2
  170. package/src/Chat/ChatComposer.doc.mjs +7 -4
  171. package/src/Chat/ChatComposer.test.tsx +84 -2
  172. package/src/Chat/ChatComposer.tsx +45 -3
  173. package/src/Chat/ChatSendButton.test.tsx +189 -0
  174. package/src/Chat/ChatToolCalls.doc.mjs +1 -1
  175. package/src/Chat/ChatToolCalls.test.tsx +33 -1
  176. package/src/Chat/ChatToolCalls.tsx +13 -34
  177. package/src/Chat/useChatNewMessages.ts +3 -3
  178. package/src/Chat/useChatPasteAsToken.test.ts +191 -0
  179. package/src/CheckboxList/CheckboxList.doc.mjs +172 -31
  180. package/src/CheckboxList/CheckboxList.spec.md +215 -0
  181. package/src/CheckboxList/CheckboxList.test.tsx +45 -0
  182. package/src/CheckboxList/CheckboxListItem.doc.mjs +11 -9
  183. package/src/CheckboxList/CheckboxListItem.tsx +5 -3
  184. package/src/Collapsible/useCollapsible.test.tsx +236 -0
  185. package/src/CommandPalette/CommandPalette.doc.mjs +72 -0
  186. package/src/CommandPalette/CommandPalette.spec.md +224 -0
  187. package/src/CommandPalette/CommandPaletteEmpty.test.tsx +101 -0
  188. package/src/ComplexSelector/ComplexSelector.doc.mjs +42 -0
  189. package/src/ComplexSelector/ComplexSelector.spec.md +190 -0
  190. package/src/ContextMenu/ContextMenu.doc.mjs +48 -0
  191. package/src/ContextMenu/ContextMenu.spec.md +207 -0
  192. package/src/DateTimeInput/DateTimeInput.doc.mjs +43 -23
  193. package/src/DateTimeInput/DateTimeInput.tsx +388 -181
  194. package/src/DateTimeInput/DateTimeInputTouch.test.tsx +10 -2
  195. package/src/DateTimeInput/NativeDateSegment.tsx +308 -0
  196. package/src/DateTimeInput/NativePickerSegments.test.tsx +626 -0
  197. package/src/DateTimeInput/NativeTimeSegment.tsx +331 -0
  198. package/src/DateTimeInput/index.ts +1 -0
  199. package/src/DateTimeInput/nativePickerSegmentStyles.ts +120 -0
  200. package/src/Divider/Divider.doc.mjs +24 -0
  201. package/src/Divider/Divider.spec.md +157 -0
  202. package/src/DropdownMenu/DropdownMenu.doc.mjs +104 -0
  203. package/src/DropdownMenu/DropdownMenu.spec.md +246 -0
  204. package/src/DropdownMenu/DropdownMenu.tsx +2 -4
  205. package/src/Field/Field.doc.mjs +1 -0
  206. package/src/Field/Field.test.tsx +39 -0
  207. package/src/Field/Field.tsx +18 -1
  208. package/src/Field/InputClearButton.test.tsx +68 -6
  209. package/src/Field/PanelSearchInput.tsx +3 -5
  210. package/src/FieldStatus/FieldStatus.doc.mjs +21 -0
  211. package/src/FieldStatus/FieldStatus.spec.md +213 -0
  212. package/src/FieldStatus/FieldStatus.test.tsx +17 -0
  213. package/src/FieldStatus/FieldStatus.tsx +7 -2
  214. package/src/Grid/Grid.doc.mjs +19 -0
  215. package/src/Grid/Grid.spec.md +162 -0
  216. package/src/Icon/Icon.doc.mjs +196 -48
  217. package/src/Icon/Icon.spec.md +192 -0
  218. package/src/IconButton/IconButton.doc.mjs +38 -0
  219. package/src/Kbd/Kbd.doc.mjs +18 -0
  220. package/src/Kbd/Kbd.spec.md +142 -0
  221. package/src/Layer/useTouchTrigger.ts +2 -4
  222. package/src/Layout/Layout.doc.mjs +57 -17
  223. package/src/Layout/Layout.spec.md +235 -0
  224. package/src/Layout/Layout.test.tsx +1 -1
  225. package/src/Layout/Layout.tsx +7 -8
  226. package/src/Layout/LayoutHeader.doc.mjs +9 -0
  227. package/src/Layout/LayoutPanel.doc.mjs +4 -3
  228. package/src/Lightbox/Lightbox.doc.mjs +149 -33
  229. package/src/Lightbox/Lightbox.spec.md +198 -0
  230. package/src/Lightbox/useLightbox.test.tsx +263 -0
  231. package/src/Markdown/Markdown.doc.mjs +60 -0
  232. package/src/Markdown/Markdown.spec.md +207 -0
  233. package/src/MobileNav/MobileNav.doc.mjs +140 -32
  234. package/src/MobileNav/MobileNav.spec.md +209 -0
  235. package/src/MoreMenu/MoreMenu.doc.mjs +42 -0
  236. package/src/MoreMenu/MoreMenu.spec.md +222 -0
  237. package/src/MultiSelector/MultiSelector.doc.mjs +124 -1
  238. package/src/MultiSelector/MultiSelector.spec.md +293 -0
  239. package/src/MultiSelector/MultiSelector.test.tsx +305 -2
  240. package/src/MultiSelector/MultiSelector.tsx +124 -55
  241. package/src/NavIcon/NavIcon.doc.mjs +17 -0
  242. package/src/NavIcon/NavIcon.spec.md +134 -0
  243. package/src/NavMenu/NavHeadingMenu.spec.md +210 -0
  244. package/src/NavMenu/NavMenu.doc.mjs +37 -0
  245. package/src/Outline/Outline.doc.mjs +35 -0
  246. package/src/Outline/Outline.spec.md +162 -0
  247. package/src/OverflowList/OverflowList.doc.mjs +24 -0
  248. package/src/OverflowList/OverflowList.spec.md +157 -0
  249. package/src/Pagination/Pagination.doc.mjs +74 -0
  250. package/src/Pagination/Pagination.spec.md +226 -0
  251. package/src/Popover/Popover.doc.mjs +264 -65
  252. package/src/Popover/Popover.spec.md +421 -0
  253. package/src/Popover/usePopover.doc.mjs +208 -33
  254. package/src/PowerSearch/PowerSearch.spec.md +291 -0
  255. package/src/PowerSearch/PowerSearchFilterEditor.test.tsx +253 -0
  256. package/src/PowerSearch/PowerSearchToken.test.tsx +228 -0
  257. package/src/ProgressBar/ProgressBar.doc.mjs +198 -38
  258. package/src/ProgressBar/ProgressBar.spec.md +261 -0
  259. package/src/RadioList/RadioList.test.tsx +136 -2
  260. package/src/RadioList/RadioListItem.doc.mjs +30 -11
  261. package/src/RadioList/RadioListItem.tsx +44 -8
  262. package/src/Resizable/Resizable.doc.mjs +130 -24
  263. package/src/Resizable/Resizable.spec.md +246 -0
  264. package/src/Resizable/ResizeHandle.test.tsx +177 -56
  265. package/src/Resizable/ResizeHandle.tsx +128 -45
  266. package/src/Resizable/index.ts +2 -0
  267. package/src/Resizable/shippedDirections.test.ts +462 -0
  268. package/src/Resizable/useResizable.doc.mjs +124 -29
  269. package/src/Resizable/useResizable.test.ts +1277 -3
  270. package/src/Resizable/useResizable.ts +760 -48
  271. package/src/Resizable/utils.test.ts +74 -0
  272. package/src/Resizable/utils.ts +35 -0
  273. package/src/Section/Section.doc.mjs +17 -0
  274. package/src/Section/Section.spec.md +139 -0
  275. package/src/SegmentedControl/SegmentedControl.doc.mjs +83 -0
  276. package/src/SegmentedControl/SegmentedControl.spec.md +152 -0
  277. package/src/Selector/Selector.doc.mjs +133 -97
  278. package/src/Selector/Selector.spec.md +300 -0
  279. package/src/Selector/Selector.test.tsx +318 -1
  280. package/src/Selector/Selector.tsx +127 -60
  281. package/src/Selector/useSelectorPresentation.ts +24 -4
  282. package/src/SideNav/SideNav.test.tsx +3 -3
  283. package/src/SideNav/SideNavCollapseButton.doc.mjs +8 -0
  284. package/src/SideNav/SideNavItem.doc.mjs +1 -1
  285. package/src/Skeleton/Skeleton.doc.mjs +12 -0
  286. package/src/Skeleton/Skeleton.spec.md +127 -0
  287. package/src/Slider/Slider.doc.mjs +184 -30
  288. package/src/Slider/Slider.spec.md +224 -0
  289. package/src/Slider/Slider.tsx +2 -5
  290. package/src/Spinner/Spinner.doc.mjs +5 -5
  291. package/src/Spinner/Spinner.test.tsx +32 -7
  292. package/src/Spinner/Spinner.tsx +46 -14
  293. package/src/Stack/Stack.doc.mjs +22 -0
  294. package/src/Stack/Stack.spec.md +155 -0
  295. package/src/StatusDot/StatusDot.doc.mjs +17 -0
  296. package/src/StatusDot/StatusDot.spec.md +138 -0
  297. package/src/Stepper/Step.doc.mjs +1 -1
  298. package/src/Stepper/Step.tsx +118 -5
  299. package/src/Stepper/Stepper.doc.mjs +51 -1
  300. package/src/Stepper/Stepper.spec.md +245 -0
  301. package/src/Stepper/Stepper.test.tsx +315 -0
  302. package/src/Stepper/Stepper.tsx +8 -0
  303. package/src/Switch/Switch.doc.mjs +40 -0
  304. package/src/Switch/Switch.spec.md +175 -0
  305. package/src/TabList/TabList.tsx +2 -3
  306. package/src/Table/Table.doc.mjs +102 -24
  307. package/src/Table/Table.perf.test.tsx +51 -1
  308. package/src/Table/Table.spec.md +325 -0
  309. package/src/Table/TableHeaderCell.test.tsx +291 -0
  310. package/src/Table/index.ts +19 -4
  311. package/src/Table/plugins/columnResize/useTableColumnResize.tsx +8 -12
  312. package/src/Table/plugins/filtering/useTableFilterState.test.tsx +143 -0
  313. package/src/Table/plugins/groupedRows/useTableGroupedRows.test.tsx +2 -1
  314. package/src/Table/plugins/rowStatus/index.ts +1 -4
  315. package/src/Table/plugins/rowStatus/useTableRowStatus.public.test.ts +92 -0
  316. package/src/Table/plugins/rowStatus/useTableRowStatus.spec.md +354 -0
  317. package/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx +431 -56
  318. package/src/Table/plugins/rowStatus/useTableRowStatus.tsx +155 -84
  319. package/src/Table/plugins/sortable/useTableSortable.test.tsx +72 -0
  320. package/src/Table/plugins/sortable/useTableSortable.tsx +18 -1
  321. package/src/Table/useTableGroupedRows.doc.mjs +2 -2
  322. package/src/Table/useTableRowStatus.doc.mjs +5 -5
  323. package/src/Text/Text.doc.mjs +190 -43
  324. package/src/Text/Text.spec.md +206 -0
  325. package/src/Text/useTruncation.ts +8 -7
  326. package/src/TextArea/TextArea.doc.mjs +255 -45
  327. package/src/TextArea/TextArea.spec.md +205 -0
  328. package/src/TimeInput/NativeTimeInput.test.tsx +269 -0
  329. package/src/TimeInput/TimeInput.doc.mjs +47 -23
  330. package/src/TimeInput/TimeInput.tsx +109 -50
  331. package/src/TimeInput/index.ts +1 -0
  332. package/src/Timestamp/Timestamp.doc.mjs +2 -2
  333. package/src/Timestamp/Timestamp.test.tsx +75 -3
  334. package/src/Timestamp/Timestamp.tsx +7 -153
  335. package/src/Timestamp/formatRelativeTime.ts +96 -0
  336. package/src/Toast/ToastViewport.test.tsx +194 -0
  337. package/src/Toast/ToastViewport.tsx +136 -58
  338. package/src/ToggleButton/ToggleButton.doc.mjs +56 -0
  339. package/src/Tokenizer/Tokenizer.doc.mjs +2 -0
  340. package/src/Tokenizer/Tokenizer.spec.md +293 -0
  341. package/src/Tokenizer/Tokenizer.test.tsx +335 -1
  342. package/src/Tokenizer/Tokenizer.tsx +129 -46
  343. package/src/Tokenizer/useEndLaneReserve.ts +146 -0
  344. package/src/Toolbar/Toolbar.doc.mjs +19 -0
  345. package/src/Toolbar/Toolbar.spec.md +174 -0
  346. package/src/Tooltip/Tooltip.doc.mjs +17 -0
  347. package/src/Tooltip/Tooltip.spec.md +142 -0
  348. package/src/Tooltip/Tooltip.test.tsx +21 -1
  349. package/src/Tooltip/useTooltip.test.tsx +318 -0
  350. package/src/Tooltip/useTooltip.tsx +60 -39
  351. package/src/TreeList/TreeList.doc.mjs +58 -0
  352. package/src/TreeList/TreeList.spec.md +224 -0
  353. package/src/Typeahead/BaseTypeahead.tsx +96 -25
  354. package/src/Typeahead/Typeahead.doc.mjs +89 -1
  355. package/src/Typeahead/Typeahead.spec.md +257 -0
  356. package/src/Typeahead/Typeahead.test.tsx +502 -1
  357. package/src/Typeahead/Typeahead.tsx +187 -69
  358. package/src/Typeahead/TypeaheadItem.test.tsx +93 -2
  359. package/src/Typeahead/busyIndicatorLane.tsx +109 -0
  360. package/src/__tests__/stylexDeclarations.ts +51 -0
  361. package/src/hooks/useFocusReturnVisibility.ts +3 -5
  362. package/src/hooks/useOverflow.ts +11 -11
  363. package/src/hooks/useScrollLock.test.ts +103 -0
  364. package/src/hooks/useScrollOverflow.ts +7 -4
  365. package/src/i18n/useTranslator.test.tsx +197 -0
  366. package/src/theme/defineTheme.test.ts +227 -0
  367. package/src/theme/defineTheme.ts +30 -0
  368. package/src/theme/derivedVarRegistry.test.ts +6 -0
  369. package/src/theme/generateThemeRules.ts +4 -1
  370. package/src/theme/localTokens.ts +272 -0
  371. package/src/theme/themingTargets.test.ts +126 -28
  372. package/src/utils/interactionModality.test.ts +83 -0
  373. package/src/utils/interactionModality.ts +88 -36
  374. package/src/utils/sharedResizeObserver.test.ts +124 -5
  375. package/src/utils/sharedResizeObserver.ts +60 -14
@@ -1167,7 +1167,12 @@ describe('Selector', () => {
1167
1167
  render(
1168
1168
  <Selector label="Fruit" options={[]} onChange={() => {}} isLoading />,
1169
1169
  );
1170
- await user.click(screen.getByRole('combobox', {name: 'Fruit'}));
1170
+ const trigger = screen.getByRole('combobox', {name: 'Fruit'});
1171
+ expect(trigger).toHaveAttribute('aria-busy', 'true');
1172
+ expect(screen.getByRole('status', {name: 'Loading'})).toBeInTheDocument();
1173
+
1174
+ await user.click(trigger);
1175
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
1171
1176
 
1172
1177
  // useAnnounce writes on the next animation frame, so an immediate read
1173
1178
  // would pass whether or not anything was announced.
@@ -2434,6 +2439,318 @@ describe('Selector', () => {
2434
2439
  expect(trigger).toHaveAttribute('tabIndex', '-1');
2435
2440
  });
2436
2441
  });
2442
+ describe('isReadOnly', () => {
2443
+ const readOnlyCases = [
2444
+ {presentation: 'popover', hasSearch: false},
2445
+ {presentation: 'popover', hasSearch: true},
2446
+ {presentation: 'bottom-sheet', hasSearch: false},
2447
+ {presentation: 'bottom-sheet', hasSearch: true},
2448
+ {presentation: 'adaptive', hasSearch: false},
2449
+ {presentation: 'adaptive', hasSearch: true},
2450
+ ] as const;
2451
+
2452
+ const sheetTransitionCases = [
2453
+ {presentation: 'bottom-sheet', hasSearch: false},
2454
+ {presentation: 'bottom-sheet', hasSearch: true},
2455
+ {presentation: 'adaptive', hasSearch: false},
2456
+ {presentation: 'adaptive', hasSearch: true},
2457
+ ] as const;
2458
+
2459
+ const disabledPrecedenceCases = (
2460
+ ['popover', 'bottom-sheet', 'adaptive'] as const
2461
+ ).flatMap(presentation =>
2462
+ [false, true].flatMap(hasSearch =>
2463
+ [false, true].map(hasDisabledMessage => ({
2464
+ presentation,
2465
+ hasSearch,
2466
+ hasDisabledMessage,
2467
+ })),
2468
+ ),
2469
+ );
2470
+
2471
+ function stubCompactTouch(presentation: 'bottom-sheet' | 'adaptive') {
2472
+ if (presentation !== 'adaptive') {
2473
+ return;
2474
+ }
2475
+ vi.mocked(matchMedia).mockImplementation(
2476
+ (query: string) =>
2477
+ ({
2478
+ matches: query === '(max-width: 768px) and (pointer: coarse)',
2479
+ media: query,
2480
+ onchange: null,
2481
+ addEventListener: vi.fn(),
2482
+ removeEventListener: vi.fn(),
2483
+ addListener: vi.fn(),
2484
+ removeListener: vi.fn(),
2485
+ dispatchEvent: vi.fn(),
2486
+ }) as MediaQueryList,
2487
+ );
2488
+ }
2489
+
2490
+ it.each(readOnlyCases)(
2491
+ 'uses a read-only combobox with no rendered popup ($presentation, search=$hasSearch)',
2492
+ async ({presentation, hasSearch}) => {
2493
+ const user = userEvent.setup();
2494
+ const {container} = render(
2495
+ <form>
2496
+ <Selector
2497
+ label="Fruit"
2498
+ htmlName="fruit"
2499
+ options={OPTIONS}
2500
+ value="Banana"
2501
+ onChange={() => {}}
2502
+ hasClear
2503
+ hasSearch={hasSearch}
2504
+ presentation={presentation}
2505
+ isDefaultOpen
2506
+ isReadOnly
2507
+ />
2508
+ </form>,
2509
+ );
2510
+
2511
+ const trigger = screen.getByRole('combobox', {name: 'Fruit'});
2512
+ expect(trigger.tagName).toBe('BUTTON');
2513
+ expect(trigger).toHaveTextContent('Banana');
2514
+ expect(trigger).toHaveAttribute('aria-readonly', 'true');
2515
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
2516
+ const readOnlyDescription = screen.getByText('Read only');
2517
+ expect(trigger.getAttribute('aria-describedby')).toContain(
2518
+ readOnlyDescription.id,
2519
+ );
2520
+ expect(trigger).not.toHaveAttribute('aria-controls');
2521
+ expect(trigger).not.toHaveAttribute('aria-haspopup');
2522
+ expect(screen.getAllByRole('combobox', h)).toHaveLength(1);
2523
+ expect(screen.queryByRole('listbox', h)).not.toBeInTheDocument();
2524
+ expect(screen.queryByRole('dialog', h)).not.toBeInTheDocument();
2525
+ expect(
2526
+ screen.queryByRole('button', {name: 'Clear Fruit'}),
2527
+ ).not.toBeInTheDocument();
2528
+ expect(
2529
+ container.querySelector('.astryx-selector-indicator-icon'),
2530
+ ).toBeNull();
2531
+ expect(container.querySelector('.astryx-selector')).toHaveAttribute(
2532
+ 'data-readonly',
2533
+ 'readonly',
2534
+ );
2535
+ expect(
2536
+ new FormData(container.querySelector('form')!).get('fruit'),
2537
+ ).toBe('Banana');
2538
+
2539
+ await user.tab();
2540
+ expect(trigger).toHaveFocus();
2541
+ },
2542
+ );
2543
+
2544
+ it('localizes the accessible read-only description', () => {
2545
+ render(
2546
+ <InternationalizationProvider
2547
+ locale="fr"
2548
+ overrides={{fr: {'@astryx.input.readOnly': 'Lecture seule'}}}>
2549
+ <Selector label="Fruit" options={OPTIONS} value="Banana" isReadOnly />
2550
+ </InternationalizationProvider>,
2551
+ );
2552
+
2553
+ const trigger = screen.getByRole('combobox', {name: 'Fruit'});
2554
+ const description = screen.getByText('Lecture seule');
2555
+ expect(trigger.getAttribute('aria-describedby')).toContain(
2556
+ description.id,
2557
+ );
2558
+ });
2559
+
2560
+ it('keeps loading feedback while blocking the selection surface', async () => {
2561
+ const user = userEvent.setup();
2562
+ render(
2563
+ <Selector
2564
+ label="Fruit"
2565
+ options={OPTIONS}
2566
+ value="Apple"
2567
+ isLoading
2568
+ isReadOnly
2569
+ />,
2570
+ );
2571
+
2572
+ const trigger = screen.getByRole('combobox', {name: 'Fruit'});
2573
+ expect(trigger).toHaveAttribute('aria-busy', 'true');
2574
+ expect(screen.getByRole('status', {name: 'Loading'})).toBeInTheDocument();
2575
+
2576
+ await user.click(trigger);
2577
+ expect(screen.queryByRole('listbox', h)).not.toBeInTheDocument();
2578
+ });
2579
+
2580
+ it('blocks pointer, keyboard, and typeahead changes', async () => {
2581
+ const user = userEvent.setup();
2582
+ const onChange = vi.fn();
2583
+ const changeAction = vi.fn();
2584
+ render(
2585
+ <Selector
2586
+ label="Fruit"
2587
+ options={OPTIONS}
2588
+ value="Apple"
2589
+ onChange={onChange}
2590
+ changeAction={changeAction}
2591
+ isReadOnly
2592
+ />,
2593
+ );
2594
+
2595
+ const trigger = screen.getByRole('combobox');
2596
+ await user.click(trigger);
2597
+ trigger.focus();
2598
+ await user.keyboard('{Enter}{ArrowDown}c{Backspace}');
2599
+
2600
+ expect(screen.queryByRole('listbox', h)).not.toBeInTheDocument();
2601
+ expect(onChange).not.toHaveBeenCalled();
2602
+ expect(changeAction).not.toHaveBeenCalled();
2603
+ });
2604
+
2605
+ it.each([false, true])(
2606
+ 'restores popover focus when an open selector becomes read-only (search=%s)',
2607
+ async hasSearch => {
2608
+ const user = userEvent.setup();
2609
+ const selector = (isReadOnly: boolean) => (
2610
+ <Selector
2611
+ label="Fruit"
2612
+ options={OPTIONS}
2613
+ value="Apple"
2614
+ hasSearch={hasSearch}
2615
+ isReadOnly={isReadOnly}
2616
+ />
2617
+ );
2618
+ const {rerender} = render(selector(false));
2619
+ const editableTrigger = screen.getByRole(
2620
+ hasSearch ? 'button' : 'combobox',
2621
+ {name: 'Fruit'},
2622
+ );
2623
+ await user.click(editableTrigger);
2624
+ if (hasSearch) {
2625
+ await waitFor(() =>
2626
+ expect(screen.getByRole('combobox', h)).toHaveFocus(),
2627
+ );
2628
+ }
2629
+
2630
+ rerender(selector(true));
2631
+
2632
+ const readOnlyTrigger = screen.getByRole('combobox', {name: 'Fruit'});
2633
+ await waitFor(() => expect(readOnlyTrigger).toHaveFocus());
2634
+ expect(screen.queryByRole('listbox', h)).not.toBeInTheDocument();
2635
+ },
2636
+ );
2637
+
2638
+ it.each(sheetTransitionCases)(
2639
+ 'finishes $presentation close and restores focus when read-only changes (search=$hasSearch)',
2640
+ async ({presentation, hasSearch}) => {
2641
+ stubCompactTouch(presentation);
2642
+ const user = userEvent.setup();
2643
+ const selector = (isReadOnly: boolean) => (
2644
+ <Selector
2645
+ label="Fruit"
2646
+ options={OPTIONS}
2647
+ value="Apple"
2648
+ hasSearch={hasSearch}
2649
+ presentation={presentation}
2650
+ isReadOnly={isReadOnly}
2651
+ />
2652
+ );
2653
+ const {rerender} = render(selector(false));
2654
+ const editableTrigger = screen.getByRole(
2655
+ hasSearch ? 'button' : 'combobox',
2656
+ {name: 'Fruit'},
2657
+ );
2658
+ await user.click(editableTrigger);
2659
+ const dialog = await screen.findByRole('dialog', {name: 'Fruit'});
2660
+ const panel = dialog.querySelector<HTMLElement>('.astryx-bottom-sheet');
2661
+ expect(panel).not.toBeNull();
2662
+ await waitFor(() =>
2663
+ expect(
2664
+ hasSearch
2665
+ ? screen.getByRole('combobox', h)
2666
+ : screen.getByRole('listbox'),
2667
+ ).toHaveFocus(),
2668
+ );
2669
+
2670
+ rerender(selector(true));
2671
+
2672
+ await waitFor(() => expect(dialog).toHaveAttribute('inert'));
2673
+ fireEvent.transitionEnd(panel!, {propertyName: 'transform'});
2674
+ const readOnlyTrigger = screen.getByRole('combobox', {name: 'Fruit'});
2675
+ await waitFor(() => expect(readOnlyTrigger).toHaveFocus());
2676
+ expect(screen.queryByRole('dialog', h)).not.toBeInTheDocument();
2677
+ expect(screen.queryByRole('listbox', h)).not.toBeInTheDocument();
2678
+ },
2679
+ );
2680
+
2681
+ it.each(disabledPrecedenceCases)(
2682
+ 'matches disabled-only semantics for $presentation (search=$hasSearch, reason=$hasDisabledMessage)',
2683
+ async ({presentation, hasSearch, hasDisabledMessage}) => {
2684
+ if (presentation === 'adaptive') {
2685
+ stubCompactTouch(presentation);
2686
+ }
2687
+ const user = userEvent.setup();
2688
+ const onChange = vi.fn();
2689
+ const selector = (isReadOnly: boolean) => (
2690
+ <form>
2691
+ <Selector
2692
+ label="Fruit"
2693
+ htmlName="fruit"
2694
+ options={OPTIONS}
2695
+ value="Banana"
2696
+ onChange={onChange}
2697
+ hasClear
2698
+ hasSearch={hasSearch}
2699
+ presentation={presentation}
2700
+ isDisabled
2701
+ isReadOnly={isReadOnly}
2702
+ disabledMessage={
2703
+ hasDisabledMessage ? 'Locked by policy' : undefined
2704
+ }
2705
+ />
2706
+ </form>
2707
+ );
2708
+ const {container, rerender} = render(selector(false));
2709
+ const getTrigger = () =>
2710
+ screen.getByRole(hasSearch ? 'button' : 'combobox', {name: 'Fruit'});
2711
+ const snapshot = () => {
2712
+ const trigger = getTrigger();
2713
+ const root = container.querySelector('.astryx-selector');
2714
+ return {
2715
+ tagName: trigger.tagName,
2716
+ role: trigger.getAttribute('role'),
2717
+ hasPopup: trigger.getAttribute('aria-haspopup'),
2718
+ expanded: trigger.getAttribute('aria-expanded'),
2719
+ controls: trigger.getAttribute('aria-controls'),
2720
+ readOnly: trigger.getAttribute('aria-readonly'),
2721
+ disabled: trigger.matches(':disabled'),
2722
+ ariaDisabled: trigger.getAttribute('aria-disabled'),
2723
+ tabIndex: trigger.tabIndex,
2724
+ rootClassName: root?.className,
2725
+ rootDisabled: root?.getAttribute('data-disabled'),
2726
+ rootReadOnly: root?.getAttribute('data-readonly'),
2727
+ hasIndicator:
2728
+ root?.querySelector('.astryx-selector-indicator-icon') != null,
2729
+ listboxes: screen.queryAllByRole('listbox', h).length,
2730
+ dialogs: screen.queryAllByRole('dialog', h).length,
2731
+ formEntries: [
2732
+ ...new FormData(container.querySelector('form')!).entries(),
2733
+ ],
2734
+ };
2735
+ };
2736
+ const disabledOnly = snapshot();
2737
+
2738
+ rerender(selector(true));
2739
+
2740
+ expect(snapshot()).toEqual(disabledOnly);
2741
+ expect(disabledOnly.rootDisabled).toBe('disabled');
2742
+ expect(disabledOnly.rootReadOnly).toBeNull();
2743
+ expect(disabledOnly.readOnly).toBeNull();
2744
+ expect(disabledOnly.formEntries).toEqual([]);
2745
+ const trigger = getTrigger();
2746
+ await user.click(trigger);
2747
+ trigger.focus();
2748
+ await user.keyboard('{Enter}{ArrowDown}c{Backspace}');
2749
+ expect(onChange).not.toHaveBeenCalled();
2750
+ },
2751
+ );
2752
+ });
2753
+
2437
2754
  describe('form participation', () => {
2438
2755
  it('submits the selected value under htmlName', () => {
2439
2756
  const {container} = render(
@@ -244,6 +244,16 @@ const styles = stylex.create({
244
244
  ':active': 'none',
245
245
  },
246
246
  },
247
+ triggerReadOnly: {
248
+ cursor: 'default',
249
+ },
250
+ triggerGhostReadOnly: {
251
+ backgroundImage: 'none',
252
+ transform: {
253
+ default: 'none',
254
+ ':active': 'none',
255
+ },
256
+ },
247
257
 
248
258
  // Clear button
249
259
  statusButton: {
@@ -506,6 +516,17 @@ interface SelectorPropsBase<
506
516
  */
507
517
  isDisabled?: boolean;
508
518
 
519
+ /**
520
+ * Whether the selector is read-only.
521
+ * The selected value stays visible, focusable, and included in form
522
+ * submission, and retains its combobox identity with `aria-readonly`. The
523
+ * selection surface and editing affordances are removed.
524
+ * Unlike `isDisabled`, a read-only selector is not dimmed and stays in the
525
+ * tab order. `isDisabled` takes precedence when both are set.
526
+ * @default false
527
+ */
528
+ isReadOnly?: boolean;
529
+
509
530
  /**
510
531
  * Explains why the selector is disabled. When set together with
511
532
  * `isDisabled`, the selector shows a tooltip with this text on hover and
@@ -804,6 +825,7 @@ export function Selector<T extends SelectorOptionType>(
804
825
  isOptional = false,
805
826
  isRequired = false,
806
827
  isDisabled = false,
828
+ isReadOnly = false,
807
829
  disabledMessage,
808
830
  options,
809
831
  value,
@@ -851,6 +873,7 @@ export function Selector<T extends SelectorOptionType>(
851
873
  variant === 'ghost' && statusVariant === 'attached'
852
874
  ? 'detached'
853
875
  : statusVariant;
876
+ const isEffectivelyReadOnly = isReadOnly && !isDisabled;
854
877
 
855
878
  // Normalize null to undefined for internal use (null is the clear sentinel)
856
879
  const normalizedValue = value === null ? undefined : value;
@@ -862,11 +885,12 @@ export function Selector<T extends SelectorOptionType>(
862
885
  const descriptionId = useId();
863
886
  const statusMessageId = useId();
864
887
  const inputLabelId = useId();
888
+ const readOnlyDescriptionId = useId();
865
889
  const searchId = useId();
866
890
  // Measure from the same outer control that usePopover anchors to; using the
867
891
  // shorter inner button makes every size's selected row land too low.
868
892
  const anchorRef = useRef<HTMLDivElement>(null);
869
- const triggerRef = useRef<HTMLButtonElement>(null);
893
+ const triggerRef = useRef<HTMLElement>(null);
870
894
  const searchRef = useRef<HTMLInputElement>(null);
871
895
  const inputGroup = useInputGroup();
872
896
 
@@ -923,6 +947,7 @@ export function Selector<T extends SelectorOptionType>(
923
947
  ? statusTooltip.describedBy
924
948
  : null,
925
949
  showsDisabledMessage ? disabledMessageTooltip.describedBy : null,
950
+ isEffectivelyReadOnly ? readOnlyDescriptionId : null,
926
951
  ],
927
952
  inputGroup,
928
953
  );
@@ -985,16 +1010,27 @@ export function Selector<T extends SelectorOptionType>(
985
1010
  },
986
1011
  });
987
1012
  const {popover} = surface;
1013
+ const hideSurface = surface.hide;
1014
+ const isSurfaceOpen = surface.isOpen;
988
1015
  const keepOpenProps = useKeepLayerOpenProps(popover.id, popover.isOpen);
989
1016
 
990
- // Open dropdown on mount when isDefaultOpen is true
1017
+ // Open dropdown on mount when isDefaultOpen is true and interaction is allowed.
991
1018
  useEffect(() => {
992
- if (isDefaultOpen) {
1019
+ if (isDefaultOpen && !isEffectivelyReadOnly) {
993
1020
  surface.show();
994
1021
  }
995
1022
  // eslint-disable-next-line @eslint-react/exhaustive-deps -- mount-only: isDefaultOpen is not reactive
996
1023
  }, []);
997
1024
 
1025
+ // Read-only is controlled by caller policy, so an already-open surface must
1026
+ // close when that policy changes. The presentation controller keeps a sheet
1027
+ // mounted through its exit and lets BottomSheet return final focus.
1028
+ useEffect(() => {
1029
+ if (isEffectivelyReadOnly && isSurfaceOpen) {
1030
+ hideSurface();
1031
+ }
1032
+ }, [isEffectivelyReadOnly, isSurfaceOpen, hideSurface]);
1033
+
998
1034
  // Announce the filtered result count from the query-change handlers (matching
999
1035
  // BaseTypeahead) rather than a reactive effect: computing the count for the
1000
1036
  // next query here fires the announcement exactly once per keystroke and does
@@ -1145,7 +1181,7 @@ export function Selector<T extends SelectorOptionType>(
1145
1181
  // highlight on it and Delete/Backspace could clear a value the action has
1146
1182
  // already replaced.
1147
1183
  value: optimisticValue,
1148
- isDisabled,
1184
+ isDisabled: isDisabled || isEffectivelyReadOnly,
1149
1185
  isOpen: surface.isOpen,
1150
1186
  hasSearch,
1151
1187
  onOpen: useCallback(() => {
@@ -1197,14 +1233,17 @@ export function Selector<T extends SelectorOptionType>(
1197
1233
 
1198
1234
  const handleTriggerKeyDown = useCallback(
1199
1235
  (e: React.KeyboardEvent) => {
1236
+ if (isDisabled || isEffectivelyReadOnly) {
1237
+ return;
1238
+ }
1200
1239
  // With hasSearch the query input owns typing, so type-to-select is off.
1201
- if (!isDisabled && !hasSearch && typeahead.onKeyDown(e)) {
1240
+ if (!hasSearch && typeahead.onKeyDown(e)) {
1202
1241
  e.preventDefault();
1203
1242
  return;
1204
1243
  }
1205
1244
  onKeyDown(e);
1206
1245
  },
1207
- [isDisabled, hasSearch, typeahead, onKeyDown],
1246
+ [isDisabled, isEffectivelyReadOnly, hasSearch, typeahead, onKeyDown],
1208
1247
  );
1209
1248
 
1210
1249
  // Keep the highlighted option visible during keyboard navigation. The
@@ -1628,30 +1667,55 @@ export function Selector<T extends SelectorOptionType>(
1628
1667
  </div>
1629
1668
  );
1630
1669
 
1631
- const selectionSurface =
1632
- surface.activePresentation === 'bottom-sheet' ? (
1633
- <SelectorBottomSheet
1634
- isOpen={surface.isSheetOpen}
1635
- onOpenChange={surface.onSheetOpenChange}
1636
- finalFocusRef={triggerRef}
1637
- initialFocusRef={hasSearch ? searchRef : listboxRef}
1638
- label={label}>
1639
- {panelContent}
1640
- </SelectorBottomSheet>
1641
- ) : (
1642
- popover.render(panelContent, {
1643
- placement: popoverPlacement,
1644
- alignment: 'start',
1645
- // The system's standard menu clearance, except in overlay mode:
1646
- // there the measured negative margin owns the block geometry and
1647
- // the menu is meant to sit on the trigger, not clear it.
1648
- offset: shouldOverlaySelectedItem
1649
- ? undefined
1650
- : spacingVars['--spacing-1'],
1651
- xstyle: [styles.popover, layerAnimations[popoverPlacement]],
1652
- style: popoverOffsetStyle,
1653
- })
1654
- );
1670
+ let selectionSurface: ReactNode = null;
1671
+ if (surface.activePresentation === 'bottom-sheet') {
1672
+ if (!isEffectivelyReadOnly || surface.isSheetPresented) {
1673
+ selectionSurface = (
1674
+ <SelectorBottomSheet
1675
+ isOpen={surface.isSheetOpen}
1676
+ onOpenChange={surface.onSheetOpenChange}
1677
+ finalFocusRef={triggerRef}
1678
+ initialFocusRef={hasSearch ? searchRef : listboxRef}
1679
+ label={label}>
1680
+ {panelContent}
1681
+ </SelectorBottomSheet>
1682
+ );
1683
+ }
1684
+ } else if (!isEffectivelyReadOnly) {
1685
+ selectionSurface = popover.render(panelContent, {
1686
+ placement: popoverPlacement,
1687
+ alignment: 'start',
1688
+ // The system's standard menu clearance, except in overlay mode:
1689
+ // there the measured negative margin owns the block geometry and
1690
+ // the menu is meant to sit on the trigger, not clear it.
1691
+ offset: shouldOverlaySelectedItem
1692
+ ? undefined
1693
+ : spacingVars['--spacing-1'],
1694
+ xstyle: [styles.popover, layerAnimations[popoverPlacement]],
1695
+ style: popoverOffsetStyle,
1696
+ });
1697
+ }
1698
+
1699
+ const triggerSharedProps: React.HTMLAttributes<HTMLElement> = {
1700
+ ...rest,
1701
+ id: triggerId,
1702
+ 'aria-describedby': ariaDescribedBy,
1703
+ 'aria-labelledby': ariaLabelledBy,
1704
+ 'aria-required': isEffectivelyRequired ? 'true' : undefined,
1705
+ 'aria-invalid': status?.type === 'error' ? 'true' : undefined,
1706
+ 'aria-busy': isBusy || undefined,
1707
+ onKeyDown: handleTriggerKeyDown,
1708
+ onFocus: event => {
1709
+ onFocus?.(event);
1710
+ surface.onTriggerFocus(event);
1711
+ },
1712
+ tabIndex: isDisabled && !showsDisabledMessage ? -1 : 0,
1713
+ ...stylex.props(
1714
+ styles.trigger,
1715
+ isEffectivelyReadOnly && styles.triggerReadOnly,
1716
+ inputGroup && styles.triggerButtonInGroup,
1717
+ ),
1718
+ };
1655
1719
 
1656
1720
  const selectorContent = (
1657
1721
  <>
@@ -1672,6 +1736,7 @@ export function Selector<T extends SelectorOptionType>(
1672
1736
  size,
1673
1737
  status: status?.type ?? null,
1674
1738
  disabled: isDisabled ? 'disabled' : null,
1739
+ readonly: isEffectivelyReadOnly ? 'readonly' : null,
1675
1740
  }),
1676
1741
  stylex.props(
1677
1742
  inputWrapperStyles.base,
@@ -1683,7 +1748,11 @@ export function Selector<T extends SelectorOptionType>(
1683
1748
  surface.isTriggerFocusRingSuppressed &&
1684
1749
  selectorPresentationStyles.pointerRestoredFocus,
1685
1750
  isDisabled && inputWrapperStyles.disabled,
1751
+ isEffectivelyReadOnly && styles.triggerReadOnly,
1686
1752
  variant === 'ghost' && isDisabled && styles.triggerGhostDisabled,
1753
+ variant === 'ghost' &&
1754
+ isEffectivelyReadOnly &&
1755
+ styles.triggerGhostReadOnly,
1687
1756
  !selectedItem && styles.triggerPlaceholder,
1688
1757
  variant !== 'ghost' &&
1689
1758
  status &&
@@ -1704,45 +1773,43 @@ export function Selector<T extends SelectorOptionType>(
1704
1773
  {inputGroup && (
1705
1774
  <VisuallyHidden id={inputLabelId}>{label}</VisuallyHidden>
1706
1775
  )}
1776
+ {isEffectivelyReadOnly && (
1777
+ <VisuallyHidden id={readOnlyDescriptionId}>
1778
+ {t('@astryx.input.readOnly')}
1779
+ </VisuallyHidden>
1780
+ )}
1707
1781
  <button
1708
- ref={triggerRef}
1709
- id={triggerId}
1782
+ {...triggerSharedProps}
1783
+ ref={triggerRef as React.Ref<HTMLButtonElement>}
1710
1784
  type="button"
1711
- // In hasSearch mode the popup's search input is the combobox (it owns
1712
- // focus + aria-activedescendant, comboboxes-4), so the trigger is a
1713
- // plain button that opens the listbox — not a second combobox.
1714
- role={hasSearch ? undefined : 'combobox'}
1715
- {...rest}
1785
+ // The read-only trigger stays a combobox even when hasSearch is set:
1786
+ // no search input is rendered in that state, and preserving the role
1787
+ // keeps the control's programmatic identity stable. Editable search
1788
+ // mode still moves combobox semantics to the popup input.
1789
+ role={isEffectivelyReadOnly || !hasSearch ? 'combobox' : undefined}
1716
1790
  aria-haspopup={
1717
- surface.activePresentation === 'bottom-sheet' ? 'dialog' : 'listbox'
1791
+ isEffectivelyReadOnly
1792
+ ? undefined
1793
+ : surface.activePresentation === 'bottom-sheet'
1794
+ ? 'dialog'
1795
+ : 'listbox'
1718
1796
  }
1719
- aria-expanded={surface.isOpen}
1720
- aria-controls={listboxId}
1797
+ aria-expanded={isEffectivelyReadOnly ? false : surface.isOpen}
1798
+ aria-controls={isEffectivelyReadOnly ? undefined : listboxId}
1799
+ aria-readonly={isEffectivelyReadOnly || undefined}
1721
1800
  aria-activedescendant={
1722
- !hasSearch && surface.isOpen && highlightedIndex >= 0
1801
+ !isEffectivelyReadOnly &&
1802
+ !hasSearch &&
1803
+ surface.isOpen &&
1804
+ highlightedIndex >= 0
1723
1805
  ? getItemId(highlightedIndex)
1724
1806
  : undefined
1725
1807
  }
1726
- aria-describedby={ariaDescribedBy}
1727
- aria-labelledby={ariaLabelledBy}
1728
- aria-required={isEffectivelyRequired ? 'true' : undefined}
1729
- aria-invalid={status?.type === 'error' ? 'true' : undefined}
1730
- aria-busy={isBusy || undefined}
1731
1808
  // With a disabledMessage the trigger keeps focusability via
1732
1809
  // aria-disabled so the reason is focus-discoverable; activation is
1733
1810
  // still blocked by the isDisabled guards in useCombobox.
1734
1811
  disabled={isDisabled && !showsDisabledMessage}
1735
- aria-disabled={showsDisabledMessage ? 'true' : undefined}
1736
- onKeyDown={handleTriggerKeyDown}
1737
- onFocus={event => {
1738
- onFocus?.(event);
1739
- surface.onTriggerFocus(event);
1740
- }}
1741
- tabIndex={isDisabled && !showsDisabledMessage ? -1 : 0}
1742
- {...stylex.props(
1743
- styles.trigger,
1744
- inputGroup && styles.triggerButtonInGroup,
1745
- )}>
1812
+ aria-disabled={showsDisabledMessage ? 'true' : undefined}>
1746
1813
  {valueContent}
1747
1814
  </button>
1748
1815
  {htmlName != null && (
@@ -1756,7 +1823,7 @@ export function Selector<T extends SelectorOptionType>(
1756
1823
  />
1757
1824
  )}
1758
1825
  {isBusy && <Spinner size="sm" />}
1759
- {hasClear && value != null && !isDisabled && (
1826
+ {hasClear && value != null && !isDisabled && !isEffectivelyReadOnly && (
1760
1827
  <InternalInputClearButton
1761
1828
  {...keepOpenProps}
1762
1829
  label={t('@astryx.selector.clearLabel', {label})}
@@ -1798,7 +1865,7 @@ export function Selector<T extends SelectorOptionType>(
1798
1865
  xstyle={styles.triggerIcon}
1799
1866
  />
1800
1867
  )
1801
- ) : (
1868
+ ) : !isEffectivelyReadOnly ? (
1802
1869
  <Icon
1803
1870
  icon="chevronDown"
1804
1871
  size="sm"
@@ -1820,7 +1887,7 @@ export function Selector<T extends SelectorOptionType>(
1820
1887
  state: surface.isOpen ? 'expanded' : 'collapsed',
1821
1888
  })}
1822
1889
  />
1823
- )}
1890
+ ) : null}
1824
1891
  </div>
1825
1892
 
1826
1893
  {selectionSurface}