@astryxdesign/core 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (551) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/dist/AlertDialog/AlertDialog.d.ts +10 -2
  3. package/dist/AlertDialog/AlertDialog.d.ts.map +1 -1
  4. package/dist/AlertDialog/AlertDialog.js +22 -3
  5. package/dist/AppShell/AppShell.d.ts +2 -2
  6. package/dist/AppShell/AppShell.d.ts.map +1 -1
  7. package/dist/AppShell/AppShell.js +40 -9
  8. package/dist/AspectRatio/AspectRatio.d.ts +13 -0
  9. package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
  10. package/dist/AspectRatio/AspectRatio.js +9 -0
  11. package/dist/Avatar/Avatar.d.ts.map +1 -1
  12. package/dist/Avatar/Avatar.js +33 -20
  13. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
  14. package/dist/AvatarGroup/AvatarGroupOverflow.js +2 -3
  15. package/dist/Banner/Banner.d.ts.map +1 -1
  16. package/dist/Banner/Banner.js +15 -14
  17. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  18. package/dist/Breadcrumbs/BreadcrumbItem.js +21 -8
  19. package/dist/Breadcrumbs/index.d.ts +2 -1
  20. package/dist/Breadcrumbs/index.d.ts.map +1 -1
  21. package/dist/Breadcrumbs/index.js +1 -0
  22. package/dist/Button/Button.d.ts.map +1 -1
  23. package/dist/Button/Button.js +11 -15
  24. package/dist/Calendar/Calendar.d.ts.map +1 -1
  25. package/dist/Calendar/Calendar.js +2 -1
  26. package/dist/Calendar/styles.d.ts +0 -2
  27. package/dist/Calendar/styles.d.ts.map +1 -1
  28. package/dist/Calendar/styles.js +0 -2
  29. package/dist/Carousel/Carousel.d.ts.map +1 -1
  30. package/dist/Carousel/Carousel.js +8 -12
  31. package/dist/Chat/ChatDictationButton.d.ts.map +1 -1
  32. package/dist/Chat/ChatDictationButton.js +2 -1
  33. package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
  34. package/dist/Chat/ChatLayoutScrollButton.js +3 -1
  35. package/dist/Chat/ChatSendButton.d.ts.map +1 -1
  36. package/dist/Chat/ChatSendButton.js +4 -3
  37. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  38. package/dist/Chat/ChatToolCalls.js +17 -17
  39. package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
  40. package/dist/Chat/useTriggerMenu.js +2 -6
  41. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  42. package/dist/CheckboxInput/CheckboxInput.js +36 -125
  43. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  44. package/dist/ClickableCard/ClickableCard.js +3 -11
  45. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  46. package/dist/CodeBlock/CodeBlock.js +84 -67
  47. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  48. package/dist/Collapsible/Collapsible.js +35 -15
  49. package/dist/CommandPalette/CommandPaletteItem.d.ts +1 -1
  50. package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
  51. package/dist/CommandPalette/CommandPaletteItem.js +6 -3
  52. package/dist/CommandPalette/CommandPaletteList.js +1 -1
  53. package/dist/ComplexSelector/ComplexSelector.d.ts +1 -1
  54. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  55. package/dist/ComplexSelector/ComplexSelector.js +35 -33
  56. package/dist/ContextMenu/ContextMenu.d.ts +2 -2
  57. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  58. package/dist/ContextMenu/ContextMenu.js +1 -1
  59. package/dist/ContextMenu/index.d.ts +2 -1
  60. package/dist/ContextMenu/index.d.ts.map +1 -1
  61. package/dist/ContextMenu/index.js +1 -0
  62. package/dist/DateInput/DateInput.d.ts +9 -2
  63. package/dist/DateInput/DateInput.d.ts.map +1 -1
  64. package/dist/DateInput/DateInput.js +24 -23
  65. package/dist/DateRangeInput/DateRangeInput.d.ts +9 -2
  66. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  67. package/dist/DateRangeInput/DateRangeInput.js +11 -16
  68. package/dist/DateTimeInput/DateTimeInput.d.ts +9 -1
  69. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  70. package/dist/DateTimeInput/DateTimeInput.js +22 -17
  71. package/dist/Dialog/Dialog.d.ts.map +1 -1
  72. package/dist/Dialog/Dialog.js +4 -4
  73. package/dist/DropdownMenu/DropdownMenu.d.ts +33 -8
  74. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  75. package/dist/DropdownMenu/DropdownMenu.js +62 -20
  76. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +8 -11
  77. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
  78. package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +32 -31
  79. package/dist/DropdownMenu/DropdownMenuDivider.d.ts +25 -0
  80. package/dist/DropdownMenu/DropdownMenuDivider.d.ts.map +1 -0
  81. package/dist/DropdownMenu/DropdownMenuDivider.js +66 -0
  82. package/dist/DropdownMenu/DropdownMenuItem.d.ts +15 -2
  83. package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
  84. package/dist/DropdownMenu/DropdownMenuItem.js +20 -8
  85. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +5 -5
  86. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +1 -1
  87. package/dist/DropdownMenu/DropdownMenuRadioItem.js +24 -67
  88. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  89. package/dist/DropdownMenu/DropdownMenuSubMenu.js +2 -8
  90. package/dist/DropdownMenu/index.d.ts +2 -1
  91. package/dist/DropdownMenu/index.d.ts.map +1 -1
  92. package/dist/DropdownMenu/index.js +4 -0
  93. package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -1
  94. package/dist/DropdownMenu/renderDropdownItems.js +35 -27
  95. package/dist/EmptyState/EmptyState.d.ts.map +1 -1
  96. package/dist/EmptyState/EmptyState.js +9 -5
  97. package/dist/Field/InputClearButton.d.ts +16 -3
  98. package/dist/Field/InputClearButton.d.ts.map +1 -1
  99. package/dist/Field/InputClearButton.js +15 -4
  100. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  101. package/dist/HoverCard/useHoverCard.js +3 -19
  102. package/dist/Indicator/CheckIndicator.d.ts +25 -0
  103. package/dist/Indicator/CheckIndicator.d.ts.map +1 -0
  104. package/dist/Indicator/CheckIndicator.js +155 -0
  105. package/dist/Indicator/CheckboxIndicator.d.ts +22 -0
  106. package/dist/Indicator/CheckboxIndicator.d.ts.map +1 -0
  107. package/dist/Indicator/CheckboxIndicator.js +173 -0
  108. package/dist/Indicator/RadioIndicator.d.ts +23 -0
  109. package/dist/Indicator/RadioIndicator.d.ts.map +1 -0
  110. package/dist/Indicator/RadioIndicator.js +137 -0
  111. package/dist/Indicator/index.d.ts +24 -0
  112. package/dist/Indicator/index.d.ts.map +1 -0
  113. package/dist/Indicator/index.js +26 -0
  114. package/dist/Indicator/indicator.markers.stylex.d.ts +16 -0
  115. package/dist/Indicator/indicator.markers.stylex.d.ts.map +1 -0
  116. package/dist/Indicator/indicator.markers.stylex.js +21 -0
  117. package/dist/Indicator/indicatorRegistry.d.ts +71 -0
  118. package/dist/Indicator/indicatorRegistry.d.ts.map +1 -0
  119. package/dist/Indicator/indicatorRegistry.js +93 -0
  120. package/dist/Indicator/types.d.ts +167 -0
  121. package/dist/Indicator/types.d.ts.map +1 -0
  122. package/dist/Indicator/types.js +1 -0
  123. package/dist/Indicator/useIndicator.d.ts +18 -0
  124. package/dist/Indicator/useIndicator.d.ts.map +1 -0
  125. package/dist/Indicator/useIndicator.js +30 -0
  126. package/dist/Item/Item.d.ts.map +1 -1
  127. package/dist/Item/Item.js +4 -8
  128. package/dist/Layer/useLayer.d.ts +14 -0
  129. package/dist/Layer/useLayer.d.ts.map +1 -1
  130. package/dist/Layer/useLayer.js +25 -2
  131. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  132. package/dist/Lightbox/Lightbox.js +56 -53
  133. package/dist/Link/Link.d.ts.map +1 -1
  134. package/dist/Link/Link.js +4 -5
  135. package/dist/MobileNav/MobileNav.js +8 -8
  136. package/dist/MoreMenu/MoreMenu.d.ts +14 -1
  137. package/dist/MoreMenu/MoreMenu.d.ts.map +1 -1
  138. package/dist/MoreMenu/MoreMenu.js +4 -0
  139. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  140. package/dist/MultiSelector/MultiSelector.js +78 -75
  141. package/dist/NumberInput/NumberInput.d.ts +27 -1
  142. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  143. package/dist/NumberInput/NumberInput.js +230 -42
  144. package/dist/Outline/Outline.d.ts.map +1 -1
  145. package/dist/Outline/Outline.js +2 -3
  146. package/dist/Pagination/Pagination.d.ts.map +1 -1
  147. package/dist/Pagination/Pagination.js +61 -63
  148. package/dist/Popover/Popover.d.ts.map +1 -1
  149. package/dist/Popover/Popover.js +6 -8
  150. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  151. package/dist/PowerSearch/PowerSearch.js +1 -1
  152. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  153. package/dist/PowerSearch/PowerSearchEditPopover.js +7 -0
  154. package/dist/ProgressBar/ProgressBar.d.ts +11 -4
  155. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  156. package/dist/ProgressBar/ProgressBar.js +96 -6
  157. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  158. package/dist/RadioList/RadioListItem.js +29 -84
  159. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  160. package/dist/Resizable/ResizeHandle.js +2 -3
  161. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  162. package/dist/SegmentedControl/SegmentedControlItem.js +2 -3
  163. package/dist/SelectableCard/SelectableCard.d.ts.map +1 -1
  164. package/dist/SelectableCard/SelectableCard.js +3 -11
  165. package/dist/Selector/Selector.d.ts +2 -1
  166. package/dist/Selector/Selector.d.ts.map +1 -1
  167. package/dist/Selector/Selector.js +164 -94
  168. package/dist/Selector/hooks.d.ts +18 -6
  169. package/dist/Selector/hooks.d.ts.map +1 -1
  170. package/dist/Selector/hooks.js +57 -40
  171. package/dist/SideNav/SideNav.d.ts.map +1 -1
  172. package/dist/SideNav/SideNav.js +1 -1
  173. package/dist/SideNav/SideNavCollapseButton.d.ts +1 -1
  174. package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
  175. package/dist/SideNav/SideNavCollapseButton.js +29 -14
  176. package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
  177. package/dist/SideNav/SideNavHeading.js +56 -20
  178. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  179. package/dist/SideNav/SideNavItem.js +28 -22
  180. package/dist/Stack/stack.stylex.d.ts +3 -3
  181. package/dist/Switch/Switch.js +11 -10
  182. package/dist/TabList/Tab.d.ts.map +1 -1
  183. package/dist/TabList/Tab.js +2 -3
  184. package/dist/TabList/TabMenu.d.ts.map +1 -1
  185. package/dist/TabList/TabMenu.js +41 -19
  186. package/dist/Table/BaseTable.d.ts.map +1 -1
  187. package/dist/Table/BaseTable.js +8 -0
  188. package/dist/Table/index.d.ts +1 -1
  189. package/dist/Table/index.d.ts.map +1 -1
  190. package/dist/Table/index.js +1 -1
  191. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts +1 -1
  192. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
  193. package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +22 -18
  194. package/dist/Table/plugins/rowExpansion/index.d.ts +1 -1
  195. package/dist/Table/plugins/rowExpansion/index.d.ts.map +1 -1
  196. package/dist/Table/plugins/rowExpansion/index.js +1 -1
  197. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts +41 -76
  198. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  199. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +94 -345
  200. package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
  201. package/dist/Table/plugins/sortable/useTableSortable.js +23 -4
  202. package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -1
  203. package/dist/Table/plugins/tree/useTableTreeData.js +29 -33
  204. package/dist/Table/tableContextMenu.d.ts.map +1 -1
  205. package/dist/Table/tableContextMenu.js +2 -1
  206. package/dist/Table/types.d.ts +11 -0
  207. package/dist/Table/types.d.ts.map +1 -1
  208. package/dist/TextArea/TextArea.d.ts +10 -1
  209. package/dist/TextArea/TextArea.d.ts.map +1 -1
  210. package/dist/TextArea/TextArea.js +16 -6
  211. package/dist/TextInput/TextInput.d.ts +10 -1
  212. package/dist/TextInput/TextInput.d.ts.map +1 -1
  213. package/dist/TextInput/TextInput.js +13 -19
  214. package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
  215. package/dist/Thumbnail/Thumbnail.js +32 -26
  216. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  217. package/dist/TimeInput/TimeInput.js +7 -15
  218. package/dist/Timestamp/TimestampHoverCard.d.ts.map +1 -1
  219. package/dist/Timestamp/TimestampHoverCard.js +16 -34
  220. package/dist/Token/Token.d.ts.map +1 -1
  221. package/dist/Token/Token.js +20 -11
  222. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  223. package/dist/Tokenizer/Tokenizer.js +4 -2
  224. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  225. package/dist/Tooltip/useTooltip.js +3 -19
  226. package/dist/TopNav/TopNav.d.ts.map +1 -1
  227. package/dist/TopNav/TopNav.js +6 -5
  228. package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
  229. package/dist/TopNav/TopNavHeading.js +54 -16
  230. package/dist/TopNav/TopNavItem.d.ts.map +1 -1
  231. package/dist/TopNav/TopNavItem.js +3 -9
  232. package/dist/TopNav/TopNavMegaMenu.d.ts +7 -0
  233. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  234. package/dist/TopNav/TopNavMegaMenu.js +146 -60
  235. package/dist/TopNav/TopNavMegaMenuItem.d.ts.map +1 -1
  236. package/dist/TopNav/TopNavMegaMenuItem.js +22 -3
  237. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  238. package/dist/TopNav/TopNavMenu.js +83 -38
  239. package/dist/TreeList/TreeList.d.ts.map +1 -1
  240. package/dist/TreeList/TreeList.js +13 -0
  241. package/dist/TreeList/TreeListBranches.d.ts +1 -1
  242. package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
  243. package/dist/TreeList/TreeListBranches.js +14 -4
  244. package/dist/TreeList/TreeListItem.d.ts +11 -1
  245. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  246. package/dist/TreeList/TreeListItem.js +57 -30
  247. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  248. package/dist/Typeahead/BaseTypeahead.js +16 -8
  249. package/dist/astryx.css +74 -85
  250. package/dist/astryx.umd.js +56 -56
  251. package/dist/astryx.umd.js.map +4 -4
  252. package/dist/hooks/containerReveal.stylex.d.ts +61 -0
  253. package/dist/hooks/containerReveal.stylex.d.ts.map +1 -0
  254. package/dist/hooks/containerReveal.stylex.js +63 -0
  255. package/dist/hooks/index.d.ts +3 -0
  256. package/dist/hooks/index.d.ts.map +1 -1
  257. package/dist/hooks/index.js +2 -0
  258. package/dist/hooks/useClipboard.d.ts +59 -0
  259. package/dist/hooks/useClipboard.d.ts.map +1 -0
  260. package/dist/hooks/useClipboard.js +106 -0
  261. package/dist/hooks/useContainerReveal.d.ts +14 -8
  262. package/dist/hooks/useContainerReveal.d.ts.map +1 -1
  263. package/dist/hooks/useContainerReveal.js +28 -69
  264. package/dist/hooks/useIndicatorFocusRing.d.ts +28 -0
  265. package/dist/hooks/useIndicatorFocusRing.d.ts.map +1 -0
  266. package/dist/hooks/useIndicatorFocusRing.js +96 -0
  267. package/dist/hooks/useLongPress.d.ts.map +1 -1
  268. package/dist/hooks/useLongPress.js +12 -2
  269. package/dist/hooks/useScrollLock.d.ts.map +1 -1
  270. package/dist/hooks/useScrollLock.js +33 -18
  271. package/dist/hooks/useStreamingText.d.ts +18 -3
  272. package/dist/hooks/useStreamingText.d.ts.map +1 -1
  273. package/dist/hooks/useStreamingText.js +47 -7
  274. package/dist/hooks/useTreeFocus.d.ts.map +1 -1
  275. package/dist/hooks/useTreeFocus.js +9 -3
  276. package/dist/hooks/useTypeahead.d.ts +3 -2
  277. package/dist/hooks/useTypeahead.d.ts.map +1 -1
  278. package/dist/hooks/useTypeahead.js +19 -7
  279. package/dist/index.d.ts +1 -0
  280. package/dist/index.d.ts.map +1 -1
  281. package/dist/index.js +1 -0
  282. package/dist/theme/defineTheme.d.ts +16 -0
  283. package/dist/theme/defineTheme.d.ts.map +1 -1
  284. package/dist/theme/defineTheme.js +8 -0
  285. package/dist/theme/derivedVarRegistry.d.ts +8 -0
  286. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  287. package/dist/theme/derivedVarRegistry.js +14 -13
  288. package/dist/theme/expandColorScale.d.ts +3 -1
  289. package/dist/theme/expandColorScale.d.ts.map +1 -1
  290. package/dist/theme/expandColorScale.js +17 -7
  291. package/dist/theme/expandTypeScale.d.ts.map +1 -1
  292. package/dist/theme/expandTypeScale.js +17 -0
  293. package/dist/theme/generateThemeRules.d.ts.map +1 -1
  294. package/dist/theme/generateThemeRules.js +11 -0
  295. package/dist/theme/index.d.ts +1 -0
  296. package/dist/theme/index.d.ts.map +1 -1
  297. package/dist/utils/focusOutline.stylex.d.ts +107 -0
  298. package/dist/utils/focusOutline.stylex.d.ts.map +1 -0
  299. package/dist/utils/focusOutline.stylex.js +130 -0
  300. package/dist/utils/focusReturn.d.ts +24 -0
  301. package/dist/utils/focusReturn.d.ts.map +1 -0
  302. package/dist/utils/focusReturn.js +29 -0
  303. package/dist/utils/index.d.ts +1 -0
  304. package/dist/utils/index.d.ts.map +1 -1
  305. package/dist/utils/index.js +1 -0
  306. package/dist/utils/mergeRefs.d.ts.map +1 -1
  307. package/dist/utils/mergeRefs.js +7 -5
  308. package/dist/utils/rtlStyles.d.ts +2 -2
  309. package/dist/utils/rtlStyles.js +2 -2
  310. package/dist/utils/themeProps.d.ts +20 -1
  311. package/dist/utils/themeProps.d.ts.map +1 -1
  312. package/dist/utils/themeProps.js +8 -2
  313. package/locales/en.json +12 -0
  314. package/locales/pseudo.json +9 -0
  315. package/package.json +7 -2
  316. package/src/AlertDialog/AlertDialog.doc.mjs +16 -3
  317. package/src/AlertDialog/AlertDialog.test.tsx +208 -1
  318. package/src/AlertDialog/AlertDialog.tsx +21 -3
  319. package/src/AppShell/AppShell.doc.mjs +11 -0
  320. package/src/AppShell/AppShell.test.tsx +139 -0
  321. package/src/AppShell/AppShell.tsx +32 -22
  322. package/src/AspectRatio/AspectRatio.doc.mjs +27 -3
  323. package/src/AspectRatio/AspectRatio.test.tsx +30 -0
  324. package/src/AspectRatio/AspectRatio.tsx +13 -0
  325. package/src/Avatar/Avatar.doc.mjs +2 -13
  326. package/src/Avatar/Avatar.test.tsx +33 -8
  327. package/src/Avatar/Avatar.tsx +52 -50
  328. package/src/AvatarGroup/AvatarGroupOverflow.tsx +2 -9
  329. package/src/Banner/Banner.tsx +9 -6
  330. package/src/Breadcrumbs/BreadcrumbItem.tsx +16 -6
  331. package/src/Breadcrumbs/index.ts +5 -1
  332. package/src/Button/Button.tsx +21 -41
  333. package/src/Calendar/Calendar.tsx +2 -1
  334. package/src/Calendar/styles.ts +0 -8
  335. package/src/Carousel/Carousel.doc.mjs +3 -0
  336. package/src/Carousel/Carousel.tsx +10 -6
  337. package/src/Chat/Chat.doc.mjs +2 -0
  338. package/src/Chat/ChatDictationButton.tsx +7 -1
  339. package/src/Chat/ChatLayoutScrollButton.test.tsx +22 -0
  340. package/src/Chat/ChatLayoutScrollButton.tsx +8 -1
  341. package/src/Chat/ChatSendButton.tsx +2 -3
  342. package/src/Chat/ChatToolCalls.test.tsx +35 -0
  343. package/src/Chat/ChatToolCalls.tsx +36 -16
  344. package/src/Chat/useTriggerMenu.tsx +2 -5
  345. package/src/CheckboxInput/CheckboxInput.doc.mjs +4 -2
  346. package/src/CheckboxInput/CheckboxInput.test.tsx +115 -0
  347. package/src/CheckboxInput/CheckboxInput.tsx +45 -202
  348. package/src/ClickableCard/ClickableCard.tsx +6 -11
  349. package/src/CodeBlock/CodeBlock.doc.mjs +3 -0
  350. package/src/CodeBlock/CodeBlock.test.tsx +91 -0
  351. package/src/CodeBlock/CodeBlock.tsx +57 -80
  352. package/src/Collapsible/Collapsible.tsx +22 -18
  353. package/src/CommandPalette/CommandPalette.test.tsx +29 -0
  354. package/src/CommandPalette/CommandPaletteItem.tsx +7 -4
  355. package/src/CommandPalette/CommandPaletteList.tsx +2 -2
  356. package/src/ComplexSelector/ComplexSelector.doc.mjs +42 -0
  357. package/src/ComplexSelector/ComplexSelector.test.tsx +64 -0
  358. package/src/ComplexSelector/ComplexSelector.tsx +46 -29
  359. package/src/ContextMenu/ContextMenu.doc.mjs +1 -1
  360. package/src/ContextMenu/ContextMenu.test.tsx +41 -0
  361. package/src/ContextMenu/ContextMenu.tsx +3 -3
  362. package/src/ContextMenu/index.ts +5 -1
  363. package/src/DateInput/DateInput.doc.mjs +18 -3
  364. package/src/DateInput/DateInput.test.tsx +93 -11
  365. package/src/DateInput/DateInput.tsx +41 -19
  366. package/src/DateRangeInput/DateRangeInput.doc.mjs +10 -2
  367. package/src/DateRangeInput/DateRangeInput.test.tsx +110 -14
  368. package/src/DateRangeInput/DateRangeInput.tsx +24 -12
  369. package/src/DateTimeInput/DateTimeInput.doc.mjs +16 -2
  370. package/src/DateTimeInput/DateTimeInput.test.tsx +75 -0
  371. package/src/DateTimeInput/DateTimeInput.tsx +35 -8
  372. package/src/Dialog/Dialog.tsx +2 -9
  373. package/src/DropdownMenu/DropdownMenu.doc.mjs +5 -9
  374. package/src/DropdownMenu/DropdownMenu.test.tsx +534 -4
  375. package/src/DropdownMenu/DropdownMenu.tsx +113 -52
  376. package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +27 -30
  377. package/src/DropdownMenu/DropdownMenuDivider.doc.mjs +34 -0
  378. package/src/DropdownMenu/DropdownMenuDivider.tsx +81 -0
  379. package/src/DropdownMenu/DropdownMenuItem.doc.mjs +39 -4
  380. package/src/DropdownMenu/DropdownMenuItem.tsx +48 -10
  381. package/src/DropdownMenu/DropdownMenuRadioItem.tsx +30 -76
  382. package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +25 -19
  383. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +44 -14
  384. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +1 -5
  385. package/src/DropdownMenu/index.ts +8 -1
  386. package/src/DropdownMenu/renderDropdownItems.tsx +33 -34
  387. package/src/EmptyState/EmptyState.doc.mjs +4 -0
  388. package/src/EmptyState/EmptyState.test.tsx +62 -0
  389. package/src/EmptyState/EmptyState.tsx +16 -6
  390. package/src/Field/Field.doc.mjs +1 -0
  391. package/src/Field/InputClearButton.test.tsx +117 -0
  392. package/src/Field/InputClearButton.tsx +30 -3
  393. package/src/HoverCard/useHoverCard.tsx +2 -23
  394. package/src/Icon/Icon.doc.mjs +2 -1
  395. package/src/Indicator/CheckIndicator.tsx +174 -0
  396. package/src/Indicator/CheckboxIndicator.tsx +257 -0
  397. package/src/Indicator/Indicator.doc.mjs +344 -0
  398. package/src/Indicator/Indicator.test.tsx +393 -0
  399. package/src/Indicator/RadioIndicator.tsx +206 -0
  400. package/src/Indicator/index.ts +45 -0
  401. package/src/Indicator/indicator.markers.stylex.ts +19 -0
  402. package/src/Indicator/indicatorRegistry.test.tsx +106 -0
  403. package/src/Indicator/indicatorRegistry.ts +120 -0
  404. package/src/Indicator/types.ts +186 -0
  405. package/src/Indicator/useIndicator.ts +39 -0
  406. package/src/Item/Item.doc.mjs +1 -1
  407. package/src/Item/Item.tsx +7 -14
  408. package/src/Layer/useLayer.doc.mjs +2 -2
  409. package/src/Layer/useLayer.test.tsx +85 -0
  410. package/src/Layer/useLayer.tsx +39 -1
  411. package/src/Lightbox/Lightbox.doc.mjs +1 -1
  412. package/src/Lightbox/Lightbox.tsx +43 -38
  413. package/src/Link/Link.tsx +4 -11
  414. package/src/MobileNav/MobileNav.tsx +3 -3
  415. package/src/MoreMenu/MoreMenu.doc.mjs +31 -0
  416. package/src/MoreMenu/MoreMenu.test.tsx +68 -1
  417. package/src/MoreMenu/MoreMenu.tsx +19 -0
  418. package/src/MultiSelector/MultiSelector.doc.mjs +9 -1
  419. package/src/MultiSelector/MultiSelector.test.tsx +255 -16
  420. package/src/MultiSelector/MultiSelector.tsx +103 -99
  421. package/src/NumberInput/NumberInput.doc.mjs +265 -50
  422. package/src/NumberInput/NumberInput.test.tsx +726 -21
  423. package/src/NumberInput/NumberInput.tsx +353 -59
  424. package/src/Outline/Outline.tsx +2 -5
  425. package/src/Pagination/Pagination.doc.mjs +1 -1
  426. package/src/Pagination/Pagination.test.tsx +22 -7
  427. package/src/Pagination/Pagination.tsx +38 -35
  428. package/src/Popover/Popover.tsx +6 -7
  429. package/src/PowerSearch/PowerSearch.tsx +1 -1
  430. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +42 -0
  431. package/src/PowerSearch/PowerSearchEditPopover.tsx +7 -0
  432. package/src/ProgressBar/ProgressBar.doc.mjs +27 -5
  433. package/src/ProgressBar/ProgressBar.test.tsx +224 -1
  434. package/src/ProgressBar/ProgressBar.tsx +124 -22
  435. package/src/RadioList/RadioList.doc.mjs +4 -2
  436. package/src/RadioList/RadioListItem.tsx +31 -130
  437. package/src/Resizable/ResizeHandle.tsx +2 -9
  438. package/src/SegmentedControl/SegmentedControlItem.tsx +2 -9
  439. package/src/SelectableCard/SelectableCard.tsx +4 -9
  440. package/src/Selector/Selector.doc.mjs +7 -4
  441. package/src/Selector/Selector.test.tsx +935 -25
  442. package/src/Selector/Selector.tsx +189 -106
  443. package/src/Selector/hooks.ts +77 -50
  444. package/src/SideNav/SideNav.test.tsx +46 -0
  445. package/src/SideNav/SideNav.tsx +1 -0
  446. package/src/SideNav/SideNavCollapseButton.tsx +15 -10
  447. package/src/SideNav/SideNavHeading.tsx +48 -10
  448. package/src/SideNav/SideNavItem.tsx +20 -12
  449. package/src/Stack/Stack.doc.mjs +110 -0
  450. package/src/StatusDot/StatusDot.doc.mjs +12 -3
  451. package/src/Switch/Switch.test.tsx +32 -0
  452. package/src/Switch/Switch.tsx +8 -8
  453. package/src/TabList/Tab.tsx +2 -9
  454. package/src/TabList/TabMenu.tsx +14 -21
  455. package/src/Table/BaseTable.tsx +11 -0
  456. package/src/Table/index.ts +1 -4
  457. package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +30 -14
  458. package/src/Table/plugins/rowExpansion/index.ts +1 -4
  459. package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +125 -175
  460. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +115 -458
  461. package/src/Table/plugins/sortable/useTableSortable.tsx +2 -6
  462. package/src/Table/plugins/tree/useTableTreeData.tsx +43 -24
  463. package/src/Table/tableContextMenu.test.tsx +27 -0
  464. package/src/Table/tableContextMenu.tsx +1 -0
  465. package/src/Table/types.ts +11 -0
  466. package/src/Table/useTableGroupedRows.doc.mjs +1 -1
  467. package/src/Table/useTableRowExpansion.doc.mjs +43 -66
  468. package/src/TextArea/TextArea.doc.mjs +42 -2
  469. package/src/TextArea/TextArea.test.tsx +219 -0
  470. package/src/TextArea/TextArea.tsx +34 -7
  471. package/src/TextInput/TextInput.doc.mjs +18 -2
  472. package/src/TextInput/TextInput.test.tsx +189 -0
  473. package/src/TextInput/TextInput.tsx +25 -31
  474. package/src/Thumbnail/Thumbnail.tsx +2 -9
  475. package/src/TimeInput/TimeInput.doc.mjs +2 -2
  476. package/src/TimeInput/TimeInput.test.tsx +30 -3
  477. package/src/TimeInput/TimeInput.tsx +9 -26
  478. package/src/Timestamp/Timestamp.doc.mjs +4 -3
  479. package/src/Timestamp/TimestampHoverCard.tsx +13 -35
  480. package/src/Token/Token.tsx +5 -28
  481. package/src/Tokenizer/Tokenizer.doc.mjs +2 -2
  482. package/src/Tokenizer/Tokenizer.test.tsx +45 -3
  483. package/src/Tokenizer/Tokenizer.tsx +10 -2
  484. package/src/Tooltip/useTooltip.tsx +2 -23
  485. package/src/TopNav/TopNav.tsx +1 -3
  486. package/src/TopNav/TopNavHeading.tsx +42 -13
  487. package/src/TopNav/TopNavItem.tsx +3 -21
  488. package/src/TopNav/TopNavMegaMenu.test.tsx +433 -33
  489. package/src/TopNav/TopNavMegaMenu.tsx +118 -53
  490. package/src/TopNav/TopNavMegaMenuItem.tsx +2 -9
  491. package/src/TopNav/TopNavMenu.tsx +34 -37
  492. package/src/TreeList/TreeList.doc.mjs +5 -0
  493. package/src/TreeList/TreeList.test.tsx +206 -7
  494. package/src/TreeList/TreeList.tsx +22 -0
  495. package/src/TreeList/TreeListBranches.tsx +25 -2
  496. package/src/TreeList/TreeListItem.tsx +89 -52
  497. package/src/Typeahead/BaseTypeahead.tsx +19 -6
  498. package/src/Typeahead/Typeahead.doc.mjs +1 -0
  499. package/src/Typeahead/Typeahead.test.tsx +76 -3
  500. package/src/hooks/containerReveal.stylex.ts +102 -0
  501. package/src/hooks/index.ts +4 -0
  502. package/src/hooks/useClipboard.doc.mjs +87 -0
  503. package/src/hooks/useClipboard.test.tsx +151 -0
  504. package/src/hooks/useClipboard.ts +135 -0
  505. package/src/hooks/useContainerReveal.doc.mjs +7 -7
  506. package/src/hooks/useContainerReveal.test.tsx +42 -62
  507. package/src/hooks/useContainerReveal.ts +36 -91
  508. package/src/hooks/useImageMode.test.ts +1 -1
  509. package/src/hooks/useIndicatorFocusRing.tsx +120 -0
  510. package/src/hooks/useLongPress.test.tsx +18 -0
  511. package/src/hooks/useLongPress.ts +12 -2
  512. package/src/hooks/useScrollLock.test.ts +72 -0
  513. package/src/hooks/useScrollLock.ts +51 -19
  514. package/src/hooks/useStreamingText.test.ts +46 -1
  515. package/src/hooks/useStreamingText.ts +49 -7
  516. package/src/hooks/useTreeFocus.test.tsx +50 -0
  517. package/src/hooks/useTreeFocus.ts +14 -3
  518. package/src/hooks/useTypeahead.test.tsx +150 -0
  519. package/src/hooks/useTypeahead.ts +24 -9
  520. package/src/i18n/InternationalizationProvider.doc.mjs +7 -2
  521. package/src/index.ts +1 -0
  522. package/src/theme/defineTheme.ts +24 -0
  523. package/src/theme/derivedVarRegistry.test.ts +21 -1
  524. package/src/theme/derivedVarRegistry.ts +19 -6
  525. package/src/theme/expandColorScale.test.ts +14 -0
  526. package/src/theme/expandColorScale.ts +24 -9
  527. package/src/theme/expandTypeScale.test.ts +43 -0
  528. package/src/theme/expandTypeScale.ts +17 -0
  529. package/src/theme/generateThemeRules.test.ts +61 -20
  530. package/src/theme/generateThemeRules.ts +11 -0
  531. package/src/theme/index.ts +13 -0
  532. package/src/utils/focusOutline.stylex.ts +163 -0
  533. package/src/utils/focusReturn.test.ts +42 -0
  534. package/src/utils/focusReturn.ts +31 -0
  535. package/src/utils/index.ts +1 -0
  536. package/src/utils/mergeRefs.test.ts +48 -0
  537. package/src/utils/mergeRefs.ts +12 -6
  538. package/src/utils/rtlStyles.ts +2 -2
  539. package/src/utils/themeProps.ts +26 -4
  540. package/dist/CheckboxInput/checkbox.markers.stylex.d.ts +0 -9
  541. package/dist/CheckboxInput/checkbox.markers.stylex.d.ts.map +0 -1
  542. package/dist/CheckboxInput/checkbox.markers.stylex.js +0 -14
  543. package/dist/RadioList/radio.markers.stylex.d.ts +0 -7
  544. package/dist/RadioList/radio.markers.stylex.d.ts.map +0 -1
  545. package/dist/RadioList/radio.markers.stylex.js +0 -12
  546. package/dist/hooks/containerReveal.pool.stylex.d.ts +0 -303
  547. package/dist/hooks/containerReveal.pool.stylex.d.ts.map +0 -1
  548. package/dist/hooks/containerReveal.pool.stylex.js +0 -292
  549. package/src/CheckboxInput/checkbox.markers.stylex.ts +0 -12
  550. package/src/RadioList/radio.markers.stylex.ts +0 -10
  551. package/src/hooks/containerReveal.pool.stylex.ts +0 -571
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "displayName": "Astryx Core",
5
5
  "description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
