@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
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * @file useHoverCard.ts
7
7
  * @input Uses useLayer, useTouchTrigger, React hooks
8
- * @output Exports useHoverCard hook for hover/focus/tap triggered layers
8
+ * @output Exports useHoverCard hook with stable trigger refs for hover/focus/tap layers
9
9
  * @position Layer hook; builds on useLayer for hover card behavior
10
10
  *
11
11
  * SYNC: When modified, update:
@@ -295,6 +295,21 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
295
295
  onShow,
296
296
  onHide,
297
297
  });
298
+ // Destructure the layer and touch objects once: both are rebuilt every
299
+ // render, but their members are stable. Depending on the members instead of
300
+ // the enclosing objects keeps the refs below identity-stable across
301
+ // unchanged rerenders, so React never detaches and re-attaches the trigger
302
+ // (and never rewrites the trigger's `anchor-name`) while the card streams
303
+ // alongside it.
304
+ const {
305
+ ref: layerRef,
306
+ anchorId: layerAnchorId,
307
+ show: showLayer,
308
+ hide: hideLayer,
309
+ isOpen: isLayerOpen,
310
+ id: layerId,
311
+ render: renderLayer,
312
+ } = layer;
298
313
 
299
314
  const popoverXstyle = styles.container;
300
315
 
@@ -321,21 +336,27 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
321
336
  // passing across the trigger, and a tap is never that.
322
337
  const showNow = useCallback(() => {
323
338
  clearTimeouts();
324
- layer.show();
325
- }, [clearTimeouts, layer]);
339
+ showLayer();
340
+ }, [clearTimeouts, showLayer]);
326
341
 
327
342
  const hideNow = useCallback(() => {
328
343
  clearTimeouts();
329
344
  isHoveringContentRef.current = false;
330
- layer.hide();
331
- }, [clearTimeouts, layer]);
345
+ hideLayer();
346
+ }, [clearTimeouts, hideLayer]);
332
347
 
333
- const touch = useTouchTrigger({
348
+ const {
349
+ isTouchPointerRef,
350
+ isTouchInteraction,
351
+ handlePointerEnter,
352
+ handlePointerDown: handleTouchPointerDown,
353
+ clearTapOpen,
354
+ } = useTouchTrigger({
334
355
  touchTrigger,
335
356
  isEnabled,
336
357
  isControlled: isOpen !== undefined,
337
- isOpen: layer.isOpen,
338
- layerId: layer.id,
358
+ isOpen: isLayerOpen,
359
+ layerId: layerId,
339
360
  triggerRef,
340
361
  show: showNow,
341
362
  hide: hideNow,
@@ -348,9 +369,9 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
348
369
  }
349
370
  clearTimeouts();
350
371
  showTimeoutRef.current = setTimeout(() => {
351
- layer.show();
372
+ showLayer();
352
373
  }, delay);
353
- }, [isEnabled, isOpen, clearTimeouts, layer, delay]);
374
+ }, [isEnabled, isOpen, clearTimeouts, showLayer, delay]);
354
375
 
355
376
  // Schedule hide with delay (suppressed when isOpen is true)
356
377
  const scheduleHide = useCallback(() => {
@@ -361,37 +382,37 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
361
382
  hideTimeoutRef.current = setTimeout(() => {
362
383
  // Don't hide if hovering content
363
384
  if (!isHoveringContentRef.current) {
364
- layer.hide();
385
+ hideLayer();
365
386
  }
366
387
  }, hideDelay);
367
- }, [isOpen, clearTimeouts, layer, hideDelay]);
388
+ }, [isOpen, clearTimeouts, hideLayer, hideDelay]);
368
389
 
369
390
  // Event handlers
370
391
  const handleMouseEnter = useCallback(() => {
371
392
  // A tap synthesizes mouseenter. On touch the tap path owns the decision,
372
393
  // so hover must not also fire — without this a hover card opens on every
373
394
  // tap of its trigger and has nothing to close it.
374
- if (touch.isTouchPointerRef.current) {
395
+ if (isTouchPointerRef.current) {
375
396
  return;
376
397
  }
377
398
  scheduleShow();
378
- }, [touch, scheduleShow]);
399
+ }, [isTouchPointerRef, scheduleShow]);
379
400
 
