@astryxdesign/core 0.7.1 → 0.7.2-canary.020e1fe

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 (377) hide show
  1. package/dist/AppShell/AppShell.d.ts.map +1 -1
  2. package/dist/AppShell/AppShell.js +40 -1
  3. package/dist/AspectRatio/AspectRatio.d.ts +6 -8
  4. package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
  5. package/dist/AspectRatio/AspectRatio.js +44 -3
  6. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  7. package/dist/Breadcrumbs/BreadcrumbItem.js +5 -2
  8. package/dist/Button/Button.d.ts.map +1 -1
  9. package/dist/Button/Button.js +38 -14
  10. package/dist/CheckboxInput/CheckboxInput.d.ts +1 -1
  11. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  12. package/dist/CheckboxInput/CheckboxInput.js +3 -1
  13. package/dist/CheckboxList/CheckboxListItem.d.ts +9 -1
  14. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  15. package/dist/CheckboxList/CheckboxListItem.js +4 -2
  16. package/dist/CommandPalette/CommandPaletteInput.d.ts +1 -1
  17. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  18. package/dist/CommandPalette/CommandPaletteInput.js +37 -5
  19. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  20. package/dist/ContextMenu/ContextMenu.js +5 -2
  21. package/dist/Dialog/Dialog.d.ts.map +1 -1
  22. package/dist/Dialog/Dialog.js +20 -8
  23. package/dist/Drawer/Drawer.d.ts +191 -0
  24. package/dist/Drawer/Drawer.d.ts.map +1 -0
  25. package/dist/Drawer/Drawer.js +382 -0
  26. package/dist/Drawer/DrawerHeader.d.ts +85 -0
  27. package/dist/Drawer/DrawerHeader.d.ts.map +1 -0
  28. package/dist/Drawer/DrawerHeader.js +134 -0
  29. package/dist/Drawer/index.d.ts +11 -0
  30. package/dist/Drawer/index.d.ts.map +1 -0
  31. package/dist/Drawer/index.js +11 -0
  32. package/dist/Drawer/useDrawerDialogPresence.d.ts +42 -0
  33. package/dist/Drawer/useDrawerDialogPresence.d.ts.map +1 -0
  34. package/dist/Drawer/useDrawerDialogPresence.js +236 -0
  35. package/dist/DropdownMenu/DropdownMenu.d.ts +1 -1
  36. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  37. package/dist/DropdownMenu/DropdownMenu.js +83 -11
  38. package/dist/DropdownMenu/DropdownMenuContext.d.ts +17 -0
  39. package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
  40. package/dist/DropdownMenu/DropdownMenuContext.js +21 -0
  41. package/dist/DropdownMenu/DropdownMenuDivider.d.ts +5 -3
  42. package/dist/DropdownMenu/DropdownMenuDivider.d.ts.map +1 -1
  43. package/dist/DropdownMenu/DropdownMenuDivider.js +7 -3
  44. package/dist/DropdownMenu/DropdownMenuGroup.d.ts +6 -4
  45. package/dist/DropdownMenu/DropdownMenuGroup.d.ts.map +1 -1
  46. package/dist/DropdownMenu/DropdownMenuGroup.js +4 -3
  47. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  48. package/dist/DropdownMenu/DropdownMenuSubMenu.js +19 -8
  49. package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts +24 -0
  50. package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts.map +1 -0
  51. package/dist/DropdownMenu/lazyMenuBottomSheet.js +46 -0
  52. package/dist/EmptyState/EmptyState.d.ts.map +1 -1
  53. package/dist/EmptyState/EmptyState.js +4 -10
  54. package/dist/Field/Field.d.ts +1 -1
  55. package/dist/Field/Field.d.ts.map +1 -1
  56. package/dist/Field/Field.js +8 -4
  57. package/dist/Grid/GridSpan.d.ts.map +1 -1
  58. package/dist/Grid/GridSpan.js +1 -0
  59. package/dist/Heading/Heading.d.ts.map +1 -1
  60. package/dist/Heading/Heading.js +4 -2
  61. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  62. package/dist/HoverCard/useHoverCard.js +10 -2
  63. package/dist/Item/Item.d.ts +6 -2
  64. package/dist/Item/Item.d.ts.map +1 -1
  65. package/dist/Item/Item.js +19 -4
  66. package/dist/Layer/layerViewportInset.stylex.d.ts +6 -2
  67. package/dist/Layer/layerViewportInset.stylex.d.ts.map +1 -1
  68. package/dist/Layer/layerViewportInset.stylex.js +6 -2
  69. package/dist/Layer/useLayer.d.ts.map +1 -1
  70. package/dist/Layer/useLayer.js +13 -7
  71. package/dist/Layer/useLayerDismissal.d.ts +1 -1
  72. package/dist/Layer/useLayerDismissal.js +1 -1
  73. package/dist/Layout/container.stylex.d.ts +5 -5
  74. package/dist/Layout/container.stylex.js +4 -4
  75. package/dist/List/List.d.ts +46 -19
  76. package/dist/List/List.d.ts.map +1 -1
  77. package/dist/List/List.js +28 -10
  78. package/dist/List/ListContext.d.ts +8 -3
  79. package/dist/List/ListContext.d.ts.map +1 -1
  80. package/dist/List/ListContext.js +7 -2
  81. package/dist/List/ListItem.d.ts +13 -5
  82. package/dist/List/ListItem.d.ts.map +1 -1
  83. package/dist/List/ListItem.js +21 -8
  84. package/dist/List/index.d.ts +1 -1
  85. package/dist/List/index.d.ts.map +1 -1
  86. package/dist/Markdown/Markdown.d.ts.map +1 -1
  87. package/dist/Markdown/Markdown.js +8 -1
  88. package/dist/Markdown/plugins/frontmatter.d.ts.map +1 -1
  89. package/dist/Markdown/plugins/frontmatter.js +7 -3
  90. package/dist/MetadataList/MetadataList.d.ts.map +1 -1
  91. package/dist/MetadataList/MetadataList.js +5 -1
  92. package/dist/MetadataList/MetadataListContext.d.ts +1 -1
  93. package/dist/MetadataList/MetadataListContext.d.ts.map +1 -1
  94. package/dist/MobileNav/MobileNav.d.ts +9 -0
  95. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  96. package/dist/MobileNav/MobileNav.js +30 -3
  97. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  98. package/dist/MultiSelector/MultiSelector.js +22 -8
  99. package/dist/MultiSelector/OptionActions.a11y.chromium.spec.js +100 -2
  100. package/dist/MultiSelector/hooks.d.ts.map +1 -1
  101. package/dist/MultiSelector/hooks.js +3 -1
  102. package/dist/Pagination/Pagination.d.ts +8 -5
  103. package/dist/Pagination/Pagination.d.ts.map +1 -1
  104. package/dist/Pagination/Pagination.js +21 -8
  105. package/dist/Popover/Popover.js +2 -2
  106. package/dist/PowerSearch/PowerSearch.js +1 -1
  107. package/dist/RadioList/RadioList.d.ts +1 -1
  108. package/dist/RadioList/RadioList.d.ts.map +1 -1
  109. package/dist/RadioList/RadioList.js +6 -2
  110. package/dist/RadioList/RadioListItem.d.ts +9 -1
  111. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  112. package/dist/RadioList/RadioListItem.js +2 -0
  113. package/dist/Selector/hooks.d.ts.map +1 -1
  114. package/dist/Selector/hooks.js +6 -2
  115. package/dist/SideNav/SideNavItem.d.ts +8 -1
  116. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  117. package/dist/SideNav/SideNavItem.js +71 -9
  118. package/dist/Switch/Switch.d.ts +1 -1
  119. package/dist/Switch/Switch.d.ts.map +1 -1
  120. package/dist/Switch/Switch.js +13 -1
  121. package/dist/TabList/TabMenu.d.ts +2 -2
  122. package/dist/TabList/TabMenu.d.ts.map +1 -1
  123. package/dist/TabList/TabMenu.js +6 -3
  124. package/dist/Text/Text.d.ts.map +1 -1
  125. package/dist/Text/Text.js +4 -2
  126. package/dist/Text/useTruncation.d.ts +7 -5
  127. package/dist/Text/useTruncation.d.ts.map +1 -1
  128. package/dist/Text/useTruncation.js +16 -5
  129. package/dist/ToggleButton/ToggleButton.d.ts +3 -2
  130. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  131. package/dist/ToggleButton/ToggleButton.js +12 -1
  132. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  133. package/dist/Tooltip/useTooltip.js +11 -0
  134. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  135. package/dist/TopNav/TopNavMegaMenu.js +3 -3
  136. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  137. package/dist/TopNav/TopNavMenu.js +3 -3
  138. package/dist/TreeList/TreeList.d.ts.map +1 -1
  139. package/dist/TreeList/TreeList.js +1 -0
  140. package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
  141. package/dist/TreeList/TreeListBranches.js +2 -2
  142. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  143. package/dist/TreeList/TreeListItem.js +57 -9
  144. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  145. package/dist/Typeahead/BaseTypeahead.js +26 -0
  146. package/dist/astryx.css +47 -13
  147. package/dist/hooks/useEntryAnimation.d.ts +2 -1
  148. package/dist/hooks/useEntryAnimation.d.ts.map +1 -1
  149. package/dist/hooks/useEntryAnimation.js +14 -12
  150. package/dist/hooks/useIsHydrating.d.ts +6 -0
  151. package/dist/hooks/useIsHydrating.d.ts.map +1 -0
  152. package/dist/hooks/useIsHydrating.js +30 -0
  153. package/dist/hooks/useMenuHover.d.ts.map +1 -1
  154. package/dist/hooks/useMenuHover.js +18 -13
  155. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  156. package/dist/i18n/generated-locales/en.generated.js +112 -1
  157. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  158. package/dist/i18n/generated-locales/pseudo.generated.js +112 -1
  159. package/dist/index.d.ts +1 -0
  160. package/dist/index.d.ts.map +1 -1
  161. package/dist/index.js +1 -0
  162. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  163. package/dist/theme/derivedVarRegistry.js +0 -3
  164. package/dist/utils/focusOutline.stylex.d.ts +4 -0
  165. package/dist/utils/focusOutline.stylex.d.ts.map +1 -1
  166. package/dist/utils/focusOutline.stylex.js +4 -0
  167. package/dist/utils/plainDate.d.ts.map +1 -1
  168. package/dist/utils/plainDate.js +15 -9
  169. package/dist/utils/sharedMutationObserver.d.ts +21 -0
  170. package/dist/utils/sharedMutationObserver.d.ts.map +1 -0
  171. package/dist/utils/sharedMutationObserver.js +69 -0
  172. package/locales/en.json +444 -0
  173. package/locales/pseudo.json +333 -0
  174. package/package.json +9 -4
  175. package/scripts/agent-doc-state.mjs +38 -0
  176. package/scripts/postinstall.mjs +1 -1
  177. package/src/AppShell/AppShell.tsx +45 -1
  178. package/src/AspectRatio/AspectRatio.test.tsx +44 -43
  179. package/src/AspectRatio/AspectRatio.tsx +38 -18
  180. package/src/AspectRatio/__tests__/AspectRatioFitLayers.a11y.browser.spec.ts +157 -0
  181. package/src/Avatar/Avatar.doc.mjs +5 -3
  182. package/src/AvatarGroup/AvatarGroup.doc.mjs +3 -2
  183. package/src/Breadcrumbs/BreadcrumbItem.tsx +4 -1
  184. package/src/Breadcrumbs/Breadcrumbs.test.tsx +30 -1
  185. package/src/Button/Button.doc.mjs +2 -2
  186. package/src/Button/Button.test.tsx +155 -9
  187. package/src/Button/Button.tsx +39 -16
  188. package/src/Button/__tests__/Button.a11y.known-failures.ts +1 -62
  189. package/src/Button/__tests__/Button.a11y.states.ts +0 -10
  190. package/src/Card/Card.doc.mjs +1 -1
  191. package/src/Chat/__tests__/ChatMessageMetadata.a11y.chromium.spec.ts +3 -1
  192. package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
  193. package/src/CheckboxInput/CheckboxInput.tsx +3 -1
  194. package/src/CheckboxList/CheckboxList.test.tsx +22 -0
  195. package/src/CheckboxList/CheckboxListItem.doc.mjs +16 -0
  196. package/src/CheckboxList/CheckboxListItem.tsx +10 -0
  197. package/src/Code/Code.doc.mjs +1 -0
  198. package/src/CommandPalette/CommandPalette.doc.mjs +2 -0
  199. package/src/CommandPalette/CommandPaletteInput.test.tsx +53 -4
  200. package/src/CommandPalette/CommandPaletteInput.tsx +33 -3
  201. package/src/CommandPalette/__tests__/CommandPaletteInputAudit.a11y.chromium.spec.ts +335 -0
  202. package/src/CommandPalette/__tests__/CommandPaletteItemAudit.a11y.chromium.spec.ts +292 -0
  203. package/src/ContextMenu/ContextMenu.doc.mjs +2 -3
  204. package/src/ContextMenu/ContextMenu.tsx +4 -1
  205. package/src/ContextMenu/__tests__/ContextMenuAudit.a11y.chromium.spec.ts +438 -0
  206. package/src/Dialog/Dialog.test.tsx +46 -0
  207. package/src/Dialog/Dialog.tsx +20 -7
  208. package/src/Dialog/__tests__/Dialog.a11y.known-failures.ts +0 -14
  209. package/src/Drawer/Drawer.doc.mjs +473 -0
  210. package/src/Drawer/Drawer.spec.md +357 -0
  211. package/src/Drawer/Drawer.test.tsx +1300 -0
  212. package/src/Drawer/Drawer.tsx +613 -0
  213. package/src/Drawer/DrawerDirection.test.tsx +127 -0
  214. package/src/Drawer/DrawerHeader.doc.mjs +293 -0
  215. package/src/Drawer/DrawerHeader.test.tsx +102 -0
  216. package/src/Drawer/DrawerHeader.tsx +235 -0
  217. package/src/Drawer/__tests__/DrawerAudit.a11y.chromium.spec.ts +401 -0
  218. package/src/Drawer/__tests__/DrawerHeaderAudit.a11y.chromium.spec.ts +297 -0
  219. package/src/Drawer/index.ts +13 -0
  220. package/src/Drawer/useDrawerDialogPresence.ts +284 -0
  221. package/src/DropdownMenu/DropdownMenu.ssr.test.tsx +57 -0
  222. package/src/DropdownMenu/DropdownMenu.test.tsx +79 -18
  223. package/src/DropdownMenu/DropdownMenu.tsx +83 -12
  224. package/src/DropdownMenu/DropdownMenuContext.tsx +24 -0
  225. package/src/DropdownMenu/DropdownMenuDivider.doc.mjs +2 -2
  226. package/src/DropdownMenu/DropdownMenuDivider.spec.md +192 -0
  227. package/src/DropdownMenu/DropdownMenuDivider.tsx +9 -5
  228. package/src/DropdownMenu/DropdownMenuGroup.doc.mjs +169 -10
  229. package/src/DropdownMenu/DropdownMenuGroup.tsx +6 -4
  230. package/src/DropdownMenu/DropdownMenuLazySheet.test.tsx +126 -0
  231. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +185 -0
  232. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +19 -5
  233. package/src/DropdownMenu/__tests__/DropdownMenuDividerAudit.a11y.chromium.spec.ts +280 -0
  234. package/src/DropdownMenu/__tests__/DropdownMenuGroupAudit.a11y.chromium.spec.ts +334 -0
  235. package/src/DropdownMenu/__tests__/Menu.a11y.chromium.spec.ts +145 -0
  236. package/src/DropdownMenu/__tests__/Menu.a11y.known-failures.ts +26 -0
  237. package/src/DropdownMenu/__tests__/Menu.a11y.renders.tsx +65 -0
  238. package/src/DropdownMenu/__tests__/Menu.a11y.states.ts +138 -0
  239. package/src/DropdownMenu/__tests__/Menu.a11y.test.tsx +136 -0
  240. package/src/DropdownMenu/__tests__/warmMenuBottomSheet.tsx +43 -0
  241. package/src/DropdownMenu/lazyMenuBottomSheet.ts +71 -0
  242. package/src/EmptyState/EmptyState.doc.mjs +3 -2
  243. package/src/EmptyState/EmptyState.spec.md +189 -0
  244. package/src/EmptyState/EmptyState.test.tsx +74 -3
  245. package/src/EmptyState/EmptyState.tsx +4 -8
  246. package/src/Field/Field.doc.mjs +14 -8
  247. package/src/Field/Field.spec.md +217 -0
  248. package/src/Field/Field.test.tsx +60 -2
  249. package/src/Field/Field.tsx +17 -8
  250. package/src/Field/FieldLabel.doc.mjs +133 -26
  251. package/src/Field/FieldLabel.spec.md +172 -0
  252. package/src/Field/__tests__/FieldLabelAudit.a11y.chromium.spec.ts +296 -0
  253. package/src/Grid/GridSpan.tsx +3 -0
  254. package/src/Heading/Heading.test.tsx +57 -0
  255. package/src/Heading/Heading.tsx +6 -2
  256. package/src/HoverCard/HoverCard.test.tsx +58 -0
  257. package/src/HoverCard/useHoverCard.tsx +10 -2
  258. package/src/Icon/Icon.doc.mjs +36 -0
  259. package/src/IconButton/IconButton.doc.mjs +2 -2
  260. package/src/IconButton/IconButton.test.tsx +3 -1
  261. package/src/Item/Item.doc.mjs +12 -8
  262. package/src/Item/Item.spec.md +226 -0
  263. package/src/Item/Item.test.tsx +61 -0
  264. package/src/Item/Item.tsx +40 -4
  265. package/src/Layer/layerViewportInset.source-build.test.mjs +90 -0
  266. package/src/Layer/layerViewportInset.stylex.ts +6 -2
  267. package/src/Layer/layerViewportInset.test.ts +1 -0
  268. package/src/Layer/useLayer.tsx +24 -13
  269. package/src/Layer/useLayerDismissal.ts +1 -1
  270. package/src/Layout/LayoutHeader.doc.mjs +5 -0
  271. package/src/Layout/container.stylex.ts +6 -6
  272. package/src/List/List.doc.mjs +14 -1
  273. package/src/List/List.spec.md +129 -77
  274. package/src/List/List.test.tsx +163 -94
  275. package/src/List/List.tsx +74 -18
  276. package/src/List/ListContext.tsx +9 -3
  277. package/src/List/ListItem.doc.mjs +24 -5
  278. package/src/List/ListItem.tsx +56 -27
  279. package/src/List/index.ts +1 -1
  280. package/src/Markdown/Markdown.tsx +14 -1
  281. package/src/Markdown/plugins/frontmatter.test.tsx +25 -0
  282. package/src/Markdown/plugins/frontmatter.ts +7 -3
  283. package/src/MetadataList/MetadataList.doc.mjs +3 -3
  284. package/src/MetadataList/MetadataList.test.tsx +38 -0
  285. package/src/MetadataList/MetadataList.tsx +8 -1
  286. package/src/MetadataList/MetadataListContext.tsx +1 -1
  287. package/src/MobileNav/MobileNav.tsx +30 -3
  288. package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +173 -15
  289. package/src/MoreMenu/MoreMenu.test.tsx +5 -1
  290. package/src/MultiSelector/MultiSelector.test.tsx +159 -0
  291. package/src/MultiSelector/MultiSelector.tsx +40 -7
  292. package/src/MultiSelector/OptionActions.a11y.chromium.spec.ts +112 -2
  293. package/src/MultiSelector/hooks.ts +3 -1
  294. package/src/MultiSelector/useMultiCombobox.doc.mjs +267 -0
  295. package/src/Pagination/Pagination.doc.mjs +7 -7
  296. package/src/Pagination/Pagination.spec.md +9 -9
  297. package/src/Pagination/Pagination.test.tsx +242 -0
  298. package/src/Pagination/Pagination.tsx +27 -11
  299. package/src/Popover/Popover.test.tsx +2 -2
  300. package/src/Popover/Popover.tsx +4 -4
  301. package/src/PowerSearch/PowerSearch.test.tsx +85 -0
  302. package/src/PowerSearch/PowerSearch.tsx +2 -2
  303. package/src/ProgressBar/ProgressBar.doc.mjs +1 -0
  304. package/src/RadioList/RadioList.fr10.test.tsx +45 -2
  305. package/src/RadioList/RadioList.test.tsx +22 -0
  306. package/src/RadioList/RadioList.tsx +4 -1
  307. package/src/RadioList/RadioListItem.doc.mjs +16 -0
  308. package/src/RadioList/RadioListItem.tsx +10 -1
  309. package/src/Resizable/useResizable.doc.mjs +2 -0
  310. package/src/ScrollableArea/ScrollableArea.doc.mjs +1 -1
  311. package/src/Selector/hooks.ts +6 -2
  312. package/src/Selector/useCombobox.doc.mjs +237 -0
  313. package/src/SideNav/SideNav.spec.md +227 -0
  314. package/src/SideNav/SideNav.test.tsx +179 -0
  315. package/src/SideNav/SideNavItem.doc.mjs +2 -2
  316. package/src/SideNav/SideNavItem.tsx +96 -17
  317. package/src/Slider/Slider.spec.md +39 -18
  318. package/src/Spinner/Spinner.doc.mjs +1 -1
  319. package/src/Switch/Switch.test.tsx +49 -0
  320. package/src/Switch/Switch.tsx +11 -0
  321. package/src/TabList/TabList.spec.md +57 -44
  322. package/src/TabList/TabList.test.tsx +67 -0
  323. package/src/TabList/TabMenu.doc.mjs +4 -3
  324. package/src/TabList/TabMenu.tsx +7 -4
  325. package/src/Text/Text.test.tsx +57 -0
  326. package/src/Text/Text.tsx +6 -2
  327. package/src/Text/useTruncation.test.tsx +69 -0
  328. package/src/Text/useTruncation.ts +16 -5
  329. package/src/TimeInput/TimeInput.fr10.test.tsx +15 -0
  330. package/src/Timestamp/Timestamp.doc.mjs +1 -1
  331. package/src/ToggleButton/ToggleButton.test.tsx +95 -0
  332. package/src/ToggleButton/ToggleButton.tsx +12 -1
  333. package/src/ToggleButton/ToggleButtonGroup.doc.mjs +4 -0
  334. package/src/Tokenizer/Tokenizer.test.tsx +54 -1
  335. package/src/Toolbar/Toolbar.test.tsx +0 -8
  336. package/src/Toolbar/__tests__/Toolbar.a11y.chromium.spec.ts +163 -0
  337. package/src/Toolbar/__tests__/Toolbar.a11y.renders.tsx +63 -0
  338. package/src/Toolbar/__tests__/Toolbar.a11y.states.ts +142 -0
  339. package/src/Toolbar/__tests__/Toolbar.a11y.test.tsx +126 -0
  340. package/src/Tooltip/Tooltip.test.tsx +31 -0
  341. package/src/Tooltip/useTooltip.test.tsx +13 -0
  342. package/src/Tooltip/useTooltip.tsx +11 -0
  343. package/src/TopNav/TopNavMegaMenu.test.tsx +22 -0
  344. package/src/TopNav/TopNavMegaMenu.tsx +7 -3
  345. package/src/TopNav/TopNavMenu.test.tsx +19 -0
  346. package/src/TopNav/TopNavMenu.tsx +11 -3
  347. package/src/TreeList/TreeList.doc.mjs +1 -0
  348. package/src/TreeList/TreeList.spec.md +29 -17
  349. package/src/TreeList/TreeList.test.tsx +327 -6
  350. package/src/TreeList/TreeList.tsx +5 -1
  351. package/src/TreeList/TreeListBranches.tsx +6 -0
  352. package/src/TreeList/TreeListItem.tsx +88 -13
  353. package/src/Typeahead/BaseTypeahead.tsx +26 -0
  354. package/src/Typeahead/Typeahead.test.tsx +260 -2
  355. package/src/docPropLiterals.test.ts +36 -10
  356. package/src/hooks/useEntryAnimation.doc.mjs +4 -4
  357. package/src/hooks/useEntryAnimation.ssr.test.tsx +143 -0
  358. package/src/hooks/useEntryAnimation.ts +15 -16
  359. package/src/hooks/useIsHydrating.test.tsx +70 -0
  360. package/src/hooks/useIsHydrating.ts +31 -0
  361. package/src/hooks/useMenuHover.ts +18 -13
  362. package/src/i18n/InternationalizationProvider.doc.mjs +8 -0
  363. package/src/i18n/InternationalizationProvider.spec.md +252 -0
  364. package/src/i18n/__tests__/InternationalizationProvider.test.tsx +128 -0
  365. package/src/i18n/generated-locales/en.generated.ts +121 -0
  366. package/src/i18n/generated-locales/pseudo.generated.ts +121 -0
  367. package/src/i18n/useTranslator.test.tsx +37 -0
  368. package/src/index.ts +1 -0
  369. package/src/postinstall.test.mjs +46 -0
  370. package/src/reset.css +0 -29
  371. package/src/theme/derivedVarRegistry.test.ts +7 -3
  372. package/src/theme/derivedVarRegistry.ts +0 -3
  373. package/src/utils/focusOutline.stylex.ts +4 -0
  374. package/src/utils/plainDate.test.ts +37 -1
  375. package/src/utils/plainDate.ts +15 -5
  376. package/src/utils/sharedMutationObserver.test.ts +53 -0
  377. package/src/utils/sharedMutationObserver.ts +78 -0
