@astryxdesign/core 0.7.2-canary.fe2c655 → 0.7.2

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 (297) hide show
  1. package/CHANGELOG.md +156 -2
  2. package/README.md +4 -4
  3. package/dist/AppShell/AppShell.d.ts.map +1 -1
  4. package/dist/AppShell/AppShell.js +40 -1
  5. package/dist/AspectRatio/AspectRatio.d.ts +6 -8
  6. package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
  7. package/dist/AspectRatio/AspectRatio.js +44 -3
  8. package/dist/Avatar/Avatar.d.ts.map +1 -1
  9. package/dist/Avatar/Avatar.js +8 -1
  10. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
  11. package/dist/AvatarGroup/AvatarGroupOverflow.js +4 -1
  12. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  13. package/dist/Breadcrumbs/BreadcrumbItem.js +5 -2
  14. package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
  15. package/dist/Chat/ChatLayoutScrollButton.js +1 -0
  16. package/dist/CheckboxInput/CheckboxInput.d.ts +1 -1
  17. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  18. package/dist/CheckboxInput/CheckboxInput.js +3 -1
  19. package/dist/CheckboxList/CheckboxList.d.ts +1 -1
  20. package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
  21. package/dist/CheckboxList/CheckboxList.js +6 -2
  22. package/dist/CheckboxList/CheckboxListItem.d.ts +9 -1
  23. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  24. package/dist/CheckboxList/CheckboxListItem.js +4 -2
  25. package/dist/CommandPalette/CommandPaletteInput.d.ts +1 -1
  26. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  27. package/dist/CommandPalette/CommandPaletteInput.js +37 -5
  28. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  29. package/dist/ContextMenu/ContextMenu.js +5 -2
  30. package/dist/Dialog/Dialog.d.ts.map +1 -1
  31. package/dist/Dialog/Dialog.js +20 -8
  32. package/dist/DropdownMenu/DropdownMenu.d.ts +1 -1
  33. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  34. package/dist/DropdownMenu/DropdownMenu.js +84 -113
  35. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +1 -1
  36. package/dist/DropdownMenu/DropdownMenuContext.d.ts +17 -0
  37. package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
  38. package/dist/DropdownMenu/DropdownMenuContext.js +21 -0
  39. package/dist/DropdownMenu/DropdownMenuDivider.d.ts +5 -3
  40. package/dist/DropdownMenu/DropdownMenuDivider.d.ts.map +1 -1
  41. package/dist/DropdownMenu/DropdownMenuDivider.js +7 -3
  42. package/dist/DropdownMenu/DropdownMenuGroup.d.ts +6 -4
  43. package/dist/DropdownMenu/DropdownMenuGroup.d.ts.map +1 -1
  44. package/dist/DropdownMenu/DropdownMenuGroup.js +4 -3
  45. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +1 -1
  46. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  47. package/dist/DropdownMenu/DropdownMenuSubMenu.js +15 -4
  48. package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts +24 -0
  49. package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts.map +1 -0
  50. package/dist/DropdownMenu/lazyMenuBottomSheet.js +46 -0
  51. package/dist/DropdownMenu/menuRegionStops.d.ts +38 -0
  52. package/dist/DropdownMenu/menuRegionStops.d.ts.map +1 -0
  53. package/dist/DropdownMenu/menuRegionStops.js +131 -0
  54. package/dist/EmptyState/EmptyState.d.ts.map +1 -1
  55. package/dist/EmptyState/EmptyState.js +4 -10
  56. package/dist/Field/Field.d.ts +1 -1
  57. package/dist/Field/Field.d.ts.map +1 -1
  58. package/dist/Field/Field.js +8 -4
  59. package/dist/FormLayout/FormLayout.d.ts +5 -4
  60. package/dist/FormLayout/FormLayout.d.ts.map +1 -1
  61. package/dist/FormLayout/FormLayout.js +1 -1
  62. package/dist/FormLayout/FormLayoutContext.d.ts +3 -3
  63. package/dist/FormLayout/FormLayoutContext.js +3 -3
  64. package/dist/Grid/Grid.d.ts +8 -7
  65. package/dist/Grid/Grid.d.ts.map +1 -1
  66. package/dist/Grid/Grid.js +5 -4
  67. package/dist/Grid/GridSpan.d.ts +1 -1
  68. package/dist/Grid/GridSpan.d.ts.map +1 -1
  69. package/dist/Grid/GridSpan.js +1 -0
  70. package/dist/Heading/Heading.d.ts.map +1 -1
  71. package/dist/Heading/Heading.js +4 -2
  72. package/dist/Item/Item.d.ts +6 -2
  73. package/dist/Item/Item.d.ts.map +1 -1
  74. package/dist/Item/Item.js +19 -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/parser.d.ts.map +1 -1
  89. package/dist/Markdown/parser.js +8 -2
  90. package/dist/Markdown/plugins/headingLinks.d.ts.map +1 -1
  91. package/dist/Markdown/plugins/headingLinks.js +9 -1
  92. package/dist/MetadataList/MetadataList.d.ts.map +1 -1
  93. package/dist/MetadataList/MetadataList.js +5 -1
  94. package/dist/MetadataList/MetadataListContext.d.ts +1 -1
  95. package/dist/MetadataList/MetadataListContext.d.ts.map +1 -1
  96. package/dist/MobileNav/MobileNav.d.ts +9 -0
  97. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  98. package/dist/MobileNav/MobileNav.js +30 -3
  99. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  100. package/dist/PowerSearch/PowerSearch.js +5 -0
  101. package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
  102. package/dist/PowerSearch/PowerSearchValueEditor.js +45 -11
  103. package/dist/RadioList/RadioList.d.ts +1 -1
  104. package/dist/RadioList/RadioList.d.ts.map +1 -1
  105. package/dist/RadioList/RadioList.js +6 -2
  106. package/dist/RadioList/RadioListItem.d.ts +9 -1
  107. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  108. package/dist/RadioList/RadioListItem.js +2 -0
  109. package/dist/SideNav/SideNavItem.d.ts +8 -1
  110. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  111. package/dist/SideNav/SideNavItem.js +76 -10
  112. package/dist/Text/Text.d.ts.map +1 -1
  113. package/dist/Text/Text.js +4 -2
  114. package/dist/ToggleButton/ToggleButton.d.ts +3 -2
  115. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  116. package/dist/ToggleButton/ToggleButton.js +12 -1
  117. package/dist/Token/Token.js +1 -1
  118. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  119. package/dist/TopNav/TopNavMegaMenu.js +5 -3
  120. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  121. package/dist/TopNav/TopNavMenu.js +5 -3
  122. package/dist/TreeList/TreeList.d.ts.map +1 -1
  123. package/dist/TreeList/TreeList.js +1 -0
  124. package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
  125. package/dist/TreeList/TreeListBranches.js +2 -2
  126. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  127. package/dist/TreeList/TreeListItem.js +57 -9
  128. package/dist/astryx.css +27 -4
  129. package/dist/hooks/useMenuHover.d.ts.map +1 -1
  130. package/dist/hooks/useMenuHover.js +18 -13
  131. package/dist/reset.a11y.chromium.spec.js +50 -1
  132. package/dist/utils/plainDate.d.ts.map +1 -1
  133. package/dist/utils/plainDate.js +15 -9
  134. package/docs.mjs +4 -4
  135. package/package.json +4 -4
  136. package/scripts/agent-doc-state.mjs +66 -6
  137. package/scripts/postinstall.mjs +44 -3
  138. package/src/AppShell/AppShell.tsx +45 -1
  139. package/src/AspectRatio/AspectRatio.test.tsx +44 -43
  140. package/src/AspectRatio/AspectRatio.tsx +38 -18
  141. package/src/AspectRatio/__tests__/AspectRatioFitLayers.a11y.browser.spec.ts +157 -0
  142. package/src/Avatar/Avatar.doc.mjs +5 -3
  143. package/src/Avatar/Avatar.tsx +11 -2
  144. package/src/AvatarGroup/AvatarGroup.doc.mjs +3 -2
  145. package/src/AvatarGroup/AvatarGroupOverflow.test.tsx +24 -1
  146. package/src/AvatarGroup/AvatarGroupOverflow.tsx +2 -0
  147. package/src/Breadcrumbs/BreadcrumbItem.tsx +4 -1
  148. package/src/Breadcrumbs/Breadcrumbs.test.tsx +30 -1
  149. package/src/Card/Card.doc.mjs +1 -1
  150. package/src/Chat/ChatLayoutScrollButton.test.tsx +13 -0
  151. package/src/Chat/ChatLayoutScrollButton.tsx +4 -2
  152. package/src/Chat/__tests__/ChatLayoutScrollButton.a11y.chromium.spec.ts +64 -19
  153. package/src/Chat/__tests__/ChatMessageMetadata.a11y.chromium.spec.ts +3 -1
  154. package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
  155. package/src/CheckboxInput/CheckboxInput.tsx +3 -1
  156. package/src/CheckboxList/CheckboxList.fr10.test.tsx +29 -2
  157. package/src/CheckboxList/CheckboxList.test.tsx +22 -0
  158. package/src/CheckboxList/CheckboxList.tsx +4 -1
  159. package/src/CheckboxList/CheckboxListItem.doc.mjs +16 -0
  160. package/src/CheckboxList/CheckboxListItem.tsx +10 -0
  161. package/src/Code/Code.doc.mjs +1 -0
  162. package/src/CommandPalette/CommandPalette.doc.mjs +2 -0
  163. package/src/CommandPalette/CommandPaletteInput.test.tsx +53 -4
  164. package/src/CommandPalette/CommandPaletteInput.tsx +33 -3
  165. package/src/CommandPalette/__tests__/CommandPaletteInputAudit.a11y.chromium.spec.ts +335 -0
  166. package/src/CommandPalette/__tests__/CommandPaletteItemAudit.a11y.chromium.spec.ts +292 -0
  167. package/src/ContextMenu/ContextMenu.doc.mjs +2 -3
  168. package/src/ContextMenu/ContextMenu.test.tsx +12 -2
  169. package/src/ContextMenu/ContextMenu.tsx +4 -1
  170. package/src/ContextMenu/__tests__/ContextMenuAudit.a11y.chromium.spec.ts +438 -0
  171. package/src/Dialog/Dialog.test.tsx +46 -0
  172. package/src/Dialog/Dialog.tsx +20 -7
  173. package/src/Dialog/__tests__/Dialog.a11y.known-failures.ts +0 -14
  174. package/src/Drawer/Drawer.doc.mjs +1 -0
  175. package/src/DropdownMenu/DropdownMenu.ssr.test.tsx +57 -0
  176. package/src/DropdownMenu/DropdownMenu.test.tsx +79 -6
  177. package/src/DropdownMenu/DropdownMenu.tsx +88 -144
  178. package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +1 -1
  179. package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +1 -1
  180. package/src/DropdownMenu/DropdownMenuContext.tsx +24 -0
  181. package/src/DropdownMenu/DropdownMenuDivider.doc.mjs +2 -2
  182. package/src/DropdownMenu/DropdownMenuDivider.spec.md +192 -0
  183. package/src/DropdownMenu/DropdownMenuDivider.tsx +9 -5
  184. package/src/DropdownMenu/DropdownMenuGroup.doc.mjs +169 -10
  185. package/src/DropdownMenu/DropdownMenuGroup.tsx +6 -4
  186. package/src/DropdownMenu/DropdownMenuLazySheet.test.tsx +126 -0
  187. package/src/DropdownMenu/DropdownMenuRadioItem.doc.mjs +1 -1
  188. package/src/DropdownMenu/DropdownMenuRadioItem.tsx +1 -1
  189. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +185 -0
  190. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +17 -3
  191. package/src/DropdownMenu/__tests__/DropdownMenuDividerAudit.a11y.chromium.spec.ts +280 -0
  192. package/src/DropdownMenu/__tests__/DropdownMenuGroupAudit.a11y.chromium.spec.ts +334 -0
  193. package/src/DropdownMenu/__tests__/warmMenuBottomSheet.tsx +43 -0
  194. package/src/DropdownMenu/lazyMenuBottomSheet.ts +71 -0
  195. package/src/DropdownMenu/menuRegionStops.ts +167 -0
  196. package/src/EmptyState/EmptyState.doc.mjs +3 -2
  197. package/src/EmptyState/EmptyState.spec.md +189 -0
  198. package/src/EmptyState/EmptyState.test.tsx +74 -3
  199. package/src/EmptyState/EmptyState.tsx +4 -8
  200. package/src/Field/Field.doc.mjs +14 -8
  201. package/src/Field/Field.spec.md +217 -0
  202. package/src/Field/Field.test.tsx +60 -2
  203. package/src/Field/Field.tsx +17 -8
  204. package/src/Field/FieldLabel.doc.mjs +133 -26
  205. package/src/Field/FieldLabel.spec.md +172 -0
  206. package/src/Field/__tests__/FieldLabelAudit.a11y.chromium.spec.ts +296 -0
  207. package/src/FormLayout/FormLayout.doc.mjs +6 -3
  208. package/src/FormLayout/FormLayout.spec.md +192 -0
  209. package/src/FormLayout/FormLayout.test.tsx +15 -0
  210. package/src/FormLayout/FormLayout.tsx +7 -6
  211. package/src/FormLayout/FormLayoutContext.ts +3 -3
  212. package/src/Grid/Grid.doc.mjs +7 -1
  213. package/src/Grid/Grid.spec.md +93 -113
  214. package/src/Grid/Grid.test.tsx +22 -0
  215. package/src/Grid/Grid.tsx +10 -9
  216. package/src/Grid/GridSpan.doc.mjs +3 -3
  217. package/src/Grid/GridSpan.tsx +4 -1
  218. package/src/Heading/Heading.test.tsx +57 -0
  219. package/src/Heading/Heading.tsx +6 -2
  220. package/src/Icon/Icon.doc.mjs +36 -0
  221. package/src/Indicator/Indicator.doc.mjs +3 -3
  222. package/src/Item/Item.doc.mjs +12 -8
  223. package/src/Item/Item.test.tsx +61 -0
  224. package/src/Item/Item.tsx +40 -4
  225. package/src/Layout/LayoutHeader.doc.mjs +5 -0
  226. package/src/List/List.doc.mjs +14 -1
  227. package/src/List/List.spec.md +129 -77
  228. package/src/List/List.test.tsx +163 -94
  229. package/src/List/List.tsx +74 -18
  230. package/src/List/ListContext.tsx +9 -3
  231. package/src/List/ListItem.doc.mjs +16 -0
  232. package/src/List/ListItem.tsx +56 -27
  233. package/src/List/index.ts +1 -1
  234. package/src/Markdown/Markdown.tsx +14 -1
  235. package/src/Markdown/parser.ts +8 -2
  236. package/src/Markdown/plugins/headingLinks.ts +9 -1
  237. package/src/MetadataList/MetadataList.doc.mjs +3 -3
  238. package/src/MetadataList/MetadataList.test.tsx +38 -0
  239. package/src/MetadataList/MetadataList.tsx +8 -1
  240. package/src/MetadataList/MetadataListContext.tsx +1 -1
  241. package/src/MobileNav/MobileNav.tsx +30 -3
  242. package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +173 -15
  243. package/src/MoreMenu/MoreMenu.test.tsx +5 -1
  244. package/src/PowerSearch/PowerSearch.test.tsx +62 -0
  245. package/src/PowerSearch/PowerSearch.tsx +9 -0
  246. package/src/PowerSearch/PowerSearchValueEditor.test.tsx +71 -0
  247. package/src/PowerSearch/PowerSearchValueEditor.tsx +45 -0
  248. package/src/ProgressBar/ProgressBar.doc.mjs +1 -0
  249. package/src/RadioList/RadioList.fr10.test.tsx +45 -2
  250. package/src/RadioList/RadioList.test.tsx +22 -0
  251. package/src/RadioList/RadioList.tsx +4 -1
  252. package/src/RadioList/RadioListItem.doc.mjs +16 -0
  253. package/src/RadioList/RadioListItem.tsx +10 -1
  254. package/src/Resizable/useResizable.doc.mjs +2 -0
  255. package/src/ScrollableArea/ScrollableArea.doc.mjs +1 -1
  256. package/src/SideNav/SideNav.spec.md +227 -0
  257. package/src/SideNav/SideNav.test.tsx +220 -0
  258. package/src/SideNav/SideNavItem.doc.mjs +2 -2
  259. package/src/SideNav/SideNavItem.tsx +99 -17
  260. package/src/Slider/Slider.spec.md +39 -18
  261. package/src/Spinner/Spinner.doc.mjs +1 -1
  262. package/src/TabList/TabList.spec.md +57 -44
  263. package/src/Table/Table.spec.md +6 -1
  264. package/src/Table/plugins/activation/useTableActivation.spec.md +254 -0
  265. package/src/Text/Text.test.tsx +57 -0
  266. package/src/Text/Text.tsx +6 -2
  267. package/src/TimeInput/TimeInput.fr10.test.tsx +15 -0
  268. package/src/Timestamp/Timestamp.doc.mjs +1 -1
  269. package/src/ToggleButton/ToggleButton.test.tsx +95 -0
  270. package/src/ToggleButton/ToggleButton.tsx +12 -1
  271. package/src/ToggleButton/ToggleButtonGroup.doc.mjs +4 -0
  272. package/src/Token/Token.tsx +1 -1
  273. package/src/Token/__tests__/TokenRemoveTarget.a11y.chromium.spec.ts +86 -0
  274. package/src/Toolbar/Toolbar.test.tsx +0 -8
  275. package/src/Toolbar/__tests__/Toolbar.a11y.chromium.spec.ts +163 -0
  276. package/src/Toolbar/__tests__/Toolbar.a11y.renders.tsx +63 -0
  277. package/src/Toolbar/__tests__/Toolbar.a11y.states.ts +142 -0
  278. package/src/Toolbar/__tests__/Toolbar.a11y.test.tsx +126 -0
  279. package/src/TopNav/TopNavMegaMenu.test.tsx +47 -0
  280. package/src/TopNav/TopNavMegaMenu.tsx +9 -3
  281. package/src/TopNav/TopNavMenu.test.tsx +46 -1
  282. package/src/TopNav/TopNavMenu.tsx +13 -3
  283. package/src/TreeList/TreeList.doc.mjs +1 -0
  284. package/src/TreeList/TreeList.test.tsx +327 -6
  285. package/src/TreeList/TreeList.tsx +5 -1
  286. package/src/TreeList/TreeListBranches.tsx +6 -0
  287. package/src/TreeList/TreeListItem.tsx +88 -13
  288. package/src/docPropLiterals.test.ts +36 -10
  289. package/src/hooks/useMenuHover.ts +18 -13
  290. package/src/postinstall.test.mjs +79 -0
  291. package/src/reset.a11y.chromium.spec.ts +52 -1
  292. package/src/reset.css +9 -29
  293. package/src/reset.test.ts +11 -0
  294. package/src/theme/derivedVarRegistry.test.ts +7 -0
  295. package/src/unicodePropertyEscapes.source-build.test.mjs +69 -0
  296. package/src/utils/plainDate.test.ts +37 -1
  297. package/src/utils/plainDate.ts +15 -5
