@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
@@ -233,14 +233,59 @@ describe('TopNavMenu — drawer focus ring', () => {
233
233
  expectSharedFocusRing(screen.getByRole('button', {name: /Products/}));
234
234
  });
235
235
 
236
- it('draws the shared ring on a drawer item', () => {
236
+ it('draws the shared ring on a drawer item', async () => {
237
+ const user = userEvent.setup();
237
238
  render(
238
239
  <TopNavRenderContext value="drawer">
239
240
  <TopNavMenu label="Products" items={mockItems} />
240
241
  </TopNavRenderContext>,
241
242
  );
243
+ await user.click(screen.getByRole('button', {name: /Products/}));
242
244
  expectSharedFocusRing(screen.getByRole('link', {name: /Analytics/}));
243
245
  });
246
+
247
+ it('clips the drawer section only while it is collapsed', async () => {
248
+ // The ring paints outside the item, so a permanent clip would cut it off.
249
+ const user = userEvent.setup();
250
+ render(
251
+ <TopNavRenderContext value="drawer">
252
+ <TopNavMenu label="Products" items={mockItems} />
253
+ </TopNavRenderContext>,
254
+ );
255
+ const trigger = screen.getByRole('button', {name: /Products/});
256
+ const section = document.getElementById(
257
+ trigger.getAttribute('aria-controls') ?? '',
258
+ )!;
259
+ expect(getComputedStyle(section).overflow).toBe('hidden');
260
+ expect(getComputedStyle(section.firstElementChild!).overflow).toBe('');
261
+
262
+ await user.click(trigger);
263
+ expect(getComputedStyle(section).overflow).toBe('visible');
264
+ });
265
+ });
266
+
267
+ describe('TopNavMenu — drawer collapsed section', () => {
268
+ it('makes collapsed items inert and hidden from assistive technology', async () => {
269
+ const user = userEvent.setup();
270
+ render(
271
+ <TopNavRenderContext value="drawer">
272
+ <TopNavMenu label="Products" items={mockItems} />
273
+ </TopNavRenderContext>,
274
+ );
275
+ const trigger = screen.getByRole('button', {name: /Products/});
276
+ const region = document.getElementById(
277
+ trigger.getAttribute('aria-controls') ?? '',
278
+ );
279
+
280
+ expect(region).toHaveAttribute('inert');
281
+ expect(region).toHaveAttribute('aria-hidden', 'true');
282
+ expect(screen.queryByRole('link', {name: /Analytics/})).toBeNull();
283
+
284
+ await user.click(trigger);
285
+ expect(region).not.toHaveAttribute('inert');
286
+ expect(region).toHaveAttribute('aria-hidden', 'false');
287
+ expect(screen.getByRole('link', {name: /Analytics/})).toBeInTheDocument();
288
+ });
244
289
  });
245
290
 