6
6
  "author": "Meta Open Source",
@@ -297,6 +297,11 @@
297
297
  "types": "./dist/IconButton/index.d.ts",
298
298
  "default": "./dist/IconButton/index.js"
299
299
  },
300
+ "./Indicator": {
301
+ "source": "./src/Indicator/index.ts",
302
+ "types": "./dist/Indicator/index.d.ts",
303
+ "default": "./dist/Indicator/index.js"
304
+ },
300
305
  "./InputGroup": {
301
306
  "source": "./src/InputGroup/index.ts",
302
307
  "types": "./dist/InputGroup/index.d.ts",
@@ -658,7 +663,7 @@
658
663
  "@testing-library/jest-dom": "^6.6.0",
659
664
  "@testing-library/react": "^16.3.2",
660
665
  "rimraf": "^6.0.1",
661
- "@astryxdesign/cli": "0.3.0"
666
+ "@astryxdesign/cli": "0.4.0"
662
667
  },
663
668
  "dependencies": {
664
669
  "intl-messageformat": "^11.2.9"
@@ -21,11 +21,21 @@ export const docs = {
21
21
  ],
22
22
  usage: {
23
23
  description:
24
- 'AlertDialog asks the user to confirm a destructive or irreversible action before it happens. Use it for things like deleting content, revoking access, or discarding unsaved changes.\n\nFor cases where you want to show an alert without managing open state, use the `useImperativeAlertDialog` hook: call `alert.show(options)` and render `alert.element` in your tree.',
24
+ 'AlertDialog asks the user to confirm a destructive or irreversible action before it happens. Use it for things like deleting content, revoking access, or discarding unsaved changes.\n\nIt implements the WAI-ARIA APG [Alert Dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/): `role="alertdialog"`, a title linked by `aria-labelledby`, a consequence description linked by `aria-describedby`, focus moved into the dialog on open and returned to the trigger on close, and no dismissal by clicking outside. Escape cancels.\n\nFor cases where you want to show an alert without managing open state, use the `useImperativeAlertDialog` hook: call `alert.show(options)` and render `alert.element` in your tree.',
25
25
  bestPractices: [
26
26
  {guidance: true, description: 'Make the action button label specific: "Delete project" is better than "OK" or "Confirm".'},
27
27
  {guidance: true, description: 'Describe what will happen in the description so the user knows the consequences before confirming.'},
28
+ {guidance: true, description: 'Keep the cancel button first: it takes initial focus, so the least destructive choice is the one already selected when the dialog opens.'},
28
29
  {guidance: false, description: 'Use AlertDialog for non-destructive actions; use a standard Dialog instead.'},
30
+ {guidance: false, description: 'Rely on color alone to signal danger; the action label itself should say what will happen.'},
31
+ {guidance: false, description: 'Close the dialog from onAction before the work finishes; hold it open with isActionLoading and call onOpenChange(false) when the action settles.'},
32
+ ],
33
+ anatomy: [
34
+ {name: 'Title', required: true, description: 'The question being asked. Renders as a level-2 heading and labels the dialog via aria-labelledby.'},
35
+ {name: 'Description', required: true, description: 'What will happen if the user confirms. Linked to the dialog via aria-describedby.'},
36
+ {name: 'Cancel button', required: true, description: 'Ghost button that dismisses without acting. Takes initial focus, and Escape does the same thing.'},
37
+ {name: 'Action button', required: true, description: 'The confirming action. Destructive by default; shows a spinner while isActionLoading is set.'},
38
+ {name: 'Backdrop', required: true, description: 'Overlay behind the dialog that blocks page interaction. Clicking it does not dismiss.'},
29
39
  ],
30
40
  },
31
41
  // Intentionally a contained isInline preview, not playground.overlay: the
@@ -107,7 +117,7 @@ export const docs = {
107
117
  name: 'isInline',
108
118
  type: 'boolean',
109
119
  default: 'false',
110
- description: 'Renders alert dialog content inline without modal behavior. For documentation previews and showcases only.',
120
+ description: 'Renders alert dialog content inline without modal behavior. For documentation previews and showcases only. Not being a modal, the inline path renders role="group" instead of role="alertdialog".',
111
121
  },
112
122
  ],
