@astryxdesign/core 0.7.1-canary.ff40b78 → 0.7.1

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 (348) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/dist/Button/Button.d.ts +1 -1
  3. package/dist/Button/Button.d.ts.map +1 -1
  4. package/dist/Button/Button.js +5 -2
  5. package/dist/Calendar/Calendar.d.ts.map +1 -1
  6. package/dist/Calendar/Calendar.js +191 -32
  7. package/dist/Chat/ChatComposerInput.d.ts +9 -0
  8. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  9. package/dist/Chat/ChatComposerInput.js +5 -0
  10. package/dist/Chat/ChatMessage.d.ts +11 -0
  11. package/dist/Chat/ChatMessage.d.ts.map +1 -1
  12. package/dist/Chat/ChatMessage.js +18 -9
  13. package/dist/Chat/useTriggerMenu.d.ts +9 -2
  14. package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
  15. package/dist/Chat/useTriggerMenu.js +222 -50
  16. package/dist/CodeBlock/CodeBlock.d.ts +10 -1
  17. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  18. package/dist/CodeBlock/CodeBlock.js +158 -38
  19. package/dist/CodeBlock/tokenizer.d.ts +39 -0
  20. package/dist/CodeBlock/tokenizer.d.ts.map +1 -1
  21. package/dist/CodeBlock/tokenizer.js +108 -0
  22. package/dist/CommandPalette/CommandPalette.d.ts +9 -5
  23. package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
  24. package/dist/CommandPalette/CommandPalette.js +38 -20
  25. package/dist/CommandPalette/CommandPaletteEmpty.d.ts +4 -3
  26. package/dist/CommandPalette/CommandPaletteEmpty.d.ts.map +1 -1
  27. package/dist/CommandPalette/CommandPaletteEmpty.js +4 -3
  28. package/dist/CommandPalette/CommandPaletteInput.d.ts +5 -4
  29. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  30. package/dist/CommandPalette/CommandPaletteInput.js +35 -18
  31. package/dist/CommandPalette/CommandPaletteItem.d.ts +11 -6
  32. package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
  33. package/dist/CommandPalette/CommandPaletteItem.js +8 -9
  34. package/dist/CommandPalette/CommandPaletteList.d.ts +4 -2
  35. package/dist/CommandPalette/CommandPaletteList.d.ts.map +1 -1
  36. package/dist/CommandPalette/CommandPaletteList.js +10 -5
  37. package/dist/ComplexSelector/ComplexSelector.d.ts +8 -5
  38. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  39. package/dist/ComplexSelector/ComplexSelector.js +12 -3
  40. package/dist/ContextMenu/ContextMenu.d.ts +3 -3
  41. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  42. package/dist/ContextMenu/ContextMenu.js +62 -48
  43. package/dist/DateInput/DateInput.js +1 -1
  44. package/dist/DateInput/MonthScroller.d.ts.map +1 -1
  45. package/dist/DateInput/MonthScroller.js +12 -4
  46. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  47. package/dist/DateRangeInput/DateRangeInput.js +12 -2
  48. package/dist/Dialog/Dialog.d.ts +1 -1
  49. package/dist/Dialog/Dialog.d.ts.map +1 -1
  50. package/dist/Dialog/Dialog.js +16 -5
  51. package/dist/Dialog/DialogHeader.d.ts +1 -1
  52. package/dist/Dialog/DialogHeader.d.ts.map +1 -1
  53. package/dist/Dialog/DialogHeader.js +7 -7
  54. package/dist/Divider/Divider.d.ts +1 -0
  55. package/dist/Divider/Divider.d.ts.map +1 -1
  56. package/dist/Divider/Divider.js +5 -4
  57. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  58. package/dist/DropdownMenu/DropdownMenu.js +161 -65
  59. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +7 -5
  60. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
  61. package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +12 -5
  62. package/dist/Field/InputClearButton.d.ts +8 -4
  63. package/dist/Field/InputClearButton.d.ts.map +1 -1
  64. package/dist/Field/InputClearButton.js +23 -7
  65. package/dist/FormLayout/FormLayout.d.ts.map +1 -1
  66. package/dist/FormLayout/FormLayout.js +1 -0
  67. package/dist/Grid/Grid.d.ts +13 -4
  68. package/dist/Grid/Grid.d.ts.map +1 -1
  69. package/dist/Grid/Grid.js +7 -1
  70. package/dist/HoverCard/useHoverCard.d.ts +1 -1
  71. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  72. package/dist/HoverCard/useHoverCard.js +67 -49
  73. package/dist/InputGroup/InputGroup.d.ts +2 -4
  74. package/dist/InputGroup/InputGroup.d.ts.map +1 -1
  75. package/dist/InputGroup/InputGroup.js +2 -4
  76. package/dist/InputGroup/groupStyles.d.ts +10 -2
  77. package/dist/InputGroup/groupStyles.d.ts.map +1 -1
  78. package/dist/InputGroup/groupStyles.js +19 -2
  79. package/dist/Layer/useLayerDismissal.d.ts +9 -9
  80. package/dist/Layer/useLayerDismissal.js +9 -9
  81. package/dist/Markdown/Markdown.d.ts.map +1 -1
  82. package/dist/Markdown/Markdown.js +25 -4
  83. package/dist/MobileNav/MobileNav.d.ts +7 -0
  84. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  85. package/dist/MobileNav/MobileNav.js +45 -2
  86. package/dist/MultiSelector/MultiSelector.d.ts +7 -4
  87. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  88. package/dist/MultiSelector/MultiSelector.js +24 -3
  89. package/dist/NumberInput/NumberInput.js +1 -1
  90. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  91. package/dist/PowerSearch/PowerSearchEditPopover.js +6 -0
  92. package/dist/Resizable/ResizeHandle.d.ts +1 -1
  93. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  94. package/dist/Resizable/ResizeHandle.js +83 -15
  95. package/dist/Resizable/useResizable.d.ts +23 -0
  96. package/dist/Resizable/useResizable.d.ts.map +1 -1
  97. package/dist/Resizable/useResizable.js +78 -0
  98. package/dist/Selector/Selector.d.ts +6 -3
  99. package/dist/Selector/Selector.d.ts.map +1 -1
  100. package/dist/Selector/Selector.js +28 -6
  101. package/dist/SideNav/SideNav.d.ts.map +1 -1
  102. package/dist/SideNav/SideNav.js +10 -2
  103. package/dist/Slider/Slider.d.ts.map +1 -1
  104. package/dist/Slider/Slider.js +18 -7
  105. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  106. package/dist/Tokenizer/Tokenizer.js +16 -4
  107. package/dist/Toolbar/Toolbar.d.ts +2 -1
  108. package/dist/Toolbar/Toolbar.d.ts.map +1 -1
  109. package/dist/Toolbar/Toolbar.js +3 -1
  110. package/dist/TreeList/TreeList.d.ts.map +1 -1
  111. package/dist/TreeList/TreeList.js +6 -3
  112. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  113. package/dist/TreeList/TreeListItem.js +7 -1
  114. package/dist/astryx.css +22 -0
  115. package/dist/hooks/useScrollOverflow.d.ts.map +1 -1
  116. package/dist/hooks/useScrollOverflow.js +55 -12
  117. package/dist/hooks/useTreeFocus.d.ts +21 -0
  118. package/dist/hooks/useTreeFocus.d.ts.map +1 -1
  119. package/dist/hooks/useTreeFocus.js +40 -2
  120. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  121. package/dist/i18n/generated-locales/en.generated.js +3 -0
  122. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  123. package/dist/i18n/generated-locales/pseudo.generated.js +3 -0
  124. package/dist/reset.a11y.chromium.spec.js +6 -1
  125. package/dist/reset.until-found.a11y.browser.spec.d.ts +2 -0
  126. package/dist/reset.until-found.a11y.browser.spec.d.ts.map +1 -0
  127. package/dist/reset.until-found.a11y.browser.spec.js +71 -0
  128. package/dist/theme/syntax/SyntaxTheme.d.ts.map +1 -1
  129. package/dist/theme/syntax/SyntaxTheme.js +43 -2
  130. package/dist/theme/syntax/defineSyntaxTheme.d.ts +18 -5
  131. package/dist/theme/syntax/defineSyntaxTheme.d.ts.map +1 -1
  132. package/dist/theme/syntax/defineSyntaxTheme.js +38 -9
  133. package/dist/theme/syntax/index.d.ts +1 -1
  134. package/dist/theme/syntax/index.d.ts.map +1 -1
  135. package/dist/theme/syntax/tokens.d.ts +12 -2
  136. package/dist/theme/syntax/tokens.d.ts.map +1 -1
  137. package/dist/theme/syntax/tokens.js +25 -4
  138. package/dist/utils/plainDate.d.ts +12 -0
  139. package/dist/utils/plainDate.d.ts.map +1 -1
  140. package/dist/utils/plainDate.js +63 -10
  141. package/locales/en.json +13 -1
  142. package/locales/pseudo.json +9 -0
  143. package/package.json +4 -4
  144. package/src/AlertDialog/AlertDialog.doc.mjs +6 -0
  145. package/src/AlertDialog/AlertDialog.spec.md +205 -0
  146. package/src/AlertDialog/AlertDialog.test.tsx +3 -14
  147. package/src/AlertDialog/__tests__/AlertDialog.a11y.chromium.spec.ts +115 -0
  148. package/src/AlertDialog/__tests__/AlertDialog.a11y.renders.tsx +52 -0
  149. package/src/AlertDialog/__tests__/AlertDialog.a11y.states.ts +68 -0
  150. package/src/AlertDialog/__tests__/AlertDialog.a11y.test.tsx +144 -0
  151. package/src/Button/Button.test.tsx +37 -0
  152. package/src/Button/Button.tsx +5 -1
  153. package/src/Calendar/Calendar.doc.mjs +234 -60
  154. package/src/Calendar/Calendar.spec.md +209 -0
  155. package/src/Calendar/Calendar.test.tsx +559 -20
  156. package/src/Calendar/Calendar.tsx +258 -41
  157. package/src/Calendar/__tests__/Calendar.a11y.chromium.spec.ts +126 -0
  158. package/src/Calendar/__tests__/Calendar.a11y.known-failures.ts +12 -0
  159. package/src/Calendar/__tests__/Calendar.a11y.renders.tsx +57 -0
  160. package/src/Calendar/__tests__/Calendar.a11y.states.ts +139 -0
  161. package/src/Calendar/__tests__/Calendar.a11y.test.tsx +137 -0
  162. package/src/Carousel/Carousel.test.tsx +54 -71
  163. package/src/Carousel/__tests__/Carousel.a11y.chromium.spec.ts +192 -0
  164. package/src/Carousel/__tests__/Carousel.a11y.known-failures.ts +27 -0
  165. package/src/Carousel/__tests__/Carousel.a11y.renders.tsx +39 -0
  166. package/src/Carousel/__tests__/Carousel.a11y.states.ts +139 -0
  167. package/src/Carousel/__tests__/Carousel.a11y.test.tsx +127 -0
  168. package/src/Chat/ChatComposerInput.doc.mjs +1 -1
  169. package/src/Chat/ChatComposerInput.spec.md +94 -55
  170. package/src/Chat/ChatComposerInput.test.tsx +202 -1
  171. package/src/Chat/ChatComposerInput.tsx +13 -0
  172. package/src/Chat/ChatMessage.doc.mjs +31 -16
  173. package/src/Chat/ChatMessage.spec.md +265 -0
  174. package/src/Chat/ChatMessage.test.tsx +246 -0
  175. package/src/Chat/ChatMessage.tsx +30 -7
  176. package/src/Chat/ChatSystemMessage.spec.md +36 -37
  177. package/src/Chat/ChatSystemMessage.test.tsx +4 -4
  178. package/src/Chat/useTriggerMenu.test.tsx +994 -0
  179. package/src/Chat/useTriggerMenu.tsx +278 -46
  180. package/src/CodeBlock/CodeBlock.doc.mjs +7 -0
  181. package/src/CodeBlock/CodeBlock.spec.md +40 -41
  182. package/src/CodeBlock/CodeBlock.test.tsx +89 -9
  183. package/src/CodeBlock/CodeBlock.tsx +205 -25
  184. package/src/CodeBlock/__tests__/CodeBlockOverflowStop.a11y.chromium.spec.ts +125 -0
  185. package/src/CodeBlock/__tests__/DiffRowPaint.a11y.chromium.spec.ts +93 -0
  186. package/src/CodeBlock/tokenizer.test.ts +112 -0
  187. package/src/CodeBlock/tokenizer.ts +106 -0
  188. package/src/Collapsible/CollapsibleGroup.spec.md +17 -8
  189. package/src/Collapsible/__tests__/CollapsibleGroup.a11y.renders.tsx +45 -0
  190. package/src/Collapsible/__tests__/CollapsibleGroup.a11y.states.ts +139 -0
  191. package/src/Collapsible/__tests__/CollapsibleGroup.a11y.test.tsx +135 -0
  192. package/src/Collapsible/__tests__/CollapsibleGroupAccordion.a11y.chromium.spec.ts +128 -0
  193. package/src/CommandPalette/CommandPalette.doc.mjs +126 -23
  194. package/src/CommandPalette/CommandPalette.spec.md +100 -49
  195. package/src/CommandPalette/CommandPalette.test.tsx +241 -2
  196. package/src/CommandPalette/CommandPalette.tsx +47 -21
  197. package/src/CommandPalette/CommandPaletteEmpty.tsx +4 -3
  198. package/src/CommandPalette/CommandPaletteInput.doc.mjs +61 -4
  199. package/src/CommandPalette/CommandPaletteInput.spec.md +215 -0
  200. package/src/CommandPalette/CommandPaletteInput.test.tsx +375 -51
  201. package/src/CommandPalette/CommandPaletteInput.tsx +74 -21
  202. package/src/CommandPalette/CommandPaletteItem.doc.mjs +73 -22
  203. package/src/CommandPalette/CommandPaletteItem.spec.md +223 -0
  204. package/src/CommandPalette/CommandPaletteItem.test.tsx +200 -5
  205. package/src/CommandPalette/CommandPaletteItem.tsx +16 -11
  206. package/src/CommandPalette/CommandPaletteList.doc.mjs +3 -3
  207. package/src/CommandPalette/CommandPaletteList.spec.md +41 -36
  208. package/src/CommandPalette/CommandPaletteList.test.tsx +80 -0
  209. package/src/CommandPalette/CommandPaletteList.tsx +10 -5
  210. package/src/CommandPalette/__tests__/CommandPalette.a11y.chromium.spec.ts +599 -0
  211. package/src/ComplexSelector/ComplexSelector.doc.mjs +10 -8
  212. package/src/ComplexSelector/ComplexSelector.spec.md +14 -9
  213. package/src/ComplexSelector/ComplexSelector.test.tsx +64 -16
  214. package/src/ComplexSelector/ComplexSelector.tsx +37 -8
  215. package/src/ContextMenu/ContextMenu.doc.mjs +6 -0
  216. package/src/ContextMenu/ContextMenu.spec.md +51 -53
  217. package/src/ContextMenu/ContextMenu.test.tsx +132 -1
  218. package/src/ContextMenu/ContextMenu.tsx +78 -48
  219. package/src/DateInput/DateInput.doc.mjs +52 -0
  220. package/src/DateInput/DateInput.spec.md +27 -15
  221. package/src/DateInput/DateInput.test.tsx +38 -0
  222. package/src/DateInput/DateInput.tsx +1 -1
  223. package/src/DateInput/DateInputTouch.test.tsx +22 -0
  224. package/src/DateInput/MonthScroller.tsx +12 -4
  225. package/src/DateRangeInput/DateRangeInput.test.tsx +49 -0
  226. package/src/DateRangeInput/DateRangeInput.tsx +20 -2
  227. package/src/Dialog/Dialog.test.tsx +27 -1
  228. package/src/Dialog/Dialog.tsx +16 -5
  229. package/src/Dialog/DialogHeader.test.tsx +27 -2
  230. package/src/Dialog/DialogHeader.tsx +7 -8
  231. package/src/Dialog/__tests__/Dialog.a11y.chromium.spec.ts +21 -14
  232. package/src/Divider/Divider.doc.mjs +5 -3
  233. package/src/Divider/Divider.spec.md +140 -54
  234. package/src/Divider/Divider.test.tsx +15 -0
  235. package/src/Divider/Divider.tsx +6 -4
  236. package/src/DropdownMenu/DropdownMenu.doc.mjs +2 -2
  237. package/src/DropdownMenu/DropdownMenu.spec.md +121 -45
  238. package/src/DropdownMenu/DropdownMenu.test.tsx +288 -23
  239. package/src/DropdownMenu/DropdownMenu.tsx +212 -81
  240. package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +38 -2
  241. package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +32 -18
  242. package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +146 -1
  243. package/src/DropdownMenu/__tests__/MenuButton.a11y.chromium.spec.ts +136 -0
  244. package/src/DropdownMenu/__tests__/MenuButton.a11y.known-failures.ts +12 -0
  245. package/src/DropdownMenu/__tests__/MenuButton.a11y.renders.tsx +67 -0
  246. package/src/DropdownMenu/__tests__/MenuButton.a11y.states.ts +141 -0
  247. package/src/DropdownMenu/__tests__/MenuButton.a11y.test.tsx +138 -0
  248. package/src/DropdownMenu/__tests__/MenuHeaderFooter.a11y.chromium.spec.ts +40 -5
  249. package/src/Field/InputClearButton.test.tsx +29 -4
  250. package/src/Field/InputClearButton.tsx +26 -8
  251. package/src/FieldStatus/__tests__/StatusMessage.a11y.chromium.spec.ts +21 -14
  252. package/src/FormLayout/FormLayout.tsx +1 -0
  253. package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +1 -1
  254. package/src/Grid/Grid.doc.mjs +14 -11
  255. package/src/Grid/Grid.spec.md +50 -32
  256. package/src/Grid/Grid.tsx +13 -4
  257. package/src/HoverCard/HoverCard.test.tsx +46 -1
  258. package/src/HoverCard/useHoverCard.tsx +68 -48
  259. package/src/InputGroup/InputGroup.doc.mjs +11 -1
  260. package/src/InputGroup/InputGroup.tsx +2 -4
  261. package/src/InputGroup/groupStyles.ts +40 -2
  262. package/src/Layer/useLayerDismissal.ts +9 -9
  263. package/src/Link/__tests__/Link.a11y.chromium.spec.ts +142 -0
  264. package/src/Link/__tests__/Link.a11y.renders.tsx +97 -0
  265. package/src/Link/__tests__/Link.a11y.states.ts +104 -0
  266. package/src/Link/__tests__/Link.a11y.test.tsx +127 -0
  267. package/src/Markdown/Markdown.doc.mjs +1 -1
  268. package/src/Markdown/Markdown.fr23.perf.test.ts +161 -40
  269. package/src/Markdown/Markdown.spec.md +45 -23
  270. package/src/Markdown/Markdown.test.tsx +38 -0
  271. package/src/Markdown/Markdown.tsx +24 -2
  272. package/src/Markdown/plugins/semanticFence.test.tsx +3 -2
  273. package/src/MobileNav/MobileNav.tsx +50 -2
  274. package/src/MobileNav/MobileNavFocus.test.tsx +553 -0
  275. package/src/MoreMenu/MoreMenu.spec.md +18 -7
  276. package/src/MoreMenu/MoreMenu.test.tsx +38 -0
  277. package/src/MultiSelector/MultiSelector.doc.mjs +8 -8
  278. package/src/MultiSelector/MultiSelector.spec.md +1 -1
  279. package/src/MultiSelector/MultiSelector.test.tsx +188 -21
  280. package/src/MultiSelector/MultiSelector.tsx +50 -11
  281. package/src/NumberInput/NumberInput.test.tsx +24 -0
  282. package/src/NumberInput/NumberInput.tsx +1 -1
  283. package/src/NumberInput/__tests__/NumberInput.a11y.chromium.spec.ts +22 -15
  284. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +78 -0
  285. package/src/PowerSearch/PowerSearchEditPopover.tsx +6 -0
  286. package/src/Resizable/Resizable.spec.md +41 -9
  287. package/src/Resizable/ResizeHandle.test.tsx +565 -2
  288. package/src/Resizable/ResizeHandle.tsx +126 -21
  289. package/src/Resizable/__tests__/ResizeHandle.a11y.chromium.spec.ts +119 -0
  290. package/src/Resizable/__tests__/ResizeHandle.a11y.known-failures.ts +39 -0
  291. package/src/Resizable/__tests__/ResizeHandle.a11y.renders.tsx +80 -0
  292. package/src/Resizable/__tests__/ResizeHandle.a11y.states.ts +197 -0
  293. package/src/Resizable/__tests__/ResizeHandle.a11y.test.tsx +123 -0
  294. package/src/Resizable/useResizable.ts +95 -0
  295. package/src/Selector/Selector.doc.mjs +7 -6
  296. package/src/Selector/Selector.spec.md +37 -24
  297. package/src/Selector/Selector.test.tsx +176 -10
  298. package/src/Selector/Selector.tsx +59 -14
  299. package/src/Selector/__tests__/Combobox.a11y.known-failures.ts +27 -0
  300. package/src/Selector/__tests__/Combobox.a11y.states.ts +145 -0
  301. package/src/Selector/__tests__/Selector.combobox.a11y.chromium.spec.ts +108 -0
  302. package/src/Selector/__tests__/Selector.combobox.a11y.test.tsx +126 -0
  303. package/src/SideNav/SideNav.test.tsx +35 -0
  304. package/src/SideNav/SideNav.tsx +16 -2
  305. package/src/Slider/Slider.spec.md +43 -17
  306. package/src/Slider/Slider.test.tsx +134 -0
  307. package/src/Slider/Slider.tsx +29 -6
  308. package/src/Slider/__tests__/Slider.a11y.chromium.spec.ts +112 -0
  309. package/src/Slider/__tests__/Slider.a11y.known-failures.ts +12 -0
  310. package/src/Slider/__tests__/Slider.a11y.renders.tsx +42 -0
  311. package/src/Slider/__tests__/Slider.a11y.states.ts +106 -0
  312. package/src/Slider/__tests__/Slider.a11y.test.tsx +105 -0
  313. package/src/Switch/__tests__/Switch.a11y.chromium.spec.ts +25 -18
  314. package/src/TabList/__tests__/Tabs.a11y.chromium.spec.ts +19 -12
  315. package/src/TextInput/__tests__/TextInput.a11y.chromium.spec.ts +21 -14
  316. package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +18 -11
  317. package/src/Tokenizer/Tokenizer.test.tsx +87 -0
  318. package/src/Tokenizer/Tokenizer.tsx +31 -3
  319. package/src/Toolbar/Toolbar.test.tsx +131 -4
  320. package/src/Toolbar/Toolbar.tsx +3 -1
  321. package/src/TreeList/TreeList.spec.md +33 -8
  322. package/src/TreeList/TreeList.test.tsx +72 -0
  323. package/src/TreeList/TreeList.tsx +10 -6
  324. package/src/TreeList/TreeListItem.tsx +5 -0
  325. package/src/TreeList/__tests__/TreeList.a11y.chromium.spec.ts +125 -0
  326. package/src/TreeList/__tests__/TreeList.a11y.renders.tsx +44 -0
  327. package/src/TreeList/__tests__/TreeList.a11y.states.ts +164 -0
  328. package/src/TreeList/__tests__/TreeList.a11y.test.tsx +138 -0
  329. package/src/TreeList/__tests__/TreeListRowNames.a11y.chromium.spec.ts +82 -0
  330. package/src/hooks/useScrollOverflow.test.tsx +222 -0
  331. package/src/hooks/useScrollOverflow.ts +55 -13
  332. package/src/hooks/useTreeFocus.test.tsx +37 -0
  333. package/src/hooks/useTreeFocus.ts +55 -2
  334. package/src/i18n/generated-locales/en.generated.ts +3 -0
  335. package/src/i18n/generated-locales/pseudo.generated.ts +3 -0
  336. package/src/reset.a11y.chromium.spec.ts +9 -1
  337. package/src/reset.css +8 -3
  338. package/src/reset.until-found.a11y.browser.spec.ts +75 -0
  339. package/src/reset.until-found.test.ts +32 -0
  340. package/src/stylexValidation.source-build.test.mjs +42 -0
  341. package/src/theme/syntax/SyntaxTheme.tsx +46 -5
  342. package/src/theme/syntax/defineSyntaxTheme.diff.test.ts +91 -0
  343. package/src/theme/syntax/defineSyntaxTheme.ts +75 -15
  344. package/src/theme/syntax/index.ts +5 -1
  345. package/src/theme/syntax/tokens.ts +32 -2
  346. package/src/theme/syntax/useSyntaxTheme.diff.test.tsx +40 -0
  347. package/src/utils/plainDate.test.ts +147 -0
  348. package/src/utils/plainDate.ts +75 -10
