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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (331) hide show
  1. package/dist/AspectRatio/AspectRatio.d.ts +6 -8
  2. package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
  3. package/dist/AspectRatio/AspectRatio.js +44 -3
  4. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  5. package/dist/Breadcrumbs/BreadcrumbItem.js +5 -2
  6. package/dist/Button/Button.d.ts.map +1 -1
  7. package/dist/Button/Button.js +38 -14
  8. package/dist/CheckboxInput/CheckboxInput.d.ts +1 -1
  9. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  10. package/dist/CheckboxInput/CheckboxInput.js +3 -1
  11. package/dist/CheckboxList/CheckboxListItem.d.ts +9 -1
  12. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  13. package/dist/CheckboxList/CheckboxListItem.js +4 -2
  14. package/dist/CommandPalette/CommandPaletteInput.d.ts +1 -1
  15. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  16. package/dist/CommandPalette/CommandPaletteInput.js +37 -5
  17. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  18. package/dist/ContextMenu/ContextMenu.js +5 -2
  19. package/dist/Dialog/Dialog.d.ts.map +1 -1
  20. package/dist/Dialog/Dialog.js +20 -8
  21. package/dist/Drawer/Drawer.d.ts +191 -0
  22. package/dist/Drawer/Drawer.d.ts.map +1 -0
  23. package/dist/Drawer/Drawer.js +382 -0
  24. package/dist/Drawer/DrawerHeader.d.ts +85 -0
  25. package/dist/Drawer/DrawerHeader.d.ts.map +1 -0
  26. package/dist/Drawer/DrawerHeader.js +134 -0
  27. package/dist/Drawer/index.d.ts +11 -0
  28. package/dist/Drawer/index.d.ts.map +1 -0
  29. package/dist/Drawer/index.js +11 -0
  30. package/dist/Drawer/useDrawerDialogPresence.d.ts +42 -0
  31. package/dist/Drawer/useDrawerDialogPresence.d.ts.map +1 -0
  32. package/dist/Drawer/useDrawerDialogPresence.js +236 -0
  33. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  34. package/dist/DropdownMenu/DropdownMenu.js +47 -4
  35. package/dist/DropdownMenu/DropdownMenuContext.d.ts +17 -0
  36. package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
  37. package/dist/DropdownMenu/DropdownMenuContext.js +21 -0
  38. package/dist/DropdownMenu/DropdownMenuDivider.d.ts +5 -3
  39. package/dist/DropdownMenu/DropdownMenuDivider.d.ts.map +1 -1
  40. package/dist/DropdownMenu/DropdownMenuDivider.js +7 -3
  41. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  42. package/dist/DropdownMenu/DropdownMenuSubMenu.js +19 -8
  43. package/dist/EmptyState/EmptyState.d.ts.map +1 -1
  44. package/dist/EmptyState/EmptyState.js +4 -10
  45. package/dist/Field/Field.d.ts +1 -1
  46. package/dist/Field/Field.d.ts.map +1 -1
  47. package/dist/Field/Field.js +8 -4
  48. package/dist/Grid/GridSpan.d.ts.map +1 -1
  49. package/dist/Grid/GridSpan.js +1 -0
  50. package/dist/Heading/Heading.d.ts.map +1 -1
  51. package/dist/Heading/Heading.js +4 -2
  52. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  53. package/dist/HoverCard/useHoverCard.js +10 -2
  54. package/dist/Item/Item.d.ts +6 -2
  55. package/dist/Item/Item.d.ts.map +1 -1
  56. package/dist/Item/Item.js +19 -4
  57. package/dist/Layer/layerViewportInset.stylex.d.ts +6 -2
  58. package/dist/Layer/layerViewportInset.stylex.d.ts.map +1 -1
  59. package/dist/Layer/layerViewportInset.stylex.js +6 -2
  60. package/dist/Layer/useLayer.d.ts.map +1 -1
  61. package/dist/Layer/useLayer.js +13 -7
  62. package/dist/Layer/useLayerDismissal.d.ts +1 -1
  63. package/dist/Layer/useLayerDismissal.js +1 -1
  64. package/dist/Layout/container.stylex.d.ts +5 -5
  65. package/dist/Layout/container.stylex.js +4 -4
  66. package/dist/List/List.d.ts +46 -19
  67. package/dist/List/List.d.ts.map +1 -1
  68. package/dist/List/List.js +28 -10
  69. package/dist/List/ListContext.d.ts +8 -3
  70. package/dist/List/ListContext.d.ts.map +1 -1
  71. package/dist/List/ListContext.js +7 -2
  72. package/dist/List/ListItem.d.ts +13 -5
  73. package/dist/List/ListItem.d.ts.map +1 -1
  74. package/dist/List/ListItem.js +21 -8
  75. package/dist/List/index.d.ts +1 -1
  76. package/dist/List/index.d.ts.map +1 -1
  77. package/dist/Markdown/Markdown.d.ts.map +1 -1
  78. package/dist/Markdown/Markdown.js +8 -1
  79. package/dist/Markdown/plugins/frontmatter.d.ts.map +1 -1
  80. package/dist/Markdown/plugins/frontmatter.js +7 -3
  81. package/dist/MetadataList/MetadataList.d.ts.map +1 -1
  82. package/dist/MetadataList/MetadataList.js +5 -1
  83. package/dist/MetadataList/MetadataListContext.d.ts +1 -1
  84. package/dist/MetadataList/MetadataListContext.d.ts.map +1 -1
  85. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  86. package/dist/MultiSelector/MultiSelector.js +22 -8
  87. package/dist/MultiSelector/OptionActions.a11y.chromium.spec.js +100 -2
  88. package/dist/MultiSelector/hooks.d.ts.map +1 -1
  89. package/dist/MultiSelector/hooks.js +3 -1
  90. package/dist/Pagination/Pagination.d.ts +8 -5
  91. package/dist/Pagination/Pagination.d.ts.map +1 -1
  92. package/dist/Pagination/Pagination.js +21 -8
  93. package/dist/Popover/Popover.js +2 -2
  94. package/dist/PowerSearch/PowerSearch.js +1 -1
  95. package/dist/RadioList/RadioList.d.ts +1 -1
  96. package/dist/RadioList/RadioList.d.ts.map +1 -1
  97. package/dist/RadioList/RadioList.js +6 -2
  98. package/dist/RadioList/RadioListItem.d.ts +9 -1
  99. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  100. package/dist/RadioList/RadioListItem.js +2 -0
  101. package/dist/Selector/hooks.d.ts.map +1 -1
  102. package/dist/Selector/hooks.js +6 -2
  103. package/dist/SideNav/SideNavItem.d.ts +8 -1
  104. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  105. package/dist/SideNav/SideNavItem.js +71 -9
  106. package/dist/Switch/Switch.d.ts +1 -1
  107. package/dist/Switch/Switch.d.ts.map +1 -1
  108. package/dist/Switch/Switch.js +13 -1
  109. package/dist/TabList/TabMenu.d.ts +2 -2
  110. package/dist/TabList/TabMenu.d.ts.map +1 -1
  111. package/dist/TabList/TabMenu.js +6 -3
  112. package/dist/Text/Text.d.ts.map +1 -1
  113. package/dist/Text/Text.js +4 -2
  114. package/dist/Text/useTruncation.d.ts +7 -5
  115. package/dist/Text/useTruncation.d.ts.map +1 -1
  116. package/dist/Text/useTruncation.js +16 -5
  117. package/dist/ToggleButton/ToggleButton.d.ts +3 -2
  118. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  119. package/dist/ToggleButton/ToggleButton.js +12 -1
  120. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  121. package/dist/Tooltip/useTooltip.js +11 -0
  122. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  123. package/dist/TopNav/TopNavMegaMenu.js +3 -3
  124. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  125. package/dist/TopNav/TopNavMenu.js +3 -3
  126. package/dist/TreeList/TreeList.d.ts.map +1 -1
  127. package/dist/TreeList/TreeList.js +1 -0
  128. package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
  129. package/dist/TreeList/TreeListBranches.js +2 -2
  130. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  131. package/dist/TreeList/TreeListItem.js +57 -9
  132. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  133. package/dist/Typeahead/BaseTypeahead.js +26 -0
  134. package/dist/astryx.css +47 -13
  135. package/dist/hooks/useEntryAnimation.d.ts +2 -1
  136. package/dist/hooks/useEntryAnimation.d.ts.map +1 -1
  137. package/dist/hooks/useEntryAnimation.js +14 -12
  138. package/dist/hooks/useIsHydrating.d.ts +6 -0
  139. package/dist/hooks/useIsHydrating.d.ts.map +1 -0
  140. package/dist/hooks/useIsHydrating.js +30 -0
  141. package/dist/hooks/useMenuHover.d.ts.map +1 -1
  142. package/dist/hooks/useMenuHover.js +18 -13
  143. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  144. package/dist/i18n/generated-locales/en.generated.js +112 -1
  145. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  146. package/dist/i18n/generated-locales/pseudo.generated.js +112 -1
  147. package/dist/index.d.ts +1 -0
  148. package/dist/index.d.ts.map +1 -1
  149. package/dist/index.js +1 -0
  150. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  151. package/dist/theme/derivedVarRegistry.js +0 -3
  152. package/dist/utils/focusOutline.stylex.d.ts +4 -0
  153. package/dist/utils/focusOutline.stylex.d.ts.map +1 -1
  154. package/dist/utils/focusOutline.stylex.js +4 -0
  155. package/dist/utils/plainDate.d.ts.map +1 -1
  156. package/dist/utils/plainDate.js +15 -9
  157. package/dist/utils/sharedMutationObserver.d.ts +21 -0
  158. package/dist/utils/sharedMutationObserver.d.ts.map +1 -0
  159. package/dist/utils/sharedMutationObserver.js +69 -0
  160. package/locales/en.json +444 -0
  161. package/locales/pseudo.json +333 -0
  162. package/package.json +9 -4
  163. package/src/AspectRatio/AspectRatio.test.tsx +44 -43
  164. package/src/AspectRatio/AspectRatio.tsx +38 -18
  165. package/src/AspectRatio/__tests__/AspectRatioFitLayers.a11y.browser.spec.ts +157 -0
  166. package/src/Avatar/Avatar.doc.mjs +5 -3
  167. package/src/AvatarGroup/AvatarGroup.doc.mjs +3 -2
  168. package/src/Breadcrumbs/BreadcrumbItem.tsx +4 -1
  169. package/src/Breadcrumbs/Breadcrumbs.test.tsx +30 -1
  170. package/src/Button/Button.doc.mjs +2 -2
  171. package/src/Button/Button.test.tsx +155 -9
  172. package/src/Button/Button.tsx +39 -16
  173. package/src/Button/__tests__/Button.a11y.known-failures.ts +1 -62
  174. package/src/Button/__tests__/Button.a11y.states.ts +0 -10
  175. package/src/Chat/__tests__/ChatMessageMetadata.a11y.chromium.spec.ts +3 -1
  176. package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
  177. package/src/CheckboxInput/CheckboxInput.tsx +3 -1
  178. package/src/CheckboxList/CheckboxList.test.tsx +22 -0
  179. package/src/CheckboxList/CheckboxListItem.doc.mjs +16 -0
  180. package/src/CheckboxList/CheckboxListItem.tsx +10 -0
  181. package/src/CommandPalette/CommandPaletteInput.test.tsx +53 -4
  182. package/src/CommandPalette/CommandPaletteInput.tsx +33 -3
  183. package/src/CommandPalette/__tests__/CommandPaletteInputAudit.a11y.chromium.spec.ts +335 -0
  184. package/src/CommandPalette/__tests__/CommandPaletteItemAudit.a11y.chromium.spec.ts +292 -0
  185. package/src/ContextMenu/ContextMenu.doc.mjs +2 -3
  186. package/src/ContextMenu/ContextMenu.tsx +4 -1
  187. package/src/ContextMenu/__tests__/ContextMenuAudit.a11y.chromium.spec.ts +438 -0
  188. package/src/Dialog/Dialog.test.tsx +46 -0
  189. package/src/Dialog/Dialog.tsx +20 -7
  190. package/src/Dialog/__tests__/Dialog.a11y.known-failures.ts +0 -14
  191. package/src/Drawer/Drawer.doc.mjs +472 -0
  192. package/src/Drawer/Drawer.spec.md +357 -0
  193. package/src/Drawer/Drawer.test.tsx +1300 -0
  194. package/src/Drawer/Drawer.tsx +613 -0
  195. package/src/Drawer/DrawerDirection.test.tsx +127 -0
  196. package/src/Drawer/DrawerHeader.doc.mjs +293 -0
  197. package/src/Drawer/DrawerHeader.test.tsx +102 -0
  198. package/src/Drawer/DrawerHeader.tsx +235 -0
  199. package/src/Drawer/__tests__/DrawerAudit.a11y.chromium.spec.ts +401 -0
  200. package/src/Drawer/__tests__/DrawerHeaderAudit.a11y.chromium.spec.ts +297 -0
  201. package/src/Drawer/index.ts +13 -0
  202. package/src/Drawer/useDrawerDialogPresence.ts +284 -0
  203. package/src/DropdownMenu/DropdownMenu.test.tsx +62 -13
  204. package/src/DropdownMenu/DropdownMenu.tsx +47 -4
  205. package/src/DropdownMenu/DropdownMenuContext.tsx +24 -0
  206. package/src/DropdownMenu/DropdownMenuDivider.doc.mjs +2 -2
  207. package/src/DropdownMenu/DropdownMenuDivider.spec.md +192 -0
  208. package/src/DropdownMenu/DropdownMenuDivider.tsx +9 -5
  209. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +185 -0
  210. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +19 -5
  211. package/src/DropdownMenu/__tests__/DropdownMenuDividerAudit.a11y.chromium.spec.ts +280 -0
  212. package/src/DropdownMenu/__tests__/Menu.a11y.chromium.spec.ts +145 -0
  213. package/src/DropdownMenu/__tests__/Menu.a11y.known-failures.ts +26 -0
  214. package/src/DropdownMenu/__tests__/Menu.a11y.renders.tsx +65 -0
  215. package/src/DropdownMenu/__tests__/Menu.a11y.states.ts +138 -0
  216. package/src/DropdownMenu/__tests__/Menu.a11y.test.tsx +136 -0
  217. package/src/EmptyState/EmptyState.doc.mjs +3 -2
  218. package/src/EmptyState/EmptyState.spec.md +189 -0
  219. package/src/EmptyState/EmptyState.test.tsx +74 -3
  220. package/src/EmptyState/EmptyState.tsx +4 -8
  221. package/src/Field/Field.doc.mjs +14 -8
  222. package/src/Field/Field.spec.md +217 -0
  223. package/src/Field/Field.test.tsx +60 -2
  224. package/src/Field/Field.tsx +17 -8
  225. package/src/Grid/GridSpan.tsx +3 -0
  226. package/src/Heading/Heading.test.tsx +57 -0
  227. package/src/Heading/Heading.tsx +6 -2
  228. package/src/HoverCard/HoverCard.test.tsx +58 -0
  229. package/src/HoverCard/useHoverCard.tsx +10 -2
  230. package/src/IconButton/IconButton.doc.mjs +2 -2
  231. package/src/IconButton/IconButton.test.tsx +3 -1
  232. package/src/Item/Item.doc.mjs +12 -8
  233. package/src/Item/Item.spec.md +226 -0
  234. package/src/Item/Item.test.tsx +61 -0
  235. package/src/Item/Item.tsx +40 -4
  236. package/src/Layer/layerViewportInset.source-build.test.mjs +90 -0
  237. package/src/Layer/layerViewportInset.stylex.ts +6 -2
  238. package/src/Layer/layerViewportInset.test.ts +1 -0
  239. package/src/Layer/useLayer.tsx +24 -13
  240. package/src/Layer/useLayerDismissal.ts +1 -1
  241. package/src/Layout/container.stylex.ts +6 -6
  242. package/src/List/List.doc.mjs +14 -1
  243. package/src/List/List.spec.md +129 -77
  244. package/src/List/List.test.tsx +163 -94
  245. package/src/List/List.tsx +74 -18
  246. package/src/List/ListContext.tsx +9 -3
  247. package/src/List/ListItem.doc.mjs +24 -5
  248. package/src/List/ListItem.tsx +56 -27
  249. package/src/List/index.ts +1 -1
  250. package/src/Markdown/Markdown.tsx +14 -1
  251. package/src/Markdown/plugins/frontmatter.test.tsx +25 -0
  252. package/src/Markdown/plugins/frontmatter.ts +7 -3
  253. package/src/MetadataList/MetadataList.doc.mjs +2 -2
  254. package/src/MetadataList/MetadataList.test.tsx +38 -0
  255. package/src/MetadataList/MetadataList.tsx +8 -1
  256. package/src/MetadataList/MetadataListContext.tsx +1 -1
  257. package/src/MultiSelector/MultiSelector.test.tsx +159 -0
  258. package/src/MultiSelector/MultiSelector.tsx +40 -7
  259. package/src/MultiSelector/OptionActions.a11y.chromium.spec.ts +112 -2
  260. package/src/MultiSelector/hooks.ts +3 -1
  261. package/src/MultiSelector/useMultiCombobox.doc.mjs +267 -0
  262. package/src/Pagination/Pagination.doc.mjs +7 -7
  263. package/src/Pagination/Pagination.spec.md +9 -9
  264. package/src/Pagination/Pagination.test.tsx +242 -0
  265. package/src/Pagination/Pagination.tsx +27 -11
  266. package/src/Popover/Popover.test.tsx +2 -2
  267. package/src/Popover/Popover.tsx +4 -4
  268. package/src/PowerSearch/PowerSearch.test.tsx +85 -0
  269. package/src/PowerSearch/PowerSearch.tsx +2 -2
  270. package/src/RadioList/RadioList.fr10.test.tsx +45 -2
  271. package/src/RadioList/RadioList.test.tsx +22 -0
  272. package/src/RadioList/RadioList.tsx +4 -1
  273. package/src/RadioList/RadioListItem.doc.mjs +16 -0
  274. package/src/RadioList/RadioListItem.tsx +10 -1
  275. package/src/Selector/hooks.ts +6 -2
  276. package/src/Selector/useCombobox.doc.mjs +237 -0
  277. package/src/SideNav/SideNav.test.tsx +179 -0
  278. package/src/SideNav/SideNavItem.doc.mjs +2 -2
  279. package/src/SideNav/SideNavItem.tsx +96 -17
  280. package/src/Slider/Slider.spec.md +39 -18
  281. package/src/Switch/Switch.test.tsx +49 -0
  282. package/src/Switch/Switch.tsx +11 -0
  283. package/src/TabList/TabList.spec.md +57 -44
  284. package/src/TabList/TabList.test.tsx +67 -0
  285. package/src/TabList/TabMenu.doc.mjs +4 -3
  286. package/src/TabList/TabMenu.tsx +7 -4
  287. package/src/Text/Text.test.tsx +57 -0
  288. package/src/Text/Text.tsx +6 -2
  289. package/src/Text/useTruncation.test.tsx +69 -0
  290. package/src/Text/useTruncation.ts +16 -5
  291. package/src/TimeInput/TimeInput.fr10.test.tsx +15 -0
  292. package/src/ToggleButton/ToggleButton.test.tsx +95 -0
  293. package/src/ToggleButton/ToggleButton.tsx +12 -1
  294. package/src/Tokenizer/Tokenizer.test.tsx +54 -1
  295. package/src/Tooltip/Tooltip.test.tsx +31 -0
  296. package/src/Tooltip/useTooltip.test.tsx +13 -0
  297. package/src/Tooltip/useTooltip.tsx +11 -0
  298. package/src/TopNav/TopNavMegaMenu.test.tsx +22 -0
  299. package/src/TopNav/TopNavMegaMenu.tsx +7 -3
  300. package/src/TopNav/TopNavMenu.test.tsx +19 -0
  301. package/src/TopNav/TopNavMenu.tsx +11 -3
  302. package/src/TreeList/TreeList.doc.mjs +1 -0
  303. package/src/TreeList/TreeList.spec.md +29 -17
  304. package/src/TreeList/TreeList.test.tsx +327 -6
  305. package/src/TreeList/TreeList.tsx +5 -1
  306. package/src/TreeList/TreeListBranches.tsx +6 -0
  307. package/src/TreeList/TreeListItem.tsx +88 -13
  308. package/src/Typeahead/BaseTypeahead.tsx +26 -0
  309. package/src/Typeahead/Typeahead.test.tsx +260 -2
  310. package/src/docPropLiterals.test.ts +36 -10
  311. package/src/hooks/useEntryAnimation.doc.mjs +4 -4
  312. package/src/hooks/useEntryAnimation.ssr.test.tsx +143 -0
  313. package/src/hooks/useEntryAnimation.ts +15 -16
  314. package/src/hooks/useIsHydrating.test.tsx +70 -0
  315. package/src/hooks/useIsHydrating.ts +31 -0
  316. package/src/hooks/useMenuHover.ts +18 -13
  317. package/src/i18n/InternationalizationProvider.doc.mjs +8 -0
  318. package/src/i18n/InternationalizationProvider.spec.md +252 -0
  319. package/src/i18n/__tests__/InternationalizationProvider.test.tsx +128 -0
  320. package/src/i18n/generated-locales/en.generated.ts +121 -0
  321. package/src/i18n/generated-locales/pseudo.generated.ts +121 -0
  322. package/src/i18n/useTranslator.test.tsx +37 -0
  323. package/src/index.ts +1 -0
  324. package/src/reset.css +0 -29
  325. package/src/theme/derivedVarRegistry.test.ts +7 -3
  326. package/src/theme/derivedVarRegistry.ts +0 -3
  327. package/src/utils/focusOutline.stylex.ts +4 -0
  328. package/src/utils/plainDate.test.ts +37 -1
  329. package/src/utils/plainDate.ts +15 -5
  330. package/src/utils/sharedMutationObserver.test.ts +53 -0
  331. package/src/utils/sharedMutationObserver.ts +78 -0
