@astryxdesign/core 0.7.6-canary.f73623d → 0.7.6-canary.f77aaf2

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 (1395) hide show
  1. package/dist/AppShell/AppShell.d.ts.map +1 -1
  2. package/dist/AppShell/AppShell.js +0 -6
  3. package/dist/Badge/Badge.d.ts.map +1 -1
  4. package/dist/Badge/Badge.js +2 -1
  5. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  6. package/dist/BottomSheet/BottomSheet.js +6 -2
  7. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  8. package/dist/BottomSheet/BottomSheetPanel.js +36 -9
  9. package/dist/BottomSheet/BottomSheetSwitcher.d.ts +1 -1
  10. package/dist/BottomSheet/BottomSheetSwitcher.js +1 -1
  11. package/dist/BottomSheet/useMobileKeyboard.d.ts.map +1 -1
  12. package/dist/BottomSheet/useMobileKeyboard.js +44 -8
  13. package/dist/BottomSheet/useSheetGestures.d.ts +36 -6
  14. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
  15. package/dist/BottomSheet/useSheetGestures.js +67 -10
  16. package/dist/Breadcrumbs/BreadcrumbItem.js +4 -2
  17. package/dist/Center/Center.d.ts +1 -1
  18. package/dist/Center/Center.js +1 -1
  19. package/dist/Chat/ChatComposer.d.ts.map +1 -1
  20. package/dist/Chat/ChatComposer.js +10 -5
  21. package/dist/Chat/ChatComposerDrawer.d.ts +1 -1
  22. package/dist/Chat/ChatComposerDrawer.js +1 -1
  23. package/dist/Chat/ChatComposerInput.d.ts +1 -1
  24. package/dist/Chat/ChatComposerInput.js +1 -1
  25. package/dist/Chat/ChatLayout.d.ts.map +1 -1
  26. package/dist/Chat/ChatLayout.js +1 -1
  27. package/dist/Chat/ChatMessageList.d.ts.map +1 -1
  28. package/dist/Chat/ChatMessageList.js +3 -1
  29. package/dist/Chat/ChatPastedTextToken.d.ts.map +1 -1
  30. package/dist/Chat/ChatPastedTextToken.js +15 -4
  31. package/dist/Chat/ChatSystemMessage.d.ts.map +1 -1
  32. package/dist/Chat/ChatSystemMessage.js +2 -1
  33. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  34. package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
  35. package/dist/CheckboxList/CheckboxList.js +5 -2
  36. package/dist/CheckboxList/CheckboxListContext.d.ts +8 -0
  37. package/dist/CheckboxList/CheckboxListContext.d.ts.map +1 -1
  38. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  39. package/dist/CheckboxList/CheckboxListItem.js +5 -2
  40. package/dist/Code/Code.d.ts +1 -1
  41. package/dist/Code/Code.js +1 -1
  42. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  43. package/dist/CommandPalette/CommandPaletteInput.js +8 -5
  44. package/dist/ComplexSelector/ComplexSelector.d.ts +2 -2
  45. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  46. package/dist/ComplexSelector/ComplexSelector.js +7 -2
  47. package/dist/DateInput/DateInput.d.ts +3 -2
  48. package/dist/DateInput/DateInput.d.ts.map +1 -1
  49. package/dist/DateInput/MonthScroller.d.ts.map +1 -1
  50. package/dist/DateInput/MonthScroller.js +17 -18
  51. package/dist/DateRangeInput/DateRangeInput.d.ts +3 -2
  52. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  53. package/dist/DateRangeInput/DateRangeInput.js +8 -4
  54. package/dist/Dialog/Dialog.d.ts.map +1 -1
  55. package/dist/Dialog/Dialog.js +10 -15
  56. package/dist/Drawer/Drawer.d.ts +1 -1
  57. package/dist/Drawer/Drawer.js +1 -1
  58. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  59. package/dist/DropdownMenu/DropdownMenu.js +19 -8
  60. package/dist/DropdownMenu/DropdownMenuGroup.d.ts.map +1 -1
  61. package/dist/DropdownMenu/DropdownMenuGroup.js +2 -1
  62. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  63. package/dist/DropdownMenu/DropdownMenuSubMenu.js +1 -1
  64. package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts.map +1 -1
  65. package/dist/DropdownMenu/MenuBottomSheetActionList.js +9 -3
  66. package/dist/Field/Field.d.ts +2 -2
  67. package/dist/Field/Field.d.ts.map +1 -1
  68. package/dist/Field/FieldLabel.d.ts.map +1 -1
  69. package/dist/Field/FieldLabel.js +19 -2
  70. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  71. package/dist/Field/PanelSearchInput.js +10 -5
  72. package/dist/Field/fieldStatusPlacement.d.ts +15 -0
  73. package/dist/Field/fieldStatusPlacement.d.ts.map +1 -0
  74. package/dist/Field/fieldStatusPlacement.js +1 -0
  75. package/dist/FieldStatus/FieldStatus.d.ts +16 -28
  76. package/dist/FieldStatus/FieldStatus.d.ts.map +1 -1
  77. package/dist/FieldStatus/FieldStatus.js +25 -3
  78. package/dist/FieldStatus/index.d.ts +7 -0
  79. package/dist/FieldStatus/index.d.ts.map +1 -1
  80. package/dist/FileInput/FileInput.d.ts +10 -2
  81. package/dist/FileInput/FileInput.d.ts.map +1 -1
  82. package/dist/FileInput/FileInput.js +86 -35
  83. package/dist/Item/Item.js +5 -5
  84. package/dist/Item/ItemSwipeLayer.d.ts.map +1 -1
  85. package/dist/Item/ItemSwipeLayer.js +8 -2
  86. package/dist/Layer/useTouchTrigger.d.ts +35 -3
  87. package/dist/Layer/useTouchTrigger.d.ts.map +1 -1
  88. package/dist/Layer/useTouchTrigger.js +80 -11
  89. package/dist/Link/Link.d.ts.map +1 -1
  90. package/dist/Link/Link.js +10 -1
  91. package/dist/Markdown/Markdown.d.ts.map +1 -1
  92. package/dist/Markdown/Markdown.js +1 -0
  93. package/dist/MetadataList/MetadataList.d.ts.map +1 -1
  94. package/dist/MetadataList/MetadataList.js +2 -1
  95. package/dist/MetadataList/MetadataListItem.d.ts.map +1 -1
  96. package/dist/MetadataList/MetadataListItem.js +2 -1
  97. package/dist/MultiSelector/MultiSelector.d.ts +2 -2
  98. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  99. package/dist/MultiSelector/MultiSelector.js +35 -25
  100. package/dist/MultiSelector/hooks.d.ts.map +1 -1
  101. package/dist/MultiSelector/hooks.js +4 -2
  102. package/dist/NumberInput/NumberInput.d.ts +3 -2
  103. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  104. package/dist/NumberInput/NumberInput.js +11 -15
  105. package/dist/OverflowList/OverflowList.d.ts.map +1 -1
  106. package/dist/OverflowList/OverflowList.js +2 -1
  107. package/dist/Pagination/Pagination.d.ts.map +1 -1
  108. package/dist/Popover/Popover.js +2 -2
  109. package/dist/Popover/usePopover.d.ts +7 -1
  110. package/dist/Popover/usePopover.d.ts.map +1 -1
  111. package/dist/Popover/usePopover.js +8 -2
  112. package/dist/PowerSearch/PowerSearch.d.ts +3 -2
  113. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  114. package/dist/PowerSearch/PowerSearch.js +4 -2
  115. package/dist/RadioList/RadioList.d.ts.map +1 -1
  116. package/dist/RadioList/RadioList.js +7 -2
  117. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  118. package/dist/RadioList/RadioListItem.js +8 -2
  119. package/dist/RadioList/RadioListReasonContext.d.ts +2 -0
  120. package/dist/RadioList/RadioListReasonContext.d.ts.map +1 -0
  121. package/dist/RadioList/RadioListReasonContext.js +22 -0
  122. package/dist/Resizable/useResizable.d.ts.map +1 -1
  123. package/dist/Resizable/useResizable.js +53 -10
  124. package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -1
  125. package/dist/SegmentedControl/SegmentedControl.js +22 -5
  126. package/dist/SegmentedControl/SegmentedControlContext.d.ts +6 -0
  127. package/dist/SegmentedControl/SegmentedControlContext.d.ts.map +1 -1
  128. package/dist/SegmentedControl/SegmentedControlItem.d.ts +1 -1
  129. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  130. package/dist/SegmentedControl/SegmentedControlItem.js +16 -3
  131. package/dist/Selector/Selector.d.ts +3 -3
  132. package/dist/Selector/Selector.d.ts.map +1 -1
  133. package/dist/Selector/Selector.js +14 -8
  134. package/dist/Selector/hooks.js +2 -2
  135. package/dist/Selector/useSelectorPresentation.d.ts +7 -1
  136. package/dist/Selector/useSelectorPresentation.d.ts.map +1 -1
  137. package/dist/Selector/useSelectorPresentation.js +2 -1
  138. package/dist/SideNav/railMotion.d.ts +5 -5
  139. package/dist/SideNav/railMotion.d.ts.map +1 -1
  140. package/dist/SideNav/railMotion.js +69 -177
  141. package/dist/Slider/Slider.d.ts.map +1 -1
  142. package/dist/Slider/Slider.js +100 -17
  143. package/dist/Table/BaseTable.d.ts.map +1 -1
  144. package/dist/Table/BaseTable.js +7 -6
  145. package/dist/Table/glidingDivider.d.ts +8 -1
  146. package/dist/Table/glidingDivider.d.ts.map +1 -1
  147. package/dist/Table/glidingDivider.js +8 -3
  148. package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
  149. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts.map +1 -1
  150. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.js +1 -2
  151. package/dist/TextArea/TextArea.d.ts +2 -2
  152. package/dist/TextArea/TextArea.d.ts.map +1 -1
  153. package/dist/TextInput/TextInput.d.ts +3 -2
  154. package/dist/TextInput/TextInput.d.ts.map +1 -1
  155. package/dist/TimeInput/TimeInput.d.ts +3 -2
  156. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  157. package/dist/Timer/Timer.js +1 -1
  158. package/dist/Toast/useToast.d.ts.map +1 -1
  159. package/dist/Toast/useToast.js +8 -2
  160. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  161. package/dist/ToggleButton/ToggleButton.js +11 -3
  162. package/dist/Token/Token.d.ts +1 -1
  163. package/dist/Token/Token.d.ts.map +1 -1
  164. package/dist/Token/Token.js +15 -0
  165. package/dist/Tokenizer/Tokenizer.d.ts +3 -2
  166. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  167. package/dist/Tokenizer/Tokenizer.js +73 -50
  168. package/dist/Tokenizer/TokenizerBottomSheet.d.ts +1 -1
  169. package/dist/Tokenizer/TokenizerBottomSheet.d.ts.map +1 -1
  170. package/dist/Tokenizer/TokenizerBottomSheet.js +9 -15
  171. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  172. package/dist/TopNav/TopNavMegaMenu.js +4 -3
  173. package/dist/TreeList/TreeList.d.ts.map +1 -1
  174. package/dist/TreeList/TreeList.js +6 -3
  175. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  176. package/dist/TreeList/TreeListItem.js +4 -3
  177. package/dist/Typeahead/BaseTypeahead.d.ts +4 -1
  178. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  179. package/dist/Typeahead/BaseTypeahead.js +262 -43
  180. package/dist/Typeahead/Typeahead.d.ts +3 -2
  181. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  182. package/dist/Typeahead/Typeahead.js +36 -10
  183. package/dist/Typeahead/index.d.ts +1 -1
  184. package/dist/Typeahead/index.d.ts.map +1 -1
  185. package/dist/Typeahead/types.d.ts +26 -3
  186. package/dist/Typeahead/types.d.ts.map +1 -1
  187. package/dist/astryx.css +1 -2
  188. package/dist/gestures/haptics.d.ts.map +1 -1
  189. package/dist/gestures/haptics.js +1 -5
  190. package/dist/hooks/menuPressMarker.d.ts +13 -0
  191. package/dist/hooks/menuPressMarker.d.ts.map +1 -0
  192. package/dist/hooks/menuPressMarker.js +15 -0
  193. package/dist/hooks/useClickableContainer.d.ts +24 -4
  194. package/dist/hooks/useClickableContainer.d.ts.map +1 -1
  195. package/dist/hooks/useClickableContainer.js +41 -14
  196. package/dist/hooks/useFocusReturnVisibility.d.ts +8 -1
  197. package/dist/hooks/useFocusReturnVisibility.d.ts.map +1 -1
  198. package/dist/hooks/useFocusReturnVisibility.js +14 -8
  199. package/dist/hooks/useInputStatusIcon.d.ts +2 -2
  200. package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
  201. package/dist/hooks/useInputStatusIcon.js +1 -2
  202. package/dist/hooks/useListFocus.d.ts +19 -0
  203. package/dist/hooks/useListFocus.d.ts.map +1 -1
  204. package/dist/hooks/useListFocus.js +26 -1
  205. package/dist/hooks/useMenuPress.d.ts +2 -2
  206. package/dist/hooks/useMenuPress.d.ts.map +1 -1
  207. package/dist/hooks/useMenuPress.js +5 -6
  208. package/dist/hooks/useTreeFocus.d.ts.map +1 -1
  209. package/dist/hooks/useTreeFocus.js +13 -5
  210. package/dist/hooks/useTypeahead.d.ts +3 -1
  211. package/dist/hooks/useTypeahead.d.ts.map +1 -1
  212. package/dist/hooks/useTypeahead.js +21 -15
  213. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  214. package/dist/i18n/generated-locales/en.generated.js +6 -0
  215. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  216. package/dist/i18n/generated-locales/pseudo.generated.js +6 -0
  217. package/dist/i18n/useListFormatter.d.ts +9 -0
  218. package/dist/i18n/useListFormatter.d.ts.map +1 -0
  219. package/dist/i18n/useListFormatter.js +41 -0
  220. package/dist/motion/animatedLayout.d.ts +130 -5
  221. package/dist/motion/animatedLayout.d.ts.map +1 -1
  222. package/dist/motion/animatedLayout.js +771 -44
  223. package/dist/motion/animator.d.ts.map +1 -1
  224. package/dist/motion/animator.js +38 -2
  225. package/dist/privateAttributes/segmentDisabled.stylex.d.ts +5 -0
  226. package/dist/privateAttributes/segmentDisabled.stylex.d.ts.map +1 -0
  227. package/dist/privateAttributes/segmentDisabled.stylex.js +28 -0
  228. package/dist/theme/Theme.d.ts +10 -0
  229. package/dist/theme/Theme.d.ts.map +1 -1
  230. package/dist/theme/Theme.js +8 -4
  231. package/dist/theme/useTheme.d.ts.map +1 -1
  232. package/dist/theme/useTheme.js +7 -1
  233. package/dist/utils/focusReturn.d.ts +5 -3
  234. package/dist/utils/focusReturn.d.ts.map +1 -1
  235. package/dist/utils/focusReturn.js +6 -4
  236. package/dist/utils/getNodeText.d.ts.map +1 -1
  237. package/dist/utils/getNodeText.js +2 -1
  238. package/dist/utils/interactionModality.d.ts +78 -7
  239. package/dist/utils/interactionModality.d.ts.map +1 -1
  240. package/dist/utils/interactionModality.js +125 -42
  241. package/dist/utils/pressDisabled.d.ts +21 -0
  242. package/dist/utils/pressDisabled.d.ts.map +1 -0
  243. package/dist/utils/pressDisabled.js +32 -0
  244. package/dist/utils/pressFeedback.d.ts.map +1 -1
  245. package/dist/utils/pressFeedback.js +5 -3
  246. package/dist/utils/typeaheadMatch.d.ts +24 -0
  247. package/dist/utils/typeaheadMatch.d.ts.map +1 -0
  248. package/dist/utils/typeaheadMatch.js +112 -0
  249. package/locales/en.json +25 -1
  250. package/locales/pseudo.json +18 -0
  251. package/package.json +19 -5
  252. package/src/AppShell/AppShell.tsx +0 -9
  253. package/src/Badge/Badge.tsx +2 -1
  254. package/src/BottomSheet/BottomSheet.tsx +4 -2
  255. package/src/BottomSheet/BottomSheetPanel.tsx +39 -8
  256. package/src/BottomSheet/BottomSheetSwitcher.tsx +1 -1
  257. package/src/BottomSheet/useMobileKeyboard.ts +51 -10
  258. package/src/BottomSheet/useSheetGestures.ts +92 -15
  259. package/src/Breadcrumbs/BreadcrumbItem.tsx +2 -1
  260. package/src/Center/Center.tsx +1 -1
  261. package/src/Chat/ChatComposer.tsx +7 -10
  262. package/src/Chat/ChatComposerDrawer.tsx +1 -1
  263. package/src/Chat/ChatComposerInput.tsx +1 -1
  264. package/src/Chat/ChatLayout.tsx +1 -6
  265. package/src/Chat/ChatMessageList.tsx +1 -0
  266. package/src/Chat/ChatPastedTextToken.tsx +14 -4
  267. package/src/Chat/ChatSystemMessage.tsx +4 -1
  268. package/src/Chat/ChatToolCalls.tsx +5 -0
  269. package/src/CheckboxList/CheckboxList.tsx +7 -1
  270. package/src/CheckboxList/CheckboxListContext.tsx +8 -0
  271. package/src/CheckboxList/CheckboxListItem.tsx +8 -2
  272. package/src/Code/Code.tsx +1 -1
  273. package/src/CommandPalette/CommandPaletteInput.tsx +5 -8
  274. package/src/ComplexSelector/ComplexSelector.tsx +22 -4
  275. package/src/DateInput/DateInput.tsx +2 -2
  276. package/src/DateInput/MonthScroller.tsx +15 -17
  277. package/src/DateRangeInput/DateRangeInput.tsx +20 -5
  278. package/src/Dialog/Dialog.tsx +9 -25
  279. package/src/Drawer/Drawer.tsx +1 -1
  280. package/src/DropdownMenu/DropdownMenu.tsx +24 -9
  281. package/src/DropdownMenu/DropdownMenuGroup.tsx +2 -1
  282. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +3 -1
  283. package/src/DropdownMenu/MenuBottomSheetActionList.tsx +6 -3
  284. package/src/Field/Field.doc.mjs +2 -1
  285. package/src/Field/Field.tsx +2 -2
  286. package/src/Field/FieldLabel.doc.mjs +4 -2
  287. package/src/Field/FieldLabel.tsx +18 -2
  288. package/src/Field/PanelSearchInput.tsx +7 -8
  289. package/src/Field/fieldStatusPlacement.ts +18 -0
  290. package/src/FieldStatus/FieldStatus.tsx +33 -3
  291. package/src/FieldStatus/index.ts +7 -0
  292. package/src/FileInput/FileInput.doc.mjs +3 -3
  293. package/src/FileInput/FileInput.tsx +103 -36
  294. package/src/HoverCard/HoverCard.doc.mjs +3 -3
  295. package/src/HoverCard/useHoverCard.doc.mjs +1 -1
  296. package/src/Item/Item.tsx +5 -5
  297. package/src/Item/ItemSwipeLayer.tsx +8 -1
  298. package/src/Layer/useTouchTrigger.ts +107 -18
  299. package/src/Link/Link.tsx +9 -2
  300. package/src/Markdown/Markdown.tsx +1 -0
  301. package/src/MetadataList/MetadataList.tsx +4 -2
  302. package/src/MetadataList/MetadataListItem.tsx +2 -1
  303. package/src/MultiSelector/MultiSelector.tsx +57 -36
  304. package/src/MultiSelector/hooks.ts +7 -5
  305. package/src/NumberInput/NumberInput.tsx +13 -20
  306. package/src/OverflowList/OverflowList.tsx +2 -1
  307. package/src/Pagination/Pagination.tsx +1 -0
  308. package/src/Popover/Popover.tsx +2 -2
  309. package/src/Popover/usePopover.tsx +7 -2
  310. package/src/PowerSearch/PowerSearch.tsx +4 -2
  311. package/src/RadioList/RadioList.tsx +13 -2
  312. package/src/RadioList/RadioListItem.tsx +9 -1
  313. package/src/RadioList/RadioListReasonContext.ts +25 -0
  314. package/src/Resizable/useResizable.ts +50 -7
  315. package/src/SegmentedControl/SegmentedControl.tsx +43 -8
  316. package/src/SegmentedControl/SegmentedControlContext.ts +6 -0
  317. package/src/SegmentedControl/SegmentedControlItem.tsx +12 -1
  318. package/src/Selector/Selector.tsx +23 -9
  319. package/src/Selector/hooks.ts +2 -2
  320. package/src/Selector/useSelectorPresentation.ts +8 -1
  321. package/src/SideNav/railMotion.ts +73 -179
  322. package/src/Slider/Slider.tsx +116 -19
  323. package/src/Table/BaseTable.tsx +10 -6
  324. package/src/Table/glidingDivider.ts +8 -3
  325. package/src/Table/plugins/sortable/useTableSortable.tsx +2 -0
  326. package/src/Table/plugins/stickyColumns/useTableStickyColumns.tsx +1 -2
  327. package/src/TextArea/TextArea.tsx +3 -2
  328. package/src/TextInput/TextInput.tsx +2 -2
  329. package/src/TimeInput/TimeInput.tsx +2 -2
  330. package/src/Timer/Timer.tsx +1 -1
  331. package/src/Toast/useToast.tsx +8 -3
  332. package/src/ToggleButton/ToggleButton.tsx +10 -3
  333. package/src/Token/Token.tsx +15 -0
  334. package/src/Tokenizer/Tokenizer.doc.mjs +12 -8
  335. package/src/Tokenizer/Tokenizer.tsx +81 -53
  336. package/src/Tokenizer/TokenizerBottomSheet.tsx +9 -15
  337. package/src/Tooltip/Tooltip.doc.mjs +3 -3
  338. package/src/Tooltip/useTooltip.doc.mjs +1 -1
  339. package/src/TopNav/TopNavMegaMenu.tsx +4 -3
  340. package/src/TreeList/TreeList.tsx +6 -3
  341. package/src/TreeList/TreeListItem.tsx +5 -3
  342. package/src/Typeahead/BaseTypeahead.doc.mjs +7 -4
  343. package/src/Typeahead/BaseTypeahead.tsx +314 -40
  344. package/src/Typeahead/Typeahead.doc.mjs +21 -4
  345. package/src/Typeahead/Typeahead.tsx +42 -11
  346. package/src/Typeahead/TypeaheadItem.doc.mjs +6 -3
  347. package/src/Typeahead/index.ts +1 -1
  348. package/src/Typeahead/types.ts +27 -3
  349. package/src/gestures/haptics.ts +1 -8
  350. package/src/hooks/menuPressMarker.ts +15 -0
  351. package/src/hooks/useClickableContainer.ts +48 -17
  352. package/src/hooks/useFocusReturnVisibility.ts +15 -12
  353. package/src/hooks/useInputStatusIcon.tsx +2 -2
  354. package/src/hooks/useListFocus.ts +38 -1
  355. package/src/hooks/useMenuPress.ts +5 -5
  356. package/src/hooks/useTreeFocus.doc.mjs +2 -2
  357. package/src/hooks/useTreeFocus.ts +16 -10
  358. package/src/hooks/useTypeahead.doc.mjs +2 -2
  359. package/src/hooks/useTypeahead.ts +21 -20
  360. package/src/i18n/generated-locales/en.generated.ts +8 -0
  361. package/src/i18n/generated-locales/pseudo.generated.ts +8 -0
  362. package/src/i18n/useListFormatter.ts +42 -0
  363. package/src/motion/animatedLayout.ts +977 -45
  364. package/src/motion/animator.ts +55 -2
  365. package/src/privateAttributes/segmentDisabled.stylex.ts +28 -0
  366. package/src/theme/Theme.tsx +8 -4
  367. package/src/theme/useTheme.ts +6 -1
  368. package/src/utils/focusReturn.ts +6 -4
  369. package/src/utils/getNodeText.ts +2 -1
  370. package/src/utils/interactionModality.ts +165 -44
  371. package/src/utils/pressDisabled.ts +32 -0
  372. package/src/utils/pressFeedback.ts +5 -3
  373. package/src/utils/typeaheadMatch.ts +141 -0
  374. package/dist/Item/ItemMarkerColumns.a11y.chromium.spec.d.ts +0 -2
  375. package/dist/Item/ItemMarkerColumns.a11y.chromium.spec.js +0 -132
  376. package/dist/Lightbox/LightboxKeyboard.a11y.browser.spec.d.ts +0 -2
  377. package/dist/Lightbox/LightboxKeyboard.a11y.browser.spec.js +0 -78
  378. package/dist/Lightbox/LightboxViewport.a11y.browser.spec.d.ts +0 -2
  379. package/dist/Lightbox/LightboxViewport.a11y.browser.spec.js +0 -66
  380. package/dist/List/ListMarkers.a11y.chromium.spec.d.ts +0 -2
  381. package/dist/List/ListMarkers.a11y.chromium.spec.js +0 -677
  382. package/dist/Markdown/Markdown.fr23.bench.d.ts +0 -2
  383. package/dist/Markdown/Markdown.fr23.bench.d.ts.map +0 -1
  384. package/dist/Markdown/Markdown.fr23.bench.js +0 -152
  385. package/dist/Markdown/MarkdownTable.a11y.chromium.spec.d.ts +0 -2
  386. package/dist/Markdown/MarkdownTable.a11y.chromium.spec.js +0 -590
  387. package/dist/Markdown/plugins/headingLinks.a11y.chromium.spec.d.ts +0 -2
  388. package/dist/Markdown/plugins/headingLinks.a11y.chromium.spec.js +0 -657
  389. package/dist/MobileNav/MobileNavPinchZoom.a11y.chromium.spec.d.ts +0 -2
  390. package/dist/MobileNav/MobileNavPinchZoom.a11y.chromium.spec.js +0 -153
  391. package/dist/MultiSelector/CreateRow.a11y.chromium.spec.d.ts +0 -2
  392. package/dist/MultiSelector/CreateRow.a11y.chromium.spec.js +0 -117
  393. package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts +0 -2
  394. package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.js +0 -49
  395. package/dist/MultiSelector/OptionActions.a11y.chromium.spec.d.ts +0 -2
  396. package/dist/MultiSelector/OptionActions.a11y.chromium.spec.js +0 -273
  397. package/dist/Outline/OutlineFocus.a11y.chromium.spec.d.ts +0 -2
  398. package/dist/Outline/OutlineFocus.a11y.chromium.spec.js +0 -421
  399. package/dist/Typeahead/BaseTypeahead.emptyState.a11y.chromium.spec.d.ts +0 -2
  400. package/dist/Typeahead/BaseTypeahead.emptyState.a11y.chromium.spec.js +0 -118
  401. package/dist/reset.a11y.chromium.spec.d.ts +0 -2
  402. package/dist/reset.a11y.chromium.spec.js +0 -231
  403. package/dist/reset.until-found.a11y.browser.spec.d.ts +0 -2
  404. package/dist/reset.until-found.a11y.browser.spec.js +0 -72
  405. package/src/AlertDialog/AlertDialog.spec.md +0 -259
  406. package/src/AlertDialog/AlertDialog.test.tsx +0 -519
  407. package/src/AlertDialog/__tests__/AlertDialog.a11y.chromium.spec.ts +0 -128
  408. package/src/AlertDialog/__tests__/AlertDialog.a11y.renders.tsx +0 -52
  409. package/src/AlertDialog/__tests__/AlertDialog.a11y.states.ts +0 -68
  410. package/src/AlertDialog/__tests__/AlertDialog.a11y.test.tsx +0 -144
  411. package/src/AlertDialog/useImperativeAlertDialog.test.tsx +0 -163
  412. package/src/AppShell/AppShell.spec.md +0 -331
  413. package/src/AppShell/AppShell.test.tsx +0 -1310
  414. package/src/AppShell/__tests__/AppShellMainKeyboard.a11y.browser.spec.ts +0 -534
  415. package/src/AppShell/observeMainOverflow.test.ts +0 -173
  416. package/src/AspectRatio/AspectRatio.spec.md +0 -179
  417. package/src/AspectRatio/AspectRatio.test.tsx +0 -398
  418. package/src/AspectRatio/__tests__/AspectRatioFitLayers.a11y.browser.spec.ts +0 -158
  419. package/src/Avatar/Avatar.spec.md +0 -253
  420. package/src/Avatar/Avatar.test.tsx +0 -867
  421. package/src/Avatar/AvatarStatusDot.spec.md +0 -208
  422. package/src/Avatar/AvatarStatusDot.test.tsx +0 -306
  423. package/src/Avatar/__tests__/AvatarForcedColors.a11y.chromium.spec.ts +0 -126
  424. package/src/AvatarGroup/AvatarGroup.spec.md +0 -205
  425. package/src/AvatarGroup/AvatarGroup.test.tsx +0 -313
  426. package/src/AvatarGroup/AvatarGroupOverflow.spec.md +0 -210
  427. package/src/AvatarGroup/AvatarGroupOverflow.test.tsx +0 -263
  428. package/src/Badge/Badge.spec.md +0 -207
  429. package/src/Badge/Badge.test.tsx +0 -125
  430. package/src/Badge/__tests__/BadgeLargerText.a11y.chromium.spec.ts +0 -260
  431. package/src/Banner/Banner.spec.md +0 -282
  432. package/src/Banner/Banner.test.tsx +0 -879
  433. package/src/Blockquote/Blockquote.spec.md +0 -171
  434. package/src/Blockquote/Blockquote.test.tsx +0 -121
  435. package/src/BottomSheet/BottomSheet.spec.md +0 -457
  436. package/src/BottomSheet/BottomSheet.test.tsx +0 -3154
  437. package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +0 -248
  438. package/src/BottomSheet/BottomSheetKeyboard.test.tsx +0 -195
  439. package/src/BottomSheet/BottomSheetPanel.test.tsx +0 -549
  440. package/src/BottomSheet/BottomSheetReplace.test.tsx +0 -1638
  441. package/src/BottomSheet/BottomSheetStacking.announce.test.tsx +0 -246
  442. package/src/BottomSheet/BottomSheetStacking.test.tsx +0 -3184
  443. package/src/BottomSheet/BottomSheetSwitcher.spec.md +0 -367
  444. package/src/BottomSheet/BottomSheetSwitcher.test.tsx +0 -2010
  445. package/src/BottomSheet/__tests__/BottomSheetDrag.a11y.chromium.spec.ts +0 -129
  446. package/src/BottomSheet/__tests__/BottomSheetFocusReturn.a11y.browser.spec.ts +0 -97
  447. package/src/BottomSheet/__tests__/BottomSheetKeyboard.a11y.browser.spec.ts +0 -415
  448. package/src/BottomSheet/__tests__/BottomSheetNativeModal.a11y.browser.spec.ts +0 -462
  449. package/src/BottomSheet/__tests__/BottomSheetPinchZoom.a11y.chromium.spec.ts +0 -247
  450. package/src/BottomSheet/__tests__/BottomSheetToastOutlet.a11y.chromium.spec.ts +0 -83
  451. package/src/BottomSheet/snapOffsets.test.ts +0 -254
  452. package/src/BottomSheet/useSheetGestures.test.ts +0 -1690
  453. package/src/Breadcrumbs/BreadcrumbItem.spec.md +0 -269
  454. package/src/Breadcrumbs/Breadcrumbs.spec.md +0 -165
  455. package/src/Breadcrumbs/Breadcrumbs.test.tsx +0 -921
  456. package/src/Breadcrumbs/__tests__/Breadcrumbs.a11y.chromium.spec.ts +0 -132
  457. package/src/Breadcrumbs/__tests__/Breadcrumbs.a11y.renders.tsx +0 -45
  458. package/src/Breadcrumbs/__tests__/Breadcrumbs.a11y.states.ts +0 -81
  459. package/src/Breadcrumbs/__tests__/Breadcrumbs.a11y.test.tsx +0 -125
  460. package/src/Button/Button.spec.md +0 -322
  461. package/src/Button/Button.test.tsx +0 -864
  462. package/src/Button/__tests__/Button.a11y.chromium.spec.ts +0 -355
  463. package/src/Button/__tests__/Button.a11y.harnesses.tsx +0 -47
  464. package/src/Button/__tests__/Button.a11y.known-failures.ts +0 -25
  465. package/src/Button/__tests__/Button.a11y.renders.tsx +0 -139
  466. package/src/Button/__tests__/Button.a11y.states.ts +0 -317
  467. package/src/Button/__tests__/Button.a11y.test.tsx +0 -187
  468. package/src/ButtonGroup/ButtonGroup.spec.md +0 -220
  469. package/src/ButtonGroup/ButtonGroup.test.tsx +0 -1025
  470. package/src/Calendar/Calendar.spec.md +0 -248
  471. package/src/Calendar/Calendar.test.tsx +0 -2186
  472. package/src/Calendar/__tests__/Calendar.a11y.chromium.spec.ts +0 -141
  473. package/src/Calendar/__tests__/Calendar.a11y.known-failures.ts +0 -12
  474. package/src/Calendar/__tests__/Calendar.a11y.renders.tsx +0 -57
  475. package/src/Calendar/__tests__/Calendar.a11y.states.ts +0 -139
  476. package/src/Calendar/__tests__/Calendar.a11y.test.tsx +0 -137
  477. package/src/Calendar/dayCellUtils.test.ts +0 -520
  478. package/src/Calendar/getInitialFocusDate.test.ts +0 -94
  479. package/src/Calendar/getStandaloneShortWeekdayNames.test.ts +0 -54
  480. package/src/Calendar/hooks/useCalendarConstraints.test.ts +0 -153
  481. package/src/Calendar/hooks/useCalendarDays.test.ts +0 -265
  482. package/src/Calendar/hooks/useCalendarNavigation.test.ts +0 -252
  483. package/src/Card/Card.spec.md +0 -205
  484. package/src/Card/Card.test.tsx +0 -202
  485. package/src/Carousel/Carousel.spec.md +0 -146
  486. package/src/Carousel/Carousel.test.tsx +0 -1036
  487. package/src/Carousel/__tests__/Carousel.a11y.chromium.spec.ts +0 -207
  488. package/src/Carousel/__tests__/Carousel.a11y.known-failures.ts +0 -12
  489. package/src/Carousel/__tests__/Carousel.a11y.renders.tsx +0 -39
  490. package/src/Carousel/__tests__/Carousel.a11y.states.ts +0 -139
  491. package/src/Carousel/__tests__/Carousel.a11y.test.tsx +0 -127
  492. package/src/Center/Center.spec.md +0 -205
  493. package/src/Center/Center.test.tsx +0 -406
  494. package/src/Chat/ChatComposer.spec.md +0 -246
  495. package/src/Chat/ChatComposer.test.tsx +0 -364
  496. package/src/Chat/ChatComposerDrawer.spec.md +0 -267
  497. package/src/Chat/ChatComposerDrawer.test.tsx +0 -130
  498. package/src/Chat/ChatComposerInput.spec.md +0 -224
  499. package/src/Chat/ChatComposerInput.test.tsx +0 -2136
  500. package/src/Chat/ChatComposerTokenElement.spec.md +0 -164
  501. package/src/Chat/ChatComposerTokenElement.test.tsx +0 -85
  502. package/src/Chat/ChatDictationButton.spec.md +0 -205
  503. package/src/Chat/ChatDictationButton.test.tsx +0 -108
  504. package/src/Chat/ChatLayout.spec.md +0 -276
  505. package/src/Chat/ChatLayout.test.tsx +0 -311
  506. package/src/Chat/ChatLayoutScrollButton.spec.md +0 -322
  507. package/src/Chat/ChatLayoutScrollButton.test.tsx +0 -99
  508. package/src/Chat/ChatMessage.spec.md +0 -265
  509. package/src/Chat/ChatMessage.test.tsx +0 -344
  510. package/src/Chat/ChatMessageBubble.spec.md +0 -263
  511. package/src/Chat/ChatMessageBubble.test.tsx +0 -294
  512. package/src/Chat/ChatMessageList.spec.md +0 -222
  513. package/src/Chat/ChatMessageList.test.tsx +0 -198
  514. package/src/Chat/ChatMessageMetadata.spec.md +0 -220
  515. package/src/Chat/ChatMessageMetadata.test.tsx +0 -66
  516. package/src/Chat/ChatPastedTextToken.test.tsx +0 -71
  517. package/src/Chat/ChatSendButton.spec.md +0 -238
  518. package/src/Chat/ChatSendButton.test.tsx +0 -309
  519. package/src/Chat/ChatSystemMessage.spec.md +0 -224
  520. package/src/Chat/ChatSystemMessage.test.tsx +0 -189
  521. package/src/Chat/ChatTokenizedText.spec.md +0 -190
  522. package/src/Chat/ChatTokenizedText.test.tsx +0 -163
  523. package/src/Chat/ChatToolCalls.spec.md +0 -194
  524. package/src/Chat/ChatToolCalls.test.tsx +0 -413
  525. package/src/Chat/__tests__/ChatLayoutScrollButton.a11y.chromium.spec.ts +0 -577
  526. package/src/Chat/__tests__/ChatLayoutScrollButtonSurface.a11y.chromium.spec.ts +0 -740
  527. package/src/Chat/__tests__/ChatMessageBubble.a11y.chromium.spec.ts +0 -457
  528. package/src/Chat/__tests__/ChatMessageBubble.auditProvenance.test.ts +0 -91
  529. package/src/Chat/__tests__/ChatMessageBubble.auditProvenance.ts +0 -91
  530. package/src/Chat/__tests__/ChatMessageList.a11y.chromium.spec.ts +0 -465
  531. package/src/Chat/__tests__/ChatMessageMetadata.a11y.chromium.spec.ts +0 -416
  532. package/src/Chat/__tests__/ChatSendButton.a11y.chromium.spec.ts +0 -1523
  533. package/src/Chat/__tests__/ChatSystemMessage.a11y.chromium.spec.ts +0 -1004
  534. package/src/Chat/__tests__/ChatTokenizedText.a11y.chromium.spec.ts +0 -574
  535. package/src/Chat/__tests__/useChatStreamScroll.a11y.chromium.spec.ts +0 -263
  536. package/src/Chat/__tests__/useChatStreamScroll.fixture.tsx +0 -61
  537. package/src/Chat/useChatDictation.test.tsx +0 -369
  538. package/src/Chat/useChatNewMessages.test.tsx +0 -209
  539. package/src/Chat/useChatPasteAsToken.test.ts +0 -191
  540. package/src/Chat/useChatStreamScroll.test.tsx +0 -381
  541. package/src/Chat/useTriggerMenu.test.tsx +0 -994
  542. package/src/CheckboxInput/CheckboxInput.spec.md +0 -218
  543. package/src/CheckboxInput/CheckboxInput.test.tsx +0 -926
  544. package/src/CheckboxInput/__tests__/Checkbox.a11y.chromium.spec.ts +0 -354
  545. package/src/CheckboxInput/__tests__/Checkbox.a11y.known-failures.ts +0 -54
  546. package/src/CheckboxInput/__tests__/Checkbox.a11y.renders.tsx +0 -310
  547. package/src/CheckboxInput/__tests__/Checkbox.a11y.states.ts +0 -404
  548. package/src/CheckboxInput/__tests__/Checkbox.a11y.test.tsx +0 -128
  549. package/src/CheckboxList/CheckboxList.fr10.test.tsx +0 -51
  550. package/src/CheckboxList/CheckboxList.spec.md +0 -332
  551. package/src/CheckboxList/CheckboxList.test.tsx +0 -1159
  552. package/src/CheckboxList/CheckboxListItem.spec.md +0 -302
  553. package/src/CheckboxList/__tests__/CheckboxList.a11y.chromium.spec.ts +0 -1998
  554. package/src/Citation/Citation.spec.md +0 -179
  555. package/src/Citation/Citation.test.tsx +0 -361
  556. package/src/ClickableCard/ClickableCard.spec.md +0 -241
  557. package/src/ClickableCard/ClickableCard.test.tsx +0 -382
  558. package/src/Code/Code.spec.md +0 -170
  559. package/src/Code/Code.test.tsx +0 -75
  560. package/src/CodeBlock/CodeBlock.highlightStyles.test.tsx +0 -87
  561. package/src/CodeBlock/CodeBlock.spec.md +0 -243
  562. package/src/CodeBlock/CodeBlock.test.tsx +0 -752
  563. package/src/CodeBlock/CodeLanguage.test.tsx +0 -173
  564. package/src/CodeBlock/__tests__/CodeBlockCopyClearance.a11y.chromium.spec.ts +0 -325
  565. package/src/CodeBlock/__tests__/CodeBlockLanguages.a11y.chromium.spec.ts +0 -64
  566. package/src/CodeBlock/__tests__/CodeBlockOverflowStop.a11y.chromium.spec.ts +0 -126
  567. package/src/CodeBlock/__tests__/DiffRowPaint.a11y.chromium.spec.ts +0 -94
  568. package/src/CodeBlock/highlightRanges.test.ts +0 -253
  569. package/src/CodeBlock/languages.test.ts +0 -402
  570. package/src/CodeBlock/tokenizer.test.ts +0 -280
  571. package/src/CodeBlock/tokenizerBudget.test.ts +0 -112
  572. package/src/Collapsible/Collapsible.spec.md +0 -247
  573. package/src/Collapsible/Collapsible.test.tsx +0 -620
  574. package/src/Collapsible/CollapsibleGroup.spec.md +0 -229
  575. package/src/Collapsible/CollapsibleGroup.test.tsx +0 -656
  576. package/src/Collapsible/__tests__/Collapsible.a11y.chromium.spec.ts +0 -194
  577. package/src/Collapsible/__tests__/Collapsible.a11y.states.ts +0 -117
  578. package/src/Collapsible/__tests__/Collapsible.a11y.test.tsx +0 -140
  579. package/src/Collapsible/__tests__/CollapsibleGroup.a11y.chromium.spec.ts +0 -559
  580. package/src/Collapsible/__tests__/CollapsibleGroup.a11y.renders.tsx +0 -45
  581. package/src/Collapsible/__tests__/CollapsibleGroup.a11y.states.ts +0 -139
  582. package/src/Collapsible/__tests__/CollapsibleGroup.a11y.test.tsx +0 -135
  583. package/src/Collapsible/__tests__/CollapsibleGroupAccordion.a11y.chromium.spec.ts +0 -141
  584. package/src/Collapsible/useCollapsible.test.tsx +0 -236
  585. package/src/CommandPalette/CommandPalette.perf.test.ts +0 -100
  586. package/src/CommandPalette/CommandPalette.spec.md +0 -278
  587. package/src/CommandPalette/CommandPalette.test.tsx +0 -750
  588. package/src/CommandPalette/CommandPaletteEmpty.spec.md +0 -190
  589. package/src/CommandPalette/CommandPaletteEmpty.test.tsx +0 -101
  590. package/src/CommandPalette/CommandPaletteFooter.test.tsx +0 -122
  591. package/src/CommandPalette/CommandPaletteGroup.test.tsx +0 -129
  592. package/src/CommandPalette/CommandPaletteInput.spec.md +0 -215
  593. package/src/CommandPalette/CommandPaletteInput.test.tsx +0 -539
  594. package/src/CommandPalette/CommandPaletteItem.spec.md +0 -223
  595. package/src/CommandPalette/CommandPaletteItem.test.tsx +0 -417
  596. package/src/CommandPalette/CommandPaletteList.spec.md +0 -194
  597. package/src/CommandPalette/CommandPaletteList.test.tsx +0 -184
  598. package/src/CommandPalette/__tests__/CommandPalette.a11y.chromium.spec.ts +0 -600
  599. package/src/CommandPalette/__tests__/CommandPaletteEmptyAudit.a11y.chromium.spec.ts +0 -715
  600. package/src/CommandPalette/__tests__/CommandPaletteFooter.a11y.chromium.spec.ts +0 -582
  601. package/src/CommandPalette/__tests__/CommandPaletteInputAudit.a11y.chromium.spec.ts +0 -336
  602. package/src/CommandPalette/__tests__/CommandPaletteItemAudit.a11y.chromium.spec.ts +0 -523
  603. package/src/ComplexSelector/ComplexSelector.focusableDisabled.test.tsx +0 -76
  604. package/src/ComplexSelector/ComplexSelector.fr10.test.tsx +0 -262
  605. package/src/ComplexSelector/ComplexSelector.spec.md +0 -266
  606. package/src/ComplexSelector/ComplexSelector.test.tsx +0 -1171
  607. package/src/ComplexSelector/__tests__/ComplexSelector.a11y.chromium.spec.ts +0 -965
  608. package/src/ContextMenu/ContextMenu.spec.md +0 -279
  609. package/src/ContextMenu/ContextMenu.test.tsx +0 -1617
  610. package/src/ContextMenu/ContextMenu.triggerProps.test.tsx +0 -54
  611. package/src/ContextMenu/ContextMenuItem.spec.md +0 -132
  612. package/src/ContextMenu/__tests__/ContextMenuAudit.a11y.chromium.spec.ts +0 -439
  613. package/src/ContextMenu/__tests__/ContextMenuCursorAnchor.a11y.browser.spec.ts +0 -154
  614. package/src/ContextMenu/__tests__/ContextMenuLongPress.a11y.chromium.spec.ts +0 -117
  615. package/src/DateInput/DateInput.fr10.test.tsx +0 -283
  616. package/src/DateInput/DateInput.spec.md +0 -319
  617. package/src/DateInput/DateInput.test.tsx +0 -1571
  618. package/src/DateInput/DateInputTouch.test.tsx +0 -2948
  619. package/src/DateInput/MonthScrollerSettle.test.tsx +0 -165
  620. package/src/DateInput/NativeDateField.test.tsx +0 -741
  621. package/src/DateInput/Presentation.test.tsx +0 -121
  622. package/src/DateInput/__tests__/DateInputAudit.a11y.chromium.spec.ts +0 -577
  623. package/src/DateInput/__tests__/MonthScrollerSteps.interaction.browser.spec.ts +0 -312
  624. package/src/DateInput/__tests__/WheelPinchZoom.a11y.chromium.spec.ts +0 -171
  625. package/src/DateRangeInput/DateRangeInput.fr10.test.tsx +0 -165
  626. package/src/DateRangeInput/DateRangeInput.spec.md +0 -203
  627. package/src/DateRangeInput/DateRangeInput.test.tsx +0 -1151
  628. package/src/DateTimeInput/DateTimeInput.fr10.test.tsx +0 -319
  629. package/src/DateTimeInput/DateTimeInput.spec.md +0 -270
  630. package/src/DateTimeInput/DateTimeInput.test.tsx +0 -2656
  631. package/src/DateTimeInput/DateTimeInputTouch.test.tsx +0 -1071
  632. package/src/DateTimeInput/NativePickerSegments.test.tsx +0 -657
  633. package/src/DateTimeInput/Presentation.test.tsx +0 -89
  634. package/src/Dialog/Dialog.source-build.test.mjs +0 -43
  635. package/src/Dialog/Dialog.spec.md +0 -189
  636. package/src/Dialog/Dialog.test.tsx +0 -1599
  637. package/src/Dialog/DialogHeader.test.tsx +0 -477
  638. package/src/Dialog/__tests__/Dialog.a11y.browser.spec.ts +0 -111
  639. package/src/Dialog/__tests__/Dialog.a11y.chromium.spec.ts +0 -218
  640. package/src/Dialog/__tests__/Dialog.a11y.known-failures.ts +0 -28
  641. package/src/Dialog/__tests__/Dialog.a11y.renders.tsx +0 -107
  642. package/src/Dialog/__tests__/Dialog.a11y.states.ts +0 -104
  643. package/src/Dialog/__tests__/Dialog.a11y.test.tsx +0 -102
  644. package/src/Dialog/__tests__/Dialog.reduced-motion.a11y.browser.spec.ts +0 -252
  645. package/src/Dialog/__tests__/DialogSecondDocument.a11y.browser.spec.ts +0 -202
  646. package/src/Dialog/modules/DialogHeader.spec.md +0 -211
  647. package/src/Dialog/useImperativeDialog.test.tsx +0 -78
  648. package/src/Divider/Divider.spec.md +0 -252
  649. package/src/Divider/Divider.test.tsx +0 -155
  650. package/src/Drawer/Drawer.spec.md +0 -388
  651. package/src/Drawer/Drawer.test.tsx +0 -1335
  652. package/src/Drawer/DrawerDirection.test.tsx +0 -127
  653. package/src/Drawer/DrawerHeader.spec.md +0 -181
  654. package/src/Drawer/DrawerHeader.test.tsx +0 -102
  655. package/src/Drawer/__tests__/DrawerAudit.a11y.chromium.spec.ts +0 -402
  656. package/src/Drawer/__tests__/DrawerHeaderAudit.a11y.chromium.spec.ts +0 -298
  657. package/src/Drawer/__tests__/DrawerNonModalPageScroll.a11y.chromium.spec.ts +0 -267
  658. package/src/DropdownMenu/DropdownMenu.spec.md +0 -562
  659. package/src/DropdownMenu/DropdownMenu.ssr.test.tsx +0 -93
  660. package/src/DropdownMenu/DropdownMenu.test.tsx +0 -4498
  661. package/src/DropdownMenu/DropdownMenuCheckboxItem.spec.md +0 -192
  662. package/src/DropdownMenu/DropdownMenuContext.public.test.ts +0 -22
  663. package/src/DropdownMenu/DropdownMenuDivider.spec.md +0 -186
  664. package/src/DropdownMenu/DropdownMenuGroup.spec.md +0 -145
  665. package/src/DropdownMenu/DropdownMenuItem.spec.md +0 -215
  666. package/src/DropdownMenu/DropdownMenuLazySheet.test.tsx +0 -126
  667. package/src/DropdownMenu/DropdownMenuRadioGroup.spec.md +0 -187
  668. package/src/DropdownMenu/DropdownMenuRadioItem.spec.md +0 -227
  669. package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +0 -635
  670. package/src/DropdownMenu/DropdownMenuSheetFirstOpen.test.tsx +0 -72
  671. package/src/DropdownMenu/DropdownMenuSubMenu.spec.md +0 -231
  672. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +0 -2442
  673. package/src/DropdownMenu/__tests__/DropdownMenuDividerAudit.a11y.chromium.spec.ts +0 -281
  674. package/src/DropdownMenu/__tests__/DropdownMenuDrillIn.a11y.chromium.spec.ts +0 -175
  675. package/src/DropdownMenu/__tests__/DropdownMenuFocusReturn.a11y.browser.spec.ts +0 -74
  676. package/src/DropdownMenu/__tests__/DropdownMenuGroupAudit.a11y.chromium.spec.ts +0 -335
  677. package/src/DropdownMenu/__tests__/DropdownMenuItemAudit.a11y.chromium.spec.ts +0 -367
  678. package/src/DropdownMenu/__tests__/DropdownMenuRadioGroupAudit.a11y.chromium.spec.ts +0 -328
  679. package/src/DropdownMenu/__tests__/DropdownMenuRadioItemAudit.a11y.chromium.spec.ts +0 -540
  680. package/src/DropdownMenu/__tests__/DropdownMenuSubMenuAudit.a11y.chromium.spec.ts +0 -671
  681. package/src/DropdownMenu/__tests__/Menu.a11y.chromium.spec.ts +0 -158
  682. package/src/DropdownMenu/__tests__/Menu.a11y.known-failures.ts +0 -26
  683. package/src/DropdownMenu/__tests__/Menu.a11y.renders.tsx +0 -65
  684. package/src/DropdownMenu/__tests__/Menu.a11y.states.ts +0 -138
  685. package/src/DropdownMenu/__tests__/Menu.a11y.test.tsx +0 -136
  686. package/src/DropdownMenu/__tests__/MenuButton.a11y.chromium.spec.ts +0 -149
  687. package/src/DropdownMenu/__tests__/MenuButton.a11y.known-failures.ts +0 -12
  688. package/src/DropdownMenu/__tests__/MenuButton.a11y.renders.tsx +0 -67
  689. package/src/DropdownMenu/__tests__/MenuButton.a11y.states.ts +0 -141
  690. package/src/DropdownMenu/__tests__/MenuButton.a11y.test.tsx +0 -138
  691. package/src/DropdownMenu/__tests__/MenuHeaderFooter.a11y.chromium.spec.ts +0 -231
  692. package/src/DropdownMenu/__tests__/MenuHoverFocus.a11y.chromium.spec.ts +0 -238
  693. package/src/DropdownMenu/__tests__/MenuPinchZoom.a11y.chromium.spec.ts +0 -173
  694. package/src/DropdownMenu/__tests__/MenuPress.a11y.chromium.spec.ts +0 -503
  695. package/src/DropdownMenu/__tests__/SubMenuHeaderFooter.a11y.chromium.spec.ts +0 -268
  696. package/src/DropdownMenu/__tests__/warmMenuBottomSheet.tsx +0 -43
  697. package/src/DropdownMenu/lazyMenuBottomSheet.test.ts +0 -60
  698. package/src/DropdownMenu/modules/useMenuHover.spec.md +0 -283
  699. package/src/DropdownMenu/modules/useMenuPress.spec.md +0 -348
  700. package/src/EmptyState/EmptyState.announce.test.tsx +0 -227
  701. package/src/EmptyState/EmptyState.spec.md +0 -203
  702. package/src/EmptyState/EmptyState.test.tsx +0 -284
  703. package/src/Field/Field.spec.md +0 -226
  704. package/src/Field/Field.test.tsx +0 -800
  705. package/src/Field/FieldLabel.spec.md +0 -183
  706. package/src/Field/FieldLabel.test.tsx +0 -244
  707. package/src/Field/InputClearButton.test.tsx +0 -395
  708. package/src/Field/__tests__/FieldLabelAudit.a11y.chromium.spec.ts +0 -297
  709. package/src/Field/fieldInputFamily.test.tsx +0 -160
  710. package/src/FieldStatus/FieldStatus.spec.md +0 -224
  711. package/src/FieldStatus/FieldStatus.test.tsx +0 -482
  712. package/src/FieldStatus/__tests__/StatusMessage.a11y.chromium.spec.ts +0 -206
  713. package/src/FieldStatus/__tests__/StatusMessage.a11y.known-failures.ts +0 -13
  714. package/src/FieldStatus/__tests__/StatusMessage.a11y.renders.tsx +0 -287
  715. package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +0 -302
  716. package/src/FieldStatus/__tests__/StatusMessage.a11y.test.tsx +0 -154
  717. package/src/FileInput/FileInput.fr10.test.tsx +0 -21
  718. package/src/FileInput/FileInput.spec.md +0 -199
  719. package/src/FileInput/FileInput.test.tsx +0 -1291
  720. package/src/FormLayout/FormLayout.spec.md +0 -209
  721. package/src/FormLayout/FormLayout.test.tsx +0 -444
  722. package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +0 -46
  723. package/src/Grid/Grid.spec.md +0 -193
  724. package/src/Grid/Grid.test.tsx +0 -509
  725. package/src/Grid/GridSpan.spec.md +0 -158
  726. package/src/HStack/HStack.test.tsx +0 -129
  727. package/src/Heading/Heading.spec.md +0 -198
  728. package/src/Heading/Heading.test.tsx +0 -328
  729. package/src/HoverCard/HoverCard.spec.md +0 -244
  730. package/src/HoverCard/HoverCard.test.tsx +0 -1339
  731. package/src/Icon/Icon.spec.md +0 -244
  732. package/src/Icon/Icon.test.tsx +0 -990
  733. package/src/Icon/globalIconRegistry.test.tsx +0 -613
  734. package/src/IconButton/IconButton.spec.md +0 -197
  735. package/src/IconButton/IconButton.test.tsx +0 -156
  736. package/src/Indicator/CheckIndicator.spec.md +0 -164
  737. package/src/Indicator/CheckboxIndicator.spec.md +0 -164
  738. package/src/Indicator/Indicator.spec.md +0 -188
  739. package/src/Indicator/Indicator.test.tsx +0 -389
  740. package/src/Indicator/indicatorRegistry.test.tsx +0 -106
  741. package/src/InputGroup/InputGroup.spec.md +0 -199
  742. package/src/InputGroup/InputGroup.test.tsx +0 -502
  743. package/src/InputGroup/InputGroupText.spec.md +0 -139
  744. package/src/InteractiveRoleContext/InteractiveRoleContext.test.tsx +0 -185
  745. package/src/Item/Item.source-build.test.mjs +0 -88
  746. package/src/Item/Item.spec.md +0 -226
  747. package/src/Item/Item.swipeLayer.test.tsx +0 -74
  748. package/src/Item/Item.test.tsx +0 -1515
  749. package/src/Item/ItemMarkerColumns.a11y.chromium.spec.ts +0 -211
  750. package/src/Item/__tests__/ItemSwipeActions.a11y.chromium.spec.ts +0 -337
  751. package/src/Item/__tests__/ItemSwipePinchZoom.a11y.chromium.spec.ts +0 -186
  752. package/src/Kbd/Kbd.spec.md +0 -185
  753. package/src/Kbd/Kbd.test.tsx +0 -299
  754. package/src/Layer/LayerProvider.spec.md +0 -149
  755. package/src/Layer/LayerProvider.test.tsx +0 -143
  756. package/src/Layer/__tests__/LayerBrowserHide.a11y.chromium.spec.ts +0 -126
  757. package/src/Layer/__tests__/LayerReducedMotion.a11y.chromium.spec.ts +0 -176
  758. package/src/Layer/__tests__/TouchTrigger.a11y.chromium.spec.ts +0 -200
  759. package/src/Layer/anchorName.test.ts +0 -146
  760. package/src/Layer/layerDismissalFamilies.test.tsx +0 -487
  761. package/src/Layer/layerDismissalInvariants.test.tsx +0 -501
  762. package/src/Layer/layerHost.test.ts +0 -117
  763. package/src/Layer/layerScopedContext.test.tsx +0 -499
  764. package/src/Layer/layerViewportInset.source-build.test.mjs +0 -90
  765. package/src/Layer/layerViewportInset.test.ts +0 -120
  766. package/src/Layer/popoverOperations.test.ts +0 -205
  767. package/src/Layer/useLayer.test.tsx +0 -2845
  768. package/src/Layer/useLayerDismissal.test.tsx +0 -580
  769. package/src/Layer/useTouchTrigger.test.ts +0 -70
  770. package/src/Layout/Layout.spec.md +0 -243
  771. package/src/Layout/Layout.test.tsx +0 -375
  772. package/src/Layout/LayoutContent.spec.md +0 -205
  773. package/src/Layout/LayoutFooter.spec.md +0 -190
  774. package/src/Layout/LayoutHeader.spec.md +0 -193
  775. package/src/Layout/LayoutPanel.spec.md +0 -194
  776. package/src/Layout/LayoutSlots.test.tsx +0 -376
  777. package/src/Layout/__tests__/Layout.a11y.chromium.spec.ts +0 -134
  778. package/src/Layout/__tests__/Layout.a11y.renders.tsx +0 -110
  779. package/src/Layout/__tests__/Layout.a11y.states.ts +0 -93
  780. package/src/Layout/__tests__/Layout.a11y.test.tsx +0 -142
  781. package/src/Layout/__tests__/childrenAsContent.test.tsx +0 -59
  782. package/src/Layout/__tests__/contentWidth.test.tsx +0 -303
  783. package/src/Layout/__tests__/edgeCompensation.test.tsx +0 -191
  784. package/src/Layout/__tests__/nestedPadding.test.tsx +0 -264
  785. package/src/Layout/overlayPaddingReset.test.tsx +0 -224
  786. package/src/Lightbox/Lightbox.spec.md +0 -199
  787. package/src/Lightbox/Lightbox.test.tsx +0 -837
  788. package/src/Lightbox/LightboxKeyboard.a11y.browser.spec.ts +0 -78
  789. package/src/Lightbox/LightboxViewport.a11y.browser.spec.ts +0 -74
  790. package/src/Lightbox/useLightbox.test.tsx +0 -263
  791. package/src/Link/Link.spec.md +0 -230
  792. package/src/Link/Link.test.tsx +0 -457
  793. package/src/Link/LinkProvider.spec.md +0 -167
  794. package/src/Link/__tests__/Link.a11y.chromium.spec.ts +0 -153
  795. package/src/Link/__tests__/Link.a11y.renders.tsx +0 -97
  796. package/src/Link/__tests__/Link.a11y.states.ts +0 -104
  797. package/src/Link/__tests__/Link.a11y.test.tsx +0 -127
  798. package/src/Link/__tests__/Link.navigation.a11y.chromium.spec.ts +0 -262
  799. package/src/Link/__tests__/Link.truncation.a11y.chromium.spec.ts +0 -58
  800. package/src/Link/useLinkComponent.test.tsx +0 -572
  801. package/src/Link/useLinkify.test.tsx +0 -146
  802. package/src/List/List.spec.md +0 -336
  803. package/src/List/List.test.tsx +0 -1052
  804. package/src/List/ListItem.source-build.test.mjs +0 -41
  805. package/src/List/ListItem.spec.md +0 -205
  806. package/src/List/ListMarkers.a11y.chromium.spec.ts +0 -904
  807. package/src/Markdown/Markdown.codeLanguages.test.tsx +0 -137
  808. package/src/Markdown/Markdown.fr23.bench.ts +0 -176
  809. package/src/Markdown/Markdown.fr23.perf.test.ts +0 -484
  810. package/src/Markdown/Markdown.helpers.perf.test.ts +0 -495
  811. package/src/Markdown/Markdown.public.test.ts +0 -309
  812. package/src/Markdown/Markdown.renderBoundary.test.tsx +0 -72
  813. package/src/Markdown/Markdown.rerender.test.tsx +0 -143
  814. package/src/Markdown/Markdown.spec.md +0 -595
  815. package/src/Markdown/Markdown.test.tsx +0 -1383
  816. package/src/Markdown/MarkdownTable.a11y.chromium.spec.ts +0 -727
  817. package/src/Markdown/__tests__/MarkdownNesting.a11y.chromium.spec.ts +0 -102
  818. package/src/Markdown/angleDestinations.test.tsx +0 -104
  819. package/src/Markdown/ast.test.ts +0 -374
  820. package/src/Markdown/atxHeadingIndentation.test.tsx +0 -53
  821. package/src/Markdown/blockNesting.test.tsx +0 -124
  822. package/src/Markdown/blockquoteMarkers.test.tsx +0 -69
  823. package/src/Markdown/characterReferences.test.ts +0 -47
  824. package/src/Markdown/codeSpans.test.tsx +0 -65
  825. package/src/Markdown/emphasis.test.tsx +0 -214
  826. package/src/Markdown/fenceInfo.test.tsx +0 -47
  827. package/src/Markdown/imageAlt.test.tsx +0 -84
  828. package/src/Markdown/incremental.test.ts +0 -1198
  829. package/src/Markdown/incrementalIndentation.test.tsx +0 -36
  830. package/src/Markdown/indentedFences.test.tsx +0 -374
  831. package/src/Markdown/indentedTables.test.tsx +0 -53
  832. package/src/Markdown/linkBrackets.test.tsx +0 -116
  833. package/src/Markdown/linkDestinationLinear.test.tsx +0 -54
  834. package/src/Markdown/linkDestinations.test.tsx +0 -127
  835. package/src/Markdown/linkTextCode.test.tsx +0 -65
  836. package/src/Markdown/listBullets.test.tsx +0 -55
  837. package/src/Markdown/listContinuation.test.tsx +0 -94
  838. package/src/Markdown/listThematicBreak.test.tsx +0 -49
  839. package/src/Markdown/longLinkLabels.test.tsx +0 -49
  840. package/src/Markdown/modules/headingLinks.spec.md +0 -262
  841. package/src/Markdown/modules/remark.spec.md +0 -269
  842. package/src/Markdown/modules/softBreaks.spec.md +0 -137
  843. package/src/Markdown/modules/sourceLines.spec.md +0 -162
  844. package/src/Markdown/openFenceFinalNewline.test.tsx +0 -85
  845. package/src/Markdown/parser.perf.test.ts +0 -398
  846. package/src/Markdown/parser.public.test.ts +0 -144
  847. package/src/Markdown/parser.test.ts +0 -1925
  848. package/src/Markdown/plugin-renderer/MarkdownPluginNodeRenderer.test.tsx +0 -300
  849. package/src/Markdown/plugins/frontmatter.test.tsx +0 -150
  850. package/src/Markdown/plugins/headingLinks.a11y.chromium.spec.ts +0 -856
  851. package/src/Markdown/plugins/headingLinks.ssr.test.tsx +0 -143
  852. package/src/Markdown/plugins/headingLinks.test.tsx +0 -637
  853. package/src/Markdown/plugins/protocol.test.tsx +0 -1083
  854. package/src/Markdown/plugins/semanticFence.test.tsx +0 -655
  855. package/src/Markdown/plugins/softBreaks.test.tsx +0 -172
  856. package/src/Markdown/plugins/sourceDecoration.test.tsx +0 -799
  857. package/src/Markdown/plugins/sourceLines.test.tsx +0 -715
  858. package/src/Markdown/plugins/textTransform.test.ts +0 -467
  859. package/src/Markdown/remark.test.tsx +0 -1877
  860. package/src/Markdown/streaming.test.ts +0 -148
  861. package/src/Markdown/streamingListShapes.test.tsx +0 -93
  862. package/src/Markdown/trailingSpaces.test.tsx +0 -45
  863. package/src/MetadataList/MetadataList.spec.md +0 -187
  864. package/src/MetadataList/MetadataList.test.tsx +0 -383
  865. package/src/MetadataList/MetadataListItem.spec.md +0 -165
  866. package/src/MobileNav/MobileNav.spec.md +0 -306
  867. package/src/MobileNav/MobileNav.test.tsx +0 -296
  868. package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +0 -584
  869. package/src/MobileNav/MobileNavCloseTiming.test.ts +0 -58
  870. package/src/MobileNav/MobileNavCloseVisibility.test.tsx +0 -172
  871. package/src/MobileNav/MobileNavEntryAnimation.test.tsx +0 -212
  872. package/src/MobileNav/MobileNavFocus.test.tsx +0 -553
  873. package/src/MobileNav/MobileNavPinchZoom.a11y.chromium.spec.ts +0 -147
  874. package/src/MobileNav/MobileNavReopen.test.tsx +0 -118
  875. package/src/MobileNav/MobileNavScrollbarGutter.test.tsx +0 -109
  876. package/src/MobileNav/MobileNavToggle.test.tsx +0 -179
  877. package/src/MobileNav/__tests__/MobileNavHeader.a11y.chromium.spec.ts +0 -89
  878. package/src/MobileNav/__tests__/MobileNavWidth.a11y.chromium.spec.ts +0 -103
  879. package/src/MoreMenu/MoreMenu.spec.md +0 -268
  880. package/src/MoreMenu/MoreMenu.test.tsx +0 -367
  881. package/src/MultiSelector/CreateRow.a11y.chromium.spec.ts +0 -148
  882. package/src/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.ts +0 -72
  883. package/src/MultiSelector/MultiSelector.focusableDisabled.test.tsx +0 -121
  884. package/src/MultiSelector/MultiSelector.fr10.test.tsx +0 -221
  885. package/src/MultiSelector/MultiSelector.spec.md +0 -357
  886. package/src/MultiSelector/MultiSelector.test.tsx +0 -4971
  887. package/src/MultiSelector/OptionActions.a11y.chromium.spec.ts +0 -310
  888. package/src/MultiSelector/__tests__/MultiSelector.a11y.chromium.spec.ts +0 -46
  889. package/src/MultiSelector/__tests__/MultiSelector.listbox.a11y.test.tsx +0 -135
  890. package/src/NavIcon/NavIcon.spec.md +0 -158
  891. package/src/NavIcon/NavIcon.test.tsx +0 -41
  892. package/src/NavMenu/NavHeadingMenu.spec.md +0 -264
  893. package/src/NavMenu/NavHeadingMenu.test.tsx +0 -714
  894. package/src/NavMenu/__tests__/NavMenusSlideSelection.a11y.chromium.spec.ts +0 -137
  895. package/src/NumberInput/NumberInput.fr10.test.tsx +0 -21
  896. package/src/NumberInput/NumberInput.spec.md +0 -270
  897. package/src/NumberInput/NumberInput.test.tsx +0 -2697
  898. package/src/NumberInput/__tests__/NumberInput.a11y.chromium.spec.ts +0 -124
  899. package/src/NumberInput/__tests__/NumberInput.a11y.known-failures.ts +0 -26
  900. package/src/NumberInput/__tests__/NumberInput.a11y.renders.tsx +0 -77
  901. package/src/NumberInput/__tests__/NumberInput.a11y.states.ts +0 -116
  902. package/src/NumberInput/__tests__/NumberInput.a11y.test.tsx +0 -154
  903. package/src/NumberInput/__tests__/NumberInput.coarse-pointer.a11y.chromium.spec.ts +0 -102
  904. package/src/NumberInput/numberInputCommit.test.ts +0 -72
  905. package/src/NumberInput/numberParser.docblock.test.ts +0 -234
  906. package/src/NumberInput/numberParser.test.ts +0 -222
  907. package/src/Outline/Outline.spec.md +0 -258
  908. package/src/Outline/Outline.test.tsx +0 -1134
  909. package/src/Outline/OutlineFocus.a11y.chromium.spec.ts +0 -551
  910. package/src/Outline/modules/parseOutlineFromMarkdown.spec.md +0 -150
  911. package/src/Outline/parseOutlineFromMarkdown.test.ts +0 -104
  912. package/src/OverflowList/OverflowList.spec.md +0 -204
  913. package/src/OverflowList/OverflowList.test.tsx +0 -1073
  914. package/src/Overlay/Overlay.spec.md +0 -304
  915. package/src/Overlay/Overlay.test.tsx +0 -424
  916. package/src/Pagination/Pagination.spec.md +0 -300
  917. package/src/Pagination/Pagination.test.tsx +0 -1754
  918. package/src/Popover/Popover.spec.md +0 -525
  919. package/src/Popover/Popover.test.tsx +0 -1508
  920. package/src/Popover/PopoverPresentation.test.tsx +0 -952
  921. package/src/Popover/__tests__/PopoverSecondDocument.a11y.chromium.spec.ts +0 -267
  922. package/src/PowerSearch/PowerSearch.spec.md +0 -661
  923. package/src/PowerSearch/PowerSearch.test.tsx +0 -1027
  924. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +0 -1190
  925. package/src/PowerSearch/PowerSearchFilterEditor.test.tsx +0 -377
  926. package/src/PowerSearch/PowerSearchToken.test.tsx +0 -230
  927. package/src/PowerSearch/PowerSearchValueEditor.test.tsx +0 -754
  928. package/src/PowerSearch/formatFilterValue.test.ts +0 -339
  929. package/src/PowerSearch/resolveDateTimeRangePart.test.ts +0 -40
  930. package/src/PowerSearch/usePowerSearchConfig.test.ts +0 -832
  931. package/src/PowerSearch/usePowerSearchSource.test.ts +0 -621
  932. package/src/ProgressBar/ProgressBar.spec.md +0 -261
  933. package/src/ProgressBar/ProgressBar.test.tsx +0 -928
  934. package/src/ProgressBar/__tests__/ProgressBar.a11y.chromium.spec.ts +0 -127
  935. package/src/ProgressBar/__tests__/ProgressBar.a11y.known-failures.ts +0 -12
  936. package/src/ProgressBar/__tests__/ProgressBar.a11y.renders.tsx +0 -38
  937. package/src/ProgressBar/__tests__/ProgressBar.a11y.states.ts +0 -123
  938. package/src/ProgressBar/__tests__/ProgressBar.a11y.test.tsx +0 -107
  939. package/src/ProgressBar/__tests__/ProgressBarMotion.a11y.chromium.spec.ts +0 -831
  940. package/src/RadioList/RadioList.fr10.test.tsx +0 -67
  941. package/src/RadioList/RadioList.spec.md +0 -227
  942. package/src/RadioList/RadioList.test.tsx +0 -1051
  943. package/src/RadioList/RadioListItem.press.source-build.test.mjs +0 -80
  944. package/src/RadioList/RadioListItem.spec.md +0 -223
  945. package/src/RadioList/__tests__/RadioGroup.a11y.chromium.spec.ts +0 -274
  946. package/src/RadioList/__tests__/RadioGroup.a11y.known-failures.ts +0 -12
  947. package/src/RadioList/__tests__/RadioGroup.a11y.renders.tsx +0 -232
  948. package/src/RadioList/__tests__/RadioGroup.a11y.states.ts +0 -503
  949. package/src/RadioList/__tests__/RadioGroup.a11y.test.tsx +0 -217
  950. package/src/Resizable/Resizable.spec.md +0 -298
  951. package/src/Resizable/ResizeHandle.test.tsx +0 -1067
  952. package/src/Resizable/__tests__/ResizeHandle.a11y.chromium.spec.ts +0 -127
  953. package/src/Resizable/__tests__/ResizeHandle.a11y.known-failures.ts +0 -39
  954. package/src/Resizable/__tests__/ResizeHandle.a11y.renders.tsx +0 -80
  955. package/src/Resizable/__tests__/ResizeHandle.a11y.states.ts +0 -197
  956. package/src/Resizable/__tests__/ResizeHandle.a11y.test.tsx +0 -123
  957. package/src/Resizable/shippedDirections.test.ts +0 -462
  958. package/src/Resizable/useResizable.test.ts +0 -1725
  959. package/src/Resizable/utils.test.ts +0 -74
  960. package/src/ScrollableArea/ScrollableArea.spec.md +0 -190
  961. package/src/ScrollableArea/ScrollableArea.test.tsx +0 -304
  962. package/src/ScrollableArea/modules/useScrollableArea.spec.md +0 -149
  963. package/src/Section/Section.spec.md +0 -197
  964. package/src/Section/Section.test.tsx +0 -376
  965. package/src/SegmentedControl/SegmentedControl.spec.md +0 -261
  966. package/src/SegmentedControl/SegmentedControl.test.tsx +0 -981
  967. package/src/SegmentedControl/SegmentedControlItem.spec.md +0 -206
  968. package/src/SelectableCard/SelectableCard.spec.md +0 -230
  969. package/src/SelectableCard/SelectableCard.test.tsx +0 -216
  970. package/src/Selector/Selector.focusableDisabled.test.tsx +0 -127
  971. package/src/Selector/Selector.fr10.test.tsx +0 -149
  972. package/src/Selector/Selector.sheetActiveDescendant.test.tsx +0 -95
  973. package/src/Selector/Selector.source-build.test.mjs +0 -79
  974. package/src/Selector/Selector.spec.md +0 -379
  975. package/src/Selector/Selector.test.tsx +0 -5117
  976. package/src/Selector/__tests__/Combobox.a11y.known-failures.ts +0 -27
  977. package/src/Selector/__tests__/Combobox.a11y.states.ts +0 -145
  978. package/src/Selector/__tests__/ComboboxField.a11y.chromium.spec.ts +0 -458
  979. package/src/Selector/__tests__/EmptyStateAnnouncement.a11y.chromium.spec.ts +0 -91
  980. package/src/Selector/__tests__/Listbox.a11y.chromium.spec.ts +0 -131
  981. package/src/Selector/__tests__/Listbox.a11y.dom.ts +0 -38
  982. package/src/Selector/__tests__/Listbox.a11y.inventory.test.ts +0 -63
  983. package/src/Selector/__tests__/Listbox.a11y.known-failures.ts +0 -12
  984. package/src/Selector/__tests__/Listbox.a11y.states.ts +0 -232
  985. package/src/Selector/__tests__/Selector.a11y.chromium.spec.ts +0 -46
  986. package/src/Selector/__tests__/Selector.combobox.a11y.chromium.spec.ts +0 -109
  987. package/src/Selector/__tests__/Selector.combobox.a11y.test.tsx +0 -133
  988. package/src/Selector/__tests__/Selector.listbox.a11y.test.tsx +0 -128
  989. package/src/Selector/useCombobox.public.test.tsx +0 -89
  990. package/src/Selector/useCombobox.test.tsx +0 -444
  991. package/src/SideNav/SideNav.spec.md +0 -247
  992. package/src/SideNav/SideNav.test.tsx +0 -4275
  993. package/src/SideNav/SideNavCollapseButton.spec.md +0 -161
  994. package/src/SideNav/SideNavHeading.spec.md +0 -226
  995. package/src/SideNav/SideNavItem.spec.md +0 -236
  996. package/src/SideNav/SideNavRailMotion.test.tsx +0 -620
  997. package/src/SideNav/SideNavSection.spec.md +0 -153
  998. package/src/SideNav/__tests__/SideNavRailMotion.interaction.browser.spec.ts +0 -542
  999. package/src/SizeContext/SizeContext.test.tsx +0 -226
  1000. package/src/Skeleton/Skeleton.spec.md +0 -184
  1001. package/src/Skeleton/Skeleton.test.tsx +0 -148
  1002. package/src/Slider/Slider.fr10.test.tsx +0 -93
  1003. package/src/Slider/Slider.spec.md +0 -285
  1004. package/src/Slider/Slider.test.tsx +0 -1661
  1005. package/src/Slider/__tests__/Slider.a11y.chromium.spec.ts +0 -125
  1006. package/src/Slider/__tests__/Slider.a11y.known-failures.ts +0 -12
  1007. package/src/Slider/__tests__/Slider.a11y.renders.tsx +0 -42
  1008. package/src/Slider/__tests__/Slider.a11y.states.ts +0 -106
  1009. package/src/Slider/__tests__/Slider.a11y.test.tsx +0 -105
  1010. package/src/Spinner/Spinner.spec.md +0 -231
  1011. package/src/Spinner/Spinner.test.tsx +0 -484
  1012. package/src/Stack/Stack.spec.md +0 -203
  1013. package/src/Stack/Stack.test.tsx +0 -473
  1014. package/src/Stack/StackItem.test.tsx +0 -105
  1015. package/src/StatusDot/StatusDot.spec.md +0 -224
  1016. package/src/StatusDot/StatusDot.test.tsx +0 -213
  1017. package/src/Stepper/Step.spec.md +0 -200
  1018. package/src/Stepper/Stepper.public.test.ts +0 -91
  1019. package/src/Stepper/Stepper.spec.md +0 -327
  1020. package/src/Stepper/Stepper.test.tsx +0 -2130
  1021. package/src/Switch/Switch.focusRing.source-build.test.mjs +0 -81
  1022. package/src/Switch/Switch.spec.md +0 -277
  1023. package/src/Switch/Switch.test.tsx +0 -1136
  1024. package/src/Switch/__tests__/Switch.a11y.chromium.spec.ts +0 -211
  1025. package/src/Switch/__tests__/Switch.a11y.known-failures.ts +0 -28
  1026. package/src/Switch/__tests__/Switch.a11y.states.ts +0 -275
  1027. package/src/Switch/__tests__/Switch.a11y.test.tsx +0 -144
  1028. package/src/TabList/Tab.spec.md +0 -201
  1029. package/src/TabList/TabList.spec.md +0 -217
  1030. package/src/TabList/TabList.test.tsx +0 -1755
  1031. package/src/TabList/TabMenu.spec.md +0 -198
  1032. package/src/TabList/__tests__/TabListOverflow.interaction.browser.spec.ts +0 -99
  1033. package/src/TabList/__tests__/TabPressPointer.interaction.browser.spec.ts +0 -163
  1034. package/src/TabList/__tests__/Tabs.a11y.chromium.spec.ts +0 -199
  1035. package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +0 -12
  1036. package/src/TabList/__tests__/Tabs.a11y.renders.tsx +0 -92
  1037. package/src/TabList/__tests__/Tabs.a11y.states.ts +0 -247
  1038. package/src/TabList/__tests__/Tabs.a11y.test.tsx +0 -153
  1039. package/src/Table/Table.perf.test.tsx +0 -569
  1040. package/src/Table/Table.spec.md +0 -395
  1041. package/src/Table/Table.test.tsx +0 -1883
  1042. package/src/Table/TableHeaderCell.test.tsx +0 -291
  1043. package/src/Table/TableSelectionToolbar.test.tsx +0 -109
  1044. package/src/Table/__tests__/Table.a11y.chromium.spec.ts +0 -141
  1045. package/src/Table/__tests__/Table.a11y.contracts.ts +0 -35
  1046. package/src/Table/__tests__/Table.a11y.renders.tsx +0 -112
  1047. package/src/Table/__tests__/Table.a11y.states.ts +0 -136
  1048. package/src/Table/__tests__/Table.a11y.test.tsx +0 -198
  1049. package/src/Table/__tests__/TableMinWidth.a11y.chromium.spec.ts +0 -75
  1050. package/src/Table/__tests__/TableRowGlide.a11y.browser.spec.ts +0 -382
  1051. package/src/Table/__tests__/TableScroll.a11y.chromium.spec.ts +0 -138
  1052. package/src/Table/__tests__/TableStickyDividers.a11y.browser.spec.ts +0 -560
  1053. package/src/Table/plugins/activation/useTableActivation.spec.md +0 -254
  1054. package/src/Table/plugins/columnResize/useTableColumnResize.spec.md +0 -373
  1055. package/src/Table/plugins/columnResize/useTableColumnResize.test.tsx +0 -941
  1056. package/src/Table/plugins/columnSettings/useTableColumnSettings.test.tsx +0 -175
  1057. package/src/Table/plugins/columnSettings/useTableColumnSettingsState.test.tsx +0 -296
  1058. package/src/Table/plugins/filtering/useTableFilterState.test.tsx +0 -143
  1059. package/src/Table/plugins/filtering/useTableFiltering.test.tsx +0 -357
  1060. package/src/Table/plugins/groupedRows/useTableGroupedRows-perf.test.tsx +0 -112
  1061. package/src/Table/plugins/groupedRows/useTableGroupedRows.test.tsx +0 -409
  1062. package/src/Table/plugins/infiniteScroll/useTableInfiniteScroll.spec.md +0 -244
  1063. package/src/Table/plugins/pagination/useTablePagination-perf.test.tsx +0 -232
  1064. package/src/Table/plugins/pagination/useTablePagination.test.tsx +0 -666
  1065. package/src/Table/plugins/rowExpansion/useTableRowExpansion.glide.test.tsx +0 -280
  1066. package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +0 -681
  1067. package/src/Table/plugins/rowIndex/useTableRowIndex.test.tsx +0 -132
  1068. package/src/Table/plugins/rowStatus/useTableRowStatus.public.test.ts +0 -92
  1069. package/src/Table/plugins/rowStatus/useTableRowStatus.spec.md +0 -355
  1070. package/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx +0 -519
  1071. package/src/Table/plugins/selection/useTableSelection-perf.test.tsx +0 -258
  1072. package/src/Table/plugins/selection/useTableSelection.test.tsx +0 -424
  1073. package/src/Table/plugins/selection/useTableSelectionState.test.tsx +0 -451
  1074. package/src/Table/plugins/sortable/__tests__/useTableSortable.a11y.test.tsx +0 -174
  1075. package/src/Table/plugins/sortable/__tests__/useTableSortable.a11y.ts +0 -86
  1076. package/src/Table/plugins/sortable/useTableSortable.test.tsx +0 -786
  1077. package/src/Table/plugins/sortable/useTableSortableState.test.tsx +0 -580
  1078. package/src/Table/plugins/stickyColumns/useTableStickyColumns.test.tsx +0 -334
  1079. package/src/Table/plugins/tree/useTableTreeData-perf.test.tsx +0 -163
  1080. package/src/Table/plugins/tree/useTableTreeData.test.tsx +0 -992
  1081. package/src/Table/plugins/tree/useTableTreeState.test.tsx +0 -740
  1082. package/src/Table/plugins/virtualization/useTableVirtualization.spec.md +0 -266
  1083. package/src/Table/tableContextMenu.test.tsx +0 -208
  1084. package/src/Text/Text.spec.md +0 -193
  1085. package/src/Text/Text.test.tsx +0 -405
  1086. package/src/Text/useTruncation.test.tsx +0 -196
  1087. package/src/TextArea/TextArea.fr10.test.tsx +0 -21
  1088. package/src/TextArea/TextArea.spec.md +0 -300
  1089. package/src/TextArea/TextArea.test.tsx +0 -1317
  1090. package/src/TextInput/TextInput.fr10.test.tsx +0 -21
  1091. package/src/TextInput/TextInput.spec.md +0 -260
  1092. package/src/TextInput/TextInput.test.tsx +0 -1280
  1093. package/src/TextInput/__tests__/TextInput.a11y.chromium.spec.ts +0 -214
  1094. package/src/TextInput/__tests__/TextInput.a11y.known-failures.ts +0 -40
  1095. package/src/TextInput/__tests__/TextInput.a11y.renders.tsx +0 -202
  1096. package/src/TextInput/__tests__/TextInput.a11y.states.ts +0 -384
  1097. package/src/TextInput/__tests__/TextInput.a11y.test.tsx +0 -130
  1098. package/src/Thumbnail/Thumbnail.spec.md +0 -244
  1099. package/src/Thumbnail/Thumbnail.test.tsx +0 -384
  1100. package/src/TimeInput/NativeTimeInput.test.tsx +0 -343
  1101. package/src/TimeInput/Presentation.test.tsx +0 -192
  1102. package/src/TimeInput/TimeInput.fr10.test.tsx +0 -312
  1103. package/src/TimeInput/TimeInput.spec.md +0 -270
  1104. package/src/TimeInput/TimeInput.test.tsx +0 -907
  1105. package/src/Timer/Timer.spec.md +0 -215
  1106. package/src/Timer/Timer.test.tsx +0 -302
  1107. package/src/Timestamp/Timestamp.spec.md +0 -246
  1108. package/src/Timestamp/Timestamp.test.tsx +0 -1661
  1109. package/src/Timestamp/tooltipEntries.test.ts +0 -566
  1110. package/src/Toast/Toast.spec.md +0 -291
  1111. package/src/Toast/ToastViewport.modalHost.test.tsx +0 -891
  1112. package/src/Toast/ToastViewport.test.tsx +0 -3130
  1113. package/src/Toast/useToast.test.tsx +0 -258
  1114. package/src/ToggleButton/ToggleButton.spec.md +0 -256
  1115. package/src/ToggleButton/ToggleButton.test.tsx +0 -1000
  1116. package/src/ToggleButton/ToggleButtonGroup.spec.md +0 -195
  1117. package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +0 -177
  1118. package/src/ToggleButton/__tests__/ToggleButton.a11y.renders.tsx +0 -133
  1119. package/src/ToggleButton/__tests__/ToggleButton.a11y.states.ts +0 -167
  1120. package/src/ToggleButton/__tests__/ToggleButton.a11y.test.tsx +0 -103
  1121. package/src/Token/Token.spec.md +0 -261
  1122. package/src/Token/Token.test.tsx +0 -707
  1123. package/src/Token/__tests__/TokenRemoveTarget.a11y.chromium.spec.ts +0 -87
  1124. package/src/Tokenizer/Tokenizer.fr10.test.tsx +0 -28
  1125. package/src/Tokenizer/Tokenizer.props-type.test.tsx +0 -126
  1126. package/src/Tokenizer/Tokenizer.spec.md +0 -615
  1127. package/src/Tokenizer/Tokenizer.ssr.test.tsx +0 -196
  1128. package/src/Tokenizer/Tokenizer.test.tsx +0 -2216
  1129. package/src/Tokenizer/TokenizerBottomSheet.test.tsx +0 -2738
  1130. package/src/Tokenizer/__tests__/TokenizerEndLane.a11y.chromium.spec.ts +0 -505
  1131. package/src/Toolbar/Toolbar.spec.md +0 -227
  1132. package/src/Toolbar/Toolbar.test.tsx +0 -600
  1133. package/src/Toolbar/__tests__/Toolbar.a11y.chromium.spec.ts +0 -176
  1134. package/src/Toolbar/__tests__/Toolbar.a11y.renders.tsx +0 -63
  1135. package/src/Toolbar/__tests__/Toolbar.a11y.states.ts +0 -142
  1136. package/src/Toolbar/__tests__/Toolbar.a11y.test.tsx +0 -126
  1137. package/src/Tooltip/Tooltip.spec.md +0 -267
  1138. package/src/Tooltip/Tooltip.test.tsx +0 -819
  1139. package/src/Tooltip/__tests__/Tooltip.a11y.chromium.spec.ts +0 -141
  1140. package/src/Tooltip/__tests__/Tooltip.a11y.renders.tsx +0 -86
  1141. package/src/Tooltip/__tests__/Tooltip.a11y.states.ts +0 -132
  1142. package/src/Tooltip/__tests__/Tooltip.a11y.test.tsx +0 -126
  1143. package/src/Tooltip/__tests__/TooltipFocusReturn.a11y.chromium.spec.ts +0 -130
  1144. package/src/Tooltip/__tests__/TooltipHoverBridge.a11y.chromium.spec.ts +0 -75
  1145. package/src/Tooltip/__tests__/TooltipTallContent.a11y.browser.spec.ts +0 -673
  1146. package/src/Tooltip/__tests__/TooltipTallContentTracking.a11y.browser.spec.ts +0 -1162
  1147. package/src/Tooltip/useTooltip.test.tsx +0 -483
  1148. package/src/TopNav/TopNav.spec.md +0 -190
  1149. package/src/TopNav/TopNav.test.tsx +0 -863
  1150. package/src/TopNav/TopNavHeading.spec.md +0 -211
  1151. package/src/TopNav/TopNavItem.spec.md +0 -189
  1152. package/src/TopNav/TopNavMegaMenu.reduced-motion.source-build.test.mjs +0 -83
  1153. package/src/TopNav/TopNavMegaMenu.spec.md +0 -190
  1154. package/src/TopNav/TopNavMegaMenu.test.tsx +0 -1153
  1155. package/src/TopNav/TopNavMegaMenuFeaturedCard.spec.md +0 -150
  1156. package/src/TopNav/TopNavMegaMenuItem.spec.md +0 -155
  1157. package/src/TopNav/TopNavMenu.spec.md +0 -202
  1158. package/src/TopNav/TopNavMenu.test.tsx +0 -502
  1159. package/src/TopNav/__tests__/TopNavMobileLongHeading.a11y.chromium.spec.ts +0 -75
  1160. package/src/TreeList/TreeList.spec.md +0 -328
  1161. package/src/TreeList/TreeList.test.tsx +0 -1737
  1162. package/src/TreeList/__tests__/TreeList.a11y.chromium.spec.ts +0 -138
  1163. package/src/TreeList/__tests__/TreeList.a11y.renders.tsx +0 -44
  1164. package/src/TreeList/__tests__/TreeList.a11y.states.ts +0 -164
  1165. package/src/TreeList/__tests__/TreeList.a11y.test.tsx +0 -138
  1166. package/src/TreeList/__tests__/TreeListGuideAlignment.interaction.browser.spec.ts +0 -127
  1167. package/src/TreeList/__tests__/TreeListRowNames.a11y.chromium.spec.ts +0 -83
  1168. package/src/Typeahead/BaseTypeahead.emptyState.a11y.chromium.spec.ts +0 -138
  1169. package/src/Typeahead/BaseTypeahead.spec.md +0 -270
  1170. package/src/Typeahead/BaseTypeahead.test.tsx +0 -553
  1171. package/src/Typeahead/Typeahead.fr10.test.tsx +0 -28
  1172. package/src/Typeahead/Typeahead.spec.md +0 -257
  1173. package/src/Typeahead/Typeahead.test.tsx +0 -2984
  1174. package/src/Typeahead/TypeaheadItem.test.tsx +0 -134
  1175. package/src/VStack/VStack.test.tsx +0 -128
  1176. package/src/VisuallyHidden/VisuallyHidden.spec.md +0 -170
  1177. package/src/VisuallyHidden/VisuallyHidden.test.tsx +0 -115
  1178. package/src/__tests__/BusyOwnerAnnouncement.test.tsx +0 -853
  1179. package/src/__tests__/CardPress.a11y.chromium.spec.ts +0 -353
  1180. package/src/__tests__/DisabledTriggerFocus.a11y.chromium.spec.ts +0 -87
  1181. package/src/__tests__/PressModality.a11y.chromium.spec.ts +0 -392
  1182. package/src/__tests__/PressedState.a11y.chromium.spec.ts +0 -814
  1183. package/src/__tests__/TestIcon.tsx +0 -26
  1184. package/src/__tests__/babelPluginAddExtensions.test.ts +0 -190
  1185. package/src/__tests__/fastRoleQueries.ts +0 -66
  1186. package/src/__tests__/forcedColors.ts +0 -60
  1187. package/src/__tests__/internalModulesNotExperimental.test.ts +0 -88
  1188. package/src/__tests__/pressState.ts +0 -216
  1189. package/src/__tests__/stubMatchMedia.test.ts +0 -79
  1190. package/src/__tests__/stubMatchMedia.ts +0 -95
  1191. package/src/__tests__/stylexDeclarations.ts +0 -51
  1192. package/src/__tests__/unthemedLength.ts +0 -34
  1193. package/src/docPropLiterals.test.ts +0 -964
  1194. package/src/docPropReferences.test.ts +0 -178
  1195. package/src/docs-script.test.mjs +0 -55
  1196. package/src/gestures/__tests__/GestureHost.test.ts +0 -2136
  1197. package/src/gestures/__tests__/LongPressGestureRecognizer.test.ts +0 -141
  1198. package/src/gestures/__tests__/PanGestureRecognizer.test.ts +0 -327
  1199. package/src/gestures/__tests__/PinchGestureRecognizer.test.ts +0 -74
  1200. package/src/gestures/__tests__/PressTracker.test.ts +0 -275
  1201. package/src/gestures/__tests__/ScreenEdgePanGestureRecognizer.test.ts +0 -498
  1202. package/src/gestures/__tests__/SwipeGestureRecognizer.test.ts +0 -108
  1203. package/src/gestures/__tests__/TapGestureRecognizer.test.ts +0 -154
  1204. package/src/gestures/__tests__/VelocityTracker.streams.test.ts +0 -243
  1205. package/src/gestures/__tests__/VelocityTracker.test.ts +0 -261
  1206. package/src/gestures/__tests__/arena.test.ts +0 -864
  1207. package/src/gestures/__tests__/arenaContinuous.test.ts +0 -207
  1208. package/src/gestures/__tests__/arenaPinchSwipeEdge.test.ts +0 -193
  1209. package/src/gestures/__tests__/arenaTapLongPress.test.ts +0 -211
  1210. package/src/gestures/__tests__/browser/build-fixture.mjs +0 -46
  1211. package/src/gestures/__tests__/browser/fixture.ts +0 -307
  1212. package/src/gestures/__tests__/browser/gestures.interaction.browser.spec.ts +0 -1137
  1213. package/src/gestures/__tests__/browserNegotiation.test.ts +0 -293
  1214. package/src/gestures/__tests__/fixtures/pointer-streams/README.md +0 -82
  1215. package/src/gestures/__tests__/fixtures/pointer-streams/android-chromium-fling.json +0 -130
  1216. package/src/gestures/__tests__/fixtures/pointer-streams/android-chromium-slow.json +0 -860
  1217. package/src/gestures/__tests__/fixtures/pointer-streams/android-chromium-stop-release.json +0 -357
  1218. package/src/gestures/__tests__/fixtures/pointer-streams/android-firefox-fling.json +0 -66
  1219. package/src/gestures/__tests__/fixtures/pointer-streams/android-firefox-slow.json +0 -268
  1220. package/src/gestures/__tests__/fixtures/pointer-streams/android-firefox-stop-release.json +0 -128
  1221. package/src/gestures/__tests__/fixtures/pointer-streams/capture.html +0 -123
  1222. package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-mouse-fling.json +0 -64
  1223. package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-mouse-slow.json +0 -268
  1224. package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-mouse-stop-release.json +0 -124
  1225. package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-pen-fling.json +0 -64
  1226. package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-pen-slow.json +0 -268
  1227. package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-pen-stop-release.json +0 -124
  1228. package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-touch-fling.json +0 -84
  1229. package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-touch-slow.json +0 -381
  1230. package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-touch-stop-release.json +0 -171
  1231. package/src/gestures/__tests__/fixtures/pointer-streams/ios-safari-fling.json +0 -45
  1232. package/src/gestures/__tests__/fixtures/pointer-streams/ios-safari-slow.json +0 -101
  1233. package/src/gestures/__tests__/fixtures/pointer-streams/ios-safari-stop-release.json +0 -130
  1234. package/src/gestures/__tests__/haptics.test.ts +0 -187
  1235. package/src/gestures/__tests__/harness.ts +0 -252
  1236. package/src/gestures/__tests__/pressTrackerFactory.test.ts +0 -59
  1237. package/src/gestures/__tests__/recognizerKind.test.ts +0 -78
  1238. package/src/gestures/__tests__/touchActionRule.test.ts +0 -54
  1239. package/src/gestures/__tests__/useGestureRecognizer.test.tsx +0 -570
  1240. package/src/hooks/__tests__/MenuPressShadowRoot.a11y.chromium.spec.ts +0 -144
  1241. package/src/hooks/__tests__/menuLinkClicks.ts +0 -71
  1242. package/src/hooks/actionGroupTracker.exports.test.ts +0 -28
  1243. package/src/hooks/actionGroupTracker.test.ts +0 -510
  1244. package/src/hooks/computeOverflow.test.ts +0 -422
  1245. package/src/hooks/deprecatedApis.public.test.ts +0 -33
  1246. package/src/hooks/menuPressGesture.test.ts +0 -253
  1247. package/src/hooks/scrollKeyboardDelegation.test.ts +0 -188
  1248. package/src/hooks/scrollbarGutter.test.ts +0 -167
  1249. package/src/hooks/useAnnounce.modal.test.tsx +0 -1291
  1250. package/src/hooks/useAnnounce.replace.test.tsx +0 -516
  1251. package/src/hooks/useAnnounce.test.tsx +0 -293
  1252. package/src/hooks/useAnnounceRenderedText.adopters.test.tsx +0 -235
  1253. package/src/hooks/useAutoMediaMode.test.ts +0 -109
  1254. package/src/hooks/useBusyAnnouncement.adopters.test.tsx +0 -69
  1255. package/src/hooks/useBusyAnnouncement.test.tsx +0 -112
  1256. package/src/hooks/useClickableContainer.test.tsx +0 -236
  1257. package/src/hooks/useClipboard.test.tsx +0 -151
  1258. package/src/hooks/useContainerReveal.test.tsx +0 -163
  1259. package/src/hooks/useDevWarning.test.tsx +0 -45
  1260. package/src/hooks/useEntryAnimation.ssr.test.tsx +0 -143
  1261. package/src/hooks/useFocusTrap.test.tsx +0 -635
  1262. package/src/hooks/useFocusTrapEscapeShim.test.tsx +0 -166
  1263. package/src/hooks/useGridFocus.test.tsx +0 -165
  1264. package/src/hooks/useHotkeys.test.ts +0 -265
  1265. package/src/hooks/useImageMode.test.ts +0 -133
  1266. package/src/hooks/useInputContainer.test.tsx +0 -180
  1267. package/src/hooks/useInputStatusIcon.test.tsx +0 -240
  1268. package/src/hooks/useIsHydrating.test.tsx +0 -70
  1269. package/src/hooks/useKeyboardHint.test.tsx +0 -92
  1270. package/src/hooks/useListFocus.test.tsx +0 -664
  1271. package/src/hooks/useLongPress.test.tsx +0 -152
  1272. package/src/hooks/useMediaQuery.test.ts +0 -165
  1273. package/src/hooks/useMenuHover.test.tsx +0 -536
  1274. package/src/hooks/useMenuPress.test.tsx +0 -565
  1275. package/src/hooks/useMergedRefs.test.tsx +0 -62
  1276. package/src/hooks/useOverflow.test.ts +0 -504
  1277. package/src/hooks/useRenamedProp.test.tsx +0 -95
  1278. package/src/hooks/useScrollLock.test.ts +0 -248
  1279. package/src/hooks/useScrollOverflow.test.tsx +0 -323
  1280. package/src/hooks/useScrollableArea.test.tsx +0 -836
  1281. package/src/hooks/useStreamingText.test.ts +0 -237
  1282. package/src/hooks/useTreeFocus.test.tsx +0 -351
  1283. package/src/hooks/useTypeahead.test.tsx +0 -351
  1284. package/src/i18n/InternationalizationProvider.spec.md +0 -252
  1285. package/src/i18n/__tests__/InternationalizationProvider.test.tsx +0 -128
  1286. package/src/i18n/__tests__/e2e-pagination.test.tsx +0 -172
  1287. package/src/i18n/__tests__/e2e-powersearch.test.tsx +0 -166
  1288. package/src/i18n/__tests__/getLocaleDirection.test.ts +0 -40
  1289. package/src/i18n/__tests__/resolve.test.ts +0 -325
  1290. package/src/i18n/__tests__/useCollator.test.tsx +0 -115
  1291. package/src/i18n/__tests__/useDirection.test.tsx +0 -66
  1292. package/src/i18n/__tests__/useLocale.test.tsx +0 -52
  1293. package/src/i18n/useTranslator.test.tsx +0 -267
  1294. package/src/inputFontFloor.test.ts +0 -106
  1295. package/src/internal/tokenizer-presentation.test.ts +0 -29
  1296. package/src/internal/touch.test.ts +0 -44
  1297. package/src/motion/__tests__/animatedLayout.interaction.browser.spec.ts +0 -1613
  1298. package/src/motion/__tests__/animatedLayoutAdopters.interaction.browser.spec.ts +0 -217
  1299. package/src/motion/__tests__/animatedLayoutBundle.ts +0 -17
  1300. package/src/motion/__tests__/animator.interaction.browser.spec.ts +0 -843
  1301. package/src/motion/__tests__/motionBundle.ts +0 -14
  1302. package/src/motion/animatedLayout.boundary.test.ts +0 -155
  1303. package/src/motion/animatedLayout.swap.test.ts +0 -827
  1304. package/src/motion/animatedLayout.test.ts +0 -1154
  1305. package/src/motion/animatedLayoutAdopters.test.tsx +0 -500
  1306. package/src/motion/animator.test.ts +0 -3746
  1307. package/src/motion/bezier.test.ts +0 -52
  1308. package/src/motion/coast.test.ts +0 -99
  1309. package/src/motion/decay.test.ts +0 -120
  1310. package/src/motion/projection.test.ts +0 -107
  1311. package/src/motion/reducedMotion.test.ts +0 -129
  1312. package/src/motion/reducedMotionPlan.test.ts +0 -208
  1313. package/src/motion/resolveMotion.test.ts +0 -123
  1314. package/src/motion/resolveSnapPoint.test.ts +0 -189
  1315. package/src/motion/roles.test.ts +0 -273
  1316. package/src/motion/rubberBand.test.ts +0 -88
  1317. package/src/motion/selectPage.test.ts +0 -31
  1318. package/src/motion/spring.test.ts +0 -650
  1319. package/src/motion/uikit/gestures.test.ts +0 -89
  1320. package/src/motion/uikit/springRows.test.ts +0 -71
  1321. package/src/motion/uikit/tableImports.test.ts +0 -293
  1322. package/src/motion/uikit/uikit.test.ts +0 -364
  1323. package/src/motion/uikit/values.test.ts +0 -121
  1324. package/src/naming.test.ts +0 -44
  1325. package/src/postinstall.test.mjs +0 -216
  1326. package/src/reset.a11y.chromium.spec.ts +0 -238
  1327. package/src/reset.test.ts +0 -205
  1328. package/src/reset.until-found.a11y.browser.spec.ts +0 -76
  1329. package/src/reset.until-found.test.ts +0 -32
  1330. package/src/serverSafeComponents.test.ts +0 -675
  1331. package/src/stylexValidation.source-build.test.mjs +0 -42
  1332. package/src/tailwind-theme.test.ts +0 -153
  1333. package/src/theme/MediaTheme.dom.test.tsx +0 -97
  1334. package/src/theme/Theme.test.tsx +0 -337
  1335. package/src/theme/__tests__/SpacingRootFontSize.a11y.chromium.spec.ts +0 -199
  1336. package/src/theme/contrast.test.ts +0 -94
  1337. package/src/theme/declarationBoundary.test.ts +0 -126
  1338. package/src/theme/defineTheme.test.ts +0 -2028
  1339. package/src/theme/derivedVarRegistry.test.ts +0 -592
  1340. package/src/theme/expandColorScale.test.ts +0 -651
  1341. package/src/theme/expandMotionScale.test.ts +0 -145
  1342. package/src/theme/expandRadiusScale.test.ts +0 -59
  1343. package/src/theme/expandTypeScale.test.ts +0 -351
  1344. package/src/theme/extensibleAxes.test.ts +0 -432
  1345. package/src/theme/generateThemeRules.test.ts +0 -1870
  1346. package/src/theme/hct.test.ts +0 -93
  1347. package/src/theme/onMediaTokens.test.ts +0 -431
  1348. package/src/theme/publicThemeHelperContract.test.ts +0 -67
  1349. package/src/theme/syntax/defineSyntaxTheme.diff.test.ts +0 -91
  1350. package/src/theme/syntax/serverSafeSyntax.test.ts +0 -68
  1351. package/src/theme/syntax/useSyntaxTheme.diff.test.tsx +0 -40
  1352. package/src/theme/themeAdaptations.test.ts +0 -1795
  1353. package/src/theme/themeMotion.test.ts +0 -620
  1354. package/src/theme/themeRegistry.test.ts +0 -59
  1355. package/src/theme/themingTargets.test.ts +0 -589
  1356. package/src/theme/tokenValueCompat.test.ts +0 -204
  1357. package/src/theme/tokens.test.ts +0 -441
  1358. package/src/theme/useTheme.test.tsx +0 -322
  1359. package/src/unicodePropertyEscapes.source-build.test.mjs +0 -69
  1360. package/src/utils/chainEventHandlers.test.ts +0 -39
  1361. package/src/utils/characters.test.ts +0 -141
  1362. package/src/utils/color.test.ts +0 -233
  1363. package/src/utils/composeEventHandlers.test.ts +0 -71
  1364. package/src/utils/customPropertyArmLayering.test.ts +0 -131
  1365. package/src/utils/dateParser.test.ts +0 -402
  1366. package/src/utils/dateTypes.test.ts +0 -40
  1367. package/src/utils/devWarning.test.ts +0 -56
  1368. package/src/utils/edgeAutoscroll.test.ts +0 -192
  1369. package/src/utils/focusReturn.test.ts +0 -119
  1370. package/src/utils/getKey.test.ts +0 -25
  1371. package/src/utils/getNodeText.test.ts +0 -24
  1372. package/src/utils/hitArea.test.ts +0 -628
  1373. package/src/utils/ime.test.ts +0 -32
  1374. package/src/utils/inputAria.test.ts +0 -39
  1375. package/src/utils/interactionModality.test.ts +0 -144
  1376. package/src/utils/isApplePlatform.test.ts +0 -61
  1377. package/src/utils/mergeProps.test.ts +0 -166
  1378. package/src/utils/mergeRefs.test.ts +0 -48
  1379. package/src/utils/parseStyleKey.test.ts +0 -56
  1380. package/src/utils/plainDate.test.ts +0 -938
  1381. package/src/utils/presentation.test.ts +0 -380
  1382. package/src/utils/pressFeedback.test.ts +0 -1012
  1383. package/src/utils/pressGesture.test.ts +0 -332
  1384. package/src/utils/pressPaintPriority.test.ts +0 -80
  1385. package/src/utils/pressPaintSource.test.ts +0 -65
  1386. package/src/utils/pressableCoverage.test.ts +0 -192
  1387. package/src/utils/primaryClick.test.ts +0 -38
  1388. package/src/utils/safeUrl.test.ts +0 -101
  1389. package/src/utils/sharedMutationObserver.test.ts +0 -106
  1390. package/src/utils/sharedResizeObserver.test.ts +0 -283
  1391. package/src/utils/themeProps.test.ts +0 -62
  1392. package/src/utils/timeFormat.test.ts +0 -204
  1393. package/src/utils/timeParser.test.ts +0 -295
  1394. package/src/utils/touchPresentation.test.ts +0 -462
  1395. package/src/utils/unicodeDigits.test.ts +0 -44
