@astryxdesign/core 0.7.1 → 0.7.2-canary.020e1fe

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (377) hide show
  1. package/dist/AppShell/AppShell.d.ts.map +1 -1
  2. package/dist/AppShell/AppShell.js +40 -1
  3. package/dist/AspectRatio/AspectRatio.d.ts +6 -8
  4. package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
  5. package/dist/AspectRatio/AspectRatio.js +44 -3
  6. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  7. package/dist/Breadcrumbs/BreadcrumbItem.js +5 -2
  8. package/dist/Button/Button.d.ts.map +1 -1
  9. package/dist/Button/Button.js +38 -14
  10. package/dist/CheckboxInput/CheckboxInput.d.ts +1 -1
  11. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  12. package/dist/CheckboxInput/CheckboxInput.js +3 -1
  13. package/dist/CheckboxList/CheckboxListItem.d.ts +9 -1
  14. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  15. package/dist/CheckboxList/CheckboxListItem.js +4 -2
  16. package/dist/CommandPalette/CommandPaletteInput.d.ts +1 -1
  17. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  18. package/dist/CommandPalette/CommandPaletteInput.js +37 -5
  19. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  20. package/dist/ContextMenu/ContextMenu.js +5 -2
  21. package/dist/Dialog/Dialog.d.ts.map +1 -1
  22. package/dist/Dialog/Dialog.js +20 -8
  23. package/dist/Drawer/Drawer.d.ts +191 -0
  24. package/dist/Drawer/Drawer.d.ts.map +1 -0
  25. package/dist/Drawer/Drawer.js +382 -0
  26. package/dist/Drawer/DrawerHeader.d.ts +85 -0
  27. package/dist/Drawer/DrawerHeader.d.ts.map +1 -0
  28. package/dist/Drawer/DrawerHeader.js +134 -0
  29. package/dist/Drawer/index.d.ts +11 -0
  30. package/dist/Drawer/index.d.ts.map +1 -0
  31. package/dist/Drawer/index.js +11 -0
  32. package/dist/Drawer/useDrawerDialogPresence.d.ts +42 -0
  33. package/dist/Drawer/useDrawerDialogPresence.d.ts.map +1 -0
  34. package/dist/Drawer/useDrawerDialogPresence.js +236 -0
  35. package/dist/DropdownMenu/DropdownMenu.d.ts +1 -1
  36. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  37. package/dist/DropdownMenu/DropdownMenu.js +83 -11
  38. package/dist/DropdownMenu/DropdownMenuContext.d.ts +17 -0
  39. package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
  40. package/dist/DropdownMenu/DropdownMenuContext.js +21 -0
  41. package/dist/DropdownMenu/DropdownMenuDivider.d.ts +5 -3
  42. package/dist/DropdownMenu/DropdownMenuDivider.d.ts.map +1 -1
  43. package/dist/DropdownMenu/DropdownMenuDivider.js +7 -3
  44. package/dist/DropdownMenu/DropdownMenuGroup.d.ts +6 -4
  45. package/dist/DropdownMenu/DropdownMenuGroup.d.ts.map +1 -1
  46. package/dist/DropdownMenu/DropdownMenuGroup.js +4 -3
  47. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  48. package/dist/DropdownMenu/DropdownMenuSubMenu.js +19 -8
  49. package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts +24 -0
  50. package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts.map +1 -0
  51. package/dist/DropdownMenu/lazyMenuBottomSheet.js +46 -0
  52. package/dist/EmptyState/EmptyState.d.ts.map +1 -1
  53. package/dist/EmptyState/EmptyState.js +4 -10
  54. package/dist/Field/Field.d.ts +1 -1
  55. package/dist/Field/Field.d.ts.map +1 -1
  56. package/dist/Field/Field.js +8 -4
  57. package/dist/Grid/GridSpan.d.ts.map +1 -1
  58. package/dist/Grid/GridSpan.js +1 -0
  59. package/dist/Heading/Heading.d.ts.map +1 -1
  60. package/dist/Heading/Heading.js +4 -2
  61. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  62. package/dist/HoverCard/useHoverCard.js +10 -2
  63. package/dist/Item/Item.d.ts +6 -2
  64. package/dist/Item/Item.d.ts.map +1 -1
  65. package/dist/Item/Item.js +19 -4
  66. package/dist/Layer/layerViewportInset.stylex.d.ts +6 -2
  67. package/dist/Layer/layerViewportInset.stylex.d.ts.map +1 -1
  68. package/dist/Layer/layerViewportInset.stylex.js +6 -2
  69. package/dist/Layer/useLayer.d.ts.map +1 -1
  70. package/dist/Layer/useLayer.js +13 -7
  71. package/dist/Layer/useLayerDismissal.d.ts +1 -1
  72. package/dist/Layer/useLayerDismissal.js +1 -1
  73. package/dist/Layout/container.stylex.d.ts +5 -5
  74. package/dist/Layout/container.stylex.js +4 -4
  75. package/dist/List/List.d.ts +46 -19
  76. package/dist/List/List.d.ts.map +1 -1
  77. package/dist/List/List.js +28 -10
  78. package/dist/List/ListContext.d.ts +8 -3
  79. package/dist/List/ListContext.d.ts.map +1 -1
  80. package/dist/List/ListContext.js +7 -2
  81. package/dist/List/ListItem.d.ts +13 -5
  82. package/dist/List/ListItem.d.ts.map +1 -1
  83. package/dist/List/ListItem.js +21 -8
  84. package/dist/List/index.d.ts +1 -1
  85. package/dist/List/index.d.ts.map +1 -1
  86. package/dist/Markdown/Markdown.d.ts.map +1 -1
  87. package/dist/Markdown/Markdown.js +8 -1
  88. package/dist/Markdown/plugins/frontmatter.d.ts.map +1 -1
  89. package/dist/Markdown/plugins/frontmatter.js +7 -3
  90. package/dist/MetadataList/MetadataList.d.ts.map +1 -1
  91. package/dist/MetadataList/MetadataList.js +5 -1
  92. package/dist/MetadataList/MetadataListContext.d.ts +1 -1
  93. package/dist/MetadataList/MetadataListContext.d.ts.map +1 -1
  94. package/dist/MobileNav/MobileNav.d.ts +9 -0
  95. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  96. package/dist/MobileNav/MobileNav.js +30 -3
  97. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  98. package/dist/MultiSelector/MultiSelector.js +22 -8
  99. package/dist/MultiSelector/OptionActions.a11y.chromium.spec.js +100 -2
  100. package/dist/MultiSelector/hooks.d.ts.map +1 -1
  101. package/dist/MultiSelector/hooks.js +3 -1
  102. package/dist/Pagination/Pagination.d.ts +8 -5
  103. package/dist/Pagination/Pagination.d.ts.map +1 -1
  104. package/dist/Pagination/Pagination.js +21 -8
  105. package/dist/Popover/Popover.js +2 -2
  106. package/dist/PowerSearch/PowerSearch.js +1 -1
  107. package/dist/RadioList/RadioList.d.ts +1 -1
  108. package/dist/RadioList/RadioList.d.ts.map +1 -1
  109. package/dist/RadioList/RadioList.js +6 -2
  110. package/dist/RadioList/RadioListItem.d.ts +9 -1
  111. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  112. package/dist/RadioList/RadioListItem.js +2 -0
  113. package/dist/Selector/hooks.d.ts.map +1 -1
  114. package/dist/Selector/hooks.js +6 -2
  115. package/dist/SideNav/SideNavItem.d.ts +8 -1
  116. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  117. package/dist/SideNav/SideNavItem.js +71 -9
  118. package/dist/Switch/Switch.d.ts +1 -1
  119. package/dist/Switch/Switch.d.ts.map +1 -1
  120. package/dist/Switch/Switch.js +13 -1
  121. package/dist/TabList/TabMenu.d.ts +2 -2
  122. package/dist/TabList/TabMenu.d.ts.map +1 -1
  123. package/dist/TabList/TabMenu.js +6 -3
  124. package/dist/Text/Text.d.ts.map +1 -1
  125. package/dist/Text/Text.js +4 -2
  126. package/dist/Text/useTruncation.d.ts +7 -5
  127. package/dist/Text/useTruncation.d.ts.map +1 -1
  128. package/dist/Text/useTruncation.js +16 -5
  129. package/dist/ToggleButton/ToggleButton.d.ts +3 -2
  130. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  131. package/dist/ToggleButton/ToggleButton.js +12 -1
  132. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  133. package/dist/Tooltip/useTooltip.js +11 -0
  134. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  135. package/dist/TopNav/TopNavMegaMenu.js +3 -3
  136. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  137. package/dist/TopNav/TopNavMenu.js +3 -3
  138. package/dist/TreeList/TreeList.d.ts.map +1 -1
  139. package/dist/TreeList/TreeList.js +1 -0
  140. package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
  141. package/dist/TreeList/TreeListBranches.js +2 -2
  142. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  143. package/dist/TreeList/TreeListItem.js +57 -9
  144. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  145. package/dist/Typeahead/BaseTypeahead.js +26 -0
  146. package/dist/astryx.css +47 -13
  147. package/dist/hooks/useEntryAnimation.d.ts +2 -1
  148. package/dist/hooks/useEntryAnimation.d.ts.map +1 -1
  149. package/dist/hooks/useEntryAnimation.js +14 -12
  150. package/dist/hooks/useIsHydrating.d.ts +6 -0
  151. package/dist/hooks/useIsHydrating.d.ts.map +1 -0
  152. package/dist/hooks/useIsHydrating.js +30 -0
  153. package/dist/hooks/useMenuHover.d.ts.map +1 -1
  154. package/dist/hooks/useMenuHover.js +18 -13
  155. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  156. package/dist/i18n/generated-locales/en.generated.js +112 -1
  157. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  158. package/dist/i18n/generated-locales/pseudo.generated.js +112 -1
  159. package/dist/index.d.ts +1 -0
  160. package/dist/index.d.ts.map +1 -1
  161. package/dist/index.js +1 -0
  162. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  163. package/dist/theme/derivedVarRegistry.js +0 -3
  164. package/dist/utils/focusOutline.stylex.d.ts +4 -0
  165. package/dist/utils/focusOutline.stylex.d.ts.map +1 -1
  166. package/dist/utils/focusOutline.stylex.js +4 -0
  167. package/dist/utils/plainDate.d.ts.map +1 -1
  168. package/dist/utils/plainDate.js +15 -9
  169. package/dist/utils/sharedMutationObserver.d.ts +21 -0
  170. package/dist/utils/sharedMutationObserver.d.ts.map +1 -0
  171. package/dist/utils/sharedMutationObserver.js +69 -0
  172. package/locales/en.json +444 -0
  173. package/locales/pseudo.json +333 -0
  174. package/package.json +9 -4
  175. package/scripts/agent-doc-state.mjs +38 -0
  176. package/scripts/postinstall.mjs +1 -1
  177. package/src/AppShell/AppShell.tsx +45 -1
  178. package/src/AspectRatio/AspectRatio.test.tsx +44 -43
  179. package/src/AspectRatio/AspectRatio.tsx +38 -18
  180. package/src/AspectRatio/__tests__/AspectRatioFitLayers.a11y.browser.spec.ts +157 -0
  181. package/src/Avatar/Avatar.doc.mjs +5 -3
  182. package/src/AvatarGroup/AvatarGroup.doc.mjs +3 -2
  183. package/src/Breadcrumbs/BreadcrumbItem.tsx +4 -1
  184. package/src/Breadcrumbs/Breadcrumbs.test.tsx +30 -1
  185. package/src/Button/Button.doc.mjs +2 -2
  186. package/src/Button/Button.test.tsx +155 -9
  187. package/src/Button/Button.tsx +39 -16
  188. package/src/Button/__tests__/Button.a11y.known-failures.ts +1 -62
  189. package/src/Button/__tests__/Button.a11y.states.ts +0 -10
  190. package/src/Card/Card.doc.mjs +1 -1
  191. package/src/Chat/__tests__/ChatMessageMetadata.a11y.chromium.spec.ts +3 -1
  192. package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
  193. package/src/CheckboxInput/CheckboxInput.tsx +3 -1
  194. package/src/CheckboxList/CheckboxList.test.tsx +22 -0
  195. package/src/CheckboxList/CheckboxListItem.doc.mjs +16 -0
  196. package/src/CheckboxList/CheckboxListItem.tsx +10 -0
  197. package/src/Code/Code.doc.mjs +1 -0
  198. package/src/CommandPalette/CommandPalette.doc.mjs +2 -0
  199. package/src/CommandPalette/CommandPaletteInput.test.tsx +53 -4
  200. package/src/CommandPalette/CommandPaletteInput.tsx +33 -3
  201. package/src/CommandPalette/__tests__/CommandPaletteInputAudit.a11y.chromium.spec.ts +335 -0
  202. package/src/CommandPalette/__tests__/CommandPaletteItemAudit.a11y.chromium.spec.ts +292 -0
  203. package/src/ContextMenu/ContextMenu.doc.mjs +2 -3
  204. package/src/ContextMenu/ContextMenu.tsx +4 -1
  205. package/src/ContextMenu/__tests__/ContextMenuAudit.a11y.chromium.spec.ts +438 -0
  206. package/src/Dialog/Dialog.test.tsx +46 -0
  207. package/src/Dialog/Dialog.tsx +20 -7
  208. package/src/Dialog/__tests__/Dialog.a11y.known-failures.ts +0 -14
  209. package/src/Drawer/Drawer.doc.mjs +473 -0
  210. package/src/Drawer/Drawer.spec.md +357 -0
  211. package/src/Drawer/Drawer.test.tsx +1300 -0
  212. package/src/Drawer/Drawer.tsx +613 -0
  213. package/src/Drawer/DrawerDirection.test.tsx +127 -0
  214. package/src/Drawer/DrawerHeader.doc.mjs +293 -0
  215. package/src/Drawer/DrawerHeader.test.tsx +102 -0
  216. package/src/Drawer/DrawerHeader.tsx +235 -0
  217. package/src/Drawer/__tests__/DrawerAudit.a11y.chromium.spec.ts +401 -0
  218. package/src/Drawer/__tests__/DrawerHeaderAudit.a11y.chromium.spec.ts +297 -0
  219. package/src/Drawer/index.ts +13 -0
  220. package/src/Drawer/useDrawerDialogPresence.ts +284 -0
  221. package/src/DropdownMenu/DropdownMenu.ssr.test.tsx +57 -0
  222. package/src/DropdownMenu/DropdownMenu.test.tsx +79 -18
  223. package/src/DropdownMenu/DropdownMenu.tsx +83 -12
  224. package/src/DropdownMenu/DropdownMenuContext.tsx +24 -0
  225. package/src/DropdownMenu/DropdownMenuDivider.doc.mjs +2 -2
  226. package/src/DropdownMenu/DropdownMenuDivider.spec.md +192 -0
  227. package/src/DropdownMenu/DropdownMenuDivider.tsx +9 -5
  228. package/src/DropdownMenu/DropdownMenuGroup.doc.mjs +169 -10
  229. package/src/DropdownMenu/DropdownMenuGroup.tsx +6 -4
  230. package/src/DropdownMenu/DropdownMenuLazySheet.test.tsx +126 -0
  231. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +185 -0
  232. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +19 -5
  233. package/src/DropdownMenu/__tests__/DropdownMenuDividerAudit.a11y.chromium.spec.ts +280 -0
  234. package/src/DropdownMenu/__tests__/DropdownMenuGroupAudit.a11y.chromium.spec.ts +334 -0
  235. package/src/DropdownMenu/__tests__/Menu.a11y.chromium.spec.ts +145 -0
  236. package/src/DropdownMenu/__tests__/Menu.a11y.known-failures.ts +26 -0
  237. package/src/DropdownMenu/__tests__/Menu.a11y.renders.tsx +65 -0
  238. package/src/DropdownMenu/__tests__/Menu.a11y.states.ts +138 -0
  239. package/src/DropdownMenu/__tests__/Menu.a11y.test.tsx +136 -0
  240. package/src/DropdownMenu/__tests__/warmMenuBottomSheet.tsx +43 -0
  241. package/src/DropdownMenu/lazyMenuBottomSheet.ts +71 -0
  242. package/src/EmptyState/EmptyState.doc.mjs +3 -2
  243. package/src/EmptyState/EmptyState.spec.md +189 -0
  244. package/src/EmptyState/EmptyState.test.tsx +74 -3
  245. package/src/EmptyState/EmptyState.tsx +4 -8
  246. package/src/Field/Field.doc.mjs +14 -8
  247. package/src/Field/Field.spec.md +217 -0
  248. package/src/Field/Field.test.tsx +60 -2
  249. package/src/Field/Field.tsx +17 -8
  250. package/src/Field/FieldLabel.doc.mjs +133 -26
  251. package/src/Field/FieldLabel.spec.md +172 -0
  252. package/src/Field/__tests__/FieldLabelAudit.a11y.chromium.spec.ts +296 -0
  253. package/src/Grid/GridSpan.tsx +3 -0
  254. package/src/Heading/Heading.test.tsx +57 -0
  255. package/src/Heading/Heading.tsx +6 -2
  256. package/src/HoverCard/HoverCard.test.tsx +58 -0
  257. package/src/HoverCard/useHoverCard.tsx +10 -2
  258. package/src/Icon/Icon.doc.mjs +36 -0
  259. package/src/IconButton/IconButton.doc.mjs +2 -2
  260. package/src/IconButton/IconButton.test.tsx +3 -1
  261. package/src/Item/Item.doc.mjs +12 -8
  262. package/src/Item/Item.spec.md +226 -0
  263. package/src/Item/Item.test.tsx +61 -0
  264. package/src/Item/Item.tsx +40 -4
  265. package/src/Layer/layerViewportInset.source-build.test.mjs +90 -0
  266. package/src/Layer/layerViewportInset.stylex.ts +6 -2
  267. package/src/Layer/layerViewportInset.test.ts +1 -0
  268. package/src/Layer/useLayer.tsx +24 -13
  269. package/src/Layer/useLayerDismissal.ts +1 -1
  270. package/src/Layout/LayoutHeader.doc.mjs +5 -0
  271. package/src/Layout/container.stylex.ts +6 -6
  272. package/src/List/List.doc.mjs +14 -1
  273. package/src/List/List.spec.md +129 -77
  274. package/src/List/List.test.tsx +163 -94
  275. package/src/List/List.tsx +74 -18
  276. package/src/List/ListContext.tsx +9 -3
  277. package/src/List/ListItem.doc.mjs +24 -5
  278. package/src/List/ListItem.tsx +56 -27
  279. package/src/List/index.ts +1 -1
  280. package/src/Markdown/Markdown.tsx +14 -1
  281. package/src/Markdown/plugins/frontmatter.test.tsx +25 -0
  282. package/src/Markdown/plugins/frontmatter.ts +7 -3
  283. package/src/MetadataList/MetadataList.doc.mjs +3 -3
  284. package/src/MetadataList/MetadataList.test.tsx +38 -0
  285. package/src/MetadataList/MetadataList.tsx +8 -1
  286. package/src/MetadataList/MetadataListContext.tsx +1 -1
  287. package/src/MobileNav/MobileNav.tsx +30 -3
  288. package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +173 -15
  289. package/src/MoreMenu/MoreMenu.test.tsx +5 -1
  290. package/src/MultiSelector/MultiSelector.test.tsx +159 -0
  291. package/src/MultiSelector/MultiSelector.tsx +40 -7
  292. package/src/MultiSelector/OptionActions.a11y.chromium.spec.ts +112 -2
  293. package/src/MultiSelector/hooks.ts +3 -1
  294. package/src/MultiSelector/useMultiCombobox.doc.mjs +267 -0
  295. package/src/Pagination/Pagination.doc.mjs +7 -7
  296. package/src/Pagination/Pagination.spec.md +9 -9
  297. package/src/Pagination/Pagination.test.tsx +242 -0
  298. package/src/Pagination/Pagination.tsx +27 -11
  299. package/src/Popover/Popover.test.tsx +2 -2
  300. package/src/Popover/Popover.tsx +4 -4
  301. package/src/PowerSearch/PowerSearch.test.tsx +85 -0
  302. package/src/PowerSearch/PowerSearch.tsx +2 -2
  303. package/src/ProgressBar/ProgressBar.doc.mjs +1 -0
  304. package/src/RadioList/RadioList.fr10.test.tsx +45 -2
  305. package/src/RadioList/RadioList.test.tsx +22 -0
  306. package/src/RadioList/RadioList.tsx +4 -1
  307. package/src/RadioList/RadioListItem.doc.mjs +16 -0
  308. package/src/RadioList/RadioListItem.tsx +10 -1
  309. package/src/Resizable/useResizable.doc.mjs +2 -0
  310. package/src/ScrollableArea/ScrollableArea.doc.mjs +1 -1
  311. package/src/Selector/hooks.ts +6 -2
  312. package/src/Selector/useCombobox.doc.mjs +237 -0
  313. package/src/SideNav/SideNav.spec.md +227 -0
  314. package/src/SideNav/SideNav.test.tsx +179 -0
  315. package/src/SideNav/SideNavItem.doc.mjs +2 -2
  316. package/src/SideNav/SideNavItem.tsx +96 -17
  317. package/src/Slider/Slider.spec.md +39 -18
  318. package/src/Spinner/Spinner.doc.mjs +1 -1
  319. package/src/Switch/Switch.test.tsx +49 -0
  320. package/src/Switch/Switch.tsx +11 -0
  321. package/src/TabList/TabList.spec.md +57 -44
  322. package/src/TabList/TabList.test.tsx +67 -0
  323. package/src/TabList/TabMenu.doc.mjs +4 -3
  324. package/src/TabList/TabMenu.tsx +7 -4
  325. package/src/Text/Text.test.tsx +57 -0
  326. package/src/Text/Text.tsx +6 -2
  327. package/src/Text/useTruncation.test.tsx +69 -0
  328. package/src/Text/useTruncation.ts +16 -5
  329. package/src/TimeInput/TimeInput.fr10.test.tsx +15 -0
  330. package/src/Timestamp/Timestamp.doc.mjs +1 -1
  331. package/src/ToggleButton/ToggleButton.test.tsx +95 -0
  332. package/src/ToggleButton/ToggleButton.tsx +12 -1
  333. package/src/ToggleButton/ToggleButtonGroup.doc.mjs +4 -0
  334. package/src/Tokenizer/Tokenizer.test.tsx +54 -1
  335. package/src/Toolbar/Toolbar.test.tsx +0 -8
  336. package/src/Toolbar/__tests__/Toolbar.a11y.chromium.spec.ts +163 -0
  337. package/src/Toolbar/__tests__/Toolbar.a11y.renders.tsx +63 -0
  338. package/src/Toolbar/__tests__/Toolbar.a11y.states.ts +142 -0
  339. package/src/Toolbar/__tests__/Toolbar.a11y.test.tsx +126 -0
  340. package/src/Tooltip/Tooltip.test.tsx +31 -0
  341. package/src/Tooltip/useTooltip.test.tsx +13 -0
  342. package/src/Tooltip/useTooltip.tsx +11 -0
  343. package/src/TopNav/TopNavMegaMenu.test.tsx +22 -0
  344. package/src/TopNav/TopNavMegaMenu.tsx +7 -3
  345. package/src/TopNav/TopNavMenu.test.tsx +19 -0
  346. package/src/TopNav/TopNavMenu.tsx +11 -3
  347. package/src/TreeList/TreeList.doc.mjs +1 -0
  348. package/src/TreeList/TreeList.spec.md +29 -17
  349. package/src/TreeList/TreeList.test.tsx +327 -6
  350. package/src/TreeList/TreeList.tsx +5 -1
  351. package/src/TreeList/TreeListBranches.tsx +6 -0
  352. package/src/TreeList/TreeListItem.tsx +88 -13
  353. package/src/Typeahead/BaseTypeahead.tsx +26 -0
  354. package/src/Typeahead/Typeahead.test.tsx +260 -2
  355. package/src/docPropLiterals.test.ts +36 -10
  356. package/src/hooks/useEntryAnimation.doc.mjs +4 -4
  357. package/src/hooks/useEntryAnimation.ssr.test.tsx +143 -0
  358. package/src/hooks/useEntryAnimation.ts +15 -16
  359. package/src/hooks/useIsHydrating.test.tsx +70 -0
  360. package/src/hooks/useIsHydrating.ts +31 -0
  361. package/src/hooks/useMenuHover.ts +18 -13
  362. package/src/i18n/InternationalizationProvider.doc.mjs +8 -0
  363. package/src/i18n/InternationalizationProvider.spec.md +252 -0
  364. package/src/i18n/__tests__/InternationalizationProvider.test.tsx +128 -0
  365. package/src/i18n/generated-locales/en.generated.ts +121 -0
  366. package/src/i18n/generated-locales/pseudo.generated.ts +121 -0
  367. package/src/i18n/useTranslator.test.tsx +37 -0
  368. package/src/index.ts +1 -0
  369. package/src/postinstall.test.mjs +46 -0
  370. package/src/reset.css +0 -29
  371. package/src/theme/derivedVarRegistry.test.ts +7 -3
  372. package/src/theme/derivedVarRegistry.ts +0 -3
  373. package/src/utils/focusOutline.stylex.ts +4 -0
  374. package/src/utils/plainDate.test.ts +37 -1
  375. package/src/utils/plainDate.ts +15 -5
  376. package/src/utils/sharedMutationObserver.test.ts +53 -0
  377. package/src/utils/sharedMutationObserver.ts +78 -0
