@astryxdesign/core 0.7.1 → 0.7.2-canary.01eab6a

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 (331) hide show
  1. package/dist/AspectRatio/AspectRatio.d.ts +6 -8
  2. package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
  3. package/dist/AspectRatio/AspectRatio.js +44 -3
  4. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  5. package/dist/Breadcrumbs/BreadcrumbItem.js +5 -2
  6. package/dist/Button/Button.d.ts.map +1 -1
  7. package/dist/Button/Button.js +38 -14
  8. package/dist/CheckboxInput/CheckboxInput.d.ts +1 -1
  9. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  10. package/dist/CheckboxInput/CheckboxInput.js +3 -1
  11. package/dist/CheckboxList/CheckboxListItem.d.ts +9 -1
  12. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  13. package/dist/CheckboxList/CheckboxListItem.js +4 -2
  14. package/dist/CommandPalette/CommandPaletteInput.d.ts +1 -1
  15. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  16. package/dist/CommandPalette/CommandPaletteInput.js +37 -5
  17. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  18. package/dist/ContextMenu/ContextMenu.js +5 -2
  19. package/dist/Dialog/Dialog.d.ts.map +1 -1
  20. package/dist/Dialog/Dialog.js +20 -8
  21. package/dist/Drawer/Drawer.d.ts +191 -0
  22. package/dist/Drawer/Drawer.d.ts.map +1 -0
  23. package/dist/Drawer/Drawer.js +382 -0
  24. package/dist/Drawer/DrawerHeader.d.ts +85 -0
  25. package/dist/Drawer/DrawerHeader.d.ts.map +1 -0
  26. package/dist/Drawer/DrawerHeader.js +134 -0
  27. package/dist/Drawer/index.d.ts +11 -0
  28. package/dist/Drawer/index.d.ts.map +1 -0
  29. package/dist/Drawer/index.js +11 -0
  30. package/dist/Drawer/useDrawerDialogPresence.d.ts +42 -0
  31. package/dist/Drawer/useDrawerDialogPresence.d.ts.map +1 -0
  32. package/dist/Drawer/useDrawerDialogPresence.js +236 -0
  33. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  34. package/dist/DropdownMenu/DropdownMenu.js +47 -4
  35. package/dist/DropdownMenu/DropdownMenuContext.d.ts +17 -0
  36. package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
  37. package/dist/DropdownMenu/DropdownMenuContext.js +21 -0
  38. package/dist/DropdownMenu/DropdownMenuDivider.d.ts +5 -3
  39. package/dist/DropdownMenu/DropdownMenuDivider.d.ts.map +1 -1
  40. package/dist/DropdownMenu/DropdownMenuDivider.js +7 -3
  41. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  42. package/dist/DropdownMenu/DropdownMenuSubMenu.js +19 -8
  43. package/dist/EmptyState/EmptyState.d.ts.map +1 -1
  44. package/dist/EmptyState/EmptyState.js +4 -10
  45. package/dist/Field/Field.d.ts +1 -1
  46. package/dist/Field/Field.d.ts.map +1 -1
  47. package/dist/Field/Field.js +8 -4
  48. package/dist/Grid/GridSpan.d.ts.map +1 -1
  49. package/dist/Grid/GridSpan.js +1 -0
  50. package/dist/Heading/Heading.d.ts.map +1 -1
  51. package/dist/Heading/Heading.js +4 -2
  52. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  53. package/dist/HoverCard/useHoverCard.js +10 -2
  54. package/dist/Item/Item.d.ts +6 -2
  55. package/dist/Item/Item.d.ts.map +1 -1
  56. package/dist/Item/Item.js +19 -4
  57. package/dist/Layer/layerViewportInset.stylex.d.ts +6 -2
  58. package/dist/Layer/layerViewportInset.stylex.d.ts.map +1 -1
  59. package/dist/Layer/layerViewportInset.stylex.js +6 -2
  60. package/dist/Layer/useLayer.d.ts.map +1 -1
  61. package/dist/Layer/useLayer.js +13 -7
  62. package/dist/Layer/useLayerDismissal.d.ts +1 -1
  63. package/dist/Layer/useLayerDismissal.js +1 -1
  64. package/dist/Layout/container.stylex.d.ts +5 -5
  65. package/dist/Layout/container.stylex.js +4 -4
  66. package/dist/List/List.d.ts +46 -19
  67. package/dist/List/List.d.ts.map +1 -1
  68. package/dist/List/List.js +28 -10
  69. package/dist/List/ListContext.d.ts +8 -3
  70. package/dist/List/ListContext.d.ts.map +1 -1
  71. package/dist/List/ListContext.js +7 -2
  72. package/dist/List/ListItem.d.ts +13 -5
  73. package/dist/List/ListItem.d.ts.map +1 -1
  74. package/dist/List/ListItem.js +21 -8
  75. package/dist/List/index.d.ts +1 -1
  76. package/dist/List/index.d.ts.map +1 -1
  77. package/dist/Markdown/Markdown.d.ts.map +1 -1
  78. package/dist/Markdown/Markdown.js +8 -1
  79. package/dist/Markdown/plugins/frontmatter.d.ts.map +1 -1
  80. package/dist/Markdown/plugins/frontmatter.js +7 -3
  81. package/dist/MetadataList/MetadataList.d.ts.map +1 -1
  82. package/dist/MetadataList/MetadataList.js +5 -1
  83. package/dist/MetadataList/MetadataListContext.d.ts +1 -1
  84. package/dist/MetadataList/MetadataListContext.d.ts.map +1 -1
  85. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  86. package/dist/MultiSelector/MultiSelector.js +22 -8
  87. package/dist/MultiSelector/OptionActions.a11y.chromium.spec.js +100 -2
  88. package/dist/MultiSelector/hooks.d.ts.map +1 -1
  89. package/dist/MultiSelector/hooks.js +3 -1
  90. package/dist/Pagination/Pagination.d.ts +8 -5
  91. package/dist/Pagination/Pagination.d.ts.map +1 -1
  92. package/dist/Pagination/Pagination.js +21 -8
  93. package/dist/Popover/Popover.js +2 -2
  94. package/dist/PowerSearch/PowerSearch.js +1 -1
  95. package/dist/RadioList/RadioList.d.ts +1 -1
  96. package/dist/RadioList/RadioList.d.ts.map +1 -1
  97. package/dist/RadioList/RadioList.js +6 -2
  98. package/dist/RadioList/RadioListItem.d.ts +9 -1
  99. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  100. package/dist/RadioList/RadioListItem.js +2 -0
  101. package/dist/Selector/hooks.d.ts.map +1 -1
  102. package/dist/Selector/hooks.js +6 -2
  103. package/dist/SideNav/SideNavItem.d.ts +8 -1
  104. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  105. package/dist/SideNav/SideNavItem.js +71 -9
  106. package/dist/Switch/Switch.d.ts +1 -1
  107. package/dist/Switch/Switch.d.ts.map +1 -1
  108. package/dist/Switch/Switch.js +13 -1
  109. package/dist/TabList/TabMenu.d.ts +2 -2
  110. package/dist/TabList/TabMenu.d.ts.map +1 -1
  111. package/dist/TabList/TabMenu.js +6 -3
  112. package/dist/Text/Text.d.ts.map +1 -1
  113. package/dist/Text/Text.js +4 -2
  114. package/dist/Text/useTruncation.d.ts +7 -5
  115. package/dist/Text/useTruncation.d.ts.map +1 -1
  116. package/dist/Text/useTruncation.js +16 -5
  117. package/dist/ToggleButton/ToggleButton.d.ts +3 -2
  118. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  119. package/dist/ToggleButton/ToggleButton.js +12 -1
  120. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  121. package/dist/Tooltip/useTooltip.js +11 -0
  122. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  123. package/dist/TopNav/TopNavMegaMenu.js +3 -3
  124. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  125. package/dist/TopNav/TopNavMenu.js +3 -3
  126. package/dist/TreeList/TreeList.d.ts.map +1 -1
  127. package/dist/TreeList/TreeList.js +1 -0
  128. package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
  129. package/dist/TreeList/TreeListBranches.js +2 -2
  130. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  131. package/dist/TreeList/TreeListItem.js +57 -9
  132. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  133. package/dist/Typeahead/BaseTypeahead.js +26 -0
  134. package/dist/astryx.css +47 -13
  135. package/dist/hooks/useEntryAnimation.d.ts +2 -1
  136. package/dist/hooks/useEntryAnimation.d.ts.map +1 -1
  137. package/dist/hooks/useEntryAnimation.js +14 -12
  138. package/dist/hooks/useIsHydrating.d.ts +6 -0
  139. package/dist/hooks/useIsHydrating.d.ts.map +1 -0
  140. package/dist/hooks/useIsHydrating.js +30 -0
  141. package/dist/hooks/useMenuHover.d.ts.map +1 -1
  142. package/dist/hooks/useMenuHover.js +18 -13
  143. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  144. package/dist/i18n/generated-locales/en.generated.js +112 -1
  145. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  146. package/dist/i18n/generated-locales/pseudo.generated.js +112 -1
  147. package/dist/index.d.ts +1 -0
  148. package/dist/index.d.ts.map +1 -1
  149. package/dist/index.js +1 -0
  150. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  151. package/dist/theme/derivedVarRegistry.js +0 -3
  152. package/dist/utils/focusOutline.stylex.d.ts +4 -0
  153. package/dist/utils/focusOutline.stylex.d.ts.map +1 -1
  154. package/dist/utils/focusOutline.stylex.js +4 -0
  155. package/dist/utils/plainDate.d.ts.map +1 -1
  156. package/dist/utils/plainDate.js +15 -9
  157. package/dist/utils/sharedMutationObserver.d.ts +21 -0
  158. package/dist/utils/sharedMutationObserver.d.ts.map +1 -0
  159. package/dist/utils/sharedMutationObserver.js +69 -0
  160. package/locales/en.json +444 -0
  161. package/locales/pseudo.json +333 -0
  162. package/package.json +9 -4
  163. package/src/AspectRatio/AspectRatio.test.tsx +44 -43
  164. package/src/AspectRatio/AspectRatio.tsx +38 -18
  165. package/src/AspectRatio/__tests__/AspectRatioFitLayers.a11y.browser.spec.ts +157 -0
  166. package/src/Avatar/Avatar.doc.mjs +5 -3
  167. package/src/AvatarGroup/AvatarGroup.doc.mjs +3 -2
  168. package/src/Breadcrumbs/BreadcrumbItem.tsx +4 -1
  169. package/src/Breadcrumbs/Breadcrumbs.test.tsx +30 -1
  170. package/src/Button/Button.doc.mjs +2 -2
  171. package/src/Button/Button.test.tsx +155 -9
  172. package/src/Button/Button.tsx +39 -16
  173. package/src/Button/__tests__/Button.a11y.known-failures.ts +1 -62
  174. package/src/Button/__tests__/Button.a11y.states.ts +0 -10
  175. package/src/Chat/__tests__/ChatMessageMetadata.a11y.chromium.spec.ts +3 -1
  176. package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
  177. package/src/CheckboxInput/CheckboxInput.tsx +3 -1
  178. package/src/CheckboxList/CheckboxList.test.tsx +22 -0
  179. package/src/CheckboxList/CheckboxListItem.doc.mjs +16 -0
  180. package/src/CheckboxList/CheckboxListItem.tsx +10 -0
  181. package/src/CommandPalette/CommandPaletteInput.test.tsx +53 -4
  182. package/src/CommandPalette/CommandPaletteInput.tsx +33 -3
  183. package/src/CommandPalette/__tests__/CommandPaletteInputAudit.a11y.chromium.spec.ts +335 -0
  184. package/src/CommandPalette/__tests__/CommandPaletteItemAudit.a11y.chromium.spec.ts +292 -0
  185. package/src/ContextMenu/ContextMenu.doc.mjs +2 -3
  186. package/src/ContextMenu/ContextMenu.tsx +4 -1
  187. package/src/ContextMenu/__tests__/ContextMenuAudit.a11y.chromium.spec.ts +438 -0
  188. package/src/Dialog/Dialog.test.tsx +46 -0
  189. package/src/Dialog/Dialog.tsx +20 -7
  190. package/src/Dialog/__tests__/Dialog.a11y.known-failures.ts +0 -14
  191. package/src/Drawer/Drawer.doc.mjs +472 -0
  192. package/src/Drawer/Drawer.spec.md +357 -0
  193. package/src/Drawer/Drawer.test.tsx +1300 -0
  194. package/src/Drawer/Drawer.tsx +613 -0
  195. package/src/Drawer/DrawerDirection.test.tsx +127 -0
  196. package/src/Drawer/DrawerHeader.doc.mjs +293 -0
  197. package/src/Drawer/DrawerHeader.test.tsx +102 -0
  198. package/src/Drawer/DrawerHeader.tsx +235 -0
  199. package/src/Drawer/__tests__/DrawerAudit.a11y.chromium.spec.ts +401 -0
  200. package/src/Drawer/__tests__/DrawerHeaderAudit.a11y.chromium.spec.ts +297 -0
  201. package/src/Drawer/index.ts +13 -0
  202. package/src/Drawer/useDrawerDialogPresence.ts +284 -0
  203. package/src/DropdownMenu/DropdownMenu.test.tsx +62 -13
  204. package/src/DropdownMenu/DropdownMenu.tsx +47 -4
  205. package/src/DropdownMenu/DropdownMenuContext.tsx +24 -0
  206. package/src/DropdownMenu/DropdownMenuDivider.doc.mjs +2 -2
  207. package/src/DropdownMenu/DropdownMenuDivider.spec.md +192 -0
  208. package/src/DropdownMenu/DropdownMenuDivider.tsx +9 -5
  209. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +185 -0
  210. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +19 -5
  211. package/src/DropdownMenu/__tests__/DropdownMenuDividerAudit.a11y.chromium.spec.ts +280 -0
  212. package/src/DropdownMenu/__tests__/Menu.a11y.chromium.spec.ts +145 -0
  213. package/src/DropdownMenu/__tests__/Menu.a11y.known-failures.ts +26 -0
  214. package/src/DropdownMenu/__tests__/Menu.a11y.renders.tsx +65 -0
  215. package/src/DropdownMenu/__tests__/Menu.a11y.states.ts +138 -0
  216. package/src/DropdownMenu/__tests__/Menu.a11y.test.tsx +136 -0
  217. package/src/EmptyState/EmptyState.doc.mjs +3 -2
  218. package/src/EmptyState/EmptyState.spec.md +189 -0
  219. package/src/EmptyState/EmptyState.test.tsx +74 -3
  220. package/src/EmptyState/EmptyState.tsx +4 -8
  221. package/src/Field/Field.doc.mjs +14 -8
  222. package/src/Field/Field.spec.md +217 -0
  223. package/src/Field/Field.test.tsx +60 -2
  224. package/src/Field/Field.tsx +17 -8
  225. package/src/Grid/GridSpan.tsx +3 -0
  226. package/src/Heading/Heading.test.tsx +57 -0
  227. package/src/Heading/Heading.tsx +6 -2
  228. package/src/HoverCard/HoverCard.test.tsx +58 -0
  229. package/src/HoverCard/useHoverCard.tsx +10 -2
  230. package/src/IconButton/IconButton.doc.mjs +2 -2
  231. package/src/IconButton/IconButton.test.tsx +3 -1
  232. package/src/Item/Item.doc.mjs +12 -8
  233. package/src/Item/Item.spec.md +226 -0
  234. package/src/Item/Item.test.tsx +61 -0
  235. package/src/Item/Item.tsx +40 -4
  236. package/src/Layer/layerViewportInset.source-build.test.mjs +90 -0
  237. package/src/Layer/layerViewportInset.stylex.ts +6 -2
  238. package/src/Layer/layerViewportInset.test.ts +1 -0
  239. package/src/Layer/useLayer.tsx +24 -13
  240. package/src/Layer/useLayerDismissal.ts +1 -1
  241. package/src/Layout/container.stylex.ts +6 -6
  242. package/src/List/List.doc.mjs +14 -1
  243. package/src/List/List.spec.md +129 -77
  244. package/src/List/List.test.tsx +163 -94
  245. package/src/List/List.tsx +74 -18
  246. package/src/List/ListContext.tsx +9 -3
  247. package/src/List/ListItem.doc.mjs +24 -5
  248. package/src/List/ListItem.tsx +56 -27
  249. package/src/List/index.ts +1 -1
  250. package/src/Markdown/Markdown.tsx +14 -1
  251. package/src/Markdown/plugins/frontmatter.test.tsx +25 -0
  252. package/src/Markdown/plugins/frontmatter.ts +7 -3
  253. package/src/MetadataList/MetadataList.doc.mjs +2 -2
  254. package/src/MetadataList/MetadataList.test.tsx +38 -0
  255. package/src/MetadataList/MetadataList.tsx +8 -1
  256. package/src/MetadataList/MetadataListContext.tsx +1 -1
  257. package/src/MultiSelector/MultiSelector.test.tsx +159 -0
  258. package/src/MultiSelector/MultiSelector.tsx +40 -7
  259. package/src/MultiSelector/OptionActions.a11y.chromium.spec.ts +112 -2
  260. package/src/MultiSelector/hooks.ts +3 -1
  261. package/src/MultiSelector/useMultiCombobox.doc.mjs +267 -0
  262. package/src/Pagination/Pagination.doc.mjs +7 -7
  263. package/src/Pagination/Pagination.spec.md +9 -9
  264. package/src/Pagination/Pagination.test.tsx +242 -0
  265. package/src/Pagination/Pagination.tsx +27 -11
  266. package/src/Popover/Popover.test.tsx +2 -2
  267. package/src/Popover/Popover.tsx +4 -4
  268. package/src/PowerSearch/PowerSearch.test.tsx +85 -0
  269. package/src/PowerSearch/PowerSearch.tsx +2 -2
  270. package/src/RadioList/RadioList.fr10.test.tsx +45 -2
  271. package/src/RadioList/RadioList.test.tsx +22 -0
  272. package/src/RadioList/RadioList.tsx +4 -1
  273. package/src/RadioList/RadioListItem.doc.mjs +16 -0
  274. package/src/RadioList/RadioListItem.tsx +10 -1
  275. package/src/Selector/hooks.ts +6 -2
  276. package/src/Selector/useCombobox.doc.mjs +237 -0
  277. package/src/SideNav/SideNav.test.tsx +179 -0
  278. package/src/SideNav/SideNavItem.doc.mjs +2 -2
  279. package/src/SideNav/SideNavItem.tsx +96 -17
  280. package/src/Slider/Slider.spec.md +39 -18
  281. package/src/Switch/Switch.test.tsx +49 -0
  282. package/src/Switch/Switch.tsx +11 -0
  283. package/src/TabList/TabList.spec.md +57 -44
  284. package/src/TabList/TabList.test.tsx +67 -0
  285. package/src/TabList/TabMenu.doc.mjs +4 -3
  286. package/src/TabList/TabMenu.tsx +7 -4
  287. package/src/Text/Text.test.tsx +57 -0
  288. package/src/Text/Text.tsx +6 -2
  289. package/src/Text/useTruncation.test.tsx +69 -0
  290. package/src/Text/useTruncation.ts +16 -5
  291. package/src/TimeInput/TimeInput.fr10.test.tsx +15 -0
  292. package/src/ToggleButton/ToggleButton.test.tsx +95 -0
  293. package/src/ToggleButton/ToggleButton.tsx +12 -1
  294. package/src/Tokenizer/Tokenizer.test.tsx +54 -1
  295. package/src/Tooltip/Tooltip.test.tsx +31 -0
  296. package/src/Tooltip/useTooltip.test.tsx +13 -0
  297. package/src/Tooltip/useTooltip.tsx +11 -0
  298. package/src/TopNav/TopNavMegaMenu.test.tsx +22 -0
  299. package/src/TopNav/TopNavMegaMenu.tsx +7 -3
  300. package/src/TopNav/TopNavMenu.test.tsx +19 -0
  301. package/src/TopNav/TopNavMenu.tsx +11 -3
  302. package/src/TreeList/TreeList.doc.mjs +1 -0
  303. package/src/TreeList/TreeList.spec.md +29 -17
  304. package/src/TreeList/TreeList.test.tsx +327 -6
  305. package/src/TreeList/TreeList.tsx +5 -1
  306. package/src/TreeList/TreeListBranches.tsx +6 -0
  307. package/src/TreeList/TreeListItem.tsx +88 -13
  308. package/src/Typeahead/BaseTypeahead.tsx +26 -0
  309. package/src/Typeahead/Typeahead.test.tsx +260 -2
  310. package/src/docPropLiterals.test.ts +36 -10
  311. package/src/hooks/useEntryAnimation.doc.mjs +4 -4
  312. package/src/hooks/useEntryAnimation.ssr.test.tsx +143 -0
  313. package/src/hooks/useEntryAnimation.ts +15 -16
  314. package/src/hooks/useIsHydrating.test.tsx +70 -0
  315. package/src/hooks/useIsHydrating.ts +31 -0
  316. package/src/hooks/useMenuHover.ts +18 -13
  317. package/src/i18n/InternationalizationProvider.doc.mjs +8 -0
  318. package/src/i18n/InternationalizationProvider.spec.md +252 -0
  319. package/src/i18n/__tests__/InternationalizationProvider.test.tsx +128 -0
  320. package/src/i18n/generated-locales/en.generated.ts +121 -0
  321. package/src/i18n/generated-locales/pseudo.generated.ts +121 -0
  322. package/src/i18n/useTranslator.test.tsx +37 -0
  323. package/src/index.ts +1 -0
  324. package/src/reset.css +0 -29
  325. package/src/theme/derivedVarRegistry.test.ts +7 -3
  326. package/src/theme/derivedVarRegistry.ts +0 -3
  327. package/src/utils/focusOutline.stylex.ts +4 -0
  328. package/src/utils/plainDate.test.ts +37 -1
  329. package/src/utils/plainDate.ts +15 -5
  330. package/src/utils/sharedMutationObserver.test.ts +53 -0
  331. package/src/utils/sharedMutationObserver.ts +78 -0
