@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
@@ -0,0 +1,86 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file TokenRemoveTarget.a11y.chromium.spec.ts
5
+ * @input The `Core/PowerSearch` Pre-set Filters story in a built Storybook and
6
+ * real mouse input, in both text directions.
7
+ * @output Real-engine proof that a Token's enlarged remove target stays on its
8
+ * X: a press on the end of the value beside it runs the token's own action,
9
+ * and a press just outside the visible X still removes.
10
+ * @position Run with `pnpm test:a11y-contract`. jsdom has no hit testing, so
11
+ * the remove target's reach is only observable here.
12
+ */
13
+
14
+ import {expect, test, type Page} from '@playwright/test';
15
+ import {
16
+ DEFAULT_STORYBOOK_DIR,
17
+ serveStorybook,
18
+ type StaticServer,
19
+ } from '@astryxdesign/a11y-spec/storybook';
20
+
21
+ let storybook: StaticServer;
22
+
23
+ test.beforeAll(async () => {
24
+ storybook = await serveStorybook(
25
+ process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
26
+ );
27
+ });
28
+
29
+ test.afterAll(async () => {
30
+ await storybook?.close();
31
+ });
32
+
33
+ const STORY = 'core-powersearch--with-preset-filters';
34
+ const REMOVE_BUTTONS = '#storybook-root button[aria-label^="Remove "]';
35
+
36
+ async function open(page: Page, direction: 'ltr' | 'rtl') {
37
+ await page.setViewportSize({width: 900, height: 600});
38
+ await page.goto(
39
+ `${storybook.origin}/iframe.html?id=${STORY}&viewMode=story&globals=direction:${direction}`,
40
+ );
41
+ await expect(page.locator(REMOVE_BUTTONS)).toHaveCount(2);
42
+ const remove = page.locator(REMOVE_BUTTONS).first();
43
+ const value = page
44
+ .locator('#storybook-root')
45
+ .getByText('Open', {exact: true})
46
+ .first();
47
+ const x = await remove.boundingBox();
48
+ const v = await value.boundingBox();
49
+ if (x == null || v == null) {
50
+ throw new Error('the Status token has no visible value or remove button');
51
+ }
52
+ return {remove, x, v};
53
+ }
54
+
55
+ for (const direction of ['ltr', 'rtl'] as const) {
56
+ test(`a press on the end of the value opens its editor (${direction})`, async ({
57
+ page,
58
+ }) => {
59
+ const {x, v} = await open(page, direction);
60
+ // Two pixels inside the value's inline-end edge, level with the X.
61
+ const px = direction === 'ltr' ? v.x + v.width - 2 : v.x + 2;
62
+ const py = x.y + x.height / 2;
63
+ // Precondition: the point is on the value, not on the visible X.
64
+ expect(px < x.x || px > x.x + x.width).toBe(true);
65
+
66
+ await page.mouse.click(px, py);
67
+
68
+ await expect(page.getByRole('combobox', {name: 'Field'})).toBeVisible();
69
+ await expect(page.locator(REMOVE_BUTTONS)).toHaveCount(2);
70
+ });
71
+
72
+ test(`a press just outside the visible X still removes (${direction})`, async ({
73
+ page,
74
+ }) => {
75
+ const {x} = await open(page, direction);
76
+ // Two pixels past the X's inline-end edge: outside the 16px glyph, inside
77
+ // the enlarged target.
78
+ const px = direction === 'ltr' ? x.x + x.width + 2 : x.x - 2;
79
+ const py = x.y + x.height / 2;
80
+
81
+ await page.mouse.click(px, py);
82
+
83
+ await expect(page.locator(REMOVE_BUTTONS)).toHaveCount(1);
84
+ await expect(page.getByRole('combobox', {name: 'Field'})).toHaveCount(0);
85
+ });
86
+ }
@@ -140,14 +140,6 @@ describe('Toolbar', () => {
140
140
  );
141
141
  });
142
142
 
