@astryxdesign/core 0.1.2 → 0.1.3-canary.6d57c8d

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 (567) hide show
  1. package/CHANGELOG.md +165 -0
  2. package/README.md +12 -12
  3. package/dist/Avatar/Avatar.d.ts.map +1 -1
  4. package/dist/Avatar/Avatar.js +11 -5
  5. package/dist/Breadcrumbs/BreadcrumbItem.d.ts +1 -1
  6. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  7. package/dist/Breadcrumbs/BreadcrumbItem.js +24 -5
  8. package/dist/Button/Button.d.ts.map +1 -1
  9. package/dist/Button/Button.js +2 -4
  10. package/dist/ButtonGroup/ButtonGroup.d.ts.map +1 -1
  11. package/dist/ButtonGroup/ButtonGroup.js +0 -1
  12. package/dist/Calendar/Calendar.d.ts +5 -4
  13. package/dist/Calendar/Calendar.d.ts.map +1 -1
  14. package/dist/Calendar/Calendar.js +82 -43
  15. package/dist/Calendar/hooks/index.d.ts +0 -2
  16. package/dist/Calendar/hooks/index.d.ts.map +1 -1
  17. package/dist/Calendar/hooks/index.js +1 -2
  18. package/dist/Calendar/index.d.ts +3 -3
  19. package/dist/Calendar/index.d.ts.map +1 -1
  20. package/dist/Calendar/index.js +1 -1
  21. package/dist/Calendar/styles.d.ts +3 -9
  22. package/dist/Calendar/styles.d.ts.map +1 -1
  23. package/dist/Calendar/styles.js +3 -11
  24. package/dist/Carousel/Carousel.d.ts.map +1 -1
  25. package/dist/Carousel/Carousel.js +17 -3
  26. package/dist/Center/Center.d.ts +11 -1
  27. package/dist/Center/Center.d.ts.map +1 -1
  28. package/dist/Center/Center.js +9 -3
  29. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  30. package/dist/Chat/ChatComposerInput.js +0 -1
  31. package/dist/Chat/ChatMessageList.d.ts +14 -1
  32. package/dist/Chat/ChatMessageList.d.ts.map +1 -1
  33. package/dist/Chat/ChatMessageList.js +2 -0
  34. package/dist/Chat/useTriggerMenu.d.ts +8 -1
  35. package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
  36. package/dist/Chat/useTriggerMenu.js +15 -3
  37. package/dist/CheckboxInput/CheckboxInput.d.ts +23 -2
  38. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  39. package/dist/CheckboxInput/CheckboxInput.js +51 -9
  40. package/dist/CheckboxList/CheckboxList.d.ts +13 -1
  41. package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
  42. package/dist/CheckboxList/CheckboxList.js +39 -9
  43. package/dist/CheckboxList/CheckboxListContext.d.ts +7 -0
  44. package/dist/CheckboxList/CheckboxListContext.d.ts.map +1 -1
  45. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  46. package/dist/CheckboxList/CheckboxListItem.js +0 -1
  47. package/dist/Citation/Citation.d.ts.map +1 -1
  48. package/dist/Citation/Citation.js +9 -5
  49. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  50. package/dist/CodeBlock/CodeBlock.js +51 -37
  51. package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
  52. package/dist/CommandPalette/CommandPalette.js +5 -5
  53. package/dist/ContextMenu/ContextMenu.d.ts +4 -5
  54. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  55. package/dist/ContextMenu/ContextMenu.js +93 -11
  56. package/dist/DateInput/DateInput.d.ts +23 -1
  57. package/dist/DateInput/DateInput.d.ts.map +1 -1
  58. package/dist/DateInput/DateInput.js +167 -99
  59. package/dist/DateRangeInput/DateRangeInput.d.ts +23 -1
  60. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  61. package/dist/DateRangeInput/DateRangeInput.js +42 -9
  62. package/dist/DateTimeInput/DateTimeInput.d.ts +35 -2
  63. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  64. package/dist/DateTimeInput/DateTimeInput.js +78 -15
  65. package/dist/Dialog/Dialog.d.ts.map +1 -1
  66. package/dist/Dialog/Dialog.js +12 -0
  67. package/dist/DropdownMenu/DropdownMenu.d.ts +1 -8
  68. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  69. package/dist/DropdownMenu/DropdownMenu.js +42 -14
  70. package/dist/Field/Field.d.ts +19 -2
  71. package/dist/Field/Field.d.ts.map +1 -1
  72. package/dist/Field/Field.js +4 -0
  73. package/dist/Field/FieldLabel.d.ts +21 -2
  74. package/dist/Field/FieldLabel.d.ts.map +1 -1
  75. package/dist/Field/FieldLabel.js +41 -26
  76. package/dist/FileInput/FileInput.d.ts +22 -1
  77. package/dist/FileInput/FileInput.d.ts.map +1 -1
  78. package/dist/FileInput/FileInput.js +56 -10
  79. package/dist/Grid/Grid.d.ts +11 -1
  80. package/dist/Grid/Grid.d.ts.map +1 -1
  81. package/dist/Grid/Grid.js +32 -6
  82. package/dist/InputGroup/InputGroup.d.ts +4 -3
  83. package/dist/InputGroup/InputGroup.d.ts.map +1 -1
  84. package/dist/InputGroup/InputGroup.js +16 -6
  85. package/dist/InputGroup/InputGroupContext.d.ts +4 -0
  86. package/dist/InputGroup/InputGroupContext.d.ts.map +1 -1
  87. package/dist/InputGroup/InputGroupContext.js +2 -2
  88. package/dist/InputGroup/groupStyles.d.ts +1 -1
  89. package/dist/InputGroup/groupStyles.d.ts.map +1 -1
  90. package/dist/InputGroup/groupStyles.js +3 -3
  91. package/dist/InputGroup/index.d.ts +1 -0
  92. package/dist/InputGroup/index.d.ts.map +1 -1
  93. package/dist/Kbd/Kbd.d.ts.map +1 -1
  94. package/dist/Kbd/Kbd.js +33 -1
  95. package/dist/Layer/layerAnimations.stylex.d.ts +4 -0
  96. package/dist/Layer/layerAnimations.stylex.d.ts.map +1 -1
  97. package/dist/Layer/layerAnimations.stylex.js +8 -4
  98. package/dist/Layer/useLayer.d.ts +10 -0
  99. package/dist/Layer/useLayer.d.ts.map +1 -1
  100. package/dist/Layer/useLayer.js +64 -9
  101. package/dist/Layout/Layout.d.ts +4 -11
  102. package/dist/Layout/Layout.d.ts.map +1 -1
  103. package/dist/Layout/Layout.js +10 -11
  104. package/dist/Layout/LayoutFooter.d.ts +2 -2
  105. package/dist/Layout/LayoutFooter.d.ts.map +1 -1
  106. package/dist/Layout/LayoutHeader.d.ts +2 -2
  107. package/dist/Layout/LayoutHeader.d.ts.map +1 -1
  108. package/dist/Layout/LayoutPanel.d.ts +2 -2
  109. package/dist/Layout/LayoutPanel.d.ts.map +1 -1
  110. package/dist/Layout/padding.stylex.d.ts +51 -0
  111. package/dist/Layout/padding.stylex.d.ts.map +1 -1
  112. package/dist/Layout/padding.stylex.js +63 -0
  113. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  114. package/dist/Lightbox/Lightbox.js +4 -2
  115. package/dist/Link/Link.d.ts +16 -1
  116. package/dist/Link/Link.d.ts.map +1 -1
  117. package/dist/Link/Link.js +18 -7
  118. package/dist/Markdown/Markdown.d.ts.map +1 -1
  119. package/dist/Markdown/Markdown.js +7 -0
  120. package/dist/MoreMenu/MoreMenu.d.ts +1 -7
  121. package/dist/MoreMenu/MoreMenu.d.ts.map +1 -1
  122. package/dist/MoreMenu/MoreMenu.js +0 -2
  123. package/dist/MultiSelector/MultiSelector.d.ts +27 -2
  124. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  125. package/dist/MultiSelector/MultiSelector.js +141 -42
  126. package/dist/MultiSelector/hooks.d.ts +12 -1
  127. package/dist/MultiSelector/hooks.d.ts.map +1 -1
  128. package/dist/MultiSelector/hooks.js +14 -1
  129. package/dist/NavMenu/NavHeadingMenu.d.ts.map +1 -1
  130. package/dist/NavMenu/NavHeadingMenu.js +34 -3
  131. package/dist/NumberInput/NumberInput.d.ts +23 -2
  132. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  133. package/dist/NumberInput/NumberInput.js +62 -12
  134. package/dist/Pagination/Pagination.d.ts +1 -1
  135. package/dist/Pagination/Pagination.d.ts.map +1 -1
  136. package/dist/Pagination/Pagination.js +15 -1
  137. package/dist/Popover/Popover.d.ts +18 -2
  138. package/dist/Popover/Popover.d.ts.map +1 -1
  139. package/dist/Popover/Popover.js +5 -2
  140. package/dist/Popover/usePopover.d.ts +38 -3
  141. package/dist/Popover/usePopover.d.ts.map +1 -1
  142. package/dist/Popover/usePopover.js +24 -9
  143. package/dist/PowerSearch/PowerSearch.d.ts +11 -1
  144. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  145. package/dist/PowerSearch/PowerSearch.js +6 -1
  146. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  147. package/dist/ProgressBar/ProgressBar.js +3 -5
  148. package/dist/RadioList/RadioList.d.ts +20 -2
  149. package/dist/RadioList/RadioList.d.ts.map +1 -1
  150. package/dist/RadioList/RadioList.js +105 -9
  151. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  152. package/dist/RadioList/RadioListItem.js +13 -2
  153. package/dist/SegmentedControl/SegmentedControl.d.ts +13 -2
  154. package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -1
  155. package/dist/SegmentedControl/SegmentedControl.js +78 -44
  156. package/dist/SegmentedControl/SegmentedControlContext.d.ts +7 -0
  157. package/dist/SegmentedControl/SegmentedControlContext.d.ts.map +1 -1
  158. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  159. package/dist/SegmentedControl/SegmentedControlItem.js +13 -2
  160. package/dist/Selector/Selector.d.ts +24 -1
  161. package/dist/Selector/Selector.d.ts.map +1 -1
  162. package/dist/Selector/Selector.js +122 -43
  163. package/dist/Selector/hooks.d.ts +8 -1
  164. package/dist/Selector/hooks.d.ts.map +1 -1
  165. package/dist/Selector/hooks.js +14 -1
  166. package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
  167. package/dist/SideNav/SideNavHeading.js +2 -0
  168. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  169. package/dist/SideNav/SideNavItem.js +1 -1
  170. package/dist/Skeleton/Skeleton.d.ts.map +1 -1
  171. package/dist/Skeleton/Skeleton.js +8 -2
  172. package/dist/Slider/Slider.d.ts +21 -0
  173. package/dist/Slider/Slider.d.ts.map +1 -1
  174. package/dist/Slider/Slider.js +36 -9
  175. package/dist/Spinner/Spinner.d.ts.map +1 -1
  176. package/dist/Spinner/Spinner.js +1 -1
  177. package/dist/Stack/Stack.d.ts +38 -23
  178. package/dist/Stack/Stack.d.ts.map +1 -1
  179. package/dist/Stack/Stack.js +25 -1
  180. package/dist/Stack/StackItem.d.ts +12 -23
  181. package/dist/Stack/StackItem.d.ts.map +1 -1
  182. package/dist/Stack/StackItem.js +8 -1
  183. package/dist/Switch/Switch.d.ts +23 -2
  184. package/dist/Switch/Switch.d.ts.map +1 -1
  185. package/dist/Switch/Switch.js +41 -11
  186. package/dist/TabList/Tab.d.ts.map +1 -1
  187. package/dist/TabList/Tab.js +6 -0
  188. package/dist/TabList/TabList.d.ts +16 -4
  189. package/dist/TabList/TabList.d.ts.map +1 -1
  190. package/dist/TabList/TabList.js +87 -10
  191. package/dist/TabList/TabListContext.d.ts +5 -0
  192. package/dist/TabList/TabListContext.d.ts.map +1 -1
  193. package/dist/TabList/TabListContext.js +5 -0
  194. package/dist/TabList/TabMenu.d.ts.map +1 -1
  195. package/dist/TabList/TabMenu.js +6 -1
  196. package/dist/Table/BaseTable.d.ts.map +1 -1
  197. package/dist/Table/BaseTable.js +11 -8
  198. package/dist/Table/Table.d.ts.map +1 -1
  199. package/dist/Table/Table.js +10 -1
  200. package/dist/Table/TableCell.d.ts +8 -1
  201. package/dist/Table/TableCell.d.ts.map +1 -1
  202. package/dist/Table/TableCell.js +10 -10
  203. package/dist/Table/TableHeaderCell.d.ts +8 -1
  204. package/dist/Table/TableHeaderCell.d.ts.map +1 -1
  205. package/dist/Table/TableHeaderCell.js +16 -16
  206. package/dist/Table/index.d.ts +2 -1
  207. package/dist/Table/index.d.ts.map +1 -1
  208. package/dist/Table/index.js +2 -2
  209. package/dist/Table/plugins/pagination/paginateData.d.ts +2 -1
  210. package/dist/Table/plugins/pagination/paginateData.d.ts.map +1 -1
  211. package/dist/Table/plugins/pagination/paginateData.js +8 -3
  212. package/dist/Table/plugins/pagination/useTablePagination.d.ts.map +1 -1
  213. package/dist/Table/plugins/pagination/useTablePagination.js +6 -1
  214. package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
  215. package/dist/Table/plugins/sortable/useTableSortable.js +59 -3
  216. package/dist/Table/tableContextMenu.d.ts +33 -0
  217. package/dist/Table/tableContextMenu.d.ts.map +1 -0
  218. package/dist/Table/tableContextMenu.js +133 -0
  219. package/dist/Table/types.d.ts +66 -0
  220. package/dist/Table/types.d.ts.map +1 -1
  221. package/dist/TextArea/TextArea.d.ts +22 -1
  222. package/dist/TextArea/TextArea.d.ts.map +1 -1
  223. package/dist/TextArea/TextArea.js +41 -9
  224. package/dist/TextInput/TextInput.d.ts +23 -2
  225. package/dist/TextInput/TextInput.d.ts.map +1 -1
  226. package/dist/TextInput/TextInput.js +55 -10
  227. package/dist/TimeInput/TimeInput.d.ts +23 -1
  228. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  229. package/dist/TimeInput/TimeInput.js +147 -76
  230. package/dist/Toast/Toast.d.ts.map +1 -1
  231. package/dist/Toast/Toast.js +14 -0
  232. package/dist/Toast/ToastViewport.d.ts.map +1 -1
  233. package/dist/Toast/ToastViewport.js +131 -3
  234. package/dist/Tokenizer/Tokenizer.d.ts +12 -1
  235. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  236. package/dist/Tokenizer/Tokenizer.js +26 -4
  237. package/dist/Toolbar/Toolbar.d.ts +3 -3
  238. package/dist/Toolbar/Toolbar.d.ts.map +1 -1
  239. package/dist/Toolbar/Toolbar.js +51 -7
  240. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  241. package/dist/Tooltip/useTooltip.js +56 -9
  242. package/dist/TopNav/TopNavHeading.d.ts +7 -1
  243. package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
  244. package/dist/TopNav/TopNavHeading.js +8 -0
  245. package/dist/TreeList/TreeList.d.ts +1 -1
  246. package/dist/TreeList/TreeList.d.ts.map +1 -1
  247. package/dist/TreeList/TreeList.js +72 -2
  248. package/dist/TreeList/TreeListItem.d.ts +11 -1
  249. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  250. package/dist/TreeList/TreeListItem.js +51 -12
  251. package/dist/TreeList/treeListItem.markers.stylex.d.ts +16 -0
  252. package/dist/TreeList/treeListItem.markers.stylex.d.ts.map +1 -0
  253. package/dist/TreeList/treeListItem.markers.stylex.js +22 -0
  254. package/dist/Typeahead/BaseTypeahead.d.ts +13 -0
  255. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  256. package/dist/Typeahead/BaseTypeahead.js +69 -6
  257. package/dist/Typeahead/Typeahead.d.ts +26 -3
  258. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  259. package/dist/Typeahead/Typeahead.js +65 -29
  260. package/dist/VisuallyHidden/VisuallyHidden.d.ts +61 -0
  261. package/dist/VisuallyHidden/VisuallyHidden.d.ts.map +1 -0
  262. package/dist/VisuallyHidden/VisuallyHidden.js +65 -0
  263. package/dist/VisuallyHidden/index.d.ts +11 -0
  264. package/dist/VisuallyHidden/index.d.ts.map +1 -0
  265. package/dist/VisuallyHidden/index.js +13 -0
  266. package/dist/astryx.css +18 -0
  267. package/dist/astryx.umd.js +55 -55
  268. package/dist/astryx.umd.js.map +4 -4
  269. package/dist/hooks/focusableSelector.d.ts +22 -0
  270. package/dist/hooks/focusableSelector.d.ts.map +1 -0
  271. package/dist/hooks/focusableSelector.js +24 -0
  272. package/dist/hooks/index.d.ts +12 -2
  273. package/dist/hooks/index.d.ts.map +1 -1
  274. package/dist/hooks/index.js +7 -2
  275. package/dist/hooks/useAnnounce.d.ts +40 -0
  276. package/dist/hooks/useAnnounce.d.ts.map +1 -0
  277. package/dist/hooks/useAnnounce.js +142 -0
  278. package/dist/hooks/useClickableContainer.d.ts +16 -1
  279. package/dist/hooks/useClickableContainer.d.ts.map +1 -1
  280. package/dist/hooks/useClickableContainer.js +16 -4
  281. package/dist/hooks/useEntryAnimation.d.ts.map +1 -1
  282. package/dist/hooks/useEntryAnimation.js +4 -4
  283. package/dist/hooks/useFocusTrap.d.ts +18 -0
  284. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  285. package/dist/hooks/useFocusTrap.js +87 -3
  286. package/dist/hooks/useGridFocus.d.ts +88 -1
  287. package/dist/hooks/useGridFocus.d.ts.map +1 -1
  288. package/dist/hooks/useGridFocus.js +261 -62
  289. package/dist/hooks/useKeyboardHint.d.ts +82 -0
  290. package/dist/hooks/useKeyboardHint.d.ts.map +1 -0
  291. package/dist/hooks/useKeyboardHint.js +220 -0
  292. package/dist/hooks/useListFocus.d.ts +78 -7
  293. package/dist/hooks/useListFocus.d.ts.map +1 -1
  294. package/dist/hooks/useListFocus.js +305 -52
  295. package/dist/hooks/useLongPress.d.ts +21 -0
  296. package/dist/hooks/useLongPress.d.ts.map +1 -0
  297. package/dist/hooks/useLongPress.js +89 -0
  298. package/dist/hooks/useTreeFocus.d.ts +157 -0
  299. package/dist/hooks/useTreeFocus.d.ts.map +1 -0
  300. package/dist/hooks/useTreeFocus.js +337 -0
  301. package/dist/hooks/useTypeahead.d.ts +63 -0
  302. package/dist/hooks/useTypeahead.d.ts.map +1 -0
  303. package/dist/hooks/useTypeahead.js +117 -0
  304. package/dist/index.d.ts +1 -0
  305. package/dist/index.d.ts.map +1 -1
  306. package/dist/index.js +1 -0
  307. package/dist/utils/dateTypes.d.ts +12 -0
  308. package/dist/utils/dateTypes.d.ts.map +1 -1
  309. package/dist/utils/dateTypes.js +41 -1
  310. package/dist/utils/index.d.ts +4 -0
  311. package/dist/utils/index.d.ts.map +1 -1
  312. package/dist/utils/index.js +3 -1
  313. package/dist/utils/inputAria.d.ts +29 -0
  314. package/dist/utils/inputAria.d.ts.map +1 -0
  315. package/dist/utils/inputAria.js +30 -0
  316. package/dist/utils/timeParser.d.ts.map +1 -1
  317. package/dist/utils/timeParser.js +5 -2
  318. package/docs.mjs +2 -2
  319. package/package.json +12 -8
  320. package/src/Avatar/Avatar.test.tsx +37 -0
  321. package/src/Avatar/Avatar.tsx +10 -5
  322. package/src/Breadcrumbs/BreadcrumbItem.tsx +25 -7
  323. package/src/Breadcrumbs/Breadcrumbs.test.tsx +31 -10
  324. package/src/Button/Button.tsx +3 -16
  325. package/src/ButtonGroup/ButtonGroup.test.tsx +9 -9
  326. package/src/ButtonGroup/ButtonGroup.tsx +0 -1
  327. package/src/Calendar/Calendar.doc.mjs +4 -4
  328. package/src/Calendar/Calendar.test.tsx +168 -30
  329. package/src/Calendar/Calendar.tsx +121 -66
  330. package/src/Calendar/hooks/index.ts +0 -6
  331. package/src/Calendar/index.ts +1 -3
  332. package/src/Calendar/styles.ts +4 -9
  333. package/src/Carousel/Carousel.doc.mjs +7 -1
  334. package/src/Carousel/Carousel.test.tsx +20 -0
  335. package/src/Carousel/Carousel.tsx +16 -1
  336. package/src/Center/Center.doc.mjs +12 -2
  337. package/src/Center/Center.tsx +28 -3
  338. package/src/Chat/ChatComposerInput.test.tsx +26 -7
  339. package/src/Chat/ChatComposerInput.tsx +0 -1
  340. package/src/Chat/ChatDictationButton.doc.mjs +1 -1
  341. package/src/Chat/ChatMessageList.doc.mjs +8 -0
  342. package/src/Chat/ChatMessageList.test.tsx +18 -0
  343. package/src/Chat/ChatMessageList.tsx +16 -0
  344. package/src/Chat/useTriggerMenu.tsx +22 -4
  345. package/src/CheckboxInput/CheckboxInput.doc.mjs +10 -0
  346. package/src/CheckboxInput/CheckboxInput.test.tsx +172 -27
  347. package/src/CheckboxInput/CheckboxInput.tsx +78 -6
  348. package/src/CheckboxList/CheckboxList.doc.mjs +9 -0
  349. package/src/CheckboxList/CheckboxList.test.tsx +192 -26
  350. package/src/CheckboxList/CheckboxList.tsx +56 -3
  351. package/src/CheckboxList/CheckboxListContext.tsx +7 -0
  352. package/src/CheckboxList/CheckboxListItem.tsx +0 -3
  353. package/src/Citation/Citation.doc.mjs +10 -0
  354. package/src/Citation/Citation.test.tsx +121 -0
  355. package/src/Citation/Citation.tsx +15 -3
  356. package/src/CodeBlock/CodeBlock.test.tsx +115 -0
  357. package/src/CodeBlock/CodeBlock.tsx +63 -33
  358. package/src/CommandPalette/CommandPalette.test.tsx +47 -1
  359. package/src/CommandPalette/CommandPalette.tsx +9 -21
  360. package/src/ContextMenu/ContextMenu.doc.mjs +4 -4
  361. package/src/ContextMenu/ContextMenu.test.tsx +155 -1
  362. package/src/ContextMenu/ContextMenu.tsx +113 -16
  363. package/src/DateInput/DateInput.doc.mjs +286 -56
  364. package/src/DateInput/DateInput.test.tsx +248 -2
  365. package/src/DateInput/DateInput.tsx +196 -100
  366. package/src/DateRangeInput/DateRangeInput.doc.mjs +251 -46
  367. package/src/DateRangeInput/DateRangeInput.test.tsx +182 -20
  368. package/src/DateRangeInput/DateRangeInput.tsx +63 -5
  369. package/src/DateTimeInput/DateTimeInput.doc.mjs +368 -67
  370. package/src/DateTimeInput/DateTimeInput.test.tsx +279 -11
  371. package/src/DateTimeInput/DateTimeInput.tsx +125 -17
  372. package/src/Dialog/Dialog.doc.mjs +2 -2
  373. package/src/Dialog/Dialog.tsx +12 -0
  374. package/src/DropdownMenu/DropdownMenu.doc.mjs +1 -8
  375. package/src/DropdownMenu/DropdownMenu.test.tsx +56 -42
  376. package/src/DropdownMenu/DropdownMenu.tsx +52 -18
  377. package/src/Field/Field.doc.mjs +59 -18
  378. package/src/Field/Field.test.tsx +32 -0
  379. package/src/Field/Field.tsx +23 -5
  380. package/src/Field/FieldLabel.tsx +55 -17
  381. package/src/FileInput/FileInput.doc.mjs +17 -0
  382. package/src/FileInput/FileInput.test.tsx +283 -49
  383. package/src/FileInput/FileInput.tsx +76 -4
  384. package/src/Grid/Grid.doc.mjs +17 -4
  385. package/src/Grid/Grid.test.tsx +43 -30
  386. package/src/Grid/Grid.tsx +38 -3
  387. package/src/Icon/Icon.doc.mjs +3 -3
  388. package/src/InputGroup/InputGroup.doc.mjs +111 -23
  389. package/src/InputGroup/InputGroup.test.tsx +257 -60
  390. package/src/InputGroup/InputGroup.tsx +23 -5
  391. package/src/InputGroup/InputGroupContext.ts +9 -4
  392. package/src/InputGroup/groupStyles.ts +7 -1
  393. package/src/InputGroup/index.ts +1 -0
  394. package/src/Kbd/Kbd.test.tsx +22 -4
  395. package/src/Kbd/Kbd.tsx +35 -3
  396. package/src/Layer/layerAnimations.stylex.ts +20 -4
  397. package/src/Layer/useLayer.doc.mjs +5 -0
  398. package/src/Layer/useLayer.test.tsx +111 -0
  399. package/src/Layer/useLayer.tsx +90 -9
  400. package/src/Layout/Layout.tsx +8 -16
  401. package/src/Layout/LayoutFooter.doc.mjs +4 -4
  402. package/src/Layout/LayoutFooter.tsx +3 -3
  403. package/src/Layout/LayoutHeader.doc.mjs +4 -4
  404. package/src/Layout/LayoutHeader.tsx +3 -3
  405. package/src/Layout/LayoutPanel.tsx +3 -3
  406. package/src/Layout/padding.stylex.ts +52 -0
  407. package/src/Lightbox/Lightbox.doc.mjs +9 -0
  408. package/src/Lightbox/Lightbox.test.tsx +53 -5
  409. package/src/Lightbox/Lightbox.tsx +9 -4
  410. package/src/Link/Link.doc.mjs +14 -0
  411. package/src/Link/Link.test.tsx +50 -0
  412. package/src/Link/Link.tsx +39 -4
  413. package/src/Markdown/Markdown.test.tsx +11 -0
  414. package/src/Markdown/Markdown.tsx +7 -0
  415. package/src/MetadataList/MetadataListItem.doc.mjs +4 -0
  416. package/src/MoreMenu/MoreMenu.doc.mjs +2 -17
  417. package/src/MoreMenu/MoreMenu.tsx +0 -9
  418. package/src/MultiSelector/MultiSelector.doc.mjs +30 -0
  419. package/src/MultiSelector/MultiSelector.test.tsx +375 -8
  420. package/src/MultiSelector/MultiSelector.tsx +194 -48
  421. package/src/MultiSelector/hooks.ts +27 -0
  422. package/src/NavMenu/NavHeadingMenu.test.tsx +77 -1
  423. package/src/NavMenu/NavHeadingMenu.tsx +50 -3
  424. package/src/NumberInput/NumberInput.doc.mjs +17 -0
  425. package/src/NumberInput/NumberInput.test.tsx +242 -40
  426. package/src/NumberInput/NumberInput.tsx +89 -11
  427. package/src/Pagination/Pagination.doc.mjs +3 -3
  428. package/src/Pagination/Pagination.test.tsx +117 -5
  429. package/src/Pagination/Pagination.tsx +21 -1
  430. package/src/Popover/Popover.doc.mjs +26 -0
  431. package/src/Popover/Popover.test.tsx +121 -0
  432. package/src/Popover/Popover.tsx +23 -2
  433. package/src/Popover/usePopover.doc.mjs +9 -3
  434. package/src/Popover/usePopover.tsx +70 -12
  435. package/src/PowerSearch/PowerSearch.doc.mjs +15 -0
  436. package/src/PowerSearch/PowerSearch.test.tsx +97 -1
  437. package/src/PowerSearch/PowerSearch.tsx +16 -3
  438. package/src/ProgressBar/ProgressBar.test.tsx +34 -26
  439. package/src/ProgressBar/ProgressBar.tsx +3 -4
  440. package/src/RadioList/RadioList.doc.mjs +9 -0
  441. package/src/RadioList/RadioList.test.tsx +252 -16
  442. package/src/RadioList/RadioList.tsx +166 -10
  443. package/src/RadioList/RadioListItem.tsx +14 -2
  444. package/src/SegmentedControl/SegmentedControl.doc.mjs +9 -0
  445. package/src/SegmentedControl/SegmentedControl.test.tsx +214 -42
  446. package/src/SegmentedControl/SegmentedControl.tsx +102 -54
  447. package/src/SegmentedControl/SegmentedControlContext.ts +7 -0
  448. package/src/SegmentedControl/SegmentedControlItem.tsx +14 -1
  449. package/src/SelectableCard/SelectableCard.doc.mjs +1 -1
  450. package/src/Selector/Selector.doc.mjs +26 -0
  451. package/src/Selector/Selector.test.tsx +387 -15
  452. package/src/Selector/Selector.tsx +175 -67
  453. package/src/Selector/hooks.ts +23 -0
  454. package/src/SideNav/SideNav.test.tsx +48 -0
  455. package/src/SideNav/SideNavHeading.tsx +8 -2
  456. package/src/SideNav/SideNavItem.tsx +2 -4
  457. package/src/Skeleton/Skeleton.test.tsx +25 -0
  458. package/src/Skeleton/Skeleton.tsx +11 -1
  459. package/src/Slider/Slider.doc.mjs +15 -0
  460. package/src/Slider/Slider.test.tsx +155 -2
  461. package/src/Slider/Slider.tsx +53 -5
  462. package/src/Spinner/Spinner.tsx +7 -1
  463. package/src/Stack/Stack.doc.mjs +77 -0
  464. package/src/Stack/Stack.test.tsx +55 -5
  465. package/src/Stack/Stack.tsx +73 -24
  466. package/src/Stack/StackItem.test.tsx +23 -0
  467. package/src/Stack/StackItem.tsx +20 -23
  468. package/src/Switch/Switch.doc.mjs +15 -0
  469. package/src/Switch/Switch.test.tsx +162 -27
  470. package/src/Switch/Switch.tsx +61 -21
  471. package/src/TabList/Tab.tsx +6 -0
  472. package/src/TabList/TabList.doc.mjs +6 -0
  473. package/src/TabList/TabList.test.tsx +251 -0
  474. package/src/TabList/TabList.tsx +103 -12
  475. package/src/TabList/TabListContext.ts +7 -3
  476. package/src/TabList/TabMenu.tsx +6 -3
  477. package/src/Table/BaseTable.tsx +21 -17
  478. package/src/Table/Table.doc.mjs +2 -0
  479. package/src/Table/Table.test.tsx +10 -0
  480. package/src/Table/Table.tsx +8 -0
  481. package/src/Table/TableCell.tsx +25 -23
  482. package/src/Table/TableHeaderCell.tsx +28 -29
  483. package/src/Table/index.ts +3 -2
  484. package/src/Table/plugins/pagination/paginateData.ts +10 -3
  485. package/src/Table/plugins/pagination/useTablePagination.test.tsx +16 -0
  486. package/src/Table/plugins/pagination/useTablePagination.tsx +8 -1
  487. package/src/Table/plugins/sortable/useTableSortable.test.tsx +91 -2
  488. package/src/Table/plugins/sortable/useTableSortable.tsx +53 -1
  489. package/src/Table/tableContextMenu.test.tsx +157 -0
  490. package/src/Table/tableContextMenu.tsx +142 -0
  491. package/src/Table/types.ts +74 -0
  492. package/src/Table/useTableColumnResize.doc.mjs +3 -3
  493. package/src/Table/useTableStickyColumns.doc.mjs +2 -2
  494. package/src/Text/Text.doc.mjs +18 -18
  495. package/src/TextArea/TextArea.doc.mjs +17 -0
  496. package/src/TextArea/TextArea.test.tsx +167 -35
  497. package/src/TextArea/TextArea.tsx +62 -15
  498. package/src/TextInput/TextInput.doc.mjs +17 -0
  499. package/src/TextInput/TextInput.test.tsx +168 -34
  500. package/src/TextInput/TextInput.tsx +77 -9
  501. package/src/Thumbnail/Thumbnail.doc.mjs +4 -4
  502. package/src/TimeInput/TimeInput.doc.mjs +340 -60
  503. package/src/TimeInput/TimeInput.test.tsx +255 -7
  504. package/src/TimeInput/TimeInput.tsx +176 -70
  505. package/src/Timestamp/Timestamp.doc.mjs +19 -19
  506. package/src/Toast/Toast.tsx +14 -0
  507. package/src/Toast/ToastViewport.test.tsx +214 -0
  508. package/src/Toast/ToastViewport.tsx +167 -21
  509. package/src/Tokenizer/Tokenizer.doc.mjs +29 -14
  510. package/src/Tokenizer/Tokenizer.test.tsx +105 -1
  511. package/src/Tokenizer/Tokenizer.tsx +37 -1
  512. package/src/Toolbar/Toolbar.test.tsx +109 -6
  513. package/src/Toolbar/Toolbar.tsx +59 -5
  514. package/src/Tooltip/Tooltip.test.tsx +68 -0
  515. package/src/Tooltip/useTooltip.tsx +55 -8
  516. package/src/TopNav/TopNav.test.tsx +50 -0
  517. package/src/TopNav/TopNavHeading.tsx +17 -1
  518. package/src/TreeList/TreeList.test.tsx +295 -6
  519. package/src/TreeList/TreeList.tsx +73 -1
  520. package/src/TreeList/TreeListItem.tsx +51 -5
  521. package/src/TreeList/treeListItem.markers.stylex.ts +20 -0
  522. package/src/Typeahead/BaseTypeahead.tsx +93 -9
  523. package/src/Typeahead/Typeahead.doc.mjs +141 -30
  524. package/src/Typeahead/Typeahead.test.tsx +331 -1
  525. package/src/Typeahead/Typeahead.tsx +105 -33
  526. package/src/VisuallyHidden/VisuallyHidden.doc.mjs +80 -0
  527. package/src/VisuallyHidden/VisuallyHidden.test.tsx +76 -0
  528. package/src/VisuallyHidden/VisuallyHidden.tsx +113 -0
  529. package/src/VisuallyHidden/index.ts +15 -0
  530. package/src/docs-script.test.mjs +50 -0
  531. package/src/hooks/focusableSelector.ts +25 -0
  532. package/src/hooks/index.ts +28 -2
  533. package/src/hooks/useAnnounce.test.tsx +101 -0
  534. package/src/hooks/useAnnounce.ts +168 -0
  535. package/src/hooks/useClickableContainer.ts +16 -4
  536. package/src/hooks/useEntryAnimation.ts +16 -4
  537. package/src/hooks/useFocusTrap.test.tsx +132 -0
  538. package/src/hooks/useFocusTrap.ts +98 -4
  539. package/src/hooks/useGridFocus.doc.mjs +40 -2
  540. package/src/hooks/useGridFocus.test.tsx +132 -0
  541. package/src/hooks/useGridFocus.ts +343 -67
  542. package/src/hooks/useKeyboardHint.doc.mjs +101 -0
  543. package/src/hooks/useKeyboardHint.test.tsx +70 -0
  544. package/src/hooks/useKeyboardHint.tsx +332 -0
  545. package/src/hooks/useListFocus.doc.mjs +50 -10
  546. package/src/hooks/useListFocus.test.tsx +375 -0
  547. package/src/hooks/useListFocus.ts +395 -57
  548. package/src/hooks/useLongPress.test.tsx +134 -0
  549. package/src/hooks/useLongPress.ts +119 -0
  550. package/src/hooks/useTreeFocus.doc.mjs +140 -0
  551. package/src/hooks/useTreeFocus.test.tsx +214 -0
  552. package/src/hooks/useTreeFocus.ts +559 -0
  553. package/src/hooks/useTypeahead.test.tsx +109 -0
  554. package/src/hooks/useTypeahead.ts +183 -0
  555. package/src/index.ts +1 -0
  556. package/src/theme/derivedVarRegistry.test.ts +10 -5
  557. package/src/utils/dateTypes.test.ts +40 -0
  558. package/src/utils/dateTypes.ts +40 -0
  559. package/src/utils/index.ts +9 -0
  560. package/src/utils/inputAria.test.ts +39 -0
  561. package/src/utils/inputAria.ts +58 -0
  562. package/src/utils/timeParser.test.ts +10 -0
  563. package/src/utils/timeParser.ts +5 -2
  564. package/dist/Calendar/hooks/useCalendarRovingTabindex.d.ts +0 -57
  565. package/dist/Calendar/hooks/useCalendarRovingTabindex.d.ts.map +0 -1
  566. package/dist/Calendar/hooks/useCalendarRovingTabindex.js +0 -96
  567. package/src/Calendar/hooks/useCalendarRovingTabindex.ts +0 -118