@@ -4,9 +4,10 @@
4
4
 
5
5
  /**
6
6
  * @file ListItem.tsx
7
- * @input Uses React, ReactNode, StyleXStyles, theme tokens, List edge compensation,
8
- * CheckboxInput, and the marker or task checkbox a ListMarkerScope sets
9
- * (Markdown's nested lists and the task items of mixed lists)
7
+ * @input Uses React, ReactNode, StyleXStyles, theme tokens, the List row edge
8
+ * treatment, CheckboxInput, and the marker or task checkbox a
9
+ * ListMarkerScope sets (Markdown's nested lists and the task items of mixed
10
+ * lists)
10
11
  * @output Exports ListItem component, ListItemProps type
11
12
  * @position Core implementation; consumed by List, index.ts, tested by List.test.tsx
12
13
  *
@@ -23,6 +24,7 @@
23
24
 
24
25
  import {use, type ReactNode} from 'react';
25
26
  import * as stylex from '@stylexjs/stylex';
27
+ import type {StyleXStyles} from '@stylexjs/stylex';
26
28
  import {
27
29
  colorVars,
28
30
  spacingVars,
@@ -34,7 +36,7 @@ import {ListContext, type ListMarker} from './ListContext';
34
36
  import {CheckboxInput} from '../CheckboxInput';
35
37
  import {mergeProps} from '../utils';
36
38
  import {Item} from '../Item';
37
- import type {ItemSwipeActions, ItemSwipeBehavior} from '../Item';
39
+ import type {ItemAlign, ItemSwipeActions, ItemSwipeBehavior} from '../Item';
38
40
  import {themeProps} from '../utils/themeProps';
39
41
 
40
42
  // =============================================================================
@@ -44,6 +46,13 @@ import {themeProps} from '../utils/themeProps';
44
46
  export interface ListItemProps extends BaseProps<HTMLLIElement> {
45
47
  /** Ref forwarded to the root element */