246
291
  describe('TopNavMenu pass-through props', () => {
@@ -194,18 +194,26 @@ const drawerStyles = stylex.create({
194
194
  chevronExpanded: {
195
195
  transform: 'rotate(180deg)',
196
196
  },
197
+ // The section clips its items only for the 0fr collapse animation: a
198
+ // permanent clip cuts off the focus ring, which paints outside the item.
199
+ // `overflow` is discrete, so it rides the transition with `allow-discrete`
200
+ // and switches to `visible` only after the open has finished. Collapsing
201
+ // clips again at once.
197
202
  items: {
198
203
  display: 'grid',
199
204
  gridTemplateRows: '0fr',
200
- transitionProperty: 'grid-template-rows',
201
- transitionDuration: durationVars['--duration-medium'],
205
+ overflow: 'hidden',
206
+ transitionProperty: 'grid-template-rows, overflow',
207
+ transitionBehavior: 'allow-discrete',
208
+ transitionDuration: `${durationVars['--duration-medium']}, 0s`,
202
209
  transitionTimingFunction: easeVars['--ease-standard'],
203
210
  },
204
211
  itemsExpanded: {
205
212
  gridTemplateRows: '1fr',
213
+ overflow: 'visible',
214
+ transitionDelay: `0s, ${durationVars['--duration-medium']}`,
206
215
  },
207
216
  itemsInner: {
208
- overflow: 'hidden',
209
217
  minHeight: 0,
210
218
  },
211
219
  item: {
@@ -486,6 +494,8 @@ export function TopNavMenu({
486
494
  </button>
487
495
  <div
488
496
  id={`${menuId}-items`}
497
+ aria-hidden={!drawerExpanded}
498
+ inert={!drawerExpanded ? true : undefined}
489
499
  {...stylex.props(
490
500
  drawerStyles.items,
491
501
  drawerExpanded && drawerStyles.itemsExpanded,
@@ -89,6 +89,7 @@ export const docs = {
89
89
  {name: '--tree-list-indent', description: 'Per-level indentation step. Each nesting level indents its rows by this distance, and the guide lines follow it so they stay aligned. Set it on the `tree-list` target to retune the metric (e.g. `var(--spacing-5)` for a wider indent).', default: 'var(--spacing-4)'},
90
90
  {name: '--tree-list-row-gap', description: 'Vertical gap between adjacent rows. Default `2px` (var(--spacing-0-5)) gives a subtle separation; set it on the `tree-list` target to widen or close the gap. The connector guides span the gap automatically (the line stays continuous) and do not overhang the last row, so no guide-height tuning is needed.', default: 'var(--spacing-0-5)'},
91
91
  {name: '--_tree-indent', description: 'Distance one row is indented, computed per row from --tree-list-indent and the row depth. Set --tree-list-indent to retune indentation; this is the resolved value.', default: '0px', private: true},
92
+ {name: '--_tree-focus-z', description: 'Stacking order of a row while its treeitem has keyboard focus, published by each treeitem so its focused row paints above the guide lines. Not a theming lever.', default: 'auto', private: true},
92
93
  ],
93
94
  },
94
95
  components: [
@@ -11,6 +11,7 @@
11
11
 
12
12
  import {describe, it, expect, vi} from 'vitest';
13
13
  import {render, screen, fireEvent} from '@testing-library/react';
14
+ import type {ReactNode} from 'react';
14
15
  import userEvent from '@testing-library/user-event';
15
16
  import * as stylex from '@stylexjs/stylex';
16
17
  import {TreeList} from './TreeList';
@@ -609,12 +610,12 @@ describe('TreeList', () => {
609
610
  });
610
611
 
611
612
  it("variant='noGuides' preserves per-level indentation on the rows", () => {
612
- // Indentation lives on the row's margin-inline-start (not the guide
613
- // element), so it must survive when the connectors are suppressed. The
614
- // per-row distance is published as the `--_tree-indent` custom property
615
- // (not an inline longhand — see #4308), so the theme layer can override
616
- // the `margin-inline-start` declaration. A deeper row is indented more
617
- // than a shallower one.
613
+ // Indentation lives inside the row's padding-inline-start (not the
614
+ // guide element), so it must survive when the connectors are
615
+ // suppressed. The per-row distance is published as the `--_tree-indent`
616
+ // custom property (not an inline longhand — see #4308), so the theme
617
+ // layer can override the `padding-inline-start` declaration. A deeper
618
+ // row is indented more than a shallower one.
618
619
  const {container} = render(
619
620
  <TreeList items={deepItems} variant="noGuides" />,
620
621
  );
@@ -720,6 +721,326 @@ describe('TreeList', () => {
720
721
  expect(indentOf('Mid')).toContain('var(--tree-list-indent)');
721
722
  expect(indentOf('Leaf')).toContain('var(--tree-list-indent)');
722
723
  });
724
+
725
+ it('indents inside the row so nested rows keep the full list width', () => {
726
+ // The row box keeps its own inline padding and stays full width; the
727
+ // indent is padding on a child inside it. A nested row's hover and
728
+ // selected fill therefore spans the list like a root row's, while its
729
+ // content lands where a margin indent would have put it.
730
+ render(<TreeList items={deepItems} />);
731
+ const rules = collectCssText();
732
+ expect(rules).toMatch(
733
+ /padding-inline-start:\s*var\(--_tree-indent,\s*0px\)/,
734
+ );
735
+ expect(rules).not.toMatch(/margin-inline-start:\s*var\(--_tree-indent/);
736
+
737
+ const row = screen
738
+ .getByText('Leaf')
739
+ .closest<HTMLElement>('.astryx-tree-list-item')!;
740
+ const indented = row.firstElementChild as HTMLElement;
741
+ expect(indented.className).toContain('indentedContent');
742
+ expect(row.className).not.toContain('indentedContent');
743
+ });
744
+
745
+ it("keeps the indent when a caller sets the row's padding", () => {
746
+ // Caller padding lands on the row box; the indent is on a separate
747
+ // element inside it, so the two compose instead of one replacing the
748
+ // other.
749
+ const items = [
750
+ {
751
+ id: 'root',
752
+ label: 'Root',
753
+ isExpanded: true,
754
+ children: [
755
+ {id: 'leaf', label: 'Leaf', style: {paddingInlineStart: '20px'}},
756
+ ],
757
+ },
758
+ ];
759
+ render(<TreeList items={items} />);
760
+ const row = screen
761
+ .getByText('Leaf')
762
+ .closest<HTMLElement>('.astryx-tree-list-item')!;
763
+ expect(row.style.paddingInlineStart).toBe('20px');
764
+ expect(row.getAttribute('style')).toContain('--_tree-indent');
765
+ expect((row.firstElementChild as HTMLElement).className).toContain(
766
+ 'indentedContent',
767
+ );
768
+ });
769
+
770
+ describe('link rows', () => {
771
+ const linkItems = (endContent?: ReactNode): TreeListItemData[] => [
772
+ {
773
+ id: 'docs',
774
+ label: 'Docs',
775
+ isExpanded: true,
776
+ children: [
777
+ {id: 'report', label: 'Report', href: '/report', endContent},
778
+ ],
779
+ },
780
+ ];
781
+ const rowSurfaceOf = (link: HTMLElement) =>
782
+ link.closest<HTMLElement>('.astryx-tree-list-item')!
783
+ .firstElementChild as HTMLElement;
784
+
785
+ it('follows the link when the row is clicked outside its label', () => {
786
+ // The indent is part of the row now, so a click there must do what
787
+ // the row does. The click is delegated to the anchor through the
788
+ // shared enlarged-surface hook, modifier keys included.
789
+ render(<TreeList items={linkItems()} />);
790
+ const link = screen.getByRole('link', {name: 'Report'});
791
+ const clicks: MouseEvent[] = [];
792
+ link.addEventListener('click', e => {
793
+ clicks.push(e);
794
+ e.preventDefault();
795
+ });
796
+
797
+ fireEvent.click(rowSurfaceOf(link), {metaKey: true});
798
+ expect(clicks).toHaveLength(1);
799
+ expect(clicks[0].metaKey).toBe(true);
800
+
801
+ // The row box's own padding follows the link too.
802
+ fireEvent.click(rowSurfaceOf(link).parentElement!);
803
+ expect(clicks).toHaveLength(2);
804
+
805
+ // Clicking the label itself reaches the anchor once, not twice.
806
+ fireEvent.click(link);
807
+ expect(clicks).toHaveLength(3);
808
+ });
809
+
810
+ it('reports one click to ancestors per press', () => {
811
+ // The delegated click bubbles from the anchor; the original stops,
812
+ // so a listener above the tree sees exactly one click.
813
+ const onTreeClick = vi.fn();
814
+ render(<TreeList items={linkItems()} onClick={onTreeClick} />);
815
+ const link = screen.getByRole('link', {name: 'Report'});
816
+ link.addEventListener('click', e => e.preventDefault());
817
+
818
+ fireEvent.click(rowSurfaceOf(link));
819
+ expect(onTreeClick).toHaveBeenCalledTimes(1);
820
+ expect(onTreeClick.mock.calls[0][0].target).toBe(link);
821
+ });
822
+
823
+ it('leaves a role-based control in the row to itself', () => {
824
+ const onSwitch = vi.fn();
825
+ render(
826
+ <TreeList
827
+ items={linkItems(
828
+ <span
829
+ role="switch"
830
+ aria-checked="false"
831
+ aria-label="Pin"
832
+ tabIndex={0}
833
+ onClick={onSwitch}>
834
+ Pin
835
+ </span>,
836
+ )}
837
+ />,
838
+ );
839
+ const link = screen.getByRole('link', {name: 'Report'});
840
+ const onLinkClick = vi.fn((e: Event) => e.preventDefault());
841
+ link.addEventListener('click', onLinkClick);
842
+
843
+ fireEvent.click(screen.getByRole('switch', {name: 'Pin'}));
844
+ expect(onSwitch).toHaveBeenCalledTimes(1);
845
+ expect(onLinkClick).not.toHaveBeenCalled();
846
+ });
847
+
848
+ it('opens the link in a new tab on a middle-click in the row', () => {
849
+ const open = vi.spyOn(window, 'open').mockImplementation(() => null);
850
+ render(<TreeList items={linkItems()} />);
851
+ const link = screen.getByRole('link', {name: 'Report'});
852
+
853
+ fireEvent.mouseUp(rowSurfaceOf(link), {button: 1});
854
+ expect(open).toHaveBeenCalledWith('/report', '_blank', 'noopener');
855
+
856
+ // A middle-click on the label is the browser's own.
857
+ open.mockClear();
858
+ fireEvent.mouseUp(link, {button: 1});
859
+ expect(open).not.toHaveBeenCalled();
860
+ open.mockRestore();
861
+ });
862
+ });
863
+
864
+ it('does not follow a disabled link row', () => {
865
+ const items = [
866
+ {
867
+ id: 'docs',
868
+ label: 'Docs',
869
+ isExpanded: true,
870
+ children: [
871
+ {id: 'report', label: 'Report', href: '/report', isDisabled: true},
872
+ ],
873
+ },
874
+ ];
875
+ render(<TreeList items={items} />);
876
+ const link = screen.getByRole('link', {name: 'Report', hidden: true});
877
+ const onLinkClick = vi.fn((e: Event) => e.preventDefault());
878
+ link.addEventListener('click', onLinkClick);
879
+ const row = link.closest<HTMLElement>('.astryx-tree-list-item')!;
880
+ fireEvent.click(row.firstElementChild!);
881
+ expect(onLinkClick).not.toHaveBeenCalled();
882
+ });
883
+
884
+ describe('row clicks outside the label (FR7)', () => {
885
+ // Each row sits under an expanded root, so it has an indent; the click
886
+ // lands on the row surface (indent, reserved chevron column, padding),
887
+ // not on the label.
888
+ const underRoot = (children: TreeListItemData[]): TreeListItemData[] => [
889
+ {id: 'root', label: 'Root', isExpanded: true, children},
890
+ ];
891
+ // The row box itself (its own padding) and the indented content inside
892
+ // it: FR7 covers clicks on both.
893
+ const rowOf = (label: string) =>
894
+ screen.getByText(label).closest<HTMLElement>('.astryx-tree-list-item')!;
895
+ const rowSurfaceOf = (label: string) =>
896
+ rowOf(label).firstElementChild as HTMLElement;
897
+ const folder: TreeListItemData = {
898
+ id: 'folder',
899
+ label: 'Folder',
900
+ children: [{id: 'file', label: 'File'}],
901
+ };
902
+
903
+ it("runs an action row's onClick once", () => {
904
+ const onClick = vi.fn();
905
+ render(
906
+ <TreeList items={underRoot([{id: 'run', label: 'Run', onClick}])} />,
907
+ );
908
+ fireEvent.click(rowSurfaceOf('Run'));
909
+ expect(onClick).toHaveBeenCalledTimes(1);
910
+ fireEvent.click(rowOf('Run'));
911
+ expect(onClick).toHaveBeenCalledTimes(2);
912
+
913
+ // The label's own button still runs it once per press, not twice.
914
+ fireEvent.click(screen.getByRole('button', {name: 'Run'}));
915
+ expect(onClick).toHaveBeenCalledTimes(3);
916
+ });
917
+
918
+ it('toggles a parent row', () => {
919
+ render(<TreeList items={underRoot([folder])} />);
920
+ expect(screen.queryByText('File')).not.toBeInTheDocument();
921
+ fireEvent.click(rowSurfaceOf('Folder'));
922
+ expect(screen.getByText('File')).toBeInTheDocument();
923
+ fireEvent.click(rowOf('Folder'));
924
+ expect(screen.queryByText('File')).not.toBeInTheDocument();
925
+ });
926
+
927
+ it('does nothing on a disabled action or parent row', () => {
928
+ const onClick = vi.fn();
929
+ render(
930
+ <TreeList
931
+ items={underRoot([
932
+ {id: 'run', label: 'Run', onClick, isDisabled: true},
933
+ {...folder, isDisabled: true},
934
+ ])}
935
+ />,
936
+ );
937
+ fireEvent.click(rowSurfaceOf('Run'));
938
+ fireEvent.click(rowOf('Run'));
939
+ expect(onClick).not.toHaveBeenCalled();
940
+ fireEvent.click(rowSurfaceOf('Folder'));
941
+ fireEvent.click(rowOf('Folder'));
942
+ expect(screen.queryByText('File')).not.toBeInTheDocument();
943
+ });
944
+
945
+ it('leaves a role-based control in start or end content to itself', () => {
946
+ // Native and role-based controls alike keep their own activation,
947
+ // the same rule link rows get from the shared enlarged-surface hook.
948
+ const onClick = vi.fn();
949
+ const onSwitch = vi.fn();
950
+ const pin = (name: string) => (
951
+ <span
952
+ role="switch"
953
+ aria-checked="false"
954
+ aria-label={name}
955
+ tabIndex={0}
956
+ onClick={onSwitch}>
957
+ {name}
958
+ </span>
959
+ );
960
+ render(
961
+ <TreeList
962
+ items={underRoot([
963
+ {
964
+ id: 'run',
965
+ label: 'Run',
966
+ onClick,
967
+ startContent: pin('Pin run start'),
968
+ endContent: pin('Pin run end'),
969
+ },
970
+ {
971
+ ...folder,
972
+ startContent: pin('Pin folder start'),
973
+ endContent: pin('Pin folder end'),
974
+ },
975
+ ])}
976
+ />,
977
+ );
978
+ for (const name of [
979
+ 'Pin run start',
980
+ 'Pin run end',
981
+ 'Pin folder start',
982
+ 'Pin folder end',
983
+ ]) {
984
+ fireEvent.click(screen.getByRole('switch', {name}));
985
+ }
986
+ expect(onSwitch).toHaveBeenCalledTimes(4);
987
+ expect(onClick).not.toHaveBeenCalled();
988
+ expect(screen.queryByText('File')).not.toBeInTheDocument();
989
+ });
990
+ });
991
+
992
+ it('draws guides above full-width rows without taking pointer input', () => {
993
+ // Guide columns now sit inside deeper rows' boxes. They paint above the
994
+ // rows so a row fill never breaks the line, and let clicks fall through
995
+ // to the row underneath.
996
+ const {container} = render(<TreeList items={deepItems} />);
997
+ const guide = container.querySelector('.astryx-tree-list-guide');
998
+ expect(guide).not.toBeNull();
999
+ const column = guide!.parentElement!;
1000
+ expect(getComputedStyle(column).pointerEvents).toBe('none');
1001
+ expect(getComputedStyle(column).zIndex).toBe('1');
1002
+ // The tree contains that stacking so a guide cannot climb over
1003
+ // positioned content outside the list.
1004
+ const treeRoot = screen.getByRole('tree').parentElement!;
1005
+ expect(treeRoot.className).toContain('astryx-tree-list');
1006
+ expect(getComputedStyle(treeRoot).isolation).toBe('isolate');
1007
+ });
1008
+
1009
+ it('lifts only rows that draw a focus ring above the guides', () => {
1010
+ // A plain row (no onClick, href or children) draws no focus ring, so it
1011
+ // stays under the guides: a focused selected plain row must not hide
1012
+ // them with its fill (DD3). Rows that draw the ring take the lift.
1013
+ render(
1014
+ <TreeList
1015
+ items={[
1016
+ {
1017
+ id: 'root',
1018
+ label: 'Root',
1019
+ isExpanded: true,
1020
+ children: [
1021
+ {id: 'plain', label: 'Plain', isSelected: true},
1022
+ {id: 'run', label: 'Run', onClick: () => {}},
1023
+ ],
1024
+ },
1025
+ ]}
1026
+ />,
1027
+ );
1028
+ const rowOf = (label: string) =>
1029
+ screen.getByText(label).closest<HTMLElement>('.astryx-tree-list-item')!;
1030
+ expect(rowOf('Plain').className).not.toContain('focusLift');
1031
+ expect(rowOf('Run').className).toContain('focusLift');
1032
+ expect(rowOf('Root').className).toContain('focusLift');
1033
+ });
1034
+
1035
+ it('lifts the focused row above the guides so they never cut its ring', () => {
1036
+ // Each treeitem publishes its own focus as `--_tree-focus-z`, and the
1037
+ // row reads it as its z-index, so the focused row paints above the
1038
+ // guide columns (z-index 1). Nested treeitems redeclare the default.
1039
+ render(<TreeList items={deepItems} />);
1040
+ const rules = collectCssText();
1041
+ expect(rules).toMatch(/--_tree-focus-z:\s*2/);
1042
+ expect(rules).toMatch(/z-index:\s*var\(--_tree-focus-z,\s*auto\)/);
1043
+ });
723
1044
  });
724
1045
 
725
1046
  // ===========================================================================
@@ -83,9 +83,13 @@ export interface TreeListProps extends BaseProps<HTMLDivElement> {
83
83
  const styles = stylex.create({
84
84
  root: {
85
85
  position: 'relative',
86
+ // Contain the guides' and focused rows' `z-index` (TreeListBranches,
87
+ // TreeListItem) inside the tree, so they never climb over a sticky header
88
+ // or other positioned content outside the list.
89
+ isolation: 'isolate',
86
90
  // Per-level indentation step. Public, themeable lever: a theme can retune
87
91
  // the tree's indent metric (e.g. to `var(--spacing-5)`) via `defineTheme`
88
- // on the `tree-list` target, and both the row margins (TreeListItem) and
92
+ // on the `tree-list` target, and both the row indents (TreeListItem) and
89
93
  // the guide-line offsets (TreeListBranches) read it so they stay aligned.
90
94
  '--tree-list-indent': spacingVars['--spacing-4'],
91
95
  // Vertical gap between adjacent rows. Public, themeable lever (default
@@ -32,10 +32,16 @@ const BRANCH_MARGIN = `calc(${spacingVars['--spacing-2']} + ${spacingVars['--spa
32
32
  const LEVEL_INDENT = 'var(--tree-list-indent)';
33
33
 
34
34
  const styles = stylex.create({
35
+ // Rows span the full width of the list and carry their indent inside, so
36
+ // the guide columns sit inside the row boxes of deeper rows. Lift the guides
37
+ // above the rows so a hovered or selected row's fill never breaks the line,
38
+ // and let pointer input fall through to the row underneath.
35
39
  container: {
36
40
  height: '100%',
37
41
  position: 'absolute',
38
42
  width: spacingVars['--spacing-5'],
43
+ zIndex: 1,
44
+ pointerEvents: 'none',
39
45
  },
40
46
  verticalLine: {
41
47
  borderRadius: 1,