@@ -9,12 +9,39 @@
9
9
  * SYNC: When RadioList.tsx or RadioListItem.tsx changes, update tests to match new behavior
10
10
  */
11
11
 
12
- import {describe, it, expect, vi} from 'vitest';
13
- import {render, screen} from '@testing-library/react';
12
+ import {describe, it, expect, vi, beforeEach} from 'vitest';
13
+ import {render, screen, fireEvent, waitFor} from '@testing-library/react';
14
14
  import userEvent from '@testing-library/user-event';
15
15
  import {RadioList} from './RadioList';
16
16
  import {RadioListItem} from './RadioListItem';
17
17
 
18
+ // Mock showPopover/hidePopover (not implemented in jsdom) so the tooltip layer
19
+ // reflects its open state via a `popover-open` attribute the tests can assert.
20
+ beforeEach(() => {
21
+ HTMLElement.prototype.showPopover = vi.fn(function (this: HTMLElement) {
22
+ this.setAttribute('popover-open', '');
23
+ const event = new Event('toggle', {bubbles: false});
24
+ Object.defineProperty(event, 'newState', {value: 'open'});
25
+ this.dispatchEvent(event);
26
+ });
27
+ HTMLElement.prototype.hidePopover = vi.fn(function (this: HTMLElement) {
28
+ this.removeAttribute('popover-open');
29
+ const event = new Event('toggle', {bubbles: false});
30
+ Object.defineProperty(event, 'newState', {value: 'closed'});
31
+ this.dispatchEvent(event);
32
+ });
33
+ const originalMatches = HTMLElement.prototype.matches;
34
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
+ (HTMLElement.prototype as any).matches = function (
36
+ selector: string,
37
+ ): boolean {
38
+ if (selector === ':popover-open') {
39
+ return this.hasAttribute('popover-open');
40
+ }
41
+ return originalMatches.call(this, selector);
42
+ };
43
+ });
44
+
18
45
  describe('RadioList', () => {
19
46
  it('renders with label', () => {
20
47
  render(
@@ -104,11 +131,7 @@ describe('RadioList', () => {
104
131
  const user = userEvent.setup();
105
132
  const handleChange = vi.fn();
106
133
  render(
107
- <RadioList
108
- label="Preference"
109
- value=""
110
- onChange={handleChange}
111
- isDisabled>
134
+ <RadioList label="Preference" value="" onChange={handleChange} isDisabled>
112
135
  <RadioListItem label="Option A" value="a" />
113
136
  </RadioList>,
114
137
  );
@@ -257,11 +280,7 @@ describe('RadioList', () => {
257
280
  it('supports data-testid on RadioListItem', () => {
258
281
  render(
259
282
  <RadioList label="Preference" value="" onChange={() => {}}>
260
- <RadioListItem
261
- label="Option A"
262
- value="a"
263
- data-testid="my-radio-item"
264
- />
283
+ <RadioListItem label="Option A" value="a" data-testid="my-radio-item" />
265
284
  </RadioList>,
266
285
  );
267
286
  expect(screen.getByTestId('my-radio-item')).toBeInTheDocument();
@@ -279,11 +298,32 @@ describe('RadioList', () => {
279
298
  );
280
299
  const label = screen.getByText('Hidden label');
281
300
  expect(label).toBeInTheDocument();
282
- // The radiogroup should still be labeled
283
- expect(screen.getByRole('radiogroup')).toHaveAttribute(
284
- 'aria-label',
285
- 'Hidden label',
301
+ // The radiogroup is named by the label element via aria-labelledby (not a
302
+ // duplicated aria-label), so its accessible name is still "Hidden label".
303
+ expect(
304
+ screen.getByRole('radiogroup', {name: 'Hidden label'}),
305
+ ).toBeInTheDocument();
306
+ expect(screen.getByRole('radiogroup')).toHaveAttribute('aria-labelledby');
307
+ expect(screen.getByRole('radiogroup')).not.toHaveAttribute('aria-label');
308
+ });
309
+
310
+ it('renders the group label as a span, not a label element (forms-14)', () => {
311
+ render(
312
+ <RadioList label="Plan" value="" onChange={() => {}}>
313
+ <RadioListItem label="Free" value="free" />
314
+ <RadioListItem label="Pro" value="pro" />
315
+ </RadioList>,
286
316
  );
317
+ // A radiogroup's accessible name must not come from a literal `<label>`
318
+ // element — a `<label>` names a single control and can't be associated
319
+ // with a group. It is rendered as a `<span>` and referenced via
320
+ // aria-labelledby (with no orphaned htmlFor).
321
+ const labelEl = screen.getByText('Plan');
322
+ expect(labelEl.tagName).toBe('SPAN');
323
+ expect(labelEl.closest('label')).toBeNull();
324
+ expect(labelEl).not.toHaveAttribute('for');
325
+ const group = screen.getByRole('radiogroup', {name: 'Plan'});
326
+ expect(group.getAttribute('aria-labelledby')).toBe(labelEl.id);
287
327
  });
288
328
 
289
329
  it('renders description on items', () => {
@@ -340,4 +380,200 @@ describe('RadioList', () => {
340
380
  'true',
341
381
  );
342
382
  });
383
+
384
+ describe('focus management (no-selection tab stop)', () => {
385
+ it('keeps focus on the selected radio when a value is selected', () => {
386
+ render(
387
+ <RadioList label="Preference" value="b" onChange={() => {}}>
388
+ <RadioListItem label="Option A" value="a" />
389
+ <RadioListItem label="Option B" value="b" />
390
+ <RadioListItem label="Option C" value="c" />
391
+ </RadioList>,
392
+ );
393
+ const selected = screen.getByLabelText('Option B');
394
+ // A selected value provides a deterministic native tab stop; focusing it
395
+ // must not be redirected elsewhere.
396
+ selected.focus();
397
+ expect(selected).toHaveFocus();
398
+ });
399
+
400
+ it('redirects to the first radio when focus enters an unselected group forward', () => {
401
+ render(
402
+ <>
403
+ <button type="button">before</button>
404
+ <RadioList label="Preference" value="" onChange={() => {}}>
405
+ <RadioListItem label="Option A" value="a" />
406
+ <RadioListItem label="Option B" value="b" />
407
+ <RadioListItem label="Option C" value="c" />
408
+ </RadioList>
409
+ </>,
410
+ );
411
+ const radios = screen.getAllByRole('radio');
412
+ const outside = screen.getByText('before');
413
+ outside.focus();
414
+ // Forward entry: the browser lands on a leading radio; the group keeps the
415
+ // first radio as the deterministic tab stop.
416
+ radios[0].focus();
417
+ expect(radios[0]).toHaveFocus();
418
+
419
+ // Landing on a middle radio from outside is normalized to the first.
420
+ outside.focus();
421
+ radios[1].focus();
422
+ expect(radios[0]).toHaveFocus();
423
+ });
424
+
425
+ it('redirects to the last radio when focus enters an unselected group backward', () => {
426
+ render(
427
+ <>
428
+ <RadioList label="Preference" value="" onChange={() => {}}>
429
+ <RadioListItem label="Option A" value="a" />
430
+ <RadioListItem label="Option B" value="b" />
431
+ <RadioListItem label="Option C" value="c" />
432
+ </RadioList>
433
+ <button type="button">after</button>
434
+ </>,
435
+ );
436
+ const radios = screen.getAllByRole('radio');
437
+ const outside = screen.getByText('after');
438
+ outside.focus();
439
+ // Backward (Shift+Tab) entry: the browser focuses the last radio; the
440
+ // group keeps it as the deterministic tab stop rather than jumping away.
441
+ radios[radios.length - 1].focus();
442
+ expect(radios[radios.length - 1]).toHaveFocus();
443
+ });
444
+
445
+ it('does not hijack focus moving between radios within the group', () => {
446
+ render(
447
+ <RadioList label="Preference" value="" onChange={() => {}}>
448
+ <RadioListItem label="Option A" value="a" />
449
+ <RadioListItem label="Option B" value="b" />
450
+ <RadioListItem label="Option C" value="c" />
451
+ </RadioList>,
452
+ );
453
+ const radios = screen.getAllByRole('radio');
454
+ // Enter the group from outside, then move to a middle radio as arrow-key
455
+ // navigation would. Intra-group movement must not be redirected back.
456
+ radios[0].focus();
457
+ radios[1].focus();
458
+ expect(radios[1]).toHaveFocus();
459
+ radios[2].focus();
460
+ expect(radios[2]).toHaveFocus();
461
+ });
462
+
463
+ it('skips disabled radios when choosing the deterministic tab stop', () => {
464
+ render(
465
+ <>
466
+ <button type="button">before</button>
467
+ <RadioList label="Preference" value="" onChange={() => {}}>
468
+ <RadioListItem label="Option A" value="a" isDisabled />
469
+ <RadioListItem label="Option B" value="b" />
470
+ <RadioListItem label="Option C" value="c" />
471
+ <RadioListItem label="Option D" value="d" />
472
+ </RadioList>
473
+ </>,
474
+ );
475
+ const outside = screen.getByText('before');
476
+ const optionB = screen.getByLabelText('Option B');
477
+ const optionC = screen.getByLabelText('Option C');
478
+ // A middle enabled radio (C) entered from outside is normalized to the
479
+ // first *enabled* radio (B) — the disabled Option A is skipped.
480
+ outside.focus();
481
+ optionC.focus();
482
+ expect(optionB).toHaveFocus();
483
+ });
484
+ });
485
+
486
+ describe('disabledMessage', () => {
487
+ const h = {hidden: true} as const;
488
+
489
+ function renderGroup(props?: {onChange?: (v: string) => void}) {
490
+ return render(
491
+ <RadioList
492
+ label="Plan"
493
+ value="free"
494
+ onChange={props?.onChange ?? (() => {})}
495
+ isDisabled
496
+ disabledMessage="Upgrade your account to change plans">
497
+ <RadioListItem label="Free" value="free" />
498
+ <RadioListItem label="Pro" value="pro" />
499
+ </RadioList>,
500
+ );
501
+ }
502
+
503
+ it('shows the reason tooltip on hover when the group is disabled with a reason', async () => {
504
+ renderGroup();
505
+ const tooltip = screen.getByRole('tooltip', h);
506
+ expect(tooltip).toHaveTextContent('Upgrade your account to change plans');
507
+ const group = screen.getByRole('radiogroup');
508
+ fireEvent.mouseEnter(group);
509
+ await waitFor(() => expect(tooltip).toHaveAttribute('popover-open'));
510
+ fireEvent.mouseLeave(group);
511
+ await waitFor(() => expect(tooltip).not.toHaveAttribute('popover-open'));
512
+ });
513
+
514
+ it('shows the reason tooltip on keyboard focus', async () => {
515
+ const user = userEvent.setup();
516
+ renderGroup();
517
+ const tooltip = screen.getByRole('tooltip', h);
518
+ await user.tab();
519
+ await waitFor(() => expect(tooltip).toHaveAttribute('popover-open'));
520
+ });
521
+
522
+ it('does not render a tooltip when not disabled', () => {
523
+ render(
524
+ <RadioList
525
+ label="Plan"
526
+ value="free"
527
+ onChange={() => {}}
528
+ disabledMessage="Upgrade your account to change plans">
529
+ <RadioListItem label="Free" value="free" />
530
+ </RadioList>,
531
+ );
532
+ expect(screen.queryByRole('tooltip', h)).not.toBeInTheDocument();
533
+ });
534
+
535
+ it('does not render a tooltip when disabled without a reason', () => {
536
+ render(
537
+ <RadioList label="Plan" value="free" onChange={() => {}} isDisabled>
538
+ <RadioListItem label="Free" value="free" />
539
+ </RadioList>,
540
+ );
541
+ expect(screen.queryByRole('tooltip', h)).not.toBeInTheDocument();
542
+ });
543
+
544
+ it('keeps radios focusable via aria-disabled when a reason is provided', () => {
545
+ renderGroup();
546
+ for (const radio of screen.getAllByRole('radio', h)) {
547
+ expect(radio).not.toBeDisabled();
548
+ expect(radio).toHaveAttribute('aria-disabled', 'true');
549
+ }
550
+ });
551
+
552
+ it('links the reason tooltip from the group via aria-describedby', () => {
553
+ renderGroup();
554
+ const group = screen.getByRole('radiogroup');
555
+ const tooltip = screen.getByRole('tooltip', h);
556
+ expect(group.getAttribute('aria-describedby')).toContain(tooltip.id);
557
+ });
558
+
559
+ it('blocks selection while focusable-disabled', () => {
560
+ const onChange = vi.fn();
561
+ renderGroup({onChange});
562
+ const pro = screen.getByRole('radio', {name: 'Pro', hidden: true});
563
+ fireEvent.click(pro);
564
+ expect(onChange).not.toHaveBeenCalled();
565
+ });
566
+
567
+ it('keeps radios natively disabled when disabled without a reason', () => {
568
+ render(
569
+ <RadioList label="Plan" value="free" onChange={() => {}} isDisabled>
570
+ <RadioListItem label="Free" value="free" />
571
+ <RadioListItem label="Pro" value="pro" />
572
+ </RadioList>,
573
+ );
574
+ for (const radio of screen.getAllByRole('radio', h)) {
575
+ expect(radio).toBeDisabled();
576
+ }
577
+ });
578
+ });
343
579
  });
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @file RadioList.tsx
7
- * @input Uses React useId, createContext, ReactNode, Field, InputStatus
7
+ * @input Uses React useId, useCallback, useRef, createContext, ReactNode, Field, InputStatus
8
8
  * @output Exports RadioList component, RadioListProps, RadioListContext
9
9
  * @position Core implementation; consumed by index.ts, tested by RadioList.test.tsx
10
10
  *
@@ -16,11 +16,19 @@
16
16
  * - /packages/cli/templates/blocks/components/RadioList/ (showcase blocks)
17
17
  */
18
18
 
19
- import React, {createContext, useId, useMemo, type ReactNode} from 'react';
19
+ import React, {
20
+ createContext,
21
+ useCallback,
22
+ useId,
23
+ useMemo,
24
+ useRef,
25
+ type ReactNode,
26
+ } from 'react';
20
27
  import * as stylex from '@stylexjs/stylex';
21
28
  import {spacingVars} from '../theme/tokens.stylex';
22
29
  import {Field} from '../Field/Field';
23
30
  import type {InputStatus} from '../Field/types';
31
+ import {useTooltip} from '../Tooltip';
24
32
  import {mergeProps} from '../utils';
25
33
  import type {BaseProps} from '../BaseProps';
26
34
  import type {SizeValue} from '../utils/types';
@@ -36,13 +44,21 @@ export interface RadioListContextValue {
36
44
  value: string;
37
45
  onChange: (value: string) => void;
38
46
  isDisabled: boolean;
47
+ /**
48
+ * True when the whole group is disabled *and* a `disabledMessage` is set. In
49
+ * that mode radios stay focusable via `aria-disabled` (instead of the native
50
+ * `disabled` attribute) so the disabled-reason tooltip is keyboard- and
51
+ * AT-discoverable; selection is still blocked in the item's onChange guard.
52
+ */
53
+ hasDisabledMessage: boolean;
39
54
  isRequired: boolean;
40
55
  size: RadioListSize;
41
56
  status?: InputStatus;
42
57
  }
43
58
 
44
- export const RadioListContext =
45
- createContext<RadioListContextValue | null>(null);
59
+ export const RadioListContext = createContext<RadioListContextValue | null>(
60
+ null,
61
+ );
46
62
  RadioListContext.displayName = 'RadioListContext';
47
63
 
48
64
  const styles = stylex.create({
@@ -95,6 +111,17 @@ export interface RadioListProps extends Omit<
95
111
  * @default false
96
112
  */
97
113
  isDisabled?: boolean;
114
+ /**
115
+ * Explains why the radio group is disabled. Applies to the whole-group
116
+ * disabled state (`isDisabled`), not individual items. When set together with
117
+ * `isDisabled`, the group shows a tooltip with this text on hover and keyboard
118
+ * focus, and its radios stay focusable (via `aria-disabled`) so the reason is
119
+ * discoverable by keyboard and assistive technology. Selection stays blocked.
120
+ *
121
+ * Use this instead of wrapping a disabled group in `Tooltip` — disabled
122
+ * controls don't emit the pointer events an external tooltip needs.
123
+ */
124
+ disabledMessage?: string;
98
125
  /**
99
126
  * Whether the radio group is required.
100
127
  * @default false
@@ -161,6 +188,7 @@ export function RadioList({
161
188
  onChange,
162
189
  orientation = 'vertical',
163
190
  isDisabled = false,
191
+ disabledMessage,
164
192
  isRequired = false,
165
193
  isOptional = false,
166
194
  size = 'md',
@@ -175,12 +203,129 @@ export function RadioList({
175
203
  }: RadioListProps) {
176
204
  const name = useId();
177
205
  const inputID = useId();
206
+ const labelID = useId();
178
207
  const descriptionID = useId();
179
208
  const statusMessageID = useId();
180
209
 
210
+ const groupRef = useRef<HTMLDivElement>(null);
211
+
212
+ // Disabled-reason tooltip. Applies to the whole-group disabled state. Disabled
213
+ // controls swallow pointer events, so the tooltip listeners attach to the
214
+ // radiogroup container and the radios stay perceivable via aria-disabled
215
+ // instead of the disabled attribute. Selection is blocked in the item's
216
+ // onChange guard.
217
+ const showsDisabledMessage = isDisabled && !!disabledMessage;
218
+ const disabledMessageTooltip = useTooltip({
219
+ placement: 'above',
220
+ // The radiogroup container is not naturally focusable; focusin bubbles up
221
+ // from the radios, so always attach focus listeners.
222
+ focusTrigger: 'always',
223
+ isEnabled: showsDisabledMessage,
224
+ });
225
+
181
226
  const contextValue = useMemo<RadioListContextValue>(
182
- () => ({name, value, onChange, isDisabled, isRequired, size, status}),
183
- [name, value, onChange, isDisabled, isRequired, size, status],
227
+ () => ({
228
+ name,
229
+ value,
230
+ onChange,
231
+ isDisabled,
232
+ hasDisabledMessage: showsDisabledMessage,
233
+ isRequired,
234
+ size,
235
+ status,
236
+ }),
237
+ [
238
+ name,
239
+ value,
240
+ onChange,
241
+ isDisabled,
242
+ showsDisabledMessage,
243
+ isRequired,
244
+ size,
245
+ status,
246
+ ],
247
+ );
248
+
249
+ /**
250
+ * Make the tab stop deterministic when a radio group has no selected value.
251
+ *
252
+ * Native `<input type="radio">` groups (same `name`) implement roving
253
+ * tabindex for free: when a value is selected, that radio is the single tab
254
+ * stop and receives focus, so no correction is needed there. But the ARIA
255
+ * radio-group pattern (APG) also requires a deterministic tab stop when the
256
+ * group has *no* selection — and browsers disagree here. Chrome, when
257
+ * Shift+Tab moves focus backward into an unselected group, lands on the
258
+ * *last* radio; forward Tab lands on the *first*. To keep the entry point
259
+ * predictable we redirect focus:
260
+ * - forward entry → first enabled radio (APG default)
261
+ * - backward entry → last enabled radio (matches Chrome's backward tab)
262
+ *
263
+ * The redirect only runs when focus arrives from *outside* the group's own
264
+ * radios (guarded via `relatedTarget` containment). Moving between radios
265
+ * inside the group — arrow keys, clicks, programmatic focus of a sibling —
266
+ * is never hijacked.
267
+ */
268
+ const handleFocus = useCallback(
269
+ (e: React.FocusEvent<HTMLDivElement>) => {
270
+ // Only correct the no-selection case; a selected value already provides
271
+ // a deterministic native tab stop.
272
+ if (value !== '') {
273
+ return;
274
+ }
275
+
276
+ const group = groupRef.current;
277
+ if (!group) {
278
+ return;
279
+ }
280
+
281
+ // Ignore focus moving *within* the group. `relatedTarget` is the element
282
+ // losing focus; if it was inside the group, this is intra-group movement
283
+ // (arrow keys, clicking a sibling) and must not be hijacked. Some browsers
284
+ // report a null `relatedTarget`; fall back to checking whether the group
285
+ // already contained the active element before this focus landed.
286
+ const relatedTarget = e.relatedTarget as Node | null;
287
+ if (relatedTarget) {
288
+ if (group.contains(relatedTarget)) {
289
+ return;
290
+ }
291
+ } else if (
292
+ document.activeElement &&
293
+ document.activeElement !== e.target &&
294
+ group.contains(document.activeElement)
295
+ ) {
296
+ return;
297
+ }
298
+
299
+ const radios = Array.from(
300
+ group.querySelectorAll<HTMLInputElement>(
301
+ 'input[type="radio"]:not([disabled])',
302
+ ),
303
+ );
304
+ if (radios.length === 0) {
305
+ return;
306
+ }
307
+
308
+ const target = e.target as HTMLElement;
309
+ const targetIndex = radios.findIndex(radio => radio === target);
310
+ // Only correct when the focus target is one of our enabled radios (not a
311
+ // nested control such as end-content).
312
+ if (targetIndex === -1) {
313
+ return;
314
+ }
315
+
316
+ // Infer tab direction from which end the browser chose. On a backward
317
+ // (Shift+Tab) entry into an unselected group, browsers focus the *last*
318
+ // radio; keep it as the deterministic backward tab stop. Any other entry
319
+ // (forward Tab, or a browser that lands mid-group) is normalized to the
320
+ // *first* enabled radio, the APG default.
321
+ const isBackwardEntry = targetIndex === radios.length - 1;
322
+ const intended = isBackwardEntry ? radios[radios.length - 1] : radios[0];
323
+
324
+ if (target !== intended) {
325
+ intended.focus();
326
+ }
327
+ },
328
+ [value],
184
329
  );
185
330
 
186
331
  return (
@@ -191,6 +336,8 @@ export function RadioList({
191
336
  isLabelHidden={isLabelHidden}
192
337
  description={description}
193
338
  inputID={inputID}
339
+ labelID={labelID}
340
+ isGroupLabel
194
341
  descriptionID={description ? descriptionID : undefined}
195
342
  isOptional={isOptional}
196
343
  isRequired={isRequired}
@@ -211,12 +358,21 @@ export function RadioList({
211
358
  className={className}
212
359
  style={style}>
213
360
  <div
361
+ ref={el => {
362
+ groupRef.current = el;
363
+ // Anchor + hover/focus listeners for the disabled-message tooltip.
364
+ // Handlers are gated internally by isEnabled, so attaching
365
+ // unconditionally is safe.
366
+ disabledMessageTooltip.ref(el);
367
+ }}
214
368
  role="radiogroup"
215
- aria-label={label}
369
+ aria-labelledby={labelID}
370
+ onFocus={handleFocus}
216
371
  aria-describedby={
217
372
  [
218
373
  description ? descriptionID : null,
219
374
  status?.message ? statusMessageID : null,
375
+ showsDisabledMessage ? disabledMessageTooltip.describedBy : null,
220
376
  ]
221
377
  .filter(Boolean)
222
378
  .join(' ') || undefined
@@ -230,10 +386,10 @@ export function RadioList({
230
386
  orientation === 'vertical' ? styles.vertical : styles.horizontal,
231
387
  ),
232
388
  )}>
233
- <RadioListContext value={contextValue}>
234
- {children}
235
- </RadioListContext>
389
+ <RadioListContext value={contextValue}>{children}</RadioListContext>
236
390
  </div>
391
+ {showsDisabledMessage &&
392
+ disabledMessageTooltip.renderTooltip(disabledMessage)}
237
393
  </Field>
238
394
  );
239
395
  }
@@ -230,6 +230,12 @@ export function RadioListItem({
230
230
  const id = useId();
231
231
  const descriptionID = useId();
232
232
  const isDisabled = context.isDisabled || isItemDisabled;
233
+ // When the whole group is disabled with a disabledMessage, radios stay
234
+ // focusable via aria-disabled (instead of native `disabled`) so the group's
235
+ // reason tooltip is keyboard-discoverable. Per-item disabling is unaffected
236
+ // and always uses the native disabled attribute.
237
+ const keepsFocusableForMessage =
238
+ context.hasDisabledMessage && !isItemDisabled;
233
239
  const isChecked = context.value === value;
234
240
  const size = context.size;
235
241
 
@@ -246,9 +252,15 @@ export function RadioListItem({
246
252
  name={context.name}
247
253
  value={value}
248
254
  checked={isChecked}
249
- disabled={isDisabled}
255
+ disabled={isDisabled && !keepsFocusableForMessage}
256
+ aria-disabled={keepsFocusableForMessage ? 'true' : undefined}
250
257
  required={context.isRequired}
251
- onChange={() => context.onChange(value)}
258
+ onChange={() => {
259
+ if (isDisabled) {
260
+ return;
261
+ }
262
+ context.onChange(value);
263
+ }}
252
264
  aria-describedby={description ? descriptionID : undefined}
253
265
  {...stylex.props(
254
266
  styles.input,
@@ -65,6 +65,12 @@ export const docs = {
65
65
  description: 'Whether the entire control is disabled.',
66
66
  default: 'false',
67
67
  },
68
+ {
69
+ name: 'disabledMessage',
70
+ type: 'string',
71
+ description:
72
+ 'Explains why the control is disabled. Applies to the whole-group disabled state (isDisabled), not per segment. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the control focusable via aria-disabled (selection stays blocked). Use this instead of wrapping a disabled SegmentedControl in Tooltip — disabled controls swallow the hover events an external Tooltip needs.',
73
+ },
68
74
  {
69
75
  name: 'children',
70
76
  type: 'ReactNode',
@@ -97,6 +103,7 @@ export const docs = {
97
103
  {guidance: true, description: 'Provide a descriptive label for the control to ensure the group is accessible to screen readers.'},
98
104
  {guidance: false, description: 'Use for page-level navigation; use TabList instead. TabList is a navigation component, while SegmentedControl is an input that always has exactly one selected option.'},
99
105
  {guidance: false, description: 'Use for simple on/off states; use ToggleButton instead. ToggleButton can be toggled on or off independently, while SegmentedControl enforces a single selection from a group.'},
106
+ {guidance: false, description: 'Wrap a disabled SegmentedControl in Tooltip to explain why it is disabled; disabled controls swallow the hover events the wrapper needs. Use the disabledMessage prop instead.'},
100
107
  ],
101
108
  },
102
109
  };
@@ -111,6 +118,7 @@ export const docsZh = {
111
118
  {guidance: true, description: 'Provide a descriptive label for the control to ensure the group is accessible to screen readers.'},
112
119
  {guidance: false, description: 'Use for page-level navigation; use TabList instead. TabList is a navigation component, while SegmentedControl is an input that always has exactly one selected option.'},
113
120
  {guidance: false, description: 'Use for simple on/off states; use ToggleButton instead. ToggleButton can be toggled on or off independently, while SegmentedControl enforces a single selection from a group.'},
121
+ {guidance: false, description: 'Wrap a disabled SegmentedControl in Tooltip to explain why it is disabled; disabled controls swallow the hover events the wrapper needs. Use the disabledMessage prop instead.'},
114
122
  ],
115
123
  },
116
124
  };
@@ -125,6 +133,7 @@ export const docsDense = {
125
133
  {guidance: true, description: 'Provide a descriptive label for the control to ensure the group is accessible to screen readers.'},
126
134
  {guidance: false, description: 'Use for page-level navigation; use TabList instead. TabList is a navigation component, while SegmentedControl is an input that always has exactly one selected option.'},
127
135
  {guidance: false, description: 'Use for simple on/off states; use ToggleButton instead. ToggleButton can be toggled on or off independently, while SegmentedControl enforces a single selection from a group.'},
136
+ {guidance: false, description: 'Wrap a disabled SegmentedControl in Tooltip to explain why it is disabled; disabled controls swallow the hover events the wrapper needs. Use the disabledMessage prop instead.'},
128
137
  ],
129
138
  },
130
139
  propDescriptions: {