46
48
  ref?: React.Ref<HTMLLIElement>;
49
+ /**
50
+ * Vertical alignment of the start and end content against the label,
51
+ * forwarded to Item. Use `first-line` to keep the start and end content level with the
52
+ * first line of a wrapped label.
53
+ * @default 'center'
54
+ */
55
+ align?: ItemAlign;
47
56
  /**
48
57
  * Primary text label for the item.
49
58
  *
@@ -142,23 +151,37 @@ const styles = stylex.create({
142
151
  withCounter: {
143
152
  counterIncrement: 'astryx-list',
144
153
  },
145
- // List's inline edge compensation cancels as much of the row's built-in
146
- // inline inset as the container padding allows (e.g. under a heading).
147
- // The margins read --_item-inset-inline — the same variable Item derives
148
- // its paddingInline from — on the row element itself (custom properties
149
- // only cascade downward, so the <ul> could not read it). Density changes
150
- // and theme paddingInline overrides on `item` move both values together.
151
- // Each edge clamps against ITS OWN container padding var: a single
152
- // start-var clamp on both margins over-cancels the end edge under
153
- // asymmetric container padding (16px start / 4px end) and the selected
154
- // row paints past the outer border. Logical properties keep RTL correct,
155
- // and a zero-padding/full-bleed surface (min(inset, 0px) = 0px) leaves
156
- // the row in place instead of pulling it outside its content edge.
157
- inlineEdgeCompensation: {
154
+ // List's edge treatments. Each reads --_item-inset-inline, the variable
155
+ // Item derives its paddingInline from, on the row itself (custom properties
156
+ // only cascade downward, so the <ul> cannot read it); density and theme
157
+ // paddingInline overrides on `item` move it. Each edge resolves against ITS
158
+ // OWN container padding var, so asymmetric padding never over-reaches an
159
+ // edge, and logical properties keep RTL correct.
160
+ //
161
+ // 'flush': no inset, so content meets the row's edges.
162
+ edgeFlush: {
163
+ '--_item-inset-inline': '0px',
164
+ },
165
+ // 'outset': the list reaches the container's padding edge (List.tsx); the
166
+ // row steps back in by whatever padding its inset does not fill, so the row
167
+ // reaches min(inset, container padding) past the content line and its text
168
+ // lands on that line when the padding is at least the inset.
169
+ edgeOutset: {
158
170
  marginInlineStart:
159
- 'calc(-1 * min(var(--_item-inset-inline), var(--container-padding-inline-start, 0px)))',
171
+ 'max(var(--container-padding-inline-start, 0px) - var(--_item-inset-inline), 0px)',
160
172
  marginInlineEnd:
161
- 'calc(-1 * min(var(--_item-inset-inline), var(--container-padding-inline-end, 0px)))',
173
+ 'max(var(--container-padding-inline-end, 0px) - var(--_item-inset-inline), 0px)',
174
+ },
175
+ // 'bleed': the row fills the list, which reaches the container's padding
176
+ // edge (List.tsx); its text sits on the container's padding line, never
177
+ // closer to the row's edge than its own inset. Square corners: the row
178
+ // meets the container's straight sides.
179
+ edgeBleed: {
180
+ paddingInlineStart:
181
+ 'max(var(--container-padding-inline-start, 0px), var(--_item-inset-inline))',
182
+ paddingInlineEnd:
183
+ 'max(var(--container-padding-inline-end, 0px), var(--_item-inset-inline))',
184
+ borderRadius: 0,
162
185
  },
163
186
  withDivider: {
164
187
  borderBlockEndWidth: borderVars['--border-width'],
@@ -296,6 +319,7 @@ export function ListItem({
296
319
  rel,
297
320
  isDisabled = false,
298
321
  isSelected = false,
322
+ align,
299
323
  xstyle,
300
324
  className,
301
325
  style,
@@ -305,8 +329,8 @@ export function ListItem({
305
329
  const ctx = use(ListContext);
306
330
  const density = ctx?.density ?? 'balanced';
307
331
  const hasDividers = ctx?.hasDividers ?? false;
332
+ const edge = ctx?.edge ?? 'padded';
308
333
  const listStyle = ctx?.listStyle ?? 'none';
309
- const edgeCompensation = ctx?.edgeCompensation;
310
334
  const hasMarkers = listStyle !== 'none';
311
335
  // ListMarkerScope may name another marker for this item.
312
336
  const markerKind: ListMarker | null =
@@ -360,14 +384,19 @@ export function ListItem({
360
384
  rel={rel}
361
385
  isDisabled={isDisabled}
362
386
  isSelected={isSelected}
387
+ align={align}
363
388
  density={density}
364
- xstyle={[
365
- hasMarkers && styles.withCounter,
366
- hasDividers && styles.withDivider,
367
- hasDividers && embeddedStyles.noRadius,
368
- edgeCompensation === 'inline' && styles.inlineEdgeCompensation,
369
- xstyle,
370
- ]}
389
+ xstyle={
390
+ [
391
+ hasMarkers && styles.withCounter,
392
+ hasDividers && styles.withDivider,
393
+ hasDividers && embeddedStyles.noRadius,
394
+ edge === 'flush' && styles.edgeFlush,
395
+ edge === 'outset' && styles.edgeOutset,
396
+ edge === 'bleed' && styles.edgeBleed,
397
+ xstyle,
398
+ ] as StyleXStyles
399
+ }
371
400
  {...mergeProps(themeProps('list-item'), {className, style})}
372
401
  {...restProps}
373
402
  />
package/src/List/index.ts CHANGED
@@ -12,6 +12,6 @@
12
12
  */
