@astryxdesign/core 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (551) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/dist/AlertDialog/AlertDialog.d.ts +10 -2
  3. package/dist/AlertDialog/AlertDialog.d.ts.map +1 -1
  4. package/dist/AlertDialog/AlertDialog.js +22 -3
  5. package/dist/AppShell/AppShell.d.ts +2 -2
  6. package/dist/AppShell/AppShell.d.ts.map +1 -1
  7. package/dist/AppShell/AppShell.js +40 -9
  8. package/dist/AspectRatio/AspectRatio.d.ts +13 -0
  9. package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
  10. package/dist/AspectRatio/AspectRatio.js +9 -0
  11. package/dist/Avatar/Avatar.d.ts.map +1 -1
  12. package/dist/Avatar/Avatar.js +33 -20
  13. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
  14. package/dist/AvatarGroup/AvatarGroupOverflow.js +2 -3
  15. package/dist/Banner/Banner.d.ts.map +1 -1
  16. package/dist/Banner/Banner.js +15 -14
  17. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  18. package/dist/Breadcrumbs/BreadcrumbItem.js +21 -8
  19. package/dist/Breadcrumbs/index.d.ts +2 -1
  20. package/dist/Breadcrumbs/index.d.ts.map +1 -1
  21. package/dist/Breadcrumbs/index.js +1 -0
  22. package/dist/Button/Button.d.ts.map +1 -1
  23. package/dist/Button/Button.js +11 -15
  24. package/dist/Calendar/Calendar.d.ts.map +1 -1
  25. package/dist/Calendar/Calendar.js +2 -1
  26. package/dist/Calendar/styles.d.ts +0 -2
  27. package/dist/Calendar/styles.d.ts.map +1 -1
  28. package/dist/Calendar/styles.js +0 -2
  29. package/dist/Carousel/Carousel.d.ts.map +1 -1
  30. package/dist/Carousel/Carousel.js +8 -12
  31. package/dist/Chat/ChatDictationButton.d.ts.map +1 -1
  32. package/dist/Chat/ChatDictationButton.js +2 -1
  33. package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
  34. package/dist/Chat/ChatLayoutScrollButton.js +3 -1
  35. package/dist/Chat/ChatSendButton.d.ts.map +1 -1
  36. package/dist/Chat/ChatSendButton.js +4 -3
  37. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  38. package/dist/Chat/ChatToolCalls.js +17 -17
  39. package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
  40. package/dist/Chat/useTriggerMenu.js +2 -6
  41. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  42. package/dist/CheckboxInput/CheckboxInput.js +36 -125
  43. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  44. package/dist/ClickableCard/ClickableCard.js +3 -11
  45. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  46. package/dist/CodeBlock/CodeBlock.js +84 -67
  47. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  48. package/dist/Collapsible/Collapsible.js +35 -15
  49. package/dist/CommandPalette/CommandPaletteItem.d.ts +1 -1
  50. package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
  51. package/dist/CommandPalette/CommandPaletteItem.js +6 -3
  52. package/dist/CommandPalette/CommandPaletteList.js +1 -1
  53. package/dist/ComplexSelector/ComplexSelector.d.ts +1 -1
  54. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  55. package/dist/ComplexSelector/ComplexSelector.js +35 -33
  56. package/dist/ContextMenu/ContextMenu.d.ts +2 -2
  57. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  58. package/dist/ContextMenu/ContextMenu.js +1 -1
  59. package/dist/ContextMenu/index.d.ts +2 -1
  60. package/dist/ContextMenu/index.d.ts.map +1 -1
  61. package/dist/ContextMenu/index.js +1 -0
  62. package/dist/DateInput/DateInput.d.ts +9 -2
  63. package/dist/DateInput/DateInput.d.ts.map +1 -1
  64. package/dist/DateInput/DateInput.js +24 -23
  65. package/dist/DateRangeInput/DateRangeInput.d.ts +9 -2
  66. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  67. package/dist/DateRangeInput/DateRangeInput.js +11 -16
  68. package/dist/DateTimeInput/DateTimeInput.d.ts +9 -1
  69. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  70. package/dist/DateTimeInput/DateTimeInput.js +22 -17
  71. package/dist/Dialog/Dialog.d.ts.map +1 -1
  72. package/dist/Dialog/Dialog.js +4 -4
  73. package/dist/DropdownMenu/DropdownMenu.d.ts +33 -8
  74. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  75. package/dist/DropdownMenu/DropdownMenu.js +62 -20
  76. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +8 -11
  77. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
  78. package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +32 -31
  79. package/dist/DropdownMenu/DropdownMenuDivider.d.ts +25 -0
  80. package/dist/DropdownMenu/DropdownMenuDivider.d.ts.map +1 -0
  81. package/dist/DropdownMenu/DropdownMenuDivider.js +66 -0
  82. package/dist/DropdownMenu/DropdownMenuItem.d.ts +15 -2
  83. package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
  84. package/dist/DropdownMenu/DropdownMenuItem.js +20 -8
  85. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +5 -5
  86. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +1 -1
  87. package/dist/DropdownMenu/DropdownMenuRadioItem.js +24 -67
  88. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  89. package/dist/DropdownMenu/DropdownMenuSubMenu.js +2 -8
  90. package/dist/DropdownMenu/index.d.ts +2 -1
  91. package/dist/DropdownMenu/index.d.ts.map +1 -1
  92. package/dist/DropdownMenu/index.js +4 -0
  93. package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -1
  94. package/dist/DropdownMenu/renderDropdownItems.js +35 -27
  95. package/dist/EmptyState/EmptyState.d.ts.map +1 -1
  96. package/dist/EmptyState/EmptyState.js +9 -5
  97. package/dist/Field/InputClearButton.d.ts +16 -3
  98. package/dist/Field/InputClearButton.d.ts.map +1 -1
  99. package/dist/Field/InputClearButton.js +15 -4
  100. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  101. package/dist/HoverCard/useHoverCard.js +3 -19
  102. package/dist/Indicator/CheckIndicator.d.ts +25 -0
  103. package/dist/Indicator/CheckIndicator.d.ts.map +1 -0
  104. package/dist/Indicator/CheckIndicator.js +155 -0
  105. package/dist/Indicator/CheckboxIndicator.d.ts +22 -0
  106. package/dist/Indicator/CheckboxIndicator.d.ts.map +1 -0
  107. package/dist/Indicator/CheckboxIndicator.js +173 -0
  108. package/dist/Indicator/RadioIndicator.d.ts +23 -0
  109. package/dist/Indicator/RadioIndicator.d.ts.map +1 -0
  110. package/dist/Indicator/RadioIndicator.js +137 -0
  111. package/dist/Indicator/index.d.ts +24 -0
  112. package/dist/Indicator/index.d.ts.map +1 -0
  113. package/dist/Indicator/index.js +26 -0
  114. package/dist/Indicator/indicator.markers.stylex.d.ts +16 -0
  115. package/dist/Indicator/indicator.markers.stylex.d.ts.map +1 -0
  116. package/dist/Indicator/indicator.markers.stylex.js +21 -0
  117. package/dist/Indicator/indicatorRegistry.d.ts +71 -0
  118. package/dist/Indicator/indicatorRegistry.d.ts.map +1 -0
  119. package/dist/Indicator/indicatorRegistry.js +93 -0
  120. package/dist/Indicator/types.d.ts +167 -0
  121. package/dist/Indicator/types.d.ts.map +1 -0
  122. package/dist/Indicator/types.js +1 -0
  123. package/dist/Indicator/useIndicator.d.ts +18 -0
  124. package/dist/Indicator/useIndicator.d.ts.map +1 -0
  125. package/dist/Indicator/useIndicator.js +30 -0
  126. package/dist/Item/Item.d.ts.map +1 -1
  127. package/dist/Item/Item.js +4 -8
  128. package/dist/Layer/useLayer.d.ts +14 -0
  129. package/dist/Layer/useLayer.d.ts.map +1 -1
  130. package/dist/Layer/useLayer.js +25 -2
  131. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  132. package/dist/Lightbox/Lightbox.js +56 -53
  133. package/dist/Link/Link.d.ts.map +1 -1
  134. package/dist/Link/Link.js +4 -5
  135. package/dist/MobileNav/MobileNav.js +8 -8
  136. package/dist/MoreMenu/MoreMenu.d.ts +14 -1
  137. package/dist/MoreMenu/MoreMenu.d.ts.map +1 -1
  138. package/dist/MoreMenu/MoreMenu.js +4 -0
  139. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  140. package/dist/MultiSelector/MultiSelector.js +78 -75
  141. package/dist/NumberInput/NumberInput.d.ts +27 -1
  142. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  143. package/dist/NumberInput/NumberInput.js +230 -42
  144. package/dist/Outline/Outline.d.ts.map +1 -1
  145. package/dist/Outline/Outline.js +2 -3
  146. package/dist/Pagination/Pagination.d.ts.map +1 -1
  147. package/dist/Pagination/Pagination.js +61 -63
  148. package/dist/Popover/Popover.d.ts.map +1 -1
  149. package/dist/Popover/Popover.js +6 -8
  150. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  151. package/dist/PowerSearch/PowerSearch.js +1 -1
  152. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  153. package/dist/PowerSearch/PowerSearchEditPopover.js +7 -0
  154. package/dist/ProgressBar/ProgressBar.d.ts +11 -4
  155. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  156. package/dist/ProgressBar/ProgressBar.js +96 -6
  157. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  158. package/dist/RadioList/RadioListItem.js +29 -84
  159. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  160. package/dist/Resizable/ResizeHandle.js +2 -3
  161. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  162. package/dist/SegmentedControl/SegmentedControlItem.js +2 -3
  163. package/dist/SelectableCard/SelectableCard.d.ts.map +1 -1
  164. package/dist/SelectableCard/SelectableCard.js +3 -11
  165. package/dist/Selector/Selector.d.ts +2 -1
  166. package/dist/Selector/Selector.d.ts.map +1 -1
  167. package/dist/Selector/Selector.js +164 -94
  168. package/dist/Selector/hooks.d.ts +18 -6
  169. package/dist/Selector/hooks.d.ts.map +1 -1
  170. package/dist/Selector/hooks.js +57 -40
  171. package/dist/SideNav/SideNav.d.ts.map +1 -1
  172. package/dist/SideNav/SideNav.js +1 -1
  173. package/dist/SideNav/SideNavCollapseButton.d.ts +1 -1
  174. package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
  175. package/dist/SideNav/SideNavCollapseButton.js +29 -14
  176. package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
  177. package/dist/SideNav/SideNavHeading.js +56 -20
  178. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  179. package/dist/SideNav/SideNavItem.js +28 -22
  180. package/dist/Stack/stack.stylex.d.ts +3 -3
  181. package/dist/Switch/Switch.js +11 -10
  182. package/dist/TabList/Tab.d.ts.map +1 -1
  183. package/dist/TabList/Tab.js +2 -3
  184. package/dist/TabList/TabMenu.d.ts.map +1 -1
  185. package/dist/TabList/TabMenu.js +41 -19
  186. package/dist/Table/BaseTable.d.ts.map +1 -1
  187. package/dist/Table/BaseTable.js +8 -0
  188. package/dist/Table/index.d.ts +1 -1
  189. package/dist/Table/index.d.ts.map +1 -1
  190. package/dist/Table/index.js +1 -1
  191. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts +1 -1
  192. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
  193. package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +22 -18
  194. package/dist/Table/plugins/rowExpansion/index.d.ts +1 -1
  195. package/dist/Table/plugins/rowExpansion/index.d.ts.map +1 -1
  196. package/dist/Table/plugins/rowExpansion/index.js +1 -1
  197. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts +41 -76
  198. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  199. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +94 -345
  200. package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
  201. package/dist/Table/plugins/sortable/useTableSortable.js +23 -4
  202. package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -1
  203. package/dist/Table/plugins/tree/useTableTreeData.js +29 -33
  204. package/dist/Table/tableContextMenu.d.ts.map +1 -1
  205. package/dist/Table/tableContextMenu.js +2 -1
  206. package/dist/Table/types.d.ts +11 -0
  207. package/dist/Table/types.d.ts.map +1 -1
  208. package/dist/TextArea/TextArea.d.ts +10 -1
  209. package/dist/TextArea/TextArea.d.ts.map +1 -1
  210. package/dist/TextArea/TextArea.js +16 -6
  211. package/dist/TextInput/TextInput.d.ts +10 -1
  212. package/dist/TextInput/TextInput.d.ts.map +1 -1
  213. package/dist/TextInput/TextInput.js +13 -19
  214. package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
  215. package/dist/Thumbnail/Thumbnail.js +32 -26
  216. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  217. package/dist/TimeInput/TimeInput.js +7 -15
  218. package/dist/Timestamp/TimestampHoverCard.d.ts.map +1 -1
  219. package/dist/Timestamp/TimestampHoverCard.js +16 -34
  220. package/dist/Token/Token.d.ts.map +1 -1
  221. package/dist/Token/Token.js +20 -11
  222. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  223. package/dist/Tokenizer/Tokenizer.js +4 -2
  224. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  225. package/dist/Tooltip/useTooltip.js +3 -19
  226. package/dist/TopNav/TopNav.d.ts.map +1 -1
  227. package/dist/TopNav/TopNav.js +6 -5
  228. package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
  229. package/dist/TopNav/TopNavHeading.js +54 -16
  230. package/dist/TopNav/TopNavItem.d.ts.map +1 -1
  231. package/dist/TopNav/TopNavItem.js +3 -9
  232. package/dist/TopNav/TopNavMegaMenu.d.ts +7 -0
  233. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  234. package/dist/TopNav/TopNavMegaMenu.js +146 -60
  235. package/dist/TopNav/TopNavMegaMenuItem.d.ts.map +1 -1
  236. package/dist/TopNav/TopNavMegaMenuItem.js +22 -3
  237. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  238. package/dist/TopNav/TopNavMenu.js +83 -38
  239. package/dist/TreeList/TreeList.d.ts.map +1 -1
  240. package/dist/TreeList/TreeList.js +13 -0
  241. package/dist/TreeList/TreeListBranches.d.ts +1 -1
  242. package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
  243. package/dist/TreeList/TreeListBranches.js +14 -4
  244. package/dist/TreeList/TreeListItem.d.ts +11 -1
  245. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  246. package/dist/TreeList/TreeListItem.js +57 -30
  247. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  248. package/dist/Typeahead/BaseTypeahead.js +16 -8
  249. package/dist/astryx.css +74 -85
  250. package/dist/astryx.umd.js +56 -56
  251. package/dist/astryx.umd.js.map +4 -4
  252. package/dist/hooks/containerReveal.stylex.d.ts +61 -0
  253. package/dist/hooks/containerReveal.stylex.d.ts.map +1 -0
  254. package/dist/hooks/containerReveal.stylex.js +63 -0
  255. package/dist/hooks/index.d.ts +3 -0
  256. package/dist/hooks/index.d.ts.map +1 -1
  257. package/dist/hooks/index.js +2 -0
  258. package/dist/hooks/useClipboard.d.ts +59 -0
  259. package/dist/hooks/useClipboard.d.ts.map +1 -0
  260. package/dist/hooks/useClipboard.js +106 -0
  261. package/dist/hooks/useContainerReveal.d.ts +14 -8
  262. package/dist/hooks/useContainerReveal.d.ts.map +1 -1
  263. package/dist/hooks/useContainerReveal.js +28 -69
  264. package/dist/hooks/useIndicatorFocusRing.d.ts +28 -0
  265. package/dist/hooks/useIndicatorFocusRing.d.ts.map +1 -0
  266. package/dist/hooks/useIndicatorFocusRing.js +96 -0
  267. package/dist/hooks/useLongPress.d.ts.map +1 -1
  268. package/dist/hooks/useLongPress.js +12 -2
  269. package/dist/hooks/useScrollLock.d.ts.map +1 -1
  270. package/dist/hooks/useScrollLock.js +33 -18
  271. package/dist/hooks/useStreamingText.d.ts +18 -3
  272. package/dist/hooks/useStreamingText.d.ts.map +1 -1
  273. package/dist/hooks/useStreamingText.js +47 -7
  274. package/dist/hooks/useTreeFocus.d.ts.map +1 -1
  275. package/dist/hooks/useTreeFocus.js +9 -3
  276. package/dist/hooks/useTypeahead.d.ts +3 -2
  277. package/dist/hooks/useTypeahead.d.ts.map +1 -1
  278. package/dist/hooks/useTypeahead.js +19 -7
  279. package/dist/index.d.ts +1 -0
  280. package/dist/index.d.ts.map +1 -1
  281. package/dist/index.js +1 -0
  282. package/dist/theme/defineTheme.d.ts +16 -0
  283. package/dist/theme/defineTheme.d.ts.map +1 -1
  284. package/dist/theme/defineTheme.js +8 -0
  285. package/dist/theme/derivedVarRegistry.d.ts +8 -0
  286. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  287. package/dist/theme/derivedVarRegistry.js +14 -13
  288. package/dist/theme/expandColorScale.d.ts +3 -1
  289. package/dist/theme/expandColorScale.d.ts.map +1 -1
  290. package/dist/theme/expandColorScale.js +17 -7
  291. package/dist/theme/expandTypeScale.d.ts.map +1 -1
  292. package/dist/theme/expandTypeScale.js +17 -0
  293. package/dist/theme/generateThemeRules.d.ts.map +1 -1
  294. package/dist/theme/generateThemeRules.js +11 -0
  295. package/dist/theme/index.d.ts +1 -0
  296. package/dist/theme/index.d.ts.map +1 -1
  297. package/dist/utils/focusOutline.stylex.d.ts +107 -0
  298. package/dist/utils/focusOutline.stylex.d.ts.map +1 -0
  299. package/dist/utils/focusOutline.stylex.js +130 -0
  300. package/dist/utils/focusReturn.d.ts +24 -0
  301. package/dist/utils/focusReturn.d.ts.map +1 -0
  302. package/dist/utils/focusReturn.js +29 -0
  303. package/dist/utils/index.d.ts +1 -0
  304. package/dist/utils/index.d.ts.map +1 -1
  305. package/dist/utils/index.js +1 -0
  306. package/dist/utils/mergeRefs.d.ts.map +1 -1
  307. package/dist/utils/mergeRefs.js +7 -5
  308. package/dist/utils/rtlStyles.d.ts +2 -2
  309. package/dist/utils/rtlStyles.js +2 -2
  310. package/dist/utils/themeProps.d.ts +20 -1
  311. package/dist/utils/themeProps.d.ts.map +1 -1
  312. package/dist/utils/themeProps.js +8 -2
  313. package/locales/en.json +12 -0
  314. package/locales/pseudo.json +9 -0
  315. package/package.json +7 -2
  316. package/src/AlertDialog/AlertDialog.doc.mjs +16 -3
  317. package/src/AlertDialog/AlertDialog.test.tsx +208 -1
  318. package/src/AlertDialog/AlertDialog.tsx +21 -3
  319. package/src/AppShell/AppShell.doc.mjs +11 -0
  320. package/src/AppShell/AppShell.test.tsx +139 -0
  321. package/src/AppShell/AppShell.tsx +32 -22
  322. package/src/AspectRatio/AspectRatio.doc.mjs +27 -3
  323. package/src/AspectRatio/AspectRatio.test.tsx +30 -0
  324. package/src/AspectRatio/AspectRatio.tsx +13 -0
  325. package/src/Avatar/Avatar.doc.mjs +2 -13
  326. package/src/Avatar/Avatar.test.tsx +33 -8
  327. package/src/Avatar/Avatar.tsx +52 -50
  328. package/src/AvatarGroup/AvatarGroupOverflow.tsx +2 -9
  329. package/src/Banner/Banner.tsx +9 -6
  330. package/src/Breadcrumbs/BreadcrumbItem.tsx +16 -6
  331. package/src/Breadcrumbs/index.ts +5 -1
  332. package/src/Button/Button.tsx +21 -41
  333. package/src/Calendar/Calendar.tsx +2 -1
  334. package/src/Calendar/styles.ts +0 -8
  335. package/src/Carousel/Carousel.doc.mjs +3 -0
  336. package/src/Carousel/Carousel.tsx +10 -6
  337. package/src/Chat/Chat.doc.mjs +2 -0
  338. package/src/Chat/ChatDictationButton.tsx +7 -1
  339. package/src/Chat/ChatLayoutScrollButton.test.tsx +22 -0
  340. package/src/Chat/ChatLayoutScrollButton.tsx +8 -1
  341. package/src/Chat/ChatSendButton.tsx +2 -3
  342. package/src/Chat/ChatToolCalls.test.tsx +35 -0
  343. package/src/Chat/ChatToolCalls.tsx +36 -16
  344. package/src/Chat/useTriggerMenu.tsx +2 -5
  345. package/src/CheckboxInput/CheckboxInput.doc.mjs +4 -2
  346. package/src/CheckboxInput/CheckboxInput.test.tsx +115 -0
  347. package/src/CheckboxInput/CheckboxInput.tsx +45 -202
  348. package/src/ClickableCard/ClickableCard.tsx +6 -11
  349. package/src/CodeBlock/CodeBlock.doc.mjs +3 -0
  350. package/src/CodeBlock/CodeBlock.test.tsx +91 -0
  351. package/src/CodeBlock/CodeBlock.tsx +57 -80
  352. package/src/Collapsible/Collapsible.tsx +22 -18
  353. package/src/CommandPalette/CommandPalette.test.tsx +29 -0
  354. package/src/CommandPalette/CommandPaletteItem.tsx +7 -4
  355. package/src/CommandPalette/CommandPaletteList.tsx +2 -2
  356. package/src/ComplexSelector/ComplexSelector.doc.mjs +42 -0
  357. package/src/ComplexSelector/ComplexSelector.test.tsx +64 -0
  358. package/src/ComplexSelector/ComplexSelector.tsx +46 -29
  359. package/src/ContextMenu/ContextMenu.doc.mjs +1 -1
  360. package/src/ContextMenu/ContextMenu.test.tsx +41 -0
  361. package/src/ContextMenu/ContextMenu.tsx +3 -3
  362. package/src/ContextMenu/index.ts +5 -1
  363. package/src/DateInput/DateInput.doc.mjs +18 -3
  364. package/src/DateInput/DateInput.test.tsx +93 -11
  365. package/src/DateInput/DateInput.tsx +41 -19
  366. package/src/DateRangeInput/DateRangeInput.doc.mjs +10 -2
  367. package/src/DateRangeInput/DateRangeInput.test.tsx +110 -14
  368. package/src/DateRangeInput/DateRangeInput.tsx +24 -12
  369. package/src/DateTimeInput/DateTimeInput.doc.mjs +16 -2
  370. package/src/DateTimeInput/DateTimeInput.test.tsx +75 -0
  371. package/src/DateTimeInput/DateTimeInput.tsx +35 -8
  372. package/src/Dialog/Dialog.tsx +2 -9
  373. package/src/DropdownMenu/DropdownMenu.doc.mjs +5 -9
  374. package/src/DropdownMenu/DropdownMenu.test.tsx +534 -4
  375. package/src/DropdownMenu/DropdownMenu.tsx +113 -52
  376. package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +27 -30
  377. package/src/DropdownMenu/DropdownMenuDivider.doc.mjs +34 -0
  378. package/src/DropdownMenu/DropdownMenuDivider.tsx +81 -0
  379. package/src/DropdownMenu/DropdownMenuItem.doc.mjs +39 -4
  380. package/src/DropdownMenu/DropdownMenuItem.tsx +48 -10
  381. package/src/DropdownMenu/DropdownMenuRadioItem.tsx +30 -76
  382. package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +25 -19
  383. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +44 -14
  384. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +1 -5
  385. package/src/DropdownMenu/index.ts +8 -1
  386. package/src/DropdownMenu/renderDropdownItems.tsx +33 -34
  387. package/src/EmptyState/EmptyState.doc.mjs +4 -0
  388. package/src/EmptyState/EmptyState.test.tsx +62 -0
  389. package/src/EmptyState/EmptyState.tsx +16 -6
  390. package/src/Field/Field.doc.mjs +1 -0
  391. package/src/Field/InputClearButton.test.tsx +117 -0
  392. package/src/Field/InputClearButton.tsx +30 -3
  393. package/src/HoverCard/useHoverCard.tsx +2 -23
  394. package/src/Icon/Icon.doc.mjs +2 -1
  395. package/src/Indicator/CheckIndicator.tsx +174 -0
  396. package/src/Indicator/CheckboxIndicator.tsx +257 -0
  397. package/src/Indicator/Indicator.doc.mjs +344 -0
  398. package/src/Indicator/Indicator.test.tsx +393 -0
  399. package/src/Indicator/RadioIndicator.tsx +206 -0
  400. package/src/Indicator/index.ts +45 -0
  401. package/src/Indicator/indicator.markers.stylex.ts +19 -0
  402. package/src/Indicator/indicatorRegistry.test.tsx +106 -0
  403. package/src/Indicator/indicatorRegistry.ts +120 -0
  404. package/src/Indicator/types.ts +186 -0
  405. package/src/Indicator/useIndicator.ts +39 -0
  406. package/src/Item/Item.doc.mjs +1 -1
  407. package/src/Item/Item.tsx +7 -14
  408. package/src/Layer/useLayer.doc.mjs +2 -2
  409. package/src/Layer/useLayer.test.tsx +85 -0
  410. package/src/Layer/useLayer.tsx +39 -1
  411. package/src/Lightbox/Lightbox.doc.mjs +1 -1
  412. package/src/Lightbox/Lightbox.tsx +43 -38
  413. package/src/Link/Link.tsx +4 -11
  414. package/src/MobileNav/MobileNav.tsx +3 -3
  415. package/src/MoreMenu/MoreMenu.doc.mjs +31 -0
  416. package/src/MoreMenu/MoreMenu.test.tsx +68 -1
  417. package/src/MoreMenu/MoreMenu.tsx +19 -0
  418. package/src/MultiSelector/MultiSelector.doc.mjs +9 -1
  419. package/src/MultiSelector/MultiSelector.test.tsx +255 -16
  420. package/src/MultiSelector/MultiSelector.tsx +103 -99
  421. package/src/NumberInput/NumberInput.doc.mjs +265 -50
  422. package/src/NumberInput/NumberInput.test.tsx +726 -21
  423. package/src/NumberInput/NumberInput.tsx +353 -59
  424. package/src/Outline/Outline.tsx +2 -5
  425. package/src/Pagination/Pagination.doc.mjs +1 -1
  426. package/src/Pagination/Pagination.test.tsx +22 -7
  427. package/src/Pagination/Pagination.tsx +38 -35
  428. package/src/Popover/Popover.tsx +6 -7
  429. package/src/PowerSearch/PowerSearch.tsx +1 -1
  430. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +42 -0
  431. package/src/PowerSearch/PowerSearchEditPopover.tsx +7 -0
  432. package/src/ProgressBar/ProgressBar.doc.mjs +27 -5
  433. package/src/ProgressBar/ProgressBar.test.tsx +224 -1
  434. package/src/ProgressBar/ProgressBar.tsx +124 -22
  435. package/src/RadioList/RadioList.doc.mjs +4 -2
  436. package/src/RadioList/RadioListItem.tsx +31 -130
  437. package/src/Resizable/ResizeHandle.tsx +2 -9
  438. package/src/SegmentedControl/SegmentedControlItem.tsx +2 -9
  439. package/src/SelectableCard/SelectableCard.tsx +4 -9
  440. package/src/Selector/Selector.doc.mjs +7 -4
  441. package/src/Selector/Selector.test.tsx +935 -25
  442. package/src/Selector/Selector.tsx +189 -106
  443. package/src/Selector/hooks.ts +77 -50
  444. package/src/SideNav/SideNav.test.tsx +46 -0
  445. package/src/SideNav/SideNav.tsx +1 -0
  446. package/src/SideNav/SideNavCollapseButton.tsx +15 -10
  447. package/src/SideNav/SideNavHeading.tsx +48 -10
  448. package/src/SideNav/SideNavItem.tsx +20 -12
  449. package/src/Stack/Stack.doc.mjs +110 -0
  450. package/src/StatusDot/StatusDot.doc.mjs +12 -3
  451. package/src/Switch/Switch.test.tsx +32 -0
  452. package/src/Switch/Switch.tsx +8 -8
  453. package/src/TabList/Tab.tsx +2 -9
  454. package/src/TabList/TabMenu.tsx +14 -21
  455. package/src/Table/BaseTable.tsx +11 -0
  456. package/src/Table/index.ts +1 -4
  457. package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +30 -14
  458. package/src/Table/plugins/rowExpansion/index.ts +1 -4
  459. package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +125 -175
  460. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +115 -458
  461. package/src/Table/plugins/sortable/useTableSortable.tsx +2 -6
  462. package/src/Table/plugins/tree/useTableTreeData.tsx +43 -24
  463. package/src/Table/tableContextMenu.test.tsx +27 -0
  464. package/src/Table/tableContextMenu.tsx +1 -0
  465. package/src/Table/types.ts +11 -0
  466. package/src/Table/useTableGroupedRows.doc.mjs +1 -1
  467. package/src/Table/useTableRowExpansion.doc.mjs +43 -66
  468. package/src/TextArea/TextArea.doc.mjs +42 -2
  469. package/src/TextArea/TextArea.test.tsx +219 -0
  470. package/src/TextArea/TextArea.tsx +34 -7
  471. package/src/TextInput/TextInput.doc.mjs +18 -2
  472. package/src/TextInput/TextInput.test.tsx +189 -0
  473. package/src/TextInput/TextInput.tsx +25 -31
  474. package/src/Thumbnail/Thumbnail.tsx +2 -9
  475. package/src/TimeInput/TimeInput.doc.mjs +2 -2
  476. package/src/TimeInput/TimeInput.test.tsx +30 -3
  477. package/src/TimeInput/TimeInput.tsx +9 -26
  478. package/src/Timestamp/Timestamp.doc.mjs +4 -3
  479. package/src/Timestamp/TimestampHoverCard.tsx +13 -35
  480. package/src/Token/Token.tsx +5 -28
  481. package/src/Tokenizer/Tokenizer.doc.mjs +2 -2
  482. package/src/Tokenizer/Tokenizer.test.tsx +45 -3
  483. package/src/Tokenizer/Tokenizer.tsx +10 -2
  484. package/src/Tooltip/useTooltip.tsx +2 -23
  485. package/src/TopNav/TopNav.tsx +1 -3
  486. package/src/TopNav/TopNavHeading.tsx +42 -13
  487. package/src/TopNav/TopNavItem.tsx +3 -21
  488. package/src/TopNav/TopNavMegaMenu.test.tsx +433 -33
  489. package/src/TopNav/TopNavMegaMenu.tsx +118 -53
  490. package/src/TopNav/TopNavMegaMenuItem.tsx +2 -9
  491. package/src/TopNav/TopNavMenu.tsx +34 -37
  492. package/src/TreeList/TreeList.doc.mjs +5 -0
  493. package/src/TreeList/TreeList.test.tsx +206 -7
  494. package/src/TreeList/TreeList.tsx +22 -0
  495. package/src/TreeList/TreeListBranches.tsx +25 -2
  496. package/src/TreeList/TreeListItem.tsx +89 -52
  497. package/src/Typeahead/BaseTypeahead.tsx +19 -6
  498. package/src/Typeahead/Typeahead.doc.mjs +1 -0
  499. package/src/Typeahead/Typeahead.test.tsx +76 -3
  500. package/src/hooks/containerReveal.stylex.ts +102 -0
  501. package/src/hooks/index.ts +4 -0
  502. package/src/hooks/useClipboard.doc.mjs +87 -0
  503. package/src/hooks/useClipboard.test.tsx +151 -0
  504. package/src/hooks/useClipboard.ts +135 -0
  505. package/src/hooks/useContainerReveal.doc.mjs +7 -7
  506. package/src/hooks/useContainerReveal.test.tsx +42 -62
  507. package/src/hooks/useContainerReveal.ts +36 -91
  508. package/src/hooks/useImageMode.test.ts +1 -1
  509. package/src/hooks/useIndicatorFocusRing.tsx +120 -0
  510. package/src/hooks/useLongPress.test.tsx +18 -0
  511. package/src/hooks/useLongPress.ts +12 -2
  512. package/src/hooks/useScrollLock.test.ts +72 -0
  513. package/src/hooks/useScrollLock.ts +51 -19
  514. package/src/hooks/useStreamingText.test.ts +46 -1
  515. package/src/hooks/useStreamingText.ts +49 -7
  516. package/src/hooks/useTreeFocus.test.tsx +50 -0
  517. package/src/hooks/useTreeFocus.ts +14 -3
  518. package/src/hooks/useTypeahead.test.tsx +150 -0
  519. package/src/hooks/useTypeahead.ts +24 -9
  520. package/src/i18n/InternationalizationProvider.doc.mjs +7 -2
  521. package/src/index.ts +1 -0
  522. package/src/theme/defineTheme.ts +24 -0
  523. package/src/theme/derivedVarRegistry.test.ts +21 -1
  524. package/src/theme/derivedVarRegistry.ts +19 -6
  525. package/src/theme/expandColorScale.test.ts +14 -0
  526. package/src/theme/expandColorScale.ts +24 -9
  527. package/src/theme/expandTypeScale.test.ts +43 -0
  528. package/src/theme/expandTypeScale.ts +17 -0
  529. package/src/theme/generateThemeRules.test.ts +61 -20
  530. package/src/theme/generateThemeRules.ts +11 -0
  531. package/src/theme/index.ts +13 -0
  532. package/src/utils/focusOutline.stylex.ts +163 -0
  533. package/src/utils/focusReturn.test.ts +42 -0
  534. package/src/utils/focusReturn.ts +31 -0
  535. package/src/utils/index.ts +1 -0
  536. package/src/utils/mergeRefs.test.ts +48 -0
  537. package/src/utils/mergeRefs.ts +12 -6
  538. package/src/utils/rtlStyles.ts +2 -2
  539. package/src/utils/themeProps.ts +26 -4
  540. package/dist/CheckboxInput/checkbox.markers.stylex.d.ts +0 -9
  541. package/dist/CheckboxInput/checkbox.markers.stylex.d.ts.map +0 -1
  542. package/dist/CheckboxInput/checkbox.markers.stylex.js +0 -14
  543. package/dist/RadioList/radio.markers.stylex.d.ts +0 -7
  544. package/dist/RadioList/radio.markers.stylex.d.ts.map +0 -1
  545. package/dist/RadioList/radio.markers.stylex.js +0 -12
  546. package/dist/hooks/containerReveal.pool.stylex.d.ts +0 -303
  547. package/dist/hooks/containerReveal.pool.stylex.d.ts.map +0 -1
  548. package/dist/hooks/containerReveal.pool.stylex.js +0 -292
  549. package/src/CheckboxInput/checkbox.markers.stylex.ts +0 -12
  550. package/src/RadioList/radio.markers.stylex.ts +0 -10
  551. package/src/hooks/containerReveal.pool.stylex.ts +0 -571
