@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
@@ -262,6 +262,51 @@ describe('ComplexSelector', () => {
262
262
  ).toHaveLength(2);
263
263
  });
264
264
 
265
+ it('applies distinct trigger chrome to the secondary variant', () => {
266
+ // data-testid reaches the native control; the variant is on the trigger root.
267
+ const triggerRoot = () =>
268
+ screen
269
+ .getByTestId('view-options')
270
+ .closest('.astryx-complex-selector') as HTMLElement;
271
+ const {rerender} = render(
272
+ <ComplexSelector
273
+ label="View options"
274
+ value={['name']}
275
+ variant="input"
276
+ data-testid="view-options">
277
+ {() => <div>Columns</div>}
278
+ </ComplexSelector>,
279
+ );
280
+ const inputClasses = triggerRoot().className;
281
+
282
+ rerender(
283
+ <ComplexSelector
284
+ label="View options"
285
+ value={['name']}
286
+ variant="secondary"
287
+ data-testid="view-options">
288
+ {() => <div>Columns</div>}
289
+ </ComplexSelector>,
290
+ );
291
+ const secondary = triggerRoot();
292
+ const secondaryClasses = secondary.className;
293
+ expect(secondary).toHaveAttribute('data-variant', 'secondary');
294
+
295
+ rerender(
296
+ <ComplexSelector
297
+ label="View options"
298
+ value={['name']}
299
+ variant="ghost"
300
+ data-testid="view-options">
301
+ {() => <div>Columns</div>}
302
+ </ComplexSelector>,
303
+ );
304
+ const ghostClasses = triggerRoot().className;
305
+
306
+ expect(secondaryClasses).not.toBe(inputClasses);
307
+ expect(secondaryClasses).not.toBe(ghostClasses);
308
+ });
309
+
265
310
  it('supports end-aligned popup positioning', () => {
266
311
  render(
267
312
  <ComplexSelector label="View options" value={[]} alignment="end">
@@ -721,22 +766,25 @@ describe('ComplexSelector onOpenChange', () => {
721
766
  );
722
767
  });
723
768
 
724
- it('forces detached when variant="ghost" even if statusVariant="attached"', () => {
725
- const {container} = render(
726
- <ComplexSelector
727
- label="Filter"
728
- value={[]}
729
- variant="ghost"
730
- status={{type: 'error', message: 'Required'}}
731
- statusVariant="attached">
732
- {() => <button type="button">Apply</button>}
733
- </ComplexSelector>,
734
- );
735
- expect(container.querySelector('.astryx-field-status')).toHaveAttribute(
736
- 'data-variant',
737
- 'detached',
738
- );
739
- });
769
+ it.each(['secondary', 'ghost'] as const)(
770
+ 'forces detached when variant="%s" even if statusVariant="attached"',
771
+ variant => {
772
+ const {container} = render(
773
+ <ComplexSelector
774
+ label="Filter"
775
+ value={[]}
776
+ variant={variant}
777
+ status={{type: 'error', message: 'Required'}}
778
+ statusVariant="attached">
779
+ {() => <button type="button">Apply</button>}
780
+ </ComplexSelector>,
781
+ );
782
+ expect(container.querySelector('.astryx-field-status')).toHaveAttribute(
783
+ 'data-variant',
784
+ 'detached',
785
+ );
786
+ },
787
+ );
740
788
 
741
789
  it('renders no message box for statusVariant="tooltip"', () => {
742
790
  const {container} = render(
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * @file ComplexSelector.tsx
7
7
  * @input Uses React, StyleX, Field, Icon slots, Layer positioning, and usePopover
8
- * @output Exports a rich-selector shell with exact token-sized input and ghost triggers, plus an imperative open/close handle
8
+ * @output Exports a rich-selector shell with exact token-sized input, secondary, and ghost triggers, plus an imperative open/close handle
9
9
  * @position Core implementation; consumed by index.ts
10
10
  *
11
11
  * SYNC: When modified, update:
@@ -151,6 +151,31 @@ const styles = stylex.create({
151
151
  '[data-astryx-press="on"]': 'scale(0.98)',
152
152
  },
153
153
  },
154
+ // Keep the input-family width contract while reusing ghost's borderless paint
155
+ // and the neutral resting fill used by secondary buttons.
156
+ triggerSecondary: {
157
+ borderWidth: 0,
158
+ backgroundColor: colorVars['--color-neutral'],
159
+ boxShadow: {
160
+ default: 'none',
161
+ ':hover:not(:focus-within):where(:not(:disabled,[aria-disabled="true"]))':
162
+ {
163
+ '@media (hover: hover)': 'none',
164
+ },
165
+ ':focus-within': 'none',
166
+ },
167
+ fontWeight: fontWeightVars['--font-weight-medium'],
168
+ transitionProperty:
169
+ 'background-image, background-color, color, opacity, transform',
170
+ transform: {
171
+ default: 'scale(1)',
172
+ ':active': {
173
+ default: 'scale(0.98)',
174
+ '@media (pointer: coarse)': 'scale(1)',
175
+ },
176
+ '[data-astryx-press="on"]': 'scale(0.98)',
177
+ },
178
+ },
154
179
  triggerGhostDisabled: {
155
180
  backgroundImage: 'none',
156
181
  transform: {
@@ -202,7 +227,7 @@ const styles = stylex.create({
202
227
  },
203
228
  });
204
229
 
205
- export type ComplexSelectorVariant = 'input' | 'ghost';
230
+ export type ComplexSelectorVariant = 'input' | 'secondary' | 'ghost';
206
231
 
207
232
  export type ComplexSelectorSize = 'sm' | 'md' | 'lg';
208
233
 
@@ -338,17 +363,20 @@ export interface ComplexSelectorProps<Value> extends Omit<
338
363
  status?: ComplexSelectorStatus;
339
364
  /**
340
365
  * How the status message is placed relative to the input.
341
- * - 'attached': message overlaps directly below the input (bordered treatment; detached when variant is ghost)
366
+ * - 'attached': message overlaps directly below the input (bordered treatment; detached when variant is secondary or ghost)
342
367
  * - 'detached': message floats below as a separate element with spacing
343
368
  * - 'tooltip': no message box; the status icon becomes a focusable info-tip button that reveals the message on hover, keyboard focus, or tap
344
- * @default 'attached'
369
+ * @default 'attached' for input selectors; 'detached' for secondary and ghost selectors
345
370
  */
346
371
  statusVariant?: FieldStatusVariant;
347
372
  /** Tooltip text displayed next to the label. */
348
373
  labelTooltip?: string;
349
374
  /** Trigger and field size. */
350
375
  size?: ComplexSelectorSize;
351
- /** Visual trigger style. Ghost matches toolbar buttons. */
376
+ /**
377
+ * Visual trigger style. Secondary matches secondary buttons; ghost matches
378
+ * ghost buttons.
379
+ */
352
380
  variant?: ComplexSelectorVariant;
353
381
  /** Icon displayed at the start of the trigger. */
354
382
  startIcon?: ReactNode | IconType;
@@ -494,7 +522,7 @@ export function ComplexSelector<Value>({
494
522
  const isEffectivelyRequired = useResolvedRequired({isRequired, isOptional});
495
523
  const placeholder = placeholderFromProps ?? t('@astryx.selector.placeholder');
496
524
  const effectiveStatusVariant =
497
- variant === 'ghost' && statusVariant === 'attached'
525
+ variant !== 'input' && statusVariant === 'attached'
498
526
  ? 'detached'
499
527
  : statusVariant;
500
528
 
@@ -709,10 +737,11 @@ export function ComplexSelector<Value>({
709
737
  // matched a mouse click on the trigger and drew the outline for
710
738
  // pointer users too. `focusWithin` here is `:has(:focus-visible)`.
711
739
  focusOutlineStyles.focusWithin,
740
+ variant === 'secondary' && styles.triggerSecondary,
712
741
  variant === 'ghost' && styles.triggerGhost,
713
- variant === 'ghost' && interactionOverlayStyles.backgroundImage,
742
+ variant !== 'input' && interactionOverlayStyles.backgroundImage,
714
743
  isDisabled && inputWrapperStyles.disabled,
715
- variant === 'ghost' && isDisabled && styles.triggerGhostDisabled,
744
+ variant !== 'input' && isDisabled && styles.triggerGhostDisabled,
716
745
  isDisabled && styles.disabled,
717
746
  triggerLabel == null && styles.placeholder,
718
747
  controlXstyle,
@@ -143,6 +143,12 @@ export const docs = {
143
143
  'When true, right-click shows the native browser context menu instead.',
144
144
  default: 'false',
145
145
  },
146
+ {
147
+ name: 'triggerXstyle',
148
+ type: 'StyleXStyles | StyleXStyles[]',
149
+ description:
150
+ 'Styles applied to the trigger wrapper. Accepts `stylex.create()` values, not inline style objects.',
151
+ },
146
152
  {
147
153
  name: 'triggerAs',
148
154
  type: "'div' | 'span'",
@@ -26,6 +26,7 @@ architecture:
26
26
  architecture:component-theming-surface,
27
27
  architecture:interaction-modality,
28
28
  architecture:layer-runtime,
29
+ architecture:public-component-api,
29
30
  ]
30
31
  contributing: []
31
32
  system_specs: []
@@ -36,17 +37,16 @@ system_specs: []
36
37
  ## Intent
37
38
 
38
39
  ContextMenu presents actions for a caller-provided region in either a
39
- cursor-positioned pointer menu or a BottomSheet-hosted touch action list. This
40
- draft records current consumer anatomy, presentation-specific ownership, and
41
- theming reachability without changing runtime behavior, styling, targets, or
42
- public API.
40
+ cursor-positioned pointer menu or a BottomSheet-hosted touch action list. The
41
+ record describes current consumer anatomy, presentation-specific ownership, and
42
+ theming reachability.
43
43
 
44
44
  ## Compatibility and migration
45
45
 
46
- - Released default preserved: `yes`
47
- - Compatibility class: additive documentation only; runtime, DOM, styling,
48
- targets, and public API remain unchanged
49
- - Controlled/uncontrolled behavior: unchanged
46
+ - Released default: `popover`
47
+ - Compatibility class: compatible behavior correction; public API, defaults,
48
+ DOM shape, styling, and targets are preserved
49
+ - Controlled/uncontrolled behavior: internally managed open state
50
50
  - Migration decision: none
51
51
 
52
52
  Consumer migration instructions belong in consumer docs and release notes.
@@ -81,16 +81,17 @@ Consumer props, content modes, and presentation policy remain documented in
81
81
 
82
82
  ## Behavioral and layout contract
83
83
 
84
- | ID | Candidate invariant | Basis | Draft review state |
85
- | --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
86
- | FR1 | Every render contains a caller-provided Trigger area. Pointer presentation opens a Pointer menu surface at the current local cursor anchor; touch presentation opens a Touch sheet frame containing a Touch menu surface and, for data mode, a Touch action list and Touch action rows. | Current source, docs, and tests | Verified current behavior; no new behavior decided |
87
- | FR2 | The current `context-menu` target remains on the painted Pointer menu surface and alternative Touch menu surface, not on the Trigger area or cursor anchor. | Current source, docs, and tests | Verified current inventory; no target change |
88
- | FR3 | Pointer action rows retain DropdownMenu ownership. The touch frame retains BottomSheet ownership, while data-driven touch lists and rows retain List ownership. | Current source and owner docs | Verified current delegation; no ownership change |
89
- | FR4 | Compound `menuContent` remains a caller-supplied pointer-menu interior and may also render inside the touch frame; the data-driven touch path instead converts the same item data to List and ListItem presentation. | Current source and tests | Verified current branches; no behavior change |
90
- | FR5 | The Pointer menu surface carries `data-astryx-menu-press` and follows `module:DropdownMenu/useMenuPress` FR1–FR7: the row under the release acts, the highlight follows a held pointer, a mouse released outside closes and a finger leaves the menu open, the stray click never acts, and `touch-action` follows overflow. Right-click, Shift+F10, long-press invocation and keyboard navigation are unchanged. | `module:DropdownMenu/useMenuPress`; `ContextMenu.test.tsx` press model suite | Proposed; verified in jsdom |
91
- | FR6 | With `triggerAs="span"` the Trigger area MUST be an inline element that owns a box, so the cursor anchor measures from the trigger's own rect on every invocation path. Every invocation path (right-click, Shift+F10, long-press) and the `context-menu` target's placement are unchanged. | `component:ContextMenu/DEC-1` | Accepted; verified in jsdom |
92
- | FR7 | Sub-menu rows follow `component:DropdownMenu` FR12 inside the pointer menu: ContextMenu mounts the same drill-in view stack, named after its `label`, so a sub-menu drills in on a phone here too. | `component:DropdownMenu/DEC-2` | Proposed; verified in jsdom |
93
- | FR8 | In the pointer menu ArrowDown on the last enabled row wraps to the first and ArrowUp on the first to the last; PageDown and PageUp page as in DropdownMenu FR6; a held key's auto-repeat does not activate; typeahead matches the row's label element alone. | Proposed in this change; `ContextMenu.test.tsx` keyboard case; shared `useListFocus` and `useTypeahead` suites | Proposed; verified in jsdom, pending owner review |
84
+ | ID | Candidate invariant | Basis | Draft review state |
85
+ | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------- |
86
+ | FR1 | Every render contains a caller-provided Trigger area. Pointer presentation opens a Pointer menu surface at the current local cursor anchor; touch presentation opens a Touch sheet frame containing a Touch menu surface and, for data mode, a Touch action list and Touch action rows. | Current source, docs, and tests | Current behavior; verified |
87
+ | FR2 | The current `context-menu` target remains on the painted Pointer menu surface and alternative Touch menu surface, not on the Trigger area or cursor anchor. | Current source, docs, and tests | Current inventory; verified |
88
+ | FR3 | Pointer action rows retain DropdownMenu ownership. The touch frame retains BottomSheet ownership, while data-driven touch lists and rows retain List ownership. | Current source and owner docs | Current delegation; verified |
89
+ | FR4 | Compound `menuContent` remains a caller-supplied pointer-menu interior and may also render inside the touch frame; the data-driven touch path instead converts the same item data to List and ListItem presentation. | Current source and tests | Current branches; verified |
90
+ | FR5 | The Pointer menu surface carries `data-astryx-menu-press` and follows `module:DropdownMenu/useMenuPress` FR1–FR7: the row under the release acts, the highlight follows a held pointer, a mouse released outside closes and a finger leaves the menu open, the stray click never acts, and `touch-action` follows overflow. Right-click, Shift+F10, long-press invocation and keyboard navigation keep their existing behavior. | `module:DropdownMenu/useMenuPress`; `ContextMenu.test.tsx` press model suite | Current module projection; verified in jsdom |
91
+ | FR6 | With `triggerAs="span"` the Trigger area MUST be an inline element that owns a box, so the cursor anchor measures from the trigger's own rect on every invocation path. Right-click, Shift+F10, long-press, and `context-menu` target placement all use that box. | `component:ContextMenu/DEC-1` | Accepted; verified in jsdom |
92
+ | FR7 | Sub-menu rows follow `component:DropdownMenu` FR12 inside the pointer menu: ContextMenu mounts the same drill-in view stack, named after its `label`, so a sub-menu drills in on a phone here too. | `component:DropdownMenu/DEC-2` | Proposed; verified in jsdom; owner decision pending |
93
+ | FR8 | In the pointer menu ArrowDown on the last enabled row wraps to the first and ArrowUp on the first to the last; PageDown and PageUp page as in DropdownMenu FR6; a held key's auto-repeat does not activate; typeahead matches the row's label element alone. | Current source and tests; shared `useListFocus` and `useTypeahead` suites | Proposed; verified in jsdom; owner decision pending |
94
+ | FR9 | While the Pointer menu surface is open, it participates in `family:overlay-dismissal` through the shared stack and provides descendant depth. One Escape press affects only the top-most present nested surface; ContextMenu closes only when it is that surface. | `family:overlay-dismissal` FR1–FR6; `ContextMenu.test.tsx` nested-layer case | Current family projection; verified in jsdom |
94
95
 
95
96
  ### Allowed variation
96
97
 
@@ -115,19 +116,21 @@ Consumer props, content modes, and presentation policy remain documented in
115
116
 
116
117
  ### Transformation and precedence order
117
118
 
118
- - No new presentation-resolution, cursor-position, long-press, item-rendering,
119
- or styling precedence rule is introduced.
119
+ - Presentation resolution, cursor positioning, long-press handling, item
120
+ rendering, and styling keep their current precedence.
120
121
 
121
122
  ### Performance and resources
122
123
 
123
- - No new loading, listener, measurement, or render constraint is introduced.
124
+ - The component introduces no separate loading, listener, measurement, or render
125
+ constraint beyond the owners named above.
124
126
 
125
127
  ## Accessibility contract
126
128
 
127
- This draft does not change ContextMenu's menu and dialog naming, keyboard
128
- invocation, long-press path, keyboard navigation, item semantics, focus
129
- return, or dismissal ordering. While a pointer is held in the pointer menu,
130
- the highlight is DOM focus per `module:DropdownMenu/useMenuPress` AR1.
129
+ ContextMenu keeps its menu and dialog naming, keyboard invocation, long-press
130
+ path, keyboard navigation, item semantics, focus return, and presentation-specific
131
+ dismissal behavior. Pointer-menu Escape requests participate in the shared layer
132
+ stack; while a pointer is held in the pointer menu, the highlight is DOM focus per
133
+ `module:DropdownMenu/useMenuPress` AR1.
131
134
 
132
135
  - **AR1 — An inline trigger keeps its semantics.** `triggerAs` changes the
133
136
  Trigger area's element and box only; it carries no role or `aria-haspopup`
@@ -185,27 +188,28 @@ anchor is positioning infrastructure rather than consumer anatomy.
185
188
  - `architecture:component-theming-surface` owns anatomy qualification, local
186
189
  target mapping, presentation-specific rows, and delegation rules.
187
190
  - `architecture:interaction-modality` owns the shared right-click, keyboard,
188
- long-press, pointer, touch, and adaptive-presentation boundaries; this draft
189
- changes none of them.
191
+ long-press, pointer, touch, and adaptive-presentation boundaries.
190
192
  - `architecture:layer-runtime` owns the current context-mode `useLayer` host and
191
- cursor-anchor positioning. ContextMenu currently retains local outside-click
192
- and Escape listeners; touch hosting delegates to BottomSheet.
193
- - `family:overlay-dismissal` owns shared Escape and platform-close ordering and
194
- records ContextMenu and BottomSheet as current local-only adoption gaps. This
195
- anatomy backfill does not migrate either path.
193
+ cursor-anchor positioning. ContextMenu retains local outside-click handling and
194
+ joins shared Escape ordering through `useLayerDismissal`; touch hosting delegates
195
+ to BottomSheet.
196
+ - `family:overlay-dismissal` owns shared Escape and platform-close ordering.
197
+ ContextMenu's pointer presentation is a shared-stack adopter; the touch
198
+ presentation delegates dismissal to BottomSheet, which is also a shared-stack
199
+ owner.
196
200
 
197
201
  ## Verification map
198
202
 
199
- | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
200
- | ------------------- | ------------------------------------------------------------------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------- |
201
- | FR1, FR4 | `ContextMenu.test.tsx` pointer, long-press, bottom-sheet, adaptive, compound, and drill-in suites | Pointer data/compound and touch data/compound | Collapsing presentation-specific parts or content owners fails structure, role, or behavior tests. | `audit:ContextMenu/anatomy` |
202
- | FR2 | ContextMenu target tests, source inspection, and theming target inventories | Pointer and touch menu surfaces | Moving `context-menu` to the trigger or removing it from a painted branch fails evidence. | `audit:ContextMenu/theming` |
203
- | FR3 | ContextMenu, DropdownMenu, BottomSheet, and List owner tests | Pointer rows, touch frame, touch list/rows | A composed part loses its owner target or is documented as a new ContextMenu target. | `audit:ContextMenu/theming` |
204
- | Layer relationships | `ContextMenu.test.tsx` and current layer/dismissal architecture records | Outside click, Escape, context anchor, sheet | Documentation claims shared dismissal where current source retains local behavior. | `audit:ContextMenu/behavior` |
205
- | FR5 | `ContextMenu.test.tsx` press model case; `module:DropdownMenu/useMenuPress` map | Finger slide across rows, stray click | A row acting on the press row or the stray click fails. | `audit:ContextMenu/behavior` |
206
- | FR6 | `ContextMenu.test.tsx` keyboard case; shared `useListFocus` and `useTypeahead` suites | wrap at both ends | Clamping arrows fail. | `audit:ContextMenu/behavior` |
207
- | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and current target | Missing, extra, prefixed, stale, or unclassified mappings fail repository validation. | `audit:ContextMenu/theming` |
208
- | FR6, AR1 | `ContextMenu.test.tsx` "inline trigger (triggerAs)" suite | `span` in a paragraph, default `div` | A block trigger inside prose, or a lost invocation path, fails. | `audit:ContextMenu/behavior` |
203
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
204
+ | ------------------- | ------------------------------------------------------------------------------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------- |
205
+ | FR1, FR4 | `ContextMenu.test.tsx` pointer, long-press, bottom-sheet, adaptive, compound, and drill-in suites | Pointer data/compound and touch data/compound | Collapsing presentation-specific parts or content owners fails structure, role, or behavior tests. | `audit:ContextMenu/anatomy` |
206
+ | FR2 | ContextMenu target tests, source inspection, and theming target inventories | Pointer and touch menu surfaces | Moving `context-menu` to the trigger or removing it from a painted branch fails evidence. | `audit:ContextMenu/theming` |
207
+ | FR3 | ContextMenu, DropdownMenu, BottomSheet, and List owner tests | Pointer rows, touch frame, touch list/rows | A composed part loses its owner target or is documented as a new ContextMenu target. | `audit:ContextMenu/theming` |
208
+ | Layer relationships | `ContextMenu.test.tsx` nested-layer case and current layer/dismissal architecture records | Outside click, Escape, nested context layers, sheet | A Pointer menu bypassing the shared stack or dismissing with its nested child fails. | `audit:ContextMenu/behavior` |
209
+ | FR5 | `ContextMenu.test.tsx` press model case; `module:DropdownMenu/useMenuPress` map | Finger slide across rows, stray click | A row acting on the press row or the stray click fails. | `audit:ContextMenu/behavior` |
210
+ | FR6 | `ContextMenu.test.tsx` keyboard case; shared `useListFocus` and `useTypeahead` suites | wrap at both ends | Clamping arrows fail. | `audit:ContextMenu/behavior` |
211
+ | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and current target | Missing, extra, prefixed, stale, or unclassified mappings fail repository validation. | `audit:ContextMenu/theming` |
212
+ | FR6, AR1 | `ContextMenu.test.tsx` "inline trigger (triggerAs)" suite | `span` in a paragraph, default `div` | A block trigger inside prose, or a lost invocation path, fails. | `audit:ContextMenu/behavior` |
209
213
 
210
214
  ## Decision log
211
215
 
@@ -222,17 +226,11 @@ doors of a context menu without becoming a block that breaks the paragraph.
222
226
  `triggerAs` renders the Trigger area as a `span`. The default `div` and its
223
227
  `position: relative` are unchanged.
224
228
 
225
- Rejected: a `triggerXstyle` recipe — cannot change the element, and a
226
- `display: inline` div inside a `<p>` is invalid HTML.
227
-
228
- Rejected: a third `contents` value, a span with `display: contents`. A
229
- box-less wrapper has no rect to measure the cursor anchor from, so the anchor
230
- falls back to the nearest positioned ancestor — which in running prose is
231
- usually the page. A mouse user gets the menu at the pointer and a keyboard
232
- user pressing Shift+F10 gets it at the page corner, and the keyboard path is
233
- the one with no way around it. A `span` is inline, owns a box, measures
234
- correctly, and is invisible to layout in every way the prose case cares
235
- about.
229
+ Rejected: styling a block `div` as inline because it remains invalid inside a
230
+ `<p>`.
231
+
232
+ Rejected: `display: contents` because a box-less wrapper has no rect for pointer
233
+ or keyboard anchoring.
236
234
 
237
235
  ## Open questions
238
236
 
@@ -8,6 +8,8 @@
8
8
  * SYNC: When ContextMenu.tsx changes, update tests to match new behavior
9
9
  */
10
10
 
11
+ import {useRef, useState} from 'react';
12
+ import {createPortal} from 'react-dom';
11
13
  import {describe, it, expect, vi, beforeEach} from 'vitest';
12
14
  import {render, screen, fireEvent, act, waitFor} from '@testing-library/react';
13
15
  import userEvent from '@testing-library/user-event';
@@ -23,6 +25,32 @@ import {
23
25
  } from './index';
24
26
  import {DropdownMenuItem} from '../DropdownMenu/DropdownMenuItem';
25
27
  import {Divider} from '../Divider';
28
+ import {useLayerDismissal} from '../Layer/useLayerDismissal';
29
+
30
+ function NestedDismissibleLayer({onDismiss}: {onDismiss: () => void}) {
31
+ const [isOpen, setIsOpen] = useState(true);
32
+ const containerRef = useRef<HTMLButtonElement>(null);
33
+
34
+ useLayerDismissal({
35
+ isActive: isOpen,
36
+ onDismiss: () => {
37
+ setIsOpen(false);
38
+ onDismiss();
39
+ },
40
+ getContainer: () => containerRef.current,
41
+ });
42
+
43
+ // Portaled outside the menu's DOM, so only the layer depth ContextMenu
44
+ // provides can rank it above the menu.
45
+ return isOpen
46
+ ? createPortal(
47
+ <button ref={containerRef} type="button">
48
+ Nested layer
49
+ </button>,
50
+ document.body,
51
+ )
52
+ : null;
53
+ }
26
54
 
27
55
  beforeEach(() => {
28
56
  HTMLDialogElement.prototype.showModal = vi.fn(function (
@@ -149,6 +177,56 @@ describe('ContextMenu', () => {
149
177
  expect(HTMLElement.prototype.showPopover).toHaveBeenCalled();
150
178
  });
151
179
 
180
+ it('composes the consumer context-menu handler with menu opening', () => {
181
+ const onContextMenu = vi.fn();
182
+ render(
183
+ <ContextMenu items={[{label: 'Item 1'}]} onContextMenu={onContextMenu}>
184
+ <div>Right-click me</div>
185
+ </ContextMenu>,
186
+ );
187
+
188
+ fireEvent.contextMenu(screen.getByText('Right-click me'));
189
+ expect(onContextMenu).toHaveBeenCalledOnce();
190
+ expect(HTMLElement.prototype.showPopover).toHaveBeenCalledOnce();
191
+ });
192
+
193
+ it('keeps component-owned opening when a consumer prevents default', () => {
194
+ render(
195
+ <ContextMenu
196
+ items={[{label: 'Item 1'}]}
197
+ onContextMenu={event => event.preventDefault()}>
198
+ <div>Right-click me</div>
199
+ </ContextMenu>,
200
+ );
201
+
202
+ fireEvent.contextMenu(screen.getByText('Right-click me'));
203
+ expect(HTMLElement.prototype.showPopover).toHaveBeenCalledOnce();
204
+ });
205
+
206
+ it('lets a nested layer consume Escape before the context menu', () => {
207
+ const onNestedDismiss = vi.fn();
208
+ render(
209
+ <ContextMenu
210
+ menuContent={<NestedDismissibleLayer onDismiss={onNestedDismiss} />}>
211
+ <div>Right-click me</div>
212
+ </ContextMenu>,
213
+ );
214
+
215
+ fireEvent.contextMenu(screen.getByText('Right-click me'));
216
+ const nestedLayer = screen.getByRole('button', {
217
+ name: 'Nested layer',
218
+ hidden: true,
219
+ });
220
+ nestedLayer.focus();
221
+ fireEvent.keyDown(nestedLayer, {key: 'Escape'});
222
+
223
+ expect(onNestedDismiss).toHaveBeenCalledOnce();
224
+ expect(HTMLElement.prototype.hidePopover).not.toHaveBeenCalled();
225
+
226
+ fireEvent.keyDown(document, {key: 'Escape'});
227
+ expect(HTMLElement.prototype.hidePopover).toHaveBeenCalledOnce();
228
+ });
229
+
152
230
  it('opens the real menu content in a BottomSheet when requested', async () => {
153
231
  render(
154
232
  <ContextMenu
@@ -393,6 +471,60 @@ describe('ContextMenu', () => {
393
471
  }
394
472
  });
395
473
 
474
+ it('composes consumer touch handlers with long-press detection', () => {
475
+ vi.useFakeTimers();
476
+ try {
477
+ const onTouchStart = vi.fn();
478
+ render(
479
+ <ContextMenu
480
+ items={[{label: 'Item 1'}]}
481
+ data-testid="ctx"
482
+ onTouchStart={onTouchStart}>
483
+ <div>Long-press me</div>
484
+ </ContextMenu>,
485
+ );
486
+
487
+ fireEvent.touchStart(screen.getByTestId('ctx'), {
488
+ touches: [{clientX: 20, clientY: 20}],
489
+ });
490
+ expect(onTouchStart).toHaveBeenCalledOnce();
491
+
492
+ act(() => {
493
+ vi.advanceTimersByTime(500);
494
+ });
495
+ expect(HTMLElement.prototype.showPopover).toHaveBeenCalledOnce();
496
+ } finally {
497
+ vi.useRealTimers();
498
+ }
499
+ });
500
+
501
+ it('clears a pending long-press when a consumer prevents default on touch end', () => {
502
+ vi.useFakeTimers();
503
+ try {
504
+ render(
505
+ <ContextMenu
506
+ items={[{label: 'Item 1'}]}
507
+ data-testid="ctx"
508
+ onTouchEnd={event => event.preventDefault()}>
509
+ <div>Long-press me</div>
510
+ </ContextMenu>,
511
+ );
512
+
513
+ const trigger = screen.getByTestId('ctx');
514
+ fireEvent.touchStart(trigger, {
515
+ touches: [{clientX: 20, clientY: 20}],
516
+ });
517
+ fireEvent.touchEnd(trigger);
518
+
519
+ act(() => {
520
+ vi.advanceTimersByTime(500);
521
+ });
522
+ expect(HTMLElement.prototype.showPopover).not.toHaveBeenCalled();
523
+ } finally {
524
+ vi.useRealTimers();
525
+ }
526
+ });
527
+
396
528
  it('suppresses native text selection on the long-press trigger', () => {
397
529
  const source = readFileSync(
398
530
  'packages/core/src/ContextMenu/ContextMenu.tsx',
@@ -1103,7 +1235,6 @@ describe('ContextMenuGroup', () => {
1103
1235
  // Three public surfaces alias this component at once. An alias that
1104
1236
  // re-exports without the semantics would pass a render test and fail a
1105
1237
  // screen reader.
1106
- const user = userEvent.setup();
1107
1238
  render(
1108
1239
  <ContextMenu
1109
1240
  menuContent={