@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
@@ -0,0 +1,553 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file MobileNavFocus.test.tsx
5
+ * @input Uses vitest, @testing-library/react, MobileNav + AppShell + SideNav
6
+ * @output Regression tests for focus restoration when the drawer closes
7
+ * @position Testing; validates the focus restore in MobileNav.tsx
8
+ *
9
+ * Repro for the umbrella accessibility issue (#3343): closing the drawer
10
+ * dropped focus to `<body>` instead of returning it to the element that
11
+ * opened the drawer.
12
+ *
13
+ * jsdom has NO `<dialog>` implementation at all — `HTMLDialogElement.prototype`
14
+ * exposes only `open`, with no `show`/`showModal`/`close` (verified on
15
+ * jsdom 27.4.0). The other MobileNav suites polyfill those two methods as
16
+ * plain attribute toggles, which means focus never moves in the first place
17
+ * and the defect is invisible.
18
+ *
19
+ * These tests model the two halves of real browser behaviour explicitly:
20
+ * - `showModal()` runs the dialog focusing steps, so focus moves INTO the
21
+ * drawer. Without this the bug cannot reproduce.
22
+ * - `close()` does NOT hand focus back. That is the behaviour reported
23
+ * against Safari 26.5.2, and it is what the component must not depend on.
24
+ *
25
+ * The native close is deferred past the slide-out (#4290), and the restore has
26
+ * to follow it: while a modal <dialog> holds the top layer the rest of the
27
+ * document is inert, so `focus()` on the opener is silently dropped. jsdom has
28
+ * no top layer and cannot reproduce that, so these tests pin the call order
29
+ * and the timing directly instead.
30
+ *
31
+ * SYNC: When MobileNav.tsx's focus handling changes, update these tests.
32
+ */
33
+
34
+ import {
35
+ describe,
36
+ it,
37
+ expect,
38
+ vi,
39
+ beforeAll,
40
+ beforeEach,
41
+ afterEach,
42
+ } from 'vitest';
43
+ import {useState} from 'react';
44
+ import {render, screen, fireEvent, act} from '@testing-library/react';
45
+ import {MobileNav} from './MobileNav';
46
+ import {AppShell} from '../AppShell/AppShell';
47
+ import {SideNav, SideNavItem, SideNavSection} from '../SideNav';
48
+ import {stubMatchMedia} from '../__tests__/stubMatchMedia';
49
+
50
+ beforeAll(() => {
51
+ // Model the dialog focusing steps: showModal() moves focus into the dialog.
52
+ HTMLDialogElement.prototype.showModal = function (this: HTMLDialogElement) {
53
+ this.setAttribute('open', '');
54
+ const target = this.querySelector<HTMLElement>('[tabindex]');
55
+ target?.focus();
56
+ };
57
+ // Deliberately does NOT restore focus — mirrors the reported Safari
58
+ // behaviour the component must not rely on.
59
+ HTMLDialogElement.prototype.close = function (this: HTMLDialogElement) {
60
+ this.removeAttribute('open');
61
+ };
62
+ });
63
+
64
+ class MockResizeObserver {
65
+ observe() {}
66
+ unobserve() {}
67
+ disconnect() {}
68
+ }
69
+
70
+ // AppShell needs its breakpoint query to match, but a blanket `matches: true`
71
+ // would also match `prefers-reduced-motion`, which caps the close delay at 0.
72
+ // The restore has to wait for that delay, so keep the real one in play.
73
+ beforeEach(() => {
74
+ vi.stubGlobal('ResizeObserver', MockResizeObserver);
75
+ stubMatchMedia({reduceMotion: false});
76
+ });
77
+
78
+ afterEach(() => {
79
+ document.documentElement.style.overflow = '';
80
+ vi.restoreAllMocks();
81
+ vi.unstubAllGlobals();
82
+ });
83
+
84
+ /** Controlled standalone harness with a real trigger button. */
85
+ function StandaloneHarness({onMounted}: {onMounted?: () => void} = {}) {
86
+ const [isOpen, setIsOpen] = useState(false);
87
+ return (
88
+ <>
89
+ <button
90
+ type="button"
91
+ data-testid="trigger"
92
+ onClick={() => {
93
+ onMounted?.();
94
+ setIsOpen(true);
95
+ }}>
96
+ Open nav
97
+ </button>
98
+ <MobileNav
99
+ isOpen={isOpen}
100
+ onOpenChange={setIsOpen}
101
+ side="start"
102
+ data-testid="nav">
103
+ <a href="/home" data-testid="nav-link">
104
+ Home
105
+ </a>
106
+ </MobileNav>
107
+ </>
108
+ );
109
+ }
110
+
111
+ /** Standalone harness whose whole subtree — trigger included — can unmount. */
112
+ function UnmountHarness() {
113
+ const [isMounted, setIsMounted] = useState(true);
114
+ return (
115
+ <>
116
+ <button
117
+ type="button"
118
+ data-testid="unmount"
119
+ onClick={() => setIsMounted(false)}>
120
+ Unmount
121
+ </button>
122
+ {isMounted ? <StandaloneHarness /> : null}
123
+ </>
124
+ );
125
+ }
126
+
127
+ /** The drawer alone can unmount; the trigger stays in the document. */
128
+ function DrawerUnmountHarness() {
129
+ const [isOpen, setIsOpen] = useState(false);
130
+ const [hasDrawer, setHasDrawer] = useState(true);
131
+ return (
132
+ <>
133
+ <button
134
+ type="button"
135
+ data-testid="trigger"
136
+ onClick={() => setIsOpen(true)}>
137
+ Open nav
138
+ </button>
139
+ <button
140
+ type="button"
141
+ data-testid="unmount-drawer"
142
+ onClick={() => setHasDrawer(false)}>
143
+ Unmount drawer
144
+ </button>
145
+ {hasDrawer ? (
146
+ <MobileNav
147
+ isOpen={isOpen}
148
+ onOpenChange={setIsOpen}
149
+ side="start"
150
+ data-testid="nav">
151
+ <span>Content</span>
152
+ </MobileNav>
153
+ ) : null}
154
+ </>
155
+ );
156
+ }
157
+
158
+ /** Removes the trigger from the DOM in the same commit that closes the drawer. */
159
+ function DetachHarness() {
160
+ const [isOpen, setIsOpen] = useState(false);
161
+ const [hasTrigger, setHasTrigger] = useState(true);
162
+ return (
163
+ <>
164
+ {hasTrigger ? (
165
+ <button
166
+ type="button"
167
+ data-testid="trigger"
168
+ onClick={() => setIsOpen(true)}>
169
+ Open nav
170
+ </button>
171
+ ) : null}
172
+ <button
173
+ type="button"
174
+ data-testid="detach"
175
+ onClick={() => {
176
+ setHasTrigger(false);
177
+ setIsOpen(false);
178
+ }}>
179
+ Detach
180
+ </button>
181
+ <MobileNav isOpen={isOpen} onOpenChange={setIsOpen} side="start">
182
+ <span>Content</span>
183
+ </MobileNav>
184
+ </>
185
+ );
186
+ }
187
+
188
+ /** Opens the drawer without ever focusing the button that opens it. */
189
+ function BodyHarness() {
190
+ const [isOpen, setIsOpen] = useState(false);
191
+ return (
192
+ <>
193
+ <button
194
+ type="button"
195
+ data-testid="programmatic"
196
+ onClick={() => setIsOpen(v => !v)}>
197
+ Toggle
198
+ </button>
199
+ <MobileNav isOpen={isOpen} onOpenChange={setIsOpen} side="start">
200
+ <span>Content</span>
201
+ </MobileNav>
202
+ </>
203
+ );
204
+ }
205
+
206
+ function AppShellHarness() {
207
+ return (
208
+ <AppShell
209
+ sideNav={
210
+ <SideNav>
211
+ <SideNavSection title="Test" isHeaderHidden>
212
+ <SideNavItem label="Home" />
213
+ </SideNavSection>
214
+ </SideNav>
215
+ }
216
+ mobileNav={{breakpoint: 'md'}}>
217
+ <div>Content</div>
218
+ </AppShell>
219
+ );
220
+ }
221
+
222
+ /** Opens the standalone drawer from a focused trigger and returns the trigger. */
223
+ function openFromTrigger(): HTMLElement {
224
+ const trigger = screen.getByTestId('trigger');
225
+ trigger.focus();
226
+ fireEvent.click(trigger);
227
+ // showModal() pulled focus into the drawer.
228
+ expect(trigger).not.toHaveFocus();
229
+ return trigger;
230
+ }
231
+
232
+ function clickDrawerClose() {
233
+ fireEvent.click(screen.getByRole('button', {name: /close navigation/i}));
234
+ }
235
+
236
+ describe('MobileNav focus restoration', () => {
237
+ it('returns focus to the trigger when the drawer closes (standalone)', () => {
238
+ vi.useFakeTimers();
239
+ try {
240
+ render(<StandaloneHarness />);
241
+ const trigger = openFromTrigger();
242
+
243
+ clickDrawerClose();
244
+ act(() => {
245
+ vi.advanceTimersByTime(400);
246
+ });
247
+
248
+ expect(trigger).toHaveFocus();
249
+ } finally {
250
+ vi.useRealTimers();
251
+ }
252
+ });
253
+
254
+ it('returns focus to the hamburger toggle when the AppShell drawer closes', () => {
255
+ vi.useFakeTimers();
256
+ try {
257
+ render(<AppShellHarness />);
258
+
259
+ const toggle = screen.getByRole('button', {name: /open navigation/i});
260
+ toggle.focus();
261
+ fireEvent.click(toggle);
262
+
263
+ expect(toggle).not.toHaveFocus();
264
+
265
+ // Inside AppShell the drawer sits in an <Activity> that hides on close,
266
+ // so the native close is the unmount-style teardown, not the delayed
267
+ // timer. Focus still has to come back, and still only after close().
268
+ const closeSpy = vi.spyOn(HTMLDialogElement.prototype, 'close');
269
+ const focusSpy = vi.spyOn(toggle, 'focus');
270
+
271
+ fireEvent.click(screen.getByRole('button', {name: /close navigation/i}));
272
+ act(() => {
273
+ vi.advanceTimersByTime(400);
274
+ });
275
+
276
+ expect(document.activeElement).not.toBe(document.body);
277
+ expect(toggle).toHaveFocus();
278
+ expect(closeSpy).toHaveBeenCalled();
279
+ expect(focusSpy.mock.invocationCallOrder[0]).toBeGreaterThan(
280
+ closeSpy.mock.invocationCallOrder[0],
281
+ );
282
+ } finally {
283
+ vi.useRealTimers();
284
+ }
285
+ });
286
+
287
+ it('restores focus only after the native dialog has closed', () => {
288
+ // Load-bearing ordering contract. Browsers hold the rest of the document
289
+ // inert while a modal <dialog> is open, so focusing the trigger before
290
+ // close() would be silently dropped — the exact class of failure this fix
291
+ // exists to remove. jsdom has no top layer and cannot reproduce that, so
292
+ // assert the call order directly instead. Restoring when `isOpen` flips
293
+ // (in an effect or its cleanup) instead of after the deferred close()
294
+ // breaks this.
295
+ vi.useFakeTimers();
296
+ try {
297
+ render(<StandaloneHarness />);
298
+ const trigger = openFromTrigger();
299
+
300
+ const closeSpy = vi.spyOn(HTMLDialogElement.prototype, 'close');
301
+ const focusSpy = vi.spyOn(trigger, 'focus');
302
+
303
+ clickDrawerClose();
304
+ act(() => {
305
+ vi.advanceTimersByTime(400);
306
+ });
307
+
308
+ expect(closeSpy).toHaveBeenCalled();
309
+ expect(focusSpy).toHaveBeenCalled();
310
+ expect(focusSpy.mock.invocationCallOrder[0]).toBeGreaterThan(
311
+ closeSpy.mock.invocationCallOrder[0],
312
+ );
313
+ } finally {
314
+ vi.useRealTimers();
315
+ }
316
+ });
317
+
318
+ it('waits for the delayed close before restoring focus', () => {
319
+ // The close is deferred so the slide-out can play (250ms cap outside
320
+ // reduced motion). Until it runs the dialog is still modal and the trigger
321
+ // is inert, so an early restore would be a silent no-op in a browser.
322
+ vi.useFakeTimers();
323
+ try {
324
+ render(<StandaloneHarness />);
325
+ const trigger = openFromTrigger();
326
+ const closeSpy = vi.spyOn(HTMLDialogElement.prototype, 'close');
327
+
328
+ clickDrawerClose();
329
+ act(() => {
330
+ vi.advanceTimersByTime(200);
331
+ });
332
+
333
+ expect(closeSpy).not.toHaveBeenCalled();
334
+ expect(screen.getByTestId('nav')).toHaveAttribute('open');
335
+ expect(trigger).not.toHaveFocus();
336
+
337
+ act(() => {
338
+ vi.advanceTimersByTime(100);
339
+ });
340
+
341
+ expect(closeSpy).toHaveBeenCalledTimes(1);
342
+ expect(trigger).toHaveFocus();
343
+ } finally {
344
+ vi.useRealTimers();
345
+ }
346
+ });
347
+
348
+ it('keeps focus in the drawer when it is reopened before the delayed close', () => {
349
+ vi.useFakeTimers();
350
+ try {
351
+ render(<StandaloneHarness />);
352
+ const trigger = openFromTrigger();
353
+ const focusSpy = vi.spyOn(trigger, 'focus');
354
+
355
+ clickDrawerClose();
356
+ act(() => {
357
+ vi.advanceTimersByTime(100);
358
+ });
359
+ // Reopened while the close was still pending: the dialog never closed,
360
+ // so there is nothing to hand focus back from.
361
+ fireEvent.click(trigger);
362
+ act(() => {
363
+ vi.advanceTimersByTime(400);
364
+ });
365
+
366
+ expect(screen.getByTestId('nav')).toHaveAttribute('open');
367
+ expect(focusSpy).not.toHaveBeenCalled();
368
+ expect(trigger).not.toHaveFocus();
369
+ } finally {
370
+ vi.useRealTimers();
371
+ }
372
+ });
373
+
374
+ it('returns focus to the original opener after a reopen interrupted the close', () => {
375
+ vi.useFakeTimers();
376
+ try {
377
+ render(<StandaloneHarness />);
378
+ const trigger = openFromTrigger();
379
+
380
+ clickDrawerClose();
381
+ act(() => {
382
+ vi.advanceTimersByTime(100);
383
+ });
384
+ // The reopen happens with focus inside the drawer. That element must
385
+ // not replace the opener the drawer captured on the way in.
386
+ fireEvent.click(trigger);
387
+ act(() => {
388
+ vi.advanceTimersByTime(400);
389
+ });
390
+ expect(trigger).not.toHaveFocus();
391
+
392
+ clickDrawerClose();
393
+ act(() => {
394
+ vi.advanceTimersByTime(400);
395
+ });
396
+
397
+ expect(screen.getByTestId('nav')).not.toHaveAttribute('open');
398
+ expect(trigger).toHaveFocus();
399
+ } finally {
400
+ vi.useRealTimers();
401
+ }
402
+ });
403
+
404
+ it('restores focus when the drawer unmounts while its delayed close is pending', () => {
405
+ vi.useFakeTimers();
406
+ try {
407
+ render(<DrawerUnmountHarness />);
408
+ const trigger = openFromTrigger();
409
+ const closeSpy = vi.spyOn(HTMLDialogElement.prototype, 'close');
410
+ const focusSpy = vi.spyOn(trigger, 'focus');
411
+
412
+ clickDrawerClose();
413
+ act(() => {
414
+ vi.advanceTimersByTime(100);
415
+ });
416
+ expect(closeSpy).not.toHaveBeenCalled();
417
+
418
+ // Unmounting cancels the timer and closes the dialog on the spot. Focus
419
+ // goes back to the trigger from that close, and only after it.
420
+ fireEvent.click(screen.getByTestId('unmount-drawer'));
421
+
422
+ expect(screen.queryByTestId('nav')).toBeNull();
423
+ expect(closeSpy).toHaveBeenCalledTimes(1);
424
+ expect(trigger).toHaveFocus();
425
+ expect(focusSpy.mock.invocationCallOrder[0]).toBeGreaterThan(
426
+ closeSpy.mock.invocationCallOrder[0],
427
+ );
428
+
429
+ // The cancelled timer must not fire a second close later.
430
+ act(() => {
431
+ vi.advanceTimersByTime(400);
432
+ });
433
+ expect(closeSpy).toHaveBeenCalledTimes(1);
434
+ } finally {
435
+ vi.useRealTimers();
436
+ }
437
+ });
438
+
439
+ it('returns focus to the trigger even when focus moved inside the drawer first', () => {
440
+ vi.useFakeTimers();
441
+ try {
442
+ render(<StandaloneHarness />);
443
+ const trigger = openFromTrigger();
444
+
445
+ // User tabs to a link inside the drawer before dismissing it.
446
+ const link = screen.getByTestId('nav-link');
447
+ link.focus();
448
+ expect(link).toHaveFocus();
449
+
450
+ clickDrawerClose();
451
+ act(() => {
452
+ vi.advanceTimersByTime(400);
453
+ });
454
+
455
+ expect(trigger).toHaveFocus();
456
+ } finally {
457
+ vi.useRealTimers();
458
+ }
459
+ });
460
+
461
+ it('restores focus when the drawer is closed via Escape', () => {
462
+ vi.useFakeTimers();
463
+ try {
464
+ render(<StandaloneHarness />);
465
+ const trigger = openFromTrigger();
466
+
467
+ const dialog = screen.getByTestId('nav');
468
+ fireEvent(
469
+ dialog,
470
+ new Event('cancel', {bubbles: false, cancelable: true}),
471
+ );
472
+ act(() => {
473
+ vi.advanceTimersByTime(400);
474
+ });
475
+
476
+ expect(trigger).toHaveFocus();
477
+ } finally {
478
+ vi.useRealTimers();
479
+ }
480
+ });
481
+
482
+ it('does not throw when the drawer unmounts while still open', () => {
483
+ vi.useFakeTimers();
484
+ try {
485
+ render(<UnmountHarness />);
486
+ openFromTrigger();
487
+
488
+ // Unmounting the whole drawer subtree also takes the trigger with it —
489
+ // the restore must not throw and must not force focus onto a detached
490
+ // node.
491
+ expect(() => {
492
+ fireEvent.click(screen.getByTestId('unmount'));
493
+ act(() => {
494
+ vi.advanceTimersByTime(400);
495
+ });
496
+ }).not.toThrow();
497
+ expect(document.activeElement).toBe(document.body);
498
+ } finally {
499
+ vi.useRealTimers();
500
+ }
501
+ });
502
+
503
+ it('does not throw when the trigger is removed before the drawer closes', () => {
504
+ vi.useFakeTimers();
505
+ try {
506
+ render(<DetachHarness />);
507
+ const trigger = openFromTrigger();
508
+
509
+ // jsdom silently ignores focus() on a detached node, so assert the call
510
+ // is never made rather than assert on activeElement.
511
+ const triggerFocus = vi.spyOn(trigger, 'focus');
512
+ expect(() => {
513
+ fireEvent.click(screen.getByTestId('detach'));
514
+ act(() => {
515
+ vi.advanceTimersByTime(400);
516
+ });
517
+ }).not.toThrow();
518
+ expect(screen.queryByTestId('trigger')).toBeNull();
519
+ expect(triggerFocus).not.toHaveBeenCalled();
520
+ } finally {
521
+ vi.useRealTimers();
522
+ }
523
+ });
524
+
525
+ it('leaves focus alone when nothing was focused before opening', () => {
526
+ vi.useFakeTimers();
527
+ try {
528
+ render(<BodyHarness />);
529
+
530
+ // fireEvent.click does not focus, so <body> is the "trigger" the drawer
531
+ // sees when it opens — there is nothing meaningful to restore to.
532
+ expect(document.activeElement).toBe(document.body);
533
+ fireEvent.click(screen.getByTestId('programmatic'));
534
+ act(() => {
535
+ vi.advanceTimersByTime(400);
536
+ });
537
+
538
+ // jsdom treats <body> as a non-focusable area, so body.focus() is a
539
+ // silent no-op there and an activeElement assertion cannot tell the two
540
+ // implementations apart. Spy on the call itself — in a real browser
541
+ // body.focus() DOES blur whatever is focused.
542
+ const bodyFocus = vi.spyOn(document.body, 'focus');
543
+ clickDrawerClose();
544
+ act(() => {
545
+ vi.advanceTimersByTime(400);
546
+ });
547
+
548
+ expect(bodyFocus).not.toHaveBeenCalled();
549
+ } finally {
550
+ vi.useRealTimers();
551
+ }
552
+ });
553
+ });
@@ -125,6 +125,16 @@ This draft does not change or extend MoreMenu's existing accessible label,
125
125
  tooltip, icon-only Button semantics, focus behavior, keyboard operation, or
126
126
  dismissal behavior.
127
127
 
128
+ - **AR1 — The anchored trigger is a Menu Button through DropdownMenu.** In the
129
+ anchored pointer presentation MoreMenu's icon-only trigger adopts the APG
130
+ Menu Button pattern's required parts as `component:DropdownMenu/AR4` records
131
+ them — `button` role, its accessible label as the name,
132
+ `aria-haspopup="menu"`, `aria-expanded`, `aria-controls`, Enter or Space
133
+ opening with focus on the first enabled row — and the same named exception
134
+ for ArrowDown and ArrowUp opening. MoreMenu adds nothing of its own to that
135
+ contract. In the bottom-sheet presentation the trigger opens a dialog and says
136
+ `aria-haspopup="dialog"`, outside this pattern.
137
+
128
138
  ## Design relationships
129
139
 
130
140
  | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
@@ -195,13 +205,14 @@ presentation.
195
205
 
196
206
  ## Verification map
197
207
 
198
- | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
199
- | ------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------- | ------------------------- |
200
- | FR1, FR4 | `MoreMenu.test.tsx` render, pointer, bottom-sheet, placement, and controlled suites | Default, custom icon, pointer, touch | Replacing the composition or changing forwarded behavior fails existing render/interaction tests. | `audit:MoreMenu/anatomy` |
201
- | FR2 | MoreMenu target test, source inspection, and theming target inventories | Closed trigger and composed menu panel | Moving `more-menu` onto the trigger or removing it from the panel fails target evidence. | `audit:MoreMenu/theming` |
202
- | FR3 | MoreMenu, DropdownMenu, Button, Icon, and List source/owner tests | Trigger, icon branches, pointer rows, touch rows | A composed part loses its owner target or is documented as a new MoreMenu-owned descendant. | `audit:MoreMenu/theming` |
203
- | Layer relationships | `MoreMenu.test.tsx`, `DropdownMenu.test.tsx`, and current layer/dismissal records | Light dismiss, Escape, pointer, touch | Documentation gives MoreMenu a parallel runtime or misstates the current dismissal owner. | `audit:MoreMenu/behavior` |
204
- | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and current target | Missing, extra, prefixed, stale, or unclassified mappings fail repository validation. | `audit:MoreMenu/theming` |
208
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
209
+ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------ |
210
+ | FR1, FR4 | `MoreMenu.test.tsx` render, pointer, bottom-sheet, placement, and controlled suites | Default, custom icon, pointer, touch | Replacing the composition or changing forwarded behavior fails existing render/interaction tests. | `audit:MoreMenu/anatomy` |
211
+ | FR2 | MoreMenu target test, source inspection, and theming target inventories | Closed trigger and composed menu panel | Moving `more-menu` onto the trigger or removing it from the panel fails target evidence. | `audit:MoreMenu/theming` |
212
+ | FR3 | MoreMenu, DropdownMenu, Button, Icon, and List source/owner tests | Trigger, icon branches, pointer rows, touch rows | A composed part loses its owner target or is documented as a new MoreMenu-owned descendant. | `audit:MoreMenu/theming` |
213
+ | AR1 | `MoreMenu.test.tsx` "renders trigger button with default aria-label" and "has aria-haspopup and aria-expanded attributes"; keyboard opening verified through DropdownMenu's AR4 tests; MoreMenu-bound Menu Button accessibility contract (`internal/a11y-spec`) pending | Default and custom-icon triggers; closed/open; Enter, Space, ArrowDown, ArrowUp | The trigger loses its label, `aria-haspopup="menu"`, `aria-expanded` or `aria-controls`, or a keyboard open does not land on the first (ArrowUp: last) enabled row | `audit:MoreMenu/accessibility` |
214
+ | Layer relationships | `MoreMenu.test.tsx`, `DropdownMenu.test.tsx`, and current layer/dismissal records | Light dismiss, Escape, pointer, touch | Documentation gives MoreMenu a parallel runtime or misstates the current dismissal owner. | `audit:MoreMenu/behavior` |
215
+ | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and current target | Missing, extra, prefixed, stale, or unclassified mappings fail repository validation. | `audit:MoreMenu/theming` |
205
216
 
206
217
  ## Decision log
207
218
 
@@ -159,6 +159,44 @@ describe('MoreMenu', () => {
159
159
  expect(button).toHaveAttribute('aria-disabled', 'true');
160
160
  });
161
161
 
162
+ it('keeps the menu closed from every opening key and press while disabled', async () => {
163
+ const user = userEvent.setup();
164
+ render(<MoreMenu items={defaultItems} isDisabled />);
165
+ const button = screen.getByRole('button', {name: 'More options'});
166
+ button.focus();
167
+ for (const key of ['{Enter}', ' ', '{ArrowDown}', '{ArrowUp}']) {
168
+ await user.keyboard(key);
169
+ expect(button).toHaveAttribute('aria-expanded', 'false');
170
+ }
171
+ await user.pointer([
172
+ {keys: '[MouseLeft>]', target: button},
173
+ {keys: '[/MouseLeft]'},
174
+ ]);
175
+ expect(button).toHaveAttribute('aria-expanded', 'false');
176
+ });
177
+
178
+ it('does not carry a refused ArrowUp into a later open', async () => {
179
+ const user = userEvent.setup();
180
+ const {rerender} = render(<MoreMenu items={defaultItems} isDisabled />);
181
+ const button = screen.getByRole('button', {name: 'More options'});
182
+ button.focus();
183
+ await user.keyboard('{ArrowUp}');
184
+ expect(button).toHaveAttribute('aria-expanded', 'false');
185
+
186
+ // Enabled again, a synthesized click (detail 0: screen-reader activation)
187
+ // opens on the FIRST row; the refused ArrowUp must not have armed "last".
188
+ rerender(<MoreMenu items={defaultItems} />);
189
+ fireEvent.click(button, {detail: 0});
190
+ await waitFor(() => {
191
+ expect(button).toHaveAttribute('aria-expanded', 'true');
192
+ });
193
+ await waitFor(() => {
194
+ expect(
195
+ screen.getByRole('menuitem', {name: 'Edit', hidden: true}),
196
+ ).toHaveFocus();
197
+ });
198
+ });
199
+
162
200
  it('supports custom label', () => {
163
201
  render(<MoreMenu items={defaultItems} label="Row actions" />);
164
202
  expect(