@astryxdesign/core 0.7.5 → 0.7.6-canary.bb18c81

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 (353) hide show
  1. package/dist/AlertDialog/AlertDialog.d.ts +1 -1
  2. package/dist/AlertDialog/AlertDialog.d.ts.map +1 -1
  3. package/dist/AlertDialog/AlertDialog.js +5 -5
  4. package/dist/Avatar/Avatar.d.ts.map +1 -1
  5. package/dist/Avatar/Avatar.js +12 -3
  6. package/dist/BottomSheet/BottomSheet.js +1 -1
  7. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  8. package/dist/BottomSheet/BottomSheetPanel.js +2 -1
  9. package/dist/BottomSheet/useSheetGestures.d.ts +12 -0
  10. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
  11. package/dist/BottomSheet/useSheetGestures.js +30 -3
  12. package/dist/Button/Button.js +4 -4
  13. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  14. package/dist/CheckboxInput/CheckboxInput.js +6 -5
  15. package/dist/DateInput/DateInput.d.ts.map +1 -1
  16. package/dist/DateInput/DateInput.js +4 -1
  17. package/dist/DateInput/MonthScroller.d.ts.map +1 -1
  18. package/dist/DateInput/MonthScroller.js +101 -15
  19. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  20. package/dist/DateRangeInput/DateRangeInput.js +1 -1
  21. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  22. package/dist/DateTimeInput/DateTimeInput.js +4 -1
  23. package/dist/Dialog/Dialog.d.ts.map +1 -1
  24. package/dist/Dialog/Dialog.js +34 -9
  25. package/dist/Dialog/DialogHeader.d.ts.map +1 -1
  26. package/dist/Dialog/DialogHeader.js +5 -4
  27. package/dist/Divider/Divider.d.ts.map +1 -1
  28. package/dist/Divider/Divider.js +2 -1
  29. package/dist/Drawer/Drawer.d.ts.map +1 -1
  30. package/dist/Drawer/Drawer.js +5 -0
  31. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  32. package/dist/DropdownMenu/DropdownMenu.js +4 -1
  33. package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts.map +1 -1
  34. package/dist/DropdownMenu/MenuBottomSheetActionList.js +6 -5
  35. package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts +10 -1
  36. package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts.map +1 -1
  37. package/dist/DropdownMenu/lazyMenuBottomSheet.js +16 -2
  38. package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -1
  39. package/dist/DropdownMenu/renderDropdownItems.js +21 -16
  40. package/dist/EmptyState/EmptyState.d.ts.map +1 -1
  41. package/dist/EmptyState/EmptyState.js +2 -1
  42. package/dist/Field/FieldLabel.d.ts +1 -1
  43. package/dist/Field/FieldLabel.d.ts.map +1 -1
  44. package/dist/Field/FieldLabel.js +4 -4
  45. package/dist/Field/InputClearButton.js +3 -3
  46. package/dist/Field/PanelSearchInput.js +2 -2
  47. package/dist/FieldStatus/FieldStatus.js +2 -2
  48. package/dist/FileInput/FileInput.d.ts.map +1 -1
  49. package/dist/FileInput/FileInput.js +7 -1
  50. package/dist/Heading/Heading.js +1 -1
  51. package/dist/Icon/Icon.js +1 -1
  52. package/dist/Indicator/CheckIndicator.d.ts.map +1 -1
  53. package/dist/Indicator/CheckIndicator.js +2 -1
  54. package/dist/Indicator/CheckboxIndicator.d.ts.map +1 -1
  55. package/dist/Indicator/CheckboxIndicator.js +3 -1
  56. package/dist/Indicator/RadioIndicator.d.ts.map +1 -1
  57. package/dist/Indicator/RadioIndicator.js +3 -1
  58. package/dist/Item/Item.d.ts.map +1 -1
  59. package/dist/Item/Item.js +2 -1
  60. package/dist/Item/ItemSwipeLayer.js +1 -1
  61. package/dist/Layout/Layout.js +1 -1
  62. package/dist/Layout/LayoutContent.js +1 -1
  63. package/dist/Layout/LayoutFooter.js +1 -1
  64. package/dist/Layout/LayoutHeader.js +1 -1
  65. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  66. package/dist/Lightbox/Lightbox.js +5 -0
  67. package/dist/Lightbox/LightboxViewport.a11y.browser.spec.js +5 -4
  68. package/dist/List/List.js +1 -1
  69. package/dist/List/ListItem.js +3 -3
  70. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  71. package/dist/MobileNav/MobileNav.js +7 -2
  72. package/dist/MultiSelector/MultiSelector.d.ts +1 -1
  73. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  74. package/dist/MultiSelector/MultiSelector.js +5 -2
  75. package/dist/NumberInput/NumberInput.d.ts +3 -1
  76. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  77. package/dist/NumberInput/NumberInput.js +3 -3
  78. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  79. package/dist/ProgressBar/ProgressBar.js +16 -12
  80. package/dist/Resizable/useResizable.d.ts.map +1 -1
  81. package/dist/Resizable/useResizable.js +28 -2
  82. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  83. package/dist/SegmentedControl/SegmentedControlItem.js +4 -1
  84. package/dist/Selector/useSelectorPresentation.d.ts +8 -1
  85. package/dist/Selector/useSelectorPresentation.d.ts.map +1 -1
  86. package/dist/Selector/useSelectorPresentation.js +20 -4
  87. package/dist/Slider/Slider.d.ts.map +1 -1
  88. package/dist/Slider/Slider.js +7 -1
  89. package/dist/Spinner/Spinner.js +2 -2
  90. package/dist/Stack/Stack.js +1 -1
  91. package/dist/Stack/StackItem.js +1 -1
  92. package/dist/TabList/TabList.d.ts.map +1 -1
  93. package/dist/TabList/TabList.js +16 -1
  94. package/dist/Table/plugins/columnResize/useTableColumnResize.d.ts.map +1 -1
  95. package/dist/Table/plugins/columnResize/useTableColumnResize.js +9 -1
  96. package/dist/Text/Text.js +1 -1
  97. package/dist/Text/useTruncation.d.ts.map +1 -1
  98. package/dist/Text/useTruncation.js +23 -5
  99. package/dist/TextArea/TextArea.d.ts +1 -1
  100. package/dist/TextArea/TextArea.d.ts.map +1 -1
  101. package/dist/TextArea/TextArea.js +13 -2
  102. package/dist/TextInput/TextInput.d.ts.map +1 -1
  103. package/dist/TextInput/TextInput.js +3 -3
  104. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  105. package/dist/Timestamp/Timestamp.js +2 -2
  106. package/dist/Timestamp/TimestampHoverCard.js +4 -4
  107. package/dist/Toast/ToastViewport.d.ts.map +1 -1
  108. package/dist/Toast/ToastViewport.js +82 -51
  109. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  110. package/dist/Tokenizer/Tokenizer.js +13 -2
  111. package/dist/Tokenizer/TokenizerBottomSheet.js +3 -3
  112. package/dist/Toolbar/Toolbar.d.ts.map +1 -1
  113. package/dist/Toolbar/Toolbar.js +2 -1
  114. package/dist/Typeahead/Typeahead.d.ts +1 -1
  115. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  116. package/dist/Typeahead/Typeahead.js +9 -1
  117. package/dist/astryx.css +14 -1
  118. package/dist/gestures/PinchGestureRecognizer.d.ts +30 -0
  119. package/dist/gestures/PinchGestureRecognizer.d.ts.map +1 -0
  120. package/dist/gestures/PinchGestureRecognizer.js +125 -0
  121. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts +70 -0
  122. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts.map +1 -0
  123. package/dist/gestures/ScreenEdgePanGestureRecognizer.js +220 -0
  124. package/dist/gestures/SwipeGestureRecognizer.d.ts +36 -0
  125. package/dist/gestures/SwipeGestureRecognizer.d.ts.map +1 -0
  126. package/dist/gestures/SwipeGestureRecognizer.js +145 -0
  127. package/dist/gestures/VelocityTracker.d.ts +6 -2
  128. package/dist/gestures/VelocityTracker.d.ts.map +1 -1
  129. package/dist/gestures/VelocityTracker.js +7 -3
  130. package/dist/gestures/types.d.ts +11 -7
  131. package/dist/gestures/types.d.ts.map +1 -1
  132. package/dist/gestures/types.js +22 -2
  133. package/dist/hooks/useAdaptivePresentation.d.ts.map +1 -1
  134. package/dist/hooks/useAdaptivePresentation.js +4 -1
  135. package/dist/hooks/useMenuHover.d.ts.map +1 -1
  136. package/dist/hooks/useMenuHover.js +12 -1
  137. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  138. package/dist/i18n/generated-locales/en.generated.js +3 -0
  139. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  140. package/dist/i18n/generated-locales/pseudo.generated.js +3 -0
  141. package/dist/motion/animator.d.ts +7 -1
  142. package/dist/motion/animator.d.ts.map +1 -1
  143. package/dist/motion/animator.js +119 -27
  144. package/dist/theme/defineTheme.js +1 -1
  145. package/dist/theme/domainTokens/domainTokenDefaults.d.ts +3 -0
  146. package/dist/theme/domainTokens/domainTokenDefaults.d.ts.map +1 -0
  147. package/dist/theme/domainTokens/domainTokenDefaults.js +19 -0
  148. package/dist/theme/domainTokens/index.d.ts +5 -4
  149. package/dist/theme/domainTokens/index.d.ts.map +1 -1
  150. package/dist/theme/domainTokens/index.js +5 -9
  151. package/dist/utils/focusReturn.d.ts +17 -1
  152. package/dist/utils/focusReturn.d.ts.map +1 -1
  153. package/dist/utils/focusReturn.js +11 -4
  154. package/dist/utils/getNodeText.d.ts +8 -0
  155. package/dist/utils/getNodeText.d.ts.map +1 -0
  156. package/dist/utils/getNodeText.js +37 -0
  157. package/dist/utils/interactionModality.d.ts +11 -2
  158. package/dist/utils/interactionModality.d.ts.map +1 -1
  159. package/dist/utils/interactionModality.js +58 -5
  160. package/dist/utils/presentation.d.ts +50 -0
  161. package/dist/utils/presentation.d.ts.map +1 -0
  162. package/dist/utils/presentation.js +125 -0
  163. package/dist/utils/touchPresentation.d.ts.map +1 -1
  164. package/dist/utils/touchPresentation.js +5 -3
  165. package/dist/utils/touchPresentation.stylex.d.ts +15 -0
  166. package/dist/utils/touchPresentation.stylex.d.ts.map +1 -0
  167. package/dist/utils/touchPresentation.stylex.js +33 -0
  168. package/locales/en.json +13 -1
  169. package/locales/pseudo.json +9 -0
  170. package/package.json +5 -4
  171. package/src/AlertDialog/AlertDialog.tsx +6 -5
  172. package/src/AlertDialog/__tests__/AlertDialog.a11y.chromium.spec.ts +18 -6
  173. package/src/Avatar/Avatar.spec.md +22 -22
  174. package/src/Avatar/Avatar.tsx +18 -1
  175. package/src/Avatar/__tests__/AvatarForcedColors.a11y.chromium.spec.ts +126 -0
  176. package/src/BottomSheet/BottomSheet.test.tsx +46 -0
  177. package/src/BottomSheet/BottomSheet.tsx +1 -1
  178. package/src/BottomSheet/BottomSheetPanel.tsx +2 -1
  179. package/src/BottomSheet/useSheetGestures.test.ts +52 -0
  180. package/src/BottomSheet/useSheetGestures.ts +37 -6
  181. package/src/Breadcrumbs/__tests__/Breadcrumbs.a11y.chromium.spec.ts +18 -6
  182. package/src/Button/Button.tsx +4 -4
  183. package/src/Calendar/Calendar.spec.md +87 -48
  184. package/src/Calendar/__tests__/Calendar.a11y.chromium.spec.ts +21 -7
  185. package/src/Carousel/__tests__/Carousel.a11y.chromium.spec.ts +21 -7
  186. package/src/CheckboxInput/CheckboxInput.tsx +6 -5
  187. package/src/Collapsible/Collapsible.spec.md +2 -0
  188. package/src/Collapsible/CollapsibleGroup.spec.md +57 -29
  189. package/src/Collapsible/__tests__/Collapsible.a11y.chromium.spec.ts +18 -7
  190. package/src/Collapsible/__tests__/CollapsibleGroupAccordion.a11y.chromium.spec.ts +18 -6
  191. package/src/ContextMenu/ContextMenu.test.tsx +35 -0
  192. package/src/DateInput/DateInput.test.tsx +53 -2
  193. package/src/DateInput/DateInput.tsx +15 -11
  194. package/src/DateInput/MonthScroller.tsx +106 -20
  195. package/src/DateInput/MonthScrollerSettle.test.tsx +165 -0
  196. package/src/DateInput/__tests__/MonthScrollerSteps.interaction.browser.spec.ts +312 -0
  197. package/src/DateRangeInput/DateRangeInput.test.tsx +46 -3
  198. package/src/DateRangeInput/DateRangeInput.tsx +16 -12
  199. package/src/DateTimeInput/DateTimeInput.spec.md +71 -48
  200. package/src/DateTimeInput/DateTimeInput.test.tsx +61 -0
  201. package/src/DateTimeInput/DateTimeInput.tsx +15 -11
  202. package/src/Dialog/Dialog.test.tsx +216 -2
  203. package/src/Dialog/Dialog.tsx +42 -9
  204. package/src/Dialog/DialogHeader.tsx +5 -4
  205. package/src/Dialog/__tests__/DialogSecondDocument.a11y.browser.spec.ts +202 -0
  206. package/src/Divider/Divider.tsx +2 -1
  207. package/src/Drawer/Drawer.spec.md +111 -80
  208. package/src/Drawer/Drawer.test.tsx +22 -0
  209. package/src/Drawer/Drawer.tsx +5 -0
  210. package/src/Drawer/DrawerHeader.spec.md +181 -0
  211. package/src/DropdownMenu/DropdownMenu.test.tsx +41 -0
  212. package/src/DropdownMenu/DropdownMenu.tsx +7 -1
  213. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +34 -0
  214. package/src/DropdownMenu/MenuBottomSheetActionList.tsx +6 -5
  215. package/src/DropdownMenu/__tests__/Menu.a11y.chromium.spec.ts +19 -7
  216. package/src/DropdownMenu/__tests__/MenuButton.a11y.chromium.spec.ts +21 -9
  217. package/src/DropdownMenu/lazyMenuBottomSheet.test.ts +53 -0
  218. package/src/DropdownMenu/lazyMenuBottomSheet.ts +23 -6
  219. package/src/DropdownMenu/renderDropdownItems.tsx +24 -19
  220. package/src/EmptyState/EmptyState.tsx +2 -1
  221. package/src/Field/FieldLabel.tsx +5 -4
  222. package/src/Field/InputClearButton.tsx +3 -3
  223. package/src/Field/PanelSearchInput.tsx +2 -2
  224. package/src/FieldStatus/FieldStatus.tsx +2 -2
  225. package/src/FileInput/FileInput.test.tsx +21 -0
  226. package/src/FileInput/FileInput.tsx +4 -1
  227. package/src/FormLayout/FormLayout.spec.md +7 -8
  228. package/src/Heading/Heading.tsx +1 -1
  229. package/src/Icon/Icon.tsx +1 -1
  230. package/src/Indicator/CheckIndicator.spec.md +47 -42
  231. package/src/Indicator/CheckIndicator.tsx +2 -1
  232. package/src/Indicator/CheckboxIndicator.spec.md +40 -37
  233. package/src/Indicator/CheckboxIndicator.tsx +3 -1
  234. package/src/Indicator/Indicator.spec.md +188 -0
  235. package/src/Indicator/RadioIndicator.tsx +3 -1
  236. package/src/Item/Item.tsx +2 -1
  237. package/src/Item/ItemSwipeLayer.tsx +1 -1
  238. package/src/Layout/Layout.spec.md +127 -131
  239. package/src/Layout/Layout.tsx +1 -1
  240. package/src/Layout/LayoutContent.spec.md +205 -0
  241. package/src/Layout/LayoutContent.tsx +1 -1
  242. package/src/Layout/LayoutFooter.spec.md +190 -0
  243. package/src/Layout/LayoutFooter.tsx +1 -1
  244. package/src/Layout/LayoutHeader.spec.md +193 -0
  245. package/src/Layout/LayoutHeader.tsx +1 -1
  246. package/src/Layout/LayoutPanel.spec.md +194 -0
  247. package/src/Layout/__tests__/Layout.a11y.chromium.spec.ts +18 -6
  248. package/src/Lightbox/Lightbox.test.tsx +18 -0
  249. package/src/Lightbox/Lightbox.tsx +5 -0
  250. package/src/Lightbox/LightboxViewport.a11y.browser.spec.ts +8 -4
  251. package/src/Link/__tests__/Link.a11y.chromium.spec.ts +18 -8
  252. package/src/List/List.tsx +1 -1
  253. package/src/List/ListItem.tsx +3 -3
  254. package/src/MobileNav/MobileNav.test.tsx +15 -0
  255. package/src/MobileNav/MobileNav.tsx +9 -1
  256. package/src/MobileNav/__tests__/MobileNavHeader.a11y.chromium.spec.ts +89 -0
  257. package/src/MoreMenu/MoreMenu.test.tsx +11 -0
  258. package/src/MultiSelector/MultiSelector.spec.md +7 -4
  259. package/src/MultiSelector/MultiSelector.test.tsx +118 -1
  260. package/src/MultiSelector/MultiSelector.tsx +6 -2
  261. package/src/MultiSelector/__tests__/MultiSelector.listbox.a11y.test.tsx +8 -1
  262. package/src/NavMenu/NavHeadingMenu.spec.md +1 -5
  263. package/src/NumberInput/NumberInput.doc.mjs +13 -7
  264. package/src/NumberInput/NumberInput.spec.md +59 -37
  265. package/src/NumberInput/NumberInput.tsx +13 -3
  266. package/src/NumberInput/__tests__/NumberInput.coarse-pointer.a11y.chromium.spec.ts +102 -0
  267. package/src/ProgressBar/ProgressBar.test.tsx +108 -32
  268. package/src/ProgressBar/ProgressBar.tsx +58 -10
  269. package/src/ProgressBar/__tests__/ProgressBar.a11y.chromium.spec.ts +21 -9
  270. package/src/ProgressBar/__tests__/ProgressBarMotion.a11y.chromium.spec.ts +831 -0
  271. package/src/Resizable/__tests__/ResizeHandle.a11y.chromium.spec.ts +21 -14
  272. package/src/Resizable/useResizable.test.ts +43 -0
  273. package/src/Resizable/useResizable.ts +32 -9
  274. package/src/SegmentedControl/SegmentedControl.test.tsx +84 -0
  275. package/src/SegmentedControl/SegmentedControlItem.tsx +11 -5
  276. package/src/Selector/Selector.test.tsx +90 -1
  277. package/src/Selector/__tests__/Selector.combobox.a11y.test.tsx +8 -1
  278. package/src/Selector/__tests__/Selector.listbox.a11y.test.tsx +8 -1
  279. package/src/Selector/useSelectorPresentation.ts +31 -3
  280. package/src/SideNav/SideNav.test.tsx +27 -0
  281. package/src/Slider/Slider.test.tsx +12 -0
  282. package/src/Slider/Slider.tsx +4 -1
  283. package/src/Slider/__tests__/Slider.a11y.chromium.spec.ts +19 -7
  284. package/src/Spinner/Spinner.tsx +2 -2
  285. package/src/Stack/Stack.tsx +1 -1
  286. package/src/Stack/StackItem.tsx +1 -1
  287. package/src/TabList/TabList.test.tsx +52 -1
  288. package/src/TabList/TabList.tsx +16 -1
  289. package/src/TabList/__tests__/TabPressPointer.interaction.browser.spec.ts +163 -0
  290. package/src/Table/__tests__/Table.a11y.chromium.spec.ts +18 -6
  291. package/src/Table/plugins/columnResize/useTableColumnResize.test.tsx +22 -0
  292. package/src/Table/plugins/columnResize/useTableColumnResize.tsx +8 -4
  293. package/src/Text/Text.tsx +1 -1
  294. package/src/Text/useTruncation.test.tsx +128 -1
  295. package/src/Text/useTruncation.ts +23 -5
  296. package/src/TextArea/TextArea.spec.md +10 -13
  297. package/src/TextArea/TextArea.test.tsx +60 -0
  298. package/src/TextArea/TextArea.tsx +15 -1
  299. package/src/TextInput/TextInput.test.tsx +21 -0
  300. package/src/TextInput/TextInput.tsx +3 -1
  301. package/src/Timestamp/Timestamp.spec.md +6 -12
  302. package/src/Timestamp/Timestamp.test.tsx +30 -5
  303. package/src/Timestamp/Timestamp.tsx +6 -2
  304. package/src/Timestamp/TimestampHoverCard.tsx +4 -4
  305. package/src/Toast/Toast.spec.md +7 -10
  306. package/src/Toast/ToastViewport.test.tsx +575 -1
  307. package/src/Toast/ToastViewport.tsx +100 -50
  308. package/src/Tokenizer/Tokenizer.spec.md +5 -3
  309. package/src/Tokenizer/Tokenizer.test.tsx +68 -0
  310. package/src/Tokenizer/Tokenizer.tsx +14 -2
  311. package/src/Tokenizer/TokenizerBottomSheet.test.tsx +15 -4
  312. package/src/Tokenizer/TokenizerBottomSheet.tsx +3 -3
  313. package/src/Toolbar/Toolbar.spec.md +5 -6
  314. package/src/Toolbar/Toolbar.test.tsx +30 -0
  315. package/src/Toolbar/Toolbar.tsx +2 -1
  316. package/src/Toolbar/__tests__/Toolbar.a11y.chromium.spec.ts +18 -6
  317. package/src/Tooltip/__tests__/Tooltip.a11y.chromium.spec.ts +18 -7
  318. package/src/TopNav/TopNav.test.tsx +27 -0
  319. package/src/Typeahead/Typeahead.test.tsx +40 -0
  320. package/src/Typeahead/Typeahead.tsx +12 -1
  321. package/src/gestures/PinchGestureRecognizer.ts +144 -0
  322. package/src/gestures/ScreenEdgePanGestureRecognizer.ts +252 -0
  323. package/src/gestures/SwipeGestureRecognizer.ts +193 -0
  324. package/src/gestures/VelocityTracker.ts +10 -3
  325. package/src/gestures/__tests__/PanGestureRecognizer.test.ts +4 -4
  326. package/src/gestures/__tests__/PinchGestureRecognizer.test.ts +74 -0
  327. package/src/gestures/__tests__/ScreenEdgePanGestureRecognizer.test.ts +498 -0
  328. package/src/gestures/__tests__/SwipeGestureRecognizer.test.ts +108 -0
  329. package/src/gestures/__tests__/arenaPinchSwipeEdge.test.ts +188 -0
  330. package/src/gestures/__tests__/harness.ts +21 -2
  331. package/src/gestures/types.ts +23 -9
  332. package/src/hooks/useAdaptivePresentation.ts +4 -1
  333. package/src/hooks/useMenuHover.ts +15 -1
  334. package/src/i18n/generated-locales/en.generated.ts +4 -0
  335. package/src/i18n/generated-locales/pseudo.generated.ts +4 -0
  336. package/src/motion/README.md +11 -1
  337. package/src/motion/animator.test.ts +151 -3
  338. package/src/motion/animator.ts +149 -33
  339. package/src/theme/defineTheme.ts +1 -1
  340. package/src/theme/derivedVarRegistry.test.ts +4 -0
  341. package/src/theme/domainTokens/domainTokenDefaults.ts +19 -0
  342. package/src/theme/domainTokens/index.ts +6 -10
  343. package/src/utils/focusReturn.ts +17 -6
  344. package/src/utils/getNodeText.test.ts +24 -0
  345. package/src/utils/getNodeText.ts +37 -0
  346. package/src/utils/interactionModality.test.ts +45 -1
  347. package/src/utils/interactionModality.ts +64 -5
  348. package/src/utils/presentation.test.ts +380 -0
  349. package/src/utils/presentation.ts +180 -0
  350. package/src/utils/pressPaintPriority.test.ts +80 -0
  351. package/src/utils/touchPresentation.stylex.ts +43 -0
  352. package/src/utils/touchPresentation.test.ts +140 -1
  353. package/src/utils/touchPresentation.ts +5 -3
