@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
@@ -4,9 +4,10 @@
4
4
 
5
5
  /**
6
6
  * @file ListItem.tsx
7
- * @input Uses React, ReactNode, StyleXStyles, theme tokens, List edge compensation,
8
- * CheckboxInput, and the marker or task checkbox a ListMarkerScope sets
9
- * (Markdown's nested lists and the task items of mixed lists)
7
+ * @input Uses React, ReactNode, StyleXStyles, theme tokens, the List row edge
8
+ * treatment, CheckboxInput, and the marker or task checkbox a
9
+ * ListMarkerScope sets (Markdown's nested lists and the task items of mixed
10
+ * lists)
10
11
  * @output Exports ListItem component, ListItemProps type
11
12
  * @position Core implementation; consumed by List, index.ts, tested by List.test.tsx
12
13
  *
@@ -23,6 +24,7 @@
23
24
 
24
25
  import {use, type ReactNode} from 'react';
25
26
  import * as stylex from '@stylexjs/stylex';
27
+ import type {StyleXStyles} from '@stylexjs/stylex';
26
28
  import {
27
29
  colorVars,
28
30
  spacingVars,
@@ -34,7 +36,7 @@ import {ListContext, type ListMarker} from './ListContext';
34
36
  import {CheckboxInput} from '../CheckboxInput';
35
37
  import {mergeProps} from '../utils';
36
38
  import {Item} from '../Item';
37
- import type {ItemSwipeActions, ItemSwipeBehavior} from '../Item';
39
+ import type {ItemAlign, ItemSwipeActions, ItemSwipeBehavior} from '../Item';
38
40
  import {themeProps} from '../utils/themeProps';
39
41
 
40
42
  // =============================================================================
@@ -44,6 +46,13 @@ import {themeProps} from '../utils/themeProps';
44
46
  export interface ListItemProps extends BaseProps<HTMLLIElement> {
45
47
  /** Ref forwarded to the root element */
46
48
  ref?: React.Ref<HTMLLIElement>;