380
401
  const handleMouseLeave = useCallback(() => {
381
402
  // On touch the synthesized mouseleave arrives with the next tap elsewhere,
382
403
  // which the outside-tap dismissal already handles.
383
- if (touch.isTouchPointerRef.current) {
404
+ if (isTouchPointerRef.current) {
384
405
  return;
385
406
  }
386
407
  scheduleHide();
387
- }, [touch, scheduleHide]);
408
+ }, [isTouchPointerRef, scheduleHide]);
388
409
 
389
410
  // Tap-to-open on touch; on a mouse this does nothing and hover still rules.
390
411
  const handlePointerDown = useCallback(
391
412
  (event: PointerEvent) => {
392
- touch.handlePointerDown(event);
413
+ handleTouchPointerDown(event);
393
414
  },
394
- [touch],
415
+ [handleTouchPointerDown],
395
416
  );
396
417
 
397
418
  const handleFocusIn = useCallback(() => {
@@ -401,7 +422,7 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
401
422
  // A tap focuses the trigger it activates. Opening on that focus would
402
423
  // reinstate exactly the behavior the touch path just decided against —
403
424
  // and on an action trigger it covers the thing the user tapped.
404
- if (touch.isTouchInteraction()) {
425
+ if (isTouchInteraction()) {
405
426
  return;
406
427
  }
407
428
  // Skip showing if we're in the middle of an Escape dismiss
@@ -410,14 +431,14 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
410
431
  return;
411
432
  }
412
433
  clearTimeouts();
413
- layer.show();
414
- }, [isEnabled, touch, clearTimeouts, layer]);
434
+ showLayer();
435
+ }, [isEnabled, isTouchInteraction, clearTimeouts, showLayer]);
415
436
 
416
437
  const handleFocusOut = useCallback(
417
438
  (e: FocusEvent) => {
418
439
  // Check if focus is moving to the hover card content
419
440
  const relatedTarget = e.relatedTarget as HTMLElement | null;
420
- const popoverElement = document.getElementById(layer.id);
441
+ const popoverElement = document.getElementById(layerId);
421
442
 
422
443
  if (popoverElement?.contains(relatedTarget)) {
423
444
  // Focus moving into hover card, keep it open
@@ -426,7 +447,7 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
426
447
 
427
448
  scheduleHide();
428
449
  },
429
- [layer.id, scheduleHide],
450
+ [layerId, scheduleHide],
430
451
  );
431
452
 
432
453
  // Escape dismissal (WCAG 1.4.13) goes through the shared layer stack: a
@@ -453,7 +474,7 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
453
474
  const el =
454
475
  typeof document === 'undefined'
455
476
  ? null
456
- : document.getElementById(layer.id);
477
+ : document.getElementById(layerId);
457
478
  if (el == null) {
458
479
  return false;
459
480
  }
@@ -462,12 +483,12 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
462
483
  } catch {
463
484
  // Browsers without the Popover API (and some test environments) cannot
464
485
  // answer the selector; fall back to the hook's own state.
465
- return layer.isOpen;
486
+ return isLayerOpen;
466
487
  }
467
488
  },
468
489
  onDismiss: () => {
469
490
  clearTimeouts();
470
- touch.clearTapOpen();
491
+ clearTapOpen();
471
492
  // Controlled: report and stop. The close — and the focus restore that
472
493
  // goes with it — happens in the controlled effect if and when the
473
494
  // consumer flips `isOpen`.
@@ -485,9 +506,9 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
485
506
  const card =
486
507
  typeof document === 'undefined'
487
508
  ? null
488
- : document.getElementById(layer.id);
509
+ : document.getElementById(layerId);
489
510
  const hadFocus = card?.contains(document.activeElement) ?? false;
490
- layer.hide();
511
+ hideLayer();
491
512
  if (hadFocus) {
492
513
  isEscapeDismissingRef.current = true;
493
514
  triggerRef.current?.focus();
@@ -496,7 +517,6 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
496
517
  });
