@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
@@ -0,0 +1,188 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file arenaPinchSwipeEdge.test.ts
5
+ * @input GestureArena with pinches, swipes, edge pans and pans, driven by
6
+ * synthetic touches with explicit timestamps
7
+ * @output Simultaneous recognition (pinch + pan), the first to recognize
8
+ * winning without permission, edge pan vs inner pan, swipe vs pan, and a
9
+ * delayed touch delivery
10
+ * @position Unit tests of packages/core/src/gestures/GestureArena.ts
11
+ */
12
+
13
+ import {describe, expect, it} from 'vitest';
14
+ import {UIKIT_PINCH, UIKIT_SWIPE} from '../../motion/uikit/gestures';
15
+ import {GestureRecognizer} from '../GestureRecognizer';
16
+ import {PanGestureRecognizer} from '../PanGestureRecognizer';
17
+ import {PinchGestureRecognizer} from '../PinchGestureRecognizer';
18
+ import {ScreenEdgePanGestureRecognizer} from '../ScreenEdgePanGestureRecognizer';
19
+ import {SwipeGestureRecognizer} from '../SwipeGestureRecognizer';
20
+ import {createHarness, RecordingConsumer} from './harness';
21
+
22
+ /**
23
+ * A recognizer that stays possible, even past its finger's lift, until the
24
+ * test fails it: something another recognizer can wait on.
25
+ */
26
+ class Held extends GestureRecognizer {
27
+ private isWaiting = true;
28
+
29
+ override isInvolved(): boolean {
30
+ return super.isInvolved() || this.isWaiting;
31
+ }
32
+
33
+ fail(): void {
34
+ this.isWaiting = false;
35
+ this.setState('failed');
36
+ }
37
+ }
38
+
39
+ describe('conflict matrix', () => {
40
+ it('pinch + pan recognize simultaneously when allowed', () => {
41
+ const h = createHarness();
42
+ const pinch = new PinchGestureRecognizer({name: 'pinch'});
43
+ const pan = new PanGestureRecognizer({name: 'pan'});
44
+ pinch.recognizeSimultaneouslyWith(pan);
45
+ h.finger(1).down(100, 100, 0, {recognizers: [pinch, pan]});
46
+ h.finger(2).down(200, 100, 0, {recognizers: [pinch, pan]});
47
+ h.finger(1).move(80, 120, 16);
48
+ h.finger(2).move(240, 120, 16);
49
+ expect(h.log.slice(0, 2)).toEqual(['pinch:began@16', 'pan:began@16']);
50
+ // Span 100 -> 160, measured against 100 + 8.
51
+ expect(pinch.scale).toBeCloseTo(160 / 108, 5);
52
+ // The pan began at the first finger's move, centroid (-10, 10), which
53
+ // the hysteresis swallows; the second finger's move brought it to (10, 20).
54
+ expect(pan.translation()).toEqual({x: 20, y: 10});
55
+ });
56
+
57
+ it('pinch vs pan without permission: the first to recognize wins', () => {
58
+ const h = createHarness();
59
+ const pinch = new PinchGestureRecognizer({name: 'pinch'});
60
+ const pan = new PanGestureRecognizer({name: 'pan'});
61
+ h.finger(1).down(100, 100, 0, {recognizers: [pinch, pan]});
62
+ h.finger(2).down(200, 100, 0, {recognizers: [pinch, pan]});
63
+ h.finger(1).move(80, 120, 16);
64
+ h.finger(2).move(240, 120, 16);
65
+ expect(h.log.slice(0, 2)).toEqual(['pinch:began@16', 'pan:failed@16']);
66
+ });
67
+
68
+ it('edge pan vs inner pan: in the band the edge pan wins, elsewhere the inner pan runs undelayed', () => {
69
+ const band = createHarness();
70
+ const edge = new ScreenEdgePanGestureRecognizer({name: 'edge'});
71
+ const inner = new PanGestureRecognizer({
72
+ name: 'inner',
73
+ direction: 'horizontal',
74
+ });
75
+ inner.requireToFail(edge);
76
+ // A thin edge view: the edge zone, where the pan takes its axis.
77
+ band.environment.view = {width: edge.edgeRegion, height: 844};
78
+ band.finger(1).down(8, 300, 0, {recognizers: [inner, edge]});
79
+ band.finger(1).drag(60, 2, 60, 6);
80
+ // The edge pan's own hysteresis is 15 px: the second 10 px step.
81
+ expect(band.log.slice(0, 2)).toEqual(['edge:began@20', 'inner:failed@20']);
82
+
83
+ const middle = createHarness();
84
+ const edge2 = new ScreenEdgePanGestureRecognizer({name: 'edge'});
85
+ const inner2 = new PanGestureRecognizer({
86
+ name: 'inner',
87
+ direction: 'horizontal',
88
+ });
89
+ inner2.requireToFail(edge2);
90
+ middle.finger(1).down(150, 300, 0, {recognizers: [inner2, edge2]});
91
+ // Failed at touch-down, before any movement: nothing waits on it.
92
+ expect(middle.log).toEqual(['edge:failed@0']);
93
+ middle.finger(1).drag(60, 2, 60, 6);
94
+ expect(middle.log[1]).toBe('inner:began@10');
95
+ });
96
+
97
+ it('swipe vs pan: the pan wins by default, the swipe when the pan requires it to fail', () => {
98
+ const plain = createHarness();
99
+ const swipe = new SwipeGestureRecognizer({name: 'swipe'});
100
+ const pan = new PanGestureRecognizer({name: 'pan'});
101
+ plain
102
+ .finger(1)
103
+ .down(0, 0, 0, {recognizers: [swipe, pan]})
104
+ .drag(80, 0, 60, 6);
105
+ expect(plain.log.slice(0, 2)).toEqual(['pan:began@10', 'swipe:failed@10']);
106
+
107
+ const ordered = createHarness();
108
+ const swipe2 = new SwipeGestureRecognizer({name: 'swipe'});
109
+ const pan2 = new PanGestureRecognizer({name: 'pan'});
110
+ pan2.requireToFail(swipe2);
111
+ ordered
112
+ .finger(1)
113
+ .down(0, 0, 0, {recognizers: [swipe2, pan2]})
114
+ .drag(80, 0, 60, 8);
115
+ expect(ordered.log.slice(0, 2)).toEqual([
116
+ 'swipe:ended@37.5',
117
+ 'pan:failed@37.5',
118
+ ]);
119
+
120
+ const slow = createHarness();
121
+ const swipe3 = new SwipeGestureRecognizer({name: 'swipe'});
122
+ const pan3 = new PanGestureRecognizer({name: 'pan'});
123
+ pan3.requireToFail(swipe3);
124
+ slow
125
+ .finger(1)
126
+ .down(0, 0, 0, {recognizers: [swipe3, pan3]})
127
+ .drag(30, 0, 300, 3);
128
+ expect(pan3.state).toBe('possible');
129
+ slow.advance(500);
130
+ // The swipe ran out of time; the waiting pan begins with what it has.
131
+ expect(slow.log.slice(0, 2)).toEqual(['swipe:failed@500', 'pan:began@500']);
132
+ });
133
+ });
134
+
135
+ describe('a recognizer waiting on another', () => {
136
+ it('a swipe holds its recognition past its lift and its deadline (spec:AST-094 FR15)', () => {
137
+ const h = createHarness();
138
+ const held = new Held({name: 'held'});
139
+ const swipe = new SwipeGestureRecognizer({name: 'swipe'});
140
+ swipe.requireToFail(held);
141
+ h.finger(1)
142
+ .down(0, 0, 0, {recognizers: [swipe, held]})
143
+ .drag(80, 0, 60, 8)
144
+ .up(70);
145
+ h.advance(UIKIT_SWIPE.maximumDuration.value + 100);
146
+ expect(swipe.state).toBe('possible');
147
+ expect(h.log.some(entry => entry.startsWith('swipe:failed'))).toBe(false);
148
+ held.fail();
149
+ expect(h.log).toContain(
150
+ `swipe:ended@${UIKIT_SWIPE.maximumDuration.value + 100}`,
151
+ );
152
+ expect(swipe.recognizedDirection).toBe('right');
153
+ });
154
+
155
+ it('a pinch keeps its start once it passes the hysteresis, so it begins at the scale its fingers show (spec:AST-094 FR15)', () => {
156
+ const h = createHarness();
157
+ const held = new Held({name: 'held'});
158
+ const pinch = new PinchGestureRecognizer({name: 'pinch'});
159
+ pinch.requireToFail(held);
160
+ h.finger(1).down(100, 100, 0, {recognizers: [pinch, held]});
161
+ h.finger(2).down(200, 100, 0, {recognizers: [pinch, held]});
162
+ // Span 100 -> 120 -> 140 -> 160 while the pinch waits.
163
+ h.finger(2).move(220, 100, 16);
164
+ h.finger(2).move(240, 100, 32);
165
+ h.finger(2).move(260, 100, 48);
166
+ expect(pinch.state).toBe('possible');
167
+ held.fail();
168
+ expect(h.log).toContain('pinch:began@48');
169
+ // Measured against 100 + 8 once, not re-based on every move.
170
+ const hysteresis = UIKIT_PINCH.hysteresis.value;
171
+ expect(pinch.scale).toBeCloseTo(160 / (100 + hysteresis), 5);
172
+ });
173
+ });
174
+
175
+ describe('delivery to the view', () => {
176
+ it('holds the began with delaysTouchesBegan until the recognizer fails', () => {
177
+ const h = createHarness();
178
+ const consumer = new RecordingConsumer();
179
+ const swipe = new SwipeGestureRecognizer({
180
+ name: 'swipe',
181
+ delaysTouchesBegan: true,
182
+ });
183
+ h.finger(1).down(0, 0, 0, {recognizers: [swipe], consumer});
184
+ expect(consumer.events).toEqual([]);
185
+ h.advance(UIKIT_SWIPE.maximumDuration.value);
186
+ expect(consumer.events).toEqual(['began:1']);
187
+ });
188
+ });
@@ -40,7 +40,13 @@ export interface Harness {
40
40
  claims: number[];
41
41
  /** Deferred clicks as they resolved. */
42
42
  deferredClicks: {id: number; allowed: boolean; time: number}[];
43
- environment: {width: number; height: number; rtl: boolean};
43
+ environment: {
44
+ width: number;
45
+ height: number;
46
+ rtl: boolean;
47
+ /** The box every recognizer's view measures, when a test sets one. */
48
+ view?: {left?: number; top?: number; width: number; height: number};
49
+ };
44
50
  finger(id: number): Finger;
45
51
  /** Advance the clock and run every timer due by `time`. */
46
52
  advance(time: number): void;
@@ -51,7 +57,11 @@ export function createHarness(): Harness {
51
57
  const log: string[] = [];
52
58
  const claims: number[] = [];
53
59
  const deferredClicks: Harness['deferredClicks'] = [];
54
- const environment = {width: 390, height: 844, rtl: false};
60
+ const environment: Harness['environment'] = {
61
+ width: 390,
62
+ height: 844,
63
+ rtl: false,
64
+ };
55
65
  const fingers = new Map<number, Finger>();
56
66
  const env: GestureEnvironment = {
57
67
  viewportSize: () => ({
@@ -59,6 +69,15 @@ export function createHarness(): Harness {
59
69
  height: environment.height,
60
70
  }),
61
71
  isRtl: () => environment.rtl,
72
+ viewRect: () =>
73
+ environment.view == null
74
+ ? null
75
+ : {
76
+ left: environment.view.left ?? 0,
77
+ top: environment.view.top ?? 0,
78
+ width: environment.view.width,
79
+ height: environment.view.height,
80
+ },
62
81
  };
63
82
  const arena: GestureArena = new GestureArena({
64
83
  clock,
@@ -46,13 +46,6 @@ export interface NativeScrollProbe {
46
46
  * lets the browser pan that axis).
47
47
  */
48
48
  canScroll(dx: number, dy: number): boolean;
49
- /**
50
- * How much further (px) the scroller that would move for a finger moving
51
- * by (dx, dy) can scroll before it reaches its edge; 0 when none would.
52
- * Lets a reader place the point where the content runs out between two
53
- * samples instead of at the next one.
54
- */
55
- distanceToEdge?(dx: number, dy: number): number;
56
49
  /** Whether any element under the touch can scroll natively at all. */
57
50
  readonly isInsideScroller: boolean;
58
51
  /**
@@ -127,6 +120,17 @@ export interface GestureEventContext {
127
120
  export interface GestureEnvironment {
128
121
  viewportSize(): {readonly width: number; readonly height: number};
129
122
  isRtl(view: unknown): boolean;
123
+ /**
124
+ * The box of a recognizer's view in viewport coordinates, or null when it
125
+ * has none to measure. A screen-edge pan reads it to tell a view within
126
+ * its edge band from any other.
127
+ */
128
+ viewRect?(view: unknown): {
129
+ readonly left: number;
130
+ readonly top: number;
131
+ readonly width: number;
132
+ readonly height: number;
133
+ } | null;
130
134
  }
131
135
 
132
136
  export const DEFAULT_ENVIRONMENT: GestureEnvironment = {
@@ -136,8 +140,18 @@ export const DEFAULT_ENVIRONMENT: GestureEnvironment = {
136
140
  }
137
141
  return {width: window.innerWidth, height: window.innerHeight};
138
142
  },
139
- isRtl() {
140
- return false;
143
+ isRtl(view) {
144
+ if (typeof window === 'undefined' || !(view instanceof Element)) {
145
+ return false;
146
+ }
147
+ return window.getComputedStyle(view).direction === 'rtl';
148
+ },
149
+ viewRect(view) {
150
+ if (typeof Element === 'undefined' || !(view instanceof Element)) {
151
+ return null;
152
+ }
153
+ const {left, top, width, height} = view.getBoundingClientRect();
154
+ return {left, top, width, height};
141
155
  },
142
156
  };
143
157
 
@@ -7,7 +7,10 @@
7
7
  * @input Uses React's external-store subscription over window.matchMedia
8
8
  * and the touch presentation's compact-touch query
9
9
  * @output Resolves popover/bottom-sheet presentation policy
10
- * @position Internal overlay policy shared across component families
10
+ * @position Internal overlay policy shared across component families. A
11
+ * component that also switches input modes under a finger resolves its
12
+ * `presentation` with usePresentation (utils/presentation.ts), whose
13
+ * surface is this one's
11
14
  */
12
15
 
13
16
  import {useCallback, useSyncExternalStore} from 'react';
@@ -288,6 +288,20 @@ export function useMenuHover<T extends HTMLElement = HTMLElement>(
288
288
  }
289
289
  }, [hide, menuRef]);
290
290
 
291
+ // A menu placed in the content can own its own list keys (NavHeadingMenu
292
+ // runs useListFocus over the same `menuitem`s); a key it consumed must not
293
+ // move focus a second time here as it bubbles out (APG menu: one item per
294
+ // arrow press).
295
+ const handleContentKeyDown = useCallback(
296
+ (event: React.KeyboardEvent) => {
297
+ if (event.defaultPrevented) {
298
+ return;
299
+ }
300
+ handleListKeyDown(event);
301
+ },
302
+ [handleListKeyDown],
303
+ );
304
+
291
305
  useEffect(() => {
292
306
  escapeHandlerRef.current = () => {
293
307
  clearTimeouts();
@@ -535,7 +549,7 @@ export function useMenuHover<T extends HTMLElement = HTMLElement>(
535
549
  contentProps: {
536
550
  onMouseEnter: handleContentMouseEnter,
537
551
  onMouseLeave: handleMouseLeave,
538
- onKeyDown: handleListKeyDown,
552
+ onKeyDown: handleContentKeyDown,
539
553
  },
540
554
  menuRef,
541
555
  focusFirst,
@@ -233,6 +233,7 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
233
233
  '@astryx.tabList.label': 'Tabs',
234
234
  '@astryx.table.label': 'Table',
235
235
  '@astryx.table.noData': 'No data',
236
+ '@astryx.table.columnResize.resizeColumn': 'Resize column {label}',
236
237
  '@astryx.table.filter.allPlaceholder': 'All',
237
238
  '@astryx.table.filter.reset': 'Reset',
238
239
  '@astryx.table.filter.apply': 'Apply',
@@ -427,6 +428,7 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
427
428
  '@astryx.tableTree.expandRow': 'Expand row',
428
429
  '@astryx.tableTree.collapseAllRows': 'Collapse all rows',
429
430
  '@astryx.tableTree.expandAllRows': 'Expand all rows',
431
+ '@astryx.textArea.counter': '{count, number}/{limit, number}',
430
432
  '@astryx.textArea.charactersRemaining':
431
433
  '{count, number} {count, plural, one {character} other {characters}} remaining',
432
434
  '@astryx.textArea.charactersOverLimit':
@@ -449,6 +451,8 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
449
451
  '@astryx.multiSelector.allSelected': 'All selected',
450
452
  '@astryx.multiSelector.selectionCount':
451
453
  '{count, number} of {total, number} selected',
454
+ '@astryx.multiSelector.selectedCount':
455
+ '{count, plural, one {# selected} other {# selected}}',
452
456
  '@astryx.multiSelector.createOption': 'Create "{query}"',
453
457
  '@astryx.multiSelector.optionCreated': '{label} created',
454
458
  '@astryx.multiSelector.emptySearchResults': 'No results found',
@@ -239,6 +239,7 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
239
239
  '@astryx.tabList.label': '⟦Ţàƀš⟧',
240
240
  '@astryx.table.label': '⟦Ţàƀłé⟧',
241
241
  '@astryx.table.noData': '⟦Ñó ðàţà⟧',
242
+ '@astryx.table.columnResize.resizeColumn': '⟦Řéšížé çółúɱñ {label}⟧',
242
243
  '@astryx.table.filter.allPlaceholder': '⟦Àłł⟧',
243
244
  '@astryx.table.filter.reset': '⟦Řéšéţ⟧',
244
245
  '@astryx.table.filter.apply': '⟦Àþþłý⟧',
@@ -436,6 +437,7 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
436
437
  '@astryx.tableTree.expandRow': '⟦Éẋþàñð řóŵ⟧',
437
438
  '@astryx.tableTree.collapseAllRows': '⟦Çółłàþšé àłł řóŵš⟧',
438
439
  '@astryx.tableTree.expandAllRows': '⟦Éẋþàñð àłł řóŵš⟧',
440
+ '@astryx.textArea.counter': '⟦{count, number}/{limit, number}⟧',
439
441
  '@astryx.textArea.charactersRemaining':
440
442
  '⟦{count, number} {count, plural, one {character} other {characters}} řéɱàíñíñĝ⟧',
441
443
  '@astryx.textArea.charactersOverLimit':
@@ -458,6 +460,8 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
458
460
  '@astryx.multiSelector.allSelected': '⟦Àłł šéłéçţéð⟧',
459
461
  '@astryx.multiSelector.selectionCount':
460
462
  '⟦{count, number} óƒ {total, number} šéłéçţéð⟧',
463
+ '@astryx.multiSelector.selectedCount':
464
+ '⟦{count, plural, one {# selected} other {# selected}}⟧',
461
465
  '@astryx.multiSelector.createOption': '⟦Çřéàţé "{query}"⟧',
462
466
  '@astryx.multiSelector.optionCreated': '⟦{label} çřéàţéð⟧',
463
467
  '@astryx.multiSelector.emptySearchResults': '⟦Ñó řéšúłţš ƒóúñð⟧',
@@ -81,7 +81,10 @@ await handle.finished; // true if it ran to the end, false if interrupted
81
81
  - Promotion: every animation the animator creates, and every `set`, gives its
82
82
  channels a compositing layer in the same task (inline `will-change`, merged
83
83
  with the element's own), cleared 500 ms after the last motion. Mobile Safari
84
- needs it to run a curve through a blocked main thread.
84
+ needs it to run a curve through a blocked main thread. A pseudo-element is
85
+ promoted through `--astryx-motion-will-change-<pseudo>` on its host; its
86
+ style opts in with
87
+ `willChange: 'var(--astryx-motion-will-change-before, auto)'`.
85
88
  - `atRest(properties)` resolves once none of them is moving, after the last
86
89
  curve ends, whoever started it.
87
90
  - `rebase({y: -shift})` moves properties at once, as a layout shift does; a
@@ -121,6 +124,13 @@ await handle.finished; // true if it ran to the end, false if interrupted
121
124
  - `animatorFor(element)` returns the one animator for an element, so code
122
125
  that animates it from several places (a swipe on x, a removal on y)
123
126
  composes in one `translate` track.
127
+ - `createAnimator(dialog, {pseudoElement: '::backdrop'})` drives a
128
+ pseudo-element; with no inline style to hold it, a static filling animation
129
+ holds its end value until the next animation replaces it, while the
130
+ pseudo-element exists. A dialog's `::backdrop` is made again each time the
131
+ dialog opens, so a presentation `set`s its starting value right after
132
+ `showModal()`, in the same task (a `set` before it lands on the backdrop
133
+ the open replaces).
124
134
 
125
135
  ### How it stays on the compositor
126
136
 
@@ -67,18 +67,23 @@ const requestFrame = (callback: () => void) => {
67
67
  * properties, so the page is modeled here).
68
68
  */
69
69
  let stylesheet: Record<string, string> = {};
70
+ /** The stylesheet's values for a pseudo-element of the element. */
71
+ let pseudoStylesheet: Record<string, string> = {};
70
72
 
71
73
  beforeEach(() => {
72
74
  clock = 1000;
73
75
  recorded = [];
74
76
  frameQueue = [];
75
77
  stylesheet = {};
78
+ pseudoStylesheet = {};
76
79
  vi.spyOn(window, 'getComputedStyle').mockImplementation(
77
- element =>
80
+ (element, pseudo) =>
78
81
  ({
79
82
  getPropertyValue: (property: string) =>
80
- (element as HTMLElement).style.getPropertyValue(property) ||
81
- (stylesheet[property] ?? ''),
83
+ pseudo != null
84
+ ? (pseudoStylesheet[property] ?? '')
85
+ : (element as HTMLElement).style.getPropertyValue(property) ||
86
+ (stylesheet[property] ?? ''),
82
87
  }) as unknown as CSSStyleDeclaration,
83
88
  );
84
89
  vi.useFakeTimers();
@@ -799,6 +804,12 @@ describe('animatorFor: one animator per element', () => {
799
804
  const b = document.createElement('div');
800
805
  expect(animatorFor(a)).toBe(animatorFor(a));
801
806
  expect(animatorFor(a)).not.toBe(animatorFor(b));
807
+ expect(animatorFor(a, {pseudoElement: '::backdrop'})).toBe(
808
+ animatorFor(a, {pseudoElement: '::backdrop'}),
809
+ );
810
+ expect(animatorFor(a, {pseudoElement: '::backdrop'})).not.toBe(
811
+ animatorFor(a),
812
+ );
802
813
  });
803
814
 
804
815
  it('composes two callers’ axes in one translate track', async () => {
@@ -902,6 +913,24 @@ describe('promotion: a compositing layer while it moves', () => {
902
913
  expect(element.style.willChange).toBe('');
903
914
  });
904
915
 
916
+ it('promotes a pseudo-element through a custom property on its host', async () => {
917
+ const {animator, element} = setup({pseudoElement: '::backdrop'});
918
+ animator.set({opacity: 0});
919
+ // set() promotes a pseudo-element too.
920
+ expect(
921
+ element.style.getPropertyValue('--astryx-motion-will-change-backdrop'),
922
+ ).toBe('opacity');
923
+ animator.animate({opacity: 1}, {spring: springs.alertPresent});
924
+ expect(
925
+ element.style.getPropertyValue('--astryx-motion-will-change-backdrop'),
926
+ ).toBe('opacity');
927
+ expect(element.style.willChange).toBe('');
928
+ await advance(5000 + PROMOTION_LINGER_MS);
929
+ expect(
930
+ element.style.getPropertyValue('--astryx-motion-will-change-backdrop'),
931
+ ).toBe('');
932
+ });
933
+
905
934
  it('ends promotion on the element it leaves', () => {
906
935
  const {animator, element} = setup();
907
936
  animator.animate({x: 50}, {spring: springs.settle});
@@ -1937,6 +1966,75 @@ describe('Reduced Motion (design:reduced-motion)', () => {
1937
1966
  expect(element.style.translate).toBe('100px 0px');
1938
1967
  });
1939
1968
 
1969
+ it("a pseudo-element's dip borrows its own opacity and hands it back without touching the host", async () => {
1970
+ for (const role of [undefined, 'exit'] as const) {
1971
+ const {animator, element} = setup({
1972
+ reduced: true,
1973
+ coarse: true,
1974
+ pseudoElement: '::before',
1975
+ });
1976
+ element.style.opacity = '0.5'; // the host's own, untouched
1977
+ pseudoStylesheet = {opacity: '0.3'};
1978
+ animator.animate(
1979
+ {x: 100},
1980
+ {spring: springs.navigationPush},
1981
+ role == null ? {} : {crossfadeAs: role},
1982
+ );
1983
+ await commitFrame(clock);
1984
+ const [fadeLayer] = live('opacity');
1985
+ const values = fadeLayer.keyframes.map(k => Number(k.opacity));
1986
+ expect(Math.max(...values)).toBeCloseTo(0.3, 9);
1987
+ await advance(fade * 2 + END_HOLD_MS + 1);
1988
+ expect(animator.get('x')).toBe(100);
1989
+ // The pseudo-element's stylesheet owns its opacity again; the host
1990
+ // keeps its own.
1991
+ expect(live('opacity')).toEqual([]);
1992
+ expect(element.style.opacity).toBe('0.5');
1993
+ }
1994
+ });
1995
+
1996
+ it("a pseudo-element's identity hold follows a node replacement", () => {
1997
+ const {animator, element} = setup({pseudoElement: '::backdrop'});
1998
+ pseudoStylesheet = {opacity: '0'};
1999
+ animator.set({opacity: 1});
2000
+ animator.attach(null);
2001
+ const next = document.createElement('div');
2002
+ document.body.append(next);
2003
+ animator.attach(next);
2004
+ const held = live('opacity').filter(
2005
+ animation => animation.element === next,
2006
+ );
2007
+ expect(held).toHaveLength(1);
2008
+ expect(Number(held[0].keyframes.at(-1)!.opacity)).toBe(1);
2009
+ expect(live('opacity').some(a => a.element === element)).toBe(false);
2010
+ next.remove();
2011
+ });
2012
+
2013
+ it('a pseudo-element shown over a hiding stylesheet keeps its own opacity through a move', async () => {
2014
+ for (const role of [undefined, 'entrance'] as const) {
2015
+ const {animator} = setup({
2016
+ reduced: true,
2017
+ coarse: true,
2018
+ pseudoElement: '::backdrop',
2019
+ });
2020
+ pseudoStylesheet = {opacity: '0'};
2021
+ animator.set({opacity: 1});
2022
+ animator.animate(
2023
+ {x: 100},
2024
+ {spring: springs.navigationPush},
2025
+ role == null ? {} : {crossfadeAs: role},
2026
+ );
2027
+ await commitFrame(clock);
2028
+ for (let step = 0; step < 8; step++) {
2029
+ await advance(fade / 4);
2030
+ }
2031
+ expect(animator.get('x')).toBe(100);
2032
+ expect(animator.get('opacity')).toBe(1);
2033
+ const [held] = live('opacity');
2034
+ expect(Number(held.keyframes.at(-1)?.opacity)).toBe(1);
2035
+ }
2036
+ });
2037
+
1940
2038
  it('a move dips: fades out, lands while transparent, fades back in', async () => {
1941
2039
  const {animator, element} = reduced();
1942
2040
  animator.animate({x: 100}, {spring: springs.navigationPush});
@@ -2555,6 +2653,56 @@ describe('decay: momentum curves (a coast)', () => {
2555
2653
  });
2556
2654
  });
2557
2655
 
2656
+ describe('pseudoElement: a dialog\u2019s ::backdrop', () => {
2657
+ it('targets the pseudo-element, writes no inline style and holds the end state', async () => {
2658
+ const {animator, element} = setup({pseudoElement: '::backdrop'});
2659
+ animator.set({opacity: 0});
2660
+ const [held] = live('opacity');
2661
+ expect(held.options).toMatchObject({
2662
+ fill: 'both',
2663
+ pseudoElement: '::backdrop',
2664
+ });
2665
+ expect(held.keyframes.map(k => k.opacity)).toEqual(['0', '0']);
2666
+ expect(element.style.opacity).toBe('');
2667
+
2668
+ animator.animate({opacity: 1}, {spring: springs.alertPresent});
2669
+ // The new animation exists before the hold goes: no frame in between.
2670
+ expect(held.cancelled).toBe(true);
2671
+ const [fade] = live('opacity');
2672
+ expect(fade.options).toMatchObject({
2673
+ fill: 'both',
2674
+ pseudoElement: '::backdrop',
2675
+ });
2676
+ expect(fade.keyframes[fade.keyframes.length - 1].opacity).toBe('1');
2677
+ expect(element.style.opacity).toBe('');
2678
+ await advance(Number(fade.options.duration) + 1000);
2679
+ // Past the curve's end, a static animation holds the end value.
2680
+ expect(fade.cancelled).toBe(true);
2681
+ const [rest] = live('opacity');
2682
+ expect(rest.options).toMatchObject({
2683
+ fill: 'both',
2684
+ pseudoElement: '::backdrop',
2685
+ });
2686
+ expect(rest.keyframes.map(k => k.opacity)).toEqual(['1', '1']);
2687
+ expect(animator.get('opacity')).toBe(1);
2688
+ });
2689
+
2690
+ it('an exit holds 0 until the next animation replaces it', async () => {
2691
+ const {animator} = setup({
2692
+ pseudoElement: '::backdrop',
2693
+ initial: {opacity: 1},
2694
+ });
2695
+ animator.animate({opacity: 0}, {spring: springs.alertDismiss});
2696
+ const [fade] = live('opacity');
2697
+ await advance(Number(fade.options.duration) + 1000);
2698
+ const [rest] = live('opacity');
2699
+ expect(rest.keyframes.map(k => k.opacity)).toEqual(['0', '0']);
2700
+ animator.animate({opacity: 1}, {spring: springs.alertPresent});
2701
+ expect(rest.cancelled).toBe(true);
2702
+ expect(live('opacity')).toHaveLength(1);
2703
+ });
2704
+ });
2705
+
2558
2706
  describe('attach', () => {
2559
2707
  it('keeps state without an element and resumes the curve on attach', () => {
2560
2708
  const animator = createAnimator(null, {