@@ -6,8 +6,12 @@
6
6
  * @file hooks.ts
7
7
  * @input Uses untransformed DOM layout geometry from the Selector's outer
8
8
  * anchor, listbox, and selected option
9
- * @output Hooks for Selector
10
- * @position Internal hooks; used by Selector.tsx
9
+ * @output Hooks for Selector: useSelectedItemOffset, and useCombobox (public,
10
+ * exported from the Selector entry point; also used by CommandPalette)
11
+ * @position Selector hooks; used by Selector.tsx and CommandPalette
12
+ *
13
+ * SYNC: When useCombobox's options or return change, update:
14
+ * - /packages/core/src/Selector/useCombobox.doc.mjs
11
15
  */
12
16
 
13
17
  import {useCallback, useState} from 'react';
@@ -0,0 +1,237 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('@astryxdesign/cli/authoring').HookDoc} */
4
+ export const docs = {
5
+ name: 'useCombobox',
6
+ displayName: 'useCombobox',
7
+ group: 'Selector',
8
+ keywords: [
9
+ 'combobox',
10
+ 'listbox',
11
+ 'select',
12
+ 'dropdown',
13
+ 'highlight',
14
+ 'active option',
15
+ 'aria-activedescendant',
16
+ 'keyboard',
17
+ 'arrow keys',
18
+ 'headless',
19
+ 'apg',
20
+ 'accessibility',
21
+ 'a11y',
22
+ ],
23
+ params: [
24
+ {
25
+ name: 'options',
26
+ type: 'UseComboboxOptions',
27
+ description: 'Configuration object.',
28
+ required: true,
29
+ },
30
+ {
31
+ name: 'options.selectableItems',
32
+ type: 'SelectorOptionData[]',
33
+ description:
34
+ 'The options in the same order as they render. Options with `disabled` cannot be picked and are skipped while moving through the open list; ArrowDown or ArrowUp that opens the list still lands on the first or last option.',
35
+ required: true,
36
+ },
37
+ {
38
+ name: 'options.isOpen',
39
+ type: 'boolean',
40
+ description: 'Whether the list is shown. The caller owns this state.',
41
+ required: true,
42
+ },
43
+ {
44
+ name: 'options.onOpen',
45
+ type: '() => unknown',
46
+ description:
47
+ 'Called to show the list. Return `false` to refuse; the hook then leaves the highlight alone.',
48
+ required: true,
49
+ },
50
+ {
51
+ name: 'options.onClose',
52
+ type: '() => void',
53
+ description:
54
+ 'Called when the hook hides the list; the hook resets the highlight first. When the caller closes the list itself (an outside press, say), it should also call `setHighlightedIndex(-1)`.',
55
+ required: true,
56
+ },
57
+ {
58
+ name: 'options.listboxId',
59
+ type: 'string',
60
+ description:
61
+ "The listbox element's id. Option ids are derived from it through `getItemId`.",
62
+ required: true,
63
+ },
64
+ {
65
+ name: 'options.value',
66
+ type: 'string',
67
+ description:
68
+ 'The selected value. Without search, opening by a press, Enter or Space highlights it, or the first option when nothing is selected; ArrowDown and ArrowUp open on the first and last option.',
69
+ },
70
+ {
71
+ name: 'options.onSelect',
72
+ type: '(value: string) => void',
73
+ description:
74
+ 'Called with the picked value. Picking also closes the list.',
75
+ },
76
+ {
77
+ name: 'options.onClear',
78
+ type: '() => void',
79
+ description:
80
+ 'Clears the value. When provided, Delete or Backspace on the closed control clears it, so clearing is not pointer-only. Ignored while the list is open or with `hasSearch`.',
81
+ },
82
+ {
83
+ name: 'options.hasSearch',
84
+ type: 'boolean',
85
+ description:
86
+ 'Whether the list has its own search field. Space then types instead of picking, opening by a press or Enter does not pre-highlight (ArrowDown and ArrowUp still open on the first and last option), and printable keys on the closed control go to `onSearchSeed`.',
87
+ default: 'false',
88
+ },
89
+ {
90
+ name: 'options.onSearchSeed',
91
+ type: '(char: string) => void',
92
+ description:
93
+ 'With `hasSearch`, receives each printable character except Space typed while the control has focus, opening the list first when it is closed, so type-to-find needs no separate open step.',
94
+ },
95
+ {
96
+ name: 'options.isDisabled',
97
+ type: 'boolean',
98
+ description: 'Whether the control ignores clicks and keys.',
99
+ default: 'false',
100
+ },
101
+ ],
102
+ returns: [
103
+ {
104
+ name: 'highlightedIndex',
105
+ type: 'number',
106
+ description:
107
+ 'Index of the active option, or -1 when none is active. With `hasSearch`, opening by a press or Enter does not set one, so it stays -1 after the hook itself closes the list; ArrowDown and ArrowUp still open on the first or last option. Render it as the active option and name it in `aria-activedescendant` only while it points at a rendered option.',
108
+ },
109
+ {
110
+ name: 'setHighlightedIndex',
111
+ type: '(index: number) => void',
112
+ description:
113
+ 'Sets the active option, for example from a filter that changes the options.',
114
+ },
115
+ {
116
+ name: 'getItemId',
117
+ type: '(index: number) => string',
118
+ description:
119
+ "The id to render on the option at `index`. Set the control's `aria-activedescendant` to `isOpen && highlightedIndex >= 0 && highlightedIndex < selectableItems.length ? getItemId(highlightedIndex) : undefined`, so it never names an option that does not exist, including in an empty list.",
120
+ },
121
+ {
122
+ name: 'onTriggerClick',
123
+ type: '() => void',
124
+ description: 'Toggles the list from a press on the control.',
125
+ },
126
+ {
127
+ name: 'onKeyDown',
128
+ type: '(e: React.KeyboardEvent) => void',
129
+ description:
130
+ 'Key handler for the control that keeps focus. ArrowDown and ArrowUp open the list or move the active option; Enter (and Space without search) picks it or opens the list; Escape and Tab close it; Home, End, PageUp and PageDown jump to the first or last option. An editable control that keeps Home and End for the caret does not pass them to this handler, as CommandPalette does.',
131
+ },
132
+ {
133
+ name: 'onItemSelect',
134
+ type: '(item: SelectorOptionData) => void',
135
+ description: 'Picks an option from a press on it, then closes the list.',
136
+ },
137
+ {
138
+ name: 'onItemMouseEnter',
139
+ type: '(item: SelectorOptionData, index: number) => void',
140
+ description:
141
+ 'Makes the hovered option active without scrolling the list or moving focus.',
142
+ },
143
+ ],
144
+ usage: {
145
+ description:
146
+ 'Tracks the active option of a single-select combobox and handles its keys, while DOM focus stays on the control. It powers Selector and CommandPalette; use it to build a combobox those components do not cover. It renders nothing and sets no attributes: the caller renders the control, the listbox and the options, and wires `role`, `aria-expanded`, `aria-controls` and `aria-activedescendant` from what it returns.',
147
+ bestPractices: [
148
+ {
149
+ guidance: true,
150
+ description:
151
+ "Render each option with `id={getItemId(index)}` and set the control's `aria-activedescendant` only while `isOpen` and `highlightedIndex` points at a rendered option, as CommandPalette does.",
152
+ },
153
+ {
154
+ guidance: true,
155
+ description:
156
+ "For type-to-select without a search field, run useTypeahead's handler first and fall through to this `onKeyDown` when it returns false, as Selector does.",
157
+ },
158
+ {
159
+ guidance: true,
160
+ description:
161
+ 'Announce the number of results through useAnnounce when the options change for a new query.',
162
+ },
163
+ {
164
+ guidance: false,
165
+ description:
166
+ 'Move DOM focus onto the options; the active option is announced through `aria-activedescendant` while focus stays on the control.',
167
+ },
168
+ ],
169
+ },
170
+ relatedComponents: ['Selector', 'CommandPalette'],
171
+ relatedHooks: ['useMultiCombobox', 'useTypeahead', 'useAnnounce'],
172
+ importPath: '@astryxdesign/core/Selector',
173
+ category: 'focus',
174
+ };
175
+
176
+ /** @type {import('@astryxdesign/cli/authoring').HookTranslationDoc} */
177
+ export const docsDense = {
178
+ description:
179
+ 'Single-select combobox state + keys; focus stays on control. Powers Selector / CommandPalette. Renders nothing, sets no attrs: caller renders control / listbox / options and wires role, aria-expanded, aria-controls, aria-activedescendant.',
180
+ paramDescriptions: {
181
+ options: 'config.',
182
+ 'options.selectableItems':
183
+ 'options in render order; disabled ones not pickable, skipped while moving in open list (ArrowDown/Up opening still lands first / last).',
184
+ 'options.isOpen': 'list shown; caller owns state.',
185
+ 'options.onOpen':
186
+ 'show list; return false to refuse (highlight untouched).',
187
+ 'options.onClose':
188
+ 'hook hides list; resets highlight first. caller closing itself (outside press): also setHighlightedIndex(-1).',
189
+ 'options.listboxId': 'listbox id; option ids derive from it (getItemId).',
190
+ 'options.value':
191
+ 'selected value; w/o search, press / Enter / Space opening highlights it (else first); ArrowDown/Up open on first / last.',
192
+ 'options.onSelect': 'called w/ picked value; picking closes.',
193
+ 'options.onClear':
194
+ 'clears value; Delete / Backspace on closed control. ignored when open or hasSearch.',
195
+ 'options.hasSearch':
196
+ 'list has own search field: Space types, press / Enter opening does not pre-highlight (ArrowDown/Up still first / last), printable keys on closed control -> onSearchSeed.',
197
+ 'options.onSearchSeed':
198
+ 'w/ hasSearch, each printable char except Space typed while control focused (opens first if closed).',
199
+ 'options.isDisabled': 'ignore clicks + keys.',
200
+ },
201
+ returnDescriptions: {
202
+ highlightedIndex:
203
+ 'active option index, -1 = none. hasSearch: press / Enter opening sets none (stays -1 after hook closes); ArrowDown/Up still first / last. name in aria-activedescendant only when it points at a rendered option.',
204
+ setHighlightedIndex: 'set active option (e.g. after filtering).',
205
+ getItemId:
206
+ 'id for option at index; aria-activedescendant = isOpen && highlightedIndex >= 0 && highlightedIndex < selectableItems.length ? getItemId(highlightedIndex) : undefined (empty list safe).',
207
+ onTriggerClick: 'toggle list from a press on control.',
208
+ onKeyDown:
209
+ 'control keydown: ArrowDown/Up open or move; Enter (Space w/o search) picks or opens; Escape / Tab close; Home / End / PageUp / PageDown jump. editable control keeping Home / End for caret: do not pass them.',
210
+ onItemSelect: 'pick from a press, then close.',
211
+ onItemMouseEnter: 'hover makes option active; no scroll, no focus move.',
212
+ },
213
+ usage: {
214
+ description:
215
+ 'Headless single-select combobox: active option + keys, focus stays on control. Caller renders + wires ARIA.',
216
+ bestPractices: [
217
+ {
218
+ guidance: true,
219
+ description:
220
+ 'Option id = getItemId(index); aria-activedescendant only while open and highlightedIndex points at a rendered option.',
221
+ },
222
+ {
223
+ guidance: true,
224
+ description:
225
+ 'Type-to-select w/o search: run useTypeahead first, fall through when it returns false.',
226
+ },
227
+ {
228
+ guidance: true,
229
+ description: 'Announce result count via useAnnounce on new query.',
230
+ },
231
+ {
232
+ guidance: false,
233
+ description: 'Move DOM focus onto options; use aria-activedescendant.',
234
+ },
235
+ ],
236
+ },
237
+ };
@@ -828,6 +828,169 @@ describe('SideNavItem', () => {
828
828
  expect(screen.getByText('Security')).toBeInTheDocument();
829
829
  });