49
+ /**
50
+ * Vertical alignment of the start and end content against the label,
51
+ * forwarded to Item. Use `first-line` to keep the start and end content level with the
52
+ * first line of a wrapped label.
53
+ * @default 'center'
54
+ */
55
+ align?: ItemAlign;
47
56
  /**
48
57
  * Primary text label for the item.
49
58
  *
@@ -142,23 +151,37 @@ const styles = stylex.create({
142
151
  withCounter: {
143
152
  counterIncrement: 'astryx-list',
144
153
  },
145
- // List's inline edge compensation cancels as much of the row's built-in
146
- // inline inset as the container padding allows (e.g. under a heading).
147
- // The margins read --_item-inset-inline — the same variable Item derives
148
- // its paddingInline from — on the row element itself (custom properties
149
- // only cascade downward, so the <ul> could not read it). Density changes
150
- // and theme paddingInline overrides on `item` move both values together.
151
- // Each edge clamps against ITS OWN container padding var: a single
152
- // start-var clamp on both margins over-cancels the end edge under
153
- // asymmetric container padding (16px start / 4px end) and the selected
154
- // row paints past the outer border. Logical properties keep RTL correct,
155
- // and a zero-padding/full-bleed surface (min(inset, 0px) = 0px) leaves
156
- // the row in place instead of pulling it outside its content edge.
157
- inlineEdgeCompensation: {
154
+ // List's edge treatments. Each reads --_item-inset-inline, the variable
155
+ // Item derives its paddingInline from, on the row itself (custom properties
156
+ // only cascade downward, so the <ul> cannot read it); density and theme
157
+ // paddingInline overrides on `item` move it. Each edge resolves against ITS
158
+ // OWN container padding var, so asymmetric padding never over-reaches an
159
+ // edge, and logical properties keep RTL correct.
160
+ //
161
+ // 'flush': no inset, so content meets the row's edges.
162
+ edgeFlush: {
163
+ '--_item-inset-inline': '0px',
164
+ },
165
+ // 'outset': the list reaches the container's padding edge (List.tsx); the
166
+ // row steps back in by whatever padding its inset does not fill, so the row
167
+ // reaches min(inset, container padding) past the content line and its text
168
+ // lands on that line when the padding is at least the inset.
169
+ edgeOutset: {
158
170
  marginInlineStart:
159
- 'calc(-1 * min(var(--_item-inset-inline), var(--container-padding-inline-start, 0px)))',
171
+ 'max(var(--container-padding-inline-start, 0px) - var(--_item-inset-inline), 0px)',
160
172
  marginInlineEnd:
161
- 'calc(-1 * min(var(--_item-inset-inline), var(--container-padding-inline-end, 0px)))',
173
+ 'max(var(--container-padding-inline-end, 0px) - var(--_item-inset-inline), 0px)',
174
+ },
175
+ // 'bleed': the row fills the list, which reaches the container's padding
176
+ // edge (List.tsx); its text sits on the container's padding line, never
177
+ // closer to the row's edge than its own inset. Square corners: the row
178
+ // meets the container's straight sides.
179
+ edgeBleed: {
180
+ paddingInlineStart:
181
+ 'max(var(--container-padding-inline-start, 0px), var(--_item-inset-inline))',
182
+ paddingInlineEnd:
183
+ 'max(var(--container-padding-inline-end, 0px), var(--_item-inset-inline))',
184
+ borderRadius: 0,
162
185
  },
163
186
  withDivider: {
164
187
  borderBlockEndWidth: borderVars['--border-width'],
@@ -296,6 +319,7 @@ export function ListItem({
296
319
  rel,
297
320
  isDisabled = false,
298
321
  isSelected = false,
322
+ align,
299
323
  xstyle,
300
324
  className,
301
325
  style,
@@ -305,8 +329,8 @@ export function ListItem({
305
329
  const ctx = use(ListContext);
306
330
  const density = ctx?.density ?? 'balanced';
307
331
  const hasDividers = ctx?.hasDividers ?? false;
332
+ const edge = ctx?.edge ?? 'padded';
308
333
  const listStyle = ctx?.listStyle ?? 'none';
309
- const edgeCompensation = ctx?.edgeCompensation;
310
334
  const hasMarkers = listStyle !== 'none';
311
335
  // ListMarkerScope may name another marker for this item.
312
336
  const markerKind: ListMarker | null =
@@ -360,14 +384,19 @@ export function ListItem({
360
384
  rel={rel}
361
385
  isDisabled={isDisabled}
362
386
  isSelected={isSelected}
387
+ align={align}
363
388
  density={density}
364
- xstyle={[
365
- hasMarkers && styles.withCounter,
366
- hasDividers && styles.withDivider,
367
- hasDividers && embeddedStyles.noRadius,
368
- edgeCompensation === 'inline' && styles.inlineEdgeCompensation,
369
- xstyle,
370
- ]}
389
+ xstyle={
390
+ [
391
+ hasMarkers && styles.withCounter,
392
+ hasDividers && styles.withDivider,
393
+ hasDividers && embeddedStyles.noRadius,
394
+ edge === 'flush' && styles.edgeFlush,
395
+ edge === 'outset' && styles.edgeOutset,
396
+ edge === 'bleed' && styles.edgeBleed,
397
+ xstyle,
398
+ ] as StyleXStyles
399
+ }
371
400
  {...mergeProps(themeProps('list-item'), {className, style})}
372
401
  {...restProps}
373
402
  />
package/src/List/index.ts CHANGED
@@ -12,6 +12,6 @@
12
12
  */
13
13
 
14
14
  export {List} from './List';
15
- export type {ListProps, ListStyle, ListDensity} from './List';
15
+ export type {ListProps, ListStyle, ListDensity, ListEdge} from './List';
16
16
  export {ListItem} from './ListItem';