@@ -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
  )}
@@ -34,13 +34,20 @@ an additive target for the interactive control surface.
34
34
 
35
35
  ## Compatibility and migration
36
36
 
37
- - Released default preserved: `yes`
37
+ - Released default preserved: `no` — FR5 changes the released `onChangeEnd`
38
+ payload; every other released default is unchanged
38
39
  - Compatibility class: additive target and state reflection; default styling,
39
40
  DOM semantics, and public props remain unchanged, and the one keyboard change
40
41
  AR1 names — the horizontal arrows follow the track's direction under RTL —
41
42
  ships as a behavior fix
43
+ - `onChangeEnd` payload (FR5): a new contract decision (`spec:AST-017/FR33`),
44
+ not a correction. Released victim: an owner that moves `value` only from
45
+ `onChangeEnd`, with no `onChange`, moves by keyboard today and does not under
46
+ FR5, so the change is breaking (`spec:AST-017/FR1`) and ships as
47
+ `[breaking]` in a declared minor (`spec:AST-017/FR31`).
42
48
  - Controlled/uncontrolled behavior: unchanged; Slider remains controlled
43
- - Migration decision: none
49
+ - Migration decision: an owner that moves `value` only from `onChangeEnd`
50
+ moves it from `onChange` instead (FR5)
44
51
 
