@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
@@ -31,10 +31,10 @@ system_specs: [spec:AST-002/DEC-1, spec:AST-002/DEC-2, spec:AST-017/DEC-1]
31
31
 
32
32
  | Area | Contract |
33
33
  | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
34
- | Public contract | Canonicalize the existing alignment mode as `edgeCompensation?: 'inline'`; deprecate `isFullBleed` and remove it in the next minor release. |
34
+ | Public contract | Canonicalize the existing mode as `edge?: 'padded' \| 'bleed'` (default `'padded'`) from the shared `edge` vocabulary; deprecate `isFullBleed` as an alias of `edge="bleed"` and remove it in the next minor release. |
35
35
  | Behavior | Move first and last visible tab content toward the host content line, reaching it when the host inset is at least the edge-tab padding, while preserving tab interaction padding, focus-ring room, overflow controls, selection-indicator geometry, and the optional divider rail. |
36
36
  | End-user impact | Edge tabs lose unnecessary extra inset where host geometry permits and remain fully interactive without clipped focus, selection, overflow, or divider treatment. |
37
- | Builder impact | Existing `isFullBleed` callsites keep working during the deprecation window and migrate mechanically to `edgeCompensation="inline"`. |
37
+ | Builder impact | Existing `isFullBleed` callsites keep working during the deprecation window and migrate mechanically to `edge="bleed"`. |
38
38
  | Compatibility/readiness | Current behavior is shipped; the canonical alias is additive, while removing the released legacy prop is a separately implemented breaking change in the next minor release. |
39
39
  | Review checks | Reject a behavior change disguised as a rename, directional values without overflow/indicator proof, or early removal of the legacy prop. |
40
40
  | Governing rules | `architecture:container-padding/INV9–INV11`; `spec:AST-002/DEC-1–DEC-2`; `spec:AST-017/DEC-1`. |
@@ -43,12 +43,13 @@ This table is a review projection; the body below is authoritative.
43
43
 
44
44
  ## Intent
45
45
 
46
- TabList's current opt-in is edge compensation: it consumes the full inherited
47
- host inset, moves visible edge-tab content toward the surrounding content line,
46
+ TabList's current opt-in is the shared `bleed` edge treatment: it consumes the
47
+ full inherited host inset, moves visible edge-tab content toward the surrounding content line,
48
48
  and retains Tab-owned interaction padding plus TabList-owned divider, overflow,
49
49
  and indicator geometry. Content reaches the host line only when that inset is at
50
50
  least the edge tab's own padding. The public `isFullBleed` name is misleading
51
- because the strip restores uncovered content inset after the outer box expands.
51
+ because the strip keeps a content line: it restores uncovered content inset
52
+ after the outer box expands, which `architecture:container-padding` names `bleed`.
52
53
 
53
54
  ## Compatibility and migration
54
55
 
@@ -61,21 +62,21 @@ because the strip restores uncovered content inset after the outer box expands.
61
62
 
62
63
  During the compatibility window:
63
64
 
64
- - `edgeCompensation="inline"` is the canonical spelling;
65
- - omitted `edgeCompensation` plus `isFullBleed={true}` selects the same mode;
65
+ - `edge="bleed"` is the canonical spelling, and omitted `edge` means `padded`;
66
+ - omitted `edge` plus `isFullBleed={true}` selects `bleed`;
66
67
  - `isFullBleed` is deprecated and maintained examples use the canonical prop;
67
- - if both props are supplied, explicit `edgeCompensation` is canonical and the
68
- legacy boolean cannot disable it.
68
+ - if both props are supplied, explicit `edge` is canonical and the legacy
69
+ boolean cannot override it.
69
70
 
70
71
  The removal release MUST include a `[breaking]` Changeset for
71
72
  `@astryxdesign/core`, consumer release notes, and an `astryx upgrade` codemod that rewrites
72
- `isFullBleed` to `edgeCompensation="inline"`.
73
+ `isFullBleed` to `edge="bleed"`.
73
74
 
74
75
  ## Ownership boundary
75
76
 
76
77
  **Owns**
77
78
 