17
17
  export type {ListItemProps} from './ListItem';
@@ -405,6 +405,16 @@ const markdownTableCellStyles = stylex.create({
405
405
  code: {
406
406
  wordBreak: 'normal',
407
407
  },
408
+ // The grid's outer edge. `dividers="grid"` draws the lines *between* cells
409
+ // and stops at the table's rim, so without this the top, bottom, and sides
410
+ // stay open and the grid reads as unfinished. On the table element, not the
411
+ // wrapper, so `border-collapse: collapse` merges it with the cell lines
412
+ // into one 1px rule rather than doubling up at the edge.
413
+ tableOuterEdge: {
414
+ borderWidth: borderVars['--border-width'],
415
+ borderStyle: 'solid',
416
+ borderColor: colorVars['--color-border'],
417
+ },
408
418
  });
409
419
 
410
420
  /**
@@ -1905,7 +1915,10 @@ function renderBlock(
1905
1915
  ),
1906
1916
  )}
1907
1917
  {...lineStamps}>
1908
- <Table dividers="rows" textOverflow="wrap">
1918
+ <Table
1919
+ dividers="grid"
1920
+ textOverflow="wrap"
1921
+ xstyle={markdownTableCellStyles.tableOuterEdge}>
1909
1922
  <TableHeader>
1910
1923
  <TableRow>
1911
1924
  {header?.children.map((h, i) => (
@@ -117,6 +117,31 @@ describe('createMarkdownFrontmatter', () => {
117
117
  ).not.toThrow();
118
118
  });
119
119
 
120
+ it('accepts the decoded fields record as metadata (identity parse)', () => {
121
+ const fields = createMarkdownFrontmatter({
122
+ name: 'fields',
123
+ parse: record => record,
124
+ });
125
+ const source = '---\ntitle: Release notes\n__proto__: kept\n---\n# Shipped';
126
+
127
+ const parsed = fields.parse(source);
128
+ expect(parsed).toEqual({
129
+ status: 'match',
130
+ contentStart: source.indexOf('# Shipped'),
131
+ metadata: {title: 'Release notes', ['__proto__']: 'kept'},
132
+ });
133
+ if (parsed.status !== 'match') {
134
+ throw new Error('expected a frontmatter match');
135
+ }
136
+ // A field named like a prototype key stays an ordinary own field.
137
+ expect(Object.hasOwn(parsed.metadata, '__proto__')).toBe(true);
138
+ expect(Object.getPrototypeOf(parsed.metadata)).toBe(Object.prototype);
139
+
140
+ const root = parseMarkdownAst(source, {plugins: [fields.plugin]});
141
+ expect(fields.getMetadata(root)).toEqual(parsed.metadata);
142
+ expect(JSON.stringify(root)).not.toContain('title: Release notes');
143
+ });
144
+
120
145
  it('returns a transform plugin that composes in the ordered pipeline', () => {
121
146
  expectTypeOf<MarkdownTransform>().toBeFunction();
122
147
  const root = parseMarkdownAst(SOURCE, {plugins: [documentMetadata.plugin]});
@@ -104,18 +104,22 @@ function readFrontmatter(source: string):
104
104
  if (close == null) {
105
105
  return {status: 'defer'};
106
106
  }
107
- const fields: Record<string, string> = Object.create(null);
107
+ const entries = new Map<string, string>();
108
108
  for (const line of body.slice(0, close.index).split(/\r?\n/)) {
109
109
  if (line.trim() === '') {
110
110
  continue;
111
111
  }
112
112
  const separator = line.indexOf(':');
113
113
  const key = line.slice(0, separator).trim();
114
- if (separator <= 0 || key === '' || Object.hasOwn(fields, key)) {
114
+ if (separator <= 0 || key === '' || entries.has(key)) {
115
115
  return {status: 'none'};
116
116
  }
117
- fields[key] = line.slice(separator + 1).trim();
117
+ entries.set(key, line.slice(separator + 1).trim());
118
118
  }
119
+ // A plain object, so a parser that returns the record unchanged passes the
120
+ // JSON-like metadata check. `fromEntries` defines every key as an own data
121
+ // property, so a field named `__proto__` stays a field.
122
+ const fields: Record<string, string> = Object.fromEntries(entries);
119
123
  let end = openingLength + close.index + close[0].length;
120
124
  if (source.startsWith('\r\n', end)) {
121
125
  end += 2;
@@ -7,7 +7,7 @@ export const docs = {
7
7
  displayName: 'Metadata List',
8
8
  group: 'MetadataList',
9
9
  category: 'Table & List',
10
- keywords: ["metadata","description","definition","keyvalue","properties","details","attributes","summary"],
10
+ keywords: ["metadata","description","definition","keyvalue","properties","details","attributes","summary","descriptionlist","descriptions"],
11
11
  theming: {
12
12
  targets: [
13
13
  {
@@ -43,8 +43,8 @@ export const docs = {
43
43
  {
44
44
  name: 'label',
45
45
  type: "{ position?: 'start' | 'top', width?: number | string }",
46
- description: "Label display configuration. position controls label placement, width sets a custom label column width. Defaults to { position: 'top' } for multi-column layouts.",
47
- default: "{ position: 'start' } (single-column) / { position: 'top' } (multi-column)",
46
+ description: "Label display configuration. position controls label placement, width sets a custom label column width. Defaults to { position: 'top' } for multi-column layouts. Position is ignored in horizontal mode, which always uses { position: 'top' }.",
47
+ default: "{ position: 'start' } (vertical single-column) / { position: 'top' } (multi-column or horizontal)",
48
48
  },
49
49
  {
50
50
  name: 'maxNumOfItems',
@@ -124,6 +124,30 @@ describe('MetadataList', () => {
124
124
  expect(screen.getByText('B')).toBeInTheDocument();
125
125
  });
126
126
 
127
+ it('uses top labels in horizontal orientation even when position is start', () => {
128
+ render(
129
+ <MetadataList orientation="horizontal" label={{position: 'start'}}>
130
+ <MetadataListItem label="A">1</MetadataListItem>
131
+ </MetadataList>,
132
+ );
133
+
134
+ const item = screen.getByRole('term').closest('.astryx-metadata-list-item');
135
+ expect(item).toContainElement(screen.getByRole('definition'));
136
+ });
137
+
138
+ it('defaults to side labels and preserves width when position is omitted', () => {
139
+ const {container} = render(
140
+ <MetadataList label={{width: 120}}>
141
+ <MetadataListItem label="A">1</MetadataListItem>
142
+ </MetadataList>,
143
+ );
144
+
145
+ const list = container.querySelector('dl');
146
+ expect(list?.getAttribute('style')).toContain('120px minmax(0, 1fr)');
147
+ expect(screen.getByRole('term').parentElement).toBe(list);
148
+ expect(screen.getByRole('definition').parentElement).toBe(list);
149
+ });
150
+
127
151
  describe('numeric columns', () => {
128
152
  // A fixed column count is a runtime value, so it arrives as a StyleX
129
153
  // dynamic style: the template lands in the element's inline style (as the
@@ -163,6 +187,20 @@ describe('MetadataList', () => {
163
187
  expect(gridTemplateOf(container)).not.toContain('repeat(');
164
188
  });
165
189
 
190
+ it('defaults to top labels for columns={2} with a width-only label config', () => {
191
+ const {container} = render(
192
+ <MetadataList columns={2} label={{width: 260}}>
193
+ <MetadataListItem label="A">1</MetadataListItem>
194
+ </MetadataList>,
195
+ );
196
+
197
+ expect(gridTemplateOf(container)).toContain('repeat(2, 1fr)');
198
+ const item = screen
199
+ .getByRole('term')
200
+ .closest('.astryx-metadata-list-item');
201
+ expect(item).toContainElement(screen.getByRole('definition'));
202
+ });
203
+
166
204
  it('ignores numeric columns in horizontal orientation', () => {
167
205
  const {container} = render(
168
206
  <MetadataList columns={3} orientation="horizontal">
@@ -206,7 +206,14 @@ export function MetadataList({
206
206
  columns === 'multi' || (typeof columns === 'number' && columns > 1);
207
207
  // Default to 'top' for multi-column and horizontal layouts since side labels
208
208
  // don't work well when items are in separate grid cells.
209
- const labelConfig = label ?? (isMultiColumn ? LABEL_TOP : LABEL_START);
209
+ const defaultLabelConfig = isMultiColumn ? LABEL_TOP : LABEL_START;
210
+ const labelConfig = useMemo(
211
+ () => ({
212
+ ...label,
213
+ position: label?.position ?? defaultLabelConfig.position,
214
+ }),
215
+ [label, defaultLabelConfig],
216
+ );
210
217
  const [isShowAll, setIsShowAll] = useState(false);
211
218
  const contentId = useId();
212
219
  const t = useTranslator();
@@ -12,7 +12,7 @@
12
12
  import {createLayerScopedContext as createContext} from '../Layer/layerScopedContext';
13
13
 
14
14
  export interface MetadataListLabelConfig {
15
- position: 'start' | 'top';
15
+ position?: 'start' | 'top';
16
16
  width?: number | string;
17
17
  }
18
18
 
@@ -260,8 +260,15 @@ const dynamicStyles = stylex.create({
260
260
  // Close timing
261
261
  // =============================================================================
262
262
 
263
- /** Longest the drawer will wait before closing, however long the hold is. */
264
- const MAX_CLOSE_DELAY_MS = 250;
263
+ /**
264
+ * Longest the drawer will wait before closing, however long the hold is.
265
+ *
266
+ * Exported so `AppShell` can defer hiding this drawer's `Activity` boundary
267
+ * by at least this long too (`ACTIVITY_HIDE_BUFFER_MS` in AppShell.tsx) —
268
+ * otherwise Activity's `display: none !important` lands before this delay's
269
+ * own scheduled `dialog.close()` has a chance to run (#5701).
270
+ */
271
+ export const MAX_CLOSE_DELAY_MS = 250;
265
272
  /** Fraction of the hold to close at, so the close never lands on its boundary. */