@@ -17,10 +17,20 @@ import {
17
17
  beforeAll,
18
18
  afterAll,
19
19
  beforeEach,
20
+ afterEach,
20
21
  } from 'vitest';
21
22
  import {render, screen, fireEvent, waitFor, act} from '@testing-library/react';
22
23
  import userEvent from '@testing-library/user-event';
23
- import {createRef, Profiler, type ProfilerOnRenderCallback} from 'react';
24
+ import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
25
+ import {
26
+ createRef,
27
+ Profiler,
28
+ startTransition,
29
+ StrictMode,
30
+ Suspense,
31
+ useState,
32
+ type ProfilerOnRenderCallback,
33
+ } from 'react';
24
34
  import {Typeahead} from './Typeahead';
25
35
  import {readFile} from 'node:fs/promises';
26
36
  import {resolve} from 'node:path';
@@ -70,6 +80,10 @@ afterAll(() => {
70
80
  (HTMLElement.prototype as any).matches = originalMatches;
71
81
  });
72
82
 
83
+ afterEach(() => {
84
+ __resetLiveRegionsForTest();
85
+ });
86
+
73
87
  // Test data
74
88
  const fruits: SearchableItem[] = [
75
89
  {id: '1', label: 'Apple'},
@@ -964,6 +978,193 @@ describe('BaseTypeahead hasEntriesOnFocus', () => {
964
978
  ).not.toBeInTheDocument();
965
979
  });