@@ -3,7 +3,7 @@
3
3
  /**
4
4
  * @file Selector.test.tsx
5
5
  * @input Uses vitest, @testing-library/react, @testing-library/user-event
6
- * @output Unit tests for Selector
6
+ * @output Unit tests for Selector behavior and selected-item geometry
7
7
  * @position Tests; validates Selector behavior
8
8
  *
9
9
  * SYNC: When Selector.tsx API changes, update these tests.
@@ -18,24 +18,26 @@ import {
18
18
  within,
19
19
  } from '@testing-library/react';
20
20
  import userEvent from '@testing-library/user-event';
21
+ import {useState} from 'react';
21
22
  import {Selector} from './Selector';
22
23
  import {SelectorOption} from './SelectorOption';
23
24
  import {Icon} from '../Icon';
25
+ import {RadioIndicator} from '../Indicator';
24
26
  import {InputGroup, InputGroupText} from '../InputGroup';
25
27
  import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
26
28
  import {defineTheme} from '../theme/defineTheme';
29
+ import {Theme} from '../theme/Theme';
27
30
  import {generateThemeCSS} from '../theme/generateThemeRules';
28
31
 
29
32
  function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
30
33
  const {prose, component} = generateThemeCSS(theme);
31
34
  return [prose, component].filter(Boolean).join('\n\n');
32
35
  }
33
- function politeRegion(): HTMLElement | null {
34
- return document.querySelector('[data-astryx-live-region="polite"]');
35
- }
36
36
 
37
37
  // Mock showPopover and hidePopover methods since they're not implemented in jsdom
38
38
  beforeEach(() => {
39
+ // The live regions are a document-level singleton; start each test clean.
40
+ __resetLiveRegionsForTest();
39
41
  HTMLElement.prototype.showPopover = vi.fn(function (this: HTMLElement) {
40
42
  this.setAttribute('popover-open', '');
41
43
  const event = new Event('toggle', {bubbles: false});
@@ -70,6 +72,23 @@ const h = {hidden: true} as const;
70
72
 
71
73
  const OPTIONS = ['Apple', 'Banana', 'Cherry'];
72
74
 
75
+ // Mirrors useTypeahead's default resetMs — how long the typed buffer survives.
76
+ const TYPEAHEAD_RESET_MS = 750;
77
+
78
+ const politeRegion = () =>
79
+ document.querySelector('[data-astryx-live-region="polite"]');
80
+
81
+ /**
82
+ * Type onto an element with no awaits between keystrokes. Typeahead only
83
+ * accumulates while keys land inside the reset window, so an awaited
84
+ * `user.keyboard` per character would put CI stalls on the critical path.
85
+ */
86
+ function type(text: string, element: HTMLElement) {
87
+ for (const key of text) {
88
+ fireEvent.keyDown(element, {key});
89
+ }
90
+ }
91
+
73
92
  function rect({
74
93
  top,
75
94
  bottom,
@@ -98,36 +117,103 @@ function rect({
98
117
  };
99
118
  }
100
119
 
101
- function mockSelectorRects() {
120
+ function mockSelectorRects({
121
+ anchor = rect({top: 160, bottom: 190, height: 30}),
122
+ trigger = rect({top: 160, bottom: 190, height: 30}),
123
+ listbox = rect({top: 190, bottom: 310, height: 120}),
124
+ selectedItem = rect({top: 220, bottom: 250, height: 30}),
125
+ listboxLayoutHeight = listbox.height,
126
+ selectedItemLayoutTop = selectedItem.top - listbox.top,
127
+ selectedItemLayoutHeight = selectedItem.height,
128
+ viewportHeight = 200,
129
+ }: {
130
+ anchor?: DOMRect;
131
+ trigger?: DOMRect;
132
+ listbox?: DOMRect;
133
+ selectedItem?: DOMRect;
134
+ listboxLayoutHeight?: number;
135
+ selectedItemLayoutTop?: number;
136
+ selectedItemLayoutHeight?: number;
137
+ viewportHeight?: number;
138
+ } = {}) {
102
139
  const originalGetBoundingClientRect =
103
140
  HTMLElement.prototype.getBoundingClientRect;
104
141
  const originalInnerHeight = Object.getOwnPropertyDescriptor(
105
142
  window,
106
143
  'innerHeight',
107
144
  );
145
+ const originalOffsetTop = Object.getOwnPropertyDescriptor(
146
+ HTMLElement.prototype,
147
+ 'offsetTop',
148
+ );
149
+ const originalOffsetHeight = Object.getOwnPropertyDescriptor(
150
+ HTMLElement.prototype,
151
+ 'offsetHeight',
152
+ );
108
153
  HTMLElement.prototype.getBoundingClientRect = function () {
154
+ if (this.classList.contains('astryx-selector')) {
155
+ return anchor;
156
+ }
109
157
  // The trigger is role="combobox" by default, or a plain button with
110
158
  // aria-haspopup="listbox" in hasSearch mode — match either.
111
159
  if (
112
160
  this.getAttribute('role') === 'combobox' ||
113
161
  this.getAttribute('aria-haspopup') === 'listbox'
114
162
  ) {
115
- return rect({top: 160, bottom: 190, height: 30});
163
+ return trigger;
116
164
  }
117
165
  if (this.getAttribute('role') === 'listbox') {
118
- return rect({top: 190, bottom: 310, height: 120});
166
+ return listbox;
119
167
  }
120
168
  if (this.id.endsWith('-item-1')) {
121
- return rect({top: 220, bottom: 250, height: 30});
169
+ return selectedItem;
122
170
  }
123
171
  return originalGetBoundingClientRect.call(this);
124
172
  };
173
+ Object.defineProperty(HTMLElement.prototype, 'offsetTop', {
174
+ configurable: true,
175
+ get() {
176
+ if (this.getAttribute('role') === 'listbox') {
177
+ return 0;
178
+ }
179
+ if (this.id.endsWith('-item-1')) {
180
+ return selectedItemLayoutTop;
181
+ }
182
+ return 0;
183
+ },
184
+ });
185
+ Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
186
+ configurable: true,
187
+ get() {
188
+ if (this.getAttribute('role') === 'listbox') {
189
+ return listboxLayoutHeight;
190
+ }
191
+ if (this.id.endsWith('-item-1')) {
192
+ return selectedItemLayoutHeight;
193
+ }
194
+ return 0;
195
+ },
196
+ });
125
197
  Object.defineProperty(window, 'innerHeight', {
126
- value: 200,
198
+ value: viewportHeight,
127
199
  configurable: true,
128
200
  });
129
201
  return () => {
130
202
  HTMLElement.prototype.getBoundingClientRect = originalGetBoundingClientRect;
203
+ if (originalOffsetTop) {
204
+ Object.defineProperty(
205
+ HTMLElement.prototype,
206
+ 'offsetTop',
207
+ originalOffsetTop,
208
+ );
209
+ }
210
+ if (originalOffsetHeight) {
211
+ Object.defineProperty(
212
+ HTMLElement.prototype,
213
+ 'offsetHeight',
214
+ originalOffsetHeight,
215
+ );
216
+ }
131
217
  if (originalInnerHeight) {
132
218
  Object.defineProperty(window, 'innerHeight', originalInnerHeight);
133
219
  }
@@ -152,6 +238,60 @@ describe('Selector', () => {
152
238
  expect(screen.getByRole('combobox')).toHaveTextContent('Banana');
153
239
  });
154
240
 
241
+ it('draws the selected mark through the check indicator', () => {
242
+ render(
243
+ <Selector
244
+ label="Fruit"
245
+ options={OPTIONS}
246
+ value="Banana"
247
+ onChange={() => {}}
248
+ />,
249
+ );
250
+
251
+ const selected = screen.getByRole('option', {name: /Banana/, hidden: true});
252
+ // The default check indicator IS the glyph — no wrapper element, so the
253
+ // host's theme target sits on the same node as astryx-icon.
254
+ const mark = selected.querySelector('.astryx-selector-check');
255
+ expect(mark).not.toBeNull();
256
+ expect(mark).toHaveClass('astryx-icon');
257
+ });
258
+
259
+ it('lets a theme replace the mark with a radio, which draws when unselected too', () => {
260
+ // The point of the indicator layer: one theme entry, and every
261
+ // single-selection mark becomes a radio — including the empty circle on
262
+ // rows that are NOT selected, which a check-only mark never drew.
263
+ const theme = defineTheme({
264
+ name: 'selector-radio-mark-test',
265
+ indicators: {check: RadioIndicator},
266
+ });
267
+
268
+ render(
269
+ <Theme theme={theme}>
270
+ <Selector
271
+ label="Fruit"
272
+ options={OPTIONS}
273
+ value="Banana"
274
+ onChange={() => {}}
275
+ />
276
+ </Theme>,
277
+ );
278
+
279
+ const options = screen.getAllByRole('option', {hidden: true});
280
+ expect(options.length).toBeGreaterThan(1);
281
+
282
+ // Every row has a radio, selected or not.
283
+ for (const option of options) {
284
+ expect(option.querySelector('.astryx-radio')).not.toBeNull();
285
+ }
286
+
287
+ // And exactly the selected one is filled.
288
+ const filled = options.filter(
289
+ o => o.querySelector('.astryx-radio-dot') != null,
290
+ );
291
+ expect(filled).toHaveLength(1);
292
+ expect(filled[0]).toHaveTextContent('Banana');
293
+ });
294
+
155
295
  it('renders custom option endContent', async () => {
156
296
  const user = userEvent.setup();
157
297
  render(
@@ -265,6 +405,81 @@ describe('Selector', () => {
265
405
  }
266
406
  });
267
407
 
408
+ it('aligns the selected item using untransformed layout geometry', async () => {
409
+ const restoreRects = mockSelectorRects({
410
+ anchor: rect({top: 160, bottom: 192, height: 32}),
411
+ trigger: rect({top: 166, bottom: 186, height: 20}),
412
+ // Simulate the 0.95 entry scale in visual rects while retaining the
413
+ // untransformed 120px list / 36px item offset used for positioning.
414
+ listbox: rect({top: 190, bottom: 304, height: 114}),
415
+ selectedItem: rect({
416
+ top: 224.2,
417
+ bottom: 254.6,
418
+ height: 30.4,
419
+ }),
420
+ listboxLayoutHeight: 120,
421
+ selectedItemLayoutTop: 36,
422
+ selectedItemLayoutHeight: 32,
423
+ viewportHeight: 900,
424
+ });
425
+ const user = userEvent.setup();
426
+ try {
427
+ render(
428
+ <Selector
429
+ label="Fruit"
430
+ options={OPTIONS}
431
+ value="Banana"
432
+ onChange={() => {}}
433
+ />,
434
+ );
435
+
436
+ await user.click(screen.getByRole('combobox'));
437
+ const popover = screen
438
+ .getByRole('listbox', {hidden: true})
439
+ .closest('[popover]');
440
+ await waitFor(() => {
441
+ // 68px geometric alignment plus the 1px optical correction.
442
+ expect(popover?.getAttribute('style')).toContain(
443
+ 'margin-block-start: -69px',
444
+ );
445
+ });
446
+ } finally {
447
+ restoreRects();
448
+ }
449
+ });
450
+
451
+ it('adds the border inset only to input-variant dropdowns', async () => {
452
+ const user = userEvent.setup();
453
+ const {unmount} = render(
454
+ <Selector
455
+ label="Input fruit"
456
+ options={OPTIONS}
457
+ value="Banana"
458
+ onChange={() => {}}
459
+ />,
460
+ );
461
+
462
+ await user.click(screen.getByRole('combobox'));
463
+ const inputDropdownClass = screen.getByRole('listbox', h).className;
464
+ unmount();
465
+
466
+ render(
467
+ <Selector
468
+ label="Ghost fruit"
469
+ options={OPTIONS}
470
+ value="Banana"
471
+ variant="ghost"
472
+ onChange={() => {}}
473
+ />,
474
+ );
475
+ await user.click(screen.getByRole('combobox'));
476
+ const ghostDropdownClass = screen.getByRole('listbox', h).className;
477
+
478
+ // The bordered input gets one extra StyleX rule for its border-width
479
+ // correction; the borderless ghost keeps the base menu inset.
480
+ expect(inputDropdownClass).not.toBe(ghostDropdownClass);
481
+ });
482
+
268
483
  it('does not apply selected-item overlay offset when placement is explicit', async () => {
269
484
  const restoreRects = mockSelectorRects();
270
485
  const user = userEvent.setup();
@@ -988,6 +1203,532 @@ describe('Selector', () => {
988
1203
  });
989
1204
  });
990
1205
 
1206
+ describe('typeahead', () => {
1207
+ it('selects the matching option by typing on the closed trigger', async () => {
1208
+ const user = userEvent.setup();
1209
+ const onChange = vi.fn();
1210
+ render(<Selector label="Fruit" options={OPTIONS} onChange={onChange} />);
1211
+
1212
+ await user.tab();
1213
+ await user.keyboard('c');
1214
+
1215
+ expect(onChange).toHaveBeenCalledWith('Cherry');
1216
+ // Native select parity: the value changes without opening the menu.
1217
+ expect(screen.getByRole('combobox')).toHaveAttribute(
1218
+ 'aria-expanded',
1219
+ 'false',
1220
+ );
1221
+ });
1222
+
1223
+ it('cycles through options sharing a first letter on repeated presses', async () => {
1224
+ const user = userEvent.setup();
1225
+ function Harness() {
1226
+ const [value, setValue] = useState<string | undefined>(undefined);
1227
+ return (
1228
+ <Selector
1229
+ label="City"
1230
+ options={['Austin', 'Chicago', 'Cleveland', 'Columbus']}
1231
+ value={value}
1232
+ onChange={setValue}
1233
+ />
1234
+ );
1235
+ }
1236
+ render(<Harness />);
1237
+
1238
+ await user.tab();
1239
+ await user.keyboard('c');
1240
+ expect(screen.getByRole('combobox')).toHaveTextContent('Chicago');
1241
+ await user.keyboard('c');
1242
+ expect(screen.getByRole('combobox')).toHaveTextContent('Cleveland');
1243
+ await user.keyboard('c');
1244
+ expect(screen.getByRole('combobox')).toHaveTextContent('Columbus');
1245
+ // Wraps back around past non-matching options.
1246
+ await user.keyboard('c');
1247
+ expect(screen.getByRole('combobox')).toHaveTextContent('Chicago');
1248
+ });
1249
+
1250
+ it('advances past the current selection on a fresh single-letter press', async () => {
1251
+ const user = userEvent.setup();
1252
+ function Harness() {
1253
+ const [value, setValue] = useState<string | undefined>('Chicago');
1254
+ return (
1255
+ <Selector
1256
+ label="City"
1257
+ options={['Austin', 'Chicago', 'Cleveland', 'Columbus']}
1258
+ value={value}
1259
+ onChange={setValue}
1260
+ />
1261
+ );
1262
+ }
1263
+ render(<Harness />);
1264
+
1265
+ const trigger = screen.getByRole('combobox');
1266
+ await user.tab();
1267
+ // Native select parity: the selected option's own initial moves on to
1268
+ // the next match. Anchoring the search AT the selection instead of after
1269
+ // it re-matches the current value, and the duplicate-select guard then
1270
+ // swallows the keystroke entirely.
1271
+ type('c', trigger);
1272
+
1273
+ expect(trigger).toHaveTextContent('Cleveland');
1274
+ });
1275
+
1276
+ it('advances the highlight past the current one with the menu open', async () => {
1277
+ const user = userEvent.setup();
1278
+ render(
1279
+ <Selector
1280
+ label="City"
1281
+ options={['Austin', 'Chicago', 'Cleveland', 'Columbus']}
1282
+ value="Chicago"
1283
+ onChange={() => {}}
1284
+ />,
1285
+ );
1286
+
1287
+ const trigger = screen.getByRole('combobox');
1288
+ await user.tab();
1289
+ await user.keyboard('{Enter}'); // opens with the highlight on Chicago
1290
+ type('c', trigger);
1291
+
1292
+ const activeId = trigger.getAttribute('aria-activedescendant');
1293
+ expect(document.getElementById(activeId ?? '')).toHaveTextContent(
1294
+ 'Cleveland',
1295
+ );
1296
+ });
1297
+
1298
+ it('treats a space mid-buffer as part of the match, not as open', async () => {
1299
+ const user = userEvent.setup();
1300
+ const onChange = vi.fn();
1301
+ render(
1302
+ <Selector
1303
+ label="State"
1304
+ options={['New Jersey', 'New York']}
1305
+ onChange={onChange}
1306
+ />,
1307
+ );
1308
+
1309
+ const trigger = screen.getByRole('combobox');
1310
+ await user.tab();
1311
+ // Synchronous keydowns: the buffer only accumulates while keystrokes
1312
+ // land inside the TYPEAHEAD_RESET_MS window, and awaiting between them
1313
+ // would put a CI stall on the critical path.
1314
+ type('new y', trigger);
1315
+
1316
+ expect(onChange).toHaveBeenLastCalledWith('New York');
1317
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
1318
+ });
1319
+
1320
+ it('opens and seeds the search input when typing on a closed hasSearch trigger', async () => {
1321
+ const user = userEvent.setup();
1322
+ render(<Selector label="Fruit" options={OPTIONS} hasSearch />);
1323
+
1324
+ await user.tab();
1325
+ await user.keyboard('c');
1326
+
1327
+ const search = screen.getByPlaceholderText('Search…');
1328
+ expect(search).toHaveValue('c');
1329
+ await waitFor(() => expect(search).toHaveFocus());
1330
+ });
1331
+
1332
+ it('accumulates a multi-character prefix and resets it after the timeout', async () => {
1333
+ const user = userEvent.setup();
1334
+ const onChange = vi.fn();
1335
+ // Controlled: the committed value has to feed back in, or the match
1336
+ // anchor stays -1 for the whole test and never gets exercised.
1337
+ function Harness() {
1338
+ const [value, setValue] = useState<string | undefined>(undefined);
1339
+ return (
1340
+ <Selector
1341
+ label="Fruit"
1342
+ options={['Apple', 'Banana', 'Blueberry']}
1343
+ value={value}
1344
+ onChange={next => {
1345
+ setValue(next);
1346
+ onChange(next);
1347
+ }}
1348
+ />
1349
+ );
1350
+ }
1351
+ render(<Harness />);
1352
+
1353
+ const trigger = screen.getByRole('combobox');
1354
+ await user.tab();
1355
+ type('b', trigger);
1356
+ expect(trigger).toHaveTextContent('Banana');
1357
+ // Within the window the buffer accumulates: "bl" → Blueberry. A
1358
+ // multi-character buffer refines, so it may keep the current match.
1359
+ type('l', trigger);
1360
+ expect(trigger).toHaveTextContent('Blueberry');
1361
+
1362
+ // Past the window the buffer starts fresh: "a" → Apple. A surviving
1363
+ // buffer would search "bla" and match nothing, so only a real reset
1364
+ // gets here — worth the one real wait in the suite.
1365
+ await new Promise(resolve =>
1366
+ setTimeout(resolve, TYPEAHEAD_RESET_MS + 100),
1367
+ );
1368
+ type('a', trigger);
1369
+ expect(trigger).toHaveTextContent('Apple');
1370
+ });
1371
+
1372
+ it('skips disabled options when matching', async () => {
1373
+ const user = userEvent.setup();
1374
+ function Harness() {
1375
+ const [value, setValue] = useState<string | undefined>(undefined);
1376
+ return (
1377
+ <Selector
1378
+ label="Fruit"
1379
+ options={[{value: 'Cherry', disabled: true}, 'Coconut']}
1380
+ value={value}
1381
+ onChange={setValue}
1382
+ />
1383
+ );
1384
+ }
1385
+ render(<Harness />);
1386
+
1387
+ const trigger = screen.getByRole('combobox');
1388
+ await user.tab();
1389
+ type('c', trigger);
1390
+ expect(trigger).toHaveTextContent('Coconut');
1391
+
1392
+ // The skip has to survive cycling too: with Coconut current, the next
1393
+ // press wraps onto the disabled Cherry and must pass over it.
1394
+ type('c', trigger);
1395
+ expect(trigger).toHaveTextContent('Coconut');
1396
+ expect(trigger).not.toHaveTextContent('Cherry');
1397
+ });
1398
+
1399
+ it('moves the highlight without committing when typing with the menu open', async () => {
1400
+ const user = userEvent.setup();
1401
+ const onChange = vi.fn();
1402
+ render(
1403
+ <Selector
1404
+ label="City"
1405
+ options={['Austin', 'Chicago', 'Cleveland']}
1406
+ onChange={onChange}
1407
+ />,
1408
+ );
1409
+
1410
+ await user.tab();
1411
+ await user.keyboard('{Enter}'); // open
1412
+ const trigger = screen.getByRole('combobox');
1413
+
1414
+ await user.keyboard('c');
1415
+ let activeId = trigger.getAttribute('aria-activedescendant');
1416
+ expect(document.getElementById(activeId ?? '')).toHaveTextContent(
1417
+ 'Chicago',
1418
+ );
1419
+
1420
+ // Repeated press cycles the highlight, still without committing.
1421
+ await user.keyboard('c');
1422
+ activeId = trigger.getAttribute('aria-activedescendant');
1423
+ expect(document.getElementById(activeId ?? '')).toHaveTextContent(
1424
+ 'Cleveland',
1425
+ );
1426
+ expect(onChange).not.toHaveBeenCalled();
1427
+ // aria-activedescendant already announces each match, so announcing
1428
+ // again here would make a screen reader say every match twice.
1429
+ // useAnnounce writes its text in a rAF callback, so let a frame pass —
1430
+ // asserting before it runs would pass no matter what the code does.
1431
+ await new Promise(resolve => requestAnimationFrame(() => resolve(null)));
1432
+ expect(politeRegion()?.textContent ?? '').toBe('');
1433
+
1434
+ await user.keyboard('{Enter}');
1435
+ expect(onChange).toHaveBeenCalledWith('Cleveland');
1436
+ });
1437
+
1438
+ it('does not fire onChange when the only match is already selected', async () => {
1439
+ const user = userEvent.setup();
1440
+ const onChange = vi.fn();
1441
+ render(
1442
+ <Selector
1443
+ label="Fruit"
1444
+ options={OPTIONS}
1445
+ value="Cherry"
1446
+ onChange={onChange}
1447
+ />,
1448
+ );
1449
+
1450
+ await user.tab();
1451
+ await user.keyboard('c');
1452
+
1453
+ expect(onChange).not.toHaveBeenCalled();
1454
+ });
1455
+
1456
+ it('ignores printable keys pressed with ctrl or meta modifiers', async () => {
1457
+ const user = userEvent.setup();
1458
+ const onChange = vi.fn();
1459
+ render(<Selector label="Fruit" options={OPTIONS} onChange={onChange} />);
1460
+
1461
+ await user.tab();
1462
+ await user.keyboard('{Control>}c{/Control}{Meta>}b{/Meta}');
1463
+
1464
+ expect(onChange).not.toHaveBeenCalled();
1465
+ });
1466
+
1467
+ it('announces the committed option to screen readers', async () => {
1468
+ const user = userEvent.setup();
1469
+ render(<Selector label="Fruit" options={OPTIONS} onChange={() => {}} />);
1470
+
1471
+ await user.tab();
1472
+ await user.keyboard('c');
1473
+
1474
+ // The trigger keeps focus and the menu never opens, so nothing else
1475
+ // prompts a re-read. The polite live region carries the new value.
1476
+ await waitFor(() => {
1477
+ expect(politeRegion()).toHaveTextContent('Cherry');
1478
+ });
1479
+ });
1480
+
1481
+ it('does not select while focusable-disabled', async () => {
1482
+ const user = userEvent.setup();
1483
+ const onChange = vi.fn();
1484
+ render(
1485
+ <Selector
1486
+ label="Fruit"
1487
+ options={OPTIONS}
1488
+ onChange={onChange}
1489
+ isDisabled
1490
+ disabledMessage="Ask an admin"
1491
+ />,
1492
+ );
1493
+
1494
+ // aria-disabled keeps the trigger focusable, so keydowns still arrive.
1495
+ screen.getByRole('combobox').focus();
1496
+ await user.keyboard('c');
1497
+
1498
+ expect(onChange).not.toHaveBeenCalled();
1499
+ });
1500
+
1501
+ it('cycles without duplicating changeAction while an action is pending', async () => {
1502
+ const user = userEvent.setup();
1503
+ const calls: string[] = [];
1504
+ render(
1505
+ <Selector
1506
+ label="City"
1507
+ options={['Chicago', 'Cleveland', 'Columbus']}
1508
+ value={undefined}
1509
+ changeAction={async value => {
1510
+ calls.push(value);
1511
+ // Never settles, so the value prop never catches up to what the
1512
+ // trigger already shows.
1513
+ await new Promise<void>(() => {});
1514
+ }}
1515
+ />,
1516
+ );
1517
+
1518
+ const trigger = screen.getByRole('combobox');
1519
+ await user.tab();
1520
+ type('ccc', trigger);
1521
+
1522
+ // The anchor must come from the optimistic value, not the stale prop.
1523
+ // Three options make that observable: with a stale anchor every press
1524
+ // re-matches Chicago, which the duplicate guard then swallows.
1525
+ expect(calls).toEqual(['Chicago', 'Cleveland', 'Columbus']);
1526
+ });
1527
+
1528
+ it('starts a fresh buffer after selecting from the open menu', async () => {
1529
+ const user = userEvent.setup();
1530
+ const onChange = vi.fn();
1531
+ render(
1532
+ <Selector
1533
+ label="Animal"
1534
+ options={['Cat', 'Dog']}
1535
+ onChange={onChange}
1536
+ />,
1537
+ );
1538
+
1539
+ await user.tab();
1540
+ await user.keyboard('{Enter}'); // open
1541
+ await user.keyboard('d'); // highlight Dog
1542
+ await user.keyboard('{Enter}'); // commit Dog, closes
1543
+ onChange.mockClear();
1544
+
1545
+ // The stale 'd' must not linger: 'c' is a fresh buffer, not "dc".
1546
+ await user.keyboard('c');
1547
+ expect(onChange).toHaveBeenCalledWith('Cat');
1548
+ });
1549
+
1550
+ it('starts a fresh buffer after the value is cleared', async () => {
1551
+ const user = userEvent.setup();
1552
+ const onChange = vi.fn();
1553
+ function Harness() {
1554
+ const [value, setValue] = useState<string | null>('Dog');
1555
+ return (
1556
+ <Selector
1557
+ label="Animal"
1558
+ options={['Cat', 'Dog']}
1559
+ hasClear
1560
+ value={value}
1561
+ onChange={next => {
1562
+ setValue(next);
1563
+ onChange(next);
1564
+ }}
1565
+ />
1566
+ );
1567
+ }
1568
+ render(<Harness />);
1569
+
1570
+ const trigger = screen.getByRole('combobox');
1571
+ await user.tab();
1572
+ // Fills the buffer with 'd' without committing — Dog is already current.
1573
+ type('d', trigger);
1574
+ fireEvent.keyDown(trigger, {key: 'Delete'});
1575
+ expect(onChange).toHaveBeenLastCalledWith(null);
1576
+
1577
+ // The stale 'd' must not survive the clear: 'c' is a fresh buffer, not
1578
+ // "dc", which would match nothing and leave the value cleared.
1579
+ type('c', trigger);
1580
+ expect(onChange).toHaveBeenLastCalledWith('Cat');
1581
+ });
1582
+
1583
+ it('matches on the label and commits the value', async () => {
1584
+ const user = userEvent.setup();
1585
+ const onChange = vi.fn();
1586
+ // Values deliberately crossed against labels: a native select matches
1587
+ // the rendered text and reports the value.
1588
+ function Harness() {
1589
+ const [value, setValue] = useState<string | undefined>(undefined);
1590
+ return (
1591
+ <Selector
1592
+ label="Fruit"
1593
+ options={[
1594
+ {value: 'zzz', label: 'Apple'},
1595
+ {value: 'apple', label: 'Zebra'},
1596
+ ]}
1597
+ value={value}
1598
+ onChange={next => {
1599
+ setValue(next);
1600
+ onChange(next);
1601
+ }}
1602
+ />
1603
+ );
1604
+ }
1605
+ render(<Harness />);
1606
+
1607
+ const trigger = screen.getByRole('combobox');
1608
+ await user.tab();
1609
+ type('a', trigger);
1610
+
1611
+ expect(onChange).toHaveBeenCalledWith('zzz');
1612
+ expect(trigger).toHaveTextContent('Apple');
1613
+
1614
+ // No other label starts with "a" — the option whose *value* is 'apple'
1615
+ // is labelled Zebra — so a second press stays put and commits nothing.
1616
+ type('a', trigger);
1617
+ expect(onChange).toHaveBeenCalledTimes(1);
1618
+ });
1619
+
1620
+ it('matches across sections, ignoring dividers and group titles', async () => {
1621
+ const user = userEvent.setup();
1622
+ const onChange = vi.fn();
1623
+ render(
1624
+ <Selector
1625
+ label="Fruit"
1626
+ options={[
1627
+ 'Almond',
1628
+ {type: 'divider'},
1629
+ {
1630
+ type: 'section',
1631
+ title: 'Tropical',
1632
+ options: [
1633
+ {value: 'mango', label: 'Mango'},
1634
+ {value: 'papaya', label: 'Papaya'},
1635
+ ],
1636
+ },
1637
+ ]}
1638
+ onChange={onChange}
1639
+ />,
1640
+ );
1641
+
1642
+ const trigger = screen.getByRole('combobox');
1643
+ await user.tab();
1644
+ type('p', trigger);
1645
+ expect(onChange).toHaveBeenCalledWith('papaya');
1646
+
1647
+ // The section title "Tropical" is decoration, not an option.
1648
+ onChange.mockClear();
1649
+ type('t', trigger);
1650
+ expect(onChange).not.toHaveBeenCalled();
1651
+ });
1652
+
1653
+ it('keeps aria-activedescendant on the matched option across a section', async () => {
1654
+ const user = userEvent.setup();
1655
+ render(
1656
+ <Selector
1657
+ label="Fruit"
1658
+ options={[
1659
+ 'Almond',
1660
+ {type: 'divider'},
1661
+ {
1662
+ type: 'section',
1663
+ title: 'Berries',
1664
+ options: [{value: 'blueberry', label: 'Blueberry'}],
1665
+ },
1666
+ ]}
1667
+ value="Almond"
1668
+ onChange={() => {}}
1669
+ />,
1670
+ );
1671
+
1672
+ const trigger = screen.getByRole('combobox');
1673
+ await user.tab();
1674
+ await user.keyboard('{Enter}'); // opens on Almond
1675
+ type('b', trigger);
1676
+
1677
+ const activeId = trigger.getAttribute('aria-activedescendant');
1678
+ expect(document.getElementById(activeId ?? '')).toHaveTextContent(
1679
+ 'Blueberry',
1680
+ );
1681
+ });
1682
+
1683
+ it('anchors at the top when the value matches no option', async () => {
1684
+ const user = userEvent.setup();
1685
+ const onChange = vi.fn();
1686
+ render(
1687
+ <Selector
1688
+ label="Fruit"
1689
+ options={['Apple', 'Apricot']}
1690
+ value="Durian"
1691
+ onChange={onChange}
1692
+ />,
1693
+ );
1694
+
1695
+ const trigger = screen.getByRole('combobox');
1696
+ await user.tab();
1697
+ type('a', trigger);
1698
+
1699
+ // Nothing is really selected, so the first match must stay reachable.
1700
+ expect(onChange).toHaveBeenCalledWith('Apple');
1701
+ });
1702
+
1703
+ it('lets Space open the menu after an abandoned typeahead', async () => {
1704
+ const user = userEvent.setup();
1705
+ render(<Selector label="Fruit" options={OPTIONS} />);
1706
+
1707
+ const trigger = screen.getByRole('combobox');
1708
+ await user.tab();
1709
+ await user.keyboard('{Enter}'); // open
1710
+ await user.keyboard('z'); // no match; buffer holds "z"
1711
+ await user.keyboard('{Escape}'); // close, abandoning the buffer
1712
+
1713
+ // A live "z" buffer would swallow Space as a match character.
1714
+ await user.keyboard(' ');
1715
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
1716
+ });
1717
+
1718
+ it('seeds every character typed before the search input takes focus', async () => {
1719
+ const user = userEvent.setup();
1720
+ render(<Selector label="Fruit" options={OPTIONS} hasSearch />);
1721
+
1722
+ await user.tab();
1723
+ // The popup opens on the first key, but focus only moves to the search
1724
+ // input on the next frame — the second key still lands on the trigger.
1725
+ await user.keyboard('ch');
1726
+
1727
+ const search = screen.getByPlaceholderText('Search…');
1728
+ await waitFor(() => expect(search).toHaveValue('ch'));
1729
+ });
1730
+ });
1731
+
991
1732
  describe('InputGroup integration', () => {
992
1733
  it('uses the group Field chrome and composes group and selector labels', () => {
993
1734
  render(
@@ -1318,6 +2059,28 @@ describe('Selector statusVariant forwarding', () => {
1318
2059
  });
1319
2060
  });
1320
2061
 
2062
+ describe('Selector empty-state theme target', () => {
2063
+ const OPTIONS = ['Apple', 'Banana', 'Cherry'];
2064
+
2065
+ it('renders the astryx-selector-empty-state target on the "No results found" element', async () => {
2066
+ const user = userEvent.setup();
2067
+ const {container} = render(
2068
+ <Selector
2069
+ label="Fruit"
2070
+ options={OPTIONS}
2071
+ onChange={() => {}}
2072
+ hasSearch
2073
+ />,
2074
+ );
2075
+ await user.click(screen.getByRole('button', {name: 'Fruit'}));
2076
+ await user.type(screen.getByRole('combobox', h), 'xyz');
2077
+
2078
+ const empty = container.querySelector('.astryx-selector-empty-state');
2079
+ expect(empty).not.toBeNull();
2080
+ expect(empty).toHaveTextContent('No results found');
2081
+ });
2082
+ });
2083
+
1321
2084
  describe('Selector clear icon theme target', () => {
1322
2085
  // Resolve the clear glyph span (the astryx-icon element inside the clear
1323
2086
  // button), independent of the theme target class.
@@ -1330,7 +2093,7 @@ describe('Selector clear icon theme target', () => {
1330
2093
  return icon as HTMLElement;
1331
2094
  };
1332
2095
 
1333
- it('renders the astryx-selector-clear-icon target on the clear glyph', () => {
2096
+ it('renders the astryx-input-clear-icon target (plus the legacy alias) on the clear glyph', () => {
1334
2097
  render(
1335
2098
  <Selector
1336
2099
  label="Fruit"
@@ -1340,11 +2103,13 @@ describe('Selector clear icon theme target', () => {
1340
2103
  hasClear
1341
2104
  />,
1342
2105
  );
1343
- // The stable theme target lands on the icon element itself (not the
1344
- // button), so a theme can restyle just this glyph (color, size, hover)
1345
- // via `defineTheme` — a button-level target could not reach the icon's
1346
- // own color/size.
2106
+ // The canonical target lands on the icon element itself (not the button),
2107
+ // so a theme can restyle just this glyph (color, size, hover) via
2108
+ // `defineTheme` — a button-level target could not reach the icon's own
2109
+ // color/size. The original per-component name rides along for a
2110
+ // deprecation window.
1347
2111
  const icon = getClearIcon();
2112
+ expect(icon).toHaveClass('astryx-input-clear-icon');
1348
2113
  expect(icon).toHaveClass('astryx-selector-clear-icon');
1349
2114
  expect(icon).toHaveClass('astryx-icon');
1350
2115
  });
@@ -1366,11 +2131,13 @@ describe('Selector clear icon theme target', () => {
1366
2131
  expect(onChange).toHaveBeenCalledWith(null);
1367
2132
  });
1368
2133
 
1369
- it('renders the default icon (secondary color, sm size) byte-identically', () => {
1370
- // Pixel-identical default guard: the clear glyph must carry the exact same
1371
- // StyleX color/size classes as a standalone secondary/sm icon. The added
1372
- // target class is purely additive it changes nothing until a theme
1373
- // targets it.
2134
+ it('routes the clear glyph through the shared clear button, keeping the legacy target', () => {
2135
+ // The clear affordance now composes the shared InputClearButton (a ghost
2136
+ // Button with a secondary/sm glyph), so the icon carries the canonical
2137
+ // `astryx-input-clear-icon` target andfor a deprecation window the
2138
+ // original `astryx-selector-clear-icon`. Aside from those target classes
2139
+ // it matches the shared button's own `close`/`sm`/`secondary` glyph
2140
+ // exactly, so the default look is defined in one place.
1374
2141
  render(
1375
2142
  <Selector
1376
2143
  label="Fruit"
@@ -1381,6 +2148,8 @@ describe('Selector clear icon theme target', () => {
1381
2148
  />,
1382
2149
  );
1383
2150
  const icon = getClearIcon();
2151
+ expect(icon).toHaveClass('astryx-input-clear-icon');
2152
+ expect(icon).toHaveClass('astryx-selector-clear-icon');
1384
2153
 
1385
2154
  const {container: refContainer} = render(
1386
2155
  <Icon icon="close" size="sm" color="secondary" />,
@@ -1390,7 +2159,11 @@ describe('Selector clear icon theme target', () => {
1390
2159
  const styleClasses = (el: HTMLElement) =>
1391
2160
  el.className
1392
2161
  .split(' ')
1393
- .filter(c => c !== 'astryx-selector-clear-icon')
2162
+ .filter(
2163
+ c =>
2164
+ c !== 'astryx-input-clear-icon' &&
2165
+ c !== 'astryx-selector-clear-icon',
2166
+ )
1394
2167
  .sort();
1395
2168
 
1396
2169
  expect(styleClasses(icon)).toEqual(styleClasses(refIcon));
@@ -1464,9 +2237,12 @@ describe('Selector indicator (chevron) icon theme target', () => {
1464
2237
  });
1465
2238
  });
1466
2239
 
1467
- it('renders the default icon (inherit color, sm size) byte-identically', () => {
2240
+ it('renders the default icon (secondary color, sm size) byte-identically', () => {
1468
2241
  // Pixel-identical default guard: the chevron glyph must carry the exact
1469
- // same StyleX color/size classes as a standalone inherit/sm icon. The added
2242
+ // same StyleX color/size classes as a standalone secondary/sm icon. The
2243
+ // glyph now sets --color-icon-secondary itself rather than inheriting it
2244
+ // from a wrapper span that set the same token, so the rendered color is
2245
+ // unchanged. The added
1470
2246
  // target class + data-state are purely additive — they change nothing until
1471
2247
  // a theme targets them.
1472
2248
  const {container} = render(
@@ -1475,12 +2251,12 @@ describe('Selector indicator (chevron) icon theme target', () => {
1475
2251
  const icon = getIndicatorIcon(container);
1476
2252
 
1477
2253
  const {container: refContainer} = render(
1478
- <Icon icon="chevronDown" size="sm" color="inherit" />,
2254
+ <Icon icon="chevronDown" size="sm" color="secondary" />,
1479
2255
  );
1480
2256
  const refIcon = refContainer.querySelector('.astryx-icon') as HTMLElement;
1481
2257
 
1482
2258
  // Exclude the additive theme-target classes (the stable target + its
1483
- // reflected state class) so only the StyleX color/size classes remain.
2259
+ // reflected state class) so only StyleX classes remain.
1484
2260
  const themeTargetClasses = new Set([
1485
2261
  'astryx-selector-indicator-icon',
1486
2262
  'collapsed',
@@ -1492,7 +2268,14 @@ describe('Selector indicator (chevron) icon theme target', () => {
1492
2268
  .filter(c => !themeTargetClasses.has(c))
1493
2269
  .sort();
1494
2270
 
1495
- expect(styleClasses(icon)).toEqual(styleClasses(refIcon));
2271
+ // A superset, not an exact match: the chevron additionally carries the
2272
+ // rotation styles, which live on the glyph precisely so a theme can reach
2273
+ // the transform through the same selector as the color. The guard that
2274
+ // matters is that every color/size class of a standalone icon is still
2275
+ // present — i.e. the default look has not drifted.
2276
+ expect(styleClasses(icon)).toEqual(
2277
+ expect.arrayContaining(styleClasses(refIcon)),
2278
+ );
1496
2279
  });
1497
2280
 
1498
2281
  it('exposes selector-indicator-icon so a theme reaches the icon size and per-state color', () => {
@@ -1640,3 +2423,130 @@ describe('Selector search affordances', () => {
1640
2423
  expect(trigger).toHaveAttribute('aria-expanded', 'true');
1641
2424
  });
1642
2425
  });
2426
+
2427
+ describe('Selector selected-marker theme target (selector-check)', () => {
2428
+ const openOptions = (): HTMLElement[] => screen.getAllByRole('option', h);
2429
+
2430
+ it('renders the astryx-selector-check target on the selected row only', () => {
2431
+ render(
2432
+ <Selector
2433
+ label="Fruit"
2434
+ options={OPTIONS}
2435
+ value="Banana"
2436
+ onChange={() => {}}
2437
+ isDefaultOpen
2438
+ />,
2439
+ );
2440
+ const options = openOptions();
2441
+ const selected = options.find(
2442
+ o => o.getAttribute('aria-selected') === 'true',
2443
+ )!;
2444
+ const check = selected.querySelector('.astryx-selector-check');
2445
+ expect(check).toBeInTheDocument();
2446
+ // The target lands on the checkmark glyph itself, so a theme can restyle or
2447
+ // hide it (e.g. to compose its own selected indicator via renderOption).
2448
+ expect(check).toHaveClass('astryx-icon');
2449
+
2450
+ const unselected = options.filter(
2451
+ o => o.getAttribute('aria-selected') !== 'true',
2452
+ );
2453
+ for (const row of unselected) {
2454
+ expect(
2455
+ row.querySelector('.astryx-selector-check'),
2456
+ ).not.toBeInTheDocument();
2457
+ }
2458
+ });
2459
+
2460
+ it('renders the default checkmark byte-identically aside from the target class', () => {
2461
+ // The added target class is purely additive — it changes nothing about the
2462
+ // glyph's own color/size until a theme targets it.
2463
+ render(
2464
+ <Selector
2465
+ label="Fruit"
2466
+ options={OPTIONS}
2467
+ value="Banana"
2468
+ onChange={() => {}}
2469
+ isDefaultOpen
2470
+ />,
2471
+ );
2472
+ const selected = screen
2473
+ .getAllByRole('option', h)
2474
+ .find(o => o.getAttribute('aria-selected') === 'true')!;
2475
+ const check = selected.querySelector(
2476
+ '.astryx-selector-check',
2477
+ ) as HTMLElement;
2478
+
2479
+ const {container: refContainer} = render(
2480
+ <Icon icon="check" size="sm" color="accent" />,
2481
+ );
2482
+ const refIcon = refContainer.querySelector('.astryx-icon') as HTMLElement;
2483
+ const styleClasses = (el: HTMLElement) =>
2484
+ el.className
2485
+ .split(' ')
2486
+ .filter(c => c !== 'astryx-selector-check')
2487
+ .sort();
2488
+ expect(styleClasses(check)).toEqual(styleClasses(refIcon));
2489
+ });
2490
+
2491
+ it('exposes selector-check so a theme can hide or restyle the marker', () => {
2492
+ const theme = defineTheme({
2493
+ name: 'selector-check-test',
2494
+ components: {
2495
+ 'selector-check': {
2496
+ base: {display: 'none'},
2497
+ },
2498
+ },
2499
+ });
2500
+ const css = generateThemeTestCSS(theme);
2501
+ expect(css).toContain('.astryx-selector-check {');
2502
+ expect(css).toContain('display: none');
2503
+ });
2504
+ });
2505
+
2506
+ describe('Selector disabled state theme target', () => {
2507
+ const getSelectorRoot = (container: HTMLElement): HTMLElement => {
2508
+ const root = container.querySelector('.astryx-selector');
2509
+ if (root == null) {
2510
+ throw new Error('selector root not found');
2511
+ }
2512
+ return root as HTMLElement;
2513
+ };
2514
+
2515
+ it('reflects data-disabled="disabled" on the root when disabled', () => {
2516
+ const {container} = render(
2517
+ <Selector
2518
+ label="Fruit"
2519
+ options={OPTIONS}
2520
+ onChange={() => {}}
2521
+ isDisabled
2522
+ />,
2523
+ );
2524
+ expect(getSelectorRoot(container)).toHaveAttribute(
2525
+ 'data-disabled',
2526
+ 'disabled',
2527
+ );
2528
+ });
2529
+
2530
+ it('omits the disabled class/attribute when enabled', () => {
2531
+ const {container} = render(
2532
+ <Selector label="Fruit" options={OPTIONS} onChange={() => {}} />,
2533
+ );
2534
+ const root = getSelectorRoot(container);
2535
+ expect(root).not.toHaveAttribute('data-disabled');
2536
+ expect(root).not.toHaveClass('disabled');
2537
+ });
2538
+
2539
+ it('exposes the disabled state so a theme can key on it', () => {
2540
+ const theme = defineTheme({
2541
+ name: 'selector-disabled-state-test',
2542
+ components: {
2543
+ selector: {
2544
+ 'disabled:disabled': {opacity: '0.4'},
2545
+ },
2546
+ },
2547
+ });
2548
+ const css = generateThemeTestCSS(theme);
2549
+ expect(css).toContain('.astryx-selector.disabled');
2550
+ expect(css).toContain('opacity: 0.4');
2551
+ });
2552
+ });