@@ -2293,6 +2293,67 @@ describe('DateTimeInput', () => {
2293
2293
  });
2294
2294
  });
2295
2295
 
2296
+ describe('calendar while the popover is closed', () => {
2297
+ const calendarGrid = () => document.querySelector('[role="grid"]');
2298
+
2299
+ it('renders no calendar until the popover opens, and none after it closes', () => {
2300
+ render(
2301
+ <DateTimeInput
2302
+ label="Meeting"
2303
+ value={'2026-01-15T10:00' as ISODateTimeString}
2304
+ onChange={() => {}}
2305
+ />,
2306
+ );
2307
+ const toggle = getButton('Open calendar');
2308
+ expect(calendarGrid()).toBeNull();
2309
+
2310
+ fireEvent.click(toggle);
2311
+ expect(calendarGrid()).not.toBeNull();
2312
+
2313
+ fireEvent.click(toggle);
2314
+ expect(calendarGrid()).toBeNull();
2315
+ });
2316
+
2317
+ it('opens on the month of a date typed while closed', () => {
2318
+ function Controlled() {
2319
+ const [value, setValue] = useState<ISODateTimeString | undefined>(
2320
+ '2026-01-15T10:00' as ISODateTimeString,
2321
+ );
2322
+ return (
2323
+ <DateTimeInput label="Meeting" value={value} onChange={setValue} />
2324
+ );
2325
+ }
2326
+ render(<Controlled />);
2327
+ const dateInput = screen.getAllByRole('combobox')[0];
2328
+
2329
+ fireEvent.change(dateInput, {target: {value: 'March 5, 2027'}});
2330
+ fireEvent.keyDown(dateInput, {key: 'ArrowDown'});
2331
+
2332
+ expect(calendarGrid()).toHaveAccessibleName('March 2027');
2333
+ });
2334
+
2335
+ it('moves focus into the calendar on each open from the toggle', async () => {
2336
+ render(
2337
+ <DateTimeInput
2338
+ label="Meeting"
2339
+ value={'2026-01-15T10:00' as ISODateTimeString}
2340
+ onChange={() => {}}
2341
+ />,
2342
+ );
2343
+ const toggle = getButton('Open calendar');
2344
+
2345
+ for (let i = 0; i < 2; i++) {
2346
+ fireEvent.click(toggle);
2347
+ // The first calendar control takes focus, not the dialog surface.
2348
+ await waitFor(() => {
2349
+ expect(getButton('Previous month')).toHaveFocus();
2350
+ });
2351
+ fireEvent.click(toggle);
2352
+ expect(calendarGrid()).toBeNull();
2353
+ }
2354
+ });
2355
+ });
2356
+
2296
2357
  describe('weekStartsOn', () => {
2297
2358
  // The calendar popover renders in the top layer; jsdom keeps the content in
2298
2359
  // the DOM but role queries skip it, so read the columnheaders directly.
@@ -1937,17 +1937,21 @@ function PointerDateTimeField({
1937
1937
 
1938
1938
  {!usesNativePicker &&
1939
1939
  popover.render(
1940
- <Calendar
1941
- handleRef={calendarRef}
1942
- mode="single"
1943
- value={valueParts.date ?? null}
1944
- onChange={(d: ISODateString) => handleDateChange(d, 'calendar')}
1945
- min={calendarMin}
1946
- max={calendarMax}
1947
- dateConstraints={dateConstraints}
1948
- numberOfMonths={numberOfMonths}
1949
- weekStartsOn={weekStartsOn}
1950
- />,
1940
+ // Mounted only while open, like the time list below; the calendar
1941
+ // opens on the current value.
1942
+ !popover.isOpen ? null : (
1943
+ <Calendar
1944
+ handleRef={calendarRef}
1945
+ mode="single"
1946
+ value={valueParts.date ?? null}
1947
+ onChange={(d: ISODateString) => handleDateChange(d, 'calendar')}
1948
+ min={calendarMin}
1949
+ max={calendarMax}
1950
+ dateConstraints={dateConstraints}
1951
+ numberOfMonths={numberOfMonths}
1952
+ weekStartsOn={weekStartsOn}
1953
+ />
1954
+ ),
1951
1955
  {placement: 'below', alignment: 'start'},
1952
1956
  )}
1953
1957
 
@@ -13,7 +13,14 @@ import {useState, type SyntheticEvent} from 'react';
13
13
  import {readFileSync} from 'node:fs';
14
14
  import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
15
15
  import type {MockInstance} from 'vitest';
16
- import {render, screen, fireEvent} from '@testing-library/react';
16
+ import {
17
+ render,
18
+ screen,
19
+ fireEvent,
20
+ within,
21
+ cleanup,
22
+ } from '@testing-library/react';
23
+ import {createPortal} from 'react-dom';
17
24
  import {
18
25
  Dialog,
19
26
  dialogFullscreenSafeAreaPaddingContract,
@@ -21,7 +28,10 @@ import {
21
28
  } from './Dialog';
22
29
  import {DialogHeader} from './DialogHeader';
23
30
  import {defineTheme, generateThemeCSS} from '../theme';
24
- import {__resetInteractionModalityForTest} from '../utils/interactionModality';
31
+ import {
32
+ __resetInteractionModalityForTest,
33
+ getInteractionModality,
34
+ } from '../utils/interactionModality';
25
35
 
26
36
  function generateThemeTestCSS(
27
37
  theme: Parameters<typeof generateThemeCSS>[0],
@@ -555,6 +565,210 @@ describe('Dialog', () => {
555
565
  }).not.toThrow();
556
566
  expect(dialog).not.toHaveAttribute('open');
557
567
  });
568
+
569
+ describe('in a second document', () => {
570
+ // An app can render a Dialog into a same-origin iframe or a Document
571
+ // Picture-in-Picture window through a portal. The control that opens it
572
+ // is focused in that document; the page's document reports only the
573
+ // iframe element.
574
+ let frame: HTMLIFrameElement;
575
+ let secondDocument: Document;
576
+
577
+ beforeEach(() => {
578
+ frame = document.createElement('iframe');
579
+ document.body.appendChild(frame);
580
+ secondDocument = frame.contentDocument!;
581
+ // The showModal/close stand-ins above, on the second window's own
582
+ // prototype.
583
+ const {prototype} = secondDocument.defaultView!.HTMLDialogElement;
584
+ prototype.showModal = HTMLDialogElement.prototype.showModal;
585
+ prototype.close = HTMLDialogElement.prototype.close;
586
+ });
587
+
588
+ afterEach(() => {
589
+ // Unmount before the frame goes, or React removes nodes from a
590
+ // document that no longer exists.
591
+ cleanup();
592
+ frame.remove();
593
+ });
594
+
595
+ function SecondDocumentDialog({isOpen}: {isOpen: boolean}) {
596
+ return createPortal(
597
+ <>
598
+ <button type="button">Open dialog</button>
599
+ <Dialog isOpen={isOpen} onOpenChange={() => {}} aria-label="Review">
600
+ <button type="button">Inside</button>
601
+ </Dialog>
602
+ </>,
603
+ secondDocument.body,
604
+ );
605
+ }
606
+
607
+ function PageOpensSecondDocumentDialog({isOpen}: {isOpen: boolean}) {
608
+ return (
609
+ <>
610
+ <button type="button">Open from the page</button>
611
+ {createPortal(
612
+ <Dialog
613
+ isOpen={isOpen}
614
+ onOpenChange={() => {}}
615
+ aria-label="Review">
616
+ <button type="button">Inside</button>
617
+ </Dialog>,
618
+ secondDocument.body,
619
+ )}
620
+ </>
621
+ );
622
+ }
623
+
624
+ function SecondDocumentFieldDialog({isOpen}: {isOpen: boolean}) {
625
+ return createPortal(
626
+ <>
627
+ <input aria-label="Search" />
628
+ <Dialog isOpen={isOpen} onOpenChange={() => {}} aria-label="Review">
629
+ <button type="button">Inside</button>
630
+ </Dialog>
631
+ </>,
632
+ secondDocument.body,
633
+ );
634
+ }
635
+
636
+ function SecondDocumentDialogs({count}: {count: number}) {
637
+ return createPortal(
638
+ <>
639
+ {count > 0 && (
640
+ <Dialog isOpen={false} onOpenChange={() => {}} aria-label="First">
641
+ First
642
+ </Dialog>
643
+ )}
644
+ {count > 1 && (
645
+ <Dialog
646
+ isOpen={false}
647
+ onOpenChange={() => {}}
648
+ aria-label="Second">
649
+ Second
650
+ </Dialog>
651
+ )}
652
+ </>,
653
+ secondDocument.body,
654
+ );
655
+ }
656
+
657
+ it('returns focus to the control that opened it, in its own document', () => {
658
+ const {rerender} = render(<SecondDocumentDialog isOpen={false} />);
659
+ const view = within(secondDocument.body);
660
+ const opener = view.getByRole('button', {name: 'Open dialog'});
661
+ opener.focus();
662
+ expect(document.activeElement).toBe(frame);
663
+
664
+ rerender(<SecondDocumentDialog isOpen={true} />);
665
+ view.getByRole('button', {name: 'Inside', hidden: true}).focus();
666
+
667
+ rerender(<SecondDocumentDialog isOpen={false} />);
668
+ expect(opener).toHaveFocus();
669
+ });
670
+
671
+ it('returns focus to a control in the page that opened it', () => {
672
+ const {rerender} = render(
673
+ <PageOpensSecondDocumentDialog isOpen={false} />,
674
+ );
675
+ const opener = screen.getByRole('button', {name: 'Open from the page'});
676
+ opener.focus();
677
+
678
+ rerender(<PageOpensSecondDocumentDialog isOpen={true} />);
679
+ within(secondDocument.body)
680
+ .getByRole('button', {name: 'Inside', hidden: true})
681
+ .focus();
682
+
683
+ rerender(<PageOpensSecondDocumentDialog isOpen={false} />);
684
+ expect(opener).toHaveFocus();
685
+ });
686
+
687
+ it('takes the ring from the last input in its own document', () => {
688
+ __resetInteractionModalityForTest();
689
+ const {rerender} = render(<SecondDocumentDialog isOpen={false} />);
690
+ const opener = within(secondDocument.body).getByRole('button', {
691
+ name: 'Open dialog',
692
+ });
693
+ // The page's last input was a pointer press (say, on the control that
694
+ // popped the window out); the dialog then closes from the keyboard in
695
+ // its own document.
696
+ fireEvent.pointerDown(document.body);
697
+ opener.focus();
698
+ rerender(<SecondDocumentDialog isOpen={true} />);
699
+ fireEvent.keyDown(secondDocument.body, {key: 'Escape'});
700
+ const focus = vi.spyOn(
701
+ secondDocument.defaultView!.HTMLElement.prototype,
702
+ 'focus',
703
+ );
704
+
705
+ rerender(<SecondDocumentDialog isOpen={false} />);
706
+ expect(opener).toHaveFocus();
707
+ expect(
708
+ focus.mock.calls.filter(
709
+ (_call, index) => focus.mock.contexts[index] === opener,
710
+ ),
711
+ ).toEqual([[]]);
712
+ focus.mockRestore();
713
+ });
714
+
715
+ it('keeps plain focus on a text field that opened it after a pointer close', () => {
716
+ const {rerender} = render(<SecondDocumentFieldDialog isOpen={false} />);
717
+ const field = within(secondDocument.body).getByRole('textbox', {
718
+ name: 'Search',
719
+ });
720
+ field.focus();
721
+ rerender(<SecondDocumentFieldDialog isOpen={true} />);
722
+ const inside = within(secondDocument.body).getByRole('button', {
723
+ name: 'Inside',
724
+ hidden: true,
725
+ });
726
+ inside.focus();
727
+ fireEvent.pointerDown(inside);
728
+ // The field's own window's prototype: a field there is never an
729
+ // instance of the page's constructors.
730
+ const {prototype} = secondDocument.defaultView!.HTMLElement;
731
+ const focus = vi.spyOn(prototype, 'focus');
732
+ const blur = vi.spyOn(prototype, 'blur');
733
+
734
+ rerender(<SecondDocumentFieldDialog isOpen={false} />);
735
+ expect(field).toHaveFocus();
736
+ expect(
737
+ focus.mock.calls.filter(
738
+ (_call, index) => focus.mock.contexts[index] === field,
739
+ ),
740
+ ).toEqual([[]]);
741
+ expect(blur.mock.contexts).not.toContain(field);
742
+ focus.mockRestore();
743
+ blur.mockRestore();
744
+ });
745
+
746
+ it('tracks its document while any Dialog there is mounted, and releases it after the last', () => {
747
+ const removeEventListener = vi.spyOn(
748
+ secondDocument,
749
+ 'removeEventListener',
750
+ );
751
+ const removedModalityListeners = () =>
752
+ removeEventListener.mock.calls
753
+ .map(([type]) => type)
754
+ .filter(type => type === 'pointerdown' || type === 'keydown')
755
+ .sort();
756
+ const {rerender} = render(<SecondDocumentDialogs count={2} />);
757
+ fireEvent.pointerDown(secondDocument.body);
758
+ expect(getInteractionModality(secondDocument)).toBe('pointer');
759
+
760
+ rerender(<SecondDocumentDialogs count={1} />);
761
+ fireEvent.keyDown(secondDocument.body, {key: 'Tab'});
762
+ expect(getInteractionModality(secondDocument)).toBe('keyboard');
763
+ expect(removedModalityListeners()).toEqual([]);
764
+
765
+ rerender(<SecondDocumentDialogs count={0} />);
766
+ expect(removedModalityListeners()).toEqual(['keydown', 'pointerdown']);
767
+ fireEvent.pointerDown(secondDocument.body);
768
+ expect(getInteractionModality(secondDocument)).toBe('keyboard');
769
+ removeEventListener.mockRestore();
770
+ });
771
+ });
558
772
  });
559
773
 
560
774
  // jsdom draws no focus ring, so these prove only which focus call Dialog
@@ -69,14 +69,18 @@ import {
69
69
  overlayPaddingReset,
70
70
  } from '../Layout/padding.stylex';
71
71
  import type {SpacingStep} from '../utils/types';
72
- import {mergeProps} from '../utils';
72
+ import {mergeProps} from '../utils/mergeProps';
73
73
  import {chainEventHandlers} from '../utils/chainEventHandlers';
74
74
  import {devWarn} from '../utils/devWarning';
75
75
  import {DialogContext} from './DialogContext';
76
76
  import {themeProps} from '../utils/themeProps';
77
77
  import type {DialogVariantMap} from './index';
78
78
  import {focusOutlineProps} from '../utils/focusOutline.stylex';
79
- import {useInteractionModalityTracking} from '../utils/interactionModality';
79
+ import {
80
+ getInteractionModality,
81
+ trackInteractionModalityIn,
82
+ useInteractionModalityTracking,
83
+ } from '../utils/interactionModality';
80
84
  import {focusForLastInput} from '../utils/focusReturn';
81
85
  import {ModalAnnounceProvider} from '../hooks/ModalAnnounceProvider';
82
86
 
@@ -91,8 +95,11 @@ function getDialogDirection(
91
95
  distance = 16,
92
96
  ): {x: number; y: number} {
93
97
  const rect = triggerEl.getBoundingClientRect();
94
- const dx = rect.left + rect.width / 2 - window.innerWidth / 2;
95
- const dy = rect.top + rect.height / 2 - window.innerHeight / 2;
98
+ // The trigger's own window: its rect is measured in that window's viewport,
99
+ // which is not the page's when the dialog renders into a second document.
100
+ const view = triggerEl.ownerDocument.defaultView ?? window;
101
+ const dx = rect.left + rect.width / 2 - view.innerWidth / 2;
102
+ const dy = rect.top + rect.height / 2 - view.innerHeight / 2;
96
103
  const dist = Math.sqrt(dx * dx + dy * dy) || 1;
97
104
  return {
98
105
  x: Math.round((dx / dist) * distance),
@@ -534,6 +541,18 @@ export function Dialog({
534
541
 
535
542
  useInteractionModalityTracking();
536
543
 
544
+ // Input in a second document (a same-origin iframe, a Document
545
+ // Picture-in-Picture window) never reaches the page's listeners, so the
546
+ // dialog's own document keeps its own last input for the focus return while
547
+ // this Dialog is mounted, from before it opens until it unmounts.
548
+ useEffect(() => {
549
+ const ownerDocument = dialogRef.current?.ownerDocument;
550
+ if (ownerDocument == null || ownerDocument === document) {
551
+ return undefined;
552
+ }
553
+ return trackInteractionModalityIn(ownerDocument);
554
+ }, [isInline]);
555
+
537
556
  // This runs in a layout effect so the capture lands before any descendant's
538
557
  // passive mount effect: all layout effects run before any passive effect,
539
558
  // while a child's passive effect runs before this parent's passive effect.
@@ -545,7 +564,15 @@ export function Dialog({
545
564
  if (isInline || !isOpen) {
546
565
  return;
547
566
  }
548
- triggerElementRef.current = document.activeElement as HTMLElement | null;
567
+ // The dialog's own document while it holds focus: an app can render a
568
+ // dialog into a second document (a same-origin iframe, a Document
569
+ // Picture-in-Picture window) through a portal, and a control there that
570
+ // opens it is focused in that document, which the page's document reports
571
+ // only as the iframe element, or not at all. A control in the page that
572
+ // opens such a dialog leaves focus in the page.
573
+ const dialogDocument = dialogRef.current?.ownerDocument ?? document;
574
+ const doc = dialogDocument.hasFocus() ? dialogDocument : document;
575
+ triggerElementRef.current = doc.activeElement as HTMLElement | null;
549
576
  }, [isOpen, isInline]);
550
577
 
551
578
  // Derive dismissal behavior from purpose
@@ -567,7 +594,7 @@ export function Dialog({
567
594
  // captured by the layout effect above, before any descendant effect
568
595
  // could move focus.
569
596
  const trigger = triggerElementRef.current;
570
- if (trigger && trigger !== document.body) {
597
+ if (trigger && trigger !== trigger.ownerDocument.body) {
571
598
  const dir = getDialogDirection(trigger);
572
599
  dialog.style.setProperty('--dialog-dir-x', `${dir.x}px`);
573
600
  dialog.style.setProperty('--dialog-dir-y', `${dir.y}px`);
@@ -591,9 +618,15 @@ export function Dialog({
591
618
  if (dialog.open) {
592
619
  dialog.close();
593
620
  }
594
- // Return focus to the element that opened the dialog
595
- if (triggerElementRef.current) {
596
- focusForLastInput(triggerElementRef.current);
621
+ // Return focus to the element that opened the dialog, with the ring
622
+ // the last input in that element's own document calls for.
623
+ const trigger = triggerElementRef.current;
624
+ if (trigger) {
625
+ focusForLastInput(
626
+ trigger,
627
+ undefined,
628
+ getInteractionModality(trigger.ownerDocument),
629
+ );
597
630
  }
598
631
  triggerElementRef.current = null;
599
632
  }
@@ -20,15 +20,16 @@ import {useEffect, useRef, type ReactNode} from 'react';
20
20
  import * as stylex from '@stylexjs/stylex';
21
21
  import {spacingVars, sizeVars, typeScaleVars} from '../theme/tokens.stylex';
22
22
  import {LayoutHeader} from '../Layout/LayoutHeader';
23
- import {Button} from '../Button';
24
- import {Icon} from '../Icon';
23
+ import {Button} from '../Button/Button';
24
+ import {Icon} from '../Icon/Icon';
25
25
  import {Heading} from '../Heading/Heading';
26
26
  import {Text} from '../Text/Text';
27
27
  import type {BaseProps} from '../BaseProps';
28
- import {isRenderable, mergeProps} from '../utils';
28
+ import {isRenderable} from '../utils/isRenderable';
29
+ import {mergeProps} from '../utils/mergeProps';
29
30
  import {themeProps} from '../utils/themeProps';
30
31
  import {useDialogContext} from './DialogContext';
31
- import {useTranslator} from '../i18n';
32
+ import {useTranslator} from '../i18n/useTranslator';
32
33
 
33
34
  const styles = stylex.create({
34
35
  container: {
@@ -0,0 +1,202 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ // @pr-a11y scope ^(packages/core/src/Dialog/|packages/core/src/utils/(focusReturn|interactionModality)\.ts|apps/storybook/stories/DialogSecondDocumentA11y)
3
+
4
+ /**
5
+ * @file DialogSecondDocument.a11y.browser.spec.ts
6
+ * @input Uses @playwright/test, the a11y-spec static Storybook server, and the
7
+ * checked-in DialogSecondDocumentA11y story
8
+ * @output Real-browser evidence that a Dialog rendered into a second document
9
+ * (a same-origin iframe, a Document Picture-in-Picture window) returns focus
10
+ * to the control that opened it when it closes, by Escape or by a control
11
+ * inside it, exactly as the same Dialog does in the page, with the focus
12
+ * ring that close's input calls for, a text field that opened it keeping
13
+ * its ring with no extra blur; and that a Dialog in an iframe opened by a
14
+ * control in the page returns focus to that control.
15
+ * @position The half of Dialog's focus return jsdom cannot reach: which
16
+ * document holds focus, and the browser's own focus restore when a modal
17
+ * dialog closes. The Vitest case in `Dialog/Dialog.test.tsx` models the
18
+ * same rule with an iframe. Document Picture-in-Picture is Chromium's
19
+ * alone, so the window's cases skip in other engines; the page and the
20
+ * iframe run everywhere.
21
+ *
22
+ * SYNC: Fixture lives in
23
+ * /apps/storybook/stories/DialogSecondDocumentA11y.stories.tsx.
24
+ */
25
+
26
+ import {
27
+ expect,
28
+ test,
29
+ type Frame,
30
+ type Keyboard,
31
+ type Locator,
32
+ type Page,
33
+ } from '@playwright/test';
34
+ import {holdMotionStill} from '@astryxdesign/a11y-spec/chromium';
35
+ import {
36
+ DEFAULT_STORYBOOK_DIR,
37
+ serveStorybook,
38
+ type StaticServer,
39
+ } from '@astryxdesign/a11y-spec/storybook';
40
+
41
+ const STORY_ID = 'a11y-dialog-in-a-second-document--second-documents';
42
+
43
+ let storybook: StaticServer;
44
+
45
+ test.beforeAll(async () => {
46
+ storybook = await serveStorybook(
47
+ process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
48
+ );
49
+ });
50
+
51
+ test.afterAll(async () => {
52
+ await storybook?.close();
53
+ });
54
+
55
+ type Where = 'page' | 'iframe' | 'Picture-in-Picture window';
56
+
57
+ /** One document of the story: where its controls live and where keys go. */
58
+ interface Surface {
59
+ frame: Frame;
60
+ keyboard: Keyboard;
61
+ }
62
+
63
+ /** Load the story and return the document the test drives. */
64
+ async function mountStory(page: Page, where: Where): Promise<Surface> {
65
+ await page.goto(
66
+ `${storybook.origin}/iframe.html?id=${STORY_ID}&viewMode=story`,
67
+ {waitUntil: 'load'},
68
+ );
69
+ await holdMotionStill(page);
70
+ const popOut = page.getByRole('button', {name: 'Pop out'});
71
+ await popOut.waitFor();
72
+
73
+ if (where === 'page') {
74
+ return {frame: page.mainFrame(), keyboard: page.keyboard};
75
+ }
76
+ if (where === 'iframe') {
77
+ const element = await page
78
+ .locator('iframe[title="Second document"]')
79
+ .elementHandle();
80
+ const frame = await element?.contentFrame();
81
+ if (frame == null) {
82
+ throw new Error('the story renders no iframe');
83
+ }
84
+ await frame
85
+ .getByRole('button', {name: 'Open dialog', exact: true})
86
+ .waitFor();
87
+ // Key presses go to whichever frame holds focus; the clicks below put it
88
+ // in this one.
89
+ return {frame, keyboard: page.keyboard};
90
+ }
91
+ // Opening the window needs a user gesture; the click is one.
92
+ const [pictureInPicture] = await Promise.all([
93
+ page.context().waitForEvent('page'),
94
+ popOut.click(),
95
+ ]);
96
+ await pictureInPicture
97
+ .getByRole('button', {
98
+ name: 'Open dialog',
99
+ exact: true,
100
+ })
101
+ .waitFor();
102
+ return {
103
+ frame: pictureInPicture.mainFrame(),
104
+ keyboard: pictureInPicture.keyboard,
105
+ };
106
+ }
107
+
108
+ /** Whether the element matches `:focus-visible`, i.e. would show a ring. */
109
+ async function showsRing(element: Locator): Promise<boolean> {
110
+ return element.evaluate(node => node.matches(':focus-visible'));
111
+ }
112
+
113
+ /** Open the dialog from its control with the pointer; returns the control. */
114
+ async function open(frame: Frame): Promise<{opener: Locator; dialog: Locator}> {
115
+ const opener = frame.getByRole('button', {name: 'Open dialog', exact: true});
116
+ await opener.click();
117
+ const dialog = frame.getByRole('dialog', {name: 'Review changes'});
118
+ await expect(dialog).toBeVisible();
119
+ return {opener, dialog};
120
+ }
121
+
122
+ for (const where of [
123
+ 'page',
124
+ 'iframe',
125
+ 'Picture-in-Picture window',
126
+ ] as const satisfies ReadonlyArray<Where>) {
127
+ test.describe(`a Dialog in the ${where}`, () => {
128
+ test.skip(
129
+ ({browserName}) =>
130
+ where === 'Picture-in-Picture window' && browserName !== 'chromium',
131
+ 'Document Picture-in-Picture is a Chromium API',
132
+ );
133
+
134
+ test('closing with Escape returns focus to the control that opened it', async ({
135
+ page,
136
+ }) => {
137
+ const {frame, keyboard} = await mountStory(page, where);
138
+ const {opener, dialog} = await open(frame);
139
+
140
+ await keyboard.press('Escape');
141
+ await expect(dialog).toBeHidden();
142
+ await expect(opener).toBeFocused();
143
+ // A key closed it, so the ring shows (`architecture:interaction-modality`
144
+ // INV1), whatever the page's own last input was (a pointer press on Pop
145
+ // out, for the Picture-in-Picture window).
146
+ await expect.poll(async () => showsRing(opener)).toBe(true);
147
+ });
148
+
149
+ test('closing from a control inside returns focus to the control that opened it', async ({
150
+ page,
151
+ }) => {
152
+ const {frame} = await mountStory(page, where);
153
+ const {opener, dialog} = await open(frame);
154
+
155
+ await frame.getByRole('button', {name: 'Close dialog'}).click();
156
+ await expect(dialog).toBeHidden();
157
+ await expect(opener).toBeFocused();
158
+ // A pointer closed it, so no ring.
159
+ await expect.poll(async () => showsRing(opener)).toBe(false);
160
+ });
161
+
162
+ test('a text field that opened it keeps focus and its ring after a pointer close', async ({
163
+ page,
164
+ }) => {
165
+ const {frame} = await mountStory(page, where);
166
+ const field = frame.getByRole('textbox', {name: 'Search'});
167
+ await field.click();
168
+ await field.press('Enter');
169
+ const dialog = frame.getByRole('dialog', {name: 'Review changes'});
170
+ await expect(dialog).toBeVisible();
171
+ // Count the field's blurs from here on: the open already moved focus
172
+ // away, so any blur now would be the return's own.
173
+ await field.evaluate(node => {
174
+ node.dataset.blurs = '0';
175
+ node.addEventListener('blur', () => {
176
+ node.dataset.blurs = String(Number(node.dataset.blurs) + 1);
177
+ });
178
+ });
179
+
180
+ await frame.getByRole('button', {name: 'Close dialog'}).click();
181
+ await expect(dialog).toBeHidden();
182
+ await expect(field).toBeFocused();
183
+ // A text field shows where typing goes whatever moved focus there.
184
+ await expect.poll(async () => showsRing(field)).toBe(true);
185
+ await expect(field).toHaveAttribute('data-blurs', '0');
186
+ });
187
+ });
188
+ }
189
+
190
+ test.describe('a Dialog in the iframe that a control in the page opens', () => {
191
+ test('closing with Escape returns focus to that control', async ({page}) => {
192
+ const {frame, keyboard} = await mountStory(page, 'iframe');
193
+ const opener = page.getByRole('button', {name: 'Open dialog in the frame'});
194
+ await opener.click();
195
+ const dialog = frame.getByRole('dialog', {name: 'Opened from the page'});
196
+ await expect(dialog).toBeVisible();
197
+
198
+ await keyboard.press('Escape');
199
+ await expect(dialog).toBeHidden();
200
+ await expect(opener).toBeFocused();
201
+ });
202
+ });
@@ -25,7 +25,8 @@ import {
25
25
  typeScaleVars,
26
26
  borderVars,
27
27
  } from '../theme/tokens.stylex';
28
- import {isRenderable, mergeProps} from '../utils';
28
+ import {isRenderable} from '../utils/isRenderable';
29
+ import {mergeProps} from '../utils/mergeProps';
29
30
  import {themeProps} from '../utils/themeProps';
30
31
  import type {DividerVariantMap} from './index';
31
32