966
980
 
981
+ it('clears the loading state when the search source is replaced during an in-flight load', async () => {
982
+ let settle: (items: SearchableItem[]) => void = () => {};
983
+ const bootstrap = vi.fn(
984
+ async () =>
985
+ new Promise<SearchableItem[]>(resolve => {
986
+ settle = resolve;
987
+ }),
988
+ );
989
+ const initialSource: SearchSource = {search: () => [], bootstrap};
990
+ const replacementSource: SearchSource = {
991
+ search: () => [],
992
+ bootstrap: () => [],
993
+ };
994
+
995
+ const {rerender} = render(
996
+ <BaseTypeahead
997
+ searchSource={initialSource}
998
+ value={null}
999
+ onChange={() => {}}
1000
+ hasEntriesOnFocus
1001
+ />,
1002
+ );
1003
+ const input = screen.getByRole('combobox');
1004
+
1005
+ fireEvent.focus(input);
1006
+ await waitFor(() => {
1007
+ expect(screen.getByRole('status', {name: 'Loading'})).toBeInTheDocument();
1008
+ });
1009
+
1010
+ rerender(
1011
+ <BaseTypeahead
1012
+ searchSource={replacementSource}
1013
+ value={null}
1014
+ onChange={() => {}}
1015
+ hasEntriesOnFocus
1016
+ />,
1017
+ );
1018
+
1019
+ await waitFor(() => {
1020
+ expect(
1021
+ screen.queryByRole('status', {name: 'Loading'}),
1022
+ ).not.toBeInTheDocument();
1023
+ });
1024
+
1025
+ await act(async () => {
1026
+ settle([]);
1027
+ await Promise.resolve();
1028
+ });
1029
+ expect(
1030
+ screen.queryByRole('status', {name: 'Loading'}),
1031
+ ).not.toBeInTheDocument();
1032
+ });
1033
+
1034
+ it('keeps the loading state under StrictMode for a pending autofocus bootstrap', async () => {
1035
+ let settle: (items: SearchableItem[]) => void = () => {};
1036
+ const bootstrap = vi.fn(
1037
+ async () =>
1038
+ new Promise<SearchableItem[]>(resolve => {
1039
+ settle = resolve;
1040
+ }),
1041
+ );
1042
+ render(
1043
+ <StrictMode>
1044
+ <Typeahead
1045
+ label="Test"
1046
+ searchSource={{search: () => [], bootstrap}}
1047
+ value={null}
1048
+ onChange={() => {}}
1049
+ hasAutoFocus
1050
+ hasEntriesOnFocus
1051
+ />
1052
+ </StrictMode>,
1053
+ );
1054
+
1055
+ expect(screen.getByRole('status', {name: 'Loading'})).toBeInTheDocument();
1056
+
1057
+ await act(async () => {
1058
+ settle([]);
1059
+ await Promise.resolve();
1060
+ });
1061
+ expect(
1062
+ screen.queryByRole('status', {name: 'Loading'}),
1063
+ ).not.toBeInTheDocument();
1064
+ });
1065
+
1066
+ let settlePendingTransitionA: (items: SearchableItem[]) => void = () => {};
1067
+ const stableTransitionSourceA: SearchSource = {
1068
+ search: vi.fn(
1069
+ async () =>
1070
+ new Promise<SearchableItem[]>(resolve => {
1071
+ settlePendingTransitionA = resolve;
1072
+ }),
1073
+ ),
1074
+ bootstrap: () => [],
1075
+ };
1076
+ const stableTransitionSourceB: SearchSource = {
1077
+ search: vi.fn(async () => []),
1078
+ bootstrap: () => [],
1079
+ };
1080
+
1081
+ it('clears loading and aria-busy when source is replaced in a transition that suspends and retries', async () => {
1082
+ let isSuspended = false;
1083
+ let resolveSuspension: () => void = () => {};
1084
+ let pendingSuspension: Promise<void> | null = null;
1085
+
1086
+ function SuspendingSibling({shouldSuspend}: {shouldSuspend: boolean}) {
1087
+ if (shouldSuspend && isSuspended && pendingSuspension) {
1088
+ // eslint-disable-next-line @typescript-eslint/only-throw-error
1089
+ throw pendingSuspension;
1090
+ }
1091
+ return null;
1092
+ }
1093
+
1094
+ let updateState!: (source: SearchSource, shouldSuspend: boolean) => void;
1095
+
1096
+ function TransitionContainer() {
1097
+ const [source, setSource] = useState<SearchSource>(
1098
+ stableTransitionSourceA,
1099
+ );
1100
+ const [shouldSuspend, setShouldSuspend] = useState(false);
1101
+ updateState = (newSource, suspend) => {
1102
+ setSource(newSource);
1103
+ setShouldSuspend(suspend);
1104
+ };
1105
+
1106
+ return (
1107
+ <Suspense fallback={<div>Suspended...</div>}>
1108
+ <Typeahead
1109
+ label="Test"
1110
+ searchSource={source}
1111
+ value={null}
1112
+ onChange={() => {}}
1113
+ debounceMs={0}
1114
+ />
1115
+ <SuspendingSibling shouldSuspend={shouldSuspend} />
1116
+ </Suspense>
1117
+ );
1118
+ }
1119
+
1120
+ render(<TransitionContainer />);
1121
+ const input = screen.getByRole('combobox');
1122
+
1123
+ // Trigger a load so aria-busy="true" is set on the combobox
1124
+ fireEvent.change(input, {target: {value: 'query'}});
1125
+ await waitFor(() => {
1126
+ expect(input).toHaveAttribute('aria-busy', 'true');
1127
+ });
1128
+ expect(screen.getByRole('status', {name: 'Loading'})).toBeInTheDocument();
1129
+
1130
+ // Create the pending promise for suspension
1131
+ isSuspended = true;
1132
+ pendingSuspension = new Promise<void>(resolve => {
1133
+ resolveSuspension = () => {
1134
+ isSuspended = false;
1135
+ resolve();
1136
+ };
1137
+ });
1138
+
1139
+ // Inside startTransition, switch from source A to source B while sibling suspends
1140
+ act(() => {
1141
+ startTransition(() => {
1142
+ updateState(stableTransitionSourceB, true);
1143
+ });
1144
+ });
1145
+
1146
+ // The render suspended, so the previous committed state remains
1147
+ expect(input).toHaveAttribute('aria-busy', 'true');
1148
+
1149
+ // Resolve the pending promise and let the transition commit
1150
+ await act(async () => {
1151
+ resolveSuspension();
1152
+ await pendingSuspension;
1153
+ });
1154
+
1155
+ // Settle the abandoned load
1156
+ await act(async () => {
1157
+ settlePendingTransitionA([]);
1158
+ await Promise.resolve();
1159
+ });
1160
+
1161
+ // The combobox must not keep aria-busy and the spinner must be gone
1162
+ expect(input).not.toHaveAttribute('aria-busy');
1163
+ expect(
1164
+ screen.queryByRole('status', {name: 'Loading'}),
1165
+ ).not.toBeInTheDocument();
1166
+ });
1167
+
967
1168
  it('shows bootstrap results on mouse click', async () => {
968
1169
  render(
969
1170
  <BaseTypeahead
@@ -1323,6 +1524,63 @@ describe('BaseTypeahead hasSearched reset', () => {
1323
1524
  // "No results found" should disappear since hasSearched is reset
1324
1525
  expect(screen.queryByText('No results found')).not.toBeInTheDocument();
1325
1526
  });
1527
+
1528
+ it('resets hasSearched and does not announce "No results found" when search source is replaced during a pending search', async () => {
1529
+ let settle: (items: SearchableItem[]) => void = () => {};
1530
+ const initialSource: SearchSource = {
1531
+ search: vi.fn(
1532
+ async () =>
1533
+ new Promise<SearchableItem[]>(resolve => {
1534
+ settle = resolve;
1535
+ }),
1536
+ ),
1537
+ bootstrap: () => [],
1538
+ };
1539
+ const replacementSource: SearchSource = {
1540
+ search: vi.fn(async () => []),
1541
+ bootstrap: () => [],
1542
+ };
1543
+
1544
+ const {rerender} = render(
1545
+ <BaseTypeahead
1546
+ searchSource={initialSource}
1547
+ value={null}
1548
+ onChange={() => {}}
1549
+ debounceMs={0}
1550
+ />,
1551
+ );
1552
+ const input = screen.getByRole('combobox');
1553
+
1554
+ // Type a query that starts an asynchronous search
1555
+ fireEvent.change(input, {target: {value: 'abc'}});
1556
+ expect(initialSource.search).toHaveBeenCalledWith('abc');
1557
+
1558
+ // Replace the search source while the search is still pending
1559
+ rerender(
1560
+ <BaseTypeahead
1561
+ searchSource={replacementSource}
1562
+ value={null}
1563
+ onChange={() => {}}
1564
+ debounceMs={0}
1565
+ />,
1566
+ );
1567
+
1568
+ // Settle the abandoned search
1569
+ await act(async () => {
1570
+ settle([]);
1571
+ await Promise.resolve();
1572
+ });
1573
+
1574
+ await act(
1575
+ async () =>
1576
+ new Promise<void>(resolve => requestAnimationFrame(() => resolve())),
1577
+ );
1578
+ expect(
1579
+ document.querySelector('[data-astryx-live-region="polite"]')
1580
+ ?.textContent ?? '',
1581
+ ).not.toContain('No results found');
1582
+ expect(screen.queryByText('No results found')).not.toBeInTheDocument();
1583
+ });
1326
1584
  });
1327
1585
 
1328
1586
  describe('BaseTypeahead popover after selection', () => {
@@ -2313,7 +2571,7 @@ describe('Typeahead consumer key handler order', () => {
2313
2571
  currentTarget = event.currentTarget;
2314
2572
  event.preventDefault();
2315
2573
  });
2316
- const {container} = render(
2574
+ render(
2317
2575
  <Typeahead
2318
2576
  label="Fruit"
2319
2577
  searchSource={fruitSource}
@@ -3,7 +3,8 @@
3
3
  /* eslint-disable @typescript-eslint/no-require-imports */
4
4
  /**
5
5
  * @file Guards doc prop types against opaque named unions (#1645).
6
- * @input Every {Name}.doc.mjs plus the TypeScript sources they describe.
6
+ * @input Every {Name}.doc.mjs plus the TypeScript sources they describe,
7
+ * including same-file private aliases used by exported literal unions.
7
8
  * @output Fails when a documented prop type hides its legal literal values.
8
9
  * @position Repo-wide doc-shape guard, sibling of docPropReferences.test.ts.
9
10
  *
@@ -188,18 +189,34 @@ for (const file of findFiles(SRC_DIR, ['.ts', '.tsx'])) {
188
189
  // markers the comment-stripped `src` has already removed.
189
190
  const rawTypeRhs = new Map<string, string>();
190
191
  for (const m of raw.matchAll(
191
- /export type ([A-Z][A-Za-z0-9]*)(?:<[^>]*>)?\s*=\s*([^;]+);/g,
192
+ /\btype ([A-Z][A-Za-z0-9]*)(?:<[^>]*>)?\s*=\s*([^;]+);/g,
192
193
  )) {
193
194
  rawTypeRhs.set(m[1], m[2]);
194
195
  }
195
- for (const m of src.matchAll(
196
- /export type ([A-Z][A-Za-z0-9]*)(?:<[^>]*>)?\s*=\s*([^;]+);/g,
197
- )) {
198
- const [, name, rhsRaw] = m;
199
- const rhs = rhsRaw.replace(/\s+/g, ' ').trim();
200
- if (LITERAL_UNION.test(rhs)) {
196
+ const declarations = [
197
+ ...src.matchAll(
198
+ /\b(export )?type ([A-Z][A-Za-z0-9]*)(?:<[^>]*>)?\s*=\s*([^;]+);/g,
199
+ ),
200
+ ];
201
+ // Resolve same-file literal aliases before registering exported types.
202
+ // Repeat until no new literal unions are found.
203
+ const fileLiterals = new Map<string, string>();
204
+ let previousSize: number;
205
+ do {
206
+ previousSize = fileLiterals.size;
207
+ for (const [, , name, rhsRaw] of declarations) {
208
+ const rhs = rhsRaw
209
+ .replace(/\s+/g, ' ')
210
+ .trim()
211
+ .split('|')
212
+ .map(part => fileLiterals.get(part.trim()) ?? part.trim())
213
+ .join(' | ');
214
+
215
+ if (!LITERAL_UNION.test(rhs)) {
216
+ continue;
217
+ }
201
218
  const deprecated = deprecatedLiteralMembers(rawTypeRhs.get(name) ?? '');
202
- literalUnions.set(
219
+ fileLiterals.set(
203
220
  name,
204
221
  rhs
205
222
  .replace(/^\|\s*/, '')
@@ -209,8 +226,17 @@ for (const file of findFiles(SRC_DIR, ['.ts', '.tsx'])) {
209
226
  .filter(member => !deprecated.has(member))
210
227
  .join(' | '),
211
228
  );
229
+ }
230
+ } while (fileLiterals.size !== previousSize);
231
+ for (const [, exported, name, rhsRaw] of declarations) {
232
+ if (!exported) {
233
+ continue;
234
+ }
235
+ const literals = fileLiterals.get(name);
236
+ if (literals !== undefined) {
237
+ literalUnions.set(name, literals);
212
238
  } else {
213
- namedBodies.set(name, rhs);
239
+ namedBodies.set(name, rhsRaw.replace(/\s+/g, ' ').trim());
214
240
  }
215
241
  }
216
242
  for (const m of src.matchAll(
@@ -18,12 +18,12 @@ export const docs = {
18
18
  {
19
19
  name: 'entryStyle',
20
20
  type: 'StyleXStyles | null',
21
- description: 'A StyleX style object for the entry animation, or null if the element was rendered on initial page load (no animation needed).',
21
+ description: 'A StyleX style object for the entry animation, or null if the element was rendered on initial page load or hydrated from server HTML (no animation needed).',
22
22
  },
23
23
  ],
24
24
  usage: {
25
25
  description:
26
- 'Returns a StyleX style for animating an element on mount. Only animates when the element is dynamically inserted after the initial page paint; elements rendered on page load are not animated. Uses Astryx motion tokens (duration, easing) for consistent animation timing. Requires "use client"; does not support SSR.',
26
+ 'Returns a StyleX style for animating an element on mount. Only animates when the element is dynamically inserted after the initial page paint; elements rendered on page load are not animated. Uses Astryx motion tokens (duration, easing) for consistent animation timing. Server-rendered elements hydrate without the entry style and never animate; elements mounted after hydration animate.',
27
27
  bestPractices: [
28
28
  { guidance: true, description: 'Use for conditionally rendered elements like validation messages, toasts, or expanding sections.' },
29
29
  { guidance: true, description: 'Spread the returned style into stylex.props() alongside other styles.' },
@@ -39,7 +39,7 @@ export const docs = {
39
39
  /** @type {import('@astryxdesign/cli/authoring').HookTranslationDoc} */
40
40
  export const docsDense = {
41
41
  description:
42
- 'Returns StyleX style for animating element on mount. Only animates when element dynamically inserted after initial page paint; elements rendered on page load not animated. Uses Astryx motion tokens (duration, easing) for consistent timing. Requires "use client"; does not support SSR.',
42
+ 'Returns StyleX style for animating element on mount. Only animates when element dynamically inserted after initial page paint; elements rendered on page load not animated. Uses Astryx motion tokens (duration, easing) for consistent timing. Server-rendered elements hydrate without entry style, never animate; elements mounted after hydration animate.',
43
43
  paramDescriptions: {
44
44
  preset: 'animation preset applied on mount.',
45
45
  },
@@ -48,7 +48,7 @@ export const docsDense = {
48
48
  },
49
49
  usage: {
50
50
  description:
51
- 'Returns StyleX style for animating element on mount. Only animates when element dynamically inserted after initial page paint; elements rendered on page load not animated. Uses Astryx motion tokens (duration, easing) for consistent timing. Requires "use client"; does not support SSR.',
51
+ 'Returns StyleX style for animating element on mount. Only animates when element dynamically inserted after initial page paint; elements rendered on page load not animated. Uses Astryx motion tokens (duration, easing) for consistent timing. Server-rendered elements hydrate without entry style, never animate; elements mounted after hydration animate.',
52
52
  bestPractices: [
53
53
  { guidance: true, description: 'Use for conditionally rendered elements like validation messages, toasts, expanding sections.' },
54
54
  { guidance: true, description: 'Spread returned style into stylex.props() alongside other styles.' },
@@ -0,0 +1,143 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file useEntryAnimation.ssr.test.tsx
5
+ * @input FieldStatus server markup, React hydration, and client mounts
6
+ * @output Proves server-rendered entry-animated elements hydrate without mismatches
7
+ * @position SSR acceptance test for useEntryAnimation
8
+ */
9
+
10
+ import {act, useState} from 'react';
11
+ import {afterEach, describe, expect, it, vi} from 'vitest';
12
+ import {createRoot, hydrateRoot} from 'react-dom/client';
13
+ import {renderToString} from 'react-dom/server';
14
+
15
+ // Each test imports a fresh copy of FieldStatus with the hook's first-frame
16
+ // callback held back, so server markup renders before the page "paints" (as on
17
+ // a real server) and `paint()` then runs that frame.
18
+ async function importFieldStatus() {
19
+ const frames: FrameRequestCallback[] = [];
20
+ vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
21
+ frames.push(callback),
22
+ );
23
+ vi.resetModules();
24
+ const {FieldStatus} = await import('../FieldStatus');
25
+ vi.unstubAllGlobals();
26
+ const paint = () => {
27
+ frames.splice(0).forEach(callback => callback(performance.now()));
28
+ };
29
+ return {FieldStatus, paint};
30
+ }
31
+
32
+ function mountServerHTML(html: string): HTMLDivElement {
33
+ const container = document.createElement('div');
34
+ container.innerHTML = html;
35
+ document.body.appendChild(container);
36
+ return container;
37
+ }
38
+
39
+ function classesOf(element: Element | null): string[] {
40
+ return (element?.getAttribute('class') ?? '').split(/\s+/).filter(Boolean);
41
+ }
42
+
43
+ afterEach(() => {
44
+ vi.restoreAllMocks();
45
+ vi.unstubAllGlobals();
46
+ document.body.replaceChildren();
47
+ });
48
+
49
+ describe('useEntryAnimation — SSR', () => {
50
+ it('hydrates server-rendered FieldStatus after the first paint without a mismatch', async () => {
51
+ const {FieldStatus, paint} = await importFieldStatus();
52
+ const tree = <FieldStatus type="error" message="x" />;
53
+ const serverHTML = renderToString(tree);
54
+ const container = mountServerHTML(serverHTML);
55
+ const serverClassName = container.firstElementChild?.className;
56
+ paint();
57
+
58
+ const consoleError = vi
59
+ .spyOn(console, 'error')
60
+ .mockImplementation(() => {});
61
+ const recoverableErrors: unknown[] = [];
62
+ let root!: ReturnType<typeof hydrateRoot>;
63
+ await act(async () => {
64
+ root = hydrateRoot(container, tree, {
65
+ onRecoverableError: error => recoverableErrors.push(error),
66
+ });
67
+ });
68
+
69
+ expect(
70
+ consoleError.mock.calls.filter(call =>
71
+ String(call[0] ?? '')
72
+ .toLowerCase()
73
+ .includes('hydrat'),
74
+ ),
75
+ ).toEqual([]);
76
+ expect(recoverableErrors).toEqual([]);
77
+ expect(container.firstElementChild?.className).toBe(serverClassName);
78
+
79
+ await act(async () => {
80
+ root.unmount();
81
+ });
82
+ });
83
+
84
+ it('still animates a client-only mount after the first paint', async () => {
85
+ const {FieldStatus, paint} = await importFieldStatus();
86
+ const staticClasses = classesOf(
87
+ mountServerHTML(renderToString(<FieldStatus type="error" message="x" />))
88
+ .firstElementChild,
89
+ );
90
+ paint();
91
+
92
+ const container = document.createElement('div');
93
+ document.body.appendChild(container);
94
+ const root = createRoot(container);
95
+ await act(async () => {
96
+ root.render(<FieldStatus type="error" message="x" />);
97
+ });
98
+
99
+ const clientClasses = classesOf(container.firstElementChild);
100
+ expect(clientClasses).toEqual(expect.arrayContaining(staticClasses));
101
+ expect(clientClasses.length).toBeGreaterThan(staticClasses.length);
102
+
103
+ await act(async () => {
104
+ root.unmount();
105
+ });
106
+ });
107
+
108
+ it('animates a FieldStatus mounted after hydration', async () => {
109
+ const {FieldStatus, paint} = await importFieldStatus();
110
+ let showSecond!: () => void;
111
+ function Form() {
112
+ const [second, setSecond] = useState(false);
113
+ showSecond = () => setSecond(true);
114
+ return (
115
+ <>
116
+ <FieldStatus type="error" message="first" />
117
+ {second ? <FieldStatus type="error" message="second" /> : null}
118
+ </>
119
+ );
120
+ }
121
+ const container = mountServerHTML(renderToString(<Form />));
122
+ const staticClasses = classesOf(container.firstElementChild);
123
+ paint();
124
+
125
+ let root!: ReturnType<typeof hydrateRoot>;
126
+ await act(async () => {
127
+ root = hydrateRoot(container, <Form />);
128
+ });
129
+ await act(async () => {
130
+ showSecond();
131
+ });
132
+
133
+ const mounted = container.children[1] ?? null;
134
+ expect(mounted).toHaveTextContent('second');
135
+ const mountedClasses = classesOf(mounted);
136
+ expect(mountedClasses).toEqual(expect.arrayContaining(staticClasses));
137
+ expect(mountedClasses.length).toBeGreaterThan(staticClasses.length);
138
+
139
+ await act(async () => {
140
+ root.unmount();
141
+ });
142
+ });
143
+ });
@@ -3,29 +3,27 @@
3
3
  'use client';
4
4
  /**
5
5
  * @file useEntryAnimation.ts
6
- * @input Uses React, StyleX, theme tokens
6
+ * @input Uses React, StyleX, theme tokens, useIsHydrating
7
7
  * @output Exports useEntryAnimation hook for mount-only entry animations
8
8
  * @position Hook utility; consumed by FieldStatus and available to consumers
9
9
  *
10
10
  * Provides entry animations for conditionally rendered elements.
11
11
  * Only animates when the element is dynamically mounted after the initial
12
12
  * page paint — statically rendered elements on page load are not animated.
13
+ * Server-rendered elements hydrate without the entry style and never play it
14
+ * (spec:AST-013 FR13).
13
15
  */
14
16
 
15
17
  import {useState} from 'react';
16
18
  import * as stylex from '@stylexjs/stylex';
17
19
  import type {StyleXStyles} from '@stylexjs/stylex';
18
20
  import {durationVars, easeVars, spacingVars} from '../theme/tokens.stylex';
21
+ import {useIsHydrating} from './useIsHydrating';
19
22
 
20
- // Track whether the initial page paint has completed.
21
- // Elements rendered on page load should not animate;
22
- // only dynamically inserted ones should.
23
- //
24
- // NOTE: This module is marked 'use client' so it never runs on the server.
25
- // If the directive is removed, this flag will always be false during SSR,
26
- // and useState(() => initialPaintComplete) will capture false — meaning
27
- // animations won't run after hydration. Keep 'use client' or add an
28
- // effect-based fallback if SSR support is needed.
23
+ // Track whether the initial page paint has completed, so elements a
24
+ // client-only root renders on page load do not animate; only dynamically
25
+ // inserted ones should. Server-rendered elements are covered by
26
+ // useIsHydrating instead: hydration can run after this flag turns true.
29
27
  let initialPaintComplete = false;
30
28
  if (typeof window !== 'undefined') {
31
29
  requestAnimationFrame(() => {
@@ -99,16 +97,14 @@ const styles = stylex.create({
99
97
  });
100
98
 
101
99
  export type EntryAnimationPreset =
102
- | 'slideDown'
103
- | 'slideUp'
104
- | 'fadeIn'
105
- | 'scaleIn';
100
+ 'slideDown' | 'slideUp' | 'fadeIn' | 'scaleIn';
106
101
 
107
102
  /**
108
103
  * Returns a StyleX style for animating an element on mount.
109
104
  *
110
105
  * Only animates when the element is dynamically inserted after the initial
111
- * page paint. Elements rendered on page load are not animated.
106
+ * page paint. Elements rendered on page load, including server-rendered
107
+ * elements while they hydrate, are not animated.
112
108
  *
113
109
  * @example
114
110
  * ```
@@ -119,6 +115,9 @@ export type EntryAnimationPreset =
119
115
  export function useEntryAnimation(
120
116
  preset: EntryAnimationPreset = 'slideDown',
121
117
  ): StyleXStyles | null {
122
- const [animate] = useState(() => initialPaintComplete);
118
+ const isHydrating = useIsHydrating();
119
+ // Captured once: a hydrated element must not start animating when React
120
+ // re-renders it with the client snapshot after hydration.
121
+ const [animate] = useState(() => !isHydrating && initialPaintComplete);
123
122
  return animate ? styles[preset] : null;
124
123
  }
@@ -0,0 +1,70 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file useIsHydrating.test.tsx
5
+ * @input useIsHydrating, React server render, hydration, and client roots
6
+ * @output Proves the hook is true only on the server and while hydrating
7
+ * @position Unit test for the internal hydration-state utility
8
+ */
9
+
10
+ import {act, useState} from 'react';
11
+ import {afterEach, describe, expect, it} from 'vitest';
12
+ import {createRoot, hydrateRoot} from 'react-dom/client';
13
+ import {renderToString} from 'react-dom/server';
14
+ import {useIsHydrating} from './useIsHydrating';
15
+
16
+ const seen: boolean[] = [];
17
+
18
+ function Probe() {
19
+ const isHydrating = useIsHydrating();
20
+ seen.push(isHydrating);
21
+ return <span data-hydrating={String(isHydrating)} />;
22
+ }
23
+
24
+ afterEach(() => {
25
+ seen.length = 0;
26
+ document.body.replaceChildren();
27
+ });
28
+
29
+ describe('useIsHydrating', () => {
30
+ it('is true on the server', () => {
31
+ expect(renderToString(<Probe />)).toContain('data-hydrating="true"');
32
+ });
33
+
34
+ it('is true while hydrating, then false after hydration', async () => {
35
+ const container = document.createElement('div');
36
+ container.innerHTML = renderToString(<Probe />);
37
+ document.body.appendChild(container);
38
+ seen.length = 0;
39
+
40
+ let root!: ReturnType<typeof hydrateRoot>;
41
+ await act(async () => {
42
+ root = hydrateRoot(container, <Probe />);
43
+ });
44
+
45
+ expect(seen[0]).toBe(true);
46
+ expect(seen.at(-1)).toBe(false);
47
+ expect(container.querySelector('span')).toHaveAttribute(
48
+ 'data-hydrating',
49
+ 'false',
50
+ );
51
+ await act(async () => root.unmount());
52
+ });
53
+
54
+ it('is false in a client-only root and for elements mounted after hydration', async () => {
55
+ let show!: () => void;
56
+ function Later() {
57
+ const [isShown, setIsShown] = useState(false);
58
+ show = () => setIsShown(true);
59
+ return isShown ? <Probe /> : null;
60
+ }
61
+ const container = document.createElement('div');
62
+ document.body.appendChild(container);
63
+ const root = createRoot(container);
64
+ await act(async () => root.render(<Later />));
65
+ await act(async () => show());
66
+
67
+ expect(seen).toEqual([false]);
68
+ await act(async () => root.unmount());
69
+ });
70
+ });