@@ -1494,3 +1494,188 @@ describe('DropdownMenuSubMenu drill-in on a phone', () => {
1494
1494
  screen.getAllByRole('menu', {name, ...h}).find(isShown);
1495
1495
  });
1496
1496
  });
1497
+
1498
+ describe('DropdownMenuSubMenu light dismiss of the root menu (#6893)', () => {
1499
+ // The browser closes the root `popover=\"auto\"` element itself on an outside
1500
+ // tap; that path never reaches the submenu, whose flyout is
1501
+ // `popover=\"manual\"`. Simulate it by hiding the root popover element
1502
+ // directly, exactly what light dismiss does to it.
1503
+ function lightDismissRootMenu() {
1504
+ const menu = screen.getByRole('menu', {name: /Actions/, hidden: true});
1505
+ const rootPopover = menu.closest<HTMLElement>('[popover]');
1506
+ expect(rootPopover).not.toBeNull();
1507
+ act(() => {
1508
+ rootPopover!.hidePopover();
1509
+ });
1510
+ }
1511
+
1512
+ function getFlyoutPopover(name: RegExp) {
1513
+ const flyout = screen.getByRole('menu', {name, hidden: true});
1514
+ return flyout.closest<HTMLElement>('[popover]');
1515
+ }
1516
+
1517
+ it('closes an open flyout when the root menu is light-dismissed', async () => {
1518
+ const user = userEvent.setup();
1519
+ render(<MoveMenu />);
1520
+ await user.click(screen.getByRole('button', {name: /Actions/}));
1521
+ const trigger = screen.getByRole('menuitem', {
1522
+ name: /Move to/,
1523
+ hidden: true,
1524
+ });
1525
+ await user.click(trigger);
1526
+ await waitFor(() => {
1527
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
1528
+ });
1529
+ const flyoutPopover = getFlyoutPopover(/Move to/);
1530
+ expect(flyoutPopover).toHaveAttribute('popover-open', '');
1531
+
1532
+ lightDismissRootMenu();
1533
+
1534
+ await waitFor(() => {
1535
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
1536
+ });
1537
+ expect(flyoutPopover).not.toHaveAttribute('popover-open');
1538
+ });
1539
+
1540
+ it('leaves the submenu collapsed when the menu reopens after a light dismiss', async () => {
1541
+ const user = userEvent.setup();
1542
+ render(<MoveMenu />);
1543
+ await user.click(screen.getByRole('button', {name: /Actions/}));
1544
+ const trigger = screen.getByRole('menuitem', {
1545
+ name: /Move to/,
1546
+ hidden: true,
1547
+ });
1548
+ await user.click(trigger);
1549
+ await waitFor(() => {
1550
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
1551
+ });
1552
+
1553
+ lightDismissRootMenu();
1554
+ await user.click(screen.getByRole('button', {name: /Actions/}));
1555
+
1556
+ await waitFor(() => {
1557
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
1558
+ });
1559
+ expect(getFlyoutPopover(/Move to/)).not.toHaveAttribute('popover-open');
1560
+ });
1561
+
1562
+ it('closes an open flyout when the root menu closes via controlled isMenuOpen', async () => {
1563
+ const user = userEvent.setup();
1564
+ function ControlledMenu() {
1565
+ const [open, setOpen] = useState(true);
1566
+ return (
1567
+ <>
1568
+ <DropdownMenu
1569
+ button={{label: 'Actions'}}
1570
+ isMenuOpen={open}
1571
+ onOpenChange={setOpen}>
1572
+ <DropdownMenuSubMenu label="Move to">
1573
+ <DropdownMenuItem label="Folder A" onClick={() => {}} />
1574
+ </DropdownMenuSubMenu>
1575
+ </DropdownMenu>
1576
+ <button type="button" onClick={() => setOpen(false)}>
1577
+ close it
1578
+ </button>
1579
+ </>
1580
+ );
1581
+ }
1582
+ render(<ControlledMenu />);
1583
+ const trigger = screen.getByRole('menuitem', {
1584
+ name: /Move to/,
1585
+ hidden: true,
1586
+ });
1587
+ await user.click(trigger);
1588
+ await waitFor(() => {
1589
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
1590
+ });
1591
+ const flyoutPopover = getFlyoutPopover(/Move to/);
1592
+
1593
+ await user.click(screen.getByRole('button', {name: /close it/}));
1594
+
1595
+ await waitFor(() => {
1596
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
1597
+ });
1598
+ expect(flyoutPopover).not.toHaveAttribute('popover-open');
1599
+ });
1600
+
1601
+ it('closes flyouts nested two levels deep on light dismiss', async () => {
1602
+ const user = userEvent.setup();
1603
+ render(
1604
+ <DropdownMenu button={{label: 'Actions'}}>
1605
+ <DropdownMenuSubMenu label="Move to">
1606
+ <DropdownMenuSubMenu label="Deeper">
1607
+ <DropdownMenuItem label="Leaf" onClick={() => {}} />
1608
+ </DropdownMenuSubMenu>
1609
+ </DropdownMenuSubMenu>
1610
+ </DropdownMenu>,
1611
+ );
1612
+ await user.click(screen.getByRole('button', {name: /Actions/}));
1613
+ const first = screen.getByRole('menuitem', {
1614
+ name: /Move to/,
1615
+ hidden: true,
1616
+ });
1617
+ await user.click(first);
1618
+ await waitFor(() => {
1619
+ expect(first).toHaveAttribute('aria-expanded', 'true');
1620
+ });
1621
+ const deeper = screen.getByRole('menuitem', {
1622
+ name: /Deeper/,
1623
+ hidden: true,
1624
+ });
1625
+ await user.click(deeper);
1626
+ await waitFor(() => {
1627
+ expect(deeper).toHaveAttribute('aria-expanded', 'true');
1628
+ });
1629
+ const firstPopover = getFlyoutPopover(/Move to/);
1630
+ const deeperPopover = getFlyoutPopover(/Deeper/);
1631
+
1632
+ lightDismissRootMenu();
1633
+
1634
+ await waitFor(() => {
1635
+ expect(first).toHaveAttribute('aria-expanded', 'false');
1636
+ });
1637
+ await waitFor(() => {
1638
+ expect(deeper).toHaveAttribute('aria-expanded', 'false');
1639
+ });
1640
+ expect(firstPopover).not.toHaveAttribute('popover-open');
1641
+ expect(deeperPopover).not.toHaveAttribute('popover-open');
1642
+ });
1643
+
1644
+ // Regression for the stale hover timer: a hover over the submenu trigger
1645
+ // schedules an open after the show delay. A press on the same trigger
1646
+ // cancels its own scheduled hover-open (that is the click guard), so the
1647
+ // way a timer is still pending at close time is a close that comes from
1648
+ // elsewhere — here the root menu's light dismiss, the #6893 case. The
1649
+ // close must cancel the scheduled hover-open: once the delay elapses the
1650
+ // flyout stays closed instead of reopening behind the dismissed menu.
1651
+ // Time is driven manually so the dismiss lands inside the show delay;
1652
+ // auto-advancing timers would let the hover-open fire before it.
1653
+ it('cancels a hover-open scheduled before the menu was closed from elsewhere', async () => {
1654
+ vi.useFakeTimers();
1655
+ try {
1656
+ render(<MoveMenu />);
1657
+ fireEvent.click(screen.getByRole('button', {name: /Actions/}));
1658
+ const trigger = screen.getByRole('menuitem', {
1659
+ name: /Move to/,
1660
+ hidden: true,
1661
+ });
1662
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
1663
+
1664
+ // Schedules the hover-open (default show delay 150ms).
1665
+ fireEvent.mouseEnter(trigger);
1666
+
1667
+ // Close from elsewhere while the hover-open is still pending.
1668
+ lightDismissRootMenu();
1669
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
1670
+
1671
+ // The scheduled delay passes; the flyout must stay closed.
1672
+ await act(async () => {
1673
+ vi.advanceTimersByTime(300);
1674
+ });
1675
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
1676
+ expect(getFlyoutPopover(/Move to/)).not.toHaveAttribute('popover-open');
1677
+ } finally {
1678
+ vi.useRealTimers();
1679
+ }
1680
+ });
1681
+ });
@@ -97,7 +97,9 @@ import {
97
97
  } from './menuItemRoles';