45
52
  Consumer migration instructions belong in consumer docs and release notes.
46
53
 
@@ -64,18 +71,20 @@ Consumer migration instructions belong in consumer docs and release notes.
64
71
 
65
72
  ## Public concepts
66
73
 
67
- The record adds one public theming target and names the keyboard model (AR1)
68
- without adding or changing a component prop or value domain. Consumer props,
74
+ The record adds one public theming target, names the keyboard model (AR1), and
75
+ states which value `onChangeEnd` carries (FR5) without adding or changing a
76
+ component prop or value domain. Consumer props,
69
77
  modes, states, and usage remain documented in `Slider.doc.mjs`.
70
78
 
71
79
  ## Behavioral and layout contract
72
80
 
73
- | ID | Candidate invariant | Basis | Review state |
74
- | --- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | -------------------------------------------------- |
75
- | FR1 | The current render places a filled range, one or two thumbs, and optional tick marks over the background track. | Current source, docs, and focused tests | Verified current behavior; no new behavior decided |
76
- | FR2 | `Slider`, `Interactive control`, `Track`, and `Thumb` carry the `slider`, `slider-control`, `slider-track`, and `slider-thumb` targets respectively. | Current source, public docs, and #6224 | Approved additive target contract |
77
- | FR3 | Filled range, tick marks, mark labels, and adjacent text value display are stable rendered parts without their own current Slider target. | Current source and public docs | Verified current asymmetry; not ratified as policy |
78
- | FR4 | Label and status presentation continue to use Field and FieldStatus; value tooltips continue to use Tooltip. | Current source and focused tests | Verified composition boundary |
81
+ | ID | Candidate invariant | Basis | Review state |
82
+ | --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | -------------------------------------------------- |
83
+ | FR1 | The current render places a filled range, one or two thumbs, and optional tick marks over the background track. | Current source, docs, and focused tests | Verified current behavior; no new behavior decided |
84
+ | FR2 | `Slider`, `Interactive control`, `Track`, and `Thumb` carry the `slider`, `slider-control`, `slider-track`, and `slider-thumb` targets respectively. | Current source, public docs, and DEC-1 | Approved additive target contract |
85
+ | FR3 | Filled range, tick marks, mark labels, and adjacent text value display are stable rendered parts without their own current Slider target. | Current source and public docs | Verified current asymmetry; not ratified as policy |
86
+ | FR4 | Label and status presentation continue to use Field and FieldStatus; value tooltips continue to use Tooltip. | Current source and focused tests | Verified composition boundary |
87
+ | FR5 | `onChangeEnd` fires once when an interaction ends — a pointer drag or track press is released or cancelled, or a keyboard step (arrow, Page, Home, End) completes — and carries the value the owner renders after its synchronous response to that interaction's `onChange` calls, including any clamp, snap, or rejection; it never carries a value Slider emitted that the owner did not render. A range reports both values the same way. | Maintainer decision (DEC-3) | Approved |
79
88
 
