@astryxdesign/core 0.7.1 → 0.7.2-canary.01eab6a

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 (331) hide show
  1. package/dist/AspectRatio/AspectRatio.d.ts +6 -8
  2. package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
  3. package/dist/AspectRatio/AspectRatio.js +44 -3
  4. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  5. package/dist/Breadcrumbs/BreadcrumbItem.js +5 -2
  6. package/dist/Button/Button.d.ts.map +1 -1
  7. package/dist/Button/Button.js +38 -14
  8. package/dist/CheckboxInput/CheckboxInput.d.ts +1 -1
  9. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  10. package/dist/CheckboxInput/CheckboxInput.js +3 -1
  11. package/dist/CheckboxList/CheckboxListItem.d.ts +9 -1
  12. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  13. package/dist/CheckboxList/CheckboxListItem.js +4 -2
  14. package/dist/CommandPalette/CommandPaletteInput.d.ts +1 -1
  15. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  16. package/dist/CommandPalette/CommandPaletteInput.js +37 -5
  17. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  18. package/dist/ContextMenu/ContextMenu.js +5 -2
  19. package/dist/Dialog/Dialog.d.ts.map +1 -1
  20. package/dist/Dialog/Dialog.js +20 -8
  21. package/dist/Drawer/Drawer.d.ts +191 -0
  22. package/dist/Drawer/Drawer.d.ts.map +1 -0
  23. package/dist/Drawer/Drawer.js +382 -0
  24. package/dist/Drawer/DrawerHeader.d.ts +85 -0
  25. package/dist/Drawer/DrawerHeader.d.ts.map +1 -0
  26. package/dist/Drawer/DrawerHeader.js +134 -0
  27. package/dist/Drawer/index.d.ts +11 -0
  28. package/dist/Drawer/index.d.ts.map +1 -0
  29. package/dist/Drawer/index.js +11 -0
  30. package/dist/Drawer/useDrawerDialogPresence.d.ts +42 -0
  31. package/dist/Drawer/useDrawerDialogPresence.d.ts.map +1 -0
  32. package/dist/Drawer/useDrawerDialogPresence.js +236 -0
  33. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  34. package/dist/DropdownMenu/DropdownMenu.js +47 -4
  35. package/dist/DropdownMenu/DropdownMenuContext.d.ts +17 -0
  36. package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
  37. package/dist/DropdownMenu/DropdownMenuContext.js +21 -0
  38. package/dist/DropdownMenu/DropdownMenuDivider.d.ts +5 -3
  39. package/dist/DropdownMenu/DropdownMenuDivider.d.ts.map +1 -1
  40. package/dist/DropdownMenu/DropdownMenuDivider.js +7 -3
  41. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  42. package/dist/DropdownMenu/DropdownMenuSubMenu.js +19 -8
  43. package/dist/EmptyState/EmptyState.d.ts.map +1 -1
  44. package/dist/EmptyState/EmptyState.js +4 -10
  45. package/dist/Field/Field.d.ts +1 -1
  46. package/dist/Field/Field.d.ts.map +1 -1
  47. package/dist/Field/Field.js +8 -4
  48. package/dist/Grid/GridSpan.d.ts.map +1 -1
  49. package/dist/Grid/GridSpan.js +1 -0
  50. package/dist/Heading/Heading.d.ts.map +1 -1
  51. package/dist/Heading/Heading.js +4 -2
  52. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  53. package/dist/HoverCard/useHoverCard.js +10 -2
  54. package/dist/Item/Item.d.ts +6 -2
  55. package/dist/Item/Item.d.ts.map +1 -1
  56. package/dist/Item/Item.js +19 -4
  57. package/dist/Layer/layerViewportInset.stylex.d.ts +6 -2
  58. package/dist/Layer/layerViewportInset.stylex.d.ts.map +1 -1
  59. package/dist/Layer/layerViewportInset.stylex.js +6 -2
  60. package/dist/Layer/useLayer.d.ts.map +1 -1
  61. package/dist/Layer/useLayer.js +13 -7
  62. package/dist/Layer/useLayerDismissal.d.ts +1 -1
  63. package/dist/Layer/useLayerDismissal.js +1 -1
  64. package/dist/Layout/container.stylex.d.ts +5 -5
  65. package/dist/Layout/container.stylex.js +4 -4
  66. package/dist/List/List.d.ts +46 -19
  67. package/dist/List/List.d.ts.map +1 -1
  68. package/dist/List/List.js +28 -10
  69. package/dist/List/ListContext.d.ts +8 -3
  70. package/dist/List/ListContext.d.ts.map +1 -1
  71. package/dist/List/ListContext.js +7 -2
  72. package/dist/List/ListItem.d.ts +13 -5
  73. package/dist/List/ListItem.d.ts.map +1 -1
  74. package/dist/List/ListItem.js +21 -8
  75. package/dist/List/index.d.ts +1 -1
  76. package/dist/List/index.d.ts.map +1 -1
  77. package/dist/Markdown/Markdown.d.ts.map +1 -1
  78. package/dist/Markdown/Markdown.js +8 -1
  79. package/dist/Markdown/plugins/frontmatter.d.ts.map +1 -1
  80. package/dist/Markdown/plugins/frontmatter.js +7 -3
  81. package/dist/MetadataList/MetadataList.d.ts.map +1 -1
  82. package/dist/MetadataList/MetadataList.js +5 -1
  83. package/dist/MetadataList/MetadataListContext.d.ts +1 -1
  84. package/dist/MetadataList/MetadataListContext.d.ts.map +1 -1
  85. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  86. package/dist/MultiSelector/MultiSelector.js +22 -8
  87. package/dist/MultiSelector/OptionActions.a11y.chromium.spec.js +100 -2
  88. package/dist/MultiSelector/hooks.d.ts.map +1 -1
  89. package/dist/MultiSelector/hooks.js +3 -1
  90. package/dist/Pagination/Pagination.d.ts +8 -5
  91. package/dist/Pagination/Pagination.d.ts.map +1 -1
  92. package/dist/Pagination/Pagination.js +21 -8
  93. package/dist/Popover/Popover.js +2 -2
  94. package/dist/PowerSearch/PowerSearch.js +1 -1
  95. package/dist/RadioList/RadioList.d.ts +1 -1
  96. package/dist/RadioList/RadioList.d.ts.map +1 -1
  97. package/dist/RadioList/RadioList.js +6 -2
  98. package/dist/RadioList/RadioListItem.d.ts +9 -1
  99. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  100. package/dist/RadioList/RadioListItem.js +2 -0
  101. package/dist/Selector/hooks.d.ts.map +1 -1
  102. package/dist/Selector/hooks.js +6 -2
  103. package/dist/SideNav/SideNavItem.d.ts +8 -1
  104. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  105. package/dist/SideNav/SideNavItem.js +71 -9
  106. package/dist/Switch/Switch.d.ts +1 -1
  107. package/dist/Switch/Switch.d.ts.map +1 -1
  108. package/dist/Switch/Switch.js +13 -1
  109. package/dist/TabList/TabMenu.d.ts +2 -2
  110. package/dist/TabList/TabMenu.d.ts.map +1 -1
  111. package/dist/TabList/TabMenu.js +6 -3
  112. package/dist/Text/Text.d.ts.map +1 -1
  113. package/dist/Text/Text.js +4 -2
  114. package/dist/Text/useTruncation.d.ts +7 -5
  115. package/dist/Text/useTruncation.d.ts.map +1 -1
  116. package/dist/Text/useTruncation.js +16 -5
  117. package/dist/ToggleButton/ToggleButton.d.ts +3 -2
  118. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  119. package/dist/ToggleButton/ToggleButton.js +12 -1
  120. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  121. package/dist/Tooltip/useTooltip.js +11 -0
  122. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  123. package/dist/TopNav/TopNavMegaMenu.js +3 -3
  124. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  125. package/dist/TopNav/TopNavMenu.js +3 -3
  126. package/dist/TreeList/TreeList.d.ts.map +1 -1
  127. package/dist/TreeList/TreeList.js +1 -0
  128. package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
  129. package/dist/TreeList/TreeListBranches.js +2 -2
  130. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  131. package/dist/TreeList/TreeListItem.js +57 -9
  132. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  133. package/dist/Typeahead/BaseTypeahead.js +26 -0
  134. package/dist/astryx.css +47 -13
  135. package/dist/hooks/useEntryAnimation.d.ts +2 -1
  136. package/dist/hooks/useEntryAnimation.d.ts.map +1 -1
  137. package/dist/hooks/useEntryAnimation.js +14 -12
  138. package/dist/hooks/useIsHydrating.d.ts +6 -0
  139. package/dist/hooks/useIsHydrating.d.ts.map +1 -0
  140. package/dist/hooks/useIsHydrating.js +30 -0
  141. package/dist/hooks/useMenuHover.d.ts.map +1 -1
  142. package/dist/hooks/useMenuHover.js +18 -13
  143. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  144. package/dist/i18n/generated-locales/en.generated.js +112 -1
  145. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  146. package/dist/i18n/generated-locales/pseudo.generated.js +112 -1
  147. package/dist/index.d.ts +1 -0
  148. package/dist/index.d.ts.map +1 -1
  149. package/dist/index.js +1 -0
  150. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  151. package/dist/theme/derivedVarRegistry.js +0 -3
  152. package/dist/utils/focusOutline.stylex.d.ts +4 -0
  153. package/dist/utils/focusOutline.stylex.d.ts.map +1 -1
  154. package/dist/utils/focusOutline.stylex.js +4 -0
  155. package/dist/utils/plainDate.d.ts.map +1 -1
  156. package/dist/utils/plainDate.js +15 -9
  157. package/dist/utils/sharedMutationObserver.d.ts +21 -0
  158. package/dist/utils/sharedMutationObserver.d.ts.map +1 -0
  159. package/dist/utils/sharedMutationObserver.js +69 -0
  160. package/locales/en.json +444 -0
  161. package/locales/pseudo.json +333 -0
  162. package/package.json +9 -4
  163. package/src/AspectRatio/AspectRatio.test.tsx +44 -43
  164. package/src/AspectRatio/AspectRatio.tsx +38 -18
  165. package/src/AspectRatio/__tests__/AspectRatioFitLayers.a11y.browser.spec.ts +157 -0
  166. package/src/Avatar/Avatar.doc.mjs +5 -3
  167. package/src/AvatarGroup/AvatarGroup.doc.mjs +3 -2
  168. package/src/Breadcrumbs/BreadcrumbItem.tsx +4 -1
  169. package/src/Breadcrumbs/Breadcrumbs.test.tsx +30 -1
  170. package/src/Button/Button.doc.mjs +2 -2
  171. package/src/Button/Button.test.tsx +155 -9
  172. package/src/Button/Button.tsx +39 -16
  173. package/src/Button/__tests__/Button.a11y.known-failures.ts +1 -62
  174. package/src/Button/__tests__/Button.a11y.states.ts +0 -10
  175. package/src/Chat/__tests__/ChatMessageMetadata.a11y.chromium.spec.ts +3 -1
  176. package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
  177. package/src/CheckboxInput/CheckboxInput.tsx +3 -1
  178. package/src/CheckboxList/CheckboxList.test.tsx +22 -0
  179. package/src/CheckboxList/CheckboxListItem.doc.mjs +16 -0
  180. package/src/CheckboxList/CheckboxListItem.tsx +10 -0
  181. package/src/CommandPalette/CommandPaletteInput.test.tsx +53 -4
  182. package/src/CommandPalette/CommandPaletteInput.tsx +33 -3
  183. package/src/CommandPalette/__tests__/CommandPaletteInputAudit.a11y.chromium.spec.ts +335 -0
  184. package/src/CommandPalette/__tests__/CommandPaletteItemAudit.a11y.chromium.spec.ts +292 -0
  185. package/src/ContextMenu/ContextMenu.doc.mjs +2 -3
  186. package/src/ContextMenu/ContextMenu.tsx +4 -1
  187. package/src/ContextMenu/__tests__/ContextMenuAudit.a11y.chromium.spec.ts +438 -0
  188. package/src/Dialog/Dialog.test.tsx +46 -0
  189. package/src/Dialog/Dialog.tsx +20 -7
  190. package/src/Dialog/__tests__/Dialog.a11y.known-failures.ts +0 -14
  191. package/src/Drawer/Drawer.doc.mjs +472 -0
  192. package/src/Drawer/Drawer.spec.md +357 -0
  193. package/src/Drawer/Drawer.test.tsx +1300 -0
  194. package/src/Drawer/Drawer.tsx +613 -0
  195. package/src/Drawer/DrawerDirection.test.tsx +127 -0
  196. package/src/Drawer/DrawerHeader.doc.mjs +293 -0
  197. package/src/Drawer/DrawerHeader.test.tsx +102 -0
  198. package/src/Drawer/DrawerHeader.tsx +235 -0
  199. package/src/Drawer/__tests__/DrawerAudit.a11y.chromium.spec.ts +401 -0
  200. package/src/Drawer/__tests__/DrawerHeaderAudit.a11y.chromium.spec.ts +297 -0
  201. package/src/Drawer/index.ts +13 -0
  202. package/src/Drawer/useDrawerDialogPresence.ts +284 -0
  203. package/src/DropdownMenu/DropdownMenu.test.tsx +62 -13
  204. package/src/DropdownMenu/DropdownMenu.tsx +47 -4
  205. package/src/DropdownMenu/DropdownMenuContext.tsx +24 -0
  206. package/src/DropdownMenu/DropdownMenuDivider.doc.mjs +2 -2
  207. package/src/DropdownMenu/DropdownMenuDivider.spec.md +192 -0
  208. package/src/DropdownMenu/DropdownMenuDivider.tsx +9 -5
  209. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +185 -0
  210. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +19 -5
  211. package/src/DropdownMenu/__tests__/DropdownMenuDividerAudit.a11y.chromium.spec.ts +280 -0
  212. package/src/DropdownMenu/__tests__/Menu.a11y.chromium.spec.ts +145 -0
  213. package/src/DropdownMenu/__tests__/Menu.a11y.known-failures.ts +26 -0
  214. package/src/DropdownMenu/__tests__/Menu.a11y.renders.tsx +65 -0
  215. package/src/DropdownMenu/__tests__/Menu.a11y.states.ts +138 -0
  216. package/src/DropdownMenu/__tests__/Menu.a11y.test.tsx +136 -0
  217. package/src/EmptyState/EmptyState.doc.mjs +3 -2
  218. package/src/EmptyState/EmptyState.spec.md +189 -0
  219. package/src/EmptyState/EmptyState.test.tsx +74 -3
  220. package/src/EmptyState/EmptyState.tsx +4 -8
  221. package/src/Field/Field.doc.mjs +14 -8
  222. package/src/Field/Field.spec.md +217 -0
  223. package/src/Field/Field.test.tsx +60 -2
  224. package/src/Field/Field.tsx +17 -8
  225. package/src/Grid/GridSpan.tsx +3 -0
  226. package/src/Heading/Heading.test.tsx +57 -0
  227. package/src/Heading/Heading.tsx +6 -2
  228. package/src/HoverCard/HoverCard.test.tsx +58 -0
  229. package/src/HoverCard/useHoverCard.tsx +10 -2
  230. package/src/IconButton/IconButton.doc.mjs +2 -2
  231. package/src/IconButton/IconButton.test.tsx +3 -1
  232. package/src/Item/Item.doc.mjs +12 -8
  233. package/src/Item/Item.spec.md +226 -0
  234. package/src/Item/Item.test.tsx +61 -0
  235. package/src/Item/Item.tsx +40 -4
  236. package/src/Layer/layerViewportInset.source-build.test.mjs +90 -0
  237. package/src/Layer/layerViewportInset.stylex.ts +6 -2
  238. package/src/Layer/layerViewportInset.test.ts +1 -0
  239. package/src/Layer/useLayer.tsx +24 -13
  240. package/src/Layer/useLayerDismissal.ts +1 -1
  241. package/src/Layout/container.stylex.ts +6 -6
  242. package/src/List/List.doc.mjs +14 -1
  243. package/src/List/List.spec.md +129 -77
  244. package/src/List/List.test.tsx +163 -94
  245. package/src/List/List.tsx +74 -18
  246. package/src/List/ListContext.tsx +9 -3
  247. package/src/List/ListItem.doc.mjs +24 -5
  248. package/src/List/ListItem.tsx +56 -27
  249. package/src/List/index.ts +1 -1
  250. package/src/Markdown/Markdown.tsx +14 -1
  251. package/src/Markdown/plugins/frontmatter.test.tsx +25 -0
  252. package/src/Markdown/plugins/frontmatter.ts +7 -3
  253. package/src/MetadataList/MetadataList.doc.mjs +2 -2
  254. package/src/MetadataList/MetadataList.test.tsx +38 -0
  255. package/src/MetadataList/MetadataList.tsx +8 -1
  256. package/src/MetadataList/MetadataListContext.tsx +1 -1
  257. package/src/MultiSelector/MultiSelector.test.tsx +159 -0
  258. package/src/MultiSelector/MultiSelector.tsx +40 -7
  259. package/src/MultiSelector/OptionActions.a11y.chromium.spec.ts +112 -2
  260. package/src/MultiSelector/hooks.ts +3 -1
  261. package/src/MultiSelector/useMultiCombobox.doc.mjs +267 -0
  262. package/src/Pagination/Pagination.doc.mjs +7 -7
  263. package/src/Pagination/Pagination.spec.md +9 -9
  264. package/src/Pagination/Pagination.test.tsx +242 -0
  265. package/src/Pagination/Pagination.tsx +27 -11
  266. package/src/Popover/Popover.test.tsx +2 -2
  267. package/src/Popover/Popover.tsx +4 -4
  268. package/src/PowerSearch/PowerSearch.test.tsx +85 -0
  269. package/src/PowerSearch/PowerSearch.tsx +2 -2
  270. package/src/RadioList/RadioList.fr10.test.tsx +45 -2
  271. package/src/RadioList/RadioList.test.tsx +22 -0
  272. package/src/RadioList/RadioList.tsx +4 -1
  273. package/src/RadioList/RadioListItem.doc.mjs +16 -0
  274. package/src/RadioList/RadioListItem.tsx +10 -1
  275. package/src/Selector/hooks.ts +6 -2
  276. package/src/Selector/useCombobox.doc.mjs +237 -0
  277. package/src/SideNav/SideNav.test.tsx +179 -0
  278. package/src/SideNav/SideNavItem.doc.mjs +2 -2
  279. package/src/SideNav/SideNavItem.tsx +96 -17
  280. package/src/Slider/Slider.spec.md +39 -18
  281. package/src/Switch/Switch.test.tsx +49 -0
  282. package/src/Switch/Switch.tsx +11 -0
  283. package/src/TabList/TabList.spec.md +57 -44
  284. package/src/TabList/TabList.test.tsx +67 -0
  285. package/src/TabList/TabMenu.doc.mjs +4 -3
  286. package/src/TabList/TabMenu.tsx +7 -4
  287. package/src/Text/Text.test.tsx +57 -0
  288. package/src/Text/Text.tsx +6 -2
  289. package/src/Text/useTruncation.test.tsx +69 -0
  290. package/src/Text/useTruncation.ts +16 -5
  291. package/src/TimeInput/TimeInput.fr10.test.tsx +15 -0
  292. package/src/ToggleButton/ToggleButton.test.tsx +95 -0
  293. package/src/ToggleButton/ToggleButton.tsx +12 -1
  294. package/src/Tokenizer/Tokenizer.test.tsx +54 -1
  295. package/src/Tooltip/Tooltip.test.tsx +31 -0
  296. package/src/Tooltip/useTooltip.test.tsx +13 -0
  297. package/src/Tooltip/useTooltip.tsx +11 -0
  298. package/src/TopNav/TopNavMegaMenu.test.tsx +22 -0
  299. package/src/TopNav/TopNavMegaMenu.tsx +7 -3
  300. package/src/TopNav/TopNavMenu.test.tsx +19 -0
  301. package/src/TopNav/TopNavMenu.tsx +11 -3
  302. package/src/TreeList/TreeList.doc.mjs +1 -0
  303. package/src/TreeList/TreeList.spec.md +29 -17
  304. package/src/TreeList/TreeList.test.tsx +327 -6
  305. package/src/TreeList/TreeList.tsx +5 -1
  306. package/src/TreeList/TreeListBranches.tsx +6 -0
  307. package/src/TreeList/TreeListItem.tsx +88 -13
  308. package/src/Typeahead/BaseTypeahead.tsx +26 -0
  309. package/src/Typeahead/Typeahead.test.tsx +260 -2
  310. package/src/docPropLiterals.test.ts +36 -10
  311. package/src/hooks/useEntryAnimation.doc.mjs +4 -4
  312. package/src/hooks/useEntryAnimation.ssr.test.tsx +143 -0
  313. package/src/hooks/useEntryAnimation.ts +15 -16
  314. package/src/hooks/useIsHydrating.test.tsx +70 -0
  315. package/src/hooks/useIsHydrating.ts +31 -0
  316. package/src/hooks/useMenuHover.ts +18 -13
  317. package/src/i18n/InternationalizationProvider.doc.mjs +8 -0
  318. package/src/i18n/InternationalizationProvider.spec.md +252 -0
  319. package/src/i18n/__tests__/InternationalizationProvider.test.tsx +128 -0
  320. package/src/i18n/generated-locales/en.generated.ts +121 -0
  321. package/src/i18n/generated-locales/pseudo.generated.ts +121 -0
  322. package/src/i18n/useTranslator.test.tsx +37 -0
  323. package/src/index.ts +1 -0
  324. package/src/reset.css +0 -29
  325. package/src/theme/derivedVarRegistry.test.ts +7 -3
  326. package/src/theme/derivedVarRegistry.ts +0 -3
  327. package/src/utils/focusOutline.stylex.ts +4 -0
  328. package/src/utils/plainDate.test.ts +37 -1
  329. package/src/utils/plainDate.ts +15 -5
  330. package/src/utils/sharedMutationObserver.test.ts +53 -0
  331. package/src/utils/sharedMutationObserver.ts +78 -0