78
- - The TabList-wide edge-compensation mode and its compatibility alias.
79
+ - The TabList-wide `bleed` edge treatment and its compatibility alias.
79
80
  - The relationship between the outer TabList box, inner strip padding, edge tabs,
80
81
  focus-ring room, overflow controls, and selection indicator.
81
82
  - Which logical edges are supported by the public mode.
@@ -84,33 +85,34 @@ The removal release MUST include a `[breaking]` Changeset for
84
85
 
85
86
  - Host padding publication, nesting, or overlay resets — owned by
86
87
  `architecture:container-padding`.
87
- - Button or List edge-compensation projections.
88
- - New `start` or `end` values without separate proof and owner approval.
88
+ - Button or List edge projections.
89
+ - The `flush` or `outset` values, or per-edge application, without separate
90
+ proof and owner approval.
89
91
  - A change to navigation/tab semantics, selection state, keyboard behavior, or
90
92
  overflow ownership.
91
93
 
92
94
  ## Public concepts
93
95
 
94
- | Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
95
- | ----------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | --------------- | ------------------- | ------------------------------- | ------------------------------------------------------ |
96
- | Edge compensation | omitted, `inline` | Consume both inherited inline host insets and move edge-tab content toward the host lines while retaining local interaction and strip-owned geometry | Navigation and tabs patterns across current layout/overflow states | omitted | `component:TabList` | accepted; canonical API pending | Other values are rejected by the public type |
97
- | Legacy alias | omitted, `false`, `true` | Compatibility spelling for omitted or inline edge compensation | Until the next minor release | omitted/`false` | `component:TabList` | deprecated | `true` maps to `inline`; explicit canonical input wins |
96
+ | Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
97
+ | -------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------ | ------------------- | ------------------------------- | -------------------------------------------- |
98
+ | Edge treatment | omitted, `padded`, `bleed` | `bleed`: consume both inherited inline host insets and move edge-tab content toward the host lines while retaining local interaction and strip-owned geometry; `padded`: current default geometry | Navigation and tabs patterns across current layout/overflow states | omitted (`padded`) | `component:TabList` | accepted; canonical API pending | Other values are rejected by the public type |
99
+ | Legacy alias | omitted, `false`, `true` | Compatibility spelling for `padded` or `bleed` | Until the next minor release | omitted/`false` | `component:TabList` | deprecated | `true` maps to `bleed`; explicit `edge` wins |
98
100
 
99
101
  ## Behavioral and layout contract
100
102
 