@@ -114,6 +114,53 @@ const MENU_TRIGGER_OPEN_BACKGROUND = `linear-gradient(${colorVars['--color-overl
114
114
  * aria-hidden subtrees) are not reachable, judged by the popover focus trap's
115
115
  * own test so the two never disagree about where Tab can go.
116
116
  */
117
+ /**
118
+ * Collapse each named radio group to the one Tab stop a browser gives it: the
119
+ * focused radio, else the checked one, else the first radio moving forward or
120
+ * the last moving backward. The stop takes the group's first position.
121
+ */
122
+ function collapseRadioGroups(
123
+ elements: HTMLElement[],
124
+ active: Element | null,
125
+ isBackward: boolean,
126
+ ): HTMLElement[] {
127
+ const groups = new Map<string, HTMLInputElement[]>();
128
+ const groupKey = (el: HTMLElement): string | null =>
129
+ el instanceof HTMLInputElement &&
130
+ el.getAttribute('type') === 'radio' &&
131
+ el.name !== ''
132
+ ? `${el.form?.id ?? ''}\u0000${el.name}`
133
+ : null;
134
+ for (const el of elements) {
135
+ const key = groupKey(el);
136
+ if (key != null) {
137
+ const members = groups.get(key) ?? [];
138
+ members.push(el as HTMLInputElement);
139
+ groups.set(key, members);
140
+ }
141
+ }
142
+ const placed = new Set<string>();
143
+ const result: HTMLElement[] = [];
144
+ for (const el of elements) {
145
+ const key = groupKey(el);
146
+ if (key == null) {
147
+ result.push(el);
148
+ continue;
149
+ }
150
+ if (placed.has(key)) {
151
+ continue;
152
+ }
153
+ placed.add(key);
154
+ const members = groups.get(key) ?? [];
155
+ const stop =
156
+ members.find(radio => radio === active) ??
157
+ members.find(radio => radio.checked) ??
158
+ (isBackward ? members[members.length - 1] : members[0]);
159
+ result.push(stop);
160
+ }
161
+ return result;
162
+ }
163
+
117
164
  function tabbablesIn(region: HTMLElement | null): HTMLElement[] {
118
165
  if (region == null) {
119
166
  return [];
@@ -148,8 +195,9 @@ function isInsideHiddenWrapper(
148
195
  }
149
196
 
150
197
  /**
151
- * Move focus to a region control. Reports whether focus actually arrived, so a
152
- * caller can close the menu instead of stranding focus on the page.
198
+ * Move focus to a Tab stop in the dialog. Reports whether focus actually
199
+ * arrived, so a caller can fall back to the menu instead of stranding focus on
200
+ * the page.
153
201
  */
154
202
  function focusRegionControl(element: HTMLElement): boolean {
155
203
  element.focus();
@@ -551,6 +599,17 @@ function visibleText(element: Element): string {
551
599
  for (const hidden of copy.querySelectorAll('[aria-hidden="true"]')) {
552
600
  hidden.remove();
553
601
  }
602
+ // An icon-only control is named by its image's alt, an inner aria-label, or
603
+ // an svg title rather than by text, so those count as its contents too.
604
+ for (const named of copy.querySelectorAll('img[alt], [aria-label]')) {
605
+ const name =
606
+ named.getAttribute(named.tagName === 'IMG' ? 'alt' : 'aria-label') ?? '';
607
+ named.replaceWith(copy.ownerDocument.createTextNode(` ${name} `));
608
+ }
609
+ for (const svg of copy.querySelectorAll('svg')) {
610
+ const title = svg.querySelector('title')?.textContent ?? '';
611
+ svg.replaceWith(copy.ownerDocument.createTextNode(` ${title} `));
612
+ }
554
613
  return (copy.textContent ?? '').replace(/\s+/g, ' ').trim();
555
614
  }
556
615
 
@@ -580,6 +639,24 @@ function readAccessibleName(element: HTMLElement | null): string | undefined {
580
639
  if (label) {
581
640
  return label;
582
641
  }
642
+ // A form control is named by its own <label>s; an input button by its value.
643
+ const labels =
644
+ 'labels' in element
645
+ ? Array.from((element as HTMLInputElement).labels ?? [])
646
+ : [];
647
+ const labelText = labels.map(visibleText).join(' ').trim();
648
+ if (labelText !== '') {
649
+ return labelText;
650
+ }
651
+ if (
652
+ element instanceof HTMLInputElement &&
653
+ ['button', 'submit', 'reset'].includes(element.getAttribute('type') ?? '')
654
+ ) {
655
+ const value = element.value.trim();
656
+ if (value !== '') {
657
+ return value;
658
+ }
659
+ }
583
660
  const text = visibleText(element);
584
661
  if (text !== '') {
585
662
  return text;
@@ -716,7 +793,15 @@ function DropdownMenuBottomSheet({
716
793
  openModalityRef.current = 'keyboard';
717
794
  }
718
795
  };
719
- const handleSheetTriggerClick = () => {
796
+ const handleSheetTriggerClick = (event: React.MouseEvent<HTMLElement>) => {
797
+ // A caller-rendered trigger that is natively or aria-disabled must not
798
+ // open the sheet; the default Button already refuses its own click.
799
+ // The DOM check covers the built-in Button too (it renders aria-disabled
800
+ // when focusable-disabled); a caller's own enabled control wins over a
801
+ // `button.isDisabled` left on the props.
802
+ if (event.currentTarget.matches(':disabled, [aria-disabled="true"]')) {
803
+ return;
804
+ }
720
805
  onClick?.();
721
806
  setOpen(!isOpen);
722
807
  };
@@ -954,6 +1039,16 @@ function DropdownMenuPopover({
954
1039
  // ArrowUp opens with the LAST enabled row highlighted.
955
1040
  const focusLastOnOpenRef = useRef(false);
956
1041
 
1042
+ // Synchronizes the dialog's name from the custom trigger's DOM, an external
1043
+ // system React does not own: the rendered control's accessible name is only
1044
+ // readable once it mounts. With a header or footer the dialog is named like
1045
+ // the menu, so that name is needed before the first open, not only at it.
1046
+ useIsomorphicLayoutEffect(() => {
1047
+ if (renderTrigger != null && hasRegions && triggerName == null) {
1048
+ setTriggerName(readAccessibleName(buttonRef.current));
1049
+ }
1050
+ }, [renderTrigger, hasRegions, triggerName]);
1051
+
957
1052
  const handleLayerShow = useCallback(() => {
958
1053
  acceptedOpenRef.current = true;
959
1054
  resetFocusReturn();
@@ -970,15 +1065,38 @@ function DropdownMenuPopover({
970
1065
  }
971
1066
  }, [isControlled, onClick, onOpenChange, renderTrigger, resetFocusReturn]);
972
1067
 
1068
+ // The menu's name: the dialog around it with a header or footer, and the
1069
+ // Back row of a drilled-in view ("Back to <this>"), both read it.
1070
+ // The design system's button names the menu by its label. A custom trigger
1071
+ // names it by reference (or the caller's `aria-label` does), so that name
1072
+ // is read off the control as the menu opens.
1073
+ const callerMenuLabel = rest['aria-label'];
1074
+ const menuLabel =
1075
+ renderTrigger != null
1076
+ ? typeof callerMenuLabel === 'string' && callerMenuLabel.trim() !== ''
1077
+ ? callerMenuLabel
1078
+ : triggerName
1079
+ : typeof button.label === 'string'
1080
+ ? button.label
1081
+ : undefined;
1082
+
973
1083
  const popover = usePopover({
974
1084
  onHide: handleLayerHide,
975
1085
  onShow: handleLayerShow,
976
1086
  hasLightDismiss: true,
977
1087
  hasCloseButton: false,
978
1088
  hasAutoFocus: false,
979
- // The popup's own role="menu" is the exposed semantics; wrapping it in a
980
- // modal dialog would announce an unnamed dialog around the menu.
981
- role: 'none',
1089
+ // Without a header or footer the popup's own role="menu" is the exposed
1090
+ // semantics; a dialog around it would announce an extra container. With
1091
+ // either, the popover holds controls that are not menu items, so it is a
1092
+ // modal dialog named like the menu, with the menu inside it. A custom
1093
+ // trigger's name is read off the control once it mounts; until then, or
1094
+ // when the control exposes no readable name, the dialog takes the default
1095
+ // menu label, so it is never unnamed and never loses its role.
1096
+ role: hasRegions ? 'dialog' : 'none',
1097
+ dialogLabel: hasRegions
1098
+ ? (menuLabel ?? t(DEFAULT_BUTTON_I18N_KEY))
1099
+ : undefined,
982
1100
  });
983
1101
 
984
1102
  const closeMenu = useCallback(() => {
@@ -1077,6 +1195,55 @@ function DropdownMenuPopover({
1077
1195
  });
1078
1196
  }, [popover.isOpen, focusFirst, focusLast, listRef]);
1079
1197
 
1198
+ // With a header or footer the popover is a modal dialog: Tab and Shift+Tab
1199
+ // walk the header's controls, the menu as one stop, then the footer's
1200
+ // controls, and wrap at either end. The key is handled here in full, so the
1201
+ // popover's own focus trap, which listens on the document, never reads it a
1202
+ // second time.
1203
+ const moveDialogTab = useCallback(
1204
+ (e: React.KeyboardEvent, isFromFlyout = false) => {
1205
+ const menu = listRef.current;
1206
+ if (menu == null) {
1207
+ return;
1208
+ }
1209
+ e.preventDefault();
1210
+ e.stopPropagation();
1211
+ // A named radio group is one stop, as the browser's own Tab order has it.
1212
+ const cycle = collapseRadioGroups(
1213
+ [
1214
+ ...tabbablesIn(headerRef.current),
1215
+ menu,
1216
+ ...tabbablesIn(footerRef.current),
1217
+ ],
1218
+ document.activeElement,
1219
+ e.shiftKey,
1220
+ );
1221
+ if (cycle.length === 1) {
1222
+ // Nothing else to reach: focus stays in the menu. From a flyout that
1223
+ // has just closed itself, that is the menu's own stop.
1224
+ if (isFromFlyout) {
1225
+ menu.focus();
1226
+ }
1227
+ return;
1228
+ }
1229
+ const active = document.activeElement;
1230
+ const menuIndex = cycle.indexOf(menu);
1231
+ // A row, or a row of an open flyout, counts as the menu's stop.
1232
+ let index = menu.contains(active)
1233
+ ? menuIndex
1234
+ : cycle.findIndex(el => el === active || el.contains(active));
1235
+ if (index < 0) {
1236
+ index = menuIndex;
1237
+ }
1238
+ const step = e.shiftKey ? -1 : 1;
1239
+ const next = cycle[(index + step + cycle.length) % cycle.length];
1240
+ if (!focusRegionControl(next)) {
1241
+ menu.focus();
1242
+ }
1243
+ },
1244
+ [listRef],
1245
+ );
1246
+
1080
1247
  // Extend useListFocus with Enter/Space activation + typeahead
1081
1248
  const listKeyDown = useCallback(
1082
1249
  (e: React.KeyboardEvent) => {
@@ -1087,18 +1254,7 @@ function DropdownMenuPopover({
1087
1254
  // itself as the key bubbled up, and the root moves focus on as Tab
1088
1255
  // from one of its own rows would.
1089
1256
  if (e.key === 'Tab' && hasRegions) {
1090
- const region = e.shiftKey ? headerRef.current : footerRef.current;
1091
- const tabbables = tabbablesIn(region);
1092
- e.stopPropagation();
1093
- const target = e.shiftKey
1094
- ? tabbables[tabbables.length - 1]
1095
- : tabbables[0];
1096
- if (target != null) {
1097
- e.preventDefault();
1098
- }
1099
- if (target == null || !focusRegionControl(target)) {
1100
- closeMenu();
1101
- }
1257
+ moveDialogTab(e, true);
1102
1258
  }
1103
1259
  return;
1104
1260
  }
@@ -1120,30 +1276,15 @@ function DropdownMenuPopover({
1120
1276
  }
1121
1277
  return;
1122
1278
  }
1123
- // APG menu-button pattern: Tab closes the menu. Menu items are
1124
- // tabIndex={-1} so the focus trap has nothing trappable and Tab would
1125
- // otherwise leak into the page while the menu stayed open (menus-5).
1126
- // Do NOT preventDefault — closing restores focus to the trigger, and the
1127
- // browser's default Tab then continues from there to the next element.
1128
- // A header or footer with something Tab can reach in that direction
1129
- // takes the Tab instead, and the menu stays open.
1279
+ // With a header or footer the popover is a dialog and Tab wraps inside
1280
+ // it. Without either, the APG menu-button pattern: Tab closes the menu.
1281
+ // Menu items are tabIndex={-1} so the focus trap has nothing trappable
1282
+ // and Tab would otherwise leak into the page while the menu stayed open
1283
+ // (menus-5). Do NOT preventDefault — closing restores focus to the
1284
+ // trigger, and the browser's default Tab then continues from there.
1130
1285
  if (e.key === 'Tab') {
1131
- const region = e.shiftKey ? headerRef.current : footerRef.current;
1132
- const tabbables = tabbablesIn(region);
1133
- if (tabbables.length > 0) {
1134
- // From a row, the browser's Shift+Tab would stop on the menu element
1135
- // itself — the row's ancestor and a Tab stop — before reaching the
1136
- // header. The menu is one stop, so go straight to the header.
1137
- if (e.shiftKey && e.target !== listRef.current) {
1138
- e.preventDefault();
1139
- // The popover's focus trap listens on the document and would read
1140
- // this same Shift+Tab against the field just focused — the trap's
1141
- // first element — and wrap to the footer. This key is handled.
1142
- e.stopPropagation();
1143
- if (!focusRegionControl(tabbables[tabbables.length - 1])) {
1144
- closeMenu();
1145
- }
1146
- }
1286
+ if (hasRegions) {
1287
+ moveDialogTab(e);
1147
1288
  return;
1148
1289
  }
1149
1290
  closeMenu();
@@ -1156,33 +1297,24 @@ function DropdownMenuPopover({
1156
1297
  }
1157
1298
  listNavKeyDown(e);
1158
1299
  },
1159
- [listNavKeyDown, closeMenu, typeahead, ownsEvent, listRef, hasRegions],
1300
+ [
1301
+ listNavKeyDown,
1302
+ closeMenu,
1303
+ typeahead,
1304
+ ownsEvent,
1305
+ hasRegions,
1306
+ moveDialogTab,
1307
+ ],
1160
1308
  );
1161
1309
 
1162
- // Tab past the last control of the footer, or Shift+Tab before the first
1163
- // control of the header, leaves the menu as Tab from its rows does: the
1164
- // menu closes, focus returns to the trigger, and the browser's own Tab
1165
- // continues from there. Between the regions, Tab moves through the menu
1166
- // as one stop.
1310
+ // Tab from a header or footer control moves through the same dialog cycle.
1167
1311
  const handleRegionKeyDown = useCallback(
1168
1312
  (e: React.KeyboardEvent<HTMLDivElement>) => {
1169
- if (e.key !== 'Tab') {
1170
- return;
1171
- }
1172
- const tabbables = tabbablesIn(e.currentTarget);
1173
- const isHeader = e.currentTarget === headerRef.current;
1174
- const exit = isHeader
1175
- ? e.shiftKey
1176
- ? tabbables[0]
1177
- : undefined
1178
- : e.shiftKey
1179
- ? undefined
1180
- : tabbables[tabbables.length - 1];
1181
- if (exit != null && exit === document.activeElement) {
1182
- closeMenu();
1313
+ if (e.key === 'Tab') {
1314
+ moveDialogTab(e);
1183
1315
  }
1184
1316
  },
1185
- [closeMenu],
1317
+ [moveDialogTab],
1186
1318
  );
1187
1319
 
1188
1320
  const openAndFocus = useCallback(
@@ -1190,6 +1322,15 @@ function DropdownMenuPopover({
1190
1322
  modality: 'keyboard' | 'pointer' = 'keyboard',
1191
1323
  notifyClick = false,
1192
1324
  ): boolean => {
1325
+ // An unavailable trigger never opens the menu. Button keeps an
1326
+ // aria-disabled trigger focusable for its tooltip and swallows only
1327
+ // Enter and Space, so the arrow keys, a press and a click would still
1328
+ // arrive here; a custom trigger may be natively disabled instead.
1329
+ if (
1330
+ buttonRef.current?.matches(':disabled, [aria-disabled="true"]') === true
1331
+ ) {
1332
+ return false;
1333
+ }
1193
1334
  acceptedOpenRef.current = false;
1194
1335
  notifyClickOnShowRef.current = notifyClick;
1195
1336
  pendingControlledOpenRef.current = isControlled;
@@ -1328,7 +1469,11 @@ function DropdownMenuPopover({
1328
1469
  return;
1329
1470
  }
1330
1471
  focusLastOnOpenRef.current = e.key === 'ArrowUp';
1331
- openAndFocus();
1472
+ if (!openAndFocus()) {
1473
+ // A refused open (disabled trigger) must not leave ArrowUp's
1474
+ // "land on the last row" armed for a later open.
1475
+ focusLastOnOpenRef.current = false;
1476
+ }
1332
1477
  }
1333
1478
  return;
1334
1479
  }
@@ -1368,20 +1513,6 @@ function DropdownMenuPopover({
1368
1513
 
1369
1514
  // The drill-in view stack for sub-menus on a phone.
1370
1515
  const {drillIn, wrapContent} = useMenuDrillIn(isOpen);
1371
- // The name a drilled-in view's Back row returns to: the menu's own name.
1372
- // The design system's button names the menu by its label. A custom trigger
1373
- // names it by reference (or the caller's `aria-label` does), so that name
1374
- // is read off the control as the menu opens.
1375
- const callerMenuLabel = rest['aria-label'];
1376
- const menuLabel =
1377
- renderTrigger != null
1378
- ? typeof callerMenuLabel === 'string' && callerMenuLabel.trim() !== ''
1379
- ? callerMenuLabel
1380
- : triggerName
1381
- : typeof button.label === 'string'
1382
- ? button.label
1383
- : undefined;
1384
-
1385
1516
  const resolvedMaxHeight = menuMaxHeight == null ? null : `${menuMaxHeight}px`;
1386
1517
  // Context for compound items
1387
1518
  const contextValue = useMemo<DropdownMenuContextValue>(
@@ -1442,9 +1573,9 @@ function DropdownMenuPopover({
1442
1573
  // keeps the browser from drawing one after pointer input (see
1443
1574
  // handleLayerHide), so its focus needs no bookkeeping here.
1444
1575
  onFocus: () => {},
1445
- 'aria-haspopup': 'menu',
1576
+ 'aria-haspopup': hasRegions ? 'dialog' : 'menu',
1446
1577
  'aria-expanded': isOpen,
1447
- 'aria-controls': menuId,
1578
+ 'aria-controls': hasRegions ? popover.id : menuId,
1448
1579
  };
1449
1580
 
1450
1581
  return (
@@ -1492,9 +1623,9 @@ function DropdownMenuPopover({
1492
1623
  button.onFocus?.(event);
1493
1624
  onFocusReturnTargetFocus();
1494
1625
  }}
1495
- aria-haspopup="menu"
1626
+ aria-haspopup={hasRegions ? 'dialog' : 'menu'}
1496
1627
  aria-expanded={isOpen}
1497
- aria-controls={menuId}
1628
+ aria-controls={hasRegions ? popover.id : menuId}
1498
1629
  data-testid={testId}
1499
1630
  />
1500
1631
  )}
@@ -1,5 +1,27 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
+ /** @type {import('@astryxdesign/cli/authoring').ComponentAnatomyElement[]} */
4
+ const anatomy = [
5
+ {
6
+ name: 'Checkable row',
7
+ required: true,
8
+ description:
9
+ 'The menuitemcheckbox root that owns the accessible name, checked state, focus, and activation.',
10
+ },
11
+ {
12
+ name: 'Label/content',
13
+ required: true,
14
+ description:
15
+ 'Caller content laid out by Item: the required label plus optional description, icon, and end content.',
16
+ },
17
+ {
18
+ name: 'Checkbox marker',
19
+ required: true,
20
+ description:
21
+ 'Decorative shared Indicator chrome that visualizes checked and disabled state without adding a second control.',
22
+ },
23
+ ];
24
+
3
25
  /** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
4
26
 
5
27
  export const docs = {
@@ -9,6 +31,11 @@ export const docs = {
9
31
  isHiddenFromOverview: true,
10
32
  description:
11
33
  'A checkable menu item (role="menuitemcheckbox") that toggles an independent boolean. Use for on/off options inside a DropdownMenu; for a one-of-N choice use DropdownMenuRadioGroup.',
34
+ usage: {
35
+ anatomy,
36
+ description:
37
+ 'An independently checkable option inside DropdownMenu. The row owns menuitemcheckbox semantics while the shared Indicator supplies decorative checkbox paint.',
38
+ },
12
39
  props: [
13
40
  {
14
41
  name: 'label',
@@ -43,7 +70,7 @@ export const docs = {
43
70
  type: 'boolean',
44
71
  default: 'false',
45
72
  description:
46
- 'Whether the item is disabled. Disabled items stay focusable (via aria-disabled) so they remain discoverable by keyboard and assistive technology, but activation is blocked.',
73
+ 'Whether the item is disabled. Disabled items expose aria-disabled, block activation, and are currently skipped by DropdownMenu arrow-key navigation.',
47
74
  },
48
75
  {
49
76
  name: 'hasCloseOnSelect',
@@ -65,6 +92,14 @@ export const docs = {
65
92
  'Content to render after the label and description, such as a keyboard shortcut hint or badge.',
66
93
  },
67
94
  ],
95
+ theming: {
96
+ targets: [
97
+ {
98
+ className: 'astryx-dropdown-menu-item',
99
+ visualProps: ['size'],
100
+ },
101
+ ],
102
+ },
68
103
  };
69
104
 
70
105
  export const docsZh = {
@@ -98,7 +133,8 @@ export const docsDense = {
98
133
  icon: 'icon before label',
99
134
  value: 'checked state (controlled, w/ onChange)',
100
135
  onChange: 'fired w/ next checked state on toggle',
101
- isDisabled: 'disabled; stays focusable via aria-disabled',
136
+ isDisabled:
137
+ 'disabled; aria-disabled, no activation; current menu focus skips the row',
102
138
  hasCloseOnSelect: 'close menu on toggle (default false, stays open)',
103
139
  ref: 'forwarded to the row root (the role="menuitemcheckbox" element)',
104
140
  endContent: 'content after label+description',
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @file DropdownMenuCheckboxItem.tsx
7
- * @input React, stylex, Item + CheckboxInput + DropdownMenu context + tokens from core
7
+ * @input React, stylex, Item + Indicator registry + DropdownMenu context + tokens from core
8
8
  * @output DropdownMenuCheckboxItem — a standalone checkable menu item.
9
9
  * @position Sub-component; used inside DropdownMenu.
10
10
  *
@@ -26,7 +26,12 @@
26
26
  * mobile.
27
27
  */
28
28
 
29
- import {useCallback, type PointerEvent, type ReactNode} from 'react';
29
+ import {
30
+ useCallback,
31
+ type MouseEvent,
32
+ type PointerEvent,
33
+ type ReactNode,
34
+ } from 'react';
30
35
  import * as stylex from '@stylexjs/stylex';
31
36
  import {renderIconSlot, type IconType} from '../Icon';
32
37
  import {useIndicator} from '../Indicator';
@@ -34,7 +39,7 @@ import {Item} from '../Item';
34
39
  import {useDropdownMenuContext} from './DropdownMenuContext';
35
40
  import {focusMenuItemOnHover} from './menuItemHover';
36
41
  import {colorVars, spacingVars} from '../theme/tokens.stylex';
37
- import {mergeProps, themeProps} from '../utils';
42
+ import {composeEventHandlers, mergeProps, themeProps} from '../utils';
38
43
  import type {BaseProps} from '../BaseProps';
39
44
 
40
45
  const styles = stylex.create({
@@ -58,8 +63,8 @@ const styles = stylex.create({
58
63
  },
59
64
  // Rendered in Item's `marker` slot as a raw flex child, so `order` moves it
60
65
  // relative to the label within the row. On touch it moves to the inline-end.
61
- // `aria-hidden` + `inert` + pointer-events:none keep the composed CheckboxInput
62
- // decorative: it adds no accessible name, and clicks fall through to the row,
66
+ // `aria-hidden` + `inert` + pointer-events:none keep the composed checkbox
67
+ // indicator decorative: it adds no accessible name, and clicks fall through to the row,
63
68
  // which owns the role and activation.
64
69
  // Placement of the marker within the row. The indicator draws its own box
65
70
  // (size, fill, border) — these are only the rules the MENU owns: where the
@@ -105,9 +110,8 @@ export interface DropdownMenuCheckboxItemProps extends Omit<
105
110
  */
106
111
  onChange?: (checked: boolean) => void;
107
112
  /**
108
- * Whether the item is disabled. Disabled items stay focusable (via
109
- * `aria-disabled`) so they remain discoverable by keyboard and assistive
110
- * technology, but activation is blocked.
113
+ * Whether the item is disabled. Disabled items expose `aria-disabled`, block
114
+ * activation, and are currently skipped by DropdownMenu arrow-key navigation.
111
115
  * @default false
112
116
  */
113
117
  isDisabled?: boolean;
@@ -132,6 +136,9 @@ export interface DropdownMenuCheckboxItemProps extends Omit<
132
136
  *
133
137
  * Must be used inside a DropdownMenu. Toggles an independent boolean; for a
134
138
  * one-of-N choice use DropdownMenuRadioGroup + DropdownMenuRadioItem instead.
139
+ * Consumer click and pointer-move handlers compose after the row's own
140
+ * activation and hover-focus behavior; disabled activation stops before the
141
+ * consumer click handler.
135
142
  *
136
143
  * @example
137
144
  * ```