@@ -0,0 +1,297 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file DrawerHeaderAudit.a11y.chromium.spec.ts
5
+ * @input Exact-head Core/DrawerHeader Storybook fixture in light/dark, neutral/custom themes, LTR/RTL, wide/narrow, rest/focus states
6
+ * @output Bounded screenshots and fail-closed sensor receipts for the DrawerHeader whole-component audit
7
+ * @position Component-owned Night Watch browser evidence; CI runs it only for a DrawerHeader component-audit pull request
8
+ */
9
+
10
+ import {createHash} from 'node:crypto';
11
+ import {execFileSync} from 'node:child_process';
12
+ import * as fs from 'node:fs';
13
+ import * as path from 'node:path';
14
+ import {expect, test, type Page} from '@playwright/test';
15
+ import {holdMotionStill} from '@astryxdesign/a11y-spec/chromium';
16
+ import {
17
+ DEFAULT_STORYBOOK_DIR,
18
+ serveStorybook,
19
+ type StaticServer,
20
+ } from '@astryxdesign/a11y-spec/storybook';
21
+
22
+ const OUTPUT = path.resolve('test-results/drawer-header-audit-evidence');
23
+ const WIDE = {width: 1024, height: 768};
24
+ const NARROW = {width: 320, height: 720};
25
+
26
+ type Mode = 'light' | 'dark';
27
+ type Direction = 'ltr' | 'rtl';
28
+ type Theme = 'neutral' | 'butter';
29
+ type State = 'rest' | 'focus';
30
+
31
+ type Case = {
32
+ key: string;
33
+ story: 'audit' | 'audit-without-close';
34
+ hasClose: boolean;
35
+ mode: Mode;
36
+ direction: Direction;
37
+ theme: Theme;
38
+ state: State;
39
+ viewport: {width: number; height: number};
40
+ };
41
+
42
+ const cases: Case[] = [];
43
+ function addPair(
44
+ key: string,
45
+ options: Partial<Omit<Case, 'key' | 'mode'>> = {},
46
+ ) {
47
+ for (const mode of ['light', 'dark'] as const) {
48
+ cases.push({
49
+ key: `${key}-${mode}`,
50
+ story: options.story ?? 'audit',
51
+ hasClose: options.hasClose ?? true,
52
+ mode,
53
+ direction: options.direction ?? 'ltr',
54
+ theme: options.theme ?? 'neutral',
55
+ state: options.state ?? 'rest',
56
+ viewport: options.viewport ?? WIDE,
57
+ });
58
+ }
59
+ }
60
+
61
+ addPair('rest');
62
+ addPair('rest-without-close', {
63
+ story: 'audit-without-close',
64
+ hasClose: false,
65
+ });
66
+ addPair('rest-custom-theme', {theme: 'butter'});
67
+ addPair('rest-rtl', {direction: 'rtl'});
68
+ addPair('rest-narrow', {viewport: NARROW});
69
+ addPair('focus', {state: 'focus'});
70
+
71
+ test.describe.configure({mode: 'serial', retries: 0});
72
+ let server: StaticServer;
73
+ let checkoutSha: string;
74
+ let storybookSha: string;
75
+
76
+ function storyUrl(item: Case) {
77
+ return `${server.origin}/iframe.html?id=core-drawerheader--${item.story}&viewMode=story&globals=colorMode:${item.mode};astryxTheme:${item.theme};direction:${item.direction}`;
78
+ }
79
+
80
+ async function settle(page: Page) {
81
+ await holdMotionStill(page);
82
+ await page.evaluate(async () => document.fonts.ready);
83
+ await page.waitForTimeout(40);
84
+ }
85
+
86
+ test.beforeAll(async () => {
87
+ fs.rmSync(OUTPUT, {recursive: true, force: true});
88
+ fs.mkdirSync(OUTPUT, {recursive: true});
89
+ checkoutSha = execFileSync('git', ['rev-parse', 'HEAD'], {
90
+ encoding: 'utf8',
91
+ }).trim();
92
+ const expected = process.env.ASTRYX_HEAD_SHA;
93
+ if (expected && expected !== checkoutSha) {
94
+ throw new Error(
95
+ `checkout ${checkoutSha} does not match expected head ${expected}`,
96
+ );
97
+ }
98
+ server = await serveStorybook(
99
+ process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
100
+ );
101
+ const response = await fetch(`${server.origin}/astryx-build-sha.txt`);
102
+ if (!response.ok) {
103
+ throw new Error(`Storybook build has no SHA stamp (${response.status})`);
104
+ }
105
+ storybookSha = (await response.text()).trim();
106
+ if (storybookSha !== checkoutSha) {
107
+ throw new Error(
108
+ `Storybook ${storybookSha} does not match checkout ${checkoutSha}`,
109
+ );
110
+ }
111
+ });
112
+
113
+ test.afterAll(async () => {
114
+ await server?.close();
115
+ });
116
+
117
+ for (const item of cases) {
118
+ test(item.key, async ({browser}) => {
119
+ const context = await browser.newContext({
120
+ viewport: item.viewport,
121
+ colorScheme: item.mode,
122
+ reducedMotion: 'reduce',
123
+ });
124
+ const page = await context.newPage();
125
+ const pageErrors: string[] = [];
126
+ page.on('pageerror', error => pageErrors.push(String(error)));
127
+ await page.goto(storyUrl(item), {waitUntil: 'load'});
128
+ await page.locator('#storybook-root').waitFor();
129
+
130
+ const drawer = page.locator('dialog.astryx-drawer:visible');
131
+ await expect(drawer).toHaveCount(1);
132
+ await settle(page);
133
+
134
+ const close = page.getByRole('button', {name: 'Close', exact: true});
135
+ if (item.hasClose) {
136
+ await expect(close).toBeVisible();
137
+ } else {
138
+ await expect(close).toHaveCount(0);
139
+ }
140
+ if (item.state === 'focus') {
141
+ await close.focus();
142
+ await page.keyboard.press('Tab');
143
+ await expect(close).toBeFocused();
144
+ expect(
145
+ await close.evaluate(element => element.matches(':focus-visible')),
146
+ ).toBe(true);
147
+ }
148
+
149
+ const header = page.locator('.astryx-drawer-header');
150
+ const startSlot = page.locator('.astryx-drawer-header-start-content');
151
+ const titleBlock = page.locator('.astryx-drawer-header-title-block');
152
+ const endSlot = page.locator('.astryx-drawer-header-end-content');
153
+ await expect(header).toHaveCount(1);
154
+ await expect(startSlot).toHaveCount(1);
155
+ await expect(titleBlock).toHaveCount(1);
156
+ await expect(endSlot).toHaveCount(1);
157
+ await expect(startSlot).toHaveText('0');
158
+ await expect(endSlot).toContainText('0');
159
+ await expect(
160
+ page.getByRole('heading', {
161
+ level: 2,
162
+ name: 'Deployment health for web-prod-04',
163
+ }),
164
+ ).toBeVisible();
165
+ await expect(
166
+ page.getByText('us-east-1, deployed 12 minutes ago', {exact: true}),
167
+ ).toBeVisible();
168
+
169
+ const [headerBox, startBox, titleBox, endBox, closeBox] = await Promise.all(
170
+ [
171
+ header.boundingBox(),
172
+ startSlot.boundingBox(),
173
+ titleBlock.boundingBox(),
174
+ endSlot.boundingBox(),
175
+ item.hasClose ? close.boundingBox() : Promise.resolve(null),
176
+ ],
177
+ );
178
+ if (
179
+ headerBox == null ||
180
+ startBox == null ||
181
+ titleBox == null ||
182
+ endBox == null ||
183
+ (item.hasClose && closeBox == null)
184
+ ) {
185
+ throw new Error(
186
+ `${item.key}: a visible DrawerHeader part has no geometry`,
187
+ );
188
+ }
189
+ const observed = await page.evaluate(() => ({
190
+ theme: document
191
+ .querySelector('[data-astryx-theme]')
192
+ ?.getAttribute('data-astryx-theme'),
193
+ mode: document.documentElement.getAttribute('data-theme'),
194
+ direction: getComputedStyle(
195
+ document.querySelector('.astryx-drawer-header') as Element,
196
+ ).direction,
197
+ viewport: {width: innerWidth, height: innerHeight},
198
+ reducedMotion: matchMedia('(prefers-reduced-motion: reduce)').matches,
199
+ horizontalOverflow: document.documentElement.scrollWidth > innerWidth + 1,
200
+ fonts: document.fonts.status,
201
+ }));
202
+
203
+ expect(pageErrors).toEqual([]);
204
+ expect(observed.theme).toBe(item.theme);
205
+ expect(observed.mode).toBe(item.mode);
206
+ expect(observed.direction).toBe(item.direction);
207
+ expect(observed.viewport).toEqual(item.viewport);
208
+ expect(observed.reducedMotion).toBe(true);
209
+ expect(observed.horizontalOverflow).toBe(false);
210
+ expect(observed.fonts).toBe('loaded');
211
+ expect(headerBox.width).toBeGreaterThan(0);
212
+ if (closeBox != null) {
213
+ expect(closeBox.width).toBeGreaterThanOrEqual(24);
214
+ expect(closeBox.height).toBeGreaterThanOrEqual(24);
215
+ }
216
+ if (item.direction === 'ltr') {
217
+ expect(startBox.x).toBeLessThan(titleBox.x);
218
+ expect(titleBox.x).toBeLessThan(endBox.x);
219
+ } else {
220
+ expect(startBox.x).toBeGreaterThan(titleBox.x);
221
+ expect(titleBox.x).toBeGreaterThan(endBox.x);
222
+ }
223
+
224
+ const png = await page.screenshot({animations: 'disabled'});
225
+ expect(png.length).toBeGreaterThan(100);
226
+ const image = `${item.key}.png`;
227
+ const receipt = {
228
+ schemaVersion: 1,
229
+ expected: {
230
+ build: checkoutSha,
231
+ storyId: `core-drawerheader--${item.story}`,
232
+ mode: item.mode,
233
+ direction: item.direction,
234
+ theme: item.theme,
235
+ state: item.state,
236
+ viewport: item.viewport,
237
+ startContent: '0',
238
+ endContent: '0',
239
+ titleRole: 'heading',
240
+ titleLevel: 2,
241
+ closeName: item.hasClose ? 'Close' : null,
242
+ },
243
+ observed: {
244
+ build: storybookSha,
245
+ ...observed,
246
+ geometry: {
247
+ header: headerBox,
248
+ startSlot: startBox,
249
+ titleBlock: titleBox,
250
+ endSlot: endBox,
251
+ close: closeBox,
252
+ },
253
+ closeFocused: item.hasClose
254
+ ? await close.evaluate(element => element === document.activeElement)
255
+ : false,
256
+ closeFocusVisible: item.hasClose
257
+ ? await close.evaluate(element => element.matches(':focus-visible'))
258
+ : false,
259
+ pageErrors,
260
+ },
261
+ image: {
262
+ file: image,
263
+ sha256: createHash('sha256').update(png).digest('hex'),
264
+ width: png.readUInt32BE(16),
265
+ height: png.readUInt32BE(20),
266
+ },
267
+ passed: true,
268
+ };
269
+ fs.writeFileSync(path.join(OUTPUT, image), png);
270
+ fs.writeFileSync(
271
+ path.join(OUTPUT, `${item.key}.sensors.json`),
272
+ `${JSON.stringify(receipt, null, 2)}\n`,
273
+ );
274
+ await context.close();
275
+ });
276
+ }
277
+
278
+ test('manifest is complete', async () => {
279
+ const receipts = fs
280
+ .readdirSync(OUTPUT)
281
+ .filter(file => file.endsWith('.sensors.json'));
282
+ expect(receipts).toHaveLength(cases.length);
283
+ const manifest = {
284
+ schemaVersion: 1,
285
+ component: 'core/DrawerHeader',
286
+ build: checkoutSha,
287
+ frames: cases.map(item => ({
288
+ key: item.key,
289
+ image: `${item.key}.png`,
290
+ receipt: `${item.key}.sensors.json`,
291
+ })),
292
+ };
293
+ fs.writeFileSync(
294
+ path.join(OUTPUT, 'manifest.json'),
295
+ `${JSON.stringify(manifest, null, 2)}\n`,
296
+ );
297
+ });
@@ -0,0 +1,13 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file index.ts
5
+ * @input Drawer.tsx, DrawerHeader.tsx
6
+ * @output Re-exports Drawer and DrawerHeader components and their prop types
7
+ * @position Core entry point for the Drawer directory (`@astryxdesign/core/Drawer`)
8
+ */
9
+
10
+ export {Drawer} from './Drawer';
11
+ export type {DrawerProps} from './Drawer';
12
+ export {DrawerHeader} from './DrawerHeader';
13
+ export type {DrawerHeaderProps} from './DrawerHeader';
@@ -0,0 +1,284 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file useDrawerDialogPresence.ts
7
+ * @input Controlled open state, modal mode, dialog ref, and rendered-state setter
8
+ * @output Coordinates native top-layer presence, exit timing, focus restoration, and unmount cleanup
9
+ * @position Drawer-internal hook; consumed only by Drawer.tsx
10
+ *
11
+ * The drawer has two independent notions of presence:
12
+ * - React's rendered state keeps the panel visible for its CSS exit.
13
+ * - Native `showModal()` or `showPopover()` state keeps it in the browser top
14
+ * layer.
15
+ *
16
+ * Their close ordering is a browser-visible invariant: the panel must finish
17
+ * its exit, then leave the active native host and hide in the same task. If the
18
+ * hide lands a frame later, a transformed ancestor becomes the containing block
19
+ * for the now-non-top-layer `position: fixed` panel and it paints back inside
20
+ * the page for one frame.
21
+ *
22
+ * SYNC: When modified, update:
23
+ * - /packages/core/src/Drawer/Drawer.test.tsx
24
+ * - /.github/scripts/modal-close-visibility.js
25
+ */
26
+
27
+ import {
28
+ useEffect,
29
+ useRef,
30
+ type Dispatch,
31
+ type RefObject,
32
+ type SetStateAction,
33
+ } from 'react';
34
+ import {flushSync} from 'react-dom';
35
+
36
+ /** Slack past the computed transition before the backstop gives up waiting. */
37
+ const EXIT_BACKSTOP_BUFFER_MS = 50;
38
+
39
+ /**
40
+ * Hold used when the transition duration cannot be read — an unresolved
41
+ * `var()` outside a real browser. Picking a fixed number would otherwise make
42
+ * an assumption about the consumer's theme.
43
+ */
44
+ const EXIT_FALLBACK_MS = 250;
45
+
46
+ type UseDrawerDialogPresenceOptions = {
47
+ dialogRef: RefObject<HTMLDialogElement | null>;
48
+ isOpen: boolean;
49
+ isModal: boolean;
50
+ setIsRendered: Dispatch<SetStateAction<boolean>>;
51
+ };
52
+
53
+ function isPopoverOpen(dialog: HTMLDialogElement): boolean {
54
+ try {
55
+ return dialog.matches(':popover-open');
56
+ } catch {
57
+ // jsdom and pre-Popover browsers do not recognize :popover-open.
58
+ return false;
59
+ }
60
+ }
61
+
62
+ function isDrawerHostOpen(
63
+ dialog: HTMLDialogElement,
64
+ isModal: boolean,
65
+ ): boolean {
66
+ return isModal ? dialog.open : isPopoverOpen(dialog) || dialog.open;
67
+ }
68
+
69
+ function showDrawerHost(dialog: HTMLDialogElement, isModal: boolean): void {
70
+ if (isModal) {
71
+ dialog.showModal();
72
+ } else if (typeof dialog.showPopover === 'function') {
73
+ dialog.showPopover();
74
+ } else {
75
+ // Reduced fallback for browsers below the Popover API support floor.
76
+ dialog.show();
77
+ }
78
+ }
79
+
80
+ function dispatchDialogClose(dialog: HTMLDialogElement): void {
81
+ const EventConstructor = dialog.ownerDocument.defaultView?.Event ?? Event;
82
+ dialog.dispatchEvent(new EventConstructor('close'));
83
+ }
84
+
85
+ /**
86
+ * Leaves the active native host. Returns whether the caller owes a synthetic
87
+ * `close` dispatch: `dialog.close()` used to power the non-modal path, and
88
+ * consumers observe its native `close` event through refs/onClose. Popover
89
+ * dismissal has no equivalent event, so that public DOM contract is preserved
90
+ * explicitly — but by the caller, not here. Native `close` is queued as a
91
+ * task, so a consumer's listener always ran after this hook's own focus
92
+ * restore; the synthetic event must keep that ordering (and fire exactly once
93
+ * per exit, including the unmount path).
94
+ */
95
+ function hideDrawerHost(dialog: HTMLDialogElement, isModal: boolean): boolean {
96
+ if (!isModal && typeof dialog.hidePopover === 'function') {
97
+ dialog.hidePopover();
98
+ return true;
99
+ }
100
+ if (dialog.open) {
101
+ dialog.close();
102
+ }
103
+ return false;
104
+ }
105
+
106
+ /**
107
+ * Coordinates the native dialog and React-rendered presence for Drawer.
108
+ *
109
+ * Opening captures the trigger, enters the modal-dialog or manual-popover host,
110
+ * and honours the component's `data-autofocus` contract. Closing waits for the
111
+ * actual transform transition (with a computed-duration backstop), then leaves
112
+ * the native host and synchronously hides the panel before the browser can paint
113
+ * it outside the top layer, restores focus to the captured trigger, and only
114
+ * then dispatches the popover host's synthetic `close`. Unmount cleanup closes
115
+ * a host left open by React Activity or a removed subtree.
116
+ */
117
+ export function useDrawerDialogPresence({
118
+ dialogRef,
119
+ isOpen,
120
+ isModal,
121
+ setIsRendered,
122
+ }: UseDrawerDialogPresenceOptions): void {
123
+ // Element focused when the drawer opened — restored on close.
124
+ const triggerElementRef = useRef<HTMLElement | null>(null);
125
+
126
+ useEffect(() => {
127
+ const dialog = dialogRef.current;
128
+ if (!dialog) {
129
+ return;
130
+ }
131
+
132
+ if (isOpen) {
133
+ if (!isDrawerHostOpen(dialog, isModal)) {
134
+ triggerElementRef.current =
135
+ document.activeElement as HTMLElement | null;
136
+ showDrawerHost(dialog, isModal);
137
+ // React's autoFocus calls .focus() during commit, before the native host
138
+ // is visible, so it silently fails — honour data-autofocus instead (same
139
+ // contract as Dialog).
140
+ dialog.querySelector<HTMLElement>('[data-autofocus]')?.focus();
141
+ }
142
+ return;
143
+ }
144
+
145
+ if (!isDrawerHostOpen(dialog, isModal)) {
146
+ return;
147
+ }
148
+
149
+ return waitForDrawerExit(dialog, () => {
150
+ const owesCloseEvent = hideDrawerHost(dialog, isModal);
151
+ // flushSync, not a plain setState: React's default scheduling can land
152
+ // the commit after the next paint, and that one frame is exactly the
153
+ // bug — the panel paints outside the top layer. Both happen in this
154
+ // task, so the browser never gets to paint between them.
155
+ // eslint-disable-next-line @eslint-react/dom-no-flush-sync -- the panel must stop painting in the task the native host closes
156
+ flushSync(() => {
157
+ setIsRendered(false);
158
+ });
159
+ // Return focus after leaving the native host: a modal dialog makes the
160
+ // rest of the document inert, so focusing earlier silently fails.
161
+ triggerElementRef.current?.focus();
162
+ triggerElementRef.current = null;
163
+ // The synthetic popover close fires after the focus restore above,
164
+ // mirroring the task-queued native event: a consumer close listener
165
+ // that retargets focus (master-detail row switching) gets the last word.
166
+ if (owesCloseEvent) {
167
+ dispatchDialogClose(dialog);
168
+ }
169
+ });
170
+ }, [dialogRef, isModal, isOpen, setIsRendered]);
171
+
172
+ // Leave the active native host on unmount. When the drawer is mounted inside
173
+ // an <Activity> that flips to mode="hidden", React runs effect cleanups (with
174
+ // stale isOpen) instead of re-running the effect with isOpen=false. Leaving
175
+ // the host active would strand the top-layer surface and skip the next open.
176
+ // This is deliberately separate from the open/close effect: putting it in
177
+ // that cleanup would cut off every delayed slide-out.
178
+ useEffect(() => {
179
+ const dialog = dialogRef.current;
180
+ return () => {
181
+ if (dialog && isDrawerHostOpen(dialog, isModal)) {
182
+ if (hideDrawerHost(dialog, isModal)) {
183
+ // No focus restore on this path — the drawer is being torn down, so
184
+ // the owed synthetic close fires immediately (still exactly once).
185
+ dispatchDialogClose(dialog);
186
+ }
187
+ }
188
+ };
189
+ }, [dialogRef, isModal]);
190
+ }
191
+
192
+ /**
193
+ * Wait for the panel's transform transition, with a computed-duration
194
+ * backstop. Mirrors BottomSheetPanel.waitForTransition: the native event is
195
+ * authoritative, and the timeout only prevents a lost event from stranding an
196
+ * open dialog.
197
+ */
198
+ function waitForDrawerExit(
199
+ element: HTMLDialogElement,
200
+ complete: () => void,
201
+ ): () => void {
202
+ let done = false;
203
+ let timer: ReturnType<typeof setTimeout> | null = null;
204
+
205
+ const finish = () => {
206
+ if (done) {
207
+ return;
208
+ }
209
+ done = true;
210
+ if (timer != null) {
211
+ clearTimeout(timer);
212
+ }
213
+ element.removeEventListener('transitionend', handleTransitionEnd);
214
+ element.removeEventListener('transitioncancel', handleTransitionEnd);
215
+ complete();
216
+ };
217
+
218
+ const handleTransitionEnd = (event: TransitionEvent) => {
219
+ if (event.target === element && event.propertyName === 'transform') {
220
+ finish();
221
+ }
222
+ };
223
+
224
+ element.addEventListener('transitionend', handleTransitionEnd);
225
+ element.addEventListener('transitioncancel', handleTransitionEnd);
226
+
227
+ const transitionMs = readTransformTransitionMs(element);
228
+ timer = setTimeout(
229
+ finish,
230
+ (transitionMs ?? EXIT_FALLBACK_MS) + EXIT_BACKSTOP_BUFFER_MS,
231
+ );
232
+
233
+ return () => {
234
+ done = true;
235
+ if (timer != null) {
236
+ clearTimeout(timer);
237
+ }
238
+ element.removeEventListener('transitionend', handleTransitionEnd);
239
+ element.removeEventListener('transitioncancel', handleTransitionEnd);
240
+ };
241
+ }
242
+
243
+ /** Read the transform transition's duration + delay from computed CSS. */
244
+ function readTransformTransitionMs(element: HTMLElement): number | null {
245
+ const computed = window.getComputedStyle(element);
246
+ const properties = computed.transitionProperty
247
+ .split(',')
248
+ .map(value => value.trim());
249
+ const durations = parseTimes(computed.transitionDuration);
250
+ const delays = parseTimes(computed.transitionDelay);
251
+
252
+ if (
253
+ properties.length === 0 ||
254
+ durations.length === 0 ||
255
+ delays.length === 0 ||
256
+ durations.includes(null) ||
257
+ delays.includes(null)
258
+ ) {
259
+ return null;
260
+ }
261
+
262
+ return properties.reduce((longest, property, index) => {
263
+ if (property !== 'transform' && property !== 'all') {
264
+ return longest;
265
+ }
266
+ const duration = durations[index % durations.length];
267
+ const delay = delays[index % delays.length];
268
+ return Math.max(longest, (duration ?? 0) + (delay ?? 0));
269
+ }, 0);
270
+ }
271
+
272
+ function parseTimes(value: string): (number | null)[] {
273
+ return value.split(',').map(part => {
274
+ const trimmed = part.trim();
275
+ const time = Number.parseFloat(trimmed);
276
+ if (!Number.isFinite(time)) {
277
+ return null;
278
+ }
279
+ if (trimmed.endsWith('ms')) {
280
+ return time;
281
+ }
282
+ return trimmed.endsWith('s') ? time * 1000 : null;
283
+ });
284
+ }
@@ -13,7 +13,7 @@ import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
13
13
  import {readFileSync} from 'node:fs';