113
123
  components: [
@@ -125,11 +135,14 @@ export const docsDense = {
125
135
  description: 'Confirms destructive/irreversible action before it happens (delete, revoke access, discard unsaved changes).',
126
136
  usage: {
127
137
  description:
128
- 'AlertDialog confirms destructive/irreversible action (delete, revoke access, discard changes). To show w/o managing open state, use useImperativeAlertDialog hook: call alert.show(options) + render alert.element in tree.',
138
+ 'AlertDialog confirms destructive/irreversible action (delete, revoke access, discard changes). Implements WAI-ARIA APG Alert Dialog pattern (https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/): role="alertdialog", aria-labelledby title, aria-describedby description, focus into dialog on open + back to trigger on close, no outside-click dismissal, Escape cancels. To show w/o managing open state, use useImperativeAlertDialog hook: call alert.show(options) + render alert.element in tree.',
129
139
  bestPractices: [
130
140
  {guidance: true, description: 'Make action button label specific: "Delete project" > "OK"/"Confirm".'},
131
141
  {guidance: true, description: 'Describe consequences in description so user knows outcome before confirming.'},
142
+ {guidance: true, description: 'Keep cancel first: it takes initial focus, so least destructive choice is preselected.'},
132
143
  {guidance: false, description: 'Use AlertDialog for non-destructive actions; use standard Dialog instead.'},
144
+ {guidance: false, description: 'Rely on color alone for danger; the action label should say what happens.'},
145
+ {guidance: false, description: 'Close from onAction before work finishes; hold open w/ isActionLoading, call onOpenChange(false) when it settles.'},
133
146
  ],
134
147
  },
135
148
  };
@@ -10,8 +10,10 @@
10
10
  */
11
11
 
12
12
  import {describe, it, expect, vi, beforeEach} from 'vitest';
13
- import {render, screen, fireEvent} from '@testing-library/react';
13
+ import {render, screen, fireEvent, waitFor} from '@testing-library/react';
14
+ import userEvent from '@testing-library/user-event';
14
15
  import {AlertDialog} from './AlertDialog';
16
+ import {useImperativeAlertDialog} from './useImperativeAlertDialog';
15
17
 
16
18
  beforeEach(() => {
17
19
  HTMLDialogElement.prototype.showModal = vi.fn(function (
@@ -111,4 +113,209 @@ describe('AlertDialog', () => {
111
113
  render(<AlertDialog {...defaultProps} />);
112
114
  expect(screen.getByText('Cancel')).toBeInTheDocument();
113
115
  });
116
+
117
+ describe('keyboard', () => {
118
+ it('calls onOpenChange(false) on Escape', async () => {
119
+ const onOpenChange = vi.fn();
120
+ render(<AlertDialog {...defaultProps} onOpenChange={onOpenChange} />);
121
+ fireEvent.keyDown(screen.getByRole('alertdialog'), {key: 'Escape'});
122
+ await waitFor(() => expect(onOpenChange).toHaveBeenCalledWith(false));
123
+ });
124
+
125
+ it('does not call onAction on Escape', () => {
126
+ const onAction = vi.fn();
127
+ render(<AlertDialog {...defaultProps} onAction={onAction} />);
128
+ fireEvent.keyDown(screen.getByRole('alertdialog'), {key: 'Escape'});
129
+ expect(onAction).not.toHaveBeenCalled();
130
+ });
131
+
132
+ it('activates cancel with Enter', async () => {
133
+ const user = userEvent.setup();
134
+ const onOpenChange = vi.fn();
135
+ render(<AlertDialog {...defaultProps} onOpenChange={onOpenChange} />);
136
+ screen.getByRole('button', {name: 'Cancel'}).focus();
137
+ await user.keyboard('{Enter}');
138
+ expect(onOpenChange).toHaveBeenCalledWith(false);
139
+ });
140
+
141
+ it('activates the action button with Space', async () => {
142
+ const user = userEvent.setup();
143
+ const onAction = vi.fn();
144
+ render(<AlertDialog {...defaultProps} onAction={onAction} />);
145
+ screen.getByRole('button', {name: 'Delete'}).focus();
146
+ await user.keyboard(' ');
147
+ expect(onAction).toHaveBeenCalled();
148
+ });
149
+
150
+ it('reaches both buttons by Tab, cancel first', async () => {
151
+ const user = userEvent.setup();
152
+ render(<AlertDialog {...defaultProps} />);
153
+ const cancel = screen.getByRole('button', {name: 'Cancel'});
154
+ const action = screen.getByRole('button', {name: 'Delete'});
155
+ cancel.focus();
156
+ expect(cancel).toHaveFocus();
157
+ await user.tab();
158
+ expect(action).toHaveFocus();
159
+ });
160
+ });
161
+
162
+ describe('focus', () => {
163
+ it('marks cancel as the initial focus target', () => {
164
+ render(<AlertDialog {...defaultProps} />);
165
+ // Dialog focuses [data-autofocus] itself after showModal(), so the
166
+ // contract this component owns is which element carries the attribute.
167
+ expect(screen.getByRole('button', {name: 'Cancel'})).toHaveAttribute(
168
+ 'data-autofocus',
169
+ );
170
+ expect(screen.getByRole('button', {name: 'Delete'})).not.toHaveAttribute(
171
+ 'data-autofocus',
172
+ );
173
+ });
174
+
175
+ it('keeps the initial focus target on the least destructive action', () => {
176
+ render(<AlertDialog {...defaultProps} cancelLabel="Never mind" />);
177
+ expect(screen.getByRole('button', {name: 'Never mind'})).toHaveAttribute(
178
+ 'data-autofocus',
179
+ );
180
+ });
181
+
182
+ it('returns focus to the trigger when it closes', async () => {
183
+ const trigger = document.createElement('button');
184
+ trigger.textContent = 'Open';
185
+ document.body.appendChild(trigger);
186
+ trigger.focus();
187
+
188
+ const {rerender} = render(<AlertDialog {...defaultProps} />);
189
+ rerender(<AlertDialog {...defaultProps} isOpen={false} />);
190
+
191
+ await waitFor(() => expect(trigger).toHaveFocus());
192
+ trigger.remove();
193
+ });
194
+
195
+ it('does not disable the cancel button while the action is loading', () => {
196
+ render(<AlertDialog {...defaultProps} isActionLoading />);
197
+ expect(screen.getByRole('button', {name: 'Cancel'})).toBeEnabled();
198
+ });
199
+ });
200
+
201
+ describe('aria', () => {
202
+ it('marks the action button busy while loading', () => {
203
+ render(<AlertDialog {...defaultProps} isActionLoading />);
204
+ expect(screen.getByRole('button', {name: /Delete/})).toHaveAttribute(
205
+ 'aria-busy',
206
+ 'true',
207
+ );
208
+ });
209
+
210
+ it('is a modal alertdialog', () => {
211
+ render(<AlertDialog {...defaultProps} />);
212
+ expect(screen.getByRole('alertdialog')).toHaveAttribute(
213
+ 'aria-modal',
214
+ 'true',
215
+ );
216
+ });
217
+
218
+ describe('the inline preview path', () => {
219
+ it('does not claim the alertdialog role', () => {
220
+ render(<AlertDialog {...defaultProps} isInline />);
221
+ expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument();
222
+ });
223
+
224
+ it('exposes a named group instead, with no aria-modal', () => {
225
+ render(<AlertDialog {...defaultProps} isInline />);
226
+ const group = screen.getByRole('group', {name: 'Delete item?'});
227
+ expect(group).not.toHaveAttribute('aria-modal');
228
+ const describedBy = group.getAttribute('aria-describedby');
229
+ expect(describedBy).toBeTruthy();
230
+ expect(document.getElementById(describedBy!)).toHaveTextContent(
231
+ 'This action cannot be undone.',
232
+ );
233
+ });
234
+ });
235
+ });
236
+ });
237
+
238
+ describe('useImperativeAlertDialog', () => {
239
+ const noop = () => {};
240
+
241
+ function Harness({onAction = noop}: {onAction?: () => unknown}) {
242
+ const alert = useImperativeAlertDialog();
243
+ return (
244
+ <>
245
+ <button
246
+ type="button"
247
+ onClick={() =>
248
+ alert.show({
249
+ title: 'Delete item?',
250
+ description: 'This action cannot be undone.',
251
+ actionLabel: 'Delete',
252
+ onAction,
253
+ })
254
+ }>
255
+ Open
256
+ </button>
257
+ <button type="button" onClick={alert.hide}>
258
+ Close it
259
+ </button>
260
+ <span data-testid="is-open">{String(alert.isOpen)}</span>
261
+ {alert.element}
262
+ </>
263
+ );
264
+ }
265
+
266
+ it('renders nothing until show is called', () => {
267
+ render(<Harness />);
268
+ expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument();
269
+ expect(screen.getByTestId('is-open')).toHaveTextContent('false');
270
+ });
271
+
272
+ it('shows the dialog with the given options', async () => {
273
+ const user = userEvent.setup();
274
+ render(<Harness />);
275
+ await user.click(screen.getByRole('button', {name: 'Open'}));
276
+ expect(screen.getByRole('alertdialog')).toBeInTheDocument();
277
+ expect(screen.getByText('Delete item?')).toBeInTheDocument();
278
+ expect(
279
+ screen.getByText('This action cannot be undone.'),
280
+ ).toBeInTheDocument();
281
+ expect(screen.getByTestId('is-open')).toHaveTextContent('true');
282
+ });
283
+
284
+ it('hides on hide()', async () => {
285
+ const user = userEvent.setup();
286
+ render(<Harness />);
287
+ await user.click(screen.getByRole('button', {name: 'Open'}));
288
+ await user.click(screen.getByRole('button', {name: 'Close it'}));
289
+ expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument();
290
+ expect(screen.getByTestId('is-open')).toHaveTextContent('false');
291
+ });
292
+
293
+ it('closes when the dialog cancels', async () => {
294
+ const user = userEvent.setup();
295
+ render(<Harness />);
296
+ await user.click(screen.getByRole('button', {name: 'Open'}));
297
+ await user.click(screen.getByRole('button', {name: 'Cancel'}));
298
+ expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument();
299
+ expect(screen.getByTestId('is-open')).toHaveTextContent('false');
300
+ });
301
+
302
+ it('forwards onAction and leaves closing to the caller', async () => {
303
+ const user = userEvent.setup();
304
+ const onAction = vi.fn();
305
+ render(<Harness onAction={onAction} />);
306
+ await user.click(screen.getByRole('button', {name: 'Open'}));
307
+ await user.click(screen.getByRole('button', {name: 'Delete'}));
308
+ expect(onAction).toHaveBeenCalledTimes(1);
309
+ expect(screen.getByRole('alertdialog')).toBeInTheDocument();
310
+ });
311
+
312
+ it('can be shown again after being hidden', async () => {
313
+ const user = userEvent.setup();
314
+ render(<Harness />);
315
+ await user.click(screen.getByRole('button', {name: 'Open'}));
316
+ await user.click(screen.getByRole('button', {name: 'Close it'}));
317
+ await user.click(screen.getByRole('button', {name: 'Open'}));
318
+ expect(screen.getByRole('alertdialog')).toBeInTheDocument();
319
+ expect(screen.getByTestId('is-open')).toHaveTextContent('true');
320
+ });
114
321
  });
@@ -39,7 +39,8 @@ export interface AlertDialogProps extends BaseProps<HTMLDialogElement> {
39
39
 
40
40
  /**
41
41
  * Renders alert dialog content inline without modal behavior.
42
- * For documentation previews and showcases only.
42
+ * For documentation previews and showcases only. The inline path is not a
43
+ * modal, so it renders `role="group"` rather than `role="alertdialog"`.
43
44
  * @default false
44
45
  */
45
46
  isInline?: boolean;
@@ -100,9 +101,16 @@ export interface AlertDialogProps extends BaseProps<HTMLDialogElement> {
100
101
  /**
101
102
  * A confirmation dialog for destructive or irreversible actions.
102
103
  *
104
+ * Implements the WAI-ARIA APG Alert Dialog pattern:
105
+ * https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/
106
+ *
103
107
  * Uses `role="alertdialog"` and requires explicit user action to dismiss.
104
108
  * Cannot be dismissed by clicking outside. Escape key triggers cancel.
105
- * Initial focus goes to the cancel button (least destructive action).
109
+ * Initial focus goes to the cancel button (least destructive action), pinned
110
+ * with `data-autofocus` so it survives any change to the footer's order.
111
+ *
112
+ * The `isInline` preview path is not modal, so it renders `role="group"`
113
+ * instead — the alertdialog role would promise modality it does not have.
106
114
  *
107
115
  * @example
108
116
  * ```
@@ -153,7 +161,12 @@ export function AlertDialog({
153
161
  onOpenChange={onOpenChange}
154
162
  width={width}
155
163
  purpose="form"
156
- role="alertdialog"
164
+ // `alertdialog` is a modal role: it promises an interruption the user
165
+ // has to deal with, a focus trap, and an inert page behind it. The
166
+ // inline path renders a plain always-present div with none of that, so
167
+ // the role would misdescribe it. `group` keeps the title and
168
+ // description associated with a container without claiming a dialog.
169
+ role={isInline ? 'group' : 'alertdialog'}
157
170
  aria-labelledby={titleId}
158
171
  aria-describedby={descriptionId}
159
172
  {...mergeProps(themeProps('alert-dialog'), {className, style})}
@@ -177,6 +190,11 @@ export function AlertDialog({
177
190
  variant="ghost"
178
191
  label={cancelLabel}
179
192
  onClick={handleCancel}
193
+ // Dialog focuses [data-autofocus] itself after showModal(),
194
+ // because React's autoFocus runs during commit while the
195
+ // dialog is still invisible. Cancel is the least destructive
196
+ // choice, so it is the one that should be preselected.
197
+ data-autofocus
180
198
  />
181
199
  <Button
182
200
  variant={actionVariant}
@@ -14,8 +14,19 @@ export const docs = {
14
14
  bestPractices: [
15
15
  {guidance: true, description: 'Choose the right height: use "fill" for dashboards with internal scrolling and "auto" for pages that grow with content.'},
16
16
  {guidance: true, description: 'Set `contentPadding` based on content type: 4 for forms and settings, 0 for tables and dashboards.'},
17
+ {guidance: true, description: 'Give every nav slot an accessible name. AppShell renders TopNav and SideNav as separate navigation landmarks, and a screen reader lists them by name, so pass `label` to each one.'},
18
+ {guidance: true, description: 'Start the page heading inside `children`. AppShell owns the skip link, the banner landmark and the main landmark, but it renders no heading, so the first heading in the content area is the page h1.'},
17
19
  {guidance: false, description: "Nest one AppShell inside another; it's the outermost layout frame."},
18
20
  {guidance: false, description: 'Use for sub-page layouts; use Layout for content areas within AppShell.'},
21
+ {guidance: false, description: 'Add your own skip link or <main> element. AppShell already renders both, and a second main landmark makes the first ambiguous.'},
22
+ ],
23
+ anatomy: [
24
+ {name: 'Skip link', required: true, description: 'First focusable element on the page. Visually hidden until focused, then moves focus to the main content area.'},
25
+ {name: 'Banner', required: false, description: 'The banner slot, for system-wide announcements. Renders above the top nav, inside the banner landmark.'},
26
+ {name: 'Top navigation', required: false, description: 'The topNav slot, typically TopNav. Below the mobile breakpoint it becomes a compact bar carrying the nav toggle.'},
27
+ {name: 'Side navigation', required: false, description: 'The sideNav slot, typically SideNav. Inline above the breakpoint, moved into the mobile drawer below it.'},
28
+ {name: 'Main content', required: true, description: 'children, rendered in the main landmark. Scrolls internally when height is fill, and with the page when it is auto.'},
29
+ {name: 'Mobile nav drawer', required: false, description: 'Generated below the breakpoint from the nav slots unless mobileNav disables or replaces it. A modal dialog: it traps focus and returns focus to the toggle on close.'},
19
30
  ],
20
31
  },
21
32
  props: [
@@ -19,11 +19,13 @@ import {
19
19
  afterEach,
20
20
  } from 'vitest';
21
21
  import {render, screen, fireEvent} from '@testing-library/react';
22
+ import userEvent from '@testing-library/user-event';
22
23
  import {AppShell} from './AppShell';
23
24
  import {InternationalizationProvider} from '../i18n';
24
25
  import {MobileNav} from '../MobileNav';
25
26
  import {SideNav, SideNavItem, SideNavSection} from '../SideNav';
26
27
  import {TopNav, TopNavHeading, TopNavItem} from '../TopNav';
28
+ import {useAppShellMobile} from './AppShellMobileContext';
27
29
 
28
30
  // jsdom doesn't implement showModal/close on <dialog>, so we mock them
29
31
  beforeAll(() => {
@@ -778,4 +780,141 @@ describe('AppShell', () => {
778
780
  expect(ref).toHaveBeenCalled();
779
781
  expect(ref.mock.calls[0][0]).toBe(screen.getByTestId('shell'));
780
782
  });
783
+ // ===========================================================================
784
+ // Keyboard operation
785
+ //
786
+ // The focus TRAP and focus RESTORE contracts of the drawer are native
787
+ // <dialog> behaviour and cannot be asserted here: jsdom has no dialog
788
+ // implementation, so this file shims showModal()/close() with an `open`
789
+ // attribute. Those two were verified in Chromium instead. What is asserted
790
+ // here is everything the shim can still prove: the keyboard path to each
791
+ // control, and the ARIA wiring between the toggle and the drawer.
792
+ // ===========================================================================
793
+
794
+ it('reaches the skip link with Tab and moves focus to main with Enter', async () => {
795
+ const user = userEvent.setup();
796
+ render(
797
+ <AppShell topNav={<TopNav label="Main navigation" />}>
798
+ <div>Content</div>
799
+ </AppShell>,
800
+ );
801
+
802
+ await user.tab();
803
+ const skipLink = screen.getByTestId('skip-to-content');
804
+ expect(skipLink).toHaveFocus();
805
+
806
+ await user.keyboard('{Enter}');
807
+ expect(screen.getByRole('main')).toHaveFocus();
808
+ });
809
+
810
+ it('opens the mobile drawer from the keyboard and points the toggle at it', async () => {
811
+ mockMql = createMockMatchMedia(true);
812
+ vi.stubGlobal('matchMedia', vi.fn().mockReturnValue(mockMql));
813
+ const user = userEvent.setup();
814
+
815
+ render(
816
+ <AppShell sideNav={<TestSideNav />}>
817
+ <div>Content</div>
818
+ </AppShell>,
819
+ );
820
+
821
+ const toggle = screen.getByRole('button', {name: 'Open navigation'});
822
+ expect(toggle).toHaveAttribute('aria-expanded', 'false');
823
+
824
+ toggle.focus();
825
+ await user.keyboard('{Enter}');
826
+
827
+ expect(toggle).toHaveAttribute('aria-expanded', 'true');
828
+ const drawer = screen.getByRole('dialog', {hidden: true});
829
+ // aria-controls has to name the drawer that actually opened, or a screen
830
+ // reader cannot follow the toggle to it.
831
+ expect(toggle.getAttribute('aria-controls')).toBe(drawer.id);
832
+ expect(drawer.id).toBeTruthy();
833
+ });
834
+
835
+ // ===========================================================================
836
+ // Banner landmark on the mobile top bar
837
+ // ===========================================================================
838
+
839
+ it('exposes the mobile top bar as a banner landmark in a sidenav-only layout', () => {
840
+ mockMql = createMockMatchMedia(true);
841
+ vi.stubGlobal('matchMedia', vi.fn().mockReturnValue(mockMql));
842
+
843
+ render(
844
+ <AppShell sideNav={<TestSideNav />}>
845
+ <div>Content</div>
846
+ </AppShell>,
847
+ );
848
+
849
+ // Same landmark structure as the topNav layout: one banner region holding
850
+ // the top bar, whichever nav slots the page happens to fill.
851
+ expect(screen.getByRole('banner')).toBeInTheDocument();
852
+ });
853
+
854
+ it('renders exactly one banner landmark when a banner slot joins the mobile top bar', () => {
855
+ mockMql = createMockMatchMedia(true);
856
+ vi.stubGlobal('matchMedia', vi.fn().mockReturnValue(mockMql));
857
+
858
+ render(
859
+ <AppShell banner={<div>Announcement</div>} sideNav={<TestSideNav />}>
860
+ <div>Content</div>
861
+ </AppShell>,
862
+ );
863
+
864
+ expect(screen.getAllByRole('banner')).toHaveLength(1);
865
+ });
866
+
867
+ // ===========================================================================
868
+ // useAppShellMobile
869
+ // ===========================================================================
870
+
871
+ function MobileProbe() {
872
+ const {isMobile, isMobileNavOpen, isMobileNavEnabled, mobileNavId} =
873
+ useAppShellMobile();
874
+ return (
875
+ <span data-testid="probe">
876
+ {JSON.stringify({
877
+ isMobile,
878
+ isMobileNavOpen,
879
+ isMobileNavEnabled,
880
+ hasId: mobileNavId != null,
881
+ })}
882
+ </span>
883
+ );
884
+ }
885
+
886
+ it('useAppShellMobile is inert outside an AppShell', () => {
887
+ render(<MobileProbe />);
888
+ expect(JSON.parse(screen.getByTestId('probe').textContent)).toEqual({
889
+ isMobile: false,
890
+ isMobileNavOpen: false,
891
+ isMobileNavEnabled: false,
892
+ hasId: false,
893
+ });
894
+ });
895
+
896
+ it('useAppShellMobile reports the shell mobile state and drawer id', async () => {
897
+ mockMql = createMockMatchMedia(true);
898
+ vi.stubGlobal('matchMedia', vi.fn().mockReturnValue(mockMql));
899
+ const user = userEvent.setup();
900
+
901
+ render(
902
+ <AppShell sideNav={<TestSideNav />}>
903
+ <MobileProbe />
904
+ </AppShell>,
905
+ );
906
+
907
+ expect(JSON.parse(screen.getByTestId('probe').textContent)).toEqual({
908
+ isMobile: true,
909
+ isMobileNavOpen: false,
910
+ isMobileNavEnabled: true,
911
+ hasId: true,
912
+ });
913
+
914
+ await user.click(screen.getByRole('button', {name: 'Open navigation'}));
915
+
916
+ expect(
917
+ JSON.parse(screen.getByTestId('probe').textContent).isMobileNavOpen,
918
+ ).toBe(true);
919
+ });
781
920
  });
@@ -51,6 +51,7 @@ import type {AppShellMobileContextValue} from './AppShellMobileContext';
51
51
  import type {SpacingStep} from '../utils/types';
52
52
  import type {BaseProps} from '../BaseProps';
53
53
  import {mergeProps, mergeRefs, isRenderable} from '../utils';
54
+ import {focusOutlineProps} from '../utils/focusOutline.stylex';
54
55
  import {useMediaQuery} from '../hooks/useMediaQuery';
55
56
  import {observeResize, unobserveResize} from '../utils/sharedResizeObserver';
56
57
  import {themeProps} from '../utils/themeProps';
@@ -217,7 +218,7 @@ export interface AppShellProps extends BaseProps<HTMLDivElement> {
217
218
  * <AppShell mobileNav={{ hasToggle: false }}>
218
219
  * <MobileNavToggle />
219
220
  * </AppShell>
220
- * <AppShell mobileNav={<MobileNav title="Menu">...</MobileNav>} />
221
+ * <AppShell mobileNav={<MobileNav header="Menu">...</MobileNav>} />
221
222
  * <AppShell mobileNav={false} />
222
223
  * ```
223
224
  */
@@ -409,8 +410,8 @@ const styles = stylex.create({
409
410
  flexShrink: 0,
410
411
  overflow: 'clip',
411
412
  position: 'sticky',
412
- top: 'var(--appshell-header-height, 0px)',
413
- height: 'calc(100dvh - var(--appshell-header-height, 0px))',
413
+ top: 'var(--_app-shell-header-height, 0px)',
414
+ height: 'calc(100dvh - var(--_app-shell-header-height, 0px))',
414
415
  // Ensure children (LayoutPanel → SideNav) fill the sticky container
415
416
  display: 'flex',
416
417
  flexDirection: 'column',
@@ -441,7 +442,7 @@ const styles = stylex.create({
441
442
  * topNav={<TopNav label="Navigation" heading={<TopNavHeading heading="My App" />} />}
442
443
  * sideNav={<SideNav>{navSections}</SideNav>}
443
444
  * mobileNav={
444
- * <MobileNav isOpen={mobileOpen} onOpenChange={(open) => setMobileOpen(open)} title="My App">
445
+ * <MobileNav isOpen={mobileOpen} onOpenChange={(open) => setMobileOpen(open)} header="My App">
445
446
  * {navSections}
446
447
  * </MobileNav>
447
448
  * }>
@@ -512,14 +513,19 @@ export function AppShell({
512
513
  const [uncontrolledMobileOpen, setUncontrolledMobileOpen] = useState(false);
513
514
  const isMobileNavOpen = mobileNavConfig?.isOpen ?? uncontrolledMobileOpen;
514
515
 
516
+ const mobileNavOnOpenChange = mobileNavConfig?.onOpenChange;
515
517
  const setMobileNavOpen = useCallback(
516
518
  (open: boolean) => {
517
519
  if (!mobileNavIsControlled) {
518
520
  setUncontrolledMobileOpen(open);
519
521
  }
520
- mobileNavConfig?.onOpenChange?.(open);
522
+ mobileNavOnOpenChange?.(open);
521
523
  },
522
- [mobileNavIsControlled, mobileNavConfig],
524
+ // Depend on the callback itself, not on the config object: the documented
525
+ // usage is an inline `mobileNav={{isOpen, onOpenChange}}` literal, which is
526
+ // a new object every render and would otherwise churn this callback and the
527
+ // context value memo built from it on every render of the shell.
528
+ [mobileNavIsControlled, mobileNavOnOpenChange],
523
529
  );
524
530
 
525
531
  // Move focus to the main content container when the skip link is activated.
@@ -577,7 +583,7 @@ export function AppShell({
577
583
 
578
584
  const updateHeight = () => {
579
585
  const height = headerEl.getBoundingClientRect().height;
580
- shellEl.style.setProperty('--appshell-header-height', `${height}px`);
586
+ shellEl.style.setProperty('--_app-shell-header-height', `${height}px`);
581
587
  };
582
588
 
583
589
  observeResize(headerEl, () => updateHeight());
@@ -669,20 +675,19 @@ export function AppShell({
669
675
  </LayoutHeader>
670
676
  ) : undefined;
671
677
 
672
- const headerContent =
673
- headerInner != null ? (
674
- <div
675
- ref={headerRef}
676
- // Top-level banner landmark for the header region (topNav + banner).
677
- // Safe here: the wrapper is never nested inside main/nav/other landmarks.
678
- role="banner"
679
- {...mergeProps(
680
- themeProps('app-shell-header', {variant}),
681
- stylex.props(navAreaStyle, isAuto && styles.headerSticky),
682
- )}>
683
- {headerInner}
684
- </div>
685
- ) : undefined;
678
+ const headerContent = isRenderable(headerInner) ? (
679
+ <div
680
+ ref={headerRef}
681
+ // Top-level banner landmark for the header region (topNav + banner).
682
+ // Safe here: the wrapper is never nested inside main/nav/other landmarks.
683
+ role="banner"
684
+ {...mergeProps(
685
+ themeProps('app-shell-header', {variant}),
686
+ stylex.props(navAreaStyle, isAuto && styles.headerSticky),
687
+ )}>
688
+ {headerInner}
689
+ </div>
690
+ ) : undefined;
686
691
 
687
692
  // =========================================================================
688
693
  // Build sideNav content
@@ -760,6 +765,11 @@ export function AppShell({
760
765
  const autoMobileTopBar =
761
766
  shouldShowAutoToggle && !hasTopNav && hasSideNav ? (
762
767
  <div
768
+ // Banner landmark for the mobile top bar, so the sidenav-only layout
769
+ // exposes the same landmark structure as the topNav one. Only when
770
+ // there is no headerContent: a banner slot with no topNav renders both,
771
+ // and two sibling banner regions is worse than the one this restores.
772
+ role={headerContent == null ? 'banner' : undefined}
763
773
  {...mergeProps(
764
774
  themeProps('app-shell-header', {variant}),
765
775
  stylex.props(navAreaStyle, isAuto && styles.headerSticky),
@@ -805,7 +815,7 @@ export function AppShell({
805
815
  <a
806
816
  href={`#${MAIN_CONTENT_ID}`}
807
817
  onClick={handleSkipLinkClick}
808
- {...stylex.props(styles.skipLink)}
818
+ {...focusOutlineProps.focusVisible(styles.skipLink)}
809
819
  data-testid="skip-to-content">
810
820
  {t('@astryx.appShell.skipToContent')}
811
821
  </a>