497
518
 
498
519
  // Interaction ref that handles event listeners only
499
- const {handlePointerEnter} = touch;
500
520
  const interactionRef: RefCallback<HTMLElement> = useCallback(
501
521
  (el: HTMLElement | null) => {
502
522
  // Cleanup previous element
@@ -554,10 +574,10 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
554
574
  // Combined ref - shorthand for calling both positionRef and interactionRef
555
575
  const ref: RefCallback<HTMLElement> = useCallback(
556
576
  (el: HTMLElement | null) => {
557
- layer.ref(el);
577
+ layerRef(el);
558
578
  interactionRef(el);
559
579
  },
560
- [layer, interactionRef],
580
+ [layerRef, interactionRef],
561
581
  );
562
582
 
563
583
  // Cleanup on unmount
@@ -570,7 +590,7 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
570
590
  // Show on mount when isDefaultOpen is true
571
591
  useEffect(() => {
572
592
  if (isDefaultOpen) {
573
- layer.show();
593
+ showLayer();
574
594
  }
575
595
  // eslint-disable-next-line @eslint-react/exhaustive-deps -- intentionally only on mount
576
596
  }, []);
@@ -582,7 +602,7 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
582
602
  }
583
603
  if (isOpen) {
584
604
  clearTimeouts();
585
- layer.show();
605
+ showLayer();
586
606
  } else {
587
607
  clearTimeouts();
588
608
  // A consumer closing the card while it holds focus would strand focus on
@@ -591,15 +611,15 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
591
611
  const card =
592
612
  typeof document === 'undefined'
593
613
  ? null
594
- : document.getElementById(layer.id);
614
+ : document.getElementById(layerId);
595
615
  const hadFocus = card?.contains(document.activeElement) ?? false;
596
- layer.hide();
616
+ hideLayer();
597
617
  if (hadFocus) {
598
618
  isEscapeDismissingRef.current = true;
599
619
  triggerRef.current?.focus();
600
620
  }
601
621
  }
602
- }, [isOpen, clearTimeouts, layer]);
622
+ }, [isOpen, clearTimeouts, showLayer, hideLayer, layerId]);
603
623
 
604
624
  // Render function that wraps layer.render with hover card behavior
605
625
  const renderHoverCard = useCallback(
@@ -636,20 +656,20 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
636
656
  as: 'div' as const,
637
657
  };
638
658
 