98
98
  import {
99
99
  DropdownMenuContext,
100
+ DropdownMenuRootOpenContext,
100
101
  useDropdownMenuContext,
102
+ useDropdownMenuRootOpen,
101
103
  type DropdownMenuContextValue,
102
104
  } from './DropdownMenuContext';
103
105
  import {focusMenuItemOnHover} from './menuItemHover';
@@ -451,13 +453,17 @@ export function DropdownMenuSubMenu(
451
453
  // Hover intent and the shared hover→click guard only: this level owns its own
452
454
  // click handling, roving focus and typeahead. popover="manual", so the
453
455
  // invoker wiring other consumers need does not apply. A drill-in row has no
454
- // hover: a finger drives it, and a press opens it.
456
+ // hover: a finger drives it, and a press opens it. Hover is also gated on
457
+ // the root menu being open, so a hover-open scheduled before the menu
458
+ // closed from elsewhere is cancelled instead of firing into the hidden
459
+ // menu (#6893).
460
+ const isRootMenuOpen = useDropdownMenuRootOpen();
455
461
  const {triggerProps, contentProps, confirmHoverOpen} =
456
462
  useMenuHover<HTMLDivElement>({
457
463
  show: showLayer,
458
464
  hide: hideLayer,
459
465
  isOpen: isFlyoutOpen,
460
- isEnabled: canOpen && !isDrillIn,
466
+ isEnabled: canOpen && !isDrillIn && isRootMenuOpen,
461
467
  // The safe triangle toward the flyout's near edge is built from the
462
468
  // flyout this component renders, not the hook's own list ref.
463
469
  flyoutRef: menuRef,
@@ -770,7 +776,7 @@ export function DropdownMenuSubMenu(
770
776
  : flyoutStyles.popoverCustomWidth(resolvedMenuWidth.value)
771
777
  : flyoutStyles.popover;
772
778
 
773
- const nestedContent = (
779
+ const nestedRows = (
774
780
  <DropdownMenuContext value={nestedMenuContext}>
775
781
  {isDrillIn && (
776
782
  <DropdownMenuItem
@@ -790,6 +796,14 @@ export function DropdownMenuSubMenu(
790
796
  {children}
791
797
  </DropdownMenuContext>
792
798
  );
799
+ // Nested rows live in this level's flyout, a layer of their own where the
800
+ // layer-scoped signal resets; re-provide it so their pending hover intent
801
+ // dies with the root menu too.
802
+ const nestedContent = (
803
+ <DropdownMenuRootOpenContext value={isRootMenuOpen}>
804
+ {nestedRows}
805
+ </DropdownMenuRootOpenContext>
806
+ );
793
807
 
794
808
  return (
795
809
  <>
@@ -0,0 +1,280 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file DropdownMenuDividerAudit.a11y.chromium.spec.ts
5
+ * @input Exact-head DropdownMenuDivider story across themes, color modes, direction, and viewport widths
6
+ * @output Screenshots and fail-closed sensor receipts for the DropdownMenuDivider component audit
7
+ * @position Component-owned Night Watch browser evidence; CI runs it only for a DropdownMenuDivider audit pull request
8
+ */
9
+
10
+ import {createHash} from 'node:crypto';
11
+ import {execFileSync} from 'node:child_process';
12
+ import * as fs from 'node:fs';
13
+ import * as path from 'node:path';
14
+ import {expect, test, type Page} from '@playwright/test';
15
+ import {holdMotionStill} from '@astryxdesign/a11y-spec/chromium';
16
+ import {
17
+ DEFAULT_STORYBOOK_DIR,
18
+ serveStorybook,
19
+ type StaticServer,
20
+ } from '@astryxdesign/a11y-spec/storybook';
21
+
22
+ const OUTPUT = path.resolve('dropdown-menu-divider-audit-evidence');
23
+ const STORY_ID = 'core-dropdownmenudivider--menu-separator';
24
+ const WIDE = {width: 1024, height: 768};
25
+ const NARROW = {width: 320, height: 720};
26
+
27
+ type Mode = 'light' | 'dark';
28
+ type Direction = 'ltr' | 'rtl';
29
+ type Theme = 'neutral' | 'butter';
30
+ type AuditCase = {
31
+ key: string;
32
+ mode: Mode;
33
+ direction: Direction;
34
+ theme: Theme;
35
+ viewport: {width: number; height: number};
36
+ };
37
+
38
+ const cases: AuditCase[] = [
39
+ {
40
+ key: 'neutral-light',
41
+ mode: 'light',
42
+ direction: 'ltr',
43
+ theme: 'neutral',
44
+ viewport: WIDE,
45
+ },
46
+ {
47
+ key: 'neutral-dark',
48
+ mode: 'dark',
49
+ direction: 'ltr',
50
+ theme: 'neutral',
51
+ viewport: WIDE,
52
+ },
53
+ {
54
+ key: 'butter-light',
55
+ mode: 'light',
56
+ direction: 'ltr',
57
+ theme: 'butter',
58
+ viewport: WIDE,
59
+ },
60
+ {
61
+ key: 'butter-dark',
62
+ mode: 'dark',
63
+ direction: 'ltr',
64
+ theme: 'butter',
65
+ viewport: WIDE,
66
+ },
67
+ {
68
+ key: 'rtl-light',
69
+ mode: 'light',
70
+ direction: 'rtl',
71
+ theme: 'neutral',
72
+ viewport: WIDE,
73
+ },
74
+ {
75
+ key: 'rtl-dark',
76
+ mode: 'dark',
77
+ direction: 'rtl',
78
+ theme: 'neutral',
79
+ viewport: WIDE,
80
+ },
81
+ {
82
+ key: 'narrow-light',
83
+ mode: 'light',
84
+ direction: 'ltr',
85
+ theme: 'neutral',
86
+ viewport: NARROW,
87
+ },
88
+ {
89
+ key: 'narrow-dark',
90
+ mode: 'dark',
91
+ direction: 'ltr',
92
+ theme: 'neutral',
93
+ viewport: NARROW,
94
+ },
95
+ ];
96
+
97
+ test.describe.configure({mode: 'serial', retries: 0});
98
+ let server: StaticServer;
99
+ let checkoutSha: string;
100
+ let storybookSha: string;
101
+
102
+ function storyUrl(item: AuditCase) {
103
+ return `${server.origin}/iframe.html?id=${STORY_ID}&viewMode=story&globals=colorMode:${item.mode};astryxTheme:${item.theme};direction:${item.direction}`;
104
+ }
105
+
106
+ async function settle(page: Page) {
107
+ await holdMotionStill(page);
108
+ await page.evaluate(async () => document.fonts.ready);
109
+ await page.waitForTimeout(40);
110
+ }
111
+
112
+ test.beforeAll(async () => {
113
+ fs.rmSync(OUTPUT, {recursive: true, force: true});
114
+ fs.mkdirSync(OUTPUT, {recursive: true});
115
+ checkoutSha = execFileSync('git', ['rev-parse', 'HEAD'], {
116
+ encoding: 'utf8',
117
+ }).trim();
118
+ const expected = process.env.ASTRYX_HEAD_SHA;
119
+ if (expected && expected !== checkoutSha) {
120
+ throw new Error(
121
+ `checkout ${checkoutSha} does not match expected head ${expected}`,
122
+ );
123
+ }
124
+ server = await serveStorybook(
125
+ process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
126
+ );
127
+ const response = await fetch(`${server.origin}/astryx-build-sha.txt`);
128
+ if (!response.ok) {
129
+ throw new Error(`Storybook build has no SHA stamp (${response.status})`);
130
+ }
131
+ storybookSha = (await response.text()).trim();
132
+ if (storybookSha !== checkoutSha) {
133
+ throw new Error(
134
+ `Storybook ${storybookSha} does not match checkout ${checkoutSha}`,
135
+ );
136
+ }
137
+ });
138
+
139
+ test.afterAll(async () => {
140
+ await server?.close();
141
+ });
142
+
143
+ for (const item of cases) {
144
+ test(item.key, async ({browser}) => {
145
+ const context = await browser.newContext({
146
+ viewport: item.viewport,
147
+ colorScheme: item.mode,
148
+ reducedMotion: 'reduce',
149
+ });
150
+ const page = await context.newPage();
151
+ const pageErrors: string[] = [];
152
+ page.on('pageerror', error => pageErrors.push(String(error)));
153
+ await page.goto(storyUrl(item), {waitUntil: 'load'});
154
+ await page.locator('#storybook-root').waitFor();
155
+ const menu = page.getByRole('menu').last();
156
+ await expect(menu).toBeVisible();
157
+ const separator = page.getByRole('separator');
158
+ await expect(separator).toBeVisible();
159
+ await expect(separator).toHaveAttribute('aria-orientation', 'horizontal');
160
+ await settle(page);
161
+
162
+ const menuItems = page.getByRole('menuitem');
163
+ await expect(menuItems).toHaveCount(2);
164
+ await menu.focus();
165
+ await page.keyboard.press('ArrowDown');
166
+ await expect(menuItems.first()).toBeFocused();
167
+ await page.keyboard.press('ArrowDown');
168
+ await expect(menuItems.last()).toBeFocused();
169
+ await expect(separator).not.toBeFocused();
170
+
171
+ const observed = await separator.evaluate(element => {
172
+ const rootBox = element.getBoundingClientRect();
173
+ const rule = element.firstElementChild;
174
+ const ruleBox = rule?.getBoundingClientRect() ?? null;
175
+ const rootStyle = getComputedStyle(element);
176
+ const ruleStyle = rule ? getComputedStyle(rule) : null;
177
+ return {
178
+ theme: document
179
+ .querySelector('[data-astryx-theme]')
180
+ ?.getAttribute('data-astryx-theme'),
181
+ mode: document.documentElement.getAttribute('data-theme'),
182
+ direction: rootStyle.direction,
183
+ viewport: {width: innerWidth, height: innerHeight},
184
+ reducedMotion: matchMedia('(prefers-reduced-motion: reduce)').matches,
185
+ horizontalOverflow:
186
+ document.documentElement.scrollWidth > innerWidth + 1,
187
+ fonts: document.fonts.status,
188
+ role: element.getAttribute('role'),
189
+ orientation: element.getAttribute('aria-orientation'),
190
+ tabIndex: element.getAttribute('tabindex'),
191
+ targetClasses: {
192
+ menuDivider: element.classList.contains(
193
+ 'astryx-dropdown-menu-divider',
194
+ ),
195
+ divider: element.classList.contains('astryx-divider'),
196
+ },
197
+ geometry: {
198
+ root: {
199
+ x: rootBox.x,
200
+ y: rootBox.y,
201
+ width: rootBox.width,
202
+ height: rootBox.height,
203
+ },
204
+ rule:
205
+ ruleBox === null
206
+ ? null
207
+ : {
208
+ x: ruleBox.x,
209
+ y: ruleBox.y,
210
+ width: ruleBox.width,
211
+ height: ruleBox.height,
212
+ },
213
+ },
214
+ ruleBackground: ruleStyle?.backgroundColor ?? null,
215
+ };
216
+ });
217
+
218
+ const geometryPass =
219
+ observed.geometry.root.width > 0 &&
220
+ observed.geometry.root.height > 0 &&
221
+ observed.geometry.rule !== null &&
222
+ observed.geometry.rule.width > 0 &&
223
+ observed.geometry.rule.height > 0;
224
+ const paintPass =
225
+ observed.ruleBackground !== null &&
226
+ observed.ruleBackground !== 'rgba(0, 0, 0, 0)' &&
227
+ observed.ruleBackground !== 'transparent';
228
+ const png = await page.screenshot({animations: 'disabled', fullPage: true});
229
+ const image = `${item.key}.png`;
230
+ const receipt = {
231
+ schemaVersion: 1,
232
+ expected: {
233
+ build: checkoutSha,
234
+ storyId: STORY_ID,
235
+ mode: item.mode,
236
+ direction: item.direction,
237
+ theme: item.theme,
238
+ viewport: item.viewport,
239
+ },
240
+ observed: {
241
+ build: storybookSha,
242
+ ...observed,
243
+ menuItemCount: await menuItems.count(),
244
+ pageErrors,
245
+ },
246
+ image: {
247
+ file: image,
248
+ sha256: createHash('sha256').update(png).digest('hex'),
249
+ width: png.readUInt32BE(16),
250
+ height: png.readUInt32BE(20),
251
+ },
252
+ checks: {geometryPass, paintPass},
253
+ passed:
254
+ storybookSha === checkoutSha &&
255
+ pageErrors.length === 0 &&
256
+ observed.theme === item.theme &&
257
+ observed.mode === item.mode &&
258
+ observed.direction === item.direction &&
259
+ observed.viewport.width === item.viewport.width &&
260
+ observed.viewport.height === item.viewport.height &&
261
+ observed.reducedMotion &&
262
+ !observed.horizontalOverflow &&
263
+ observed.fonts === 'loaded' &&
264
+ observed.role === 'separator' &&
265
+ observed.orientation === 'horizontal' &&
266
+ observed.tabIndex === null &&
267
+ observed.targetClasses.menuDivider &&
268
+ observed.targetClasses.divider &&
269
+ geometryPass &&
270
+ paintPass,
271
+ };
272
+ fs.writeFileSync(path.join(OUTPUT, image), png);
273
+ fs.writeFileSync(
274
+ path.join(OUTPUT, `${item.key}.sensors.json`),
275
+ `${JSON.stringify(receipt, null, 2)}\n`,
276
+ );
277
+ expect(receipt.passed).toBe(true);
278
+ await context.close();
279
+ });
280
+ }