@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
@@ -729,6 +729,18 @@ export function Tokenizer<T extends SearchableItem>({
729
729
  [value],
730
730
  );
731
731
 
732
+ // Selected labels, lowercased, for the Create entry's duplicate check. The
733
+ // typed text is a label, not an id: real selections usually carry an opaque
734
+ // id (`usr_123` under the label "Alice"), so comparing the text against ids
735
+ // alone misses an already-tokenized label — and the search results cannot
736
+ // catch it either, because filteredSource has already removed the selected
737
+ // item from them. The result was a "Create Alice" entry that, when picked,
738
+ // added a second "Alice" token under a different id.
739
+ const selectedLabels = useMemo(
740
+ () => new Set(value.map(item => item.label.toLowerCase())),
741
+ [value],
742
+ );
743
+
732
744
  const filteredSource: SearchSource<T> = useMemo(
733
745
  () => ({
734
746
  search: async (query: string) => {
@@ -759,6 +771,7 @@ export function Tokenizer<T extends SearchableItem>({
759
771
  }
760
772
  const alreadyExists =
761
773
  selectedIds.has(trimmed) ||
774
+ selectedLabels.has(trimmed.toLowerCase()) ||
762
775
  results.some(
763
776
  item => item.label.toLowerCase() === trimmed.toLowerCase(),
764
777
  );
@@ -773,7 +786,7 @@ export function Tokenizer<T extends SearchableItem>({
773
786
  } as unknown as T,
774
787
  ];
775
788
  },
776
- [hasCreate, selectedIds],
789
+ [hasCreate, selectedIds, selectedLabels],
777
790
  );
778
791
 
779
792
  const emptySource: SearchSource<T> = useMemo(
@@ -807,7 +820,13 @@ export function Tokenizer<T extends SearchableItem>({
807
820
  item.id.startsWith(CREATABLE_ID_PREFIX)
808
821
  ) {
809
822
  const createdValue = item.id.slice(CREATABLE_ID_PREFIX.length);
810
- if (selectedIds.has(createdValue)) {
823
+ // Same domain fix as createEntries: the created value names itself by
824
+ // label, so a selection carrying an opaque id with that label is also a
825
+ // duplicate, not just one whose id happens to equal the text.
826
+ if (
827
+ selectedIds.has(createdValue) ||
828
+ selectedLabels.has(createdValue.toLowerCase())
829
+ ) {
811
830
  return;
812
831
  }
813
832
  const base = {id: createdValue, label: createdValue};
@@ -825,7 +844,16 @@ export function Tokenizer<T extends SearchableItem>({
825
844
  onChange(newItems, {item, type: 'add'});
826
845
  announce(t('@astryx.tokenizer.tokenAdded', {label: item.label}));
827
846
  },
828
- [value, onChange, isAtMax, selectedIds, hasCreate, announce, t],
847
+ [
848
+ value,
849
+ onChange,
850
+ isAtMax,
851
+ selectedIds,
852
+ selectedLabels,
853
+ hasCreate,
854
+ announce,
855
+ t,
856
+ ],
829
857
  );
830
858
 
831
859
  // Handle removing an item. Single removal path: both Backspace on an empty
@@ -2,18 +2,19 @@
2
2
 
3
3
  /**
4
4
  * @file Toolbar.test.tsx
5
- * @input Uses vitest, @testing-library/react, Toolbar
6
- * @output Unit tests for Toolbar component
7
- * @position Testing; validates Toolbar implementation
5
+ * @input Uses vitest, @testing-library/react, user-event, Toolbar, MoreMenu
6
+ * @output Unit tests for Toolbar component and nested popover focus ownership
7
+ * @position Testing; validates Toolbar implementation and post-dismissal tab stops
8
8
  *
9
9
  * SYNC: When Toolbar component changes, update tests to match new behavior
10
10
  */
11
11
 
12
12
  import {describe, it, expect, vi} from 'vitest';
13
- import {render, screen} from '@testing-library/react';
13
+ import {act, fireEvent, render, screen, waitFor} from '@testing-library/react';
14
14
  import userEvent from '@testing-library/user-event';
15
15
  import * as stylex from '@stylexjs/stylex';
16
16
  import {Toolbar} from './Toolbar';
17
+ import {MoreMenu} from '../MoreMenu';
17
18
  import {
18
19
  paddingBlockStyles,
19
20
  paddingInlineStyles,
@@ -377,6 +378,132 @@ describe('Toolbar', () => {
377
378
  expect(buttons[2]).toHaveAttribute('tabindex', '-1');
378
379
  });
379
380
 
381
+ it('keeps its tab stop after outside dismissal of MoreMenu', async () => {
382
+ const user = userEvent.setup();
383
+ render(
384
+ <>
385
+ <button type="button">Before</button>
386
+ <Toolbar
387
+ label="Actions"
388
+ startContent={<button type="button">Copy</button>}
389
+ endContent={
390
+ <MoreMenu
391
+ label="More actions"
392
+ items={[{label: 'Duplicate'}, {label: 'Delete'}]}
393
+ />
394
+ }
395
+ />
396
+ <button type="button">After</button>
397
+ </>,
398
+ );
399
+
400
+ const trigger = screen.getByRole('button', {name: 'More actions'});
401
+ // Seed the trigger's tab stop with keyboard navigation so this tests
402
+ // boundary ownership independently of pointer-focus synchronization.
403
+ act(() => screen.getByRole('button', {name: 'Copy'}).focus());
404
+ await user.keyboard('{ArrowRight}');
405
+ expect(trigger).toHaveFocus();
406
+ await user.click(trigger);
407
+ await waitFor(() =>
408
+ expect(screen.getByRole('menu', {hidden: true})).toHaveFocus(),
409
+ );
410
+
411
+ const item = screen.getByRole('menuitem', {
412
+ name: 'Duplicate',
413
+ hidden: true,
414
+ });
415
+ act(() => item.focus());
416
+ expect(item).toHaveFocus();
417
+ expect(trigger.getAttribute('tabindex')).toBe('0');
418
+ expect(
419
+ screen.getByRole('button', {name: 'Copy'}).getAttribute('tabindex'),
420
+ ).toBe('-1');
421
+ expect(
422
+ screen.getByRole('menu', {hidden: true}).getAttribute('tabindex'),
423
+ ).not.toBe('0');
424
+
425
+ await user.click(screen.getByRole('button', {name: 'After'}));
426
+ // jsdom has no native light dismiss. Deliver the browser's close event
427
+ // after focus has moved outside, without restoring it to the trigger.
428
+ const popover = item.closest('[popover]');
429
+ fireEvent(
430
+ popover!,
431
+ Object.assign(new Event('toggle'), {newState: 'closed'}),
432
+ );
433
+ await waitFor(() =>
434
+ expect(trigger).toHaveAttribute('aria-expanded', 'false'),
435
+ );
436
+ expect(screen.getByRole('button', {name: 'After'})).toHaveFocus();
437
+ await user.tab({shift: true});
438
+ expect(trigger).toHaveFocus();
439
+ await user.tab({shift: true});
440
+ expect(screen.getByRole('button', {name: 'Before'})).toHaveFocus();
441
+ });
442
+
443
+ it('leaves navigation inside an open MoreMenu to the menu', async () => {
444
+ const user = userEvent.setup();
445
+ render(
446
+ <Toolbar
447
+ label="Actions"
448
+ startContent={<button type="button">Copy</button>}
449
+ endContent={
450
+ <MoreMenu
451
+ label="More actions"
452
+ items={[{label: 'Duplicate'}, {label: 'Delete'}]}
453
+ />
454
+ }
455
+ />,
456
+ );
457
+ const trigger = screen.getByRole('button', {name: 'More actions'});
458
+ act(() => screen.getByRole('button', {name: 'Copy'}).focus());
459
+ await user.keyboard('{ArrowRight}');
460
+ expect(trigger).toHaveFocus();
461
+ await user.click(trigger);
462
+ await waitFor(() =>
463
+ expect(screen.getByRole('menu', {hidden: true})).toHaveFocus(),
464
+ );
465
+
466
+ const first = screen.getByRole('menuitem', {
467
+ name: 'Duplicate',
468
+ hidden: true,
469
+ });
470
+ const last = screen.getByRole('menuitem', {name: 'Delete', hidden: true});
471
+ act(() => first.focus());
472
+ await user.keyboard('{ArrowLeft}');
473
+ expect(first).toHaveFocus();
474
+ await user.keyboard('{ArrowRight}');
475
+ expect(first).toHaveFocus();
476
+
477
+ await user.keyboard('{ArrowDown}');
478
+ expect(last).toHaveFocus();
479
+
480
+ expect(trigger).toHaveAttribute('tabindex', '0');
481
+ });
482
+
483
+ it('keeps its own arrow navigation when rendered inside a popover', async () => {
484
+ const user = userEvent.setup();
485
+ render(
486
+ <div popover="manual">
487
+ <Toolbar
488
+ label="Actions"
489
+ startContent={
490
+ <>
491
+ <button type="button">Copy</button>
492
+ <button type="button">Paste</button>
493
+ </>
494
+ }
495
+ />
496
+ </div>,
497
+ );
498
+ // jsdom does not open native popovers; the ancestor still tests ownership.
499
+ const copy = screen.getByRole('button', {name: 'Copy', hidden: true});
500
+ const paste = screen.getByRole('button', {name: 'Paste', hidden: true});
501
+ act(() => copy.focus());
502
+ await user.keyboard('{ArrowRight}');
503
+ expect(paste).toHaveFocus();
504
+ expect(paste.getAttribute('tabindex')).toBe('0');
505
+ });
506
+
380
507
  it('does not steal caret keys from a text input mid-line (navigation-4)', async () => {
381
508
  const user = userEvent.setup();
382
509
  render(
@@ -6,7 +6,8 @@
6
6
  * @file Toolbar.tsx
7
7
  * @input Uses Section, SizeContext, useListFocus, useKeyboardHint, StyleX, spacingVars, sizeVars
8
8
  * @output Exports Toolbar component and ToolbarProps
9
- * @position Core implementation; consumed by index.ts
9
+ * @position Core implementation; consumed by index.ts. Owns toolbar focus and
10
+ * keyboard navigation, excluding nested toolbars and popover contents.
10
11
  *
11
12
  * SYNC: When modified, update these files to stay in sync:
12
13
  * - /packages/core/src/Toolbar/Toolbar.doc.mjs
@@ -248,6 +249,7 @@ export function Toolbar({
248
249
 
249
250
  const {listRef, handleKeyDown, handleFocus} = useListFocus<HTMLDivElement>({
250
251
  itemSelector: 'button, input, [tabindex]',
252
+ boundarySelector: '[role="toolbar"], [popover]',
251
253
  orientation,
252
254
  hasRovingTabIndex: true,
253
255
  hasCaretGuard: true,
@@ -148,6 +148,21 @@ The styling seam does not change TreeList's tree, treeitem, group, roving-focus,
148
148
  expansion, selection, disabled, or activation behavior. Styling inputs MUST NOT
149
149
  replace component-owned semantic or interaction props.
150
150
 
151
+ - **AR1 — TreeList adopts the APG Tree View pattern** (`spec:AST-020/FR15`), as
152
+ one composite control: the Tree list is `role="tree"`, named by its Header
153
+ when one is rendered and otherwise by the caller's `aria-label` or
154
+ `aria-labelledby`; every row is `role="treeitem"`, named by its label and described by its
155
+ description, `aria-expanded` when it has children,
156
+ `aria-selected` when it is the selected row, `aria-level` for its depth, and
157
+ `aria-posinset`/`aria-setsize` among its siblings; children sit in a `role="group"`. One row holds the Tab stop
158
+ (roving focus). ArrowDown/ArrowUp move between visible rows; ArrowRight expands
159
+ a closed row, then moves into its first child, and ArrowLeft collapses an open
160
+ row or moves to the parent — mirrored under RTL; Home and End move to the first
161
+ and last visible rows; Enter and Space activate the focused row; typing letters
162
+ moves to the next row whose label starts with them. Selection is caller-owned:
163
+ callers mark at most one row selected (`isSelected`); the tree does not
164
+ enforce it, claims no `aria-multiselectable`, and has no selection keyboard.
165
+
151
166
  ## Design relationships
152
167
 
153
168
  | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
@@ -227,14 +242,15 @@ authorize or prohibit a future target.
227
242
 
228
243
  ## Verification map
229
244
 
230
- | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
231
- | ------------------- | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
232
- | FR1 | `TreeList.test.tsx` rendering, hierarchy, slot, and accessibility suites plus source inspection | Flat, nested, described, headed, and slotted items | Removing asserted Tree list, Header, Item, Chevron, Item label, Item description, Start content, End content, or Guide behavior fails existing role, content, hierarchy, or state assertions; Chevron glyph presence remains source-inspected only. | `audit:TreeList/anatomy` |
233
- | FR2 | `TreeList.test.tsx` and `themingTargets.test.ts` | Five local targets and their documented states | Removing or moving any current local target fails focused class assertions or the global inventory. | `audit:TreeList/theming` |
234
- | FR3 | Source inspection plus Icon public target metadata | Expandable rows and caller-provided slots | Existing focused tests do not assert the composed Icon instance; changing glyph or slot ownership requires this map and the relevant owner metadata to change. | `audit:TreeList/theming` |
235
- | FR4 | `TreeList.test.tsx` description rendering test and source inspection | Item with description | Removing description fails content coverage; adding a target requires an explicit map update. | `audit:TreeList/anatomy` |
236
- | FR5, FR6, ORD1 | Focused `TreeList.test.tsx` row-identity and styling-composition coverage, required with implementation | One input at a time; all three together; sibling rows | Dropping any mechanism, applying mechanisms to different elements, targeting the `<li>`, changing the merge order, or styling a sibling row fails. | `audit:TreeList/api` |
237
- | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and five current local targets | Missing, extra, duplicated, prefixed, or stale mappings fail repository validation. | `audit:TreeList/theming` |
245
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
246
+ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
247
+ | FR1 | `TreeList.test.tsx` rendering, hierarchy, slot, and accessibility suites plus source inspection | Flat, nested, described, headed, and slotted items | Removing asserted Tree list, Header, Item, Chevron, Item label, Item description, Start content, End content, or Guide behavior fails existing role, content, hierarchy, or state assertions; Chevron glyph presence remains source-inspected only. | `audit:TreeList/anatomy` |
248
+ | FR2 | `TreeList.test.tsx` and `themingTargets.test.ts` | Five local targets and their documented states | Removing or moving any current local target fails focused class assertions or the global inventory. | `audit:TreeList/theming` |
249
+ | FR3 | Source inspection plus Icon public target metadata | Expandable rows and caller-provided slots | Existing focused tests do not assert the composed Icon instance; changing glyph or slot ownership requires this map and the relevant owner metadata to change. | `audit:TreeList/theming` |
250
+ | FR4 | `TreeList.test.tsx` description rendering test and source inspection | Item with description | Removing description fails content coverage; adding a target requires an explicit map update. | `audit:TreeList/anatomy` |
251
+ | AR1 | Tree View accessibility contract (`internal/a11y-spec`, pending) bound to TreeList, in jsdom and Chromium; `TreeList.test.tsx` row-naming cases ("names a parent row by its label alone, not the chevron toggle", "names a plain row by its label and describes it with its description", "names and describes link and action rows themselves, not only their inner control", "keeps start and end content, including controls, out of the row name", "typeahead matches the label, not the description or start content") and `TreeListRowNames.a11y.chromium.spec.ts` for the browser-computed names; `TreeList.test.tsx` structural ARIA cases ("sets aria-level, aria-posinset, and aria-setsize at the top level", "sets aria-level/posinset/setsize correctly at deeper levels", "sets aria-level across three depths"); `TreeList.test.tsx` and `useTreeFocus.test.tsx` keyboard suites | Flat and nested trees; expanded/collapsed; selected/unselected; LTR and RTL; typeahead | A row loses its role, label name, state or position, two rows hold a Tab stop, an arrow key stops moving or stops mirroring under RTL, Home/End or typeahead stop working, or the tree claims multi-select | `audit:TreeList/accessibility` |
252
+ | FR5, FR6, ORD1 | Focused `TreeList.test.tsx` row-identity and styling-composition coverage, required with implementation | One input at a time; all three together; sibling rows | Dropping any mechanism, applying mechanisms to different elements, targeting the `<li>`, changing the merge order, or styling a sibling row fails. | `audit:TreeList/api` |
253
+ | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and five current local targets | Missing, extra, duplicated, prefixed, or stale mappings fail repository validation. | `audit:TreeList/theming` |
238
254
 
239
255
  Focused target-placement assertions cover all five local targets. Runtime support
240
256
  and mutation-sensitive coverage for the item styling seam may land separately,
@@ -261,6 +277,15 @@ Rejected: applying the seam to the outer semantic `<li>`. That element owns tree
261
277
  semantics and child grouping, while the row box is the stable visual and hover
262
278
  boundary callers need to address.
263
279
 
280
+ ### DEC-2 — The tree is one composite control with single, caller-owned selection
281
+
282
+ **Reference:** `component:TreeList/DEC-2`
283
+ **Decider:** `cixzhang`, `2026-10-13`
284
+
285
+ TreeList already plays the Tree View's role as one widget — one Tab stop, arrows, Home/End, activation, typeahead — so it adopts the pattern's keyboard model except the optional `*` key (AR1), not only the required parts. Callers mark at most one row selected (`isSelected`) and the tree does not enforce it, so the tree never claims `aria-multiselectable` and adds no range or toggle keyboard.
286
+
287
+ Rejected: multi-select (`aria-multiselectable`, Shift/Ctrl ranges); a Tab stop per row.
288
+
264
289
  ## Open questions
265
290
 
266
291
  None.
@@ -170,6 +170,78 @@ describe('TreeList', () => {
170
170
  expect(screen.getByText('Description text')).toBeInTheDocument();
171
171
  });
172
172
 
173
+ // ===========================================================================
174
+ // Row accessible name and description (APG tree view: named by the label)
175
+ // ===========================================================================
176
+
177
+ it('names a parent row by its label alone, not the chevron toggle', () => {
178
+ const items: TreeListItemData[] = [
179
+ {id: 'docs', label: 'Documents', children: [{id: 'a', label: 'Annual'}]},
180
+ ];
181
+ render(<TreeList items={items} />);
182
+ const row = screen.getByText('Documents').closest('li')!;
183
+ expect(row).toHaveAccessibleName('Documents');
184
+ });
185
+
186
+ it('names a plain row by its label and describes it with its description', () => {
187
+ const items: TreeListItemData[] = [
188
+ {id: 'a', label: 'Budget', description: 'Updated yesterday'},
189
+ ];
190
+ render(<TreeList items={items} />);
191
+ const row = screen.getByText('Budget').closest('li')!;
192
+ expect(row).toHaveAccessibleName('Budget');
193
+ expect(row).toHaveAccessibleDescription('Updated yesterday');
194
+ });
195
+
196
+ it('names and describes link and action rows themselves, not only their inner control', () => {
197
+ const items: TreeListItemData[] = [
198
+ {id: 'l', label: 'Report', description: 'Opens the report', href: '#'},
199
+ {
200
+ id: 'b',
201
+ label: 'Export',
202
+ description: 'Downloads a file',
203
+ onClick: () => {},
204
+ },
205
+ ];
206
+ render(<TreeList items={items} />);
207
+ const link = screen.getByText('Report').closest('li')!;
208
+ expect(link).toHaveAccessibleName('Report');
209
+ expect(link).toHaveAccessibleDescription('Opens the report');
210
+ const action = screen.getByText('Export').closest('li')!;
211
+ expect(action).toHaveAccessibleName('Export');
212
+ expect(action).toHaveAccessibleDescription('Downloads a file');
213
+ });
214
+
215
+ it('keeps start and end content, including controls, out of the row name', () => {
216
+ const items: TreeListItemData[] = [
217
+ {
218
+ id: 'a',
219
+ label: 'Invoices',
220
+ startContent: <span>12</span>,
221
+ endContent: <button type="button">Pin folder</button>,
222
+ },
223
+ ];
224
+ render(<TreeList items={items} />);
225
+ const row = screen.getByText('Invoices').closest('li')!;
226
+ expect(row).toHaveAccessibleName('Invoices');
227
+ });
228
+
229
+ it('typeahead matches the label, not the description or start content', async () => {
230
+ const user = userEvent.setup();
231
+ const items: TreeListItemData[] = [
232
+ {id: 'a', label: 'Apple', description: 'Cherry-red fruit'},
233
+ {id: 'b', label: 'Banana', startContent: <span>Curved</span>},
234
+ {id: 'c', label: 'Cherry'},
235
+ ];
236
+ render(<TreeList items={items} />);
237
+ const apple = screen.getByText('Apple').closest('li')!;
238
+ apple.focus();
239
+ await user.keyboard('c');
240
+ expect(document.activeElement).toBe(
241
+ screen.getByText('Cherry').closest('li'),
242
+ );
243
+ });
244
+
173
245
  // ===========================================================================
174
246
  // Semantic HTML
175
247
  // ===========================================================================
@@ -27,7 +27,7 @@ import type {
27
27
  TreeListVariant,
28
28
  } from './TreeListTypes';
29
29
  import {themeProps} from '../utils/themeProps';
30
- import {useTreeFocus} from '../hooks/useTreeFocus';
30
+ import {useTreeFocusInternal} from '../hooks/useTreeFocus';
31
31
 
32
32
  // =============================================================================
33
33
  // Types
@@ -255,11 +255,15 @@ export function TreeList({
255
255
  return false;
256
256
  }, []);
257
257
 
258
- const {treeRef, handleKeyDown, handleFocus} = useTreeFocus<HTMLUListElement>({
259
- onToggleExpand: handleToggle,
260
- onActivate: activateItem,
261
- hasRovingTabIndex: true,
262
- });
258
+ const {treeRef, handleKeyDown, handleFocus} =
259
+ useTreeFocusInternal<HTMLUListElement>({
260
+ onToggleExpand: handleToggle,
261
+ onActivate: activateItem,
262
+ hasRovingTabIndex: true,
263
+ // Rows are named by their label (AR1); typeahead matches that label, not
264
+ // the chevron toggle, start content or description.
265
+ matchLabelledBy: true,
266
+ });
263
267
 
264
268
  const handleRootKeyDown = useCallback(
265
269
  (e: React.KeyboardEvent<HTMLDivElement>) => {
@@ -557,6 +557,11 @@ export function TreeListItem({
557
557
  return (
558
558
  <li
559
559
  role="treeitem"
560
+ // The row is named by its label alone and described by its description
561
+ // (APG tree view): without these the name would be the row's whole text,
562
+ // chevron button label and description included.
563
+ aria-labelledby={labelId}
564
+ aria-describedby={description != null ? descriptionId : undefined}
560
565
  aria-expanded={hasChildren ? isExpanded : undefined}
561
566
  aria-selected={isSelected || undefined}
562
567
  aria-disabled={isDisabled || undefined}
@@ -0,0 +1,125 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file TreeList.a11y.chromium.spec.ts
5
+ * @input Built TreeList tree-view stories and TREE_VIEW_PATTERN in real Chromium
6
+ * @output Browser evidence for every tree-view expectation across the binding's states
7
+ * @position Chromium lane of the TreeList tree-view binding (component:TreeList AR1)
8
+ */
9
+
10
+ import {
11
+ expect,
12
+ test,
13
+ type CDPSession,
14
+ type Locator,
15
+ type Page,
16
+ } from '@playwright/test';
17
+ import {
18
+ TREE_VIEW_PATTERN,
19
+ blockingResults,
20
+ checkAccessibilitySpec,
21
+ formatFailures,
22
+ neverExercised,
23
+ summarize,
24
+ type BindingResult,
25
+ } from '@astryxdesign/a11y-spec';
26
+ import {
27
+ createChromiumHarness,
28
+ holdMotionStill,
29
+ } from '@astryxdesign/a11y-spec/chromium';
30
+ import {
31
+ DEFAULT_STORYBOOK_DIR,
32
+ serveStorybook,
33
+ type StaticServer,
34
+ } from '@astryxdesign/a11y-spec/storybook';
35
+ import {
36
+ TREE_LIST_BINDING_STATES,
37
+ type TreeListBindingState,
38
+ } from './TreeList.a11y.states';
39
+
40
+ let storybook: StaticServer;
41
+
42
+ test.beforeAll(async () => {
43
+ storybook = await serveStorybook(
44
+ process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
45
+ );
46
+ });
47
+
48
+ test.afterAll(async () => {
49
+ await storybook?.close();
50
+ });
51
+
52
+ function storyUrl(storyId: string): string {
53
+ return `${storybook.origin}/iframe.html?id=${storyId}&viewMode=story`;
54
+ }
55
+
56
+ function rowLocator(page: Page, id: string): Locator {
57
+ return page.locator(
58
+ `#storybook-root [role="treeitem"][data-tree-id="${id}"]`,
59
+ );
60
+ }
61
+
62
+ async function mountState(
63
+ page: Page,
64
+ state: TreeListBindingState,
65
+ ): Promise<{subject: Locator; related: Record<string, Locator>}> {
66
+ await page.goto(storyUrl(state.storyId), {waitUntil: 'load'});
67
+ await holdMotionStill(page);
68
+ const subject = rowLocator(page, state.subject);
69
+ await subject.waitFor({state: 'attached'});
70
+ const root = page.locator('#storybook-root');
71
+ const related: Record<string, Locator> = {
72
+ tree: root.getByRole('tree'),
73
+ before: root.getByRole('button', {name: 'Before the tree'}),
74
+ after: root.getByRole('button', {name: 'After the tree'}),
75
+ group: subject.locator('[role="group"]'),
76
+ };
77
+ for (const [name, id] of Object.entries(state.relations)) {
78
+ related[name] = rowLocator(page, id);
79
+ }
80
+ return {subject, related};
81
+ }
82
+
83
+ async function runState(
84
+ page: Page,
85
+ cdp: CDPSession,
86
+ state: TreeListBindingState,
87
+ ): Promise<BindingResult> {
88
+ return checkAccessibilitySpec({
89
+ spec: TREE_VIEW_PATTERN,
90
+ binding: 'TreeList',
91
+ state: state.id,
92
+ facts: state.facts,
93
+ mount: async () => {
94
+ const {subject, related} = await mountState(page, state);
95
+ return createChromiumHarness({page, cdp, subject, related});
96
+ },
97
+ });
98
+ }
99
+
100
+ test('every tree-view expectation is exercised by a TreeList state', async ({
101
+ page,
102
+ }) => {
103
+ const cdp = await page.context().newCDPSession(page);
104
+ const results: BindingResult[] = [];
105
+ for (const state of TREE_LIST_BINDING_STATES) {
106
+ results.push(await runState(page, cdp, state));
107
+ }
108
+ expect(neverExercised(results)).toEqual([]);
109
+ });
110
+
111
+ for (const state of TREE_LIST_BINDING_STATES) {
112
+ test(`TreeList [${state.id}] — ${state.summary}`, async ({page}) => {
113
+ const cdp = await page.context().newCDPSession(page);
114
+ const result = await runState(page, cdp, state);
115
+ const report = summarize(TREE_VIEW_PATTERN, [result]);
116
+ expect(formatFailures(blockingResults([result]))).toBe('');
117
+ expect(
118
+ result.results.filter(
119
+ row => row.status !== 'pass' && row.status !== 'not-applicable',
120
+ ),
121
+ ).toEqual([]);
122
+ expect(report.unrunLayers).toEqual([]);
123
+ expect(report.counts.unexpectedPass).toBe(0);
124
+ });
125
+ }
@@ -0,0 +1,44 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file TreeList.a11y.renders.tsx
5
+ * @input One TreeList tree-view binding state
6
+ * @output The React tree every lane renders for that state (the jsdom test directly, the story through Storybook)
7
+ * @position Shared fixture for the TreeList tree-view binding
8
+ */
9
+
10
+ import type {ReactElement} from 'react';
11
+ import {TreeList} from '../TreeList';
12
+ import type {TreeListItemData} from '../TreeListTypes';
13
+ import {
14
+ TREE_LIST_LABEL,
15
+ type TreeListBindingState,
16
+ } from './TreeList.a11y.states';
17
+
18
+ export function renderTreeListState(
19
+ state: Pick<TreeListBindingState, 'expanded' | 'disabled' | 'direction'>,
20
+ ): ReactElement {
21
+ const expanded = (id: string) => state.expanded.includes(id);
22
+ const disabled = (id: string) => state.disabled?.includes(id) ?? false;
23
+ const items: TreeListItemData[] = [
24
+ {
25
+ id: 'docs',
26
+ label: 'Documents',
27
+ isExpanded: expanded('docs'),
28
+ isDisabled: disabled('docs'),
29
+ children: [
30
+ {id: 'annual', label: 'Annual report'},
31
+ {id: 'budget', label: 'Budget'},
32
+ ],
33
+ },
34
+ {id: 'photos', label: 'Photos', isDisabled: disabled('photos')},
35
+ {id: 'trash', label: 'Trash', isDisabled: disabled('trash')},
36
+ ];
37
+ return (
38
+ <div dir={state.direction ?? 'ltr'}>
39
+ <button type="button">Before the tree</button>
40
+ <TreeList aria-label={TREE_LIST_LABEL} items={items} />
41
+ <button type="button">After the tree</button>
42
+ </div>
43
+ );
44
+ }