639
- return layer.render(
659
+ return renderLayer(
640
660
  <div
641
661
  {...stylex.props(styles.content)}
642
662
  onMouseEnter={() => {
643
663
  // Touch synthesizes these over the card too; letting a tap inside
644
664
  // register as "hovering content" would block every later hide.
645
- if (touch.isTouchPointerRef.current) {
665
+ if (isTouchPointerRef.current) {
646
666
  return;
647
667
  }
648
668
  isHoveringContentRef.current = true;
649
669
  clearTimeouts();
650
670
  }}
651
671
  onMouseLeave={() => {
652
- if (touch.isTouchPointerRef.current) {
672
+ if (isTouchPointerRef.current) {
653
673
  return;
654
674
  }
655
675
  isHoveringContentRef.current = false;
@@ -679,27 +699,27 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
679
699
  );
680
700
  },
681
701
  [
682
- layer,
702
+ renderLayer,
683
703
  placement,
684
704
  alignment,
685
705
  label,
686
706
  clearTimeouts,
687
707
  scheduleHide,
688
708
  popoverXstyle,
689
- touch,
709
+ isTouchPointerRef,
690
710
  ],
691
711
  );
692
712
 
693
713
  return {
694
714
  ref,
695
- positionRef: layer.ref,
715
+ positionRef: layerRef,
696
716
  interactionRef,
697
- anchorId: layer.anchorId,
698
- id: layer.id,
699
- describedBy: layer.id,
700
- isOpen: layer.isOpen,
717
+ anchorId: layerAnchorId,
718
+ id: layerId,
719
+ describedBy: layerId,
720
+ isOpen: isLayerOpen,
701
721
  renderHoverCard,
702
- show: layer.show,
703
- hide: layer.hide,
722
+ show: showLayer,
723
+ hide: hideLayer,
704
724
  };
705
725
  }
@@ -30,7 +30,7 @@ export const docs = {
30
30
  name: 'children',
31
31
  type: 'ReactNode',
32
32
  description:
33
- 'InputGroupText and compatible input children: TextInput, NumberInput, TimeInput, DateInput, Typeahead, Selector, or MultiSelector.',
33
+ 'InputGroupText, Button addons, and compatible input children: TextInput, NumberInput, TimeInput, DateInput, Typeahead, Selector, or MultiSelector.',
34
34
  required: true,
35
35
  },
36
36
  {
@@ -110,6 +110,11 @@ export const docs = {
110
110
  description:
111
111
  'Use InputGroupText for static prefixes/suffixes like "$", "kg", or "https://".',
112
112
  },
113
+ {
114
+ guidance: true,
115
+ description:
116
+ 'Use Button for an action attached to the input; InputGroup connects its corners automatically.',
117
+ },
113
118
  {
114
119
  guidance: true,
115
120
  description:
@@ -181,6 +186,11 @@ export const docsDense = {
181
186
  description:
182
187
  'Use InputGroupText for static prefixes/suffixes like "$", "kg", or "https://".',
183
188
  },
189
+ {
190
+ guidance: true,
191
+ description:
192
+ 'Use Button for an action attached to the input; InputGroup connects its corners automatically.',
193
+ },
184
194
  {
185
195
  guidance: true,
186
196
  description:
@@ -8,10 +8,8 @@
8
8
  * @output Exports InputGroup component with group label/description ARIA wiring
9
9
  * @position Groups input with prefix/suffix addons; consumed by index.ts
10
10
  *
11
- * Children (TextInput, NumberInput, TimeInput, DateInput, Typeahead,
12
- * Selector, MultiSelector) consume the InputGroup context
13
- * to remove their own border/radius so the group container provides
14
- * the unified border treatment.
11
+ * Compatible input children consume InputGroup context to remove competing
12
+ * border/radius geometry. Button addons consume it to join the same outer shape.
15
13
  *
16
14
  * SYNC: When modified, update these files to stay in sync:
17
15
  * - /packages/core/src/InputGroup/InputGroup.doc.mjs
@@ -3,8 +3,8 @@
3
3
  /**
4
4
  * @file groupStyles.ts
5
5
  * @input Uses StyleX, theme tokens, and layer-aware group end-cap selectors
6
- * @output Exports shared group-aware styles for input components
7
- * @position Shared styles consumed by InputGroup-compatible controls
6
+ * @output Exports shared group-aware styles for InputGroup children
7
+ * @position Shared styles consumed by InputGroup-compatible controls and addons
8
8
  */
9
9
 
10
10
  import * as stylex from '@stylexjs/stylex';
@@ -15,7 +15,45 @@ import {radiusVars, borderVars} from '../theme/tokens.stylex';
15
15
  // are not visual group members; skip them when finding the trailing edge.
16
16
  const IS_LAST_ITEM = ':not(:has(~ *:not([popover]):not(template):not(dialog)))';
17
17
 
18
+ // Button consumes InputGroup context anywhere below the group, including buttons
19
+ // inside an input's own implementation. Keep the visual adaptation on direct
20
+ // children so internal clear, calendar, and disclosure actions stay unchanged.
21
+ const DIRECT_INPUT_GROUP_ADDON = ':where(.astryx-input-group > *)';
22
+ const DIRECT_INPUT_GROUP_ADDON_FOCUS_VISIBLE =
23
+ ':where(.astryx-input-group > *):focus-visible';
24
+ const DIRECT_INPUT_GROUP_ADDON_NOT_FIRST =
25
+ ':where(.astryx-input-group > *):not(:first-child)';
26
+ const DIRECT_INPUT_GROUP_ADDON_WITH_MEMBER_AFTER =
27
+ ':where(.astryx-input-group > *):has(~ *:not([popover]):not(template):not(dialog))';
28
+
18
29
  export const groupStyles = stylex.create({
30
+ buttonAddon: {
31
+ borderStartStartRadius: {
32
+ default: null,
33
+ [DIRECT_INPUT_GROUP_ADDON_NOT_FIRST]: 0,
34
+ },
35
+ borderEndStartRadius: {
36
+ default: null,
37
+ [DIRECT_INPUT_GROUP_ADDON_NOT_FIRST]: 0,
38
+ },
39
+ borderStartEndRadius: {
40
+ default: null,
41
+ [DIRECT_INPUT_GROUP_ADDON_WITH_MEMBER_AFTER]: 0,
42
+ },
43
+ borderEndEndRadius: {
44
+ default: null,
45
+ [DIRECT_INPUT_GROUP_ADDON_WITH_MEMBER_AFTER]: 0,
46
+ },
47
+ height: {
48
+ default: null,
49
+ [DIRECT_INPUT_GROUP_ADDON]: '100%',
50
+ },
51
+ // Keep the keyboard ring above the adjacent group member at the shared edge.
52
+ zIndex: {
53
+ default: null,
54
+ [DIRECT_INPUT_GROUP_ADDON_FOCUS_VISIBLE]: 2,
55
+ },
56
+ },
19
57
  inGroup: {
20
58
  flex: 1,
21
59
  minWidth: 0,
@@ -96,16 +96,16 @@ export interface UseLayerDismissalReturn {
96
96
  *
97
97
  * The layer does NOT attach a key listener — the stack owns one listener and
98
98
  * routes each Escape press to the top-most REGISTERED layer, so one press
99
- * dismisses exactly one of them. Dialog (and what is built on it), Popover and
100
- * the menus built on it, Tooltip, HoverCard, Lightbox, MobileNav, BottomSheet,
101
- * BottomSheetSwitcher and lab's `Drawer` register today.
99
+ * dismisses exactly one of them. Dialog (and what is built on it, including
100
+ * CommandPalette), Popover and the menus built on it, ContextMenu, Tooltip,
101
+ * HoverCard, Lightbox, MobileNav, BottomSheet, BottomSheetSwitcher and lab's
102
+ * `Drawer` register today.
102
103
  *
103
- * `CommandPalette`, `ContextMenu`, `DropdownMenuSubMenu`, and
104
- * `PowerSearchEditPopover` still run their own Escape listeners. They stay safe
105
- * next to the stack only because each claims the press at element level, and the
106
- * stack stands down on an already-`defaultPrevented` press — but a registered
107
- * layer opened inside one of them does not get the press. Migrating them is the
108
- * fix.
104
+ * `DropdownMenuSubMenu` and `PowerSearchEditPopover` still run their own Escape
105
+ * listeners. They stay safe next to the stack only because each claims the press
106
+ * at element level, and the stack stands down on an already-`defaultPrevented`
107
+ * press — but a registered layer opened inside one of them does not get the
108
+ * press. Migrating them is the fix.
109
109
  *
110
110
  * Wrap the layer's own content in `LayerDepthProvider` so anything opened from
111
111
  * inside it registers as nested.
@@ -0,0 +1,142 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * Real-browser and accessibility-tree binding evidence for Link.
5
+ *
6
+ * A link's activation leaves no trace on the link, so the binding counts every
7
+ * navigation the story requests: native navigations through the browser's
8
+ * Navigation API (cancelled, so the story stays mounted), router navigations
9
+ * the story's router component records, and new tabs the page opens.
10
+ */
11
+
12
+ import {expect, test, type CDPSession, type Page} from '@playwright/test';
13
+ import {
14
+ LINK_PATTERN,
15
+ blockingResults,
16
+ checkAccessibilitySpec,
17
+ formatFailures,
18
+ neverExercised,
19
+ summarize,
20
+ type BindingResult,
21
+ } from '@astryxdesign/a11y-spec';
22
+ import {
23
+ createChromiumHarness,
24
+ holdMotionStill,
25
+ } from '@astryxdesign/a11y-spec/chromium';
26
+ import {
27
+ DEFAULT_STORYBOOK_DIR,
28
+ serveStorybook,
29
+ type StaticServer,
30
+ } from '@astryxdesign/a11y-spec/storybook';
31
+ import {LINK_A11Y_STATES, type LinkA11yRow} from './Link.a11y.states';
32
+
33
+ const SUBJECT_SELECTOR = '[data-a11y-link]';
34
+ const NEW_TAB_SETTLE_MS = 500;
35
+ let storybook: StaticServer;
36
+
37
+ interface NavigationWindow extends Window {
38
+ __a11yLinkNavigations?: number;
39
+ }
40
+
41
+ test.beforeAll(async () => {
42
+ storybook = await serveStorybook(
43
+ process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
44
+ );
45
+ });
46
+
47
+ test.afterAll(async () => storybook?.close());
48
+
49
+ function storyUrl(id: string): string {
50
+ return `${storybook.origin}/iframe.html?id=${id}&viewMode=story`;
51
+ }
52
+
53
+ async function countNavigations(
54
+ page: Page,
55
+ opensNewTab: boolean,
56
+ ): Promise<() => Promise<number>> {
57
+ let newTabs = 0;
58
+ page.context().on('page', opened => {
59
+ newTabs += 1;
60
+ void opened.close();
61
+ });
62
+ await page.evaluate(() => {
63
+ const target = window as NavigationWindow & {navigation: EventTarget};
64
+ target.__a11yLinkNavigations = 0;
65
+ target.navigation.addEventListener('navigate', event => {
66
+ target.__a11yLinkNavigations = (target.__a11yLinkNavigations ?? 0) + 1;
67
+ if (event.cancelable) {
68
+ event.preventDefault();
69
+ }
70
+ });
71
+ });
72
+ return async () => {
73
+ // A new tab reaches the test through the browser's target events, after
74
+ // the click or key that opened it has returned. Give it time to arrive so
75
+ // a later reading cannot miss a navigation an earlier one started.
76
+ if (opensNewTab) {
77
+ await page.waitForTimeout(NEW_TAB_SETTLE_MS);
78
+ }
79
+ const inPage = await page.evaluate(
80
+ () => (window as NavigationWindow).__a11yLinkNavigations ?? 0,
81
+ );
82
+ return inPage + newTabs;
83
+ };
84
+ }
85
+
86
+ async function runState(
87
+ page: Page,
88
+ cdp: CDPSession,
89
+ state: LinkA11yRow,
90
+ ): Promise<BindingResult> {
91
+ let navigations: () => Promise<number> = async () => {
92
+ throw new Error('the Link story was not mounted');
93
+ };
94
+ return checkAccessibilitySpec({
95
+ spec: LINK_PATTERN,
96
+ binding: 'Link',
97
+ state: state.id,
98
+ facts: state.facts,
99
+ mount: async () => {
100
+ await page.goto(storyUrl(state.storyId), {waitUntil: 'load'});
101
+ await holdMotionStill(page);
102
+ const subject = page.locator(SUBJECT_SELECTOR);
103
+ await subject.waitFor({state: 'visible'});
104
+ await expect(subject).toHaveCount(1);
105
+ navigations = await countNavigations(
106
+ page,
107
+ state.id === 'external-new-tab',
108
+ );
109
+ return createChromiumHarness({page, cdp, subject});
110
+ },
111
+ activations: async () => navigations(),
112
+ });
113
+ }
114
+
115
+ test('every Link expectation is exercised by at least one Link state', async ({
116
+ page,
117
+ }) => {
118
+ // Mounts every state in one test, so it gets every state's time budget.
119
+ test.setTimeout(30_000 * LINK_A11Y_STATES.length);
120
+ const cdp = await page.context().newCDPSession(page);
121
+ const results: BindingResult[] = [];
122
+ for (const state of LINK_A11Y_STATES) {
123
+ results.push(await runState(page, cdp, state));
124
+ }
125
+ expect(neverExercised(results)).toEqual([]);
126
+ });
127
+
128
+ for (const state of LINK_A11Y_STATES) {
129
+ test(`Link [${state.id}] — ${state.summary}`, async ({page}) => {
130
+ const cdp = await page.context().newCDPSession(page);
131
+ const result = await runState(page, cdp, state);
132
+ const report = summarize(LINK_PATTERN, [result]);
133
+ expect(formatFailures(blockingResults([result]))).toBe('');
134
+ expect(
135
+ result.results
136
+ .filter(row => row.status === 'fail')
137
+ .map(row => `${row.expectation}: ${row.detail ?? ''}`),
138
+ ).toEqual([]);
139
+ expect(report.unrunLayers).toEqual([]);
140
+ expect(report.counts.unexpectedPass).toBe(0);
141
+ });
142
+ }
@@ -0,0 +1,97 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /** Stable Link renders shared by jsdom, Storybook, and Chromium. */
6
+
7
+ import type {ComponentProps, MouseEvent, ReactElement} from 'react';
8
+ import {Icon} from '../../Icon';
9
+ import {Link} from '../Link';
10
+ import {LinkProvider} from '../LinkProvider';
11
+ import {LINK_A11Y_DESTINATION, type LinkA11yRow} from './Link.a11y.states';
12
+
13
+ interface NavigationWindow extends Window {
14
+ __a11yLinkNavigations?: number;
15
+ }
16
+
17
+ /**
18
+ * A minimal client-side router link. It takes over activation the way router
19
+ * links do — cancel the browser navigation, then navigate itself — and counts
20
+ * each navigation on the window, where the browser binding reads it.
21
+ */
22
+ function RouterLink({onClick, children, ...props}: ComponentProps<'a'>) {
23
+ const navigate = (event: MouseEvent<HTMLAnchorElement>) => {
24
+ onClick?.(event);
25
+ if (event.defaultPrevented) {
26
+ return;
27
+ }
28
+ event.preventDefault();
29
+ const target = window as NavigationWindow;
30
+ target.__a11yLinkNavigations = (target.__a11yLinkNavigations ?? 0) + 1;
31
+ };
32
+ return (
33
+ // eslint-disable-next-line @astryx/no-hardcoded-anchor -- this stands in for a third-party router's own anchor, which Link receives through LinkProvider and `as`.
34
+ <a {...props} data-a11y-link-router onClick={navigate}>
35
+ {children}
36
+ </a>
37
+ );
38
+ }
39
+
40
+ /** A tab stop after the link, so focus has somewhere to go when it leaves. */
41
+ function Frame({children}: {children: ReactElement}) {
42
+ return (
43
+ <div>
44
+ {children}
45
+ <button type="button">After</button>
46
+ </div>
47
+ );
48
+ }
49
+
50
+ export const LINK_A11Y_RENDERS: Readonly<
51
+ Record<LinkA11yRow['id'], () => ReactElement>
52
+ > = {
53
+ 'text-native': () => (
54
+ <Frame>
55
+ <Link data-a11y-link href={LINK_A11Y_DESTINATION}>
56
+ Documentation
57
+ </Link>
58
+ </Frame>
59
+ ),
60
+ 'icon-labelled': () => (
61
+ <Frame>
62
+ <Link data-a11y-link href={LINK_A11Y_DESTINATION} label="Close dialog">
63
+ <Icon icon="close" />
64
+ </Link>
65
+ </Frame>
66
+ ),
67
+ 'current-page': () => (
68
+ <Frame>
69
+ <Link aria-current="page" data-a11y-link href={LINK_A11Y_DESTINATION}>
70
+ Overview
71
+ </Link>
72
+ </Frame>
73
+ ),
74
+ 'external-new-tab': () => (
75
+ <Frame>
76
+ <Link data-a11y-link href={LINK_A11Y_DESTINATION} isExternalLink>
77
+ GitHub
78
+ </Link>
79
+ </Frame>
80
+ ),
81
+ 'provider-router': () => (
82
+ <LinkProvider component={RouterLink}>
83
+ <Frame>
84
+ <Link data-a11y-link href="/projects">
85
+ Projects
86
+ </Link>
87
+ </Frame>
88
+ </LinkProvider>
89
+ ),
90
+ 'as-router': () => (
91
+ <Frame>
92
+ <Link as={RouterLink} data-a11y-link href="/settings">
93
+ Settings
94
+ </Link>
95
+ </Frame>
96
+ ),
97
+ };