80
89
  ### Observed current target asymmetry
81
90
 
@@ -106,6 +115,8 @@ either component's remaining target shape.
106
115
 
107
116
  - ORD1: an arrow or Page key result snaps to the nearest step, then clamps to
108
117
  the bounds; Home and End go to `min` and `max` (AR1).
118
+ - ORD2: within one interaction, Slider calls `onChange`, the owner responds and
119
+ renders, then `onChangeEnd` reports the rendered value (FR5).
109
120
  - The anatomy decision (DEC-1) introduces no value, snapping, geometry,
110
121
  interaction, or styling precedence rule of its own.
111
122
 
@@ -214,14 +225,15 @@ separately listed value tooltip retains Tooltip's target.
214
225
 
215
226
  ## Verification map
216
227
 
217
- | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
218
- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
219
- | FR1 | `Slider.test.tsx` structure, range, and marks suites | Single, range, horizontal, vertical, marks | Removing or misaligning stable parts breaks existing role, position, or mark assertions. | `audit:Slider/anatomy` |
220
- | AR1 | Slider accessibility contract (`internal/a11y-spec`, pending) bound to Slider, in jsdom and Chromium; `Slider.test.tsx` keyboard suites, including "follows the mirrored track under RTL: ArrowLeft increases, ArrowRight decreases, Up/Down unchanged" and "keeps ArrowLeft decreasing on a vertical track under RTL"; the Page, Home/End and Up/Down cases under the contract | Single and range; horizontal LTR and RTL; vertical; disabled; formatted value | A thumb loses its role, name or value properties; an arrow, Page, Home or End key stops stepping or steps against the mirrored track; a range thumb crosses the other | `audit:Slider/accessibility` |
221
- | FR2 | `Slider.test.tsx`, `themingTargets.test.ts`, generated probe theme, and Chromium | Root, horizontal/vertical control, disabled control, track, and thumb | A target class/state is missing, undocumented, or placed outside its owning anatomy. | `audit:Slider/theming` |
222
- | FR3 | Source and consumer-doc review | Filled range, marks, labels, adjacent text value | A missing target is inaccurately documented as present or intentionally permanent. | `audit:Slider/theming` |
223
- | FR4 | Source inspection; focused tests cover label, status, and disabled-reason Tooltip only | Label, status, value tooltip, disabled reason | Shared composition or its accessible association disappears; value-tooltip composition still lacks focused coverage. | `audit:Slider/anatomy` |
224
- | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and four current local targets | Missing, extra, prefixed, stale, or unclaimed mappings fail repository validation. | `audit:Slider/theming` |
228
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
229
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
230
+ | FR1 | `Slider.test.tsx` structure, range, and marks suites | Single, range, horizontal, vertical, marks | Removing or misaligning stable parts breaks existing role, position, or mark assertions. | `audit:Slider/anatomy` |
231
+ | AR1 | Slider accessibility contract (`internal/a11y-spec`, pending) bound to Slider, in jsdom and Chromium; `Slider.test.tsx` keyboard suites, including "follows the mirrored track under RTL: ArrowLeft increases, ArrowRight decreases, Up/Down unchanged" and "keeps ArrowLeft decreasing on a vertical track under RTL"; the Page, Home/End and Up/Down cases under the contract | Single and range; horizontal LTR and RTL; vertical; disabled; formatted value | A thumb loses its role, name or value properties; an arrow, Page, Home or End key stops stepping or steps against the mirrored track; a range thumb crosses the other | `audit:Slider/accessibility` |
232
+ | FR2 | `Slider.test.tsx`, `themingTargets.test.ts`, generated probe theme, and Chromium | Root, horizontal/vertical control, disabled control, track, and thumb | A target class/state is missing, undocumented, or placed outside its owning anatomy. | `audit:Slider/theming` |
233
+ | FR3 | Source and consumer-doc review | Filled range, marks, labels, adjacent text value | A missing target is inaccurately documented as present or intentionally permanent. | `audit:Slider/theming` |
234
+ | FR4 | Source inspection; focused tests cover label, status, and disabled-reason Tooltip only | Label, status, value tooltip, disabled reason | Shared composition or its accessible association disappears; value-tooltip composition still lacks focused coverage. | `audit:Slider/anatomy` |
235
+ | FR5 | Pending: `Slider.test.tsx` change-end cases driven by an owner that renders, clamps, or rejects the emitted value (not yet in the suite) | Single and range; pointer release before and after the owner re-renders; each keyboard step | `onChangeEnd` reports a value the owner did not render, fires more than once per interaction, or skips a completed keyboard step | `audit:Slider/accessibility` |
236
+ | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and four current local targets | Missing, extra, prefixed, stale, or unclaimed mappings fail repository validation. | `audit:Slider/theming` |
225
237
 