830
830
 
831
+ it('keeps nested rows full width and indents their content inside the row', () => {
832
+ render(
833
+ <SideNavItem label="Settings">
834
+ <SideNavItem label="General" href="/settings/general">
835
+ <SideNavItem label="Advanced" href="/settings/general/advanced" />
836
+ </SideNavItem>
837
+ </SideNavItem>,
838
+ );
839
+
840
+ // The children group adds no inset of its own, so a nested row's hover
841
+ // and selected fill spans the nav list like a top-level row's.
842
+ const groups = screen.getAllByRole('group', {hidden: true});
843
+ for (const group of groups) {
844
+ expect(declaredOn(group, 'padding-inline-start')).toEqual([]);
845
+ expect(
846
+ declaredOn(group.firstElementChild!, 'padding-inline-start'),
847
+ ).toEqual([]);
848
+ }
849
+
850
+ // The indent lives inside each row instead, one step per nesting level.
851
+ // An ordinary row is the link itself: its own padding plus the steps.
852
+ const top = screen.getByRole('button', {name: 'Settings'});
853
+ const general = screen.getByRole('link', {name: 'General'});
854
+ const advanced = screen.getByRole('link', {name: 'Advanced'});
855
+ expect(indentOf(top)).toBeNull();
856
+ expect(indentOf(advanced)).toBe(
857
+ 'calc(var(--spacing-2) + 2 * var(--spacing-6))',
858
+ );
859
+
860
+ // A split row (own href plus children) keeps the wrapper's base padding
861
+ // and puts the steps on the link, so the whole indent is clickable
862
+ // instead of dead space that still shows hover and press feedback.
863
+ expect(indentOf(general.parentElement!)).toBeNull();
864
+ expect(indentOf(general)).toBe('calc(1 * var(--spacing-6))');
865
+ });
866
+
867
+ it('clips the children group only while it is collapsed', async () => {
868
+ // Nested rows sit flush with the group, so a permanent clip would cut off
869
+ // their focus ring. The group clips for the collapse animation only.
870
+ const user = userEvent.setup();
871
+ render(
872
+ <SideNavItem label="Settings" collapsible>
873
+ <SideNavItem label="General" href="/settings/general" />
874
+ </SideNavItem>,
875
+ );
876
+ const group = () => screen.getByRole('group', {hidden: true});
877
+ expect(declaredOn(group(), 'overflow')).toEqual(['visible']);
878
+ expect(declaredOn(group().firstElementChild!, 'overflow')).toEqual([]);
879
+
880
+ await user.click(screen.getByRole('button', {name: 'Settings'}));
881
+ expect(declaredOn(group(), 'overflow')).toEqual(['hidden']);
882
+ });
883
+
884
+ it('puts the indent on the primary link of a nested actions row', () => {
885
+ render(
886
+ <SideNavItem label="Projects">
887
+ <SideNavItem
888
+ label="Alpha"
889
+ href="/projects/alpha"
890
+ actions={
891
+ <button type="button" aria-label="Alpha actions">
892
+ More
893
+ </button>
894
+ }
895
+ />
896
+ </SideNavItem>,
897
+ );
898
+ const alpha = screen.getByRole('link', {name: 'Alpha'});
899
+ expect(indentOf(alpha.parentElement!)).toBeNull();
900
+ expect(indentOf(alpha)).toBe('calc(1 * var(--spacing-6))');
901
+ });
902
+
903
+ it("composes a caller's style with the indent on a nested row", () => {
904
+ render(
905
+ <SideNavItem label="Settings">
906
+ <SideNavItem
907
+ label="General"
908
+ href="/settings/general"
909
+ style={{color: 'rebeccapurple'}}
910
+ />
911
+ <SideNavItem
912
+ label="Security"
913
+ href="/settings/security"
914
+ style={{color: 'rebeccapurple'}}>
915
+ <SideNavItem label="Keys" href="/settings/security/keys" />
916
+ </SideNavItem>
917
+ </SideNavItem>,
918
+ );
919
+ // Ordinary row: the link is the row.
920
+ const general = screen.getByRole('link', {name: 'General'});
921
+ expect(general.style.color).toBe('rebeccapurple');
922
+ expect(indentOf(general)).toBe(
923
+ 'calc(var(--spacing-2) + 1 * var(--spacing-6))',
924
+ );
925
+ // Split row: the caller's style still lands on the primary link.
926
+ const security = screen.getByRole('link', {name: 'Security'});
927
+ expect(security.style.color).toBe('rebeccapurple');
928
+ expect(indentOf(security)).toBe('calc(1 * var(--spacing-6))');
929
+ });
930
+
931
+ it("lets a caller's inline-start padding replace the indent", () => {
932
+ // The caller's `style` composes after the indent: a caller-set
933
+ // inline-start padding is an inline declaration, so it wins over the
934
+ // indent's class rule on the element that carries the indent, on both
935
+ // the ordinary and the split row path.
936
+ render(
937
+ <SideNavItem label="Settings">
938
+ <SideNavItem
939
+ label="General"
940
+ href="/settings/general"
941
+ style={{paddingInlineStart: '20px'}}
942
+ />
943
+ <SideNavItem
944
+ label="Security"
945
+ href="/settings/security"
946
+ style={{paddingInlineStart: '20px'}}>
947
+ <SideNavItem label="Keys" href="/settings/security/keys" />
948
+ </SideNavItem>
949
+ </SideNavItem>,
950
+ );
951
+ const general = screen.getByRole('link', {name: 'General'});
952
+ expect(general.style.paddingInlineStart).toBe('20px');
953
+ const security = screen.getByRole('link', {name: 'Security'});
954
+ expect(security.style.paddingInlineStart).toBe('20px');
955
+ // A row whose caller sets no padding keeps its indent.
956
+ expect(indentOf(screen.getByRole('link', {name: 'Keys'}))).toBe(
957
+ 'calc(var(--spacing-2) + 2 * var(--spacing-6))',
958
+ );
959
+ });
960
+
961
+ it('indents a small nested row from the small row padding', () => {
962
+ render(
963
+ <SideNavItem label="Settings">
964
+ <SideNavItem label="General" href="/settings/general" size="sm" />
965
+ </SideNavItem>,
966
+ );
967
+ expect(indentOf(screen.getByRole('link', {name: 'General'}))).toBe(
968
+ 'calc(var(--spacing-1) + 1 * var(--spacing-6))',
969
+ );
970
+ });
971
+
972
+ it('does not indent the first level of a collapsed rail flyout', async () => {
973
+ const user = userEvent.setup();
974
+ renderCollapsed(
975
+ <SideNavItem label="Settings" icon={StubIcon}>
976
+ <SideNavItem label="General" href="/settings/general">
977
+ <SideNavItem label="Advanced" href="/settings/general/advanced" />
978
+ </SideNavItem>
979
+ </SideNavItem>,
980
+ );
981
+ await user.click(screen.getByRole('button', {name: 'Settings'}));
982
+
983
+ // jsdom leaves the native popover `display: none`, hence `hidden: true`.
984
+ // The flyout lists the parent's children as its own top level.
985
+ const general = screen.getByRole('link', {name: 'General', hidden: true});
986
+ const advanced = screen.getByRole('link', {name: 'Advanced', hidden: true});
987
+ expect(indentOf(general)).toBeNull();
988
+ expect(indentOf(general.parentElement!)).toBeNull();
989
+ expect(indentOf(advanced)).toBe(
990
+ 'calc(var(--spacing-2) + 1 * var(--spacing-6))',
991
+ );
992
+ });
993
+
831
994
  it('passes data-testid', () => {
832
995
  render(<SideNavItem label="Dashboard" data-testid="nav-item" />);
833
996
  expect(screen.getByTestId('nav-item')).toBeInTheDocument();
@@ -2457,6 +2620,22 @@ function declaredOn(el: Element, prop: string): string[] {
2457
2620
  .filter(value => value != null);
2458
2621
  }
2459
2622
 
2623
+ /**
2624
+ * The nesting indent a row carries, or null for none. The indent is a
2625
+ * runtime StyleX value, so it rides the row's inline style as a custom
2626
+ * property; read the value rather than the generated property name.
2627
+ */
2628
+ function indentOf(el: HTMLElement): string | null {
2629
+ const {style} = el;
2630
+ for (let i = 0; i < style.length; i++) {
2631
+ const value = style.getPropertyValue(style[i]).trim();
2632
+ if (value.includes('var(--spacing-6)')) {
2633
+ return value;
2634
+ }
2635
+ }
2636
+ return null;
2637
+ }
2638
+
2460
2639
  /**
2461
2640
  * The selectors that actually paint the shared ring on `el`, read out of the
2462
2641
  * live stylesheet with StyleX's `:not(#\#)` specificity padding stripped so
@@ -90,7 +90,7 @@ export const docs = {
90
90
  {
91
91
  name: 'children',
92
92
  type: 'ReactNode',
93
- description: 'Sub-items for nesting.',
93
+ description: 'Sub-items for nesting. Nested items span the full width of the nav list, like top-level items; their content is indented one step per nesting level.',
94
94
  slotElements: [
95
95
  {
96
96
  __element: 'SideNavItem',
@@ -213,7 +213,7 @@ export const docsDense = {
213
213
  'Passive right-side content only (badges, counts). Interactive controls go in actions.',
214
214
  actions:
215
215
  'Row-level secondary controls (icon buttons, menus) rendered as siblings of the primary element, after the expand/collapse toggle and before nested children in DOM/focus order. Inherit the row control size via SizeContext (explicit size wins). Hidden when rail collapsed. Passive content goes in endContent; interactive content goes here.',
216
- children: 'Sub-items for nesting.',
216
+ children: 'Sub-items for nesting. Nested rows stay full width; content indents one step per level.',
217
217
  collapsible: 'Enables collapse for items w/ children. true=uncontrolled, object=controlled mode.',
218
218
  size: "Row size variant: 'sm' | 'md' | 'lg'.",
219
219
  },
@@ -17,6 +17,11 @@
17
17
  * `TopNavMenu` use). Hover is a progressive enhancement over the popover's
18
18
  * click behavior and is inert on coarse pointers.
19
19
  *
20
+ * Nested rows span the full width of the nav list, like top-level rows, so
21
+ * their hover and selected fill lines up with the rows above them. The
22
+ * nesting indent sits inside the row as extra inline-start padding, one step
23
+ * per level, tracked through a private depth context.
24
+ *
20
25
  * SYNC: When modified, update these files to stay in sync:
21
26
  * - /packages/core/src/SideNav/SideNav.doc.mjs
22
27
  * - /packages/core/src/SideNav/SideNav.test.tsx
@@ -26,6 +31,8 @@
26
31
  */
27
32
 
28
33
  import {
34
+ createContext,
35
+ use,
29
36
  useCallback,
30
37
  useId,
31
38
  useMemo,
@@ -108,26 +115,45 @@ const styles = stylex.create({
108
115
  gap: spacingVars['--spacing-1'],
109
116
  pointerEvents: 'auto',
110
117
  },
111
- children: {
112
- paddingInlineStart: spacingVars['--spacing-6'],
113
- },
118
+ // The group clips its rows only for the 0fr collapse animation. Nested rows
119
+ // sit flush with the group, so a permanent clip would cut off their focus
120
+ // ring, which paints outside the row. The expanded group therefore stops
121
+ // clipping once it has finished opening: `overflow` is discrete, so it rides
122
+ // the same transition with `allow-discrete` and a delay equal to the open
123
+ // duration, holding `hidden` until the rows' height has settled. Collapsing
124
+ // flips back to `hidden` at once (no delay on the collapsed state), so
125
+ // nothing spills while the group closes.
126
+ //
127
+ // Reduced fallback: a browser without `transition-behavior: allow-discrete`
128
+ // (the tier below current Baseline) cannot delay the switch, so the group
129
+ // stops clipping as soon as it starts opening and the opening rows briefly
130
+ // overlap the row below. The group still opens and the ring still shows.
114
131
  childrenCollapsible: {
115
132
  display: 'grid',
116
133
  gridTemplateRows: '1fr',
117
- transitionProperty: 'grid-template-rows',
134
+ overflow: 'visible',
135
+ transitionProperty: 'grid-template-rows, overflow',
136
+ transitionBehavior: 'allow-discrete',
118
137
  transitionDuration: {
119
- default: durationVars['--duration-medium'],
120
- '@media (prefers-reduced-motion: reduce)': '0s',
138
+ default: `${durationVars['--duration-medium']}, 0s`,
139
+ '@media (prefers-reduced-motion: reduce)': '0s, 0s',
140
+ },
141
+ transitionDelay: {
142
+ default: `0s, ${durationVars['--duration-medium']}`,
143
+ '@media (prefers-reduced-motion: reduce)': '0s, 0s',
121
144
  },
122
145
  transitionTimingFunction: easeVars['--ease-standard'],
123
146
  },
124
147
  childrenCollapsed: {
125
148
  gridTemplateRows: '0fr',
149
+ overflow: 'hidden',
150
+ transitionDelay: '0s, 0s',
126
151
  },
152
+ // No inline padding: nested rows fill the full width of the nav list, and
153
+ // each row carries its own indent (`indentStyles` below). `minHeight: 0`
154
+ // lets the 0fr track shrink this item to nothing; the group clips it.
127
155
  childrenInner: {
128
- overflow: 'hidden',
129
156
  minHeight: 0,
130
- paddingInlineStart: spacingVars['--spacing-6'],
131
157
  },
132
158
  expandChevron: {
133
159
  display: 'inline-flex',
@@ -232,6 +258,35 @@ const styles = stylex.create({
232
258
  },
233
259
  });
234
260
 
261
+ // Nesting indent, drawn inside the row: one `--spacing-6` step per nesting
262
+ // level. The row itself stays full width, so its hover and selected fill
263
+ // matches a top-level row's.
264
+ // - `sm` / `md` — the ordinary row, where the row element is the link or
265
+ // button: its own inline-start padding plus the steps.
266
+ // SYNC: the base padding matches navItemStyles.sm / .md / .lg.
267
+ // - `primary` — the row-wrapper path: the wrapper keeps its base padding and
268
+ // the steps go on the primary link or button, so the whole indent is part
269
+ // of the click target rather than dead space that still shows feedback.
270
+ const indentStyles = stylex.create({
271
+ sm: (depth: number) => ({
272
+ paddingInlineStart: `calc(${spacingVars['--spacing-1']} + ${depth} * ${spacingVars['--spacing-6']})`,
273
+ }),
274
+ md: (depth: number) => ({
275
+ paddingInlineStart: `calc(${spacingVars['--spacing-2']} + ${depth} * ${spacingVars['--spacing-6']})`,
276
+ }),
277
+ primary: (depth: number) => ({
278
+ paddingInlineStart: `calc(${depth} * ${spacingVars['--spacing-6']})`,
279
+ }),
280
+ });
281
+
282
+ /**
283
+ * How many SideNavItems deep the current item sits: 0 at the top level,
284
+ * 1 inside one parent, and so on. A collapsed rail's flyout resets it to 0,
285
+ * since the flyout lists a parent's children as its own top level.
286
+ */
287
+ const SideNavItemDepthContext = createContext(0);
288
+ SideNavItemDepthContext.displayName = 'SideNavItemDepthContext';
289
+
235
290
  /**
236
291
  * Cascaded to the `actions` slot through `SizeContext` so a consumer's row
237
292
  * controls come out the same height as the built-in expand/collapse toggle,
@@ -363,7 +418,9 @@ export interface SideNavItemProps extends BaseProps<HTMLElement> {
363
418
  */
364
419
  actions?: ReactNode;
365
420
  /**
366
- * Sub-items for nesting.
421
+ * Sub-items for nesting. Nested items span the full width of the nav
422
+ * list, like top-level items; their content is indented one step per
423
+ * nesting level.
367
424
  */
368
425
  children?: ReactNode;
369
426
  /**
@@ -455,6 +512,7 @@ export function SideNavItem({
455
512
  const id = useId();
456
513
  const hasChildren = !!children;
457
514
  const itemRef = useRef<HTMLDivElement>(null);
515
+ const depth = use(SideNavItemDepthContext);
458
516
 
459
517
  // Popover for collapsed items with children
460
518
  const popover = usePopover({
@@ -608,7 +666,9 @@ export function SideNavItem({
608
666
  onClick={() => popover.hide()}>
609
667
  <div {...stylex.props(styles.popoverHeader)}>{label}</div>
610
668
  <SideNavCollapseContext value={EXPANDED_COLLAPSE_STATE}>
611
- {children}
669
+ <SideNavItemDepthContext value={0}>
670
+ {children}
671
+ </SideNavItemDepthContext>
612
672
  </SideNavCollapseContext>
613
673
  </div>,
614
674
  {placement: 'end', alignment: 'start', xstyle: styles.popoverGap},
@@ -686,6 +746,14 @@ export function SideNavItem({
686
746
  isSelected && navItemStyles.selected,
687
747
  isDisabled && navItemStyles.disabled,
688
748
  ] as const;
749
+ const rowIndent =
750
+ depth > 0 && indentStyles[size === 'sm' ? 'sm' : 'md'](depth);
751
+ const primaryIndent = depth > 0 && indentStyles.primary(depth);
752
+
753
+ // The indent is a runtime value, so it reaches the element as an inline
754
+ // style. These props are spread after `rest`, so fold the caller's `style`
755
+ // back in (caller wins) rather than letting the indent replace it.
756
+ const callerStyle = {style: rest.style};
689
757
 
690
758
  // Three shapes of the same row appearance:
691
759
  // - `rowProps` — split-action path (toggle, no actions): presentational
@@ -702,7 +770,10 @@ export function SideNavItem({
702
770
  const focusableRowProps = {
703
771
  ...mergeProps(
704
772
  itemThemeProps,
705
- focusOutlineProps.focusVisible(...itemStyleArgs),
773
+ mergeProps(
774
+ focusOutlineProps.focusVisible(...itemStyleArgs, rowIndent),
775
+ callerStyle,
776
+ ),
706
777
  ),
707
778
  ...pressable,
708
779
  };
@@ -748,11 +819,17 @@ export function SideNavItem({
748
819
  onClick={handleClick}
749
820
  {...rest}
750
821
  {...rowPrimaryAriaProps}
751
- {...(hasActions
752
- ? // The wrapper rings for this element; suppressing here is what
753
- // keeps the UA's own ring from painting inside that one.
754
- focusOutlineProps.suppressed(styles.splitAction)
755
- : focusOutlineProps.focusVisible(styles.splitAction))}>
822
+ {...mergeProps(
823
+ hasActions
824
+ ? // The wrapper rings for this element; suppressing here is what
825
+ // keeps the UA's own ring from painting inside that one.
826
+ focusOutlineProps.suppressed(styles.splitAction, primaryIndent)
827
+ : focusOutlineProps.focusVisible(
828
+ styles.splitAction,
829
+ primaryIndent,
830
+ ),
831
+ callerStyle,
832
+ )}>
756
833
  {itemContent}
757
834
  </NavItemElement>
758
835
  {hasIndependentToggle && (
@@ -831,7 +908,9 @@ export function SideNavItem({
831
908
  <span id={`${id}-label`} hidden>
832
909
  {label}
833
910
  </span>
834
- {children}
911
+ <SideNavItemDepthContext value={depth + 1}>
912
+ {children}
913
+ </SideNavItemDepthContext>
835
914
  </div>
836
915
  </div>
837
916
  )}