@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
@@ -0,0 +1,293 @@
1
+ ---
2
+ schema_version: 3
3
+ template_version: 3
4
+ kind: component
5
+ id: component:MultiSelector
6
+ authority: draft
7
+ archive_reason: null
8
+ superseded_by: null
9
+ approved_by: null
10
+ approved_at: null
11
+ owners: [cixzhang]
12
+ review_triggers: [public-api, behavior, theming, accessibility]
13
+ verified_by:
14
+ [
15
+ packages/core/src/MultiSelector/MultiSelector.test.tsx,
16
+ packages/core/src/BottomSheet/BottomSheet.test.tsx,
17
+ packages/core/src/CheckboxInput/CheckboxInput.test.tsx,
18
+ packages/core/src/Divider/Divider.test.tsx,
19
+ packages/core/src/Field/Field.test.tsx,
20
+ packages/core/src/Icon/Icon.test.tsx,
21
+ packages/core/src/Text/Text.test.tsx,
22
+ scripts/check-knowledge.mjs,
23
+ ]
24
+ modules: []
25
+ families: [family:input-fields, family:overlay-dismissal]
26
+ design_specs: []
27
+ architecture:
28
+ [
29
+ architecture:component-theming-surface,
30
+ architecture:layer-runtime,
31
+ architecture:public-component-api,
32
+ ]
33
+ contributing: []
34
+ system_specs: [spec:AST-011/DEC-1]
35
+ ---
36
+
37
+ # MultiSelector component contract
38
+
39
+ ## Intent
40
+
41
+ MultiSelector renders a multi-selection control with an optional standalone Field
42
+ shell and one shared panel content tree in either an anchored pointer popup or a
43
+ touch-oriented BottomSheet. It also supports a read-only state that preserves and
44
+ submits the selected values without exposing that panel or an editing affordance.
45
+ This draft records current consumer anatomy, theming ownership, and read-only
46
+ behavior.
47
+
48
+ ## Compatibility and migration
49
+
50
+ - Released default preserved: `yes`
51
+ - `isReadOnly` is additive and defaults to `false`. It preserves selected values,
52
+ focus, and form participation while removing selection-surface and editing
53
+ affordances. `isDisabled` takes precedence when both are set.
54
+ - Existing DOM, styling, targets, and public API remain unchanged when the prop is
55
+ omitted.
56
+ - Controlled/uncontrolled behavior: unchanged
57
+ - Migration decision: none
58
+
59
+ Consumer migration instructions belong in consumer docs and release notes.
60
+
61
+ ## Ownership boundary
62
+
63
+ **Owns**
64
+
65
+ - The painted Trigger and its current `multi-selector` target.
66
+ - Indicator icon, Search row, Option row, Section heading, Empty state, and
67
+ Pointer popup presentation through the other six current MultiSelector targets.
68
+ - One shared panel-content tree containing optional search, option, divider,
69
+ section, and empty-state content regardless of which presentation hosts it.
70
+
71
+ **Does not own / non-goals**
72
+
73
+ - Standalone field-shell presentation — rendered by Field and themed through
74
+ Field's current `field` target; omitted when MultiSelector is inside InputGroup.
75
+ - Shared clear actions — rendered by Field's InputClearButton and themed through
76
+ Field's current `input-clear-button` target.
77
+ - Option checkbox presentation — rendered by CheckboxInput and themed through
78
+ CheckboxInput's current `checkbox-indicator` target.
79
+ - Public option dividers — rendered by Divider and themed through Divider's
80
+ current `divider` target.
81
+ - General Icon presentation for start, search, and status icons — rendered by
82
+ Icon and themed through Icon's current `icon` target.
83
+ - The Touch sheet heading — rendered by Heading and themed through Text's current
84
+ `heading` target.
85
+ - The Touch sheet panel — rendered by BottomSheet and themed through
86
+ BottomSheet's current `bottom-sheet` target.
87
+ - Arbitrary ReactNode start content supplied by the caller.
88
+ - Shared layer lifecycle, positioning, and dismissal behavior.
89
+
90
+ ## Public concepts
91
+
92
+ This table names the additive concept introduced by `spec:AST-011`. Complete prop
93
+ syntax and examples remain in `MultiSelector.doc.mjs`.
94
+
95
+ | Concept | Closed values or states | Meaning | Availability | Default | Owner | Stability | Invalid-value behavior |
96
+ | --------------- | ----------------------- | --------------------------------------------------------- | -------------- | ------- | ------ | --------- | ---------------------- |
97
+ | read-only state | `false`, `true` | Preserves and submits values without selection affordance | Closed trigger | `false` | Caller | additive | Boolean normalization |
98
+
99
+ ## Behavioral and layout contract
100
+
101
+ | ID | Candidate invariant | Basis | Draft review state |
102
+ | --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | -------------------------------------------------- |
103
+ | FR1 | The current standalone presentation contains a Field and painted Trigger; the supported InputGroup path renders the same Trigger without a Field. Optional start content, clear action, status icon, or Indicator icon follows supplied props and state. | Current source, docs, and tests | Verified current behavior; no new behavior decided |
104
+ | FR2 | The shared panel content contains the optional Search row, zero or more Option rows, optional public Option dividers and Section headings, and an Empty state when the applicable result set is empty and the value is not loading. | Current source, docs, and tests | Verified current behavior; no new behavior decided |
105
+ | FR3 | Trigger, Indicator icon, Search row, Option row, Section heading, Empty state, and Pointer popup carry the seven current non-deprecated MultiSelector targets documented in `MultiSelector.doc.mjs`. | Current source, docs, and tests | Verified current behavior; no target change |
106
+ | FR4 | When present, Field, shared clear actions, option checkbox indicators, public Option dividers, general icons, Touch sheet heading, and Touch sheet retain their Field, CheckboxInput, Divider, Icon, Text, and BottomSheet theming owners. | Current composition and owners | Verified current behavior; no target change |
107
+ | FR5 | Pointer popup and Touch sheet are separate surface anatomy rows, but both host the same `panelContent` tree; the Touch sheet additionally renders its Heading. Changing presentation does not create a second search, option, divider, section, or empty-state content model. | Current source and tests | Verified current behavior; no normalization |
108
+ | FR6 | `multi-selector-clear-icon` remains a deprecated compatibility alias for `input-clear-icon`; it is not a current target and does not claim a separate anatomy part. | Current public target metadata | Verified current compatibility state |
109
+ | FR7 | While `isReadOnly` is true, selected values remain focusable and form-submittable, while the selection surface, clear action, disclosure indicator, and every value-change path are unavailable. | `spec:AST-011`, docs, and tests | Accepted read-only behavior |
110
+
111
+ ### Allowed variation
112
+
113
+ - Standalone MultiSelector renders Field; the supported InputGroup path omits it.
114
+ - `presentation="popover"` uses the Pointer popup; `presentation="bottom-sheet"`
115
+ uses the Touch sheet and its Heading; `presentation="adaptive"` selects between
116
+ those existing branches from current modality and viewport behavior.
117
+ - Search row, Option divider, Section heading, Empty state, select-all Option row,
118
+ and both clear actions are optional under their documented prop and data conditions.
119
+ - Semantic names and icon component types render through Icon. Arbitrary ReactNode
120
+ start content renders directly and stays caller-owned.
121
+ - Trigger text, labels, badges, and custom option content may vary inside the
122
+ Trigger or Option row without creating another MultiSelector target.
123
+
124
+ ### Representative states
125
+
126
+ | State | Required invariant | Allowed variation |
127
+ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
128
+ | Standalone or InputGroup | Field is present only standalone; Trigger anatomy and targets remain the same in both paths. | InputGroup owns grouped layout and labeling support. |
129
+ | Pointer presentation | Pointer popup carries `multi-selector-popup` and hosts the shared panel content. | Search, groups, options, and empty content follow their ordinary states. |
130
+ | Touch presentation | BottomSheet owns the Sheet panel, Text owns its Heading, and the sheet hosts the same shared panel content. | Adaptive or explicit selection may choose this branch. |
131
+ | Search enabled with a query | Search row owns `multi-selector-search`; its icons/actions keep their shared owners. | Results may be flat, grouped, or empty. |
132
+ | Option, select all, selected, disabled | Option row owns `multi-selector-option` and reflects its current size/state metadata. | Checkbox state and custom row content vary independently. |
133
+ | Attached or tooltip status | Status icon replaces the Indicator icon. | Tooltip status wraps the icon in a button without changing Icon owner. |
134
+ | Read-only | Values stay focusable and submittable; panel, clear, and disclosure affordances are absent. | Trigger display, status, and busy presentation remain available. |
135
+
136
+ ### Transformation and precedence order
137
+
138
+ - No new search, selection, presentation, or styling precedence rule is introduced.
139
+
140
+ ### Performance and resources
141
+
142
+ - No new performance or resource rule is introduced.
143
+
144
+ ## Accessibility contract
145
+
146
+ Existing field, combobox, listbox, checkbox, live-region, focus, and dismissal
147
+ behavior remains unchanged when editable. A read-only value stays focusable,
148
+ retains `role="combobox"`, and exposes `aria-readonly="true"` plus
149
+ `aria-expanded="false"`. Its rendered content remains the combobox value. It does
150
+ not expose `aria-controls`, `aria-activedescendant`, or an active selection
151
+ surface. The implicit listbox popup describes the widget's selection capability;
152
+ read-only state communicates that its values cannot currently change. Chromium's
153
+ button-hosted combobox mapping omits the ARIA read-only property, so a localized
154
+ accessible description also announces that state. Search and non-search modes use
155
+ the same read-only combobox semantics because neither exposes a search control in
156
+ this state.
157
+
158
+ ## Design relationships
159
+
160
+ | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
161
+ | ----------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------ | ----------------- | ------------------ |
162
+ | Field | Provides the label and optional supporting content outside InputGroup; omitted inside InputGroup. | Field component | Supporting | FR1, FR4 |
163
+ | Trigger | Presents the current selection or placeholder and opens the active presentation. | Current source and public docs | Prominent | FR1, FR3 |
164
+ | Icon-rendered start icon | Presents a semantic or component icon through Icon. | Icon component | Supporting | FR1, FR4 |
165
+ | Caller-rendered start content | Presents arbitrary caller content directly in the trigger's start slot. | Caller-supplied content | Context-dependent | FR1 |
166
+ | Trigger clear button | Removes every selected value through the shared field clear action. | Field component | Supporting | FR1, FR4 |
167
+ | Status icon | Presents attached or tooltip validation state through Icon. | Icon component | Supporting | FR1, FR4 |
168
+ | Indicator icon | Presents disclosure state when status presentation does not replace it. | Current source and public docs | Supporting | FR1, FR3 |
169
+ | Search row | Provides query entry at the top of the shared panel content. | Current source and public docs | Supporting | FR2, FR3, FR5 |
170
+ | Search icon | Presents the search affordance through Icon. | Icon component | Supporting | FR2, FR4 |
171
+ | Search clear button | Clears a present query through the shared field clear action. | Field component | Supporting | FR2, FR4 |
172
+ | Option row | Presents one option or the select-all choice and owns row interaction/state. | Current source and public docs | Prominent | FR2, FR3 |
173
+ | Option checkbox indicator | Presents the option's selected, unselected, or indeterminate state through CheckboxInput. | CheckboxInput component | Supporting | FR2, FR4 |
174
+ | Option divider | Separates adjacent groups when a public divider entry is present in the options data. | Divider component | Supporting | FR2, FR4 |
175
+ | Section heading | Labels a visible group of option rows. | Current source and public docs | Supporting | FR2, FR3 |
176
+ | Empty state | Presents no-options or no-results content when the panel has no selectable rows to show. | Current source and public docs | Prominent | FR2, FR3 |
177
+ | Pointer popup | Paints the anchored pointer surface around the shared panel content. | Current source and public docs | Prominent | FR3, FR5 |
178
+ | Touch sheet heading | Names the bottom-sheet presentation above the shared panel content. | Text component | Prominent | FR4, FR5 |
179
+ | Touch sheet | Paints the BottomSheet surface around the same shared panel content. | BottomSheet component | Prominent | FR4, FR5 |
180
+
181
+ The seven current MultiSelector targets remain on their shipped visible elements.
182
+ Nested shared primitives keep their own owners. The Pointer popup and Touch sheet
183
+ are intentionally separate rows because they use different surface owners; the
184
+ Touch sheet adds a Heading owned by Text. Search, option, divider, section, and
185
+ empty-state content remains one shared tree.
186
+
187
+ ### Theming anatomy
188
+
189
+ <!-- anatomy-theming:v1 -->
190
+
191
+ ```json
192
+ {
193
+ "Field": {"delegatesTo": {"owner": "component:Field", "target": "field"}},
194
+ "Trigger": {"target": "multi-selector"},
195
+ "Icon-rendered start icon": {
196
+ "delegatesTo": {"owner": "component:Icon", "target": "icon"}
197
+ },
198
+ "Caller-rendered start content": {
199
+ "none": {
200
+ "reason": "intentional: Arbitrary ReactNode content is caller-owned and receives no MultiSelector target."
201
+ }
202
+ },
203
+ "Trigger clear button": {
204
+ "delegatesTo": {
205
+ "owner": "component:Field",
206
+ "target": "input-clear-button"
207
+ }
208
+ },
209
+ "Status icon": {
210
+ "delegatesTo": {"owner": "component:Icon", "target": "icon"}
211
+ },
212
+ "Indicator icon": {"target": "multi-selector-indicator-icon"},
213
+ "Search row": {"target": "multi-selector-search"},
214
+ "Search icon": {
215
+ "delegatesTo": {"owner": "component:Icon", "target": "icon"}
216
+ },
217
+ "Search clear button": {
218
+ "delegatesTo": {
219
+ "owner": "component:Field",
220
+ "target": "input-clear-button"
221
+ }
222
+ },
223
+ "Option row": {"target": "multi-selector-option"},
224
+ "Option checkbox indicator": {
225
+ "delegatesTo": {
226
+ "owner": "component:CheckboxInput",
227
+ "target": "checkbox-indicator"
228
+ }
229
+ },
230
+ "Option divider": {
231
+ "delegatesTo": {"owner": "component:Divider", "target": "divider"}
232
+ },
233
+ "Section heading": {"target": "multi-selector-section-heading"},
234
+ "Empty state": {"target": "multi-selector-empty-state"},
235
+ "Pointer popup": {"target": "multi-selector-popup"},
236
+ "Touch sheet heading": {
237
+ "delegatesTo": {"owner": "component:Text", "target": "heading"}
238
+ },
239
+ "Touch sheet": {
240
+ "delegatesTo": {
241
+ "owner": "component:BottomSheet",
242
+ "target": "bottom-sheet"
243
+ }
244
+ }
245
+ }
246
+ ```
247
+
248
+ ## Family and system relationships
249
+
250
+ - `architecture:public-component-api` and `spec:AST-011/DEC-1` own the additive
251
+ read-only API and its cross-input meaning.
252
+ - `architecture:component-theming-surface` owns anatomy qualification, local
253
+ target mapping, delegation, and exclusion of deprecated aliases.
254
+ - `architecture:layer-runtime` owns the current Popover host and the distinction
255
+ between browser popover and native-dialog sheet presentation.
256
+ - `family:overlay-dismissal` owns shared Escape and platform-close ordering. The
257
+ Pointer popup participates through its composed Popover owner; the Touch sheet
258
+ inherits BottomSheet's current local-only adoption gap.
259
+ - Field, CheckboxInput, Divider, Icon, Text, and BottomSheet retain ownership of
260
+ their delegated targets and presentation.
261
+
262
+ ## Verification map
263
+
264
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
265
+ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
266
+ | FR1, FR2 | `MultiSelector.test.tsx` render, search, divider, grouping, status, select-all, and empty-state suites plus InputGroup source inspection | Standalone/InputGroup, search, divided, selected, empty, status | Removing a documented part or misreporting Field presence conflicts with existing structure, content assertions, or source. | `audit:MultiSelector/anatomy` |
267
+ | FR3, FR6 | Component target suites, source inspection, and public target inventory | All seven current targets and deprecated alias | A current target is missed, invented, moved, or confused with the deprecated clear-icon alias. | `audit:MultiSelector/theming` |
268
+ | FR4 | Composed owner source/tests for Field, CheckboxInput, Divider, Icon, Text, and BottomSheet | Field omission/presence, clear actions, checkbox, divider, icons, heading, sheet | A shared part gains the wrong local owner or its documented owner no longer renders it. | `audit:MultiSelector/theming` |
269
+ | FR5 | `MultiSelector.test.tsx` presentation suites and `panelContent` source inspection | Explicit popover, explicit sheet, adaptive touch | A presentation stops using its owner or receives a divergent panel-content implementation. | `audit:MultiSelector/overlay` |
270
+ | FR7 | read-only interaction, form, ARIA, and theme-state tests | search/non-search, clearable, open→read-only, disabled precedence | A value changes, popup or edit affordance remains, form value disappears, or read-only semantics are absent. | `audit:MultiSelector/accessibility` |
271
+ | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and current local targets | Missing, extra, prefixed, stale, or multiply assigned mappings fail repository validation. | `audit:MultiSelector/theming` |
272
+
273
+ Existing component tests directly assert the Trigger, Indicator icon, Search row,
274
+ Option row, Section heading, Empty state, and Pointer popup targets. Presentation
275
+ tests directly distinguish the Popover and BottomSheet branches. Source inspection
276
+ distinguishes standalone Field presence from InputGroup omission and confirms that both surface branches receive the same `panelContent`, while the touch branch
277
+ adds Heading; Divider, Text, BottomSheet, and other shared primitive tests provide
278
+ owner evidence rather than new MultiSelector targets.
279
+
280
+ ## Decision log
281
+
282
+ None. This draft records current facts and introduces no component-local design,
283
+ API, theming, or layer-system decision.
284
+
285
+ ## Open questions
286
+
287
+ None.
288
+
289
+ ## Content boundary
290
+
291
+ This file does not duplicate consumer prop tables, examples, search or selection
292
+ algorithms, implementation steps, or shared layer and theming rules. It links to
293
+ their owners.
@@ -1140,7 +1140,9 @@ describe('MultiSelector', () => {
1140
1140
  />,
1141
1141
  );
1142
1142
 
1143
- await user.click(screen.getByRole('combobox', {name: 'Fruit'}));
1143
+ const trigger = screen.getByRole('combobox', {name: 'Fruit'});
1144
+ await user.click(trigger);
1145
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
1144
1146
 
1145
1147
  // useAnnounce writes on the next animation frame, so an immediate read
1146
1148
  // would pass whether or not anything was announced.
@@ -1395,7 +1397,7 @@ describe('MultiSelector', () => {
1395
1397
  expect(group).toHaveAttribute('aria-label', 'Citrus');
1396
1398
  });
1397
1399
 
1398
- it('shows loading state with aria-busy', () => {
1400
+ it('shows loading state with a spinner and aria-busy', () => {
1399
1401
  render(
1400
1402
  <MultiSelector
1401
1403
  label="Fruit"
@@ -1406,6 +1408,7 @@ describe('MultiSelector', () => {
1406
1408
  />,
1407
1409
  );
1408
1410
  expect(screen.getByRole('combobox')).toHaveAttribute('aria-busy', 'true');
1411
+ expect(screen.getByRole('status', {name: 'Loading'})).toBeInTheDocument();
1409
1412
  });
1410
1413
 
1411
1414
  it('renders with custom selectAllLabel', async () => {
@@ -1885,6 +1888,306 @@ describe('MultiSelector', () => {
1885
1888
  expect(trigger).toHaveAttribute('tabIndex', '-1');
1886
1889
  });
1887
1890
  });
1891
+ describe('isReadOnly', () => {
1892
+ const readOnlyCases = [
1893
+ {presentation: 'popover', hasSearch: false},
1894
+ {presentation: 'popover', hasSearch: true},
1895
+ {presentation: 'bottom-sheet', hasSearch: false},
1896
+ {presentation: 'bottom-sheet', hasSearch: true},
1897
+ {presentation: 'adaptive', hasSearch: false},
1898
+ {presentation: 'adaptive', hasSearch: true},
1899
+ ] as const;
1900
+
1901
+ const sheetTransitionCases = [
1902
+ {presentation: 'bottom-sheet', hasSearch: false},
1903
+ {presentation: 'bottom-sheet', hasSearch: true},
1904
+ {presentation: 'adaptive', hasSearch: false},
1905
+ {presentation: 'adaptive', hasSearch: true},
1906
+ ] as const;
1907
+
1908
+ const disabledPrecedenceCases = (
1909
+ ['popover', 'bottom-sheet', 'adaptive'] as const
1910
+ ).flatMap(presentation =>
1911
+ [false, true].flatMap(hasSearch =>
1912
+ [false, true].map(hasDisabledMessage => ({
1913
+ presentation,
1914
+ hasSearch,
1915
+ hasDisabledMessage,
1916
+ })),
1917
+ ),
1918
+ );
1919
+
1920
+ function stubCompactTouch(presentation: 'bottom-sheet' | 'adaptive') {
1921
+ if (presentation !== 'adaptive') {
1922
+ return;
1923
+ }
1924
+ vi.mocked(matchMedia).mockImplementation(
1925
+ (query: string) =>
1926
+ ({
1927
+ matches: query === '(max-width: 768px) and (pointer: coarse)',
1928
+ media: query,
1929
+ onchange: null,
1930
+ addEventListener: vi.fn(),
1931
+ removeEventListener: vi.fn(),
1932
+ addListener: vi.fn(),
1933
+ removeListener: vi.fn(),
1934
+ dispatchEvent: vi.fn(),
1935
+ }) as MediaQueryList,
1936
+ );
1937
+ }
1938
+
1939
+ it.each(readOnlyCases)(
1940
+ 'uses a read-only combobox with no rendered popup ($presentation, search=$hasSearch)',
1941
+ async ({presentation, hasSearch}) => {
1942
+ const user = userEvent.setup();
1943
+ const {container} = render(
1944
+ <form>
1945
+ <MultiSelector
1946
+ label="Fruit"
1947
+ htmlName="fruit"
1948
+ options={defaultOptions}
1949
+ value={['Apple', 'Banana']}
1950
+ onChange={() => {}}
1951
+ hasClear
1952
+ hasSearch={hasSearch}
1953
+ presentation={presentation}
1954
+ isDefaultOpen
1955
+ triggerDisplay="labels"
1956
+ isReadOnly
1957
+ />
1958
+ </form>,
1959
+ );
1960
+
1961
+ const trigger = screen.getByRole('combobox', {name: 'Fruit'});
1962
+ expect(trigger.tagName).toBe('BUTTON');
1963
+ expect(trigger).toHaveTextContent('Apple, Banana');
1964
+ expect(trigger).toHaveAttribute('aria-readonly', 'true');
1965
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
1966
+ const readOnlyDescription = screen.getByText('Read only');
1967
+ expect(trigger.getAttribute('aria-describedby')).toContain(
1968
+ readOnlyDescription.id,
1969
+ );
1970
+ expect(trigger).not.toHaveAttribute('aria-controls');
1971
+ expect(trigger).not.toHaveAttribute('aria-haspopup');
1972
+ expect(screen.getAllByRole('combobox', h)).toHaveLength(1);
1973
+ expect(screen.queryByRole('listbox', h)).not.toBeInTheDocument();
1974
+ expect(screen.queryByRole('dialog', h)).not.toBeInTheDocument();
1975
+ expect(
1976
+ screen.queryByRole('button', {name: 'Clear all Fruit'}),
1977
+ ).not.toBeInTheDocument();
1978
+ expect(
1979
+ container.querySelector('.astryx-multi-selector-indicator-icon'),
1980
+ ).toBeNull();
1981
+ expect(
1982
+ container.querySelector('.astryx-multi-selector'),
1983
+ ).toHaveAttribute('data-readonly', 'readonly');
1984
+ expect(
1985
+ new FormData(container.querySelector('form')!).getAll('fruit'),
1986
+ ).toEqual(['Apple', 'Banana']);
1987
+
1988
+ await user.tab();
1989
+ expect(trigger).toHaveFocus();
1990
+ },
1991
+ );
1992
+
1993
+ it('keeps loading feedback while blocking the selection surface', async () => {
1994
+ const user = userEvent.setup();
1995
+ render(
1996
+ <MultiSelector
1997
+ label="Fruit"
1998
+ options={defaultOptions}
1999
+ value={['Apple']}
2000
+ onChange={() => {}}
2001
+ isLoading
2002
+ isReadOnly
2003
+ />,
2004
+ );
2005
+
2006
+ const trigger = screen.getByRole('combobox', {name: 'Fruit'});
2007
+ expect(trigger).toHaveAttribute('aria-busy', 'true');
2008
+ expect(screen.getByRole('status', {name: 'Loading'})).toBeInTheDocument();
2009
+
2010
+ await user.click(trigger);
2011
+ expect(screen.queryByRole('listbox', h)).not.toBeInTheDocument();
2012
+ });
2013
+
2014
+ it('blocks pointer, keyboard, and typeahead changes', async () => {
2015
+ const user = userEvent.setup();
2016
+ const onChange = vi.fn();
2017
+ const changeAction = vi.fn();
2018
+ render(
2019
+ <MultiSelector
2020
+ label="Fruit"
2021
+ options={defaultOptions}
2022
+ value={['Apple']}
2023
+ onChange={onChange}
2024
+ changeAction={changeAction}
2025
+ isReadOnly
2026
+ />,
2027
+ );
2028
+
2029
+ const trigger = screen.getByRole('combobox');
2030
+ await user.click(trigger);
2031
+ trigger.focus();
2032
+ await user.keyboard('{Enter}{ArrowDown}b{Backspace}');
2033
+
2034
+ expect(screen.queryByRole('listbox', h)).not.toBeInTheDocument();
2035
+ expect(onChange).not.toHaveBeenCalled();
2036
+ expect(changeAction).not.toHaveBeenCalled();
2037
+ });
2038
+
2039
+ it.each([false, true])(
2040
+ 'restores popover focus when an open selector becomes read-only (search=%s)',
2041
+ async hasSearch => {
2042
+ const user = userEvent.setup();
2043
+ const selector = (isReadOnly: boolean) => (
2044
+ <MultiSelector
2045
+ label="Fruit"
2046
+ options={defaultOptions}
2047
+ value={['Apple']}
2048
+ onChange={() => {}}
2049
+ hasSearch={hasSearch}
2050
+ isReadOnly={isReadOnly}
2051
+ />
2052
+ );
2053
+ const {rerender} = render(selector(false));
2054
+ const editableTrigger = screen.getByRole(
2055
+ hasSearch ? 'button' : 'combobox',
2056
+ {name: 'Fruit'},
2057
+ );
2058
+ await user.click(editableTrigger);
2059
+ if (hasSearch) {
2060
+ await waitFor(() =>
2061
+ expect(screen.getByRole('combobox', h)).toHaveFocus(),
2062
+ );
2063
+ }
2064
+
2065
+ rerender(selector(true));
2066
+
2067
+ const readOnlyTrigger = screen.getByRole('combobox', {name: 'Fruit'});
2068
+ await waitFor(() => expect(readOnlyTrigger).toHaveFocus());
2069
+ expect(screen.queryByRole('listbox', h)).not.toBeInTheDocument();
2070
+ },
2071
+ );
2072
+
2073
+ it.each(sheetTransitionCases)(
2074
+ 'finishes $presentation close and restores focus when read-only changes (search=$hasSearch)',
2075
+ async ({presentation, hasSearch}) => {
2076
+ stubCompactTouch(presentation);
2077
+ const user = userEvent.setup();
2078
+ const selector = (isReadOnly: boolean) => (
2079
+ <MultiSelector
2080
+ label="Fruit"
2081
+ options={defaultOptions}
2082
+ value={['Apple']}
2083
+ onChange={() => {}}
2084
+ hasSearch={hasSearch}
2085
+ presentation={presentation}
2086
+ isReadOnly={isReadOnly}
2087
+ />
2088
+ );
2089
+ const {rerender} = render(selector(false));
2090
+ const editableTrigger = screen.getByRole(
2091
+ hasSearch ? 'button' : 'combobox',
2092
+ {name: 'Fruit'},
2093
+ );
2094
+ await user.click(editableTrigger);
2095
+ const dialog = await screen.findByRole('dialog', {name: 'Fruit'});
2096
+ const panel = dialog.querySelector<HTMLElement>('.astryx-bottom-sheet');
2097
+ expect(panel).not.toBeNull();
2098
+ await waitFor(() =>
2099
+ expect(
2100
+ hasSearch
2101
+ ? screen.getByRole('combobox', h)
2102
+ : screen.getByRole('listbox'),
2103
+ ).toHaveFocus(),
2104
+ );
2105
+
2106
+ rerender(selector(true));
2107
+
2108
+ await waitFor(() => expect(dialog).toHaveAttribute('inert'));
2109
+ fireEvent.transitionEnd(panel!, {propertyName: 'transform'});
2110
+ const readOnlyTrigger = screen.getByRole('combobox', {name: 'Fruit'});
2111
+ await waitFor(() => expect(readOnlyTrigger).toHaveFocus());
2112
+ expect(screen.queryByRole('dialog', h)).not.toBeInTheDocument();
2113
+ expect(screen.queryByRole('listbox', h)).not.toBeInTheDocument();
2114
+ },
2115
+ );
2116
+
2117
+ it.each(disabledPrecedenceCases)(
2118
+ 'matches disabled-only semantics for $presentation (search=$hasSearch, reason=$hasDisabledMessage)',
2119
+ async ({presentation, hasSearch, hasDisabledMessage}) => {
2120
+ if (presentation === 'adaptive') {
2121
+ stubCompactTouch(presentation);
2122
+ }
2123
+ const user = userEvent.setup();
2124
+ const onChange = vi.fn();
2125
+ const selector = (isReadOnly: boolean) => (
2126
+ <form>
2127
+ <MultiSelector
2128
+ label="Fruit"
2129
+ htmlName="fruit"
2130
+ options={defaultOptions}
2131
+ value={['Apple', 'Banana']}
2132
+ onChange={onChange}
2133
+ hasClear
2134
+ hasSearch={hasSearch}
2135
+ presentation={presentation}
2136
+ isDisabled
2137
+ isReadOnly={isReadOnly}
2138
+ disabledMessage={
2139
+ hasDisabledMessage ? 'Locked by policy' : undefined
2140
+ }
2141
+ />
2142
+ </form>
2143
+ );
2144
+ const {container, rerender} = render(selector(false));
2145
+ const getTrigger = () =>
2146
+ screen.getByRole(hasSearch ? 'button' : 'combobox', {name: 'Fruit'});
2147
+ const snapshot = () => {
2148
+ const trigger = getTrigger();
2149
+ const root = container.querySelector('.astryx-multi-selector');
2150
+ return {
2151
+ tagName: trigger.tagName,
2152
+ role: trigger.getAttribute('role'),
2153
+ hasPopup: trigger.getAttribute('aria-haspopup'),
2154
+ expanded: trigger.getAttribute('aria-expanded'),
2155
+ controls: trigger.getAttribute('aria-controls'),
2156
+ readOnly: trigger.getAttribute('aria-readonly'),
2157
+ disabled: trigger.matches(':disabled'),
2158
+ ariaDisabled: trigger.getAttribute('aria-disabled'),
2159
+ tabIndex: trigger.tabIndex,
2160
+ rootClassName: root?.className,
2161
+ rootDisabled: root?.getAttribute('data-disabled'),
2162
+ rootReadOnly: root?.getAttribute('data-readonly'),
2163
+ hasIndicator:
2164
+ root?.querySelector('.astryx-multi-selector-indicator-icon') !=
2165
+ null,
2166
+ listboxes: screen.queryAllByRole('listbox', h).length,
2167
+ dialogs: screen.queryAllByRole('dialog', h).length,
2168
+ formEntries: [
2169
+ ...new FormData(container.querySelector('form')!).entries(),
2170
+ ],
2171
+ };
2172
+ };
2173
+ const disabledOnly = snapshot();
2174
+
2175
+ rerender(selector(true));
2176
+
2177
+ expect(snapshot()).toEqual(disabledOnly);
2178
+ expect(disabledOnly.rootDisabled).toBe('disabled');
2179
+ expect(disabledOnly.rootReadOnly).toBeNull();
2180
+ expect(disabledOnly.readOnly).toBeNull();
2181
+ expect(disabledOnly.formEntries).toEqual([]);
2182
+ const trigger = getTrigger();
2183
+ await user.click(trigger);
2184
+ trigger.focus();
2185
+ await user.keyboard('{Enter}{ArrowDown}b{Backspace}');
2186
+ expect(onChange).not.toHaveBeenCalled();
2187
+ },
2188
+ );
2189
+ });
2190
+
1888
2191
  describe('form participation', () => {
1889
2192
  it('submits one entry per selected value under htmlName', () => {
1890
2193
  const {container} = render(