@@ -1,4971 +0,0 @@
1
- // Copyright (c) Meta Platforms, Inc. and affiliates.
2
-
3
- /**
4
- * @file MultiSelector.test.tsx
5
- * @input Uses vitest, @testing-library/react, @testing-library/user-event
6
- * @output Unit tests for MultiSelector
7
- * @position Tests; validates MultiSelector behavior
8
- *
9
- * SYNC: When MultiSelector.tsx API changes, update these tests.
10
- */
11
-
12
- import {
13
- describe,
14
- it,
15
- expect,
16
- vi,
17
- beforeAll,
18
- beforeEach,
19
- afterEach,
20
- } from 'vitest';
21
- import {
22
- act,
23
- render,
24
- screen,
25
- fireEvent,
26
- waitFor,
27
- within,
28
- } from '@testing-library/react';
29
- import userEvent from '@testing-library/user-event';
30
- import * as stylex from '@stylexjs/stylex';
31
- import {
32
- MultiSelector,
33
- type MultiSelectorHandle,
34
- type MultiSelectorChange,
35
- } from './MultiSelector';
36
- import {useRef, useState} from 'react';
37
- import {Dialog} from '../Dialog/Dialog';
38
- import {Icon} from '../Icon';
39
- import {InternationalizationProvider} from '../i18n';
40
- import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
41
- import {__resetInteractionModalityForTest} from '../utils/interactionModality';
42
- import {defineTheme} from '../theme/defineTheme';
43
- import {generateThemeCSS} from '../theme/generateThemeRules';
44
- import {selectorPresentationStyles} from '../Selector/selectorPresentation.stylex';
45
- import {groupStyles} from '../InputGroup/groupStyles';
46
- import {InputGroup, InputGroupText} from '../InputGroup';
47
- import {colorVars, focusVars} from '../theme/tokens.stylex';
48
- import {COMPACT_TOUCH_PRESENTATION_QUERY} from '../hooks/useAdaptivePresentation';
49
-
50
- /**
51
- * Enters a search query as one change. Typing it key by key can land several
52
- * keystrokes in one frame, whose result announcements are then joined into
53
- * one (spec:AST-086 FR9), so a test of what one query announces pastes it.
54
- */
55
- async function enterQuery(
56
- user: ReturnType<typeof userEvent.setup>,
57
- input: HTMLElement,
58
- query: string,
59
- ): Promise<void> {
60
- await user.click(input);
61
- await user.paste(query);
62
- }
63
-
64
- function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
65
- const {prose, component} = generateThemeCSS(theme);
66
- return [prose, component].filter(Boolean).join('\n\n');
67
- }
68
- // Module-level constants to satisfy @eslint-react/no-unstable-default-props.
69
- const ANNOUNCE_OPTIONS = ['Apple', 'Banana', 'Orange'] as const;
70
- const EMPTY_VALUE: string[] = [];
71
-
72
- function politeRegion(): HTMLElement | null {
73
- return document.querySelector('[data-astryx-live-region="polite"]');
74
- }
75
-
76
- // Mock showPopover and hidePopover methods since they're not implemented in jsdom
77
- // The bottom-sheet presentation renders BottomSheet through React.lazy. Its
78
- // first import transforms the BottomSheet module tree, which on a loaded
79
- // runner takes longer than a query's 1 s wait, so the first sheet test timed
80
- // out before the dialog existed. Load the module once up front: lazy() then
81
- // resolves from the module cache and every sheet test waits only on the open.
82
- beforeAll(async () => {
83
- await import('../BottomSheet/BottomSheet');
84
- });
85
-
86
- beforeEach(() => {
87
- __resetInteractionModalityForTest();
88
- HTMLDialogElement.prototype.showModal = vi.fn(function (
89
- this: HTMLDialogElement,
90
- ) {
91
- this.setAttribute('open', '');
92
- });
93
- HTMLDialogElement.prototype.close = vi.fn(function (this: HTMLDialogElement) {
94
- this.removeAttribute('open');
95
- });
96
- vi.stubGlobal(
97
- 'matchMedia',
98
- vi.fn().mockImplementation((query: string) => ({
99
- matches: false,
100
- media: query,
101
- onchange: null,
102
- addEventListener: vi.fn(),
103
- removeEventListener: vi.fn(),
104
- addListener: vi.fn(),
105
- removeListener: vi.fn(),
106
- dispatchEvent: vi.fn(),
107
- })),
108
- );
109
- HTMLElement.prototype.showPopover = vi.fn(function (this: HTMLElement) {
110
- this.setAttribute('popover-open', '');
111
- const event = new Event('toggle', {bubbles: false});
112
- Object.defineProperty(event, 'newState', {value: 'open'});
113
- this.dispatchEvent(event);
114
- });
115
- HTMLElement.prototype.hidePopover = vi.fn(function (this: HTMLElement) {
116
- this.removeAttribute('popover-open');
117
- const event = new Event('toggle', {bubbles: false});
118
- Object.defineProperty(event, 'newState', {value: 'closed'});
119
- this.dispatchEvent(event);
120
- });
121
- const originalMatches = HTMLElement.prototype.matches;
122
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
123
- (HTMLElement.prototype as any).matches = function (
124
- selector: string,
125
- ): boolean {
126
- if (selector === ':popover-open') {
127
- return this.hasAttribute('popover-open');
128
- }
129
- return originalMatches.call(this, selector);
130
- };
131
- });
132
-
133
- afterEach(() => {
134
- __resetLiveRegionsForTest();
135
- });
136
-
137
- // Helper: jsdom popover content is in the DOM but may not be
138
- // "visible" in the accessibility tree. Use hidden: true to find it.
139
- const h = {hidden: true} as const;
140
-
141
- /** A busy control's Spinner is decorative and silent (spec:AST-086 FR8). */
142
- function expectSilentBusySpinner() {
143
- const spinner = document.querySelector('.astryx-spinner');
144
- expect(spinner).not.toBeNull();
145
- expect(spinner).toHaveAttribute('aria-hidden', 'true');
146
- expect(screen.queryByRole('status', {name: 'Loading'})).toBeNull();
147
- }
148
-
149
- // The bottom sheet is loaded on demand (SelectorBottomSheet's lazy import).
150
- // Loading its module is not part of an open, so the file waits for it once
151
- // here, and each test's open waits only for the open.
152
- beforeAll(async () => {
153
- await import('../BottomSheet/BottomSheet');
154
- });
155
-
156
- describe('MultiSelector', () => {
157
- const defaultOptions = ['Apple', 'Banana', 'Orange'];
158
-
159
- it('uses a bottom sheet and stays open while toggling options', async () => {
160
- const user = userEvent.setup();
161
- const onChange = vi.fn();
162
- render(
163
- <MultiSelector
164
- label="Fruit"
165
- options={defaultOptions}
166
- value={[]}
167
- onChange={onChange}
168
- presentation="bottom-sheet"
169
- />,
170
- );
171
-
172
- const trigger = screen.getByRole('combobox');
173
- await user.click(trigger);
174
-
175
- expect(
176
- await screen.findByRole('dialog', {name: 'Fruit'}),
177
- ).toBeInTheDocument();
178
- expect(HTMLElement.prototype.showPopover).not.toHaveBeenCalled();
179
-
180
- await user.click(screen.getByRole('option', {name: 'Banana'}));
181
- expect(onChange).toHaveBeenCalledWith(['Banana']);
182
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
183
- });
184
-
185
- it('uses a bottom sheet for adaptive presentation on compact touch', async () => {
186
- vi.stubGlobal(
187
- 'matchMedia',
188
- vi.fn().mockImplementation((query: string) => ({
189
- matches: query === COMPACT_TOUCH_PRESENTATION_QUERY,
190
- media: query,
191
- onchange: null,
192
- addEventListener: vi.fn(),
193
- removeEventListener: vi.fn(),
194
- addListener: vi.fn(),
195
- removeListener: vi.fn(),
196
- dispatchEvent: vi.fn(),
197
- })),
198
- );
199
- const user = userEvent.setup();
200
- render(
201
- <MultiSelector
202
- label="Fruit"
203
- options={defaultOptions}
204
- value={[]}
205
- onChange={() => {}}
206
- presentation="adaptive"
207
- />,
208
- );
209
-
210
- await user.click(screen.getByRole('combobox'));
211
- expect(
212
- await screen.findByRole('dialog', {name: 'Fruit'}),
213
- ).toBeInTheDocument();
214
- expect(HTMLElement.prototype.showPopover).not.toHaveBeenCalled();
215
- });
216
-
217
- it('moves keyboard focus into a bottom-sheet listbox', async () => {
218
- const user = userEvent.setup();
219
- render(
220
- <MultiSelector
221
- label="Fruit"
222
- options={defaultOptions}
223
- value={[]}
224
- onChange={() => {}}
225
- presentation="bottom-sheet"
226
- />,
227
- );
228
-
229
- const trigger = screen.getByRole('combobox');
230
- trigger.focus();
231
- await user.keyboard('{Enter}');
232
-
233
- await waitFor(() => expect(screen.getByRole('listbox')).toHaveFocus());
234
- });
235
-
236
- it('restores touch focus without painting a trigger focus ring', async () => {
237
- render(
238
- <MultiSelector
239
- label="Fruit"
240
- options={defaultOptions}
241
- value={[]}
242
- onChange={() => {}}
243
- presentation="bottom-sheet"
244
- />,
245
- );
246
-
247
- const trigger = screen.getByRole('combobox');
248
- fireEvent.pointerDown(trigger, {pointerType: 'touch'});
249
- fireEvent.click(trigger, {detail: 1});
250
- const dialog = await screen.findByRole('dialog', {name: 'Fruit'});
251
- fireEvent.pointerDown(dialog, {pointerType: 'touch'});
252
- fireEvent.click(dialog, {detail: 1});
253
-
254
- trigger.focus();
255
- expect(trigger).toHaveFocus();
256
- expect(trigger.parentElement).toHaveClass(
257
- stylex.props(selectorPresentationStyles.pointerRestoredFocus).className!,
258
- );
259
- });
260
-
261
- it('shows no disabled focus ring after a pointer pick (spec:AST-085 FR6)', async () => {
262
- const field = (isDisabled: boolean) => (
263
- <MultiSelector
264
- label="Fruit"
265
- options={defaultOptions}
266
- value={[]}
267
- onChange={() => {}}
268
- presentation="bottom-sheet"
269
- isDisabled={isDisabled}
270
- />
271
- );
272
- const {rerender} = render(field(false));
273
- const trigger = screen.getByRole('combobox');
274
- fireEvent.pointerDown(trigger, {pointerType: 'touch'});
275
- fireEvent.click(trigger, {detail: 1});
276
- const dialog = await screen.findByRole('dialog', {name: 'Fruit'});
277
- fireEvent.pointerDown(dialog, {pointerType: 'touch'});
278
- fireEvent.click(dialog, {detail: 1});
279
- rerender(field(true));
280
- const ring = stylex
281
- .props(
282
- stylex.create({
283
- ring: {
284
- boxShadow: {
285
- default: null,
286
- ':has(> [aria-disabled="true"]:focus-visible)': `inset 0px 0px 0px 2px ${colorVars['--color-accent-muted']}`,
287
- },
288
- },
289
- }).ring,
290
- )
291
- .className!.split(' ')
292
- .pop()!;
293
- const wrapper = screen.getByRole('combobox').parentElement!;
294
- expect(wrapper).toHaveClass(
295
- stylex.props(selectorPresentationStyles.pointerRestoredFocus).className!,
296
- );
297
- expect(wrapper).not.toHaveClass(ring);
298
- });
299
-
300
- it('renders with label', () => {
301
- render(
302
- <MultiSelector
303
- label="Fruit"
304
- options={defaultOptions}
305
- value={[]}
306
- onChange={() => {}}
307
- />,
308
- );
309
- expect(screen.getByLabelText('Fruit')).toBeInTheDocument();
310
- });
311
-
312
- it('applies distinct trigger chrome to the secondary variant', () => {
313
- // data-testid reaches the native control; the variant is on the trigger root.
314
- const triggerRoot = () =>
315
- screen
316
- .getByTestId('selector')
317
- .closest('.astryx-multi-selector') as HTMLElement;
318
- const {rerender} = render(
319
- <MultiSelector
320
- label="Fruit"
321
- options={defaultOptions}
322
- value={[]}
323
- onChange={() => {}}
324
- variant="input"
325
- data-testid="selector"
326
- />,
327
- );
328
- const inputClasses = triggerRoot().className;
329
-
330
- rerender(
331
- <MultiSelector
332
- label="Fruit"
333
- options={defaultOptions}
334
- value={[]}
335
- onChange={() => {}}
336
- variant="secondary"
337
- data-testid="selector"
338
- />,
339
- );
340
- const secondary = triggerRoot();
341
- const secondaryClasses = secondary.className;
342
- expect(secondary).toHaveAttribute('data-variant', 'secondary');
343
-
344
- rerender(
345
- <MultiSelector
346
- label="Fruit"
347
- options={defaultOptions}
348
- value={[]}
349
- onChange={() => {}}
350
- variant="ghost"
351
- data-testid="selector"
352
- />,
353
- );
354
- const ghostClasses = triggerRoot().className;
355
-
356
- expect(secondaryClasses).not.toBe(inputClasses);
357
- expect(secondaryClasses).not.toBe(ghostClasses);
358
- });
359
-
360
- it('renders custom option content with renderOption', async () => {
361
- const user = userEvent.setup();
362
- render(
363
- <MultiSelector
364
- label="Fruit"
365
- options={[{value: 'apple', label: 'Apple'}]}
366
- value={[]}
367
- onChange={() => {}}
368
- renderOption={option => (
369
- <span data-testid="custom-option">{option.label}</span>
370
- )}
371
- />,
372
- );
373
-
374
- await user.click(screen.getByRole('combobox'));
375
- expect(screen.getByTestId('custom-option')).toHaveTextContent('Apple');
376
- });
377
- it('renders placeholder when no value selected', () => {
378
- render(
379
- <MultiSelector
380
- label="Fruit"
381
- options={defaultOptions}
382
- value={[]}
383
- onChange={() => {}}
384
- placeholder="Pick fruits..."
385
- />,
386
- );
387
- expect(screen.getByText('Pick fruits...')).toBeInTheDocument();
388
- });
389
-
390
- it('shows count display by default', () => {
391
- render(
392
- <MultiSelector
393
- label="Fruit"
394
- options={defaultOptions}
395
- value={['Apple', 'Banana']}
396
- onChange={() => {}}
397
- />,
398
- );
399
- expect(screen.getByText('2 selected')).toBeInTheDocument();
400
- });
401
-
402
- it('formats the count display with formatValue', () => {
403
- render(
404
- <MultiSelector
405
- label="Spaces"
406
- options={defaultOptions}
407
- value={['Apple', 'Banana']}
408
- onChange={() => {}}
409
- formatValue={items => `${items.length} spaces selected`}
410
- />,
411
- );
412
- expect(screen.getByText('2 spaces selected')).toBeInTheDocument();
413
- });
414
-
415
- it('passes selected values and resolved labels to formatValue', () => {
416
- const formatValue = vi.fn(
417
- (items: {value: string; label: string}[]) =>
418
- `${items[0].label} and ${items.length - 1} more`,
419
- );
420
- render(
421
- <MultiSelector
422
- label="Fruit"
423
- options={[
424
- {value: 'a', label: 'Apple'},
425
- {value: 'b', label: 'Banana'},
426
- ]}
427
- value={['a', 'b']}
428
- onChange={() => {}}
429
- formatValue={formatValue}
430
- />,
431
- );
432
- expect(formatValue).toHaveBeenCalledWith([
433
- {value: 'a', label: 'Apple'},
434
- {value: 'b', label: 'Banana'},
435
- ]);
436
- expect(screen.getByText('Apple and 1 more')).toBeInTheDocument();
437
- });
438
-
439
- it('formats the labels display with formatValue', () => {
440
- render(
441
- <MultiSelector
442
- label="Fruit"
443
- options={defaultOptions}
444
- value={['Apple', 'Banana', 'Orange']}
445
- onChange={() => {}}
446
- triggerDisplay="labels"
447
- formatValue={items => items.map(item => item.label).join(' & ')}
448
- />,
449
- );
450
- expect(screen.getByText('Apple & Banana & Orange')).toBeInTheDocument();
451
- });
452
-
453
- it('ignores formatValue in badges display', () => {
454
- render(
455
- <MultiSelector
456
- label="Fruit"
457
- options={defaultOptions}
458
- value={['Apple', 'Banana']}
459
- onChange={() => {}}
460
- triggerDisplay="badges"
461
- formatValue={() => 'formatted'}
462
- />,
463
- );
464
- expect(screen.queryByText('formatted')).not.toBeInTheDocument();
465
- const trigger = within(screen.getByRole('combobox'));
466
- expect(trigger.getByText('Apple')).toBeInTheDocument();
467
- expect(trigger.getByText('Banana')).toBeInTheDocument();
468
- });
469
-
470
- it('shows the placeholder rather than calling formatValue when empty', () => {
471
- const formatValue = vi.fn(() => 'formatted');
472
- render(
473
- <MultiSelector
474
- label="Fruit"
475
- options={defaultOptions}
476
- value={[]}
477
- onChange={() => {}}
478
- placeholder="Pick fruits..."
479
- formatValue={formatValue}
480
- />,
481
- );
482
- expect(screen.getByText('Pick fruits...')).toBeInTheDocument();
483
- expect(formatValue).not.toHaveBeenCalled();
484
- });
485
-
486
- it('shows labels display', () => {
487
- render(
488
- <MultiSelector
489
- label="Fruit"
490
- options={defaultOptions}
491
- value={['Apple', 'Banana']}
492
- onChange={() => {}}
493
- triggerDisplay="labels"
494
- />,
495
- );
496
- expect(screen.getByText('Apple, Banana')).toBeInTheDocument();
497
- });
498
-
499
- it('shows labels display with overflow', () => {
500
- render(
501
- <MultiSelector
502
- label="Fruit"
503
- options={['Apple', 'Banana', 'Orange', 'Mango', 'Pineapple']}
504
- value={['Apple', 'Banana', 'Orange', 'Mango', 'Pineapple']}
505
- onChange={() => {}}
506
- triggerDisplay="labels"
507
- />,
508
- );
509
- expect(screen.getByText('Apple, Banana, Orange, +2')).toBeInTheDocument();
510
- });
511
-
512
- it('opens dropdown on click', async () => {
513
- const user = userEvent.setup();
514
- render(
515
- <MultiSelector
516
- label="Fruit"
517
- options={defaultOptions}
518
- value={[]}
519
- onChange={() => {}}
520
- />,
521
- );
522
- const trigger = screen.getByRole('combobox');
523
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
524
-
525
- await user.click(trigger);
526
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
527
- });
528
-
529
- it('toggles item on click without closing dropdown', async () => {
530
- const user = userEvent.setup();
531
- const onChange = vi.fn();
532
- render(
533
- <MultiSelector
534
- label="Fruit"
535
- options={defaultOptions}
536
- value={[]}
537
- onChange={onChange}
538
- />,
539
- );
540
-
541
- await user.click(screen.getByRole('combobox'));
542
-
543
- const options = screen.getAllByRole('option', h);
544
- await user.click(options[0]);
545
-
546
- expect(onChange).toHaveBeenCalledWith(['Apple']);
547
- // Dropdown should still be open
548
- expect(screen.getByRole('combobox')).toHaveAttribute(
549
- 'aria-expanded',
550
- 'true',
551
- );
552
- });
553
-
554
- it('deselects item when clicking selected item', async () => {
555
- const user = userEvent.setup();
556
- const onChange = vi.fn();
557
- render(
558
- <MultiSelector
559
- label="Fruit"
560
- options={defaultOptions}
561
- value={['Apple', 'Banana']}
562
- onChange={onChange}
563
- />,
564
- );
565
-
566
- await user.click(screen.getByRole('combobox'));
567
- const options = screen.getAllByRole('option', h);
568
- await user.click(options[0]); // Click Apple to deselect
569
-
570
- expect(onChange).toHaveBeenCalledWith(['Banana']);
571
- });
572
-
573
- it('does not toggle disabled items', async () => {
574
- const user = userEvent.setup();
575
- const onChange = vi.fn();
576
- render(
577
- <MultiSelector
578
- label="Fruit"
579
- options={[
580
- {value: 'apple', label: 'Apple', disabled: true},
581
- {value: 'banana', label: 'Banana'},
582
- ]}
583
- value={[]}
584
- onChange={onChange}
585
- />,
586
- );
587
-
588
- await user.click(screen.getByRole('combobox'));
589
- const options = screen.getAllByRole('option', h);
590
- await user.click(options[0]); // Click disabled Apple
591
-
592
- expect(onChange).not.toHaveBeenCalled();
593
- });
594
-
595
- it('renders disabled state', () => {
596
- render(
597
- <MultiSelector
598
- label="Fruit"
599
- options={defaultOptions}
600
- value={[]}
601
- onChange={() => {}}
602
- isDisabled
603
- />,
604
- );
605
- const trigger = screen.getByRole('combobox');
606
- expect(trigger).not.toHaveAttribute('disabled');
607
- expect(trigger).toHaveAttribute('aria-disabled', 'true');
608
- });
609
-
610
- it('has correct ARIA attributes', () => {
611
- render(
612
- <MultiSelector
613
- label="Fruit"
614
- options={defaultOptions}
615
- value={[]}
616
- onChange={() => {}}
617
- isRequired
618
- />,
619
- );
620
- const trigger = screen.getByRole('combobox');
621
- expect(trigger).toHaveAttribute('aria-haspopup', 'listbox');
622
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
623
- expect(trigger).toHaveAttribute('aria-required', 'true');
624
- });
625
-
626
- it('shows error status with aria-invalid', () => {
627
- render(
628
- <MultiSelector
629
- label="Fruit"
630
- options={defaultOptions}
631
- value={[]}
632
- onChange={() => {}}
633
- status={{type: 'error', message: 'Required'}}
634
- />,
635
- );
636
- const trigger = screen.getByRole('combobox');
637
- expect(trigger).toHaveAttribute('aria-invalid', 'true');
638
- expect(screen.getByText('Required')).toBeInTheDocument();
639
- });
640
-
641
- it('closes on Escape', async () => {
642
- const user = userEvent.setup();
643
- render(
644
- <MultiSelector
645
- label="Fruit"
646
- options={defaultOptions}
647
- value={[]}
648
- onChange={() => {}}
649
- />,
650
- );
651
-
652
- const trigger = screen.getByRole('combobox');
653
- await user.click(trigger);
654
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
655
-
656
- await user.keyboard('{Escape}');
657
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
658
- });
659
-
660
- it('closes combobox on Tab and moves focus to next element', async () => {
661
- const user = userEvent.setup();
662
- render(
663
- <>
664
- <MultiSelector
665
- label="Fruit"
666
- options={defaultOptions}
667
- value={[]}
668
- onChange={() => {}}
669
- />
670
- <button type="button">Next</button>
671
- </>,
672
- );
673
-
674
- const trigger = screen.getByRole('combobox');
675
- await user.click(trigger);
676
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
677
-
678
- await user.keyboard('{Tab}');
679
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
680
- expect(screen.getByRole('button', {name: 'Next'})).toHaveFocus();
681
- });
682
-
683
- it('supports keyboard navigation with ArrowDown/ArrowUp', async () => {
684
- const user = userEvent.setup();
685
- render(
686
- <MultiSelector
687
- label="Fruit"
688
- options={defaultOptions}
689
- value={[]}
690
- onChange={() => {}}
691
- />,
692
- );
693
-
694
- const trigger = screen.getByRole('combobox');
695
- await user.click(trigger);
696
-
697
- await user.keyboard('{ArrowDown}');
698
- const activeId = trigger.getAttribute('aria-activedescendant');
699
- expect(activeId).toBeTruthy();
700
- });
701
-
702
- it('End/Home jump the highlight to the last/first option (non-search)', async () => {
703
- const user = userEvent.setup();
704
- render(
705
- <MultiSelector
706
- label="Fruit"
707
- options={defaultOptions}
708
- value={[]}
709
- onChange={() => {}}
710
- />,
711
- );
712
-
713
- const trigger = screen.getByRole('combobox');
714
- await user.click(trigger);
715
- const options = screen.getAllByRole('option', h);
716
-
717
- await user.keyboard('{End}');
718
- expect(trigger).toHaveAttribute(
719
- 'aria-activedescendant',
720
- options[options.length - 1].id,
721
- );
722
- await user.keyboard('{Home}');
723
- expect(trigger).toHaveAttribute('aria-activedescendant', options[0].id);
724
- });
725
-
726
- it('toggles item with Enter key', async () => {
727
- const user = userEvent.setup();
728
- const onChange = vi.fn();
729
- render(
730
- <MultiSelector
731
- label="Fruit"
732
- options={defaultOptions}
733
- value={[]}
734
- onChange={onChange}
735
- />,
736
- );
737
-
738
- const trigger = screen.getByRole('combobox');
739
- await user.click(trigger);
740
- await user.keyboard('{Enter}');
741
- expect(onChange).toHaveBeenCalledWith(['Apple']);
742
- });
743
-
744
- it('does not toggle the highlighted option on a composing Enter (IME)', async () => {
745
- const user = userEvent.setup();
746
- const onChange = vi.fn();
747
- render(
748
- <MultiSelector
749
- label="Fruit"
750
- options={defaultOptions}
751
- value={[]}
752
- onChange={onChange}
753
- hasSearch
754
- />,
755
- );
756
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
757
- const search = screen.getByRole('combobox', h);
758
- // Filter to Banana and highlight it so an unguarded Enter would toggle it.
759
- await user.type(search, 'ban');
760
- await user.keyboard('{ArrowDown}');
761
- expect(search).toHaveAttribute('aria-activedescendant');
762
-
763
- // The composing keydown (isComposing / legacy keyCode 229) that commits an
764
- // IME candidate must not be read as "toggle the highlighted option".
765
- fireEvent.keyDown(search, {key: 'Enter', isComposing: true});
766
- expect(onChange).not.toHaveBeenCalled();
767
- fireEvent.keyDown(search, {key: 'Enter', keyCode: 229});
768
- expect(onChange).not.toHaveBeenCalled();
769
-
770
- // A real, non-composing Enter still toggles the highlighted option.
771
- fireEvent.keyDown(search, {key: 'Enter'});
772
- expect(onChange).toHaveBeenCalledWith(['Banana']);
773
- });
774
-
775
- it('toggles the correct item when selected items are sorted to top', async () => {
776
- const user = userEvent.setup();
777
- const onChange = vi.fn();
778
- // Orange is selected, so sorted order is: Orange, Apple, Banana
779
- render(
780
- <MultiSelector
781
- label="Fruit"
782
- options={defaultOptions}
783
- value={['Orange']}
784
- onChange={onChange}
785
- />,
786
- );
787
-
788
- const trigger = screen.getByRole('combobox');
789
- await user.click(trigger);
790
- // highlightedIndex starts at 0 which is Orange (sorted first)
791
- await user.keyboard('{ArrowDown}');
792
- // Now at index 1 which should be Apple
793
- await user.keyboard('{Enter}');
794
- expect(onChange).toHaveBeenCalledWith(['Orange', 'Apple']);
795
- });
796
-
797
- it('renders select-all checkbox when hasSelectAll', async () => {
798
- const user = userEvent.setup();
799
- render(
800
- <MultiSelector
801
- label="Fruit"
802
- options={defaultOptions}
803
- value={[]}
804
- onChange={() => {}}
805
- hasSelectAll
806
- />,
807
- );
808
-
809
- await user.click(screen.getByRole('combobox'));
810
- expect(screen.getByText('Select all')).toBeInTheDocument();
811
- });
812
-
813
- it('select-all selects all enabled items', async () => {
814
- const user = userEvent.setup();
815
- const onChange = vi.fn();
816
- render(
817
- <MultiSelector
818
- label="Fruit"
819
- options={[
820
- {value: 'apple', label: 'Apple'},
821
- {value: 'banana', label: 'Banana', disabled: true},
822
- {value: 'orange', label: 'Orange'},
823
- ]}
824
- value={[]}
825
- onChange={onChange}
826
- hasSelectAll
827
- />,
828
- );
829
-
830
- await user.click(screen.getByRole('combobox'));
831
- const selectAll = screen.getByText('Select all');
832
- await user.click(selectAll);
833
-
834
- expect(onChange).toHaveBeenCalledWith(['apple', 'orange']);
835
- });
836
-
837
- it('select-all deselects all when all are selected', async () => {
838
- const user = userEvent.setup();
839
- const onChange = vi.fn();
840
- render(
841
- <MultiSelector
842
- label="Fruit"
843
- options={defaultOptions}
844
- value={['Apple', 'Banana', 'Orange']}
845
- onChange={onChange}
846
- hasSelectAll
847
- />,
848
- );
849
-
850
- await user.click(screen.getByRole('combobox'));
851
- const selectAll = screen.getByText('Select all');
852
- await user.click(selectAll);
853
-
854
- expect(onChange).toHaveBeenCalledWith([]);
855
- });
856
-
857
- it('select-all is a role="option" in the listbox', async () => {
858
- const user = userEvent.setup();
859
- render(
860
- <MultiSelector
861
- label="Fruit"
862
- options={defaultOptions}
863
- value={[]}
864
- onChange={() => {}}
865
- hasSelectAll
866
- />,
867
- );
868
-
869
- await user.click(screen.getByRole('combobox'));
870
- const options = screen.getAllByRole('option', h);
871
- expect(options[0]).toHaveTextContent('Select all');
872
- });
873
-
874
- it('select-all accessible name reflects none/partial/all selection', async () => {
875
- const user = userEvent.setup();
876
- const options = [
877
- {value: 'apple', label: 'Apple'},
878
- {value: 'banana', label: 'Banana'},
879
- ];
880
- const {rerender} = render(
881
- <MultiSelector
882
- label="Fruit"
883
- options={options}
884
- value={[]}
885
- onChange={() => {}}
886
- hasSelectAll
887
- />,
888
- );
889
-
890
- await user.click(screen.getByRole('combobox'));
891
-
892
- // None selected: plain name, not selected
893
- let selectAll = screen.getAllByRole('option', h)[0];
894
- expect(selectAll).not.toHaveAccessibleName(/partially selected/);
895
- expect(selectAll).toHaveAttribute('aria-selected', 'false');
896
-
897
- // Partial: aria-selected="mixed" is invalid on role="option", so the
898
- // indeterminate state must be conveyed through the accessible name.
899
- rerender(
900
- <MultiSelector
901
- label="Fruit"
902
- options={options}
903
- value={['apple']}
904
- onChange={() => {}}
905
- hasSelectAll
906
- />,
907
- );
908
- selectAll = screen.getAllByRole('option', h)[0];
909
- expect(selectAll).toHaveAccessibleName('Select all, partially selected');
910
- expect(selectAll).toHaveAttribute('aria-selected', 'false');
911
-
912
- // All selected: plain name again, selected
913
- rerender(
914
- <MultiSelector
915
- label="Fruit"
916
- options={options}
917
- value={['apple', 'banana']}
918
- onChange={() => {}}
919
- hasSelectAll
920
- />,
921
- );
922
- selectAll = screen.getAllByRole('option', h)[0];
923
- expect(selectAll).not.toHaveAccessibleName(/partially selected/);
924
- expect(selectAll).toHaveAttribute('aria-selected', 'true');
925
- });
926
-
927
- it('select-all toggles via keyboard Enter', async () => {
928
- const user = userEvent.setup();
929
- const onChange = vi.fn();
930
- render(
931
- <MultiSelector
932
- label="Fruit"
933
- options={defaultOptions}
934
- value={[]}
935
- onChange={onChange}
936
- hasSelectAll
937
- />,
938
- );
939
-
940
- const trigger = screen.getByRole('combobox');
941
- await user.click(trigger);
942
- // highlightedIndex starts at 0 which is select-all
943
- await user.keyboard('{Enter}');
944
- expect(onChange).toHaveBeenCalledWith(['Apple', 'Banana', 'Orange']);
945
- });
946
-
947
- it('renders search input when hasSearch', async () => {
948
- const user = userEvent.setup();
949
- render(
950
- <MultiSelector
951
- label="Fruit"
952
- options={defaultOptions}
953
- value={[]}
954
- onChange={() => {}}
955
- hasSearch
956
- />,
957
- );
958
-
959
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
960
- const searchInput = screen.getByRole('combobox', h);
961
- expect(searchInput).toBeInTheDocument();
962
- expect(searchInput).toHaveAttribute('aria-autocomplete', 'list');
963
- });
964
-
965
- it('filters options when searching', async () => {
966
- const user = userEvent.setup();
967
- render(
968
- <MultiSelector
969
- label="Fruit"
970
- options={defaultOptions}
971
- value={[]}
972
- onChange={() => {}}
973
- hasSearch
974
- />,
975
- );
976
-
977
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
978
- const searchInput = screen.getByRole('combobox', h);
979
- await user.type(searchInput, 'app');
980
-
981
- const options = screen.getAllByRole('option', h);
982
- expect(options).toHaveLength(1);
983
- });
984
-
985
- describe('grouped search', () => {
986
- const GROUPED = [
987
- {
988
- type: 'section' as const,
989
- title: 'Citrus',
990
- options: [
991
- {value: 'orange', label: 'Orange'},
992
- {value: 'lemon', label: 'Lemon'},
993
- ],
994
- },
995
- {
996
- type: 'section' as const,
997
- title: 'Berries',
998
- options: [
999
- {value: 'strawberry', label: 'Strawberry'},
1000
- {value: 'blueberry', label: 'Blueberry'},
1001
- ],
1002
- },
1003
- ];
1004
-
1005
- it('keeps the group header above matching items while searching', async () => {
1006
- const user = userEvent.setup();
1007
- render(
1008
- <MultiSelector
1009
- label="Fruit"
1010
- options={GROUPED}
1011
- value={[]}
1012
- onChange={() => {}}
1013
- hasSearch
1014
- />,
1015
- );
1016
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
1017
- await user.type(screen.getByRole('combobox', h), 'orange');
1018
-
1019
- expect(
1020
- screen.getByRole('group', {name: 'Citrus', ...h}),
1021
- ).toBeInTheDocument();
1022
- const options = screen.getAllByRole('option', h);
1023
- expect(options).toHaveLength(1);
1024
- expect(options[0]).toHaveTextContent('Orange');
1025
- });
1026
-
1027
- it('hides a group whose items have no match', async () => {
1028
- const user = userEvent.setup();
1029
- render(
1030
- <MultiSelector
1031
- label="Fruit"
1032
- options={GROUPED}
1033
- value={[]}
1034
- onChange={() => {}}
1035
- hasSearch
1036
- />,
1037
- );
1038
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
1039
- await user.type(screen.getByRole('combobox', h), 'berry');
1040
-
1041
- expect(
1042
- screen.getByRole('group', {name: 'Berries', ...h}),
1043
- ).toBeInTheDocument();
1044
- expect(
1045
- screen.queryByRole('group', {name: 'Citrus', ...h}),
1046
- ).not.toBeInTheDocument();
1047
- expect(screen.getAllByRole('option', h)).toHaveLength(2);
1048
- });
1049
- });
1050
-
1051
- it('PageDown/PageUp jump the highlight to the last/first filtered option', async () => {
1052
- const user = userEvent.setup();
1053
- render(
1054
- <MultiSelector
1055
- label="Fruit"
1056
- options={defaultOptions}
1057
- value={[]}
1058
- onChange={() => {}}
1059
- hasSearch
1060
- />,
1061
- );
1062
-
1063
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
1064
- const searchInput = screen.getByRole('combobox', h);
1065
- // Filter to Banana and Orange so "last" means last *visible* option.
1066
- await user.type(searchInput, 'an');
1067
- const options = screen.getAllByRole('option', h);
1068
- expect(options).toHaveLength(2);
1069
-
1070
- await user.keyboard('{PageDown}');
1071
- expect(searchInput).toHaveAttribute(
1072
- 'aria-activedescendant',
1073
- options[options.length - 1].id,
1074
- );
1075
- await user.keyboard('{PageUp}');
1076
- expect(searchInput).toHaveAttribute('aria-activedescendant', options[0].id);
1077
- });
1078
-
1079
- it('Home/End move the search caret, not the option highlight', async () => {
1080
- const user = userEvent.setup();
1081
- render(
1082
- <MultiSelector
1083
- label="Fruit"
1084
- options={defaultOptions}
1085
- value={[]}
1086
- onChange={() => {}}
1087
- hasSearch
1088
- />,
1089
- );
1090
-
1091
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
1092
- const searchInput = screen.getByRole<HTMLInputElement>('combobox', h);
1093
- await user.type(searchInput, 'an');
1094
- expect(searchInput.selectionStart).toBe(2);
1095
- const activeBefore = searchInput.getAttribute('aria-activedescendant');
1096
- // Home/End stay on the input for caret movement (APG editable combobox);
1097
- // the option highlight must not move.
1098
- await user.keyboard('{Home}');
1099
- expect(searchInput.selectionStart).toBe(0);
1100
- expect(searchInput.getAttribute('aria-activedescendant')).toBe(
1101
- activeBefore,
1102
- );
1103
- await user.keyboard('{End}');
1104
- expect(searchInput.selectionStart).toBe(2);
1105
- expect(searchInput.getAttribute('aria-activedescendant')).toBe(
1106
- activeBefore,
1107
- );
1108
- });
1109
-
1110
- it('shows empty state when search has no results', async () => {
1111
- const user = userEvent.setup();
1112
- render(
1113
- <MultiSelector
1114
- label="Fruit"
1115
- options={defaultOptions}
1116
- value={[]}
1117
- onChange={() => {}}
1118
- hasSearch
1119
- />,
1120
- );
1121
-
1122
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
1123
- const searchInput = screen.getByRole('combobox', h);
1124
- await user.type(searchInput, 'xyz');
1125
-
1126
- expect(
1127
- within(screen.getByRole('listbox', h)).getByText('No results found'),
1128
- ).toBeInTheDocument();
1129
- });
1130
-
1131
- it('empty-state message is not exposed as a listbox child', async () => {
1132
- const user = userEvent.setup();
1133
- render(
1134
- <MultiSelector
1135
- label="Fruit"
1136
- options={defaultOptions}
1137
- value={[]}
1138
- onChange={() => {}}
1139
- hasSearch
1140
- />,
1141
- );
1142
-
1143
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
1144
- await user.type(screen.getByRole('combobox', h), 'xyz');
1145
-
1146
- // role="listbox" only permits option/group children — the visual
1147
- // empty-state message must be presentational (it is announced through
1148
- // the result-count live region instead).
1149
- const listbox = screen.getByRole('listbox', h);
1150
- const empty = within(listbox).getByText('No results found');
1151
- expect(empty).toHaveAttribute('role', 'presentation');
1152
- });
1153
-
1154
- it('renders emptySearchText in place of the default message', async () => {
1155
- const user = userEvent.setup();
1156
- render(
1157
- <MultiSelector
1158
- label="Fruit"
1159
- options={defaultOptions}
1160
- value={[]}
1161
- onChange={() => {}}
1162
- hasSearch
1163
- emptySearchText="Nothing like that here"
1164
- />,
1165
- );
1166
-
1167
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
1168
- await user.type(screen.getByRole('combobox', h), 'xyz');
1169
-
1170
- const listbox = screen.getByRole('listbox', h);
1171
- expect(
1172
- within(listbox).getByText('Nothing like that here'),
1173
- ).toBeInTheDocument();
1174
- expect(
1175
- within(listbox).queryByText('No results found'),
1176
- ).not.toBeInTheDocument();
1177
- });
1178
-
1179
- it('renders emptyText, not emptySearchText, with no options and no query', async () => {
1180
- const user = userEvent.setup();
1181
- render(
1182
- <MultiSelector
1183
- label="Fruit"
1184
- options={[]}
1185
- value={[]}
1186
- onChange={() => {}}
1187
- hasSearch
1188
- emptySearchText="Nothing like that here"
1189
- />,
1190
- );
1191
-
1192
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
1193
-
1194
- const listbox = screen.getByRole('listbox', h);
1195
- expect(within(listbox).getByText('No options')).toBeInTheDocument();
1196
- expect(
1197
- within(listbox).queryByText('Nothing like that here'),
1198
- ).not.toBeInTheDocument();
1199
- });
1200
-
1201
- it('announces emptySearchText, not the catalog default', async () => {
1202
- const user = userEvent.setup();
1203
- render(
1204
- <MultiSelector
1205
- label="Fruit"
1206
- options={defaultOptions}
1207
- value={[]}
1208
- onChange={() => {}}
1209
- hasSearch
1210
- emptySearchText="Nothing like that here"
1211
- />,
1212
- );
1213
-
1214
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
1215
- await enterQuery(user, screen.getByRole('combobox', h), 'xyz');
1216
-
1217
- // The panel message is role="presentation", so the live region is the
1218
- // only thing a screen reader gets — it has to say the same words.
1219
- await waitFor(() =>
1220
- expect(politeRegion()?.textContent).toBe('Nothing like that here'),
1221
- );
1222
- });
1223
-
1224
- // `emptySearchText` is typed ReactNode, which invites an element — a
1225
- // "no results, create one" row is exactly what a dead end is for. The
1226
- // announcement used to take the prop only when it was a string and
1227
- // announce the catalog default otherwise, so a screen-reader user was told
1228
- // something the sighted user was not reading (`spec:AST-056` AR1).
1229
- it('announces the text of an element emptySearchText', async () => {
1230
- const user = userEvent.setup();
1231
- render(
1232
- <MultiSelector
1233
- label="Fruit"
1234
- options={defaultOptions}
1235
- value={[]}
1236
- onChange={() => {}}
1237
- hasSearch
1238
- emptySearchText={
1239
- <span>
1240
- Nothing like that here. <a href="/new">Add a fruit</a>
1241
- </span>
1242
- }
1243
- />,
1244
- );
1245
-
1246
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
1247
- await enterQuery(user, screen.getByRole('combobox', h), 'xyz');
1248
-
1249
- await waitFor(() =>
1250
- expect(politeRegion()?.textContent).toBe(
1251
- 'Nothing like that here. Add a fruit',
1252
- ),
1253
- );
1254
- });
1255
-
1256
- it('announces the text of an element emptyText', async () => {
1257
- const user = userEvent.setup();
1258
- render(
1259
- <MultiSelector
1260
- label="Fruit"
1261
- options={[]}
1262
- value={[]}
1263
- onChange={() => {}}
1264
- emptyText={
1265
- <span>
1266
- Add a fruit first<span aria-hidden="true"> →</span>
1267
- </span>
1268
- }
1269
- />,
1270
- );
1271
-
1272
- await user.click(screen.getByRole('combobox', {name: 'Fruit'}));
1273
-
1274
- // The decorative arrow is hidden from the accessibility tree on screen,
1275
- // so it stays out of the announcement too.
1276
- await waitFor(() =>
1277
- expect(politeRegion()?.textContent).toBe('Add a fruit first'),
1278
- );
1279
- });
1280
-
1281
- it('announces text a child component renders, which is only in the DOM', async () => {
1282
- const user = userEvent.setup();
1283
- // The words exist nowhere in the node the caller passed — they are
1284
- // produced inside this component's own render. Reading the DOM after
1285
- // render is what makes them announceable.
1286
- function Message() {
1287
- return <span>Nothing like that here</span>;
1288
- }
1289
- render(
1290
- <MultiSelector
1291
- label="Fruit"
1292
- options={defaultOptions}
1293
- value={[]}
1294
- onChange={() => {}}
1295
- hasSearch
1296
- emptySearchText={<Message />}
1297
- />,
1298
- );
1299
-
1300
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
1301
- await enterQuery(user, screen.getByRole('combobox', h), 'xyz');
1302
-
1303
- await waitFor(() =>
1304
- expect(politeRegion()?.textContent).toBe('Nothing like that here'),
1305
- );
1306
- });
1307
-
1308
- it('announces the empty state when opened with no options', async () => {
1309
- const user = userEvent.setup();
1310
- render(
1311
- <MultiSelector
1312
- label="Fruit"
1313
- options={[]}
1314
- value={[]}
1315
- onChange={() => {}}
1316
- emptyText="Add a fruit first"
1317
- />,
1318
- );
1319
-
1320
- await user.click(screen.getByRole('combobox', {name: 'Fruit'}));
1321
-
1322
- await waitFor(() =>
1323
- expect(politeRegion()?.textContent).toBe('Add a fruit first'),
1324
- );
1325
- });
1326
-
1327
- it('shows and announces nothing while isLoading', async () => {
1328
- const user = userEvent.setup();
1329
- render(
1330
- <MultiSelector
1331
- label="Fruit"
1332
- options={[]}
1333
- value={[]}
1334
- onChange={() => {}}
1335
- isLoading
1336
- />,
1337
- );
1338
-
1339
- const trigger = screen.getByRole('combobox', {name: 'Fruit'});
1340
- await user.click(trigger);
1341
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
1342
-
1343
- // useAnnounce writes on the next animation frame, so an immediate read
1344
- // would pass whether or not anything was announced.
1345
- await act(
1346
- async () =>
1347
- void (await new Promise(resolve => requestAnimationFrame(resolve))),
1348
- );
1349
-
1350
- // The options have not arrived, so "No options" would be a claim the
1351
- // component cannot make; the trigger's spinner carries the state.
1352
- const listbox = screen.getByRole('listbox', h);
1353
- expect(within(listbox).queryByText('No options')).not.toBeInTheDocument();
1354
- // Only the busy start is spoken (spec:AST-086 FR8), never an empty or
1355
- // no-results message the panel is suppressing.
1356
- // (The channel clears its text on a timer, so it may already be empty.)
1357
- expect(['', 'Loading']).toContain(politeRegion()?.textContent ?? '');
1358
- });
1359
-
1360
- it('announces nothing while isLoading, matching the silent panel', async () => {
1361
- const user = userEvent.setup();
1362
- render(
1363
- <MultiSelector
1364
- label="Fruit"
1365
- options={defaultOptions}
1366
- value={[]}
1367
- onChange={() => {}}
1368
- hasSearch
1369
- isLoading
1370
- emptySearchText="Nothing like that here"
1371
- />,
1372
- );
1373
-
1374
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
1375
- await user.type(screen.getByRole('combobox', h), 'xyz');
1376
-
1377
- // useAnnounce writes on the next animation frame, so an immediate read
1378
- // would pass whether or not anything was announced.
1379
- await act(
1380
- async () =>
1381
- void (await new Promise(resolve => requestAnimationFrame(resolve))),
1382
- );
1383
-
1384
- // The panel is deliberately blank while loading; the live region is the
1385
- // only channel left, so a result there would be a claim the screen
1386
- // refuses to make.
1387
- const listbox = screen.getByRole('listbox', h);
1388
- expect(
1389
- within(listbox).queryByText('Nothing like that here'),
1390
- ).not.toBeInTheDocument();
1391
- // Only the busy start is spoken (spec:AST-086 FR8), never an empty or
1392
- // no-results message the panel is suppressing.
1393
- // (The channel clears its text on a timer, so it may already be empty.)
1394
- expect(['', 'Loading']).toContain(politeRegion()?.textContent ?? '');
1395
- });
1396
-
1397
- it('renders emptyText when there are no options and no search input', async () => {
1398
- const user = userEvent.setup();
1399
- render(
1400
- <MultiSelector
1401
- label="Fruit"
1402
- options={[]}
1403
- value={[]}
1404
- onChange={() => {}}
1405
- emptyText="Add a fruit first"
1406
- />,
1407
- );
1408
-
1409
- // Without hasSearch the trigger itself is the combobox.
1410
- await user.click(screen.getByRole('combobox', {name: 'Fruit'}));
1411
-
1412
- const listbox = screen.getByRole('listbox', h);
1413
- expect(within(listbox).getByText('Add a fruit first')).toBeInTheDocument();
1414
- });
1415
-
1416
- describe('count trigger text', () => {
1417
- it('shows the default count in the catalog message', () => {
1418
- render(
1419
- <MultiSelector
1420
- label="Fruit"
1421
- options={defaultOptions}
1422
- value={['Apple', 'Banana']}
1423
- onChange={() => {}}
1424
- triggerDisplay="count"
1425
- />,
1426
- );
1427
- expect(screen.getByText('2 selected')).toBeInTheDocument();
1428
- });
1429
-
1430
- it('translates the default count, with plural forms, under another locale', () => {
1431
- render(
1432
- <InternationalizationProvider
1433
- locale="fr"
1434
- overrides={{
1435
- fr: {
1436
- '@astryx.multiSelector.selectedCount':
1437
- '{count, number} {count, plural, one {sélectionné} other {sélectionnés}}',
1438
- },
1439
- }}>
1440
- <MultiSelector
1441
- label="Fruit"
1442
- options={defaultOptions}
1443
- value={['Apple', 'Banana']}
1444
- onChange={() => {}}
1445
- triggerDisplay="count"
1446
- />
1447
- </InternationalizationProvider>,
1448
- );
1449
- expect(screen.getByText('2 sélectionnés')).toBeInTheDocument();
1450
- expect(screen.queryByText('2 selected')).not.toBeInTheDocument();
1451
- });
1452
-
1453
- it('still lets formatValue replace the count text', () => {
1454
- render(
1455
- <MultiSelector
1456
- label="Fruit"
1457
- options={defaultOptions}
1458
- value={['Apple', 'Banana']}
1459
- onChange={() => {}}
1460
- triggerDisplay="count"
1461
- formatValue={items => `${items.length} fruits`}
1462
- />,
1463
- );
1464
- expect(screen.getByText('2 fruits')).toBeInTheDocument();
1465
- });
1466
- });
1467
-
1468
- describe('result announcements', () => {
1469
- it('announces the match count politely while searching', async () => {
1470
- const user = userEvent.setup();
1471
- render(
1472
- <InternationalizationProvider
1473
- locale="fr"
1474
- overrides={{
1475
- fr: {
1476
- '@astryx.multiSelector.resultCount':
1477
- '{count, number} {count, plural, one {résultat} other {résultats}}',
1478
- },
1479
- }}>
1480
- <MultiSelector
1481
- label="Fruit"
1482
- options={defaultOptions}
1483
- value={EMPTY_VALUE}
1484
- onChange={() => {}}
1485
- hasSearch
1486
- />
1487
- </InternationalizationProvider>,
1488
- );
1489
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
1490
- // "an" matches Banana and Orange.
1491
- await enterQuery(user, screen.getByRole('combobox', h), 'an');
1492
- await waitFor(() => {
1493
- // The plural branch of a message no catalog supplies.
1494
- expect(politeRegion()?.textContent).toBe('2 résultats');
1495
- });
1496
- });
1497
-
1498
- it('announces the singular form when one option matches', async () => {
1499
- const user = userEvent.setup();
1500
- render(
1501
- <InternationalizationProvider
1502
- locale="fr"
1503
- overrides={{
1504
- fr: {
1505
- '@astryx.multiSelector.resultCount':
1506
- '{count, number} {count, plural, one {résultat} other {résultats}}',
1507
- },
1508
- }}>
1509
- <MultiSelector
1510
- label="Fruit"
1511
- options={defaultOptions}
1512
- value={EMPTY_VALUE}
1513
- onChange={() => {}}
1514
- hasSearch
1515
- />
1516
- </InternationalizationProvider>,
1517
- );
1518
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
1519
- // "app" matches only Apple. Exact match, so "1 résultats" would fail.
1520
- await enterQuery(user, screen.getByRole('combobox', h), 'app');
1521
- await waitFor(() => {
1522
- expect(politeRegion()?.textContent).toBe('1 résultat');
1523
- });
1524
- });
1525
-
1526
- it('announces the empty-results message when nothing matches', async () => {
1527
- const user = userEvent.setup();
1528
- render(
1529
- <InternationalizationProvider
1530
- locale="fr"
1531
- overrides={{
1532
- fr: {'@astryx.multiSelector.emptySearchResults': 'Aucun résultat'},
1533
- }}>
1534
- <MultiSelector
1535
- label="Fruit"
1536
- options={defaultOptions}
1537
- value={EMPTY_VALUE}
1538
- onChange={() => {}}
1539
- hasSearch
1540
- />
1541
- </InternationalizationProvider>,
1542
- );
1543
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
1544
- await enterQuery(user, screen.getByRole('combobox', h), 'xyz');
1545
- await waitFor(() => {
1546
- expect(politeRegion()?.textContent).toBe('Aucun résultat');
1547
- });
1548
- });
1549
-
1550
- it('speaks the result count from a provider catalog (plural)', async () => {
1551
- const user = userEvent.setup();
1552
- render(
1553
- <InternationalizationProvider
1554
- locale="fr"
1555
- messages={{
1556
- fr: {
1557
- '@astryx.multiSelector.resultCount': {
1558
- defaultMessage:
1559
- '{count, number} {count, plural, one {résultat} other {résultats}}',
1560
- },
1561
- },
1562
- }}>
1563
- <MultiSelector
1564
- label="Fruit"
1565
- options={defaultOptions}
1566
- value={EMPTY_VALUE}
1567
- onChange={() => {}}
1568
- hasSearch
1569
- />
1570
- </InternationalizationProvider>,
1571
- );
1572
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
1573
- // "an" matches Banana and Orange.
1574
- await enterQuery(user, screen.getByRole('combobox', h), 'an');
1575
- await waitFor(() => {
1576
- // Same key through the catalog path rather than `overrides`.
1577
- expect(politeRegion()?.textContent).toBe('2 résultats');
1578
- });
1579
- });
1580
-
1581
- it('does not announce results until the user searches', async () => {
1582
- const user = userEvent.setup();
1583
- render(
1584
- <MultiSelector
1585
- label="Fruit"
1586
- options={defaultOptions}
1587
- value={EMPTY_VALUE}
1588
- onChange={() => {}}
1589
- hasSearch
1590
- />,
1591
- );
1592
- // Popover closed: nothing announced.
1593
- expect(politeRegion()?.textContent ?? '').toBe('');
1594
- // Open with an empty query: still nothing announced.
1595
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
1596
- expect(politeRegion()?.textContent ?? '').toBe('');
1597
- });
1598
- });
1599
-
1600
- it('renders with description', () => {
1601
- render(
1602
- <MultiSelector
1603
- label="Fruit"
1604
- description="Choose your fruits"
1605
- options={defaultOptions}
1606
- value={[]}
1607
- onChange={() => {}}
1608
- />,
1609
- );
1610
- expect(screen.getByText('Choose your fruits')).toBeInTheDocument();
1611
- });
1612
-
1613
- it('supports data-testid', () => {
1614
- render(
1615
- <MultiSelector
1616
- label="Fruit"
1617
- options={defaultOptions}
1618
- value={[]}
1619
- onChange={() => {}}
1620
- data-testid="fruit-selector"
1621
- />,
1622
- );
1623
- expect(screen.getByTestId('fruit-selector')).toBeInTheDocument();
1624
- });
1625
-
1626
- it('renders sections with dividers', async () => {
1627
- const user = userEvent.setup();
1628
- render(
1629
- <MultiSelector
1630
- label="Fruit"
1631
- options={[
1632
- {value: 'apple', label: 'Apple'},
1633
- {
1634
- type: 'section',
1635
- title: 'Citrus',
1636
- options: [
1637
- {value: 'orange', label: 'Orange'},
1638
- {value: 'lemon', label: 'Lemon'},
1639
- ],
1640
- },
1641
- ]}
1642
- value={[]}
1643
- onChange={() => {}}
1644
- />,
1645
- );
1646
-
1647
- await user.click(screen.getByRole('combobox'));
1648
- const options = screen.getAllByRole('option', h);
1649
- expect(options).toHaveLength(3);
1650
- expect(screen.getByText('Citrus')).toBeInTheDocument();
1651
- });
1652
-
1653
- it('shows loading state with a spinner and aria-busy', () => {
1654
- render(
1655
- <MultiSelector
1656
- label="Fruit"
1657
- options={defaultOptions}
1658
- value={[]}
1659
- onChange={() => {}}
1660
- isLoading
1661
- />,
1662
- );
1663
- expect(screen.getByRole('combobox')).toHaveAttribute('aria-busy', 'true');
1664
- expectSilentBusySpinner();
1665
- });
1666
-
1667
- it('renders with custom selectAllLabel', async () => {
1668
- const user = userEvent.setup();
1669
- render(
1670
- <MultiSelector
1671
- label="Fruit"
1672
- options={defaultOptions}
1673
- value={[]}
1674
- onChange={() => {}}
1675
- hasSelectAll
1676
- selectAllLabel="Check all"
1677
- />,
1678
- );
1679
-
1680
- await user.click(screen.getByRole('combobox'));
1681
- expect(screen.getByText('Check all')).toBeInTheDocument();
1682
- });
1683
-
1684
- it('sorts selected items to top', async () => {
1685
- const user = userEvent.setup();
1686
- render(
1687
- <MultiSelector
1688
- label="Fruit"
1689
- options={['Apple', 'Banana', 'Orange']}
1690
- value={['Orange']}
1691
- onChange={() => {}}
1692
- />,
1693
- );
1694
-
1695
- await user.click(screen.getByRole('combobox'));
1696
- const options = screen.getAllByRole('option', h);
1697
- // Orange is selected so it should appear first
1698
- expect(options[0]).toHaveAttribute('aria-selected', 'true');
1699
- expect(options[0]).toHaveTextContent('Orange');
1700
- expect(options[1]).toHaveTextContent('Apple');
1701
- expect(options[2]).toHaveTextContent('Banana');
1702
- });
1703
-
1704
- it('sorts selected items to top within sections', async () => {
1705
- const user = userEvent.setup();
1706
- render(
1707
- <MultiSelector
1708
- label="Fruit"
1709
- options={[
1710
- {
1711
- type: 'section',
1712
- title: 'Citrus',
1713
- options: [
1714
- {value: 'orange', label: 'Orange'},
1715
- {value: 'lemon', label: 'Lemon'},
1716
- {value: 'lime', label: 'Lime'},
1717
- ],
1718
- },
1719
- ]}
1720
- value={['lime']}
1721
- onChange={() => {}}
1722
- />,
1723
- );
1724
-
1725
- await user.click(screen.getByRole('combobox'));
1726
- const options = screen.getAllByRole('option', h);
1727
- // Lime is selected so it should appear first within the section
1728
- expect(options[0]).toHaveTextContent('Lime');
1729
- expect(options[1]).toHaveTextContent('Orange');
1730
- expect(options[2]).toHaveTextContent('Lemon');
1731
- });
1732
-
1733
- it('has displayName', () => {
1734
- expect(MultiSelector.displayName).toBe('MultiSelector');
1735
- });
1736
-
1737
- describe('keyboard accessibility', () => {
1738
- it('trigger is focusable via Tab when enabled', async () => {
1739
- const user = userEvent.setup();
1740
- render(
1741
- <MultiSelector
1742
- label="Fruit"
1743
- options={defaultOptions}
1744
- value={[]}
1745
- onChange={() => {}}
1746
- />,
1747
- );
1748
- await user.tab();
1749
- expect(screen.getByRole('combobox')).toHaveFocus();
1750
- });
1751
-
1752
- it('trigger keeps its tab stop when disabled (spec:AST-085 FR1)', () => {
1753
- render(
1754
- <MultiSelector
1755
- label="Fruit"
1756
- options={defaultOptions}
1757
- value={[]}
1758
- onChange={() => {}}
1759
- isDisabled
1760
- />,
1761
- );
1762
- expect(screen.getByRole('combobox')).toHaveAttribute('tabIndex', '0');
1763
- });
1764
-
1765
- it('opens the listbox with ArrowDown from a focused trigger', async () => {
1766
- const user = userEvent.setup();
1767
- render(
1768
- <MultiSelector
1769
- label="Fruit"
1770
- options={defaultOptions}
1771
- value={[]}
1772
- onChange={() => {}}
1773
- />,
1774
- );
1775
- const trigger = screen.getByRole('combobox');
1776
- await user.tab();
1777
- expect(trigger).toHaveFocus();
1778
- await user.keyboard('{ArrowDown}');
1779
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
1780
- });
1781
-
1782
- it('clear button is reachable by keyboard', () => {
1783
- render(
1784
- <MultiSelector
1785
- label="Fruit"
1786
- options={defaultOptions}
1787
- value={['Apple', 'Banana']}
1788
- onChange={() => {}}
1789
- hasClear
1790
- />,
1791
- );
1792
- const clear = screen.getByRole('button', {name: 'Clear all Fruit'});
1793
- expect(clear).not.toHaveAttribute('tabIndex', '-1');
1794
- });
1795
-
1796
- it('returns focus to the trigger when the clear button is activated by keyboard', async () => {
1797
- function Controlled() {
1798
- const [value, setValue] = useState(['Apple', 'Banana']);
1799
- return (
1800
- <MultiSelector
1801
- label="Fruit"
1802
- options={defaultOptions}
1803
- value={value}
1804
- onChange={setValue}
1805
- hasClear
1806
- />
1807
- );
1808
- }
1809
- const user = userEvent.setup();
1810
- render(<Controlled />);
1811
- screen.getByRole('button', {name: 'Clear all Fruit'}).focus();
1812
- await user.keyboard('{Enter}');
1813
- expect(
1814
- screen.queryByRole('button', {name: 'Clear all Fruit'}),
1815
- ).not.toBeInTheDocument();
1816
- expect(screen.getByRole('combobox')).toHaveFocus();
1817
- });
1818
-
1819
- it('leaves focus in the search input when the clear button is clicked with the list open', async () => {
1820
- function Controlled() {
1821
- const [value, setValue] = useState(['Apple', 'Banana']);
1822
- return (
1823
- <MultiSelector
1824
- label="Fruit"
1825
- options={defaultOptions}
1826
- value={value}
1827
- onChange={setValue}
1828
- hasClear
1829
- hasSearch
1830
- />
1831
- );
1832
- }
1833
- const user = userEvent.setup();
1834
- const {container} = render(<Controlled />);
1835
- await user.click(container.querySelector('[aria-haspopup]') as Element);
1836
- const search = screen.getByRole('combobox', h);
1837
- search.focus();
1838
- await user.click(screen.getByRole('button', {name: 'Clear all Fruit'}));
1839
- expect(
1840
- screen.queryByRole('button', {name: 'Clear all Fruit'}),
1841
- ).not.toBeInTheDocument();
1842
- expect(search).toHaveFocus();
1843
- });
1844
-
1845
- it('scrolls the highlighted option into view during arrow navigation', async () => {
1846
- const scrollIntoView = vi.fn();
1847
- Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
1848
- configurable: true,
1849
- value: scrollIntoView,
1850
- });
1851
- try {
1852
- const user = userEvent.setup();
1853
- const longOptions = Array.from(
1854
- {length: 20},
1855
- (_, i) => `Option ${i + 1}`,
1856
- );
1857
- render(
1858
- <MultiSelector
1859
- label="Fruit"
1860
- options={longOptions}
1861
- value={[]}
1862
- onChange={() => {}}
1863
- />,
1864
- );
1865
-
1866
- const trigger = screen.getByRole('combobox');
1867
- await user.click(trigger);
1868
- scrollIntoView.mockClear();
1869
- await user.keyboard('{ArrowDown}');
1870
- await user.keyboard('{ArrowDown}');
1871
-
1872
- expect(scrollIntoView).toHaveBeenCalledWith({block: 'nearest'});
1873
- } finally {
1874
- delete (HTMLElement.prototype as unknown as {scrollIntoView?: unknown})
1875
- .scrollIntoView;
1876
- }
1877
- });
1878
-
1879
- it('highlights on hover without scrolling, keyboard still scrolls (#6077)', async () => {
1880
- // Hover must highlight only: scrollIntoView under a stationary pointer
1881
- // moves the next option under it, re-highlighting and scrolling again —
1882
- // a runaway auto-scroll loop with no user input.
1883
- const scrollIntoView = vi.fn();
1884
- Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
1885
- configurable: true,
1886
- value: scrollIntoView,
1887
- });
1888
- try {
1889
- const user = userEvent.setup();
1890
- const longOptions = Array.from(
1891
- {length: 20},
1892
- (_, i) => `Option ${i + 1}`,
1893
- );
1894
- render(
1895
- <MultiSelector
1896
- label="Fruit"
1897
- options={longOptions}
1898
- value={[]}
1899
- onChange={() => {}}
1900
- />,
1901
- );
1902
-
1903
- const trigger = screen.getByRole('combobox');
1904
- await user.click(trigger);
1905
- scrollIntoView.mockClear();
1906
-
1907
- const options = screen.getAllByRole('option', {hidden: true});
1908
- fireEvent.mouseEnter(options[3]);
1909
-
1910
- expect(trigger.getAttribute('aria-activedescendant')).toBe(
1911
- options[3].id,
1912
- );
1913
- expect(scrollIntoView).not.toHaveBeenCalled();
1914
-
1915
- await user.keyboard('{ArrowDown}');
1916
- expect(scrollIntoView).toHaveBeenCalledWith({block: 'nearest'});
1917
- } finally {
1918
- delete (HTMLElement.prototype as unknown as {scrollIntoView?: unknown})
1919
- .scrollIntoView;
1920
- }
1921
- });
1922
-
1923
- it('clears all values via Delete on the focused trigger', async () => {
1924
- const user = userEvent.setup();
1925
- const onChange = vi.fn();
1926
- render(
1927
- <MultiSelector
1928
- label="Fruit"
1929
- options={defaultOptions}
1930
- value={['Apple', 'Banana']}
1931
- onChange={onChange}
1932
- hasClear
1933
- />,
1934
- );
1935
- const trigger = screen.getByRole('combobox');
1936
- trigger.focus();
1937
- await user.keyboard('{Delete}');
1938
- expect(onChange).toHaveBeenCalledWith([]);
1939
- });
1940
-
1941
- it('clears all values via Backspace on the focused trigger', async () => {
1942
- const user = userEvent.setup();
1943
- const onChange = vi.fn();
1944
- render(
1945
- <MultiSelector
1946
- label="Fruit"
1947
- options={defaultOptions}
1948
- value={['Apple', 'Banana']}
1949
- onChange={onChange}
1950
- hasClear
1951
- />,
1952
- );
1953
- const trigger = screen.getByRole('combobox');
1954
- trigger.focus();
1955
- await user.keyboard('{Backspace}');
1956
- expect(onChange).toHaveBeenCalledWith([]);
1957
- });
1958
-
1959
- it('does not clear via Delete when nothing is selected', async () => {
1960
- const user = userEvent.setup();
1961
- const onChange = vi.fn();
1962
- render(
1963
- <MultiSelector
1964
- label="Fruit"
1965
- options={defaultOptions}
1966
- value={[]}
1967
- onChange={onChange}
1968
- hasClear
1969
- />,
1970
- );
1971
- const trigger = screen.getByRole('combobox');
1972
- trigger.focus();
1973
- await user.keyboard('{Delete}');
1974
- expect(onChange).not.toHaveBeenCalled();
1975
- });
1976
- });
1977
-
1978
- describe('announcements', () => {
1979
- it('announces the selection count politely when toggling an option', async () => {
1980
- const user = userEvent.setup();
1981
- render(
1982
- <InternationalizationProvider
1983
- locale="fr"
1984
- overrides={{
1985
- fr: {
1986
- '@astryx.multiSelector.selectionCount':
1987
- '{count, number} sur {total, number} sélectionnés',
1988
- },
1989
- }}>
1990
- <MultiSelector
1991
- label="Fruit"
1992
- options={[...ANNOUNCE_OPTIONS]}
1993
- value={EMPTY_VALUE}
1994
- onChange={() => {}}
1995
- />
1996
- </InternationalizationProvider>,
1997
- );
1998
- await user.click(screen.getByRole('combobox'));
1999
- const options = screen.getAllByRole('option', {hidden: true});
2000
- await user.click(options[0]);
2001
- await waitFor(() => {
2002
- // Both arguments land, in the order the message asks for them.
2003
- expect(politeRegion()?.textContent).toBe('1 sur 3 sélectionnés');
2004
- });
2005
- });
2006
-
2007
- it('announces the all-selected message when select-all selects everything', async () => {
2008
- const user = userEvent.setup();
2009
- render(
2010
- <InternationalizationProvider
2011
- locale="fr"
2012
- overrides={{
2013
- fr: {'@astryx.multiSelector.allSelected': 'Tout est là'},
2014
- }}>
2015
- <MultiSelector
2016
- label="Fruit"
2017
- options={[...ANNOUNCE_OPTIONS]}
2018
- value={EMPTY_VALUE}
2019
- onChange={() => {}}
2020
- hasSelectAll
2021
- />
2022
- </InternationalizationProvider>,
2023
- );
2024
- await user.click(screen.getByRole('combobox'));
2025
- await user.click(screen.getByText('Select all'));
2026
- await waitFor(() => {
2027
- expect(politeRegion()?.textContent).toBe('Tout est là');
2028
- });
2029
- });
2030
-
2031
- it('announces the selection-cleared message when clearing', async () => {
2032
- const user = userEvent.setup();
2033
- render(
2034
- <InternationalizationProvider
2035
- locale="fr"
2036
- overrides={{
2037
- fr: {'@astryx.multiSelector.selectionCleared': 'Plus rien'},
2038
- }}>
2039
- <MultiSelector
2040
- label="Fruit"
2041
- options={[...ANNOUNCE_OPTIONS]}
2042
- value={['Apple', 'Banana']}
2043
- onChange={() => {}}
2044
- hasClear
2045
- />
2046
- </InternationalizationProvider>,
2047
- );
2048
- await user.click(screen.getByRole('button', {name: 'Clear all Fruit'}));
2049
- await waitFor(() => {
2050
- expect(politeRegion()?.textContent).toBe('Plus rien');
2051
- });
2052
- });
2053
-
2054
- it('prefers a provider override over the provider catalog', async () => {
2055
- const user = userEvent.setup();
2056
- render(
2057
- <InternationalizationProvider
2058
- locale="fr"
2059
- messages={{
2060
- fr: {
2061
- '@astryx.multiSelector.selectionCount': {
2062
- defaultMessage: '{count, number} du catalogue',
2063
- },
2064
- },
2065
- }}
2066
- overrides={{
2067
- fr: {
2068
- '@astryx.multiSelector.selectionCount':
2069
- '{count, number} remplacé',
2070
- },
2071
- }}>
2072
- <MultiSelector
2073
- label="Fruit"
2074
- options={[...ANNOUNCE_OPTIONS]}
2075
- value={EMPTY_VALUE}
2076
- onChange={() => {}}
2077
- />
2078
- </InternationalizationProvider>,
2079
- );
2080
- await user.click(screen.getByRole('combobox'));
2081
- const options = screen.getAllByRole('option', {hidden: true});
2082
- await user.click(options[0]);
2083
- await waitFor(() => {
2084
- expect(politeRegion()?.textContent).toBe('1 remplacé');
2085
- });
2086
- });
2087
- });
2088
-
2089
- describe('disabledMessage', () => {
2090
- it('shows the reason tooltip on hover when disabled with a reason', async () => {
2091
- render(
2092
- <MultiSelector
2093
- label="Fruit"
2094
- options={defaultOptions}
2095
- value={[]}
2096
- onChange={() => {}}
2097
- isDisabled
2098
- disabledMessage="Select a table first"
2099
- data-testid="fruit-multi-selector"
2100
- />,
2101
- );
2102
-
2103
- // data-testid lands on the trigger; the reason tooltip anchors to the
2104
- // control box around it.
2105
- const container = screen
2106
- .getByTestId('fruit-multi-selector')
2107
- .closest('.astryx-multi-selector') as HTMLElement;
2108
- const tooltip = screen.getByRole('tooltip', h);
2109
- expect(tooltip).toHaveTextContent('Select a table first');
2110
-
2111
- fireEvent.mouseEnter(container);
2112
- await waitFor(() => {
2113
- expect(tooltip).toHaveAttribute('popover-open');
2114
- });
2115
-
2116
- fireEvent.mouseLeave(container);
2117
- await waitFor(() => {
2118
- expect(tooltip).not.toHaveAttribute('popover-open');
2119
- });
2120
- });
2121
-
2122
- it('shows the reason tooltip on keyboard focus', async () => {
2123
- const user = userEvent.setup();
2124
- render(
2125
- <MultiSelector
2126
- label="Fruit"
2127
- options={defaultOptions}
2128
- value={[]}
2129
- onChange={() => {}}
2130
- isDisabled
2131
- disabledMessage="Select a table first"
2132
- />,
2133
- );
2134
-
2135
- const tooltip = screen.getByRole('tooltip', h);
2136
- await user.tab();
2137
- expect(screen.getByRole('combobox')).toHaveFocus();
2138
- await waitFor(() => {
2139
- expect(tooltip).toHaveAttribute('popover-open');
2140
- });
2141
- });
2142
-
2143
- it('does not render a tooltip when not disabled', () => {
2144
- render(
2145
- <MultiSelector
2146
- label="Fruit"
2147
- options={defaultOptions}
2148
- value={[]}
2149
- onChange={() => {}}
2150
- disabledMessage="Select a table first"
2151
- />,
2152
- );
2153
- expect(screen.queryByRole('tooltip', h)).not.toBeInTheDocument();
2154
- });
2155
-
2156
- it('does not render a tooltip when disabled without a reason', () => {
2157
- render(
2158
- <MultiSelector
2159
- label="Fruit"
2160
- options={defaultOptions}
2161
- value={[]}
2162
- onChange={() => {}}
2163
- isDisabled
2164
- />,
2165
- );
2166
- expect(screen.queryByRole('tooltip', h)).not.toBeInTheDocument();
2167
- });
2168
-
2169
- it('keeps the trigger focusable via aria-disabled when a reason is provided', () => {
2170
- render(
2171
- <MultiSelector
2172
- label="Fruit"
2173
- options={defaultOptions}
2174
- value={[]}
2175
- onChange={() => {}}
2176
- isDisabled
2177
- disabledMessage="Select a table first"
2178
- />,
2179
- );
2180
- const trigger = screen.getByRole('combobox');
2181
- expect(trigger).not.toBeDisabled();
2182
- expect(trigger).toHaveAttribute('aria-disabled', 'true');
2183
- expect(trigger).toHaveAttribute('tabIndex', '0');
2184
- });
2185
-
2186
- it('links the reason tooltip from the trigger via aria-describedby', () => {
2187
- render(
2188
- <MultiSelector
2189
- label="Fruit"
2190
- options={defaultOptions}
2191
- value={[]}
2192
- onChange={() => {}}
2193
- isDisabled
2194
- disabledMessage="Select a table first"
2195
- />,
2196
- );
2197
- const trigger = screen.getByRole('combobox');
2198
- const tooltip = screen.getByRole('tooltip', h);
2199
- expect(trigger.getAttribute('aria-describedby')).toContain(tooltip.id);
2200
- });
2201
-
2202
- it('blocks activation while focusable-disabled', async () => {
2203
- const user = userEvent.setup();
2204
- const onChange = vi.fn();
2205
- render(
2206
- <MultiSelector
2207
- label="Fruit"
2208
- options={defaultOptions}
2209
- value={[]}
2210
- onChange={onChange}
2211
- isDisabled
2212
- disabledMessage="Select a table first"
2213
- />,
2214
- );
2215
-
2216
- const trigger = screen.getByRole('combobox');
2217
- await user.click(trigger);
2218
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
2219
-
2220
- await user.keyboard('{Enter}');
2221
- await user.keyboard('{ArrowDown}');
2222
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
2223
- expect(onChange).not.toHaveBeenCalled();
2224
- });
2225
-
2226
- it('stays focusable when disabled without a reason (spec:AST-085 FR1)', () => {
2227
- render(
2228
- <MultiSelector
2229
- label="Fruit"
2230
- options={defaultOptions}
2231
- value={[]}
2232
- onChange={() => {}}
2233
- isDisabled
2234
- />,
2235
- );
2236
- const trigger = screen.getByRole('combobox');
2237
- expect(trigger).not.toHaveAttribute('disabled');
2238
- expect(trigger).toHaveAttribute('aria-disabled', 'true');
2239
- expect(trigger).toHaveAttribute('tabIndex', '0');
2240
- });
2241
- });
2242
- describe('isReadOnly', () => {
2243
- const readOnlyCases = [
2244
- {presentation: 'popover', hasSearch: false},
2245
- {presentation: 'popover', hasSearch: true},
2246
- {presentation: 'bottom-sheet', hasSearch: false},
2247
- {presentation: 'bottom-sheet', hasSearch: true},
2248
- {presentation: 'adaptive', hasSearch: false},
2249
- {presentation: 'adaptive', hasSearch: true},
2250
- ] as const;
2251
-
2252
- const sheetTransitionCases = [
2253
- {presentation: 'bottom-sheet', hasSearch: false},
2254
- {presentation: 'bottom-sheet', hasSearch: true},
2255
- {presentation: 'adaptive', hasSearch: false},
2256
- {presentation: 'adaptive', hasSearch: true},
2257
- ] as const;
2258
-
2259
- const disabledPrecedenceCases = (
2260
- ['popover', 'bottom-sheet', 'adaptive'] as const
2261
- ).flatMap(presentation =>
2262
- [false, true].flatMap(hasSearch =>
2263
- [false, true].map(hasDisabledMessage => ({
2264
- presentation,
2265
- hasSearch,
2266
- hasDisabledMessage,
2267
- })),
2268
- ),
2269
- );
2270
-
2271
- function stubCompactTouch(presentation: 'bottom-sheet' | 'adaptive') {
2272
- if (presentation !== 'adaptive') {
2273
- return;
2274
- }
2275
- vi.mocked(matchMedia).mockImplementation(
2276
- (query: string) =>
2277
- ({
2278
- matches: query === COMPACT_TOUCH_PRESENTATION_QUERY,
2279
- media: query,
2280
- onchange: null,
2281
- addEventListener: vi.fn(),
2282
- removeEventListener: vi.fn(),
2283
- addListener: vi.fn(),
2284
- removeListener: vi.fn(),
2285
- dispatchEvent: vi.fn(),
2286
- }) as MediaQueryList,
2287
- );
2288
- }
2289
-
2290
- it.each(readOnlyCases)(
2291
- 'uses a read-only combobox with no rendered popup ($presentation, search=$hasSearch)',
2292
- async ({presentation, hasSearch}) => {
2293
- const user = userEvent.setup();
2294
- const {container} = render(
2295
- <form>
2296
- <MultiSelector
2297
- label="Fruit"
2298
- htmlName="fruit"
2299
- options={defaultOptions}
2300
- value={['Apple', 'Banana']}
2301
- onChange={() => {}}
2302
- hasClear
2303
- hasSearch={hasSearch}
2304
- presentation={presentation}
2305
- isDefaultOpen
2306
- triggerDisplay="labels"
2307
- isReadOnly
2308
- />
2309
- </form>,
2310
- );
2311
-
2312
- const trigger = screen.getByRole('combobox', {name: 'Fruit'});
2313
- expect(trigger.tagName).toBe('BUTTON');
2314
- expect(trigger).toHaveTextContent('Apple, Banana');
2315
- expect(trigger).toHaveAttribute('aria-readonly', 'true');
2316
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
2317
- const readOnlyDescription = screen.getByText('Read only');
2318
- expect(trigger.getAttribute('aria-describedby')).toContain(
2319
- readOnlyDescription.id,
2320
- );
2321
- expect(trigger).not.toHaveAttribute('aria-controls');
2322
- expect(trigger).not.toHaveAttribute('aria-haspopup');
2323
- expect(screen.getAllByRole('combobox', h)).toHaveLength(1);
2324
- expect(screen.queryByRole('listbox', h)).not.toBeInTheDocument();
2325
- expect(screen.queryByRole('dialog', h)).not.toBeInTheDocument();
2326
- expect(
2327
- screen.queryByRole('button', {name: 'Clear all Fruit'}),
2328
- ).not.toBeInTheDocument();
2329
- expect(
2330
- container.querySelector('.astryx-multi-selector-indicator-icon'),
2331
- ).toBeNull();
2332
- expect(
2333
- container.querySelector('.astryx-multi-selector'),
2334
- ).toHaveAttribute('data-readonly', 'readonly');
2335
- expect(
2336
- new FormData(container.querySelector('form')!).getAll('fruit'),
2337
- ).toEqual(['Apple', 'Banana']);
2338
-
2339
- await user.tab();
2340
- expect(trigger).toHaveFocus();
2341
- },
2342
- );
2343
-
2344
- it('keeps loading feedback while blocking the selection surface', async () => {
2345
- const user = userEvent.setup();
2346
- render(
2347
- <MultiSelector
2348
- label="Fruit"
2349
- options={defaultOptions}
2350
- value={['Apple']}
2351
- onChange={() => {}}
2352
- isLoading
2353
- isReadOnly
2354
- />,
2355
- );
2356
-
2357
- const trigger = screen.getByRole('combobox', {name: 'Fruit'});
2358
- expect(trigger).toHaveAttribute('aria-busy', 'true');
2359
- expectSilentBusySpinner();
2360
-
2361
- await user.click(trigger);
2362
- expect(screen.queryByRole('listbox', h)).not.toBeInTheDocument();
2363
- });
2364
-
2365
- it('blocks pointer, keyboard, and typeahead changes', async () => {
2366
- const user = userEvent.setup();
2367
- const onChange = vi.fn();
2368
- const changeAction = vi.fn();
2369
- render(
2370
- <MultiSelector
2371
- label="Fruit"
2372
- options={defaultOptions}
2373
- value={['Apple']}
2374
- onChange={onChange}
2375
- changeAction={changeAction}
2376
- isReadOnly
2377
- />,
2378
- );
2379
-
2380
- const trigger = screen.getByRole('combobox');
2381
- await user.click(trigger);
2382
- trigger.focus();
2383
- await user.keyboard('{Enter}{ArrowDown}b{Backspace}');
2384
-
2385
- expect(screen.queryByRole('listbox', h)).not.toBeInTheDocument();
2386
- expect(onChange).not.toHaveBeenCalled();
2387
- expect(changeAction).not.toHaveBeenCalled();
2388
- });
2389
-
2390
- it.each([false, true])(
2391
- 'restores popover focus when an open selector becomes read-only (search=%s)',
2392
- async hasSearch => {
2393
- const user = userEvent.setup();
2394
- const selector = (isReadOnly: boolean) => (
2395
- <MultiSelector
2396
- label="Fruit"
2397
- options={defaultOptions}
2398
- value={['Apple']}
2399
- onChange={() => {}}
2400
- hasSearch={hasSearch}
2401
- isReadOnly={isReadOnly}
2402
- />
2403
- );
2404
- const {rerender} = render(selector(false));
2405
- const editableTrigger = screen.getByRole(
2406
- hasSearch ? 'button' : 'combobox',
2407
- {name: 'Fruit'},
2408
- );
2409
- await user.click(editableTrigger);
2410
- if (hasSearch) {
2411
- await waitFor(() =>
2412
- expect(screen.getByRole('combobox', h)).toHaveFocus(),
2413
- );
2414
- }
2415
-
2416
- rerender(selector(true));
2417
-
2418
- const readOnlyTrigger = screen.getByRole('combobox', {name: 'Fruit'});
2419
- await waitFor(() => expect(readOnlyTrigger).toHaveFocus());
2420
- expect(screen.queryByRole('listbox', h)).not.toBeInTheDocument();
2421
- },
2422
- );
2423
-
2424
- it.each(sheetTransitionCases)(
2425
- 'finishes $presentation close and restores focus when read-only changes (search=$hasSearch)',
2426
- async ({presentation, hasSearch}) => {
2427
- stubCompactTouch(presentation);
2428
- const user = userEvent.setup();
2429
- const selector = (isReadOnly: boolean) => (
2430
- <MultiSelector
2431
- label="Fruit"
2432
- options={defaultOptions}
2433
- value={['Apple']}
2434
- onChange={() => {}}
2435
- hasSearch={hasSearch}
2436
- presentation={presentation}
2437
- isReadOnly={isReadOnly}
2438
- />
2439
- );
2440
- const {rerender} = render(selector(false));
2441
- const editableTrigger = screen.getByRole(
2442
- hasSearch ? 'button' : 'combobox',
2443
- {name: 'Fruit'},
2444
- );
2445
- await user.click(editableTrigger);
2446
- const dialog = await screen.findByRole('dialog', {name: 'Fruit'});
2447
- const panel = dialog.querySelector<HTMLElement>('.astryx-bottom-sheet');
2448
- expect(panel).not.toBeNull();
2449
- await waitFor(() =>
2450
- expect(
2451
- hasSearch
2452
- ? screen.getByRole('combobox', h)
2453
- : screen.getByRole('listbox'),
2454
- ).toHaveFocus(),
2455
- );
2456
-
2457
- rerender(selector(true));
2458
-
2459
- await waitFor(() => expect(dialog).toHaveAttribute('inert'));
2460
- fireEvent.transitionEnd(panel!, {propertyName: 'transform'});
2461
- const readOnlyTrigger = screen.getByRole('combobox', {name: 'Fruit'});
2462
- await waitFor(() => expect(readOnlyTrigger).toHaveFocus());
2463
- expect(screen.queryByRole('dialog', h)).not.toBeInTheDocument();
2464
- expect(screen.queryByRole('listbox', h)).not.toBeInTheDocument();
2465
- },
2466
- );
2467
-
2468
- it.each(disabledPrecedenceCases)(
2469
- 'matches disabled-only semantics for $presentation (search=$hasSearch, reason=$hasDisabledMessage)',
2470
- async ({presentation, hasSearch, hasDisabledMessage}) => {
2471
- if (presentation === 'adaptive') {
2472
- stubCompactTouch(presentation);
2473
- }
2474
- const user = userEvent.setup();
2475
- const onChange = vi.fn();
2476
- const selector = (isReadOnly: boolean) => (
2477
- <form>
2478
- <MultiSelector
2479
- label="Fruit"
2480
- htmlName="fruit"
2481
- options={defaultOptions}
2482
- value={['Apple', 'Banana']}
2483
- onChange={onChange}
2484
- hasClear
2485
- hasSearch={hasSearch}
2486
- presentation={presentation}
2487
- isDisabled
2488
- isReadOnly={isReadOnly}
2489
- disabledMessage={
2490
- hasDisabledMessage ? 'Locked by policy' : undefined
2491
- }
2492
- />
2493
- </form>
2494
- );
2495
- const {container, rerender} = render(selector(false));
2496
- const getTrigger = () =>
2497
- screen.getByRole(hasSearch ? 'button' : 'combobox', {name: 'Fruit'});
2498
- const snapshot = () => {
2499
- const trigger = getTrigger();
2500
- const root = container.querySelector('.astryx-multi-selector');
2501
- return {
2502
- tagName: trigger.tagName,
2503
- role: trigger.getAttribute('role'),
2504
- hasPopup: trigger.getAttribute('aria-haspopup'),
2505
- expanded: trigger.getAttribute('aria-expanded'),
2506
- controls: trigger.getAttribute('aria-controls'),
2507
- readOnly: trigger.getAttribute('aria-readonly'),
2508
- disabled: trigger.matches(':disabled'),
2509
- ariaDisabled: trigger.getAttribute('aria-disabled'),
2510
- tabIndex: trigger.tabIndex,
2511
- rootClassName: root?.className,
2512
- rootDisabled: root?.getAttribute('data-disabled'),
2513
- rootReadOnly: root?.getAttribute('data-readonly'),
2514
- hasIndicator:
2515
- root?.querySelector('.astryx-multi-selector-indicator-icon') !=
2516
- null,
2517
- listboxes: screen.queryAllByRole('listbox', h).length,
2518
- dialogs: screen.queryAllByRole('dialog', h).length,
2519
- formEntries: [
2520
- ...new FormData(container.querySelector('form')!).entries(),
2521
- ],
2522
- };
2523
- };
2524
- const disabledOnly = snapshot();
2525
-
2526
- rerender(selector(true));
2527
-
2528
- expect(snapshot()).toEqual(disabledOnly);
2529
- expect(disabledOnly.rootDisabled).toBe('disabled');
2530
- expect(disabledOnly.rootReadOnly).toBeNull();
2531
- expect(disabledOnly.readOnly).toBeNull();
2532
- expect(disabledOnly.formEntries).toEqual([]);
2533
- const trigger = getTrigger();
2534
- await user.click(trigger);
2535
- trigger.focus();
2536
- await user.keyboard('{Enter}{ArrowDown}b{Backspace}');
2537
- expect(onChange).not.toHaveBeenCalled();
2538
- },
2539
- );
2540
- });
2541
-
2542
- describe('form participation', () => {
2543
- it('submits one entry per selected value under htmlName', () => {
2544
- const {container} = render(
2545
- <form>
2546
- <MultiSelector
2547
- label="Fruit"
2548
- htmlName="fruit"
2549
- options={['Apple', 'Banana', 'Orange']}
2550
- value={['Apple', 'Orange']}
2551
- onChange={() => {}}
2552
- />
2553
- </form>,
2554
- );
2555
- const data = new FormData(container.querySelector('form')!);
2556
- expect(data.getAll('fruit')).toEqual(['Apple', 'Orange']);
2557
- });
2558
-
2559
- it('is excluded from form data when disabled', () => {
2560
- const {container} = render(
2561
- <form>
2562
- <MultiSelector
2563
- label="Fruit"
2564
- htmlName="fruit"
2565
- options={['Apple']}
2566
- value={['Apple']}
2567
- onChange={() => {}}
2568
- isDisabled
2569
- />
2570
- </form>,
2571
- );
2572
- expect([
2573
- ...new FormData(container.querySelector('form')!).keys(),
2574
- ]).toEqual([]);
2575
- });
2576
- });
2577
- });
2578
-
2579
- describe('MultiSelector statusVariant forwarding', () => {
2580
- it('defaults to attached (status renders with data-variant="attached")', () => {
2581
- const {container} = render(
2582
- <MultiSelector
2583
- label="Fruit"
2584
- options={['Apple', 'Banana']}
2585
- value={[]}
2586
- onChange={() => {}}
2587
- status={{type: 'error', message: 'Required'}}
2588
- />,
2589
- );
2590
- expect(container.querySelector('.astryx-field-status')).toHaveAttribute(
2591
- 'data-variant',
2592
- 'attached',
2593
- );
2594
- });
2595
-
2596
- it('forwards statusVariant="detached" to the underlying Field status', () => {
2597
- const {container} = render(
2598
- <MultiSelector
2599
- label="Fruit"
2600
- options={['Apple', 'Banana']}
2601
- value={[]}
2602
- onChange={() => {}}
2603
- status={{type: 'error', message: 'Required'}}
2604
- statusVariant="detached"
2605
- />,
2606
- );
2607
- expect(container.querySelector('.astryx-field-status')).toHaveAttribute(
2608
- 'data-variant',
2609
- 'detached',
2610
- );
2611
- });
2612
-
2613
- it('keeps the on-field status icon for the attached variant', () => {
2614
- const {container} = render(
2615
- <MultiSelector
2616
- label="Fruit"
2617
- options={['Apple', 'Banana']}
2618
- value={[]}
2619
- onChange={() => {}}
2620
- status={{type: 'error', message: 'Required'}}
2621
- />,
2622
- );
2623
- // Attached: the status glyph replaces the chevron indicator on the field.
2624
- expect(
2625
- container.querySelector('.astryx-multi-selector-indicator-icon'),
2626
- ).toBeNull();
2627
- });
2628
-
2629
- it('suppresses the on-field status icon for the detached variant', () => {
2630
- const {container} = render(
2631
- <MultiSelector
2632
- label="Fruit"
2633
- options={['Apple', 'Banana']}
2634
- value={[]}
2635
- onChange={() => {}}
2636
- status={{type: 'error', message: 'Required'}}
2637
- statusVariant="detached"
2638
- />,
2639
- );
2640
- // Detached: the message box below carries its own leading icon, so the
2641
- // field keeps its chevron indicator rather than duplicating the glyph.
2642
- expect(
2643
- container.querySelector('.astryx-multi-selector-indicator-icon'),
2644
- ).not.toBeNull();
2645
- });
2646
-
2647
- it.each(['secondary', 'ghost'] as const)(
2648
- 'detaches attached status by default for the %s variant',
2649
- variant => {
2650
- const {container} = render(
2651
- <MultiSelector
2652
- label="Fruit"
2653
- options={['Apple', 'Banana']}
2654
- value={[]}
2655
- onChange={() => {}}
2656
- variant={variant}
2657
- status={{type: 'error', message: 'Required'}}
2658
- />,
2659
- );
2660
- expect(container.querySelector('.astryx-multi-selector')).toHaveAttribute(
2661
- 'data-variant',
2662
- variant,
2663
- );
2664
- expect(container.querySelector('.astryx-field-status')).toHaveAttribute(
2665
- 'data-variant',
2666
- 'detached',
2667
- );
2668
- },
2669
- );
2670
-
2671
- it('uses a status tooltip for ghost multi-selectors when requested', () => {
2672
- const {container} = render(
2673
- <MultiSelector
2674
- label="Fruit"
2675
- options={['Apple', 'Banana']}
2676
- value={[]}
2677
- onChange={() => {}}
2678
- variant="ghost"
2679
- status={{type: 'warning', message: 'Some rows are hidden'}}
2680
- statusVariant="tooltip"
2681
- />,
2682
- );
2683
- expect(container.querySelector('.astryx-field-status')).toBeNull();
2684
- const statusButton = screen.getByRole('button', {
2685
- name: /warning details/i,
2686
- });
2687
- const tooltip = screen.getByRole('tooltip', h);
2688
- expect(tooltip).toHaveTextContent('Some rows are hidden');
2689
- expect(statusButton.getAttribute('aria-describedby')).toContain(tooltip.id);
2690
- expect(
2691
- screen.getByRole('combobox').getAttribute('aria-describedby'),
2692
- ).toContain(tooltip.id);
2693
- });
2694
- });
2695
-
2696
- describe('MultiSelector empty-state theme target', () => {
2697
- const OPTIONS = ['Apple', 'Banana', 'Cherry'];
2698
-
2699
- it('renders the astryx-multi-selector-empty-state target on the "No results found" element', async () => {
2700
- const user = userEvent.setup();
2701
- const {container} = render(
2702
- <MultiSelector
2703
- label="Fruit"
2704
- options={OPTIONS}
2705
- value={[]}
2706
- onChange={() => {}}
2707
- hasSearch
2708
- />,
2709
- );
2710
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
2711
- await user.type(screen.getByRole('combobox', h), 'xyz');
2712
-
2713
- const empty = container.querySelector('.astryx-multi-selector-empty-state');
2714
- expect(empty).not.toBeNull();
2715
- expect(empty).toHaveTextContent('No results found');
2716
- });
2717
- });
2718
-
2719
- describe('MultiSelector clear icon theme target', () => {
2720
- const ICON_OPTIONS = ['Apple', 'Banana', 'Orange'];
2721
-
2722
- // Resolve the clear glyph span (the astryx-icon element inside the clear
2723
- // button), independent of the theme target class.
2724
- const getClearIcon = (): HTMLElement => {
2725
- const button = screen.getByRole('button', {name: 'Clear all Fruit'});
2726
- const icon = button.querySelector('.astryx-icon');
2727
- if (icon == null) {
2728
- throw new Error('clear icon not found');
2729
- }
2730
- return icon as HTMLElement;
2731
- };
2732
-
2733
- it('renders only the astryx-input-clear-icon target on the clear glyph', () => {
2734
- render(
2735
- <MultiSelector
2736
- label="Fruit"
2737
- options={ICON_OPTIONS}
2738
- value={['Banana']}
2739
- onChange={() => {}}
2740
- hasClear
2741
- />,
2742
- );
2743
- // The canonical target lands on the icon element itself (not the button),
2744
- // so a theme can restyle just this glyph (color, size, hover) via
2745
- // `defineTheme` — a button-level target could not reach the icon's own
2746
- // color/size.
2747
- const icon = getClearIcon();
2748
- expect(icon).toHaveClass('astryx-input-clear-icon');
2749
- expect(icon).not.toHaveClass('astryx-multi-selector-clear-icon');
2750
- expect(icon).toHaveClass('astryx-icon');
2751
- });
2752
-
2753
- it('keeps the clear button functional alongside the target', () => {
2754
- const onChange = vi.fn();
2755
- render(
2756
- <MultiSelector
2757
- label="Fruit"
2758
- options={ICON_OPTIONS}
2759
- value={['Banana']}
2760
- onChange={onChange}
2761
- hasClear
2762
- />,
2763
- );
2764
- const clear = screen.getByRole('button', {name: 'Clear all Fruit'});
2765
- expect(clear.tagName).toBe('BUTTON');
2766
- fireEvent.click(clear);
2767
- expect(onChange).toHaveBeenCalledWith([]);
2768
- });
2769
-
2770
- it('routes the clear glyph through the shared clear button', () => {
2771
- // The clear affordance composes the shared InputClearButton (a ghost Button
2772
- // with a secondary/sm glyph), so the icon carries the canonical
2773
- // `astryx-input-clear-icon` target. Aside from that target class it matches
2774
- // the shared button's own `close`/`sm`/`secondary` glyph exactly, so the
2775
- // default look is defined in one place.
2776
- render(
2777
- <MultiSelector
2778
- label="Fruit"
2779
- options={ICON_OPTIONS}
2780
- value={['Banana']}
2781
- onChange={() => {}}
2782
- hasClear
2783
- />,
2784
- );
2785
- const icon = getClearIcon();
2786
- expect(icon).toHaveClass('astryx-input-clear-icon');
2787
- expect(icon).not.toHaveClass('astryx-multi-selector-clear-icon');
2788
-
2789
- const {container: refContainer} = render(
2790
- <Icon icon="close" size="sm" color="secondary" />,
2791
- );
2792
- const refIcon = refContainer.querySelector('.astryx-icon') as HTMLElement;
2793
-
2794
- const styleClasses = (el: HTMLElement) =>
2795
- el.className
2796
- .split(' ')
2797
- .filter(c => c !== 'astryx-input-clear-icon')
2798
- .sort();
2799
-
2800
- expect(styleClasses(icon)).toEqual(styleClasses(refIcon));
2801
- });
2802
-
2803
- it('uses input-clear-icon so a theme reaches the icon color, size, and hover', () => {
2804
- // jsdom cannot resolve the @layer cascade, so the DOM-class assertion above
2805
- // (target lands on the icon element) plus this generation assertion (the
2806
- // theme emits same-element icon rules in @layer astryx-theme) together
2807
- // prove the seam: a same-element theme rule wins over the icon's own
2808
- // base-layer color/size.
2809
- const theme = defineTheme({
2810
- name: 'multi-selector-shared-clear-icon-test',
2811
- components: {
2812
- 'input-clear-icon': {
2813
- base: {
2814
- width: '12px',
2815
- height: '12px',
2816
- fontSize: '12px',
2817
- color: 'var(--color-icon-secondary)',
2818
- ':hover': {color: 'var(--color-icon-primary)'},
2819
- },
2820
- },
2821
- },
2822
- });
2823
- const css = generateThemeTestCSS(theme);
2824
- expect(css).toContain('.astryx-input-clear-icon {');
2825
- expect(css).toContain('width: 12px');
2826
- expect(css).toContain('height: 12px');
2827
- expect(css).toContain('.astryx-input-clear-icon:hover');
2828
- expect(css).toContain('color: var(--color-icon-primary)');
2829
- });
2830
- });
2831
-
2832
- describe('MultiSelector indicator (chevron) icon theme target', () => {
2833
- const ICON_OPTIONS = ['Apple', 'Banana', 'Orange'];
2834
-
2835
- const getIndicatorIcon = (container: HTMLElement): HTMLElement => {
2836
- // The chevron is the only glyph carrying the indicator target class.
2837
- const icon = container.querySelector(
2838
- '.astryx-multi-selector-indicator-icon',
2839
- );
2840
- if (icon == null) {
2841
- throw new Error('indicator icon not found');
2842
- }
2843
- return icon as HTMLElement;
2844
- };
2845
-
2846
- it('renders the astryx-multi-selector-indicator-icon target on the chevron glyph', () => {
2847
- const {container} = render(
2848
- <MultiSelector
2849
- label="Fruit"
2850
- options={ICON_OPTIONS}
2851
- value={[]}
2852
- onChange={() => {}}
2853
- />,
2854
- );
2855
- // The stable theme target lands on the icon element itself (not the trigger
2856
- // button), so a theme can restyle just this glyph (color, size, hover) —
2857
- // and each open/closed state — via `defineTheme`. A button-level target
2858
- // could not reach the icon's own color/size.
2859
- const icon = getIndicatorIcon(container);
2860
- expect(icon).toHaveClass('astryx-multi-selector-indicator-icon');
2861
- expect(icon).toHaveClass('astryx-icon');
2862
- // Open/closed state is reflected so a theme can target each state alone.
2863
- expect(icon).toHaveAttribute('data-state', 'collapsed');
2864
- });
2865
-
2866
- it('reflects the expanded state on the chevron when the popover is open', async () => {
2867
- const user = userEvent.setup();
2868
- const {container} = render(
2869
- <MultiSelector
2870
- label="Fruit"
2871
- options={ICON_OPTIONS}
2872
- value={[]}
2873
- onChange={() => {}}
2874
- />,
2875
- );
2876
- await user.click(screen.getByRole('combobox'));
2877
- await waitFor(() => {
2878
- expect(getIndicatorIcon(container)).toHaveAttribute(
2879
- 'data-state',
2880
- 'expanded',
2881
- );
2882
- });
2883
- });
2884
-
2885
- it('renders the default icon (secondary color, sm size) byte-identically', () => {
2886
- // Pixel-identical default guard: the chevron glyph must carry the exact
2887
- // same StyleX color/size classes as a standalone secondary/sm icon. The
2888
- // glyph now sets --color-icon-secondary itself rather than inheriting it
2889
- // from a wrapper span that set the same token, so the rendered color is
2890
- // unchanged. The added
2891
- // target class + data-state are purely additive — they change nothing until
2892
- // a theme targets them.
2893
- const {container} = render(
2894
- <MultiSelector
2895
- label="Fruit"
2896
- options={ICON_OPTIONS}
2897
- value={[]}
2898
- onChange={() => {}}
2899
- />,
2900
- );
2901
- const icon = getIndicatorIcon(container);
2902
-
2903
- const {container: refContainer} = render(
2904
- <Icon icon="chevronDown" size="sm" color="secondary" />,
2905
- );
2906
- const refIcon = refContainer.querySelector('.astryx-icon') as HTMLElement;
2907
-
2908
- // Exclude the additive theme-target classes (the stable target + its
2909
- // reflected state class) so only StyleX classes remain.
2910
- const themeTargetClasses = new Set([
2911
- 'astryx-multi-selector-indicator-icon',
2912
- 'collapsed',
2913
- 'expanded',
2914
- ]);
2915
- const styleClasses = (el: HTMLElement) =>
2916
- el.className
2917
- .split(' ')
2918
- .filter(c => !themeTargetClasses.has(c))
2919
- .sort();
2920
-
2921
- // A superset, not an exact match: the chevron additionally carries the
2922
- // rotation styles, which live on the glyph precisely so a theme can reach
2923
- // the transform through the same selector as the color. The guard that
2924
- // matters is that every color/size class of a standalone icon is still
2925
- // present — i.e. the default look has not drifted.
2926
- expect(styleClasses(icon)).toEqual(
2927
- expect.arrayContaining(styleClasses(refIcon)),
2928
- );
2929
- });
2930
-
2931
- it('exposes multi-selector-indicator-icon so a theme reaches the icon size and per-state color', () => {
2932
- // jsdom cannot resolve the @layer cascade, so the DOM-class assertions
2933
- // above (target lands on the icon element) plus this generation assertion
2934
- // (the theme emits same-element icon rules in @layer astryx-theme) together
2935
- // prove the seam: a same-element theme rule wins over the icon's own
2936
- // base-layer color/size.
2937
- const theme = defineTheme({
2938
- name: 'multi-selector-indicator-icon-test',
2939
- components: {
2940
- 'multi-selector-indicator-icon': {
2941
- base: {width: '14px', height: '14px', fontSize: '14px'},
2942
- 'state:expanded': {color: 'var(--color-icon-primary)'},
2943
- },
2944
- },
2945
- });
2946
- const css = generateThemeTestCSS(theme);
2947
- expect(css).toContain('.astryx-multi-selector-indicator-icon {');
2948
- expect(css).toContain('width: 14px');
2949
- expect(css).toContain('height: 14px');
2950
- expect(css).toContain(
2951
- '.astryx-multi-selector-indicator-icon[data-state="expanded"]',
2952
- );
2953
- expect(css).toContain('color: var(--color-icon-primary)');
2954
- });
2955
- });
2956
-
2957
- describe('MultiSelector list structure', () => {
2958
- it('does not draw a divider under select-all', async () => {
2959
- const user = userEvent.setup();
2960
- render(
2961
- <MultiSelector
2962
- label="Fruit"
2963
- options={['Apple', 'Banana', 'Orange']}
2964
- value={[]}
2965
- onChange={() => {}}
2966
- hasSelectAll
2967
- />,
2968
- );
2969
- await user.click(screen.getByRole('combobox'));
2970
- // Select-all is the first row of the list, not a section of its own. No
2971
- // option here declares a divider and there is no search row, so the panel
2972
- // should contain no rule at all.
2973
- expect(document.querySelectorAll('[role="separator"]')).toHaveLength(0);
2974
- const [first] = screen.getAllByRole('option', h);
2975
- expect(first).toHaveTextContent('Select all');
2976
- });
2977
-
2978
- it('renders a section title as a plain heading inside the group, not a divider', async () => {
2979
- const user = userEvent.setup();
2980
- render(
2981
- <MultiSelector
2982
- label="Fruit"
2983
- options={[
2984
- {
2985
- type: 'section',
2986
- title: 'Citrus',
2987
- options: [
2988
- {value: 'orange', label: 'Orange'},
2989
- {value: 'lemon', label: 'Lemon'},
2990
- ],
2991
- },
2992
- ]}
2993
- value={[]}
2994
- onChange={() => {}}
2995
- />,
2996
- );
2997
- await user.click(screen.getByRole('combobox'));
2998
-
2999
- // A labeled Divider used to stand in for the heading; it rendered a
3000
- // role="separator" as a direct child of the listbox and stacked a second
3001
- // rule under the search row's own.
3002
- expect(document.querySelectorAll('[role="separator"]')).toHaveLength(0);
3003
-
3004
- const group = screen.getByRole('group', {name: 'Citrus', ...h});
3005
- const heading = group.querySelector(
3006
- '.astryx-multi-selector-section-heading',
3007
- );
3008
- expect(heading).toBeTruthy();
3009
- expect(heading).toHaveTextContent('Citrus');
3010
- // The group already carries the title as its accessible name, so the
3011
- // visible heading must not announce it a second time.
3012
- expect(heading).toHaveAttribute('aria-hidden', 'true');
3013
- // ...and it precedes the options it heads.
3014
- const [firstOption] = within(group).getAllByRole('option', h);
3015
- expect(
3016
- heading!.compareDocumentPosition(firstOption) &
3017
- Node.DOCUMENT_POSITION_FOLLOWING,
3018
- ).toBeTruthy();
3019
- });
3020
-
3021
- it('hides a standalone divider from the accessibility tree (#4994)', async () => {
3022
- const user = userEvent.setup();
3023
- render(
3024
- <MultiSelector
3025
- label="Fruit"
3026
- options={['Apple', {type: 'divider'}, 'Banana']}
3027
- value={[]}
3028
- onChange={() => {}}
3029
- />,
3030
- );
3031
- await user.click(screen.getByRole('combobox'));
3032
-
3033
- // role="listbox" only permits option/group children. The divider still
3034
- // renders role="separator" (unchanged visual/DOM), but must be excluded
3035
- // from the accessibility tree so it never reaches the listbox's exposed
3036
- // children (axe aria-required-children).
3037
- const divider = document.querySelector('[role="separator"]');
3038
- expect(divider).toBeTruthy();
3039
- expect(divider).toHaveAttribute('aria-hidden', 'true');
3040
- });
3041
- });
3042
-
3043
- describe('MultiSelector search affordances', () => {
3044
- it('renders the search row seamlessly — no nested input box, a divider under it', async () => {
3045
- const user = userEvent.setup();
3046
- render(
3047
- <MultiSelector
3048
- label="Fruit"
3049
- options={['Apple', 'Banana', 'Orange']}
3050
- value={[]}
3051
- onChange={() => {}}
3052
- hasSearch
3053
- />,
3054
- );
3055
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
3056
-
3057
- const search = screen.getByRole('combobox', h);
3058
- // The row is the outer gutter; the input sits inside the rounded field.
3059
- const row = search.closest('.astryx-multi-selector-search');
3060
- const field = search.parentElement;
3061
- if (!row || !field) {
3062
- throw new Error('search row not found');
3063
- }
3064
- // The panel is already a bordered surface: the field inside it must not be
3065
- // a second bordered box (this used to render a TextInput).
3066
- expect(row).not.toHaveClass('astryx-text-input');
3067
- expect(search.closest('.astryx-text-input')).toBeNull();
3068
- // The field is a rounded box inset from the panel edge, shaped like the
3069
- // option rows under it — not a full-bleed header strip.
3070
- expect(field).not.toBe(row);
3071
- // ...and a divider separates it from the options.
3072
- const separator = document.querySelector('[role="separator"]');
3073
- if (!separator) {
3074
- throw new Error('divider not found');
3075
- }
3076
- // Order: row, then divider, then the listbox.
3077
- expect(
3078
- row.compareDocumentPosition(separator) & Node.DOCUMENT_POSITION_FOLLOWING,
3079
- ).toBeTruthy();
3080
- const listbox = screen.getByRole('listbox', h);
3081
- expect(
3082
- separator.compareDocumentPosition(listbox) &
3083
- Node.DOCUMENT_POSITION_FOLLOWING,
3084
- ).toBeTruthy();
3085
- });
3086
-
3087
- it('keeps the search row outside the scrolling option list', async () => {
3088
- const user = userEvent.setup();
3089
- render(
3090
- <MultiSelector
3091
- label="Fruit"
3092
- options={['Apple', 'Banana', 'Orange']}
3093
- value={[]}
3094
- onChange={() => {}}
3095
- hasSearch
3096
- />,
3097
- );
3098
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
3099
- // The options scroll under the header rather than carrying it away, so the
3100
- // field stays reachable in a long list.
3101
- const search = screen.getByRole('combobox', h);
3102
- const listbox = screen.getByRole('listbox', h);
3103
- expect(listbox.contains(search)).toBe(false);
3104
- });
3105
-
3106
- it('renders a decorative (aria-hidden) magnifier icon whenever hasSearch is on', async () => {
3107
- const user = userEvent.setup();
3108
- render(
3109
- <MultiSelector
3110
- label="Fruit"
3111
- options={['Apple', 'Banana', 'Orange']}
3112
- value={[]}
3113
- onChange={() => {}}
3114
- hasSearch
3115
- />,
3116
- );
3117
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
3118
- const search = screen.getByRole('combobox', h);
3119
- // The magnifier leads the search row, as a sibling of the <input>.
3120
- const wrapper = search.parentElement;
3121
- const magnifier = wrapper?.querySelector('.astryx-icon');
3122
- expect(magnifier).toBeTruthy();
3123
- expect(magnifier?.getAttribute('aria-hidden')).toBe('true');
3124
- expect(magnifier?.getAttribute('aria-label')).toBeNull();
3125
- });
3126
-
3127
- it('renders the clear button once a query is typed and clears + refocuses on click', async () => {
3128
- const user = userEvent.setup();
3129
- render(
3130
- <MultiSelector
3131
- label="Fruit"
3132
- options={['Apple', 'Banana', 'Orange']}
3133
- value={[]}
3134
- onChange={() => {}}
3135
- hasSearch
3136
- />,
3137
- );
3138
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
3139
- const search = screen.getByRole('combobox', h);
3140
- await user.type(search, 'ap');
3141
- expect(search).toHaveValue('ap');
3142
-
3143
- // The clear button is TextInput's built-in hasClear affordance; its name is
3144
- // derived from the field label ("Search options").
3145
- const clear = screen.getByRole('button', {
3146
- name: 'Clear Search options',
3147
- hidden: true,
3148
- });
3149
-
3150
- await user.click(clear);
3151
- expect(search).toHaveValue('');
3152
- expect(search).toHaveFocus();
3153
- });
3154
-
3155
- it('does not render the clear button when the query is empty', async () => {
3156
- const user = userEvent.setup();
3157
- render(
3158
- <MultiSelector
3159
- label="Fruit"
3160
- options={['Apple', 'Banana', 'Orange']}
3161
- value={[]}
3162
- onChange={() => {}}
3163
- hasSearch
3164
- />,
3165
- );
3166
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
3167
- expect(
3168
- screen.queryByRole('button', {
3169
- name: 'Clear Search options',
3170
- hidden: true,
3171
- }),
3172
- ).not.toBeInTheDocument();
3173
- });
3174
-
3175
- it('keeps the combobox contract on the input, not the affordances', async () => {
3176
- const user = userEvent.setup();
3177
- render(
3178
- <MultiSelector
3179
- label="Fruit"
3180
- options={['Apple', 'Banana', 'Orange']}
3181
- value={[]}
3182
- onChange={() => {}}
3183
- hasSearch
3184
- />,
3185
- );
3186
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
3187
- const search = screen.getByRole('combobox', h);
3188
- expect(search.tagName).toBe('INPUT');
3189
- expect(search).toHaveAttribute('aria-autocomplete', 'list');
3190
- });
3191
-
3192
- it('tabs from the search input to the clear button (keeping the popup open) when a query is showing it', async () => {
3193
- const user = userEvent.setup();
3194
- render(
3195
- <MultiSelector
3196
- label="Fruit"
3197
- options={['Apple', 'Banana', 'Orange']}
3198
- value={[]}
3199
- onChange={() => {}}
3200
- hasSearch
3201
- />,
3202
- );
3203
- await user.click(screen.getByRole('button', {name: 'Fruit'}));
3204
- const trigger = screen.getByRole('button', {name: 'Fruit'});
3205
- const search = screen.getByRole('combobox', h);
3206
- await user.type(search, 'ap');
3207
- expect(search).toHaveFocus();
3208
-
3209
- // Forward-tab lands on the clear (✕) button and the popup stays open, so
3210
- // the affordance is keyboard-reachable rather than being skipped when the
3211
- // input's Tab dismisses the popup.
3212
- await user.tab();
3213
- const clear = screen.getByRole('button', {
3214
- name: 'Clear Search options',
3215
- hidden: true,
3216
- });
3217
- expect(clear).toHaveFocus();
3218
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
3219
- });
3220
-
3221
- it('dismisses on Tab from the search input when there is no query (no clear button)', async () => {
3222
- const user = userEvent.setup();
3223
- render(
3224
- <MultiSelector
3225
- label="Fruit"
3226
- options={['Apple', 'Banana', 'Orange']}
3227
- value={[]}
3228
- onChange={() => {}}
3229
- hasSearch
3230
- />,
3231
- );
3232
- const trigger = screen.getByRole('button', {name: 'Fruit'});
3233
- await user.click(trigger);
3234
- const search = screen.getByRole('combobox', h);
3235
- // Focus moves into the search input on open (via rAF).
3236
- await waitFor(() => expect(search).toHaveFocus());
3237
-
3238
- // With no query there is no clear button, so Tab dismisses the popup as a
3239
- // plain combobox does.
3240
- await user.tab();
3241
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
3242
- });
3243
- });
3244
-
3245
- describe('MultiSelector disabled state theme target', () => {
3246
- const getSelectorRoot = (container: HTMLElement): HTMLElement => {
3247
- const root = container.querySelector('.astryx-multi-selector');
3248
- if (root == null) {
3249
- throw new Error('multi-selector root not found');
3250
- }
3251
- return root as HTMLElement;
3252
- };
3253
-
3254
- it('reflects data-state="disabled" on the root when disabled', () => {
3255
- const {container} = render(
3256
- <MultiSelector
3257
- label="Fruit"
3258
- options={['Apple', 'Banana', 'Orange']}
3259
- value={[]}
3260
- onChange={() => {}}
3261
- isDisabled
3262
- />,
3263
- );
3264
- expect(getSelectorRoot(container)).toHaveAttribute(
3265
- 'data-disabled',
3266
- 'disabled',
3267
- );
3268
- });
3269
-
3270
- it('omits the disabled class/attribute when enabled', () => {
3271
- const {container} = render(
3272
- <MultiSelector
3273
- label="Fruit"
3274
- options={['Apple', 'Banana', 'Orange']}
3275
- value={[]}
3276
- onChange={() => {}}
3277
- />,
3278
- );
3279
- const root = getSelectorRoot(container);
3280
- expect(root).not.toHaveAttribute('data-disabled');
3281
- expect(root).not.toHaveAttribute('data-disabled');
3282
- });
3283
-
3284
- it('exposes the disabled state so a theme can key on it', () => {
3285
- const theme = defineTheme({
3286
- name: 'multi-selector-disabled-state-test',
3287
- components: {
3288
- 'multi-selector': {
3289
- 'disabled:disabled': {opacity: '0.4'},
3290
- },
3291
- },
3292
- });
3293
- const css = generateThemeTestCSS(theme);
3294
- expect(css).toContain('.astryx-multi-selector[data-disabled="disabled"]');
3295
- expect(css).toContain('opacity: 0.4');
3296
- });
3297
- });
3298
-
3299
- describe('MultiSelector dropdown option theme target', () => {
3300
- const ROW_OPTIONS = ['Apple', 'Banana', 'Orange'];
3301
-
3302
- it('renders astryx-multi-selector-option, with its size, on every dropdown row', async () => {
3303
- const user = userEvent.setup();
3304
- render(
3305
- <MultiSelector
3306
- label="Fruit"
3307
- options={ROW_OPTIONS}
3308
- value={[]}
3309
- onChange={() => {}}
3310
- size="lg"
3311
- />,
3312
- );
3313
- await user.click(screen.getByRole('combobox'));
3314
- const options = screen.getAllByRole('option', h);
3315
- expect(options).toHaveLength(3);
3316
- for (const option of options) {
3317
- expect(option).toHaveClass('astryx-multi-selector-option');
3318
- expect(option).toHaveAttribute('data-size', 'lg');
3319
- expect(option).toHaveAttribute('data-size', 'lg');
3320
- }
3321
- });
3322
-
3323
- it('carries the selected and disabled states a theme keys on', async () => {
3324
- const user = userEvent.setup();
3325
- render(
3326
- <MultiSelector
3327
- label="Fruit"
3328
- options={[
3329
- {value: 'apple', label: 'Apple'},
3330
- {value: 'banana', label: 'Banana'},
3331
- {value: 'orange', label: 'Orange', disabled: true},
3332
- ]}
3333
- value={['apple']}
3334
- onChange={() => {}}
3335
- />,
3336
- );
3337
- await user.click(screen.getByRole('combobox'));
3338
- const [selected, plain, disabled] = screen.getAllByRole('option', h);
3339
-
3340
- expect(selected).toHaveAttribute('data-selected', 'selected');
3341
- expect(selected).toHaveAttribute('data-selected', 'selected');
3342
- expect(plain).not.toHaveAttribute('data-selected');
3343
- expect(plain).not.toHaveAttribute('data-selected');
3344
-
3345
- expect(disabled).toHaveAttribute('data-disabled', 'disabled');
3346
- expect(disabled).toHaveAttribute('data-disabled', 'disabled');
3347
- expect(plain).not.toHaveAttribute('data-disabled');
3348
- });
3349
-
3350
- it('marks the Select All row with the select-all state, not a separate target', async () => {
3351
- const user = userEvent.setup();
3352
- render(
3353
- <MultiSelector
3354
- label="Fruit"
3355
- options={ROW_OPTIONS}
3356
- value={[]}
3357
- onChange={() => {}}
3358
- hasSelectAll
3359
- />,
3360
- );
3361
- await user.click(screen.getByRole('combobox'));
3362
-
3363
- const [selectAllRow, ...regularRows] = screen.getAllByRole('option', h);
3364
- expect(selectAllRow).toHaveTextContent('Select all');
3365
- expect(selectAllRow).toHaveClass('astryx-multi-selector-option');
3366
- expect(selectAllRow).toHaveAttribute('data-select-all', 'select-all');
3367
- expect(selectAllRow).toHaveAttribute('data-select-all', 'select-all');
3368
-
3369
- for (const row of regularRows) {
3370
- expect(row).toHaveClass('astryx-multi-selector-option');
3371
- expect(row).not.toHaveClass('select-all');
3372
- expect(row).not.toHaveAttribute('data-select-all');
3373
- }
3374
- });
3375
-
3376
- it('keeps the row targetable when renderOption replaces the label', async () => {
3377
- const user = userEvent.setup();
3378
- render(
3379
- <MultiSelector
3380
- label="Fruit"
3381
- options={[{value: 'apple', label: 'Apple'}]}
3382
- value={[]}
3383
- onChange={() => {}}
3384
- renderOption={option => (
3385
- <span data-testid="custom-row">{option.label}</span>
3386
- )}
3387
- />,
3388
- );
3389
- await user.click(screen.getByRole('combobox'));
3390
- const option = screen.getAllByRole('option', h)[0];
3391
- // The row owns the typography, so custom content inherits the same
3392
- // treatment the fallback label gets — and one row override reaches both.
3393
- expect(option).toHaveClass('astryx-multi-selector-option');
3394
- expect(within(option).getByTestId('custom-row')).toHaveTextContent('Apple');
3395
- });
3396
-
3397
- it('exposes the row target, its states and its size to defineTheme', () => {
3398
- const theme = defineTheme({
3399
- name: 'multi-selector-option-target-test',
3400
- components: {
3401
- 'multi-selector-option': {
3402
- base: {borderRadius: '8px', fontWeight: '600'},
3403
- selected: {backgroundColor: 'var(--color-background-muted)'},
3404
- 'select-all': {fontWeight: '700'},
3405
- 'size:lg': {borderRadius: '12px'},
3406
- },
3407
- },
3408
- });
3409
- const css = generateThemeTestCSS(theme);
3410
- expect(css).toContain('.astryx-multi-selector-option {');
3411
- expect(css).toContain(
3412
- '.astryx-multi-selector-option[data-selected="selected"]',
3413
- );
3414
- expect(css).toContain(
3415
- '.astryx-multi-selector-option[data-select-all="select-all"]',
3416
- );
3417
- expect(css).toContain('.astryx-multi-selector-option[data-size="lg"]');
3418
- });
3419
- });
3420
-
3421
- describe('MultiSelector popup theme target', () => {
3422
- it('puts astryx-multi-selector-popup on the surface that paints, not the list inside it', async () => {
3423
- const user = userEvent.setup();
3424
- render(
3425
- <MultiSelector
3426
- label="Fruit"
3427
- options={['Apple', 'Banana']}
3428
- value={[]}
3429
- onChange={() => {}}
3430
- />,
3431
- );
3432
- await user.click(screen.getByRole('combobox', {name: /Fruit/}));
3433
-
3434
- const popup = document.querySelector(
3435
- '.astryx-multi-selector-popup',
3436
- ) as HTMLElement;
3437
- expect(popup).not.toBeNull();
3438
- expect(popup).toHaveClass('astryx-popover');
3439
- // The scrolling list is a descendant, not the target itself.
3440
- expect(popup.querySelector('[role="listbox"]')).not.toBeNull();
3441
- expect(popup.getAttribute('role')).toBeNull();
3442
-
3443
- const layer = document.querySelector('[popover]') as HTMLElement;
3444
- expect(popup).not.toBe(layer);
3445
- expect(layer.contains(popup)).toBe(true);
3446
- });
3447
-
3448
- it('stays closed when the trigger click follows its own light dismiss (#5004)', async () => {
3449
- const user = userEvent.setup();
3450
- render(
3451
- <MultiSelector
3452
- label="Fruit"
3453
- options={['Apple', 'Banana', 'Cherry']}
3454
- value={[]}
3455
- onChange={() => {}}
3456
- />,
3457
- );
3458
- const trigger = screen.getByRole('combobox');
3459
- await user.click(trigger);
3460
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
3461
-
3462
- // The browser dismissed the popup on pointerup and queued the toggle. When
3463
- // that event lands before the click — WebKit, or any engine under load —
3464
- // the click used to read a closed popup and reopen it.
3465
- fireEvent.pointerDown(trigger);
3466
- const popover = document.querySelector('[popover]') as HTMLElement;
3467
- act(() => {
3468
- popover.dispatchEvent(
3469
- Object.assign(new Event('toggle'), {
3470
- oldState: 'open',
3471
- newState: 'closed',
3472
- }),
3473
- );
3474
- });
3475
- // Synchronously: the click falls inside the one gesture the guard covers,
3476
- // as it does in a browser a few milliseconds behind the dismissal.
3477
- fireEvent.click(trigger);
3478
-
3479
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
3480
- expect(trigger.parentElement).toHaveClass(
3481
- stylex.props(selectorPresentationStyles.pointerRestoredFocus).className!,
3482
- );
3483
- });
3484
- });
3485
-
3486
- describe('MultiSelector trigger render prop', () => {
3487
- const OPTIONS = [
3488
- {value: 'bug', label: 'Bug'},
3489
- {value: 'feature', label: 'Feature'},
3490
- {value: 'docs', label: 'Docs'},
3491
- ];
3492
-
3493
- it('renders the caller control instead of the field and button', () => {
3494
- render(
3495
- <MultiSelector
3496
- label="Labels"
3497
- options={OPTIONS}
3498
- value={[]}
3499
- onChange={() => {}}
3500
- renderTrigger={props => (
3501
- <button type="button" {...props}>
3502
- Pick labels
3503
- </button>
3504
- )}
3505
- />,
3506
- );
3507
- const opener = screen.getByRole('button', {name: 'Pick labels'});
3508
- expect(opener).toHaveAttribute('aria-haspopup', 'listbox');
3509
- expect(opener).toHaveAttribute('aria-expanded', 'false');
3510
- expect(opener).toHaveAttribute('aria-controls');
3511
- // No field chrome: exactly one button, and no Field label element.
3512
- expect(screen.getAllByRole('button')).toHaveLength(1);
3513
- expect(screen.queryByText('Labels')).toBeNull();
3514
- });
3515
-
3516
- // spec:AST-055 AR3: a light dismiss returns focus to the caller's control,
3517
- // even when the press that closed the list focused another field.
3518
- it('returns focus to the caller control after a light dismiss onto another field', async () => {
3519
- const user = userEvent.setup();
3520
- render(
3521
- <>
3522
- <MultiSelector
3523
- label="Labels"
3524
- options={OPTIONS}
3525
- value={[]}
3526
- onChange={() => {}}
3527
- renderTrigger={props => (
3528
- <button type="button" {...props}>
3529
- Pick labels
3530
- </button>
3531
- )}
3532
- />
3533
- <input aria-label="Notes" />
3534
- </>,
3535
- );
3536
- const opener = screen.getByRole('button', {name: 'Pick labels'});
3537
- await user.click(opener);
3538
- expect(opener).toHaveAttribute('aria-expanded', 'true');
3539
-
3540
- act(() => {
3541
- screen.getByRole('textbox', {name: 'Notes'}).focus();
3542
- });
3543
- const popover = document.querySelector('[popover]') as HTMLElement;
3544
- act(() => {
3545
- popover.dispatchEvent(
3546
- Object.assign(new Event('toggle'), {
3547
- oldState: 'open',
3548
- newState: 'closed',
3549
- }),
3550
- );
3551
- });
3552
-
3553
- expect(opener).toHaveAttribute('aria-expanded', 'false');
3554
- expect(opener).toHaveFocus();
3555
- });
3556
-
3557
- it('opens against the caller control and names the listbox by label', async () => {
3558
- const user = userEvent.setup();
3559
- render(
3560
- <MultiSelector
3561
- label="Labels"
3562
- options={OPTIONS}
3563
- value={['bug']}
3564
- onChange={() => {}}
3565
- renderTrigger={props => (
3566
- <button type="button" {...props}>
3567
- Pick labels
3568
- </button>
3569
- )}
3570
- />,
3571
- );
3572
- const opener = screen.getByRole('button', {name: 'Pick labels'});
3573
- await user.click(opener);
3574
-
3575
- expect(opener).toHaveAttribute('aria-expanded', 'true');
3576
- const listbox = screen.getByRole('listbox', {name: 'Labels', ...h});
3577
- expect(opener.getAttribute('aria-controls')).toBe(listbox.id);
3578
- expect(screen.getAllByRole('option', h)).toHaveLength(3);
3579
- expect(screen.getByRole('option', {name: 'Bug', ...h})).toHaveAttribute(
3580
- 'aria-selected',
3581
- 'true',
3582
- );
3583
- });
3584
-
3585
- it('the listbox owns the keyboard once open and toggles on Enter', async () => {
3586
- const user = userEvent.setup();
3587
- const onChange = vi.fn();
3588
- render(
3589
- <MultiSelector
3590
- label="Labels"
3591
- options={OPTIONS}
3592
- value={[]}
3593
- onChange={onChange}
3594
- renderTrigger={props => (
3595
- <button type="button" {...props}>
3596
- Pick labels
3597
- </button>
3598
- )}
3599
- />,
3600
- );
3601
- await user.click(screen.getByRole('button', {name: 'Pick labels'}));
3602
- const listbox = screen.getByRole('listbox', h);
3603
- expect(listbox).toHaveAttribute('tabindex', '0');
3604
- await waitFor(() => expect(listbox).toHaveFocus());
3605
-
3606
- // Opening by click highlights the first option; Enter toggles it.
3607
- fireEvent.keyDown(listbox, {key: 'ArrowDown'});
3608
- fireEvent.keyDown(listbox, {key: 'Enter'});
3609
- expect(onChange).toHaveBeenCalledWith(['feature']);
3610
- });
3611
-
3612
- it('handleRef opens and closes the panel and onOpenChange reports it', async () => {
3613
- const onOpenChange = vi.fn();
3614
- function Harness() {
3615
- const handleRef = useRef<MultiSelectorHandle>(null);
3616
- return (
3617
- <>
3618
- <button type="button" onClick={() => handleRef.current?.open()}>
3619
- open from outside
3620
- </button>
3621
- <button type="button" onClick={() => handleRef.current?.close()}>
3622
- close from outside
3623
- </button>
3624
- <MultiSelector
3625
- label="Labels"
3626
- options={OPTIONS}
3627
- value={[]}
3628
- onChange={() => {}}
3629
- handleRef={handleRef}
3630
- onOpenChange={onOpenChange}
3631
- renderTrigger={props => (
3632
- <button type="button" {...props}>
3633
- anchor
3634
- </button>
3635
- )}
3636
- />
3637
- </>
3638
- );
3639
- }
3640
- const user = userEvent.setup();
3641
- render(<Harness />);
3642
- const anchor = screen.getByRole('button', {name: 'anchor'});
3643
- expect(anchor).toHaveAttribute('aria-expanded', 'false');
3644
- expect(onOpenChange).not.toHaveBeenCalled();
3645
-
3646
- await user.click(screen.getByRole('button', {name: 'open from outside'}));
3647
- expect(anchor).toHaveAttribute('aria-expanded', 'true');
3648
- expect(onOpenChange).toHaveBeenLastCalledWith(true);
3649
-
3650
- await user.click(screen.getByRole('button', {name: 'close from outside'}));
3651
- await waitFor(() => {
3652
- expect(anchor).toHaveAttribute('aria-expanded', 'false');
3653
- });
3654
- expect(onOpenChange).toHaveBeenLastCalledWith(false);
3655
- expect(onOpenChange).toHaveBeenCalledTimes(2);
3656
- });
3657
-
3658
- it('the handle does not open a disabled selector', () => {
3659
- const handleRef = {current: null as MultiSelectorHandle | null};
3660
- render(
3661
- <MultiSelector
3662
- label="Labels"
3663
- options={OPTIONS}
3664
- value={[]}
3665
- onChange={() => {}}
3666
- isDisabled
3667
- handleRef={handleRef}
3668
- renderTrigger={props => (
3669
- <button type="button" {...props}>
3670
- anchor
3671
- </button>
3672
- )}
3673
- />,
3674
- );
3675
- act(() => {
3676
- handleRef.current?.open();
3677
- });
3678
- expect(handleRef.current?.isOpen()).toBe(false);
3679
- expect(screen.getByRole('button', {name: 'anchor'})).toHaveAttribute(
3680
- 'aria-expanded',
3681
- 'false',
3682
- );
3683
- });
3684
- });
3685
-
3686
- describe('MultiSelector renderTrigger — read-only', () => {
3687
- it("a read-only selector advertises no panel on the caller's control", () => {
3688
- // spec:AST-011 FR4. A read-only selector has no surface to open, so a
3689
- // control that says "collapsed, has popup" sends a screen-reader user
3690
- // to press Enter on something that cannot respond — with nothing to
3691
- // tell them the value is read-only rather than the control broken.
3692
- const onOpen = vi.fn();
3693
- render(
3694
- <MultiSelector
3695
- label="Labels"
3696
- value={['a']}
3697
- onChange={() => {}}
3698
- isReadOnly
3699
- options={[
3700
- {value: 'a', label: 'Alpha'},
3701
- {value: 'b', label: 'Beta'},
3702
- ]}
3703
- renderTrigger={props => (
3704
- <button type="button" {...props} onClick={onOpen}>
3705
- Labels
3706
- </button>
3707
- )}
3708
- />,
3709
- );
3710
-
3711
- const trigger = screen.getByRole('button', {name: 'Labels'});
3712
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
3713
- expect(trigger).not.toHaveAttribute('aria-haspopup');
3714
- expect(trigger).not.toHaveAttribute('aria-controls');
3715
- // Withholding the disclosure attributes stops the control lying about a
3716
- // panel; this says why it does not open, which the field path conveys
3717
- // through chrome the caller's control does not have.
3718
- expect(trigger).toHaveAttribute('aria-readonly', 'true');
3719
- });
3720
-
3721
- it('still advertises the panel when the selector is not read-only', () => {
3722
- render(
3723
- <MultiSelector
3724
- label="Labels"
3725
- value={[]}
3726
- onChange={() => {}}
3727
- options={[{value: 'a', label: 'Alpha'}]}
3728
- renderTrigger={props => (
3729
- <button type="button" {...props}>
3730
- Labels
3731
- </button>
3732
- )}
3733
- />,
3734
- );
3735
-
3736
- const trigger = screen.getByRole('button', {name: 'Labels'});
3737
- expect(trigger).toHaveAttribute('aria-haspopup', 'listbox');
3738
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
3739
- expect(trigger).toHaveAttribute('aria-controls');
3740
- });
3741
- });
3742
-
3743
- describe('MultiSelector renderTrigger — focus return', () => {
3744
- it('returns focus to a control that takes no focus of its own', async () => {
3745
- // A glyph is not keyboard-openable, but when it IS opened by pointer the
3746
- // panel still has to give focus back somewhere the person recognises.
3747
- // Without the fallback focus lands on the body and they lose their place.
3748
- const user = userEvent.setup();
3749
- render(
3750
- <MultiSelector
3751
- label="Labels"
3752
- value={[]}
3753
- onChange={() => {}}
3754
- options={[
3755
- {value: 'a', label: 'Alpha'},
3756
- {value: 'b', label: 'Beta'},
3757
- ]}
3758
- renderTrigger={props => (
3759
- <span {...props} data-testid="glyph">
3760
- #
3761
- </span>
3762
- )}
3763
- />,
3764
- );
3765
-
3766
- const glyph = screen.getByTestId('glyph');
3767
- expect(glyph).toHaveAttribute('tabindex', '-1');
3768
-
3769
- await user.click(glyph);
3770
- await waitFor(() => expect(glyph).toHaveAttribute('aria-expanded', 'true'));
3771
- await user.keyboard('{Escape}');
3772
- await waitFor(() => expect(glyph).toHaveFocus());
3773
- expect(document.body).not.toHaveFocus();
3774
- });
3775
-
3776
- it('leaves a real control\u2019s own focusability alone', () => {
3777
- render(
3778
- <MultiSelector
3779
- label="Labels"
3780
- value={[]}
3781
- onChange={() => {}}
3782
- options={[{value: 'a', label: 'Alpha'}]}
3783
- renderTrigger={props => (
3784
- <button type="button" {...props}>
3785
- Labels
3786
- </button>
3787
- )}
3788
- />,
3789
- );
3790
-
3791
- expect(screen.getByRole('button', {name: 'Labels'})).not.toHaveAttribute(
3792
- 'tabindex',
3793
- );
3794
- });
3795
- });
3796
-
3797
- describe('MultiSelector hasCreate', () => {
3798
- const OPTIONS = [
3799
- {value: 'bug', label: 'Bug'},
3800
- {value: 'feature', label: 'Feature'},
3801
- {value: 'cafe', label: 'Café'},
3802
- ];
3803
-
3804
- it('offers Create "<query>" first and reports the creation through onChange', async () => {
3805
- const user = userEvent.setup();
3806
- const onChange =
3807
- vi.fn<(value: string[], change?: MultiSelectorChange) => void>();
3808
- render(
3809
- <MultiSelector
3810
- label="Labels"
3811
- options={OPTIONS}
3812
- value={['bug']}
3813
- onChange={onChange}
3814
- hasSearch
3815
- hasCreate
3816
- />,
3817
- );
3818
- await user.click(screen.getByRole('button', {name: 'Labels'}));
3819
- const search = screen.getByRole('combobox', h);
3820
- await user.type(search, 'Urgent');
3821
-
3822
- const options = screen.getAllByRole('option', h);
3823
- expect(options).toHaveLength(1);
3824
- expect(options[0]).toHaveTextContent('Create "Urgent"');
3825
- expect(options[0]).toHaveAttribute('aria-selected', 'false');
3826
- expect(options[0].querySelector('input[type="checkbox"]')).toBeNull();
3827
- expect(screen.queryByText('No results found')).toBeNull();
3828
-
3829
- await user.click(options[0]);
3830
- // One update: the query joins the value and the change says it is a
3831
- // creation, so the caller adds the option and accepts the value together.
3832
- expect(onChange).toHaveBeenCalledTimes(1);
3833
- expect(onChange).toHaveBeenCalledWith(['bug', 'Urgent'], {
3834
- type: 'create',
3835
- query: 'Urgent',
3836
- });
3837
- // The filter is cleared so the option the caller adds is visible.
3838
- expect(search).toHaveValue('');
3839
- });
3840
-
3841
- it('says only the latest status when several keystrokes land in one frame (spec:AST-086 FR9)', async () => {
3842
- // A barcode scanner, dictation or automation types faster than a frame:
3843
- // each keystroke states the panel's status through the search's
3844
- // `hasReplace` announcer, so a status the next keystroke replaces before
3845
- // it is exposed is withdrawn, not joined.
3846
- const user = userEvent.setup();
3847
- render(
3848
- <MultiSelector
3849
- label="Labels"
3850
- options={OPTIONS}
3851
- value={['bug']}
3852
- onChange={() => {}}
3853
- hasSearch
3854
- hasCreate
3855
- />,
3856
- );
3857
- await user.click(screen.getByRole('button', {name: 'Labels'}));
3858
- const search = screen.getByRole('combobox', h);
3859
- for (const value of ['Ur', 'Urg', 'Urge', 'Urgent']) {
3860
- fireEvent.change(search, {target: {value}});
3861
- }
3862
- await act(
3863
- async () =>
3864
- new Promise<void>(resolve => requestAnimationFrame(() => resolve())),
3865
- );
3866
- expect(politeRegion()?.textContent).toBe('Create "Urgent"');
3867
- });
3868
-
3869
- it('says only the empty panel’s message when the next keystroke in the frame empties the panel (spec:AST-086 FR13)', async () => {
3870
- const user = userEvent.setup();
3871
- render(
3872
- <MultiSelector
3873
- label="Labels"
3874
- options={OPTIONS}
3875
- value={[]}
3876
- onChange={() => {}}
3877
- hasSearch
3878
- />,
3879
- );
3880
- await user.click(screen.getByRole('button', {name: 'Labels'}));
3881
- const search = screen.getByRole('combobox', h);
3882
- // "Fe" matches Feature; "Fex" matches nothing, in the same frame.
3883
- fireEvent.change(search, {target: {value: 'Fe'}});
3884
- fireEvent.change(search, {target: {value: 'Fex'}});
3885
- await act(
3886
- async () =>
3887
- new Promise<void>(resolve => requestAnimationFrame(() => resolve())),
3888
- );
3889
- expect(politeRegion()?.textContent).toBe('No results found');
3890
- });
3891
-
3892
- it('says only the count when the next keystroke in the frame fills an empty panel (spec:AST-086 FR13)', async () => {
3893
- // The empty panel's message comes from the search's `hasReplace`
3894
- // announcer too, so the count that follows it in the frame replaces it.
3895
- const user = userEvent.setup();
3896
- render(
3897
- <MultiSelector
3898
- label="Labels"
3899
- options={OPTIONS}
3900
- value={[]}
3901
- onChange={() => {}}
3902
- hasSearch
3903
- />,
3904
- );
3905
- await user.click(screen.getByRole('button', {name: 'Labels'}));
3906
- const search = screen.getByRole('combobox', h);
3907
- fireEvent.change(search, {target: {value: 'Fex'}});
3908
- fireEvent.change(search, {target: {value: 'Fe'}});
3909
- await act(
3910
- async () =>
3911
- new Promise<void>(resolve => requestAnimationFrame(() => resolve())),
3912
- );
3913
- expect(politeRegion()?.textContent).toBe('1 result');
3914
- });
3915
-
3916
- it('says only the empty panel’s message from inside a modal when the next keystroke in the frame empties the panel (spec:AST-086 FR14)', async () => {
3917
- // Inside an open modal the search's announcer writes to the modal's own
3918
- // container, which replaces the same way.
3919
- HTMLDialogElement.prototype.showModal = vi.fn(function (
3920
- this: HTMLDialogElement,
3921
- ) {
3922
- this.setAttribute('open', '');
3923
- this.dataset.modal = 'true';
3924
- });
3925
- // The file's own `matches` stand-in (for :popover-open) sits on
3926
- // HTMLElement.prototype; :modal is added on top of it.
3927
- const matches = HTMLElement.prototype.matches;
3928
- vi.spyOn(HTMLElement.prototype, 'matches').mockImplementation(function (
3929
- this: HTMLElement,
3930
- selector: string,
3931
- ) {
3932
- return selector === ':modal'
3933
- ? this instanceof HTMLDialogElement &&
3934
- this.open &&
3935
- this.dataset.modal === 'true'
3936
- : matches.call(this, selector);
3937
- });
3938
- const user = userEvent.setup();
3939
- render(
3940
- <Dialog isOpen onOpenChange={() => {}} aria-label="Edit labels">
3941
- <MultiSelector
3942
- label="Labels"
3943
- options={OPTIONS}
3944
- value={[]}
3945
- onChange={() => {}}
3946
- hasSearch
3947
- />
3948
- </Dialog>,
3949
- );
3950
- const dialog = screen.getByRole('dialog', {name: 'Edit labels'});
3951
- await user.click(screen.getByRole('button', {name: 'Labels'}));
3952
- const search = screen.getByRole('combobox', h);
3953
- fireEvent.change(search, {target: {value: 'Fe'}});
3954
- fireEvent.change(search, {target: {value: 'Fex'}});
3955
- await act(
3956
- async () =>
3957
- new Promise<void>(resolve => requestAnimationFrame(() => resolve())),
3958
- );
3959
- expect(
3960
- dialog.querySelector(':scope > [data-astryx-modal-live-region="polite"]')
3961
- ?.textContent,
3962
- ).toBe('No results found');
3963
- });
3964
-
3965
- it('still says a selection change and a result count from the same frame: each has its own announcer (spec:AST-086 FR9)', async () => {
3966
- const user = userEvent.setup();
3967
- render(
3968
- <MultiSelector
3969
- label="Labels"
3970
- options={OPTIONS}
3971
- value={[]}
3972
- onChange={() => {}}
3973
- hasSearch
3974
- />,
3975
- );
3976
- await user.click(screen.getByRole('button', {name: 'Labels'}));
3977
- await act(
3978
- async () =>
3979
- new Promise<void>(resolve => requestAnimationFrame(() => resolve())),
3980
- );
3981
- fireEvent.click(screen.getByRole('option', {name: 'Bug', hidden: true}));
3982
- fireEvent.change(screen.getByRole('combobox', h), {
3983
- target: {value: 'Fe'},
3984
- });
3985
- await act(
3986
- async () =>
3987
- new Promise<void>(resolve => requestAnimationFrame(() => resolve())),
3988
- );
3989
- expect(politeRegion()?.textContent).toBe('1 of 3 selected 1 result');
3990
- });
3991
-
3992
- it('a plain toggle passes no descriptor, so a one-argument handler is unchanged', async () => {
3993
- const user = userEvent.setup();
3994
- const onChange = vi.fn();
3995
- render(
3996
- <MultiSelector
3997
- label="Labels"
3998
- options={OPTIONS}
3999
- value={[]}
4000
- onChange={onChange}
4001
- hasSearch
4002
- hasCreate
4003
- />,
4004
- );
4005
- await user.click(screen.getByRole('button', {name: 'Labels'}));
4006
- await user.click(screen.getByRole('option', {name: 'Bug', ...h}));
4007
- expect(onChange).toHaveBeenCalledWith(['bug']);
4008
- expect(onChange.mock.calls[0]).toHaveLength(1);
4009
- });
4010
-
4011
- it('sits above partial matches and is skipped for a label the filter matches exactly', async () => {
4012
- const user = userEvent.setup();
4013
- render(
4014
- <MultiSelector
4015
- label="Labels"
4016
- options={OPTIONS}
4017
- value={[]}
4018
- onChange={() => {}}
4019
- hasSearch
4020
- hasCreate
4021
- />,
4022
- );
4023
- await user.click(screen.getByRole('button', {name: 'Labels'}));
4024
- const search = screen.getByRole('combobox', h);
4025
-
4026
- await user.type(search, 'Bu');
4027
- expect(screen.getAllByRole('option', h).map(o => o.textContent)).toEqual([
4028
- 'Create "Bu"',
4029
- 'Bug',
4030
- ]);
4031
-
4032
- await user.type(search, 'g');
4033
- expect(screen.getAllByRole('option', h).map(o => o.textContent)).toEqual([
4034
- 'Bug',
4035
- ]);
4036
-
4037
- await user.clear(search);
4038
- await user.type(search, 'bug');
4039
- // Case-insensitive, like the filter: "bug" is Bug, so nothing to create.
4040
- expect(screen.getAllByRole('option', h).map(o => o.textContent)).toEqual([
4041
- 'Bug',
4042
- ]);
4043
-
4044
- await user.clear(search);
4045
- await user.type(search, 'CAFE');
4046
- // The duplicate rule is the filter's rule: the filter cannot surface
4047
- // Café for CAFE, so refusing to create would be a dead end.
4048
- expect(screen.getAllByRole('option', h).map(o => o.textContent)).toEqual([
4049
- 'Create "CAFE"',
4050
- ]);
4051
- });
4052
-
4053
- it('leads the select-all row and is not handed to renderOption', async () => {
4054
- const user = userEvent.setup();
4055
- const renderOption = vi.fn((option: {label?: string}) => (
4056
- <span>{`row:${option.label}`}</span>
4057
- ));
4058
- render(
4059
- <MultiSelector
4060
- label="Labels"
4061
- options={OPTIONS}
4062
- value={[]}
4063
- onChange={() => {}}
4064
- hasSearch
4065
- hasSelectAll
4066
- hasCreate
4067
- renderOption={renderOption}
4068
- />,
4069
- );
4070
- await user.click(screen.getByRole('button', {name: 'Labels'}));
4071
- await user.type(screen.getByRole('combobox', h), 'Bu');
4072
- const options = screen.getAllByRole('option', h);
4073
- expect(options[0]).toHaveTextContent('Create "Bu"');
4074
- expect(options[1]).toHaveTextContent('Select all');
4075
- expect(options[2]).toHaveTextContent('row:Bug');
4076
- expect(
4077
- renderOption.mock.calls.some(([option]) =>
4078
- (option.label ?? '').startsWith('Create'),
4079
- ),
4080
- ).toBe(false);
4081
- });
4082
-
4083
- it('Enter with nothing highlighted commits the create row; ArrowDown reaches it', async () => {
4084
- const user = userEvent.setup();
4085
- const onChange = vi.fn();
4086
- render(
4087
- <MultiSelector
4088
- label="Labels"
4089
- options={OPTIONS}
4090
- value={[]}
4091
- onChange={onChange}
4092
- hasSearch
4093
- hasCreate
4094
- />,
4095
- );
4096
- await user.click(screen.getByRole('button', {name: 'Labels'}));
4097
- const search = screen.getByRole('combobox', h);
4098
- await user.type(search, 'Urgent');
4099
- await user.keyboard('{Enter}');
4100
- expect(onChange).toHaveBeenLastCalledWith(['Urgent'], {
4101
- type: 'create',
4102
- query: 'Urgent',
4103
- });
4104
-
4105
- await user.type(search, 'Later');
4106
- fireEvent.keyDown(search, {key: 'ArrowDown'});
4107
- const createRow = screen.getByRole('option', {
4108
- name: 'Create "Later"',
4109
- ...h,
4110
- });
4111
- expect(search).toHaveAttribute('aria-activedescendant', createRow.id);
4112
- fireEvent.keyDown(search, {key: 'Enter'});
4113
- expect(onChange).toHaveBeenLastCalledWith(
4114
- expect.arrayContaining(['Later']),
4115
- {type: 'create', query: 'Later'},
4116
- );
4117
- });
4118
-
4119
- it('offers nothing while isLoading, and Enter commits nothing', async () => {
4120
- const user = userEvent.setup();
4121
- const onChange = vi.fn();
4122
- const {rerender} = render(
4123
- <MultiSelector
4124
- label="Labels"
4125
- options={[]}
4126
- value={[]}
4127
- onChange={onChange}
4128
- hasSearch
4129
- hasCreate
4130
- isLoading
4131
- />,
4132
- );
4133
- await user.click(screen.getByRole('button', {name: 'Labels'}));
4134
- const search = screen.getByRole('combobox', h);
4135
- await user.type(search, 'Bug');
4136
- expect(screen.queryAllByRole('option', h)).toHaveLength(0);
4137
- expect(politeRegion()?.textContent ?? '').toBe('');
4138
- await user.keyboard('{Enter}');
4139
- expect(onChange).not.toHaveBeenCalled();
4140
-
4141
- // The options land: Bug exists, so still no create row for "Bug" — the
4142
- // duplicate the gate was there to prevent.
4143
- rerender(
4144
- <MultiSelector
4145
- label="Labels"
4146
- options={OPTIONS}
4147
- value={[]}
4148
- onChange={onChange}
4149
- hasSearch
4150
- hasCreate
4151
- />,
4152
- );
4153
- expect(screen.getAllByRole('option', h).map(o => o.textContent)).toEqual([
4154
- 'Bug',
4155
- ]);
4156
- });
4157
-
4158
- it('announces the create row, not "No results found", when it is the only result', async () => {
4159
- const user = userEvent.setup();
4160
- render(
4161
- <MultiSelector
4162
- label="Labels"
4163
- options={OPTIONS}
4164
- value={[]}
4165
- onChange={() => {}}
4166
- hasSearch
4167
- hasCreate
4168
- />,
4169
- );
4170
- await user.click(screen.getByRole('button', {name: 'Labels'}));
4171
- await enterQuery(user, screen.getByRole('combobox', h), 'Urgent');
4172
- // The empty-state message is role="presentation", so the live region is
4173
- // what a screen reader gets — it must say what is on screen: a row.
4174
- await waitFor(() => {
4175
- expect(politeRegion()?.textContent).toBe('Create "Urgent"');
4176
- });
4177
- });
4178
-
4179
- it('warns in development and offers nothing when hasCreate is set without hasSearch', async () => {
4180
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
4181
- try {
4182
- const user = userEvent.setup();
4183
- render(
4184
- <MultiSelector
4185
- label="Labels"
4186
- options={OPTIONS}
4187
- value={[]}
4188
- onChange={() => {}}
4189
- hasCreate
4190
- />,
4191
- );
4192
- expect(warn).toHaveBeenCalledWith(
4193
- expect.stringMatching(/MultiSelector[\s\S]*hasSearch/),
4194
- );
4195
- await user.click(screen.getByRole('combobox', {name: 'Labels'}));
4196
- // No search input, no query, no create row: the three real options only.
4197
- expect(
4198
- screen.queryByRole('combobox', {name: /search/i, ...h}),
4199
- ).toBeNull();
4200
- expect(screen.getAllByRole('option', h)).toHaveLength(3);
4201
- } finally {
4202
- warn.mockRestore();
4203
- }
4204
- });
4205
-
4206
- it('is off by default', async () => {
4207
- const user = userEvent.setup();
4208
- render(
4209
- <MultiSelector
4210
- label="Labels"
4211
- options={OPTIONS}
4212
- value={[]}
4213
- onChange={() => {}}
4214
- hasSearch
4215
- />,
4216
- );
4217
- await user.click(screen.getByRole('button', {name: 'Labels'}));
4218
- await user.type(screen.getByRole('combobox', h), 'Urgent');
4219
- expect(screen.queryAllByRole('option', h)).toHaveLength(0);
4220
- expect(screen.getByText('No results found')).toBeInTheDocument();
4221
- });
4222
- });
4223
-
4224
- describe('MultiSelector option actions (grid)', () => {
4225
- const edit = vi.fn();
4226
- const OPTIONS = [
4227
- {
4228
- value: 'bug',
4229
- label: 'Bug',
4230
- action: (
4231
- <button
4232
- type="button"
4233
- onClick={() => {
4234
- edit('bug');
4235
- }}>
4236
- Edit Bug
4237
- </button>
4238
- ),
4239
- },
4240
- {value: 'feature', label: 'Feature'},
4241
- {
4242
- value: 'docs',
4243
- label: 'Docs',
4244
- disabled: true,
4245
- action: (
4246
- <button
4247
- type="button"
4248
- onClick={() => {
4249
- edit('docs');
4250
- }}>
4251
- Edit Docs
4252
- </button>
4253
- ),
4254
- },
4255
- ];
4256
- beforeEach(() => {
4257
- edit.mockClear();
4258
- });
4259
-
4260
- it('stays a listbox when no option carries an action, and action: null is none', async () => {
4261
- const user = userEvent.setup();
4262
- render(
4263
- <MultiSelector
4264
- label="Labels"
4265
- options={[
4266
- {value: 'bug', label: 'Bug', action: null},
4267
- {value: 'feature', label: 'Feature'},
4268
- ]}
4269
- value={[]}
4270
- onChange={() => {}}
4271
- />,
4272
- );
4273
- const trigger = screen.getByRole('combobox', {name: 'Labels'});
4274
- expect(trigger).toHaveAttribute('aria-haspopup', 'listbox');
4275
- await user.click(trigger);
4276
- expect(screen.getByRole('listbox', h)).toBeInTheDocument();
4277
- expect(screen.queryByRole('grid', h)).toBeNull();
4278
- expect(screen.getAllByRole('option', h)).toHaveLength(2);
4279
- });
4280
-
4281
- it('is a grid of rows with exactly two gridcells once an option carries an action', async () => {
4282
- const user = userEvent.setup();
4283
- render(
4284
- <MultiSelector
4285
- label="Labels"
4286
- options={OPTIONS}
4287
- value={['bug']}
4288
- onChange={() => {}}
4289
- hasSelectAll
4290
- />,
4291
- );
4292
- const trigger = screen.getByRole('combobox', {name: 'Labels'});
4293
- expect(trigger).toHaveAttribute('aria-haspopup', 'grid');
4294
- await user.click(trigger);
4295
-
4296
- const grid = screen.getByRole('grid', h);
4297
- expect(screen.queryByRole('listbox', h)).toBeNull();
4298
- expect(screen.queryByRole('option', h)).toBeNull();
4299
- expect(grid).toHaveAttribute('aria-multiselectable', 'true');
4300
- expect(trigger.getAttribute('aria-controls')).toBe(grid.id);
4301
-
4302
- const rows = screen.getAllByRole('row', h);
4303
- // select-all + 3 options
4304
- expect(rows).toHaveLength(4);
4305
- for (const row of rows) {
4306
- const cells = Array.from(row.children);
4307
- expect(cells).toHaveLength(2);
4308
- expect(cells[0]).toHaveAttribute('role', 'gridcell');
4309
- expect(cells[1]).toHaveAttribute('role', 'gridcell');
4310
- }
4311
- // Every exposed child of the grid is a row.
4312
- expect(
4313
- Array.from(grid.children).every(c => c.getAttribute('role') === 'row'),
4314
- ).toBe(true);
4315
-
4316
- const bug = screen.getByRole('row', {name: 'Bug', ...h});
4317
- expect(bug).toHaveAttribute('aria-selected', 'true');
4318
- expect(bug.children[1]).toContainElement(
4319
- screen.getByRole('button', {name: 'Edit Bug', ...h}),
4320
- );
4321
- // Rows without an action, and the select-all row, have an empty cell.
4322
- expect(
4323
- screen.getByRole('row', {name: 'Feature', ...h}).children[1],
4324
- ).toBeEmptyDOMElement();
4325
- expect(rows[0].children[1]).toBeEmptyDOMElement();
4326
- // The row's name is the option; the control keeps its own.
4327
- expect(bug).toHaveAccessibleName('Bug');
4328
- });
4329
-
4330
- it('latches: a query that filters the actioned option out keeps the grid', async () => {
4331
- const user = userEvent.setup();
4332
- const {rerender} = render(
4333
- <MultiSelector
4334
- label="Labels"
4335
- options={OPTIONS}
4336
- value={[]}
4337
- onChange={() => {}}
4338
- hasSearch
4339
- />,
4340
- );
4341
- await user.click(screen.getByRole('button', {name: 'Labels'}));
4342
- expect(screen.getByRole('grid', h)).toBeInTheDocument();
4343
- await user.type(screen.getByRole('combobox', h), 'Feat');
4344
- expect(screen.getAllByRole('row', h).map(r => r.textContent)).toEqual([
4345
- 'Feature',
4346
- ]);
4347
- expect(screen.getByRole('grid', h)).toBeInTheDocument();
4348
- // The action removed on a later render: still a grid.
4349
- rerender(
4350
- <MultiSelector
4351
- label="Labels"
4352
- options={[{value: 'feature', label: 'Feature'}]}
4353
- value={[]}
4354
- onChange={() => {}}
4355
- hasSearch
4356
- />,
4357
- );
4358
- expect(screen.getByRole('grid', h)).toBeInTheDocument();
4359
- });
4360
-
4361
- it('becomes a grid when options arrive after loading, inside a section too', async () => {
4362
- const user = userEvent.setup();
4363
- const {rerender} = render(
4364
- <MultiSelector
4365
- label="Labels"
4366
- options={[]}
4367
- value={[]}
4368
- onChange={() => {}}
4369
- isLoading
4370
- />,
4371
- );
4372
- expect(screen.getByRole('combobox', {name: 'Labels'})).toHaveAttribute(
4373
- 'aria-haspopup',
4374
- 'listbox',
4375
- );
4376
- rerender(
4377
- <MultiSelector
4378
- label="Labels"
4379
- options={[
4380
- {
4381
- type: 'section',
4382
- title: 'Type',
4383
- options: OPTIONS,
4384
- },
4385
- ]}
4386
- value={[]}
4387
- onChange={() => {}}
4388
- />,
4389
- );
4390
- const trigger = screen.getByRole('combobox', {name: 'Labels'});
4391
- expect(trigger).toHaveAttribute('aria-haspopup', 'grid');
4392
- await user.click(trigger);
4393
- const group = screen.getByRole('rowgroup', {name: 'Type', ...h});
4394
- expect(screen.queryByRole('group', h)).toBeNull();
4395
- expect(group.querySelectorAll('[role="row"]')).toHaveLength(3);
4396
- });
4397
-
4398
- it('pressing the option cell toggles; pressing the action fires only the action', async () => {
4399
- const user = userEvent.setup();
4400
- const onChange = vi.fn();
4401
- render(
4402
- <MultiSelector
4403
- label="Labels"
4404
- options={OPTIONS}
4405
- value={[]}
4406
- onChange={onChange}
4407
- />,
4408
- );
4409
- await user.click(screen.getByRole('combobox', {name: 'Labels'}));
4410
- const bug = screen.getByRole('row', {name: 'Bug', ...h});
4411
- await user.click(bug.children[0]);
4412
- expect(onChange).toHaveBeenCalledWith(['bug']);
4413
-
4414
- onChange.mockClear();
4415
- await user.click(screen.getByRole('button', {name: 'Edit Bug', ...h}));
4416
- expect(edit).toHaveBeenCalledWith('bug');
4417
- expect(onChange).not.toHaveBeenCalled();
4418
- // The panel stays open.
4419
- expect(screen.getByRole('grid', h)).toBeInTheDocument();
4420
-
4421
- // A disabled option's action is the caller's control, still live.
4422
- await user.click(screen.getByRole('button', {name: 'Edit Docs', ...h}));
4423
- expect(edit).toHaveBeenCalledWith('docs');
4424
- expect(onChange).not.toHaveBeenCalled();
4425
- });
4426
-
4427
- it('draws the standard focus ring on the active action cell, from the focus tokens', async () => {
4428
- // jsdom resolves no custom property, so the ring is read from StyleX
4429
- // classes: a class name hashes one property and its value, and the same
4430
- // declaration yields the same class wherever it is written.
4431
- const ring = stylex.create({
4432
- active: {
4433
- outlineWidth: focusVars['--focus-outline-width'],
4434
- outlineStyle: focusVars['--focus-outline-style'],
4435
- outlineColor: focusVars['--focus-outline-color'],
4436
- outlineOffset: focusVars['--focus-outline-offset'],
4437
- },
4438
- });
4439
- const ringClasses = stylex
4440
- .props(ring.active)
4441
- .className!.split(' ')
4442
- .filter(token => /^x[a-z0-9]+$/.test(token));
4443
- expect(ringClasses).toHaveLength(4);
4444
-
4445
- const user = userEvent.setup();
4446
- render(
4447
- <MultiSelector
4448
- label="Labels"
4449
- options={OPTIONS}
4450
- value={[]}
4451
- onChange={() => {}}
4452
- />,
4453
- );
4454
- const trigger = screen.getByRole('combobox', {name: 'Labels'});
4455
- await user.click(trigger);
4456
- const bug = screen.getByRole('row', {name: 'Bug', ...h});
4457
- const actionCell = bug.children[1] as HTMLElement;
4458
- for (const token of ringClasses) {
4459
- expect(actionCell).not.toHaveClass(token);
4460
- }
4461
- fireEvent.keyDown(trigger, {key: 'ArrowRight'});
4462
- expect(trigger).toHaveAttribute('aria-activedescendant', actionCell.id);
4463
- for (const token of ringClasses) {
4464
- expect(actionCell).toHaveClass(token);
4465
- }
4466
- });
4467
-
4468
- it('draws no focus ring on an action cell highlighted after pointer input, until a key moves the highlight', async () => {
4469
- const ring = stylex.create({
4470
- active: {
4471
- outlineWidth: focusVars['--focus-outline-width'],
4472
- outlineStyle: focusVars['--focus-outline-style'],
4473
- outlineColor: focusVars['--focus-outline-color'],
4474
- outlineOffset: focusVars['--focus-outline-offset'],
4475
- },
4476
- });
4477
- const ringClasses = stylex
4478
- .props(ring.active)
4479
- .className!.split(' ')
4480
- .filter(token => /^x[a-z0-9]+$/.test(token));
4481
- expect(ringClasses).toHaveLength(4);
4482
-
4483
- const user = userEvent.setup();
4484
- render(
4485
- <MultiSelector
4486
- label="Labels"
4487
- options={OPTIONS}
4488
- value={[]}
4489
- onChange={() => {}}
4490
- />,
4491
- );
4492
- const trigger = screen.getByRole('combobox', {name: 'Labels'});
4493
- await user.click(trigger);
4494
- const bug = screen.getByRole('row', {name: 'Bug', ...h});
4495
- const actionCell = bug.children[1] as HTMLElement;
4496
- fireEvent.mouseEnter(actionCell);
4497
- // The hover moved the highlight onto the action cell, without the ring.
4498
- expect(trigger).toHaveAttribute('aria-activedescendant', actionCell.id);
4499
- for (const token of ringClasses) {
4500
- expect(actionCell).not.toHaveClass(token);
4501
- }
4502
- // Keyboard navigation back onto the cell draws it.
4503
- fireEvent.keyDown(trigger, {key: 'ArrowLeft'});
4504
- fireEvent.keyDown(trigger, {key: 'ArrowRight'});
4505
- expect(trigger).toHaveAttribute('aria-activedescendant', actionCell.id);
4506
- for (const token of ringClasses) {
4507
- expect(actionCell).toHaveClass(token);
4508
- }
4509
- });
4510
-
4511
- describe('ring under mixed input', () => {
4512
- const ringClasses = () => {
4513
- const ring = stylex.create({
4514
- active: {
4515
- outlineWidth: focusVars['--focus-outline-width'],
4516
- outlineStyle: focusVars['--focus-outline-style'],
4517
- outlineColor: focusVars['--focus-outline-color'],
4518
- outlineOffset: focusVars['--focus-outline-offset'],
4519
- },
4520
- });
4521
- return stylex
4522
- .props(ring.active)
4523
- .className!.split(' ')
4524
- .filter(token => /^x[a-z0-9]+$/.test(token));
4525
- };
4526
- const hasRing = (cell: HTMLElement) =>
4527
- ringClasses().every(token => cell.classList.contains(token));
4528
-
4529
- async function openWithKeyboardOnBugAction() {
4530
- const user = userEvent.setup();
4531
- render(
4532
- <MultiSelector
4533
- label="Labels"
4534
- options={OPTIONS}
4535
- value={[]}
4536
- onChange={() => {}}
4537
- />,
4538
- );
4539
- const trigger = screen.getByRole('combobox', {name: 'Labels'});
4540
- await user.click(trigger);
4541
- const row = (name: string) => screen.getByRole('row', {name, ...h});
4542
- fireEvent.keyDown(trigger, {key: 'ArrowRight'});
4543
- const bugAction = row('Bug').children[1] as HTMLElement;
4544
- expect(trigger).toHaveAttribute('aria-activedescendant', bugAction.id);
4545
- expect(hasRing(bugAction)).toBe(true);
4546
- return {trigger, row, bugAction};
4547
- }
4548
-
4549
- it('keeps the ring when the pointer crosses a disabled option the highlight ignores', async () => {
4550
- const {trigger, row, bugAction} = await openWithKeyboardOnBugAction();
4551
- fireEvent.mouseEnter(row('Docs').children[0]);
4552
- expect(trigger).toHaveAttribute('aria-activedescendant', bugAction.id);
4553
- expect(hasRing(bugAction)).toBe(true);
4554
- });
4555
-
4556
- it('follows the keyboard modality when a hover moves the highlight to another action', async () => {
4557
- const {trigger, row} = await openWithKeyboardOnBugAction();
4558
- const docsAction = row('Docs').children[1] as HTMLElement;
4559
- fireEvent.mouseEnter(docsAction);
4560
- expect(trigger).toHaveAttribute('aria-activedescendant', docsAction.id);
4561
- expect(hasRing(docsAction)).toBe(true);
4562
- });
4563
-
4564
- it('draws no ring for a modifier chord after pointer input', async () => {
4565
- const user = userEvent.setup();
4566
- render(
4567
- <MultiSelector
4568
- label="Labels"
4569
- options={OPTIONS}
4570
- value={[]}
4571
- onChange={() => {}}
4572
- />,
4573
- );
4574
- const trigger = screen.getByRole('combobox', {name: 'Labels'});
4575
- await user.click(trigger);
4576
- const bugAction = screen.getByRole('row', {name: 'Bug', ...h})
4577
- .children[1] as HTMLElement;
4578
- fireEvent.mouseEnter(bugAction);
4579
- expect(hasRing(bugAction)).toBe(false);
4580
- fireEvent.keyDown(trigger, {key: 'ArrowRight', ctrlKey: true});
4581
- expect(hasRing(bugAction)).toBe(false);
4582
- });
4583
- });
4584
-
4585
- it('the inline-end arrow reaches the action cell, Enter fires it, inline-start returns', async () => {
4586
- const user = userEvent.setup();
4587
- const onChange = vi.fn();
4588
- render(
4589
- <MultiSelector
4590
- label="Labels"
4591
- options={OPTIONS}
4592
- value={[]}
4593
- onChange={onChange}
4594
- />,
4595
- );
4596
- const trigger = screen.getByRole('combobox', {name: 'Labels'});
4597
- await user.click(trigger);
4598
- const bug = screen.getByRole('row', {name: 'Bug', ...h});
4599
- // Click-open highlights the first row, on its option cell.
4600
- expect(trigger).toHaveAttribute('aria-activedescendant', bug.id);
4601
-
4602
- fireEvent.keyDown(trigger, {key: 'ArrowRight'});
4603
- expect(trigger).toHaveAttribute(
4604
- 'aria-activedescendant',
4605
- bug.children[1].id,
4606
- );
4607
- fireEvent.keyDown(trigger, {key: 'Enter'});
4608
- expect(edit).toHaveBeenCalledWith('bug');
4609
- expect(onChange).not.toHaveBeenCalled();
4610
- // The highlight stays where it was.
4611
- expect(trigger).toHaveAttribute(
4612
- 'aria-activedescendant',
4613
- bug.children[1].id,
4614
- );
4615
-
4616
- fireEvent.keyDown(trigger, {key: 'ArrowLeft'});
4617
- expect(trigger).toHaveAttribute('aria-activedescendant', bug.id);
4618
- fireEvent.keyDown(trigger, {key: 'Enter'});
4619
- expect(onChange).toHaveBeenCalledWith(['bug']);
4620
-
4621
- // A row without an action: the inline-end arrow is a no-op there, and
4622
- // Down from the action cell lands on the next row's option cell.
4623
- fireEvent.keyDown(trigger, {key: 'ArrowRight'});
4624
- fireEvent.keyDown(trigger, {key: 'ArrowDown'});
4625
- const feature = screen.getByRole('row', {name: 'Feature', ...h});
4626
- expect(trigger).toHaveAttribute('aria-activedescendant', feature.id);
4627
- fireEvent.keyDown(trigger, {key: 'ArrowRight'});
4628
- expect(trigger).toHaveAttribute('aria-activedescendant', feature.id);
4629
- });
4630
-
4631
- it('follows visual direction under RTL', async () => {
4632
- const user = userEvent.setup();
4633
- const original = window.getComputedStyle;
4634
- const spy = vi
4635
- .spyOn(window, 'getComputedStyle')
4636
- .mockImplementation((el: Element, pseudo?: string | null) => {
4637
- const style = original.call(window, el, pseudo);
4638
- Object.defineProperty(style, 'direction', {value: 'rtl'});
4639
- return style;
4640
- });
4641
- try {
4642
- render(
4643
- <MultiSelector
4644
- label="Labels"
4645
- options={OPTIONS}
4646
- value={[]}
4647
- onChange={() => {}}
4648
- />,
4649
- );
4650
- const trigger = screen.getByRole('combobox', {name: 'Labels'});
4651
- await user.click(trigger);
4652
- const bug = screen.getByRole('row', {name: 'Bug', ...h});
4653
- fireEvent.keyDown(trigger, {key: 'ArrowRight'});
4654
- expect(trigger).toHaveAttribute('aria-activedescendant', bug.id);
4655
- fireEvent.keyDown(trigger, {key: 'ArrowLeft'});
4656
- expect(trigger).toHaveAttribute(
4657
- 'aria-activedescendant',
4658
- bug.children[1].id,
4659
- );
4660
- } finally {
4661
- spy.mockRestore();
4662
- }
4663
- });
4664
-
4665
- it('with search, Left/Right move the caret until a row is highlighted', async () => {
4666
- const user = userEvent.setup();
4667
- render(
4668
- <MultiSelector
4669
- label="Labels"
4670
- options={OPTIONS}
4671
- value={[]}
4672
- onChange={() => {}}
4673
- hasSearch
4674
- />,
4675
- );
4676
- await user.click(screen.getByRole('button', {name: 'Labels'}));
4677
- const search = screen.getByRole('combobox', h);
4678
- await user.type(search, 'Bu');
4679
- // No row highlighted: the arrow is the input's.
4680
- const right = fireEvent.keyDown(search, {key: 'ArrowRight'});
4681
- expect(right).toBe(true);
4682
- expect(search).not.toHaveAttribute('aria-activedescendant');
4683
-
4684
- fireEvent.keyDown(search, {key: 'ArrowDown'});
4685
- const bug = screen.getByRole('row', {name: 'Bug', ...h});
4686
- expect(search).toHaveAttribute('aria-activedescendant', bug.id);
4687
- const moved = fireEvent.keyDown(search, {key: 'ArrowRight'});
4688
- expect(moved).toBe(false);
4689
- expect(search).toHaveAttribute('aria-activedescendant', bug.children[1].id);
4690
- fireEvent.keyDown(search, {key: 'Enter'});
4691
- expect(edit).toHaveBeenCalledWith('bug');
4692
- expect(search).toHaveValue('Bu');
4693
- });
4694
-
4695
- it('hovering the action cell highlights that row on its action cell', async () => {
4696
- const user = userEvent.setup();
4697
- render(
4698
- <MultiSelector
4699
- label="Labels"
4700
- options={OPTIONS}
4701
- value={[]}
4702
- onChange={() => {}}
4703
- />,
4704
- );
4705
- const trigger = screen.getByRole('combobox', {name: 'Labels'});
4706
- await user.click(trigger);
4707
- const bug = screen.getByRole('row', {name: 'Bug', ...h});
4708
- const feature = screen.getByRole('row', {name: 'Feature', ...h});
4709
- fireEvent.mouseEnter(feature.children[0]);
4710
- expect(trigger).toHaveAttribute('aria-activedescendant', feature.id);
4711
- fireEvent.mouseEnter(bug.children[1]);
4712
- expect(trigger).toHaveAttribute(
4713
- 'aria-activedescendant',
4714
- bug.children[1].id,
4715
- );
4716
- fireEvent.mouseEnter(bug.children[0]);
4717
- expect(trigger).toHaveAttribute('aria-activedescendant', bug.id);
4718
- });
4719
-
4720
- it('Tab closes the panel as before: no action control is a stop inside it', async () => {
4721
- const user = userEvent.setup();
4722
- render(
4723
- <MultiSelector
4724
- label="Labels"
4725
- options={OPTIONS}
4726
- value={[]}
4727
- onChange={() => {}}
4728
- />,
4729
- );
4730
- const trigger = screen.getByRole('combobox', {name: 'Labels'});
4731
- await user.click(trigger);
4732
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
4733
- fireEvent.keyDown(trigger, {key: 'Tab'});
4734
- await waitFor(() => {
4735
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
4736
- });
4737
- });
4738
-
4739
- it('warns in development about an action control with no accessible name', async () => {
4740
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
4741
- try {
4742
- const user = userEvent.setup();
4743
- render(
4744
- <MultiSelector
4745
- label="Labels"
4746
- options={[
4747
- {value: 'bug', label: 'Bug', action: <button type="button" />},
4748
- ]}
4749
- value={[]}
4750
- onChange={() => {}}
4751
- />,
4752
- );
4753
- await user.click(screen.getByRole('combobox', {name: 'Labels'}));
4754
- expect(warn).toHaveBeenCalledWith(
4755
- expect.stringMatching(/MultiSelector[\s\S]*accessible name/),
4756
- );
4757
- } finally {
4758
- warn.mockRestore();
4759
- }
4760
- });
4761
- });
4762
-
4763
- describe('MultiSelector create row inside a grid', () => {
4764
- it('is a row with exactly two gridcells, the action cell empty, and the arrow stays on the option', async () => {
4765
- // spec:AST-058 FR3: in a grid every row has exactly two cells, and a
4766
- // system-minted row is no exception. The create row has no control of
4767
- // its own, so its action cell is present and empty, and the inline-end
4768
- // arrow has nowhere to go.
4769
- const user = userEvent.setup();
4770
- const onChange =
4771
- vi.fn<(value: string[], change?: MultiSelectorChange) => void>();
4772
- render(
4773
- <MultiSelector
4774
- label="Labels"
4775
- options={[
4776
- {
4777
- value: 'bug',
4778
- label: 'Bug',
4779
- action: <button type="button">Edit Bug</button>,
4780
- },
4781
- {value: 'feature', label: 'Feature'},
4782
- ]}
4783
- value={[]}
4784
- onChange={onChange}
4785
- hasSearch
4786
- hasCreate
4787
- />,
4788
- );
4789
- await user.click(screen.getByRole('button', {name: 'Labels'}));
4790
- const search = screen.getByRole('combobox', {hidden: true});
4791
- await user.type(search, 'Urgent');
4792
-
4793
- const rows = screen.getAllByRole('row', {hidden: true});
4794
- expect(rows).toHaveLength(1);
4795
- const create = rows[0];
4796
- expect(create).toHaveTextContent('Create "Urgent"');
4797
- const cells = Array.from(create.children).filter(
4798
- c => c.getAttribute('role') === 'gridcell',
4799
- );
4800
- expect(cells).toHaveLength(2);
4801
- expect(cells[1]).toBeEmptyDOMElement();
4802
-
4803
- await user.keyboard('{ArrowDown}{ArrowRight}{Enter}');
4804
- expect(onChange).toHaveBeenCalledWith(['Urgent'], {
4805
- type: 'create',
4806
- query: 'Urgent',
4807
- });
4808
- });
4809
- });
4810
-
4811
- describe('MultiSelector in an InputGroup', () => {
4812
- const groupClasses = () =>
4813
- stylex.props(groupStyles.inGroup).className!.split(' ');
4814
- const triggerBox = (name: RegExp) =>
4815
- screen
4816
- .getByRole('combobox', {name})
4817
- .closest('.astryx-multi-selector') as HTMLElement;
4818
-
4819
- it.each(['input', 'secondary'] as const)(
4820
- 'gives a grouped %s trigger the connected group geometry in every position',
4821
- variant => {
4822
- const selector = (label: string) => (
4823
- <MultiSelector
4824
- label={label}
4825
- isLabelHidden
4826
- variant={variant}
4827
- options={['Apple', 'Banana', 'Cherry']}
4828
- value={EMPTY_VALUE}
4829
- onChange={() => {}}
4830
- />
4831
- );
4832
- render(
4833
- <>
4834
- <InputGroup label="First">
4835
- {selector('Fruit first')}
4836
- <InputGroupText>kg</InputGroupText>
4837
- </InputGroup>
4838
- <InputGroup label="Middle">
4839
- <InputGroupText>#</InputGroupText>
4840
- {selector('Fruit middle')}
4841
- <InputGroupText>kg</InputGroupText>
4842
- </InputGroup>
4843
- <InputGroup label="Last">
4844
- <InputGroupText>#</InputGroupText>
4845
- {selector('Fruit last')}
4846
- </InputGroup>
4847
- </>,
4848
- );
4849
-
4850
- // Row height, seam radii, and focus stacking come from these classes;
4851
- // a secondary trigger keeps its fill but not its standalone edges.
4852
- for (const position of ['first', 'middle', 'last']) {
4853
- const classes = new Set(
4854
- triggerBox(new RegExp(`Fruit ${position}`)).className.split(' '),
4855
- );
4856
- expect(groupClasses().every(c => classes.has(c))).toBe(true);
4857
- }
4858
- },
4859
- );
4860
-
4861
- it('keeps a standalone secondary trigger out of the group geometry', () => {
4862
- render(
4863
- <MultiSelector
4864
- label="Fruit"
4865
- variant="secondary"
4866
- options={['Apple', 'Banana', 'Cherry']}
4867
- value={EMPTY_VALUE}
4868
- onChange={() => {}}
4869
- />,
4870
- );
4871
- const classes = new Set(triggerBox(/Fruit/).className.split(' '));
4872
- expect(groupClasses().some(c => classes.has(c))).toBe(false);
4873
- });
4874
- });
4875
-
4876
- describe('MultiSelector caller aria-labelledby', () => {
4877
- const FRUITS = ['Apple', 'Banana', 'Cherry'];
4878
-
4879
- it('names the trigger by its label and a caller aria-labelledby', () => {
4880
- render(
4881
- <>
4882
- <span id="lunch-heading">Lunch</span>
4883
- <MultiSelector
4884
- label="Fruits"
4885
- options={FRUITS}
4886
- value={[]}
4887
- onChange={() => {}}
4888
- aria-labelledby="lunch-heading"
4889
- />
4890
- </>,
4891
- );
4892
- const trigger = screen.getByRole('combobox', {name: 'Fruits Lunch'});
4893
- expect(trigger.getAttribute('aria-labelledby')?.split(' ')).toContain(
4894
- 'lunch-heading',
4895
- );
4896
- });
4897
-
4898
- it('names a grouped trigger by the group, its label, and a caller aria-labelledby', () => {
4899
- render(
4900
- <>
4901
- <span id="lunch-heading">Lunch</span>
4902
- <InputGroup label="Order">
4903
- <MultiSelector
4904
- label="Fruits"
4905
- options={FRUITS}
4906
- value={[]}
4907
- onChange={() => {}}
4908
- aria-labelledby="lunch-heading"
4909
- />
4910
- </InputGroup>
4911
- </>,
4912
- );
4913
- expect(
4914
- screen.getByRole('combobox', {name: 'Order Fruits Lunch'}),
4915
- ).toBeInTheDocument();
4916
- });
4917
-
4918
- it('leaves a standalone trigger named by its label element without a caller aria-labelledby', () => {
4919
- render(
4920
- <MultiSelector
4921
- label="Fruits"
4922
- options={FRUITS}
4923
- value={[]}
4924
- onChange={() => {}}
4925
- />,
4926
- );
4927
- const trigger = screen.getByRole('combobox', {name: 'Fruits'});
4928
- expect(trigger).not.toHaveAttribute('aria-labelledby');
4929
- });
4930
-
4931
- it('still hands a caller aria-labelledby to a renderTrigger control unchanged', () => {
4932
- render(
4933
- <>
4934
- <span id="lunch-heading">Lunch</span>
4935
- <MultiSelector
4936
- label="Fruits"
4937
- options={FRUITS}
4938
- value={[]}
4939
- onChange={() => {}}
4940
- aria-labelledby="lunch-heading"
4941
- renderTrigger={props => (
4942
- <button type="button" {...props}>
4943
- Pick fruits
4944
- </button>
4945
- )}
4946
- />
4947
- </>,
4948
- );
4949
- const opener = screen.getByRole('button', {name: 'Lunch'});
4950
- expect(opener).toHaveAttribute('aria-labelledby', 'lunch-heading');
4951
- });
4952
-
4953
- it("leaves a renderTrigger control's own aria-labelledby in place without a caller value", () => {
4954
- render(
4955
- <>
4956
- <span id="row-title">Lunch fruits</span>
4957
- <MultiSelector
4958
- label="Fruits"
4959
- options={FRUITS}
4960
- value={[]}
4961
- onChange={() => {}}
4962
- renderTrigger={props => (
4963
- <button type="button" aria-labelledby="row-title" {...props} />
4964
- )}
4965
- />
4966
- </>,
4967
- );
4968
- const opener = screen.getByRole('button', {name: 'Lunch fruits'});
4969
- expect(opener).toHaveAttribute('aria-labelledby', 'row-title');
4970
- });
4971
- });