14
14
  import {render, screen, fireEvent, waitFor, act} from '@testing-library/react';
15
15
  import userEvent from '@testing-library/user-event';
16
- import {useState} from 'react';
16
+ import {createRef, useState} from 'react';
17
17
  import * as stylex from '@stylexjs/stylex';
18
18
  import {DropdownMenu, type DropdownMenuTriggerProps} from './DropdownMenu';
19
19
  import {DropdownMenuItem} from './DropdownMenuItem';
@@ -24,6 +24,7 @@ import {Divider} from '../Divider';
24
24
  import {rtlStyles} from '../utils';
25
25
  import {__resetInteractionModalityForTest} from '../utils/interactionModality';
26
26
  import {focusOutlineStyles} from '../utils/focusOutline.stylex';
27
+ import {spacingVars} from '../theme/tokens.stylex';
27
28
 
28
29
  // Mock showPopover and hidePopover methods since they're not implemented in jsdom
29
30
  beforeEach(() => {
@@ -1352,18 +1353,6 @@ describe('DropdownMenu items', () => {
1352
1353
  );
1353
1354
  expect(handleClick).not.toHaveBeenCalled();
1354
1355
  });
1355
-
1356
- it('has aria-disabled when disabled', () => {
1357
- render(
1358
- <DropdownMenu
1359
- button={{label: 'Actions'}}
1360
- items={[{label: 'Edit', isDisabled: true}]}
1361
- />,
1362
- );
1363
- expect(
1364
- screen.getByRole('menuitem', {name: 'Edit', hidden: true}),
1365
- ).toHaveAttribute('aria-disabled', 'true');
1366
- });
1367
1356
  });