266
273
  const CLOSE_WITHIN_HOLD = 0.6;
267
274
 
@@ -488,6 +495,21 @@ export function MobileNav({
488
495
  // Element that was focused when the drawer opened; focus goes back to it
489
496
  // once the native dialog has closed (see closeAndRestoreFocus).
490
497
  const triggerElementRef = useRef<HTMLElement | null>(null);
498
+
499
+ // Keeps the dialog rendered from the commit that closes it until the delayed
500
+ // `close()` below has run. The `display` transition on the dialog is meant to
501
+ // do this, but a browser that does not hold a discrete `display` transition
502
+ // (WebKit) drops the dialog to `display: none` at once while it is still an
503
+ // open modal: nothing is visible, yet the whole page stays inert until the
504
+ // timer fires. Set during render, so it is already true in the commit that
505
+ // closes. The drawer's transform and the backdrop stay keyed to `isOpen`, so
506
+ // the slide-out is unchanged.
507
+ const [isCloseHeld, setIsCloseHeld] = useState(false);
508
+ const [wasOpen, setWasOpen] = useState(isOpen);
509
+ if (wasOpen !== isOpen) {
510
+ setWasOpen(isOpen);
511
+ setIsCloseHeld(!isOpen);
512
+ }
491
513
  const gutterRef = useRef<ScrollbarGutterHold | null>(null);
492
514
 
493
515
  // Gives back the gutter held open in place of the hidden scrollbar.
@@ -563,7 +585,12 @@ export function MobileNav({
563
585
 
564
586
  closeTimeoutRef.current = setTimeout(() => {
565
587
  closeAndRestoreFocus(dialog, triggerElementRef);
588
+ setIsCloseHeld(false);
566
589
  }, resolveCloseDelay(dialog));
590
+ } else {
591
+ // Nothing is open, so there is no close to hold the dialog for, and no
592
+ // close timer is left to release the hold.
593
+ setIsCloseHeld(false);
567
594
  }
568
595
 
569
596
  return () => {
@@ -647,7 +674,7 @@ export function MobileNav({
647
674
  layerStructureReset.reset,
648
675
  styles.dialog,
649
676
  overlayPaddingReset.reset,
650
- isOpen && styles.open,
677
+ (isOpen || isCloseHeld) && styles.open,
651
678
  styles.backdrop,
652
679
  isOpen && styles.backdropOpen,
653
680
  xstyle,
@@ -26,7 +26,7 @@ import {
26
26
  beforeEach,
27
27
  afterEach,
28
28
  } from 'vitest';
29
- import {StrictMode} from 'react';
29
+ import {StrictMode, useLayoutEffect} from 'react';
30
30
  import {render, screen, fireEvent, act} from '@testing-library/react';
31
31
  import {MobileNav} from './MobileNav';
32
32
  import {AppShell} from '../AppShell/AppShell';
@@ -136,6 +136,42 @@ describe('MobileNav close path edge cases', () => {
136
136
  }
137
137
  });
138
138
 
139
+ // A browser that does not hold a discrete `display` transition (WebKit)
140
+ // drops the dialog to `display: none` the moment the open styling goes, while
141
+ // it is still an open modal: nothing is visible, yet the page stays inert
142
+ // until the close timer fires. So the dialog's own open styling has to be
143
+ // held by the component, not left to that transition. jsdom applies no
144
+ // StyleX styles, so this reads the readable class names StyleX emits in
145
+ // tests: the `open` class (display) is held through the closing commit, the
146
+ // `backdropOpen` class is not, and the hold ends when the dialog closes.
147
+ it('keeps the dialog rendered until its delayed close has run', () => {
148
+ vi.useFakeTimers();
149
+ try {
150
+ const {rerender} = render(<Drawer isOpen />);
151
+ const dialog = screen.getByTestId('mobile-nav');
152
+ expect(dialog.className).toMatch(/styles\.open/);
153
+ expect(dialog.className).toMatch(/styles\.backdropOpen/);
154
+
155
+ act(() => {
156
+ rerender(<Drawer isOpen={false} />);
157
+ });
158
+ // Still an open modal, so still rendered; only the backdrop's open
159
+ // styling (the fade-out) has gone.
160
+ expect(dialog).toHaveAttribute('open');
161
+ expect(dialog.className).toMatch(/styles\.open/);
162
+ expect(dialog.className).not.toMatch(/styles\.backdropOpen/);
163
+
164
+ act(() => {
165
+ vi.advanceTimersByTime(300);
166
+ });
167
+ // Closed: the hold is released along with the dialog.
168
+ expect(dialog).not.toHaveAttribute('open');
169
+ expect(dialog.className).not.toMatch(/styles\.open/);
170
+ } finally {
171
+ vi.useRealTimers();
172
+ }
173
+ });
174
+
139
175
  it('closes the dialog when unmounted while fully open', () => {
140
176
  const {unmount} = render(<Drawer isOpen />);
141
177
  const dialog = screen.getByTestId('mobile-nav');
@@ -401,26 +437,148 @@ describe('MobileNav close path edge cases', () => {
401
437
  });
402
438
 
403
439
  it('closes the AppShell drawer when dismissed with Escape', () => {
404
- render(<TestShell />);
405
- fireEvent.click(screen.getByRole('button', {name: /open navigation/i}));
406
- const dialog = screen.getAllByRole('dialog', {hidden: true})[0];
407
- expect(dialog).toHaveAttribute('open');
440
+ // Fake timers: AppShell now defers hiding this drawer's Activity boundary
441
+ // (#5701) so MobileNav's own close effect gets a real chance to run —
442
+ // the dialog closes via its normal deferred setTimeout, not synchronously.
443
+ vi.useFakeTimers();
444
+ try {
445
+ render(<TestShell />);
446
+ act(() => {
447
+ fireEvent.click(screen.getByRole('button', {name: /open navigation/i}));
448
+ });
449
+ const dialog = screen.getAllByRole('dialog', {hidden: true})[0];
450
+ expect(dialog).toHaveAttribute('open');
408
451
 
409
- fireEvent(dialog, new Event('cancel', {cancelable: true, bubbles: false}));
452
+ act(() => {
453
+ fireEvent(
454
+ dialog,
455
+ new Event('cancel', {cancelable: true, bubbles: false}),
456
+ );
457
+ });
458
+ act(() => {
459
+ vi.advanceTimersByTime(300);
460
+ });
410
461
 
411
- expect(dialog).not.toHaveAttribute('open');
462
+ expect(dialog).not.toHaveAttribute('open');
463
+ } finally {
464
+ vi.useRealTimers();
465
+ }
412
466
  });
413
467
 
414
468
  it('closes the AppShell drawer when the backdrop is clicked', () => {
415
- render(<TestShell />);
416
- fireEvent.click(screen.getByRole('button', {name: /open navigation/i}));
417
- const dialog = screen.getAllByRole('dialog', {hidden: true})[0];
418
- expect(dialog).toHaveAttribute('open');
469
+ vi.useFakeTimers();
470
+ try {
471
+ render(<TestShell />);
472
+ act(() => {
473
+ fireEvent.click(screen.getByRole('button', {name: /open navigation/i}));
474
+ });
475
+ const dialog = screen.getAllByRole('dialog', {hidden: true})[0];
476
+ expect(dialog).toHaveAttribute('open');
419
477
 
420
- // A click landing on the dialog itself, not the drawer panel, is a
421
- // backdrop dismiss.
422
- fireEvent.click(dialog);
478
+ // A click landing on the dialog itself, not the drawer panel, is a
479
+ // backdrop dismiss.
480
+ act(() => {
481
+ fireEvent.click(dialog);
482
+ });
483
+ act(() => {
484
+ vi.advanceTimersByTime(300);
485
+ });
423
486
 
424
- expect(dialog).not.toHaveAttribute('open');
487
+ expect(dialog).not.toHaveAttribute('open');
488
+ } finally {
489
+ vi.useRealTimers();
490
+ }
491
+ });
492
+
493
+ // #5701: AppShell mounts the drawer inside a React.Activity boundary that
494
+ // used to flip to mode="hidden" in the SAME commit as the close, which
495
+ // forced this file's own unmount-only safety-net effect to call
496
+ // dialog.close() synchronously — closing correctly, but skipping the
497
+ // slide-out transition entirely, because Activity's display:none landed
498
+ // before any transition could run. AppShell now defers that flip, so the
499
+ // dialog should still be open (and rendered) for a beat after closing,
500
+ // not synchronously cut off.
501
+ it('keeps the drawer open long enough for its close transition to run (#5701)', () => {
502
+ vi.useFakeTimers();
503
+ try {
504
+ render(<TestShell />);
505
+ act(() => {
506
+ fireEvent.click(screen.getByRole('button', {name: /open navigation/i}));
507
+ });
508
+ const dialog = screen.getAllByRole('dialog', {hidden: true})[0];
509
+ expect(dialog).toHaveAttribute('open');
510
+
511
+ act(() => {
512
+ fireEvent.click(
513
+ screen.getByRole('button', {name: /close navigation/i}),
514
+ );
515
+ });
516
+
517
+ // Immediately after closing, with no timers advanced: the old
518
+ // synchronous-hide bug closed the dialog on this exact tick. The fix
519
+ // means it must still be open here — Activity hasn't hidden the
520
+ // subtree yet, so MobileNav's own deferred close effect is still the
521
+ // one in control.
522
+ expect(dialog).toHaveAttribute('open');
523
+
524
+ // MobileNav's own close delay (capped at 250ms) runs first: about
525
+ // 260ms after the click the dialog has closed on its own timing while
526
+ // AppShell's Activity boundary (held for 250ms + 50ms) still shows the
527
+ // subtree. Hiding first would cut off that close (#5701).
528
+ act(() => {
529
+ vi.advanceTimersByTime(260);
530
+ });
531
+ expect(dialog).not.toHaveAttribute('open');
532
+ // Activity hides its subtree with an inline `display: none`; a closed
533
+ // dialog's own UA style does not count here.
534
+ expect(dialog.style.display).not.toBe('none');
535
+
536
+ // After the hold, Activity hides the subtree.
537
+ act(() => {
538
+ vi.advanceTimersByTime(60);
539
+ });
540
+ expect(dialog.style.display).toBe('none');
541
+ } finally {
542
+ vi.useRealTimers();
543
+ }
544
+ });
545
+ // A controlled open must be visible in the commit that carries it. The
546
+ // Activity boundary only defers the CLOSE; if opening waited for an effect
547
+ // to copy the open state, the drawer would render hidden for one commit
548
+ // with `isOpen` already true.
549
+ it('shows a controlled open in the same commit, not one effect later', () => {
550
+ const commits: {isOpen: boolean; isHidden: boolean}[] = [];
551
+ function Probe({isOpen}: {isOpen: boolean}) {
552
+ useLayoutEffect(() => {
553
+ const drawer = document.querySelector('dialog');
554
+ commits.push({
555
+ isOpen,
556
+ isHidden:
557
+ drawer != null && getComputedStyle(drawer).display === 'none',
558
+ });
559
+ });
560
+ return null;
561
+ }
562
+ const shell = (isOpen: boolean) => (
563
+ <AppShell
564
+ sideNav={
565
+ <SideNav>
566
+ <SideNavSection title="Test" isHeaderHidden>
567
+ <SideNavItem label="Home" />
568
+ </SideNavSection>
569
+ </SideNav>
570
+ }
571
+ mobileNav={{breakpoint: 'md', isOpen, onOpenChange: () => {}}}>
572
+ <Probe isOpen={isOpen} />
573
+ <div>Content</div>
574
+ </AppShell>
575
+ );
576
+
577
+ const {rerender} = render(shell(false));
578
+ rerender(shell(true));
579
+
580
+ const openCommits = commits.filter(commit => commit.isOpen);
581
+ expect(openCommits.length).toBeGreaterThan(0);
582
+ expect(openCommits.every(commit => !commit.isHidden)).toBe(true);
425
583
  });
426
584
  });
@@ -9,11 +9,15 @@
9
9
  * SYNC: When MoreMenu.tsx changes, update tests to match new behavior
10
10
  */
11
11
 
12
- import {describe, it, expect, vi, beforeEach} from 'vitest';
12
+ import {describe, it, expect, vi, beforeAll, beforeEach} from 'vitest';
13
13
  import {render, screen, fireEvent, waitFor} from '@testing-library/react';
14
14
  import userEvent from '@testing-library/user-event';
15
15
  import {MoreMenu} from './MoreMenu';
16
16
  import {DropdownMenu} from '../DropdownMenu/DropdownMenu';
17
+ import {warmMenuBottomSheet} from '../DropdownMenu/__tests__/warmMenuBottomSheet';
18
+
19
+ // DropdownMenu's touch sheet is loaded lazily; resolve it once up front.
20
+ beforeAll(warmMenuBottomSheet, 20000);
17
21
 
18
22
  // Mock showPopover and hidePopover methods since they're not implemented in jsdom
19
23
  beforeEach(() => {