13
13
 
14
14
  export {List} from './List';
15
- export type {ListProps, ListStyle, ListDensity} from './List';
15
+ export type {ListProps, ListStyle, ListDensity, ListEdge} from './List';
16
16
  export {ListItem} from './ListItem';
17
17
  export type {ListItemProps} from './ListItem';
@@ -405,6 +405,16 @@ const markdownTableCellStyles = stylex.create({
405
405
  code: {
406
406
  wordBreak: 'normal',
407
407
  },
408
+ // The grid's outer edge. `dividers="grid"` draws the lines *between* cells
409
+ // and stops at the table's rim, so without this the top, bottom, and sides
410
+ // stay open and the grid reads as unfinished. On the table element, not the
411
+ // wrapper, so `border-collapse: collapse` merges it with the cell lines
412
+ // into one 1px rule rather than doubling up at the edge.
413
+ tableOuterEdge: {
414
+ borderWidth: borderVars['--border-width'],
415
+ borderStyle: 'solid',
416
+ borderColor: colorVars['--color-border'],
417
+ },
408
418
  });
409
419
 
410
420
  /**
@@ -1905,7 +1915,10 @@ function renderBlock(
1905
1915
  ),
1906
1916
  )}
1907
1917
  {...lineStamps}>
1908
- <Table dividers="rows" textOverflow="wrap">
1918
+ <Table
1919
+ dividers="grid"
1920
+ textOverflow="wrap"
1921
+ xstyle={markdownTableCellStyles.tableOuterEdge}>
1909
1922
  <TableHeader>
1910
1923
  <TableRow>
1911
1924
  {header?.children.map((h, i) => (
@@ -117,6 +117,31 @@ describe('createMarkdownFrontmatter', () => {
117
117
  ).not.toThrow();
118
118
  });
119
119
 
120
+ it('accepts the decoded fields record as metadata (identity parse)', () => {
121
+ const fields = createMarkdownFrontmatter({
122
+ name: 'fields',
123
+ parse: record => record,
124
+ });
125
+ const source = '---\ntitle: Release notes\n__proto__: kept\n---\n# Shipped';
126
+
127
+ const parsed = fields.parse(source);
128
+ expect(parsed).toEqual({
129
+ status: 'match',
130
+ contentStart: source.indexOf('# Shipped'),
131
+ metadata: {title: 'Release notes', ['__proto__']: 'kept'},
132
+ });
133
+ if (parsed.status !== 'match') {
134
+ throw new Error('expected a frontmatter match');
135
+ }
136
+ // A field named like a prototype key stays an ordinary own field.
137
+ expect(Object.hasOwn(parsed.metadata, '__proto__')).toBe(true);
138
+ expect(Object.getPrototypeOf(parsed.metadata)).toBe(Object.prototype);
139
+
140
+ const root = parseMarkdownAst(source, {plugins: [fields.plugin]});
141
+ expect(fields.getMetadata(root)).toEqual(parsed.metadata);
142
+ expect(JSON.stringify(root)).not.toContain('title: Release notes');
143
+ });
144
+
120
145
  it('returns a transform plugin that composes in the ordered pipeline', () => {
121
146
  expectTypeOf<MarkdownTransform>().toBeFunction();
122
147
  const root = parseMarkdownAst(SOURCE, {plugins: [documentMetadata.plugin]});
@@ -104,18 +104,22 @@ function readFrontmatter(source: string):
104
104
  if (close == null) {
105
105
  return {status: 'defer'};
106
106
  }
107
- const fields: Record<string, string> = Object.create(null);
107
+ const entries = new Map<string, string>();
108
108
  for (const line of body.slice(0, close.index).split(/\r?\n/)) {
109
109
  if (line.trim() === '') {
110
110
  continue;
111
111
  }
112
112
  const separator = line.indexOf(':');
113
113
  const key = line.slice(0, separator).trim();
114
- if (separator <= 0 || key === '' || Object.hasOwn(fields, key)) {
114
+ if (separator <= 0 || key === '' || entries.has(key)) {
115
115
  return {status: 'none'};
116
116
  }
117
- fields[key] = line.slice(separator + 1).trim();
117
+ entries.set(key, line.slice(separator + 1).trim());
118
118
  }
119
+ // A plain object, so a parser that returns the record unchanged passes the
120
+ // JSON-like metadata check. `fromEntries` defines every key as an own data
121
+ // property, so a field named `__proto__` stays a field.
122
+ const fields: Record<string, string> = Object.fromEntries(entries);
119
123
  let end = openingLength + close.index + close[0].length;
120
124
  if (source.startsWith('\r\n', end)) {
121
125
  end += 2;
@@ -43,8 +43,8 @@ export const docs = {
43
43
  {
44
44
  name: 'label',
45
45
  type: "{ position?: 'start' | 'top', width?: number | string }",
46
- description: "Label display configuration. position controls label placement, width sets a custom label column width. Defaults to { position: 'top' } for multi-column layouts.",
47
- default: "{ position: 'start' } (single-column) / { position: 'top' } (multi-column)",
46
+ description: "Label display configuration. position controls label placement, width sets a custom label column width. Defaults to { position: 'top' } for multi-column layouts. Position is ignored in horizontal mode, which always uses { position: 'top' }.",
47
+ default: "{ position: 'start' } (vertical single-column) / { position: 'top' } (multi-column or horizontal)",
48
48
  },
49
49
  {
50
50
  name: 'maxNumOfItems',
@@ -124,6 +124,30 @@ describe('MetadataList', () => {
124
124
  expect(screen.getByText('B')).toBeInTheDocument();
125
125
  });
126
126
 
127
+ it('uses top labels in horizontal orientation even when position is start', () => {
128
+ render(
129
+ <MetadataList orientation="horizontal" label={{position: 'start'}}>
130
+ <MetadataListItem label="A">1</MetadataListItem>
131
+ </MetadataList>,
132
+ );
133
+
134
+ const item = screen.getByRole('term').closest('.astryx-metadata-list-item');
135
+ expect(item).toContainElement(screen.getByRole('definition'));
136
+ });
137
+
138
+ it('defaults to side labels and preserves width when position is omitted', () => {
139
+ const {container} = render(
140
+ <MetadataList label={{width: 120}}>
141
+ <MetadataListItem label="A">1</MetadataListItem>
142
+ </MetadataList>,
143
+ );
144
+
145
+ const list = container.querySelector('dl');
146
+ expect(list?.getAttribute('style')).toContain('120px minmax(0, 1fr)');
147
+ expect(screen.getByRole('term').parentElement).toBe(list);
148
+ expect(screen.getByRole('definition').parentElement).toBe(list);
149
+ });
150
+
127
151
  describe('numeric columns', () => {
128
152
  // A fixed column count is a runtime value, so it arrives as a StyleX
129
153
  // dynamic style: the template lands in the element's inline style (as the
@@ -163,6 +187,20 @@ describe('MetadataList', () => {
163
187
  expect(gridTemplateOf(container)).not.toContain('repeat(');
164
188
  });
165
189
 
190
+ it('defaults to top labels for columns={2} with a width-only label config', () => {
191
+ const {container} = render(
192
+ <MetadataList columns={2} label={{width: 260}}>
193
+ <MetadataListItem label="A">1</MetadataListItem>
194
+ </MetadataList>,
195
+ );
196
+
197
+ expect(gridTemplateOf(container)).toContain('repeat(2, 1fr)');
198
+ const item = screen
199
+ .getByRole('term')
200
+ .closest('.astryx-metadata-list-item');
201
+ expect(item).toContainElement(screen.getByRole('definition'));
202
+ });
203
+
166
204
  it('ignores numeric columns in horizontal orientation', () => {
167
205
  const {container} = render(
168
206
  <MetadataList columns={3} orientation="horizontal">
@@ -206,7 +206,14 @@ export function MetadataList({
206
206
  columns === 'multi' || (typeof columns === 'number' && columns > 1);
207
207
  // Default to 'top' for multi-column and horizontal layouts since side labels
208
208
  // don't work well when items are in separate grid cells.
209
- const labelConfig = label ?? (isMultiColumn ? LABEL_TOP : LABEL_START);
209
+ const defaultLabelConfig = isMultiColumn ? LABEL_TOP : LABEL_START;
210
+ const labelConfig = useMemo(
211
+ () => ({
212
+ ...label,
213
+ position: label?.position ?? defaultLabelConfig.position,
214
+ }),
215
+ [label, defaultLabelConfig],
216
+ );
210
217
  const [isShowAll, setIsShowAll] = useState(false);
211
218
  const contentId = useId();
212
219
  const t = useTranslator();
@@ -12,7 +12,7 @@
12
12
  import {createLayerScopedContext as createContext} from '../Layer/layerScopedContext';
13
13
 
14
14
  export interface MetadataListLabelConfig {
15
- position: 'start' | 'top';
15
+ position?: 'start' | 'top';
16
16
  width?: number | string;
17
17
  }
18
18
 
@@ -35,6 +35,7 @@ import {generateThemeCSS} from '../theme/generateThemeRules';
35
35
  import {selectorPresentationStyles} from '../Selector/selectorPresentation.stylex';
36
36
  import {groupStyles} from '../InputGroup/groupStyles';
37
37
  import {InputGroup, InputGroupText} from '../InputGroup';
38
+ import {focusVars} from '../theme/tokens.stylex';
38
39
 
39
40
  function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
40
41
  const {prose, component} = generateThemeCSS(theme);
@@ -4083,6 +4084,164 @@ describe('MultiSelector option actions (grid)', () => {
4083
4084
  expect(onChange).not.toHaveBeenCalled();
4084
4085
  });
4085
4086
 
4087
+ it('draws the standard focus ring on the active action cell, from the focus tokens', async () => {
4088
+ // jsdom resolves no custom property, so the ring is read from StyleX
4089
+ // classes: a class name hashes one property and its value, and the same
4090
+ // declaration yields the same class wherever it is written.
4091
+ const ring = stylex.create({
4092
+ active: {
4093
+ outlineWidth: focusVars['--focus-outline-width'],
4094
+ outlineStyle: focusVars['--focus-outline-style'],
4095
+ outlineColor: focusVars['--focus-outline-color'],
4096
+ outlineOffset: focusVars['--focus-outline-offset'],
4097
+ },
4098
+ });
4099
+ const ringClasses = stylex
4100
+ .props(ring.active)
4101
+ .className!.split(' ')
4102
+ .filter(token => /^x[a-z0-9]+$/.test(token));
4103
+ expect(ringClasses).toHaveLength(4);
4104
+
4105
+ const user = userEvent.setup();
4106
+ render(
4107
+ <MultiSelector
4108
+ label="Labels"
4109
+ options={OPTIONS}
4110
+ value={[]}
4111
+ onChange={() => {}}
4112
+ />,
4113
+ );
4114
+ const trigger = screen.getByRole('combobox', {name: 'Labels'});
4115
+ await user.click(trigger);
4116
+ const bug = screen.getByRole('row', {name: 'Bug', ...h});
4117
+ const actionCell = bug.children[1] as HTMLElement;
4118
+ for (const token of ringClasses) {
4119
+ expect(actionCell).not.toHaveClass(token);
4120
+ }
4121
+ fireEvent.keyDown(trigger, {key: 'ArrowRight'});
4122
+ expect(trigger).toHaveAttribute('aria-activedescendant', actionCell.id);
4123
+ for (const token of ringClasses) {
4124
+ expect(actionCell).toHaveClass(token);
4125
+ }
4126
+ });
4127
+
4128
+ it('draws no focus ring on an action cell highlighted after pointer input, until a key moves the highlight', async () => {
4129
+ const ring = stylex.create({
4130
+ active: {
4131
+ outlineWidth: focusVars['--focus-outline-width'],
4132
+ outlineStyle: focusVars['--focus-outline-style'],
4133
+ outlineColor: focusVars['--focus-outline-color'],
4134
+ outlineOffset: focusVars['--focus-outline-offset'],
4135
+ },
4136
+ });
4137
+ const ringClasses = stylex
4138
+ .props(ring.active)
4139
+ .className!.split(' ')
4140
+ .filter(token => /^x[a-z0-9]+$/.test(token));
4141
+ expect(ringClasses).toHaveLength(4);
4142
+
4143
+ const user = userEvent.setup();
4144
+ render(
4145
+ <MultiSelector
4146
+ label="Labels"
4147
+ options={OPTIONS}
4148
+ value={[]}
4149
+ onChange={() => {}}
4150
+ />,
4151
+ );
4152
+ const trigger = screen.getByRole('combobox', {name: 'Labels'});
4153
+ await user.click(trigger);
4154
+ const bug = screen.getByRole('row', {name: 'Bug', ...h});
4155
+ const actionCell = bug.children[1] as HTMLElement;
4156
+ fireEvent.mouseEnter(actionCell);
4157
+ // The hover moved the highlight onto the action cell, without the ring.
4158
+ expect(trigger).toHaveAttribute('aria-activedescendant', actionCell.id);
4159
+ for (const token of ringClasses) {
4160
+ expect(actionCell).not.toHaveClass(token);
4161
+ }
4162
+ // Keyboard navigation back onto the cell draws it.
4163
+ fireEvent.keyDown(trigger, {key: 'ArrowLeft'});
4164
+ fireEvent.keyDown(trigger, {key: 'ArrowRight'});
4165
+ expect(trigger).toHaveAttribute('aria-activedescendant', actionCell.id);
4166
+ for (const token of ringClasses) {
4167
+ expect(actionCell).toHaveClass(token);
4168
+ }
4169
+ });
4170
+
4171
+ describe('ring under mixed input', () => {
4172
+ const ringClasses = () => {
4173
+ const ring = stylex.create({
4174
+ active: {
4175
+ outlineWidth: focusVars['--focus-outline-width'],
4176
+ outlineStyle: focusVars['--focus-outline-style'],
4177
+ outlineColor: focusVars['--focus-outline-color'],
4178
+ outlineOffset: focusVars['--focus-outline-offset'],
4179
+ },
4180
+ });
4181
+ return stylex
4182
+ .props(ring.active)
4183
+ .className!.split(' ')
4184
+ .filter(token => /^x[a-z0-9]+$/.test(token));
4185
+ };
4186
+ const hasRing = (cell: HTMLElement) =>
4187
+ ringClasses().every(token => cell.classList.contains(token));
4188
+
4189
+ async function openWithKeyboardOnBugAction() {
4190
+ const user = userEvent.setup();
4191
+ render(
4192
+ <MultiSelector
4193
+ label="Labels"
4194
+ options={OPTIONS}
4195
+ value={[]}
4196
+ onChange={() => {}}
4197
+ />,
4198
+ );
4199
+ const trigger = screen.getByRole('combobox', {name: 'Labels'});
4200
+ await user.click(trigger);
4201
+ const row = (name: string) => screen.getByRole('row', {name, ...h});
4202
+ fireEvent.keyDown(trigger, {key: 'ArrowRight'});
4203
+ const bugAction = row('Bug').children[1] as HTMLElement;
4204
+ expect(trigger).toHaveAttribute('aria-activedescendant', bugAction.id);
4205
+ expect(hasRing(bugAction)).toBe(true);
4206
+ return {trigger, row, bugAction};
4207
+ }
4208
+
4209
+ it('keeps the ring when the pointer crosses a disabled option the highlight ignores', async () => {
4210
+ const {trigger, row, bugAction} = await openWithKeyboardOnBugAction();
4211
+ fireEvent.mouseEnter(row('Docs').children[0]);
4212
+ expect(trigger).toHaveAttribute('aria-activedescendant', bugAction.id);
4213
+ expect(hasRing(bugAction)).toBe(true);
4214
+ });
4215
+
4216
+ it('follows the keyboard modality when a hover moves the highlight to another action', async () => {
4217
+ const {trigger, row} = await openWithKeyboardOnBugAction();
4218
+ const docsAction = row('Docs').children[1] as HTMLElement;
4219
+ fireEvent.mouseEnter(docsAction);
4220
+ expect(trigger).toHaveAttribute('aria-activedescendant', docsAction.id);
4221
+ expect(hasRing(docsAction)).toBe(true);
4222
+ });
4223
+
4224
+ it('draws no ring for a modifier chord after pointer input', async () => {
4225
+ const user = userEvent.setup();
4226
+ render(
4227
+ <MultiSelector
4228
+ label="Labels"
4229
+ options={OPTIONS}
4230
+ value={[]}
4231
+ onChange={() => {}}
4232
+ />,
4233
+ );
4234
+ const trigger = screen.getByRole('combobox', {name: 'Labels'});
4235
+ await user.click(trigger);
4236
+ const bugAction = screen.getByRole('row', {name: 'Bug', ...h})
4237
+ .children[1] as HTMLElement;
4238
+ fireEvent.mouseEnter(bugAction);
4239
+ expect(hasRing(bugAction)).toBe(false);
4240
+ fireEvent.keyDown(trigger, {key: 'ArrowRight', ctrlKey: true});
4241
+ expect(hasRing(bugAction)).toBe(false);
4242
+ });
4243
+ });
4244
+
4086
4245
  it('the inline-end arrow reaches the action cell, Enter fires it, inline-start returns', async () => {
4087
4246
  const user = userEvent.setup();
4088
4247
  const onChange = vi.fn();
@@ -50,6 +50,7 @@ import type {IndicatorPosition} from '../Indicator';
50
50
  import {Badge} from '../Badge';
51
51
  import {
52
52
  colorVars,
53
+ focusVars,
53
54
  sizeVars,
54
55
  spacingVars,
55
56
  radiusVars,
@@ -74,7 +75,10 @@ import {
74
75
  import {useMultiCombobox} from './hooks';
75
76
  import {getInputARIA, isImeKeyEvent, mergeProps} from '../utils';
76
77
  import {devWarn, warnOnce} from '../utils/devWarning';
77
- import {FOCUS_OUTLINE_PARTS} from '../utils/focusOutline.stylex';
78
+ import {
79
+ getInteractionModality,
80
+ useInteractionModalityTracking,
81
+ } from '../utils/interactionModality';
78
82
  import {useAnnounce} from '../hooks/useAnnounce';
79
83
  import {FOCUSABLE_SELECTOR} from '../hooks/focusableSelector';
80
84
  import {useAnnounceRenderedText} from '../hooks/useAnnounceRenderedText';
@@ -436,11 +440,15 @@ const styles = stylex.create({
436
440
  flexShrink: 0,
437
441
  borderRadius: radiusVars['--radius-element'],
438
442
  },
443
+ // The standard ring, from the focus tokens themselves: inside
444
+ // `stylex.create` StyleX resolves an imported `defineVars` group, and a
445
+ // plain object such as `FOCUS_OUTLINE_PARTS` compiles to variables nothing
446
+ // defines.
439
447
  actionCellActive: {
440
- outlineWidth: FOCUS_OUTLINE_PARTS.outlineWidth,
441
- outlineStyle: FOCUS_OUTLINE_PARTS.outlineStyle,
442
- outlineColor: FOCUS_OUTLINE_PARTS.outlineColor,
443
- outlineOffset: FOCUS_OUTLINE_PARTS.outlineOffset,
448
+ outlineWidth: focusVars['--focus-outline-width'],
449
+ outlineStyle: focusVars['--focus-outline-style'],
450
+ outlineColor: focusVars['--focus-outline-color'],
451
+ outlineOffset: focusVars['--focus-outline-offset'],
444
452
  },
445
453
 
446
454
  // Empty state
@@ -1752,8 +1760,8 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
1752
1760
  getActionCellId,
1753
1761
  activeDescendantId,
1754
1762
  onTriggerClick,
1755
- onKeyDown,
1756
- onItemMouseEnter,
1763
+ onKeyDown: onComboboxKeyDown,
1764
+ onItemMouseEnter: onComboboxItemMouseEnter,
1757
1765
  } = useMultiCombobox({
1758
1766
  selectableItems: sortedItems,
1759
1767
  isDisabled: isDisabled || isEffectivelyReadOnly,
@@ -1784,6 +1792,29 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
1784
1792
  ),
1785
1793
  });
1786
1794
 
1795
+ // A hover moves the highlight too, but the focus ring follows the shared
1796
+ // interaction modality (architecture:interaction-modality INV1): it is read
1797
+ // whenever a key or the pointer moves the highlight, so an action cell
1798
+ // highlighted after pointer input keeps the hover treatment and no ring.
1799
+ useInteractionModalityTracking();
1800
+ const [isKeyboardHighlight, setIsKeyboardHighlight] = useState(
1801
+ () => getInteractionModality() === 'keyboard',
1802
+ );
1803
+ const onKeyDown = useCallback(
1804
+ (event: React.KeyboardEvent<HTMLElement>) => {
1805
+ onComboboxKeyDown(event);
1806
+ setIsKeyboardHighlight(getInteractionModality() === 'keyboard');
1807
+ },
1808
+ [onComboboxKeyDown],
1809
+ );
1810
+ const onItemMouseEnter = useCallback(
1811
+ (...args: Parameters<typeof onComboboxItemMouseEnter>) => {
1812
+ onComboboxItemMouseEnter(...args);
1813
+ setIsKeyboardHighlight(getInteractionModality() === 'keyboard');
1814
+ },
1815
+ [onComboboxItemMouseEnter],
1816
+ );
1817
+
1787
1818
  // The trigger button owns its click so a consumer `onClick`, which lands on
1788
1819
  // the button, composes after the built-in toggle. A press elsewhere in the
1789
1820
  // control box (padding, start icon, chevron) still opens the panel.
@@ -2117,6 +2148,7 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
2117
2148
  styles.actionCell,
2118
2149
  isHighlighted &&
2119
2150
  highlightedCell === 'action' &&
2151
+ isKeyboardHighlight &&
2120
2152
  styles.actionCellActive,
2121
2153
  )}>
2122
2154
  {isSelectAll || isCreate ? null : item.action}
@@ -2131,6 +2163,7 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
2131
2163
  [
2132
2164
  isGrid,
2133
2165
  highlightedCell,
2166
+ isKeyboardHighlight,
2134
2167
  getActionCellId,
2135
2168
  renderOption,
2136
2169
  indicatorPosition,