1368
1357
 
1369
1358
  describe('DropdownMenu sections', () => {
@@ -2520,6 +2509,66 @@ describe('DropdownMenu data/compound parity', () => {
2520
2509
  expect(fromCompound).toHaveClass('astryx-dropdown-menu-divider');
2521
2510
  });
2522
2511
 
2512
+ it('forwards host attributes to a compound divider while keeping its role', () => {
2513
+ render(
2514
+ <DropdownMenu button={{label: 'Actions'}}>
2515
+ <DropdownMenuItem label="Edit" />
2516
+ <DropdownMenuDivider
2517
+ id="menu-sep"
2518
+ data-testid="menu-separator"
2519
+ aria-label="Danger zone"
2520
+ // @ts-expect-error role is component-owned
2521
+ role="presentation"
2522
+ />
2523
+ <DropdownMenuItem label="Delete" />
2524
+ </DropdownMenu>,
2525
+ );
2526
+ const separator = screen.getByTestId('menu-separator');
2527
+ expect(separator).toHaveAttribute('id', 'menu-sep');
2528
+ expect(separator).toHaveAttribute('aria-label', 'Danger zone');
2529
+ expect(separator).toHaveAttribute('role', 'separator');
2530
+ expect(separator).toHaveClass('astryx-dropdown-menu-divider');
2531
+ });
2532
+
2533
+ it('composes the complete public root surface on the separator', () => {
2534
+ const ref = createRef<HTMLDivElement>();
2535
+ const onClick = vi.fn();
2536
+ const customStyles = stylex.create({
2537
+ divider: {marginBlock: spacingVars['--spacing-5']},
2538
+ });
2539
+ const customClass = stylex
2540
+ .props(customStyles.divider)
2541
+ .className!.split(' ')
2542
+ .pop()!;
2543
+
2544
+ render(
2545
+ <DropdownMenu button={{label: 'Actions'}}>
2546
+ <DropdownMenuItem label="Edit" />
2547
+ <DropdownMenuDivider
2548
+ ref={ref}
2549
+ data-testid="menu-separator"
2550
+ className="consumer-divider"
2551
+ style={{backgroundColor: 'rgb(1, 2, 3)'}}
2552
+ xstyle={customStyles.divider}
2553
+ onClick={onClick}
2554
+ />
2555
+ <DropdownMenuItem label="Delete" />
2556
+ </DropdownMenu>,
2557
+ );
2558
+
2559
+ const separator = screen.getByTestId('menu-separator');
2560
+ expect(ref.current).toBe(separator);
2561
+ expect(separator).toHaveClass(
2562
+ 'astryx-dropdown-menu-divider',
2563
+ 'astryx-divider',
2564
+ 'consumer-divider',
2565
+ customClass,
2566
+ );
2567
+ expect(separator).toHaveStyle({backgroundColor: 'rgb(1, 2, 3)'});
2568
+ fireEvent.click(separator);
2569
+ expect(onClick).toHaveBeenCalledTimes(1);
2570
+ });
2571
+
2523
2572
  it('skips a compound divider in the arrow-key order', async () => {
2524
2573
  const user = userEvent.setup();
2525
2574
  render(