@@ -157,6 +164,8 @@ export function DropdownMenuCheckboxItem({
157
164
  xstyle,
158
165
  className,
159
166
  style,
167
+ onClick,
168
+ onPointerMove,
160
169
  ...rest
161
170
  }: DropdownMenuCheckboxItemProps) {
162
171
  const ctx = useDropdownMenuContext();
@@ -164,15 +173,20 @@ export function DropdownMenuCheckboxItem({
164
173
  const controlSize = menuSize === 'sm' ? 'sm' : 'md';
165
174
  const CheckboxControl = useIndicator('checkbox');
166
175
 
167
- const handleClick = useCallback(() => {
168
- if (isDisabled) {
169
- return;
170
- }
171
- onChange?.(!value);
172
- if (hasCloseOnSelect) {
173
- ctx?.closeMenu();
174
- }
175
- }, [isDisabled, onChange, value, hasCloseOnSelect, ctx]);
176
+ const handleClick = useCallback(
177
+ (event: MouseEvent) => {
178
+ if (isDisabled) {
179
+ event.preventDefault();
180
+ return;
181
+ }
182
+ onChange?.(!value);
183
+ if (hasCloseOnSelect) {
184
+ ctx?.closeMenu();
185
+ }
186
+ onClick?.(event as MouseEvent<HTMLElement>);
187
+ },
188
+ [isDisabled, onChange, value, hasCloseOnSelect, ctx, onClick],
189
+ );
176
190
 
177
191
  const handlePointerMove = useCallback(
178
192
  (e: PointerEvent<HTMLElement>) => focusMenuItemOnHover(e, isDisabled),
@@ -185,7 +199,7 @@ export function DropdownMenuCheckboxItem({
185
199
  role="menuitemcheckbox"
186
200
  aria-checked={value}
187
201
  tabIndex={isDisabled ? undefined : -1}
188
- onPointerMove={handlePointerMove}
202
+ onPointerMove={composeEventHandlers(handlePointerMove, onPointerMove)}
189
203
  marker={
190
204
  // No wrapper, and no menu-specific theme target: the shared
191
205
  // `astryx-checkbox-indicator` target is already on this element (main