101
- | ID | Invariant | Basis | Status |
102
- | --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------- |
103
- | FR1 | The current opt-in MUST remain edge compensation: the outer TabList box consumes the full inherited inline host inset and any `hasDivider` rail spans that expanded width; the inner strip restores `max(host inset - edge-tab padding, 0)` at each logical edge. Visible edge-tab content reaches the host line only when the host inset is at least the tab padding; otherwise it moves toward that line without crossing the host boundary. | Current source and `component:TabList/DEC-1` | Shipped behavior |
104
- | FR2 | Compensation MUST preserve Tab interaction padding, focus-ring room, overflow detection and controls, scroll access, selection-indicator geometry, and the optional divider rail's expanded inline span. | Current source plus owner direction | Shipped behavior; expanded evidence required with rename |
105
- | FR3 | `edgeCompensation?: 'inline'` MUST become the canonical API without changing FR1–FR2 geometry or the omitted default. | `architecture:container-padding/INV10`; `component:TabList/DEC-1` | Accepted contract; implementation pending |
106
- | FR4 | During the deprecation window, omitted `edgeCompensation` plus `isFullBleed={true}` MUST produce the canonical `inline` behavior. Explicit `edgeCompensation` MUST remain authoritative when both props are supplied. | Compatibility decision | Accepted contract; implementation pending |
107
- | FR5 | `isFullBleed` MUST be removed in the next minor release only after maintained docs/examples use `edgeCompensation`, both paths have compatibility evidence, and the removal includes a `[breaking]` Changeset plus an `astryx upgrade` codemod for mechanical consumer migration. | `component:TabList/DEC-1`; `spec:AST-017/DEC-1` | Accepted contract; implementation pending |
108
- | FR6 | `start` and `end` MUST remain rejected until per-edge overflow, clipping, scroll-control, focus-ring, indicator, direction, and theme-padding evidence receives owner approval. | Shared logical vocabulary and TabList boundary | Accepted boundary |
103
+ | ID | Invariant | Basis | Status |
104
+ | --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------- |
105
+ | FR1 | The current opt-in MUST remain the `bleed` treatment: the outer TabList box consumes the full inherited inline host inset and any `hasDivider` rail spans that expanded width; the inner strip restores `max(host inset - edge-tab padding, 0)` at each logical edge. Visible edge-tab content reaches the host line only when the host inset is at least the tab padding; otherwise it moves toward that line without crossing the host boundary. | Current source and `component:TabList/DEC-1` | Shipped behavior |
106
+ | FR2 | Compensation MUST preserve Tab interaction padding, focus-ring room, overflow detection and controls, scroll access, selection-indicator geometry, and the optional divider rail's expanded inline span. | Current source plus owner direction | Shipped behavior; expanded evidence required with rename |
107
+ | FR3 | `edge?: 'padded' \| 'bleed'` MUST become the canonical API, with `bleed` selecting FR1–FR2 geometry and `padded` the omitted default, without changing either. | `architecture:container-padding/INV10`; `component:TabList/DEC-1` | Accepted contract; implementation pending |
108
+ | FR4 | During the deprecation window, omitted `edge` plus `isFullBleed={true}` MUST produce the canonical `bleed` behavior. Explicit `edge` MUST remain authoritative when both props are supplied. | Compatibility decision | Accepted contract; implementation pending |
109
+ | FR5 | `isFullBleed` MUST be removed in the next minor release only after maintained docs/examples use `edge`, both paths have compatibility evidence, and the removal includes a `[breaking]` Changeset plus an `astryx upgrade` codemod for mechanical consumer migration. | `component:TabList/DEC-1`; `spec:AST-017/DEC-1` | Accepted contract; implementation pending |
110
+ | FR6 | `flush`, `outset`, and per-edge application MUST remain rejected until overflow, clipping, scroll-control, focus-ring, indicator, direction, and theme-padding evidence receives owner approval. | Shared edge vocabulary and TabList boundary | Accepted boundary |
109
111
 
110
112
  ### Allowed variation
111
113
 
112
114
  - Layout, size, role, selected value, labels, tab content, and overflow mode may
113
- vary while the same edge-compensation meaning is preserved.
115
+ vary while the same `bleed` meaning is preserved.
114
116
  - The implementation may retain the current outer-box/inner-strip technique or
115
117
  replace it with equivalent geometry that satisfies FR1–FR2.
116
118
  - A host may be painted or bounded only where TabList's component-specific
@@ -118,20 +120,20 @@ The removal release MUST include a `[breaking]` Changeset for
118
120
 
119
121
  ### Representative states
120
122
 
121
- | State | Required invariant | Allowed variation |
122
- | --------------------- | ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
123
- | Default/omitted | Current TabList box, strip, and tab geometry are unchanged. | Any current layout, role, or overflow mode. |
124
- | Canonical inline mode | Outer box consumes each host inset; edge content reaches the host line only when that inset is at least the tab padding. | Host inset smaller than, equal to, or larger than edge-tab padding; asymmetric insets; LTR/RTL. |
125
- | Divider | `hasDivider` rail spans the expanded outer inline width. | Fitting or overflowing strips. |
126
- | Legacy alias | `isFullBleed={true}` matches canonical inline geometry during the compatibility window. | The canonical prop may also be present. |
127
- | Overflowing strip | Scroll access, edge fades, controls, and selected-tab reveal remain correct. | Start/end overflow independently. |
128
- | Focused edge tab | The focus ring remains visible and local to the tab. | Keyboard or pointer focus. |
123
+ | State | Required invariant | Allowed variation |
124
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
125
+ | Default/omitted | Current TabList box, strip, and tab geometry are unchanged. | Any current layout, role, or overflow mode. |
126
+ | Canonical `bleed` | Outer box consumes each host inset; edge content reaches the host line only when that inset is at least the tab padding. | Host inset smaller than, equal to, or larger than edge-tab padding; asymmetric insets; LTR/RTL. |
127
+ | Divider | `hasDivider` rail spans the expanded outer inline width. | Fitting or overflowing strips. |
128
+ | Legacy alias | `isFullBleed={true}` matches canonical `bleed` geometry during the compatibility window. | The canonical prop may also be present. |
129
+ | Overflowing strip | Scroll access, edge fades, controls, and selected-tab reveal remain correct. | Start/end overflow independently. |
130
+ | Focused edge tab | The focus ring remains visible and local to the tab. | Keyboard or pointer focus. |
129
131
 