143
- it('sets aria-orientation to vertical', () => {
144
- render(<Toolbar label="Actions" orientation="vertical" />);
145
- expect(screen.getByRole('toolbar')).toHaveAttribute(
146
- 'aria-orientation',
147
- 'vertical',
148
- );
149
- });
150
-
151
143
  it('reflects the size', () => {
152
144
  render(<Toolbar label="Actions" size="sm" />);
153
145
  const toolbar = screen.getByRole('toolbar');
@@ -0,0 +1,163 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Toolbar.a11y.chromium.spec.ts
5
+ * @input Built toolbar stories and TOOLBAR_PATTERN in real Chromium
6
+ * @output Browser evidence for every toolbar expectation across Toolbar's representative states, and a paint check that each state's controls sit on the axis and in the direction it declares
7
+ * @position Chromium lane of the Toolbar binding
8
+ */
9
+
10
+ import {
11
+ expect,
12
+ test,
13
+ type CDPSession,
14
+ type Locator,
15
+ type Page,
16
+ } from '@playwright/test';
17
+ import {
18
+ TOOLBAR_PATTERN,
19
+ blockingResults,
20
+ checkAccessibilitySpec,
21
+ formatFailures,
22
+ neverExercised,
23
+ summarize,
24
+ type BindingResult,
25
+ } from '@astryxdesign/a11y-spec';
26
+ import {
27
+ createChromiumHarness,
28
+ holdMotionStill,
29
+ } from '@astryxdesign/a11y-spec/chromium';
30
+ import {
31
+ DEFAULT_STORYBOOK_DIR,
32
+ serveStorybook,
33
+ type StaticServer,
34
+ } from '@astryxdesign/a11y-spec/storybook';
35
+ import {
36
+ TOOLBAR_BINDING_STATES,
37
+ type ToolbarBindingState,
38
+ } from './Toolbar.a11y.states';
39
+
40
+ let storybook: StaticServer;
41
+
42
+ test.beforeAll(async () => {
43
+ storybook = await serveStorybook(
44
+ process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
45
+ );
46
+ });
47
+
48
+ test.afterAll(async () => {
49
+ await storybook?.close();
50
+ });
51
+
52
+ function storyUrl(storyId: string): string {
53
+ return `${storybook.origin}/iframe.html?id=${storyId}&viewMode=story`;
54
+ }
55
+
56
+ async function mountState(
57
+ page: Page,
58
+ state: ToolbarBindingState,
59
+ ): Promise<{subject: Locator; related: Record<string, Locator>}> {
60
+ await page.goto(storyUrl(state.storyId), {waitUntil: 'load'});
61
+ await holdMotionStill(page);
62
+ const root = page.locator('#storybook-root');
63
+ const subject = root.getByRole('toolbar', {name: state.label, exact: true});
64
+ await expect(subject).toBeVisible();
65
+ const related: Record<string, Locator> = {
66
+ before: root.getByRole('button', {
67
+ name: 'Before the toolbar',
68
+ exact: true,
69
+ }),
70
+ after: root.getByRole('button', {name: 'After the toolbar', exact: true}),
71
+ };
72
+ for (const [relation, name] of Object.entries(state.controls)) {
73
+ related[relation] = subject.getByRole('button', {name, exact: true});
74
+ }
75
+ return {subject, related};
76
+ }
77
+
78
+ async function runState(
79
+ page: Page,
80
+ cdp: CDPSession,
81
+ state: ToolbarBindingState,
82
+ ): Promise<BindingResult> {
83
+ return checkAccessibilitySpec({
84
+ spec: TOOLBAR_PATTERN,
85
+ binding: 'Toolbar',
86
+ state: state.id,
87
+ facts: state.facts,
88
+ mount: async () => {
89
+ const {subject, related} = await mountState(page, state);
90
+ return createChromiumHarness({page, cdp, subject, related});
91
+ },
92
+ });
93
+ }
94
+
95
+ test('every toolbar expectation is exercised by a Toolbar state', async ({
96
+ page,
97
+ }) => {
98
+ const cdp = await page.context().newCDPSession(page);
99
+ const results: BindingResult[] = [];
100
+ for (const state of TOOLBAR_BINDING_STATES) {
101
+ results.push(await runState(page, cdp, state));
102
+ }
103
+ expect(neverExercised(results)).toEqual([]);
104
+ });
105
+
106
+ /**
107
+ * The contract reads `orientation` and `direction` as facts the binding
108
+ * declares. This checks the painted page agrees: the second control sits
109
+ * below the first in a vertical state, and after it in reading order — to
110
+ * its right, or its left under right-to-left — in a horizontal one.
111
+ */
112
+ for (const state of TOOLBAR_BINDING_STATES) {
113
+ test(`Toolbar [${state.id}] paints its controls on the declared axis and direction`, async ({
114
+ page,
115
+ }) => {
116
+ const {related} = await mountState(page, state);
117
+ const first = await related['first-control'].boundingBox();
118
+ const second = await related['second-control'].boundingBox();
119
+ if (first == null || second == null) {
120
+ throw new Error(`state "${state.id}" has an unpainted control`);
121
+ }
122
+ const firstMiddle = {
123
+ x: first.x + first.width / 2,
124
+ y: first.y + first.height / 2,
125
+ };
126
+ const secondMiddle = {
127
+ x: second.x + second.width / 2,
128
+ y: second.y + second.height / 2,
129
+ };
130
+ if (state.facts.orientation === 'vertical') {
131
+ expect(second.y, 'second control below the first').toBeGreaterThanOrEqual(
132
+ first.y + first.height,
133
+ );
134
+ } else {
135
+ expect(
136
+ Math.abs(secondMiddle.y - firstMiddle.y),
137
+ 'controls share a row',
138
+ ).toBeLessThan(first.height / 2);
139
+ if (state.facts.direction === 'rtl') {
140
+ expect(secondMiddle.x, 'second control left of the first').toBeLessThan(
141
+ firstMiddle.x,
142
+ );
143
+ } else {
144
+ expect(
145
+ secondMiddle.x,
146
+ 'second control right of the first',
147
+ ).toBeGreaterThan(firstMiddle.x);
148
+ }
149
+ }
150
+ });
151
+ }
152
+
153
+ for (const state of TOOLBAR_BINDING_STATES) {
154
+ test(`Toolbar [${state.id}] — ${state.summary}`, async ({page}) => {
155
+ const cdp = await page.context().newCDPSession(page);
156
+ const result = await runState(page, cdp, state);
157
+ const report = summarize(TOOLBAR_PATTERN, [result]);
158
+ // Required rows block (AST-020 FR9).
159
+ expect(formatFailures(blockingResults([result]))).toBe('');
160
+ expect(report.unrunLayers).toEqual([]);
161
+ expect(report.counts.unexpectedPass).toBe(0);
162
+ });
163
+ }
@@ -0,0 +1,63 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file Toolbar.a11y.renders.tsx
7
+ * @input One toolbar binding state from Toolbar.a11y.states.ts
8
+ * @output The React tree every lane renders for that state (the jsdom test directly, the story through Storybook): a page button before the Toolbar, the Toolbar, and a page button after it
9
+ * @position Shared fixture for the Toolbar binding
10
+ */
11
+
12
+ import type {ReactElement} from 'react';
13
+ import {Button} from '../../Button/Button';
14
+ import {VStack} from '../../VStack/VStack';
15
+ import {Toolbar} from '../Toolbar';
16
+ import type {ToolbarBindingState, ToolbarControl} from './Toolbar.a11y.states';
17
+
18
+ /**
19
+ * One slot's controls. A slot lays its children out in a row, so a vertical
20
+ * toolbar's caller stacks them, as a product toolbar would, to arrange the
21
+ * controls on the axis the state declares.
22
+ */
23
+ function controls(
24
+ list: ReadonlyArray<ToolbarControl>,
25
+ orientation: ToolbarBindingState['facts']['orientation'],
26
+ ): ReactElement | undefined {
27
+ if (list.length === 0) {
28
+ return undefined;
29
+ }
30
+ const buttons = list.map(control => (
31
+ <Button
32
+ key={control.label}
33
+ label={control.label}
34
+ variant="ghost"
35
+ isDisabled={control.isDisabled}
36
+ />
37
+ ));
38
+ return orientation === 'vertical' ? (
39
+ <VStack gap={1}>{buttons}</VStack>
40
+ ) : (
41
+ <>{buttons}</>
42
+ );
43
+ }
44
+
45
+ export function renderToolbarState(
46
+ state: Pick<
47
+ ToolbarBindingState,
48
+ 'label' | 'startControls' | 'endControls' | 'facts'
49
+ >,
50
+ ): ReactElement {
51
+ return (
52
+ <div dir={state.facts.direction} style={{padding: 24, maxWidth: 640}}>
53
+ <button type="button">Before the toolbar</button>
54
+ <Toolbar
55
+ label={state.label}
56
+ orientation={state.facts.orientation}
57
+ startContent={controls(state.startControls, state.facts.orientation)}
58
+ endContent={controls(state.endControls, state.facts.orientation)}
59
+ />
60
+ <button type="button">After the toolbar</button>
61
+ </div>
62
+ );
63
+ }
@@ -0,0 +1,142 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Toolbar.a11y.states.ts
5
+ * @input Toolbar's representative states and the facts TOOLBAR_PATTERN reads
6
+ * @output TOOLBAR_BINDING_STATES and the explicit ownership exclusions for this binding
7
+ * @position Binding inventory shared by the jsdom test and the Chromium spec (Toolbar under spec:AST-020 FR15)
8
+ */
9
+
10
+ import type {ToolbarStateFacts} from '@astryxdesign/a11y-spec';
11
+
12
+ export interface ToolbarControl {
13
+ readonly label: string;
14
+ readonly isDisabled?: boolean;
15
+ }
16
+
17
+ export interface ToolbarBindingState {
18
+ readonly id: string;
19
+ readonly summary: string;
20
+ readonly storyId: string;
21
+ /** The Toolbar `label`, which names the toolbar. */
22
+ readonly label: string;
23
+ /** Controls in the start slot, in order. */
24
+ readonly startControls: ReadonlyArray<ToolbarControl>;
25
+ /** Controls in the end slot, in order. */
26
+ readonly endControls: ReadonlyArray<ToolbarControl>;
27
+ /** Relation name → accessible name of the control it designates. */
28
+ readonly controls: Readonly<
29
+ Record<'first-control' | 'second-control' | 'last-control', string>
30
+ >;
31
+ readonly facts: ToolbarStateFacts;
32
+ }
33
+
34
+ const story = (id: string) => `a11y-toolbar--${id}`;
35
+
36
+ const FORMATTING = [
37
+ {label: 'Bold'},
38
+ {label: 'Italic'},
39
+ {label: 'Underline'},
40
+ ] as const;
41
+
42
+ const FORMATTING_CONTROLS = {
43
+ 'first-control': 'Bold',
44
+ 'second-control': 'Italic',
45
+ 'last-control': 'Settings',
46
+ } as const;
47
+
48
+ const HORIZONTAL_LTR: ToolbarStateFacts = {
49
+ orientation: 'horizontal',
50
+ direction: 'ltr',
51
+ wraps: true,
52
+ homeEnd: true,
53
+ };
54
+
55
+ export const TOOLBAR_BINDING_STATES = [
56
+ {
57
+ id: 'horizontal',
58
+ summary:
59
+ 'a named horizontal Toolbar with three formatting buttons in the start slot and Settings in the end slot',
60
+ storyId: story('horizontal'),
61
+ label: 'Text formatting',
62
+ startControls: FORMATTING,
63
+ endControls: [{label: 'Settings'}],
64
+ controls: FORMATTING_CONTROLS,
65
+ facts: HORIZONTAL_LTR,
66
+ },
67
+ {
68
+ id: 'horizontal-rtl',
69
+ summary:
70
+ 'the same Toolbar inside a right-to-left container, where ArrowLeft moves to the next control',
71
+ storyId: story('horizontal-rtl'),
72
+ label: 'Text formatting',
73
+ startControls: FORMATTING,
74
+ endControls: [{label: 'Settings'}],
75
+ controls: FORMATTING_CONTROLS,
76
+ facts: {...HORIZONTAL_LTR, direction: 'rtl'},
77
+ },
78
+ {
79
+ id: 'vertical',
80
+ summary:
81
+ 'a vertical Toolbar of drawing tools, moved through with ArrowDown and ArrowUp',
82
+ storyId: story('vertical'),
83
+ label: 'Drawing tools',
84
+ startControls: [{label: 'Select'}, {label: 'Pen'}, {label: 'Eraser'}],
85
+ endControls: [],
86
+ controls: {
87
+ 'first-control': 'Select',
88
+ 'second-control': 'Pen',
89
+ 'last-control': 'Eraser',
90
+ },
91
+ facts: {...HORIZONTAL_LTR, orientation: 'vertical'},
92
+ },
93
+ {
94
+ id: 'first-control-disabled',
95
+ summary:
96
+ 'a Toolbar whose first button is disabled, so Tab enters on the first enabled button and the arrows skip the disabled one',
97
+ storyId: story('first-control-disabled'),
98
+ label: 'Edit actions',
99
+ startControls: [
100
+ {label: 'Undo', isDisabled: true},
101
+ {label: 'Cut'},
102
+ {label: 'Copy'},
103
+ ],
104
+ endControls: [{label: 'Paste'}],
105
+ controls: {
106
+ 'first-control': 'Cut',
107
+ 'second-control': 'Copy',
108
+ 'last-control': 'Paste',
109
+ },
110
+ facts: HORIZONTAL_LTR,
111
+ },
112
+ ] as const satisfies ReadonlyArray<ToolbarBindingState>;
113
+
114
+ export const TOOLBAR_EXCLUSIONS = [
115
+ {
116
+ id: 'control-semantics',
117
+ owner:
118
+ 'the button, toggle-button and menu-button patterns and their bindings',
119
+ reason:
120
+ 'Each control placed in a Toolbar exposes its own role, name, state and activation through its own pattern; this binding renders plain Buttons only so the toolbar container and the movement between controls are what is checked.',
121
+ },
122
+ {
123
+ id: 'caret-guard-and-nested-popovers',
124
+ owner:
125
+ 'Toolbar.test.tsx (caret-guard, nested popover and onKeyDown composition suites)',
126
+ reason:
127
+ 'Leaving caret keys to a text field mid-line, leaving keys inside an open MoreMenu to that menu, and composing a caller onKeyDown are binding behaviour the APG leaves to the author, so no state here places a text field or popover in the toolbar.',
128
+ },
129
+ {
130
+ id: 'three-lane-layout',
131
+ owner:
132
+ 'Toolbar.test.tsx (centerContent and three-slot layout tests) and the repository visual gate',
133
+ reason:
134
+ 'Center content switches the row to a three-column grid, but the controls stay in DOM order inside the same toolbar element and the same roving tab stop walks them, so the role, name, Tab stop and arrow order this contract reads do not change; the grid itself is layout paint.',
135
+ },
136
+ {
137
+ id: 'outer-chrome',
138
+ owner: 'component:Section and the repository visual gate',
139
+ reason:
140
+ 'Variant, dividers, block padding and edge compensation are Section and layout paint around the toolbar row; they change no role, name, Tab stop or arrow movement, so every state uses the default transparent chrome.',
141
+ },
142
+ ] as const;
@@ -0,0 +1,126 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ /** @vitest-environment jsdom */
3
+
4
+ /**
5
+ * @file Toolbar.a11y.test.tsx
6
+ * @input TOOLBAR_PATTERN bound to Toolbar's representative states
7
+ * @output jsdom-lane proof of the DOM expectations and an honest "unrun" report for the browser layers
8
+ * @position Binding test for Toolbar (the Chromium spec covers the rest)
9
+ */
10
+
11
+ import {cleanup, render, screen} from '@testing-library/react';
12
+ import {describe, expect, it} from 'vitest';
13
+ import {
14
+ TOOLBAR_PATTERN,
15
+ checkAccessibilitySpec,
16
+ createJsdomHarness,
17
+ expectAccessibilitySpec,
18
+ summarize,
19
+ type BindingResult,
20
+ } from '@astryxdesign/a11y-spec';
21
+ import {renderToolbarState} from './Toolbar.a11y.renders';
22
+ import {
23
+ TOOLBAR_BINDING_STATES,
24
+ TOOLBAR_EXCLUSIONS,
25
+ type ToolbarBindingState,
26
+ } from './Toolbar.a11y.states';
27
+
28
+ function toolbarFor(state: ToolbarBindingState): HTMLElement {
29
+ return screen.getByRole('toolbar', {name: state.label});
30
+ }
31
+
32
+ function relatedFor(
33
+ state: ToolbarBindingState,
34
+ ): Readonly<Record<string, Element>> {
35
+ const related: Record<string, Element> = {
36
+ before: screen.getByRole('button', {name: 'Before the toolbar'}),
37
+ after: screen.getByRole('button', {name: 'After the toolbar'}),
38
+ };
39
+ for (const [relation, name] of Object.entries(state.controls)) {
40
+ related[relation] = screen.getByRole('button', {name});
41
+ }
42
+ return related;
43
+ }
44
+
45
+ async function checkState(state: ToolbarBindingState): Promise<BindingResult> {
46
+ return checkAccessibilitySpec({
47
+ spec: TOOLBAR_PATTERN,
48
+ binding: 'Toolbar',
49
+ state: state.id,
50
+ facts: state.facts,
51
+ mount: async () => {
52
+ render(renderToolbarState(state));
53
+ return createJsdomHarness({
54
+ subject: toolbarFor(state),
55
+ related: relatedFor(state),
56
+ });
57
+ },
58
+ unmount: cleanup,
59
+ });
60
+ }
61
+
62
+ describe('Toolbar — toolbar pattern, jsdom lane', () => {
63
+ it.each(
64
+ TOOLBAR_BINDING_STATES.map(
65
+ state => [state.id, state.summary, state] as const,
66
+ ),
67
+ )('%s — %s', async (_id, _summary, state) => {
68
+ await expectAccessibilitySpec({
69
+ spec: TOOLBAR_PATTERN,
70
+ binding: 'Toolbar',
71
+ state: state.id,
72
+ facts: state.facts,
73
+ render: () => {
74
+ render(renderToolbarState(state));
75
+ },
76
+ subject: () => toolbarFor(state),
77
+ related: () => relatedFor(state),
78
+ cleanup,
79
+ });
80
+ });
81
+
82
+ it('runs the DOM layer and reports the browser-owned outcomes as unrun', async () => {
83
+ const results: BindingResult[] = [];
84
+ for (const state of TOOLBAR_BINDING_STATES) {
85
+ results.push(await checkState(state));
86
+ }
87
+ const report = summarize(TOOLBAR_PATTERN, results);
88
+ expect(report.counts.pass).toBeGreaterThan(0);
89
+ expect(report.unrunLayers).toEqual(['accessibility-tree', 'real-browser']);
90
+ expect(report.counts.unexpectedPass).toBe(0);
91
+ });
92
+ });
93
+
94
+ describe('toolbar binding inventory', () => {
95
+ it('gives every representative state a distinct checked-in story', () => {
96
+ const stories = TOOLBAR_BINDING_STATES.map(state => state.storyId);
97
+ expect(new Set(stories).size).toBe(stories.length);
98
+ });
99
+
100
+ it('keeps control semantics, editing and popover keys, the three-lane layout, and outer chrome explicit', () => {
101
+ expect(TOOLBAR_EXCLUSIONS.map(row => row.id)).toEqual([
102
+ 'control-semantics',
103
+ 'caret-guard-and-nested-popovers',
104
+ 'three-lane-layout',
105
+ 'outer-chrome',
106
+ ]);
107
+ for (const exclusion of TOOLBAR_EXCLUSIONS) {
108
+ expect(exclusion.owner.length, exclusion.id).toBeGreaterThan(20);
109
+ expect(exclusion.reason.length, exclusion.id).toBeGreaterThan(80);
110
+ }
111
+ });
112
+
113
+ it('renders the toolbar and every control relation it names for every state', () => {
114
+ for (const state of TOOLBAR_BINDING_STATES) {
115
+ render(renderToolbarState(state));
116
+ const toolbar = toolbarFor(state);
117
+ for (const name of Object.values(state.controls)) {
118
+ expect(
119
+ toolbar.contains(screen.getByRole('button', {name})),
120
+ `${state.id}: ${name}`,
121
+ ).toBe(true);
122
+ }
123
+ cleanup();
124
+ }
125
+ });
126
+ });
@@ -769,6 +769,53 @@ describe('TopNavMegaMenu — drawer mode', () => {
769
769
  expect(handleClick).toHaveBeenCalledTimes(1);
770
770
  });
771
771
 
772
+ it('makes collapsed items inert and hidden from assistive technology', async () => {
773
+ const user = userEvent.setup();
774
+ render(
775
+ <TopNavRenderContext value="drawer">
776
+ <TopNavMegaMenu
777
+ label="Products"
778
+ items={<TopNavMegaMenuItem title="Analytics" href="/analytics" />}
779
+ />
780
+ </TopNavRenderContext>,
781
+ );
782
+ const trigger = screen.getByRole('button', {name: 'Products'});
783
+ const region = document.getElementById(
784
+ trigger.getAttribute('aria-controls') ?? '',
785
+ );
786
+
787
+ expect(region).toHaveAttribute('inert');
788
+ expect(region).toHaveAttribute('aria-hidden', 'true');
789
+ expect(screen.queryByRole('link', {name: 'Analytics'})).toBeNull();
790
+
791
+ await user.click(trigger);
792
+ expect(region).not.toHaveAttribute('inert');
793
+ expect(region).toHaveAttribute('aria-hidden', 'false');
794
+ expect(screen.getByRole('link', {name: 'Analytics'})).toBeInTheDocument();
795
+ });
796
+
797
+ it('clips the drawer section only while it is collapsed', async () => {
798
+ // The ring paints outside the item, so a permanent clip would cut it off.
799
+ const user = userEvent.setup();
800
+ render(
801
+ <TopNavRenderContext value="drawer">
802
+ <TopNavMegaMenu
803
+ label="Products"
804
+ items={<TopNavMegaMenuItem title="Analytics" href="/analytics" />}
805
+ />
806
+ </TopNavRenderContext>,
807
+ );
808
+ const trigger = screen.getByRole('button', {name: 'Products'});
809
+ const section = document.getElementById(
810
+ trigger.getAttribute('aria-controls') ?? '',
811
+ )!;
812
+ expect(getComputedStyle(section).overflow).toBe('hidden');
813
+ expect(getComputedStyle(section.firstElementChild!).overflow).toBe('');
814
+
815
+ await user.click(trigger);
816
+ expect(getComputedStyle(section).overflow).toBe('visible');
817
+ });
818
+
772
819
  it('renders featured content when expanded', async () => {
773
820
  const user = userEvent.setup();
774
821
 
@@ -229,18 +229,22 @@ const styles = stylex.create({
229
229
  drawerChevronExpanded: {
230
230
  transform: 'rotate(180deg)',
231
231
  },
232
+ // Clips only while collapsed or animating; see TopNavMenu drawerStyles.items.
232
233
  drawerItems: {
233
234
  display: 'grid',
234
235
  gridTemplateRows: '0fr',
235
- transitionProperty: 'grid-template-rows',
236
- transitionDuration: durationVars['--duration-medium'],
236
+ overflow: 'hidden',
237
+ transitionProperty: 'grid-template-rows, overflow',
238
+ transitionBehavior: 'allow-discrete',
239
+ transitionDuration: `${durationVars['--duration-medium']}, 0s`,
237
240
  transitionTimingFunction: easeVars['--ease-standard'],
238
241
  },
239
242
  drawerItemsExpanded: {
240
243
  gridTemplateRows: '1fr',
244
+ overflow: 'visible',
245
+ transitionDelay: `0s, ${durationVars['--duration-medium']}`,
241
246
  },
242
247
  drawerItemsInner: {
243
- overflow: 'hidden',
244
248
  minHeight: 0,
245
249
  },
246
250
 
@@ -595,6 +599,8 @@ function DrawerMegaMenu({
595
599
  {/* Animated expand/collapse container */}
596
600
  <div
597
601
  id={`${menuId}-items`}
602
+ aria-hidden={!isExpanded}
603
+ inert={!isExpanded ? true : undefined}
598
604
  {...stylex.props(
599
605
  styles.drawerItems,
600
606
  isExpanded && styles.drawerItemsExpanded,