226
238
  The focused Slider suite asserts the `slider-control` target's orientation and
227
239
  disabled state. Source/metadata guards cover all four target declarations. The
@@ -250,6 +262,15 @@ A thumb already plays the slider role as one composite control, so it carries th
250
262
 
251
263
  Rejected: keeping ArrowRight as "increase" under RTL; a Tab stop or arrow model per range thumb that differs from the single thumb.
252
264
 
265
+ ### DEC-3 — `onChangeEnd` reports the value the owner renders
266
+
267
+ **Reference:** `component:Slider/DEC-3`
268
+ **Decider:** `cixzhang`, `2026-10-16`
269
+
270
+ `onChangeEnd` marks the end of an interaction, so it reports what the slider shows when the interaction ends: the controlled value after the owner has answered `onChange`. An owner that clamps, snaps, or rejects a value therefore receives the value it kept, and the commit agrees with the thumb (FR5).
271
+
272
+ Rejected: reporting the last value Slider emitted through `onChange`.
273
+
253
274
  ## Open questions
254
275
 
255
276
  - **OQ1 — After a joint ProgressBar and Slider family audit, should any shared
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'Spinner',
7
7
  displayName: 'Spinner',
8
8
  category: 'Feedback & Status',
9
- keywords: ["spinner","loader","loading","circular","progress","spin","activity","busy","indeterminate"],
9
+ keywords: ["spinner","loader","loading","circular","progress","spin","activity","busy","indeterminate","circularprogress"],
10
10
  props: [
11
11
  {
12
12
  name: 'size',
@@ -834,6 +834,55 @@ describe('Switch', () => {
834
834
  expect(root).toHaveAttribute('id', 'switch-1');
835
835
  expect(root).toHaveAttribute('aria-label', 'Toggle notifications');
836
836
  });
837
+
838
+ it('composes a caller aria-describedby onto the switch control', () => {
839
+ const {container} = render(
840
+ <>
841
+ <p id="external-hint">Applies to every repository</p>
842
+ <Switch
843
+ label="Notifications"
844
+ description="Sends a daily digest"
845
+ value={false}
846
+ onChange={() => {}}
847
+ data-testid="my-switch"
848
+ aria-describedby="external-hint"
849
+ />
850
+ </>,
851
+ );
852
+ const control = screen.getByRole('switch');
853
+ const description = screen.getByText('Sends a daily digest');
854
+ expect(control.getAttribute('aria-describedby')?.split(' ')).toEqual([
855
+ 'external-hint',
856
+ description.id,
857
+ ]);
858
+ expect(control).toHaveAccessibleDescription(
859
+ 'Applies to every repository Sends a daily digest',
860
+ );
861
+ // The id reference lives on the control only, not on the field wrapper.
862
+ const root = container.querySelector('[data-testid="my-switch"]');
863
+ expect(root).not.toHaveAttribute('aria-describedby');
864
+ });
865
+
866
+ it('names the switch control from a caller aria-labelledby', () => {
867
+ const {container} = render(
868
+ <>
869
+ <span id="row-title">Release notes</span>
870
+ <Switch
871
+ label="Pin Release notes"
872
+ isLabelHidden
873
+ value={false}
874
+ onChange={() => {}}
875
+ data-testid="my-switch"
876
+ aria-labelledby="row-title"
877
+ />
878
+ </>,
879
+ );
880
+ const control = screen.getByRole('switch', {name: 'Release notes'});
881
+ expect(control).toHaveAttribute('aria-labelledby', 'row-title');
882
+ // The id reference lives on the control only, not on the field wrapper.
883
+ const root = container.querySelector('[data-testid="my-switch"]');
884
+ expect(root).not.toHaveAttribute('aria-labelledby');
885
+ });
837
886
  });