130
132
  ### Transformation and precedence order
131
133
 
132
- - **ORD1 — Public input.** Resolve explicit `edgeCompensation`; otherwise map
133
- `isFullBleed={true}` to `inline` during the compatibility window; otherwise use
134
- no compensation.
134
+ - **ORD1 — Public input.** Resolve explicit `edge`; otherwise map
135
+ `isFullBleed={true}` to `bleed` during the compatibility window; otherwise use
136
+ `padded`.
135
137
  - **ORD2 — Host geometry.** Read the matching inherited logical host insets, with
136
138
  missing values falling back to zero.
137
139
  - **ORD3 — Component geometry.** Resolve edge-tab padding and focus/indicator
@@ -145,10 +147,10 @@ The removal release MUST include a `[breaking]` Changeset for
145
147
 
146
148
  ## Accessibility contract
147
149
 
148
- - **AR1 — Interaction remains stable.** Edge compensation MUST NOT change the
150
+ - **AR1 — Interaction remains stable.** The `bleed` treatment MUST NOT change the
149
151
  tab/nav pattern, accessible names, selected state, keyboard movement, tab stop,
150
152
  focus target, or controlled value behavior.
151
- - **AR2 — Overflow remains reachable.** Compensated edge geometry MUST NOT clip or
153
+ - **AR2 — Overflow remains reachable.** `bleed` geometry MUST NOT clip or
152
154
  hide a focus ring, selected indicator, visible tab content, or an overflow
153
155
  control required to reach offscreen tabs.
154
156
 
@@ -162,8 +164,8 @@ The removal release MUST include a `[breaking]` Changeset for
162
164
 
163
165
  ## Family and system relationships
164
166
 
165
- - `architecture:container-padding` owns host publication, the shared
166
- `edgeCompensation` vocabulary, logical-edge semantics, and overlay reset.
167
+ - `architecture:container-padding` owns host publication, the shared `edge`
168
+ vocabulary, logical-edge semantics, and overlay reset.
167
169
  - `architecture:public-component-api` and `spec:AST-002` own public API
168
170
  admission; `spec:AST-017/DEC-1` owns the breaking classification and migration
169
171
  evidence required when the released alias is removed.
@@ -189,9 +191,20 @@ TabList's current mode consumes the full inherited inline host inset, keeps an
189
191
  optional divider across that expanded width, and restores only the portion of
190
192
  content inset not already supplied by edge-tab padding. The user-facing intent is
191
193
  therefore edge compensation, not a promise that tab content itself reaches an
192
- outer edge. `edgeCompensation="inline"` becomes the canonical spelling;
193
- `isFullBleed` remains a compatibility alias until its `[breaking]` next-minor
194
- removal, with the required codemod and release evidence.
194
+ outer edge. DEC-2 sets the canonical spelling; `isFullBleed` remains a
195
+ compatibility alias until its `[breaking]` next-minor removal, with the required
196
+ codemod and release evidence.
197
+
198
+ ### DEC-2 — TabList spells its mode with the shared edge vocabulary
199
+
200
+ **Reference:** `component:TabList/DEC-2`
201
+
202
+ **Decider:** cixzhang, 2026-10-16
203
+
204
+ `architecture:container-padding` names TabList's geometry `bleed`: paint reaches
205
+ the host's padding edge and content stays on the host content line, or at the
206
+ edge tab's own padding when the host inset is smaller. `edge="bleed"` is the
207
+ canonical spelling, so TabList and List share one prop name and one value set.
195
208
 
196
209
  ## Open questions
197
210
 
@@ -783,6 +783,73 @@ describe('TabMenu', () => {
783
783
  });
784
784
  });
785
785
 
786
+ describe('TabMenu host attributes', () => {
787
+ const menuOptions = [
788
+ {value: 'analytics', label: 'Analytics'},
789
+ {value: 'reports', label: 'Reports'},
790
+ ];
791
+
792
+ it('forwards host attributes to the trigger, not the popup', () => {
793
+ render(
794
+ <TabList value="home" onChange={() => {}}>
795
+ <Tab value="home" label="Home" />
796
+ <TabMenu
797
+ label="More"
798
+ options={menuOptions}
799
+ id="more-tabs"
800
+ aria-label="More views"
801
+ data-view="overflow"
802
+ />
803
+ </TabList>,
804
+ );
805
+
806
+ const trigger = screen.getByRole('button', {name: 'More views'});
807
+ expect(trigger).toHaveAttribute('id', 'more-tabs');
808
+ expect(trigger).toHaveAttribute('data-view', 'overflow');
809
+ const menu = screen.getByRole('menu', {name: 'More', hidden: true});
810
+ expect(trigger).toHaveAttribute('aria-controls', menu.id);
811
+ });
812
+
813
+ it('runs drag-and-drop handlers on the trigger', () => {
814
+ const onDragOver = vi.fn();
815
+ const onDrop = vi.fn();
816
+ render(
817
+ <TabList value="home" onChange={() => {}}>
818
+ <Tab value="home" label="Home" />
819
+ <TabMenu
820
+ label="More"
821
+ options={menuOptions}
822
+ onDragOver={onDragOver}
823
+ onDrop={onDrop}
824
+ />
825
+ </TabList>,
826
+ );
827
+ const trigger = screen.getByRole('button', {name: 'More'});
828
+ fireEvent.dragOver(trigger);
829
+ fireEvent.drop(trigger);
830
+ expect(onDragOver).toHaveBeenCalledTimes(1);
831
+ expect(onDrop).toHaveBeenCalledTimes(1);
832
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
833
+ });
834
+
835
+ it('onClick preserves the toggle', async () => {
836
+ const user = userEvent.setup();
837
+ const onClick = vi.fn((event: React.MouseEvent<HTMLButtonElement>) => {
838
+ event.preventDefault();
839
+ });
840
+ render(
841
+ <TabList value="home" onChange={() => {}}>
842
+ <Tab value="home" label="Home" />
843
+ <TabMenu label="More" options={menuOptions} onClick={onClick} />
844
+ </TabList>,
845
+ );
846
+ const trigger = screen.getByRole('button', {name: 'More'});
847
+ await user.click(trigger);
848
+ expect(onClick).toHaveBeenCalledTimes(1);
849
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
850
+ });
851
+ });
852
+
786
853
  describe('TabMenu keyboard navigation (roving tabindex)', () => {
787
854
  const menuOptions = [
788
855
  {value: 'analytics', label: 'Analytics'},
@@ -7,7 +7,7 @@ export const docs = {
7
7
  subComponentOf: 'TabList',
8
8
  displayName: 'Tab Menu',
9
9
  isHiddenFromOverview: true,
10
- description: "Overflow menu trigger that opens a dropdown of additional tab options, showing the selected option's label as the trigger text.",
10
+ description: "Overflow menu trigger that opens a dropdown of additional tab options, showing the selected option's label as the trigger text. Host attributes and DOM event handlers (id, aria-*, data-*, drag and drop) reach the trigger button, not the popup or its options. The button keeps its role, tab stop, and menu disclosure attributes.",
11
11
  // TabMenu requires TabList context; wrap it so the preview doesn't throw.
12
12
  // The wrapper value matches none of the options, so the trigger shows its
13
13
  // own label (the idle overflow state).
@@ -42,7 +42,7 @@ export const docsZh = {
42
42
  name: 'TabMenu',
43
43
  isHiddenFromOverview: true,
44
44
  displayName: 'Tab Menu',
45
- description: '溢出菜单触发器,打开包含额外标签选项的下拉菜单,将选中选项的标签显示为触发器文本。',
45
+ description: '溢出菜单触发器,打开包含额外标签选项的下拉菜单,将选中选项的标签显示为触发器文本。宿主属性和 DOM 事件处理器(id、aria-*、data-*、拖放)传递给触发按钮,而不是弹出菜单或选项。按钮保留自己的角色、Tab 停靠点及菜单展开属性。',
46
46
  props: [
47
47
  {
48
48
  name: 'label',
@@ -63,7 +63,8 @@ export const docsDense = {
63
63
  name: 'TabMenu',
64
64
  isHiddenFromOverview: true,
65
65
  displayName: 'Tab Menu',
66
- description: "Overflow menu trigger; dropdown of extra tab options, shows selected option's label as trigger text.",
66
+ description:
67
+ "Overflow menu trigger; shows selected option's label. Host attributes/DOM handlers reach the trigger button, not the popup/options; role, tab stop and disclosure attributes stay owned.",
67
68
  propDescriptions: {
68
69
  label: 'Trigger text (when no option selected) + dropdown heading.',
69
70
  options: 'Menu options array rendered in dropdown.',
@@ -37,7 +37,7 @@ import {useListFocus} from '../hooks/useListFocus';
37
37
  import {useTabListContext} from './TabListContext';
38
38
  import type {TabListSize} from './TabListContext';
39
39
  import {tabScope} from './tab.markers.stylex';
40
- import {mergeProps} from '../utils';
40
+ import {composeEventHandlers, mergeProps} from '../utils';
41
41
  import {useMergedRefs} from '../hooks/useMergedRefs';
42
42
  import type {BaseProps} from '../BaseProps';
43
43
  import {themeProps} from '../utils/themeProps';
@@ -51,9 +51,9 @@ export interface TabMenuOption {
51
51
  icon?: ReactNode | IconType;
52
52
  }
53
53
 
54
- export interface TabMenuProps extends Pick<
54
+ export interface TabMenuProps extends Omit<
55
55
  BaseProps<HTMLButtonElement>,
56
- 'xstyle' | 'className' | 'style'
56
+ 'role' | 'tabIndex' | 'aria-haspopup' | 'aria-expanded' | 'aria-controls'
57
57
  > {
58
58
  ref?: React.Ref<HTMLButtonElement>;
59
59
  /**
@@ -263,6 +263,8 @@ export function TabMenu({
263
263
  xstyle,
264
264
  className,
265
265
  style,
266
+ onClick,
267
+ ...rest
266
268
  }: TabMenuProps) {
267
269
  const tabListCtx = useTabListContext();
268
270
  const menuId = useId();
@@ -346,6 +348,7 @@ export function TabMenu({
346
348
  return (
347
349
  <>
348
350
  <button
351
+ {...rest}
349
352
  ref={setButtonRef}
350
353
  type="button"
351
354
  aria-haspopup="menu"
@@ -353,7 +356,7 @@ export function TabMenu({
353
356
  aria-controls={menuId}
354
357
  data-tab-menu=""
355
358
  tabIndex={hasSelectedOption ? 0 : -1}
356
- onClick={handleToggle}
359
+ onClick={composeEventHandlers(handleToggle, onClick)}
357
360
  {...mergeProps(
358
361
  themeProps('tab-menu'),
359
362
  focusOutlineProps.focusVisible(
@@ -346,3 +346,60 @@ describe('truncated text shows one tooltip, not two', () => {
346
346
  }
347
347
  });
348
348
  });
349
+
350
+ // With hasTruncateTooltip={false} nothing reads the measured overflow, so a
351
+ // long table of truncated cells must not pay a layout read per cell.
352
+ function countScrollWidthReads(run: () => void): number {
353
+ const proto = window.HTMLElement.prototype;
354
+ const original = Object.getOwnPropertyDescriptor(proto, 'scrollWidth');
355
+ let reads = 0;
356
+ Object.defineProperty(proto, 'scrollWidth', {
357
+ configurable: true,
358
+ get() {
359
+ reads += 1;
360
+ return 0;
361
+ },
362
+ });
363
+ try {
364
+ run();
365
+ } finally {
366
+ if (original) {
367
+ Object.defineProperty(proto, 'scrollWidth', original);
368
+ } else {
369
+ delete (proto as {scrollWidth?: number}).scrollWidth;
370
+ }
371
+ }
372
+ return reads;
373
+ }
374
+
375
+ describe('Text maxLines without a truncation tooltip', () => {
376
+ it('takes no truncation measurement when hasTruncateTooltip is false', () => {
377
+ const reads = countScrollWidthReads(() => {
378
+ render(
379
+ <>
380
+ {Array.from({length: 20}, (_, index) => (
381
+ <Text
382
+ type="body"
383
+ maxLines={1}
384
+ hasTruncateTooltip={false}
385
+ key={index}>
386
+ Cell {index} with text far wider than its column
387
+ </Text>
388
+ ))}
389
+ </>,
390
+ );
391
+ });
392
+ expect(reads).toBe(0);
393
+ });
394
+
395
+ it('still measures when the tooltip is enabled', () => {
396
+ const reads = countScrollWidthReads(() => {
397
+ render(
398
+ <Text type="body" maxLines={1}>
399
+ Text far wider than its column
400
+ </Text>,
401
+ );
402
+ });
403
+ expect(reads).toBeGreaterThan(0);
404
+ });
405
+ });
package/src/Text/Text.tsx CHANGED
@@ -260,8 +260,12 @@ export function Text({
260
260
  // Resolve display - force block when maxLines > 0 or hasCapsize
261
261
  const resolvedDisplay = maxLines > 0 || hasCapsize ? 'block' : display;
262
262
 
263
- // Truncation detection
264
- const truncation = useTruncation({maxLines});
263
+ // Truncation detection. Only the tooltip reads the measured overflow, so
264
+ // with the tooltip off the text is clipped by CSS alone and nothing is
265
+ // measured or observed.
266
+ const truncation = useTruncation({
267
+ maxLines: hasTruncateTooltip === false ? 0 : maxLines,
268
+ });
265
269
 
266
270
  // Tooltip for truncated text
267
271
  const tooltipPlacement: LayerPlacement =
@@ -0,0 +1,69 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file useTruncation.test.tsx
5
+ * Tests for useTruncation
6
+ */
7
+
8
+ import {render, screen, waitFor} from '@testing-library/react';
9
+ import {afterEach, describe, it, expect, vi} from 'vitest';
10
+ import {useTruncation} from './useTruncation';
11
+
12
+ function Probe({text}: {text: string}) {
13
+ const truncation = useTruncation({maxLines: 1});
14
+ return (
15
+ <>
16
+ <span ref={truncation.ref}>{text}</span>
17
+ <output data-testid="full-text">{truncation.fullText}</output>
18
+ </>
19
+ );
20
+ }
21
+
22
+ // A status line at a fixed width never resizes when its message changes, so
23
+ // the tooltip text must follow the content, not only the element's size.
24
+ describe('useTruncation', () => {
25
+ it('re-measures when the text changes without a resize', async () => {
26
+ const {rerender} = render(<Probe text="planning the first step" />);
27
+ expect(screen.getByTestId('full-text')).toHaveTextContent(
28
+ 'planning the first step',
29
+ );
30
+
31
+ rerender(<Probe text="writing the second step" />);
32
+
33
+ await waitFor(() =>
34
+ expect(screen.getByTestId('full-text')).toHaveTextContent(
35
+ 'writing the second step',
36
+ ),
37
+ );
38
+ });
39
+
40
+ describe('content observation', () => {
41
+ afterEach(() => {
42
+ vi.unstubAllGlobals();
43
+ });
44
+
45
+ it('shares one observer across every truncating instance', () => {
46
+ const NativeMutationObserver = globalThis.MutationObserver;
47
+ let created = 0;
48
+ vi.stubGlobal(
49
+ 'MutationObserver',
50
+ class extends NativeMutationObserver {
51
+ constructor(callback: MutationCallback) {
52
+ super(callback);
53
+ created += 1;
54
+ }
55
+ },
56
+ );
57
+
58
+ render(
59
+ <>
60
+ {Array.from({length: 50}, (_, index) => (
61
+ <Probe key={index} text={`cell ${index}`} />
62
+ ))}
63
+ </>,
64
+ );
65
+
66
+ expect(created).toBe(1);
67
+ });
68
+ });
69
+ });
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @file useTruncation.ts
7
- * @input Uses React hooks, sharedResizeObserver
7
+ * @input Uses React hooks, sharedResizeObserver, sharedMutationObserver
8
8
  * @output Exports useTruncation hook for detecting text overflow
9
9
  * @position Hook; consumed by Text.tsx, Heading.tsx
10
10
  *
@@ -14,6 +14,7 @@
14
14
 
15
15
  import {useCallback, useRef, useState, type RefCallback} from 'react';
16
16
  import {observeResize} from '../utils/sharedResizeObserver';
17
+ import {observeContent} from '../utils/sharedMutationObserver';
17
18
 
18
19
  export interface UseTruncationOptions {
19
20
  /**
@@ -43,10 +44,12 @@ export interface UseTruncationReturn {
43
44
  /**
44
45
  * Hook for detecting text overflow/truncation.
45
46
  *
46
- * Uses a shared ResizeObserver singleton (via observeResize)
47
- * for efficient detection when content or container changes. A single
48
- * ResizeObserver instance is shared across all mounted useTruncation hooks,
49
- * so even hundreds of table cells only create one observer.
47
+ * Uses a shared ResizeObserver singleton (via observeResize) for size
48
+ * changes and a shared MutationObserver singleton (via observeContent) for
49
+ * content changes, so even hundreds of table cells create one observer of
50
+ * each kind. Observing content matters because an element whose text changes
51
+ * without resizing (a fixed-width status line) must still be re-measured for
52
+ * the tooltip text and the truncated state to follow the current content.
50
53
  *
51
54
  * - Single-line: compares scrollWidth > offsetWidth
52
55
  * - Multi-line: uses Range.getBoundingClientRect() to measure actual content
@@ -71,6 +74,7 @@ export function useTruncation(
71
74
  const [fullText, setFullText] = useState('');
72
75
  const elementRef = useRef<HTMLElement | null>(null);
73
76
  const unobserveRef = useRef<(() => void) | null>(null);
77
+ const unobserveContentRef = useRef<(() => void) | null>(null);
74
78
 
75
79
  const checkTruncation = useCallback(
76
80
  (element: HTMLElement) => {
@@ -113,6 +117,8 @@ export function useTruncation(
113
117
  // by element leaves any other observer of the same node untouched.
114
118
  unobserveRef.current?.();
115
119
  unobserveRef.current = null;
120
+ unobserveContentRef.current?.();
121
+ unobserveContentRef.current = null;
116
122
 
117
123
  elementRef.current = element;
118
124
 
@@ -127,6 +133,11 @@ export function useTruncation(
127
133
  } else {
128
134
  checkTruncation(element);
129
135
  }
136
+ // New text in a box that keeps its size fires no resize, so the
137
+ // captured text and truncated state would otherwise stay stale.
138
+ unobserveContentRef.current = observeContent(element, () => {
139
+ checkTruncation(element);
140
+ });
130
141
  } else {
131
142
  setIsTruncated(false);
132
143
  setFullText('');
@@ -191,6 +191,21 @@ for (const [surface, presentation, selector] of SURFACES) {
191
191
  expect(own?.textContent).toContain('Start');
192
192
  });
193
193
 
194
+ it('puts hidden, inert, and dir on the control', () => {
195
+ render(
196
+ <TimeInput
197
+ label="Start"
198
+ presentation={presentation}
199
+ hidden
200
+ inert
201
+ dir="rtl"
202
+ />,
203
+ );
204
+ expect(control(selector)).toHaveAttribute('hidden');
205
+ expect(control(selector)).toHaveAttribute('inert');
206
+ expect(control(selector)).toHaveAttribute('dir', 'rtl');
207
+ });
208
+
194
209
  it('keeps control* on the control box in a horizontal-labels FormLayout', () => {
195
210
  vi.spyOn(console, 'warn').mockImplementation(() => {});
196
211
  const {container} = render(
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'Timestamp',
7
7
  displayName: 'Timestamp',
8
8
  category: 'Content',
9
- keywords: ['date', 'time', 'datetime', 'relative', 'ago', 'clock', 'format', 'duration'],
9
+ keywords: ['date', 'time', 'datetime', 'relative', 'ago', 'clock', 'format', 'duration', 'relativetime'],
10
10
  playground: {
11
11
  // `value` is required but has no semantic default, so the properties-tab
12
12
  // preview fell back to the literal string "value" — which