838
887
 
839
888
  describe('pressed state', () => {
@@ -566,6 +566,8 @@ export function Switch({
566
566
  className,
567
567
  style,
568
568
  ref,
569
+ 'aria-describedby': ariaDescribedByProp,
570
+ 'aria-labelledby': ariaLabelledByProp,
569
571
  ...rest
570
572
  }: SwitchProps) {
571
573
  const t = useTranslator();
@@ -610,7 +612,12 @@ export function Switch({
610
612
  // Only include descriptionID when the element actually renders.
611
613
  // FieldLabel renders the description (with descriptionID) even when the
612
614
  // label is visually hidden — it's sr-only, so keep it linked.
615
+ // A consumer's own `aria-describedby` describes the control, so it comes
616
+ // first here rather than landing on the field wrapper via `...rest`.
613
617
  const describedByParts: string[] = [];
618
+ if (ariaDescribedByProp) {
619
+ describedByParts.push(ariaDescribedByProp);
620
+ }
614
621
  if (description) {
615
622
  describedByParts.push(descriptionID);
616
623
  }
@@ -658,6 +665,10 @@ export function Switch({
658
665
  }}
659
666
  onFocus={onFocus}
660
667
  onBlur={onBlur}
668
+ // A caller's `aria-labelledby` names the control (a visible title
669
+ // elsewhere in the row, for example), so it reaches the input rather
670
+ // than the field wrapper via `...rest`.
671
+ aria-labelledby={ariaLabelledByProp}
661
672
  aria-describedby={ariaDescribedBy}
662
673
  aria-invalid={status?.type === 'error' ? true : undefined}
663
674
  aria-busy={isBusy || undefined}