@astryxdesign/core 0.7.5 → 0.7.6-canary.0d71e8d

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 (912) 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/AppShell/AppShell.d.ts.map +1 -1
  5. package/dist/AppShell/AppShell.js +183 -5
  6. package/dist/AppShell/observeMainOverflow.d.ts +9 -0
  7. package/dist/AppShell/observeMainOverflow.d.ts.map +1 -0
  8. package/dist/AppShell/observeMainOverflow.js +125 -0
  9. package/dist/Avatar/Avatar.d.ts.map +1 -1
  10. package/dist/Avatar/Avatar.js +12 -3
  11. package/dist/Badge/Badge.d.ts.map +1 -1
  12. package/dist/Badge/Badge.js +7 -1
  13. package/dist/BottomSheet/BottomSheet.js +1 -1
  14. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  15. package/dist/BottomSheet/BottomSheetPanel.js +2 -1
  16. package/dist/BottomSheet/useSheetGestures.d.ts +12 -0
  17. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
  18. package/dist/BottomSheet/useSheetGestures.js +30 -3
  19. package/dist/Button/Button.js +4 -4
  20. package/dist/Carousel/Carousel.d.ts +3 -1
  21. package/dist/Carousel/Carousel.d.ts.map +1 -1
  22. package/dist/Carousel/Carousel.js +12 -3
  23. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  24. package/dist/CheckboxInput/CheckboxInput.js +6 -5
  25. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  26. package/dist/ClickableCard/ClickableCard.js +37 -4
  27. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  28. package/dist/ComplexSelector/ComplexSelector.js +41 -4
  29. package/dist/DateInput/DateInput.d.ts.map +1 -1
  30. package/dist/DateInput/DateInput.js +12 -4
  31. package/dist/DateInput/MonthScroller.d.ts.map +1 -1
  32. package/dist/DateInput/MonthScroller.js +101 -15
  33. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  34. package/dist/DateInput/TouchDateField.js +8 -3
  35. package/dist/DateInput/Wheel.d.ts.map +1 -1
  36. package/dist/DateInput/Wheel.js +11 -7
  37. package/dist/DateInput/usePointerDragScroll.d.ts +10 -1
  38. package/dist/DateInput/usePointerDragScroll.d.ts.map +1 -1
  39. package/dist/DateInput/usePointerDragScroll.js +25 -9
  40. package/dist/DateInput/useScrollSettle.d.ts +3 -1
  41. package/dist/DateInput/useScrollSettle.d.ts.map +1 -1
  42. package/dist/DateInput/useScrollSettle.js +37 -8
  43. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  44. package/dist/DateRangeInput/DateRangeInput.js +1 -1
  45. package/dist/DateTimeInput/DateTimeInput.d.ts +5 -2
  46. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  47. package/dist/DateTimeInput/DateTimeInput.js +23 -12
  48. package/dist/DateTimeInput/NativeTimeSegment.d.ts.map +1 -1
  49. package/dist/DateTimeInput/NativeTimeSegment.js +5 -4
  50. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  51. package/dist/DateTimeInput/TouchDateTimeField.js +4 -3
  52. package/dist/Dialog/Dialog.d.ts.map +1 -1
  53. package/dist/Dialog/Dialog.js +34 -9
  54. package/dist/Dialog/DialogHeader.d.ts.map +1 -1
  55. package/dist/Dialog/DialogHeader.js +5 -4
  56. package/dist/Divider/Divider.d.ts.map +1 -1
  57. package/dist/Divider/Divider.js +2 -1
  58. package/dist/Drawer/Drawer.d.ts +1 -0
  59. package/dist/Drawer/Drawer.d.ts.map +1 -1
  60. package/dist/Drawer/Drawer.js +7 -2
  61. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  62. package/dist/DropdownMenu/DropdownMenu.js +35 -22
  63. package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts.map +1 -1
  64. package/dist/DropdownMenu/MenuBottomSheetActionList.js +6 -5
  65. package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts +20 -2
  66. package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts.map +1 -1
  67. package/dist/DropdownMenu/lazyMenuBottomSheet.js +31 -3
  68. package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -1
  69. package/dist/DropdownMenu/renderDropdownItems.js +21 -16
  70. package/dist/EmptyState/EmptyState.d.ts.map +1 -1
  71. package/dist/EmptyState/EmptyState.js +2 -1
  72. package/dist/Field/FieldLabel.d.ts +1 -1
  73. package/dist/Field/FieldLabel.d.ts.map +1 -1
  74. package/dist/Field/FieldLabel.js +4 -4
  75. package/dist/Field/InputClearButton.js +3 -3
  76. package/dist/Field/PanelSearchInput.js +2 -2
  77. package/dist/FieldStatus/FieldStatus.d.ts +3 -0
  78. package/dist/FieldStatus/FieldStatus.d.ts.map +1 -1
  79. package/dist/FieldStatus/FieldStatus.js +3 -2
  80. package/dist/FileInput/FileInput.d.ts +3 -0
  81. package/dist/FileInput/FileInput.d.ts.map +1 -1
  82. package/dist/FileInput/FileInput.js +34 -13
  83. package/dist/Heading/Heading.js +1 -1
  84. package/dist/Icon/Icon.js +1 -1
  85. package/dist/Indicator/CheckIndicator.d.ts.map +1 -1
  86. package/dist/Indicator/CheckIndicator.js +2 -1
  87. package/dist/Indicator/CheckboxIndicator.d.ts.map +1 -1
  88. package/dist/Indicator/CheckboxIndicator.js +3 -1
  89. package/dist/Indicator/RadioIndicator.d.ts.map +1 -1
  90. package/dist/Indicator/RadioIndicator.js +3 -1
  91. package/dist/Item/Item.d.ts.map +1 -1
  92. package/dist/Item/Item.js +21 -7
  93. package/dist/Item/ItemSwipeLayer.js +1 -1
  94. package/dist/Layer/popoverOperations.d.ts +27 -0
  95. package/dist/Layer/popoverOperations.d.ts.map +1 -0
  96. package/dist/Layer/popoverOperations.js +159 -0
  97. package/dist/Layer/useLayer.d.ts +1 -1
  98. package/dist/Layer/useLayer.d.ts.map +1 -1
  99. package/dist/Layer/useLayer.js +120 -65
  100. package/dist/Layout/Layout.js +1 -1
  101. package/dist/Layout/LayoutContent.js +1 -1
  102. package/dist/Layout/LayoutFooter.js +1 -1
  103. package/dist/Layout/LayoutHeader.js +1 -1
  104. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  105. package/dist/Lightbox/Lightbox.js +5 -0
  106. package/dist/Lightbox/LightboxViewport.a11y.browser.spec.js +5 -4
  107. package/dist/Link/Link.d.ts.map +1 -1
  108. package/dist/Link/Link.js +8 -3
  109. package/dist/List/List.js +1 -1
  110. package/dist/List/ListItem.js +3 -3
  111. package/dist/Markdown/plugins/headingLinks.a11y.chromium.spec.js +1 -1
  112. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  113. package/dist/MobileNav/MobileNav.js +7 -2
  114. package/dist/MultiSelector/CreateRow.a11y.chromium.spec.js +7 -6
  115. package/dist/MultiSelector/MultiSelector.d.ts +1 -1
  116. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  117. package/dist/MultiSelector/MultiSelector.js +26 -10
  118. package/dist/NavMenu/NavHeadingMenuItem.d.ts.map +1 -1
  119. package/dist/NavMenu/NavHeadingMenuItem.js +5 -29
  120. package/dist/NumberInput/NumberInput.d.ts +3 -1
  121. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  122. package/dist/NumberInput/NumberInput.js +26 -5
  123. package/dist/NumberInput/numberParser.d.ts +3 -0
  124. package/dist/NumberInput/numberParser.d.ts.map +1 -1
  125. package/dist/NumberInput/numberParser.js +7 -44
  126. package/dist/Outline/Outline.d.ts.map +1 -1
  127. package/dist/Outline/Outline.js +27 -0
  128. package/dist/Popover/Popover.d.ts.map +1 -1
  129. package/dist/Popover/Popover.js +32 -12
  130. package/dist/Popover/usePopover.d.ts.map +1 -1
  131. package/dist/Popover/usePopover.js +10 -3
  132. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  133. package/dist/PowerSearch/PowerSearch.js +37 -30
  134. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  135. package/dist/ProgressBar/ProgressBar.js +16 -12
  136. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  137. package/dist/RadioList/RadioListItem.js +11 -3
  138. package/dist/Resizable/useResizable.d.ts.map +1 -1
  139. package/dist/Resizable/useResizable.js +28 -2
  140. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  141. package/dist/SegmentedControl/SegmentedControlItem.js +4 -1
  142. package/dist/Selector/useSelectorPresentation.d.ts +8 -1
  143. package/dist/Selector/useSelectorPresentation.d.ts.map +1 -1
  144. package/dist/Selector/useSelectorPresentation.js +20 -4
  145. package/dist/SideNav/SideNavItem.d.ts +2 -2
  146. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  147. package/dist/SideNav/SideNavItem.js +18 -7
  148. package/dist/Skeleton/Skeleton.d.ts.map +1 -1
  149. package/dist/Skeleton/Skeleton.js +1 -1
  150. package/dist/Slider/Slider.d.ts.map +1 -1
  151. package/dist/Slider/Slider.js +10 -2
  152. package/dist/Spinner/Spinner.d.ts.map +1 -1
  153. package/dist/Spinner/Spinner.js +3 -3
  154. package/dist/Stack/Stack.js +1 -1
  155. package/dist/Stack/StackItem.js +1 -1
  156. package/dist/Stepper/Step.d.ts.map +1 -1
  157. package/dist/Stepper/Step.js +26 -6
  158. package/dist/Switch/Switch.d.ts.map +1 -1
  159. package/dist/Switch/Switch.js +10 -3
  160. package/dist/TabList/TabList.d.ts.map +1 -1
  161. package/dist/TabList/TabList.js +16 -1
  162. package/dist/Table/Table.d.ts.map +1 -1
  163. package/dist/Table/Table.js +36 -10
  164. package/dist/Table/plugins/columnResize/useTableColumnResize.d.ts.map +1 -1
  165. package/dist/Table/plugins/columnResize/useTableColumnResize.js +9 -1
  166. package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
  167. package/dist/Table/plugins/sortable/useTableSortable.js +46 -58
  168. package/dist/Text/Text.js +1 -1
  169. package/dist/Text/useTruncation.d.ts.map +1 -1
  170. package/dist/Text/useTruncation.js +23 -5
  171. package/dist/TextArea/TextArea.d.ts +1 -1
  172. package/dist/TextArea/TextArea.d.ts.map +1 -1
  173. package/dist/TextArea/TextArea.js +13 -2
  174. package/dist/TextInput/TextInput.d.ts.map +1 -1
  175. package/dist/TextInput/TextInput.js +3 -3
  176. package/dist/TimeInput/TimeInput.d.ts +6 -2
  177. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  178. package/dist/TimeInput/TimeInput.js +21 -14
  179. package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
  180. package/dist/TimeInput/TouchTimeField.js +5 -4
  181. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  182. package/dist/Timestamp/Timestamp.js +2 -2
  183. package/dist/Timestamp/TimestampHoverCard.js +4 -4
  184. package/dist/Toast/Toast.d.ts.map +1 -1
  185. package/dist/Toast/Toast.js +60 -20
  186. package/dist/Toast/ToastViewport.d.ts.map +1 -1
  187. package/dist/Toast/ToastViewport.js +101 -54
  188. package/dist/ToggleButton/ToggleButtonGroup.d.ts +7 -1
  189. package/dist/ToggleButton/ToggleButtonGroup.d.ts.map +1 -1
  190. package/dist/ToggleButton/ToggleButtonGroup.js +16 -2
  191. package/dist/Token/Token.d.ts +1 -1
  192. package/dist/Token/Token.d.ts.map +1 -1
  193. package/dist/Token/Token.js +52 -5
  194. package/dist/Token/TokenLink.d.ts.map +1 -1
  195. package/dist/Token/TokenLink.js +14 -2
  196. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  197. package/dist/Tokenizer/Tokenizer.js +44 -7
  198. package/dist/Tokenizer/TokenizerBottomSheet.js +3 -3
  199. package/dist/Tokenizer/TokenizerClearAllMessageContext.d.ts +2 -0
  200. package/dist/Tokenizer/TokenizerClearAllMessageContext.d.ts.map +1 -0
  201. package/dist/Tokenizer/TokenizerClearAllMessageContext.js +17 -0
  202. package/dist/Toolbar/Toolbar.d.ts.map +1 -1
  203. package/dist/Toolbar/Toolbar.js +2 -1
  204. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  205. package/dist/TopNav/TopNavMegaMenu.js +3 -0
  206. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  207. package/dist/TopNav/TopNavMenu.js +1 -0
  208. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  209. package/dist/TreeList/TreeListItem.js +27 -4
  210. package/dist/Typeahead/BaseTypeahead.d.ts +4 -4
  211. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  212. package/dist/Typeahead/BaseTypeahead.js +43 -7
  213. package/dist/Typeahead/Typeahead.d.ts +1 -1
  214. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  215. package/dist/Typeahead/Typeahead.js +43 -9
  216. package/dist/astryx.css +25 -4
  217. package/dist/gestures/GestureArena.d.ts +32 -1
  218. package/dist/gestures/GestureArena.d.ts.map +1 -1
  219. package/dist/gestures/GestureArena.js +110 -9
  220. package/dist/gestures/GestureHost.d.ts +351 -0
  221. package/dist/gestures/GestureHost.d.ts.map +1 -0
  222. package/dist/gestures/GestureHost.js +1773 -0
  223. package/dist/gestures/GestureRecognizer.d.ts +13 -1
  224. package/dist/gestures/GestureRecognizer.d.ts.map +1 -1
  225. package/dist/gestures/GestureRecognizer.js +19 -1
  226. package/dist/gestures/LongPressGestureRecognizer.d.ts +1 -0
  227. package/dist/gestures/LongPressGestureRecognizer.d.ts.map +1 -1
  228. package/dist/gestures/LongPressGestureRecognizer.js +3 -0
  229. package/dist/gestures/PanGestureRecognizer.d.ts +1 -0
  230. package/dist/gestures/PanGestureRecognizer.d.ts.map +1 -1
  231. package/dist/gestures/PanGestureRecognizer.js +3 -0
  232. package/dist/gestures/PinchGestureRecognizer.d.ts +31 -0
  233. package/dist/gestures/PinchGestureRecognizer.d.ts.map +1 -0
  234. package/dist/gestures/PinchGestureRecognizer.js +128 -0
  235. package/dist/gestures/PressTracker.d.ts +112 -0
  236. package/dist/gestures/PressTracker.d.ts.map +1 -0
  237. package/dist/gestures/PressTracker.js +267 -0
  238. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts +71 -0
  239. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts.map +1 -0
  240. package/dist/gestures/ScreenEdgePanGestureRecognizer.js +223 -0
  241. package/dist/gestures/SwipeGestureRecognizer.d.ts +37 -0
  242. package/dist/gestures/SwipeGestureRecognizer.d.ts.map +1 -0
  243. package/dist/gestures/SwipeGestureRecognizer.js +148 -0
  244. package/dist/gestures/TapGestureRecognizer.d.ts +1 -0
  245. package/dist/gestures/TapGestureRecognizer.d.ts.map +1 -1
  246. package/dist/gestures/TapGestureRecognizer.js +3 -0
  247. package/dist/gestures/VelocityTracker.d.ts +6 -2
  248. package/dist/gestures/VelocityTracker.d.ts.map +1 -1
  249. package/dist/gestures/VelocityTracker.js +7 -3
  250. package/dist/gestures/haptics.d.ts +65 -0
  251. package/dist/gestures/haptics.d.ts.map +1 -0
  252. package/dist/gestures/haptics.js +182 -0
  253. package/dist/gestures/index.d.ts +37 -0
  254. package/dist/gestures/index.d.ts.map +1 -0
  255. package/dist/gestures/index.js +25 -0
  256. package/dist/gestures/nativeScroll.d.ts +42 -0
  257. package/dist/gestures/nativeScroll.d.ts.map +1 -0
  258. package/dist/gestures/nativeScroll.js +228 -0
  259. package/dist/gestures/touchAction.d.ts +60 -0
  260. package/dist/gestures/touchAction.d.ts.map +1 -0
  261. package/dist/gestures/touchAction.js +157 -0
  262. package/dist/gestures/touchActionRule.d.ts +34 -0
  263. package/dist/gestures/touchActionRule.d.ts.map +1 -0
  264. package/dist/gestures/touchActionRule.js +140 -0
  265. package/dist/gestures/types.d.ts +18 -7
  266. package/dist/gestures/types.d.ts.map +1 -1
  267. package/dist/gestures/types.js +22 -2
  268. package/dist/gestures/useGestureRecognizer.d.ts +56 -0
  269. package/dist/gestures/useGestureRecognizer.d.ts.map +1 -0
  270. package/dist/gestures/useGestureRecognizer.js +226 -0
  271. package/dist/hooks/index.d.ts +1 -1
  272. package/dist/hooks/index.d.ts.map +1 -1
  273. package/dist/hooks/modalAnnounce.d.ts.map +1 -1
  274. package/dist/hooks/modalAnnounce.js +107 -61
  275. package/dist/hooks/scrollKeyboardDelegation.d.ts.map +1 -1
  276. package/dist/hooks/scrollKeyboardDelegation.js +7 -3
  277. package/dist/hooks/useAdaptivePresentation.d.ts.map +1 -1
  278. package/dist/hooks/useAdaptivePresentation.js +4 -1
  279. package/dist/hooks/useAnnounce.d.ts +101 -42
  280. package/dist/hooks/useAnnounce.d.ts.map +1 -1
  281. package/dist/hooks/useAnnounce.js +161 -75
  282. package/dist/hooks/useAnnounceRenderedText.d.ts +11 -3
  283. package/dist/hooks/useAnnounceRenderedText.d.ts.map +1 -1
  284. package/dist/hooks/useAnnounceRenderedText.js +5 -3
  285. package/dist/hooks/useBusyAnnouncement.d.ts.map +1 -1
  286. package/dist/hooks/useBusyAnnouncement.js +6 -12
  287. package/dist/hooks/useMenuHover.d.ts.map +1 -1
  288. package/dist/hooks/useMenuHover.js +12 -1
  289. package/dist/hooks/useScrollOverflow.d.ts +2 -1
  290. package/dist/hooks/useScrollOverflow.d.ts.map +1 -1
  291. package/dist/hooks/useScrollOverflow.js +56 -10
  292. package/dist/i18n/generated-locales/af-ZA.generated.d.ts.map +1 -1
  293. package/dist/i18n/generated-locales/af-ZA.generated.js +0 -5
  294. package/dist/i18n/generated-locales/ar-SA.generated.d.ts.map +1 -1
  295. package/dist/i18n/generated-locales/ar-SA.generated.js +0 -5
  296. package/dist/i18n/generated-locales/ca-ES.generated.d.ts.map +1 -1
  297. package/dist/i18n/generated-locales/ca-ES.generated.js +0 -5
  298. package/dist/i18n/generated-locales/cs-CZ.generated.d.ts.map +1 -1
  299. package/dist/i18n/generated-locales/cs-CZ.generated.js +0 -5
  300. package/dist/i18n/generated-locales/da-DK.generated.d.ts.map +1 -1
  301. package/dist/i18n/generated-locales/da-DK.generated.js +0 -5
  302. package/dist/i18n/generated-locales/de-DE.generated.d.ts.map +1 -1
  303. package/dist/i18n/generated-locales/de-DE.generated.js +0 -5
  304. package/dist/i18n/generated-locales/el-GR.generated.d.ts.map +1 -1
  305. package/dist/i18n/generated-locales/el-GR.generated.js +0 -5
  306. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  307. package/dist/i18n/generated-locales/en.generated.js +8 -5
  308. package/dist/i18n/generated-locales/es-ES.generated.d.ts.map +1 -1
  309. package/dist/i18n/generated-locales/es-ES.generated.js +0 -5
  310. package/dist/i18n/generated-locales/fi-FI.generated.d.ts.map +1 -1
  311. package/dist/i18n/generated-locales/fi-FI.generated.js +0 -5
  312. package/dist/i18n/generated-locales/fr-FR.generated.d.ts.map +1 -1
  313. package/dist/i18n/generated-locales/fr-FR.generated.js +0 -5
  314. package/dist/i18n/generated-locales/he-IL.generated.d.ts.map +1 -1
  315. package/dist/i18n/generated-locales/he-IL.generated.js +0 -5
  316. package/dist/i18n/generated-locales/hu-HU.generated.d.ts.map +1 -1
  317. package/dist/i18n/generated-locales/hu-HU.generated.js +0 -5
  318. package/dist/i18n/generated-locales/it-IT.generated.d.ts.map +1 -1
  319. package/dist/i18n/generated-locales/it-IT.generated.js +0 -5
  320. package/dist/i18n/generated-locales/ja-JP.generated.d.ts.map +1 -1
  321. package/dist/i18n/generated-locales/ja-JP.generated.js +0 -5
  322. package/dist/i18n/generated-locales/ko-KR.generated.d.ts.map +1 -1
  323. package/dist/i18n/generated-locales/ko-KR.generated.js +0 -5
  324. package/dist/i18n/generated-locales/nl-NL.generated.d.ts.map +1 -1
  325. package/dist/i18n/generated-locales/nl-NL.generated.js +0 -5
  326. package/dist/i18n/generated-locales/no-NO.generated.d.ts.map +1 -1
  327. package/dist/i18n/generated-locales/no-NO.generated.js +0 -5
  328. package/dist/i18n/generated-locales/pl-PL.generated.d.ts.map +1 -1
  329. package/dist/i18n/generated-locales/pl-PL.generated.js +0 -5
  330. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  331. package/dist/i18n/generated-locales/pseudo.generated.js +8 -5
  332. package/dist/i18n/generated-locales/pt-BR.generated.d.ts.map +1 -1
  333. package/dist/i18n/generated-locales/pt-BR.generated.js +0 -5
  334. package/dist/i18n/generated-locales/pt-PT.generated.d.ts.map +1 -1
  335. package/dist/i18n/generated-locales/pt-PT.generated.js +0 -5
  336. package/dist/i18n/generated-locales/ro-RO.generated.d.ts.map +1 -1
  337. package/dist/i18n/generated-locales/ro-RO.generated.js +0 -5
  338. package/dist/i18n/generated-locales/ru-RU.generated.d.ts.map +1 -1
  339. package/dist/i18n/generated-locales/ru-RU.generated.js +0 -5
  340. package/dist/i18n/generated-locales/sr-SP.generated.d.ts.map +1 -1
  341. package/dist/i18n/generated-locales/sr-SP.generated.js +0 -5
  342. package/dist/i18n/generated-locales/sv-SE.generated.d.ts.map +1 -1
  343. package/dist/i18n/generated-locales/sv-SE.generated.js +0 -5
  344. package/dist/i18n/generated-locales/tr-TR.generated.d.ts.map +1 -1
  345. package/dist/i18n/generated-locales/tr-TR.generated.js +0 -5
  346. package/dist/i18n/generated-locales/uk-UA.generated.d.ts.map +1 -1
  347. package/dist/i18n/generated-locales/uk-UA.generated.js +0 -5
  348. package/dist/i18n/generated-locales/vi-VN.generated.d.ts.map +1 -1
  349. package/dist/i18n/generated-locales/vi-VN.generated.js +0 -5
  350. package/dist/i18n/generated-locales/zh-CN.generated.d.ts.map +1 -1
  351. package/dist/i18n/generated-locales/zh-CN.generated.js +0 -5
  352. package/dist/i18n/generated-locales/zh-TW.generated.d.ts.map +1 -1
  353. package/dist/i18n/generated-locales/zh-TW.generated.js +0 -5
  354. package/dist/i18n/useListFormatter.d.ts +9 -0
  355. package/dist/i18n/useListFormatter.d.ts.map +1 -0
  356. package/dist/i18n/useListFormatter.js +41 -0
  357. package/dist/motion/animator.d.ts +83 -8
  358. package/dist/motion/animator.d.ts.map +1 -1
  359. package/dist/motion/animator.js +701 -119
  360. package/dist/motion/coast.d.ts +4 -1
  361. package/dist/motion/coast.d.ts.map +1 -1
  362. package/dist/motion/coast.js +4 -2
  363. package/dist/motion/reducedMotion.d.ts.map +1 -1
  364. package/dist/motion/reducedMotion.js +6 -2
  365. package/dist/motion/reducedMotionPlan.d.ts +7 -0
  366. package/dist/motion/reducedMotionPlan.d.ts.map +1 -1
  367. package/dist/motion/reducedMotionPlan.js +14 -1
  368. package/dist/motion/resolveMotion.d.ts +42 -0
  369. package/dist/motion/resolveMotion.d.ts.map +1 -0
  370. package/dist/motion/resolveMotion.js +43 -0
  371. package/dist/motion/roleValue.d.ts +2 -1
  372. package/dist/motion/roleValue.d.ts.map +1 -1
  373. package/dist/motion/roleValue.js +2 -1
  374. package/dist/motion/uikit/timing.d.ts +7 -1
  375. package/dist/motion/uikit/timing.d.ts.map +1 -1
  376. package/dist/motion/uikit/timing.js +19 -1
  377. package/dist/theme/defineTheme.d.ts +19 -5
  378. package/dist/theme/defineTheme.d.ts.map +1 -1
  379. package/dist/theme/defineTheme.js +64 -4
  380. package/dist/theme/domainTokens/domainTokenDefaults.d.ts +3 -0
  381. package/dist/theme/domainTokens/domainTokenDefaults.d.ts.map +1 -0
  382. package/dist/theme/domainTokens/domainTokenDefaults.js +19 -0
  383. package/dist/theme/domainTokens/index.d.ts +5 -4
  384. package/dist/theme/domainTokens/index.d.ts.map +1 -1
  385. package/dist/theme/domainTokens/index.js +5 -9
  386. package/dist/theme/index.d.ts +1 -0
  387. package/dist/theme/index.d.ts.map +1 -1
  388. package/dist/theme/motionKnobs.d.ts +56 -0
  389. package/dist/theme/motionKnobs.d.ts.map +1 -0
  390. package/dist/theme/motionKnobs.js +48 -0
  391. package/dist/theme/resolveThemeValues.d.ts +11 -4
  392. package/dist/theme/resolveThemeValues.d.ts.map +1 -1
  393. package/dist/theme/resolveThemeValues.js +40 -4
  394. package/dist/theme/themeAdaptations.d.ts +7 -3
  395. package/dist/theme/themeAdaptations.d.ts.map +1 -1
  396. package/dist/theme/themeAdaptations.js +36 -7
  397. package/dist/theme/themeMotion.d.ts +96 -0
  398. package/dist/theme/themeMotion.d.ts.map +1 -0
  399. package/dist/theme/themeMotion.js +177 -0
  400. package/dist/theme/tokenReferences.d.ts +31 -0
  401. package/dist/theme/tokenReferences.d.ts.map +1 -0
  402. package/dist/theme/tokenReferences.js +132 -0
  403. package/dist/theme/tokens.d.ts +1 -0
  404. package/dist/theme/tokens.d.ts.map +1 -1
  405. package/dist/theme/tokens.js +9 -129
  406. package/dist/theme/tokens.stylex.d.ts +12 -0
  407. package/dist/theme/tokens.stylex.d.ts.map +1 -1
  408. package/dist/theme/tokens.stylex.js +12 -0
  409. package/dist/utils/focusOutline.stylex.d.ts +6 -0
  410. package/dist/utils/focusOutline.stylex.d.ts.map +1 -1
  411. package/dist/utils/focusOutline.stylex.js +7 -0
  412. package/dist/utils/focusReturn.d.ts +17 -1
  413. package/dist/utils/focusReturn.d.ts.map +1 -1
  414. package/dist/utils/focusReturn.js +11 -4
  415. package/dist/utils/getNodeText.d.ts +8 -0
  416. package/dist/utils/getNodeText.d.ts.map +1 -0
  417. package/dist/utils/getNodeText.js +37 -0
  418. package/dist/utils/interactionModality.d.ts +11 -2
  419. package/dist/utils/interactionModality.d.ts.map +1 -1
  420. package/dist/utils/interactionModality.js +58 -5
  421. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
  422. package/dist/utils/interactionOverlay.stylex.js +17 -8
  423. package/dist/utils/plainDate.d.ts +2 -0
  424. package/dist/utils/plainDate.d.ts.map +1 -1
  425. package/dist/utils/plainDate.js +46 -3
  426. package/dist/utils/presentation.d.ts +50 -0
  427. package/dist/utils/presentation.d.ts.map +1 -0
  428. package/dist/utils/presentation.js +125 -0
  429. package/dist/utils/pressFeedback.d.ts.map +1 -1
  430. package/dist/utils/pressFeedback.js +20 -1
  431. package/dist/utils/primaryClick.d.ts +11 -0
  432. package/dist/utils/primaryClick.d.ts.map +1 -0
  433. package/dist/utils/primaryClick.js +25 -0
  434. package/dist/utils/sharedMutationObserver.d.ts +19 -11
  435. package/dist/utils/sharedMutationObserver.d.ts.map +1 -1
  436. package/dist/utils/sharedMutationObserver.js +66 -26
  437. package/dist/utils/timeFormat.d.ts +47 -0
  438. package/dist/utils/timeFormat.d.ts.map +1 -0
  439. package/dist/utils/timeFormat.js +240 -0
  440. package/dist/utils/touchPresentation.d.ts +2 -2
  441. package/dist/utils/touchPresentation.d.ts.map +1 -1
  442. package/dist/utils/touchPresentation.js +13 -8
  443. package/dist/utils/touchPresentation.stylex.d.ts +15 -0
  444. package/dist/utils/touchPresentation.stylex.d.ts.map +1 -0
  445. package/dist/utils/touchPresentation.stylex.js +33 -0
  446. package/dist/utils/touchQueries.d.ts +13 -0
  447. package/dist/utils/touchQueries.d.ts.map +1 -0
  448. package/dist/utils/touchQueries.js +15 -0
  449. package/dist/utils/unicodeDigits.d.ts +20 -0
  450. package/dist/utils/unicodeDigits.d.ts.map +1 -0
  451. package/dist/utils/unicodeDigits.js +70 -0
  452. package/dist/utils/useFocusAcrossSwap.d.ts +19 -0
  453. package/dist/utils/useFocusAcrossSwap.d.ts.map +1 -0
  454. package/dist/utils/useFocusAcrossSwap.js +41 -0
  455. package/locales/af-ZA.json +0 -21
  456. package/locales/ar-SA.json +0 -21
  457. package/locales/ca-ES.json +0 -21
  458. package/locales/cs-CZ.json +0 -21
  459. package/locales/da-DK.json +0 -21
  460. package/locales/de-DE.json +0 -21
  461. package/locales/el-GR.json +0 -21
  462. package/locales/en.json +35 -23
  463. package/locales/es-ES.json +0 -21
  464. package/locales/fi-FI.json +0 -21
  465. package/locales/fr-FR.json +0 -21
  466. package/locales/he-IL.json +0 -21
  467. package/locales/hu-HU.json +0 -21
  468. package/locales/it-IT.json +0 -21
  469. package/locales/ja-JP.json +0 -21
  470. package/locales/ko-KR.json +0 -21
  471. package/locales/nl-NL.json +0 -21
  472. package/locales/no-NO.json +0 -21
  473. package/locales/pl-PL.json +0 -21
  474. package/locales/pseudo.json +23 -14
  475. package/locales/pt-BR.json +0 -21
  476. package/locales/pt-PT.json +0 -21
  477. package/locales/ro-RO.json +0 -21
  478. package/locales/ru-RU.json +0 -21
  479. package/locales/sr-SP.json +0 -21
  480. package/locales/sv-SE.json +0 -21
  481. package/locales/tr-TR.json +0 -21
  482. package/locales/uk-UA.json +0 -21
  483. package/locales/vi-VN.json +0 -21
  484. package/locales/zh-CN.json +0 -21
  485. package/locales/zh-TW.json +0 -21
  486. package/package.json +5 -4
  487. package/src/AlertDialog/AlertDialog.tsx +6 -5
  488. package/src/AlertDialog/__tests__/AlertDialog.a11y.chromium.spec.ts +18 -6
  489. package/src/AppShell/AppShell.doc.mjs +1 -1
  490. package/src/AppShell/AppShell.spec.md +9 -12
  491. package/src/AppShell/AppShell.test.tsx +164 -2
  492. package/src/AppShell/AppShell.tsx +225 -5
  493. package/src/AppShell/__tests__/AppShellMainKeyboard.a11y.browser.spec.ts +534 -0
  494. package/src/AppShell/observeMainOverflow.test.ts +173 -0
  495. package/src/AppShell/observeMainOverflow.ts +143 -0
  496. package/src/Avatar/Avatar.spec.md +22 -22
  497. package/src/Avatar/Avatar.tsx +18 -1
  498. package/src/Avatar/__tests__/AvatarForcedColors.a11y.chromium.spec.ts +126 -0
  499. package/src/Badge/Badge.doc.mjs +1 -1
  500. package/src/Badge/Badge.tsx +19 -2
  501. package/src/Badge/__tests__/BadgeLargerText.a11y.chromium.spec.ts +260 -0
  502. package/src/BottomSheet/BottomSheet.test.tsx +46 -0
  503. package/src/BottomSheet/BottomSheet.tsx +1 -1
  504. package/src/BottomSheet/BottomSheetPanel.tsx +2 -1
  505. package/src/BottomSheet/useSheetGestures.test.ts +52 -0
  506. package/src/BottomSheet/useSheetGestures.ts +37 -6
  507. package/src/Breadcrumbs/__tests__/Breadcrumbs.a11y.chromium.spec.ts +18 -6
  508. package/src/Button/Button.tsx +4 -4
  509. package/src/Calendar/Calendar.spec.md +87 -48
  510. package/src/Calendar/__tests__/Calendar.a11y.chromium.spec.ts +21 -7
  511. package/src/Carousel/Carousel.doc.mjs +1 -1
  512. package/src/Carousel/Carousel.spec.md +9 -9
  513. package/src/Carousel/Carousel.test.tsx +124 -3
  514. package/src/Carousel/Carousel.tsx +10 -2
  515. package/src/Carousel/__tests__/Carousel.a11y.chromium.spec.ts +21 -7
  516. package/src/Carousel/__tests__/Carousel.a11y.known-failures.ts +1 -16
  517. package/src/Chat/ChatComposer.spec.md +75 -59
  518. package/src/Chat/ChatComposerTokenElement.spec.md +45 -33
  519. package/src/Chat/ChatDictationButton.spec.md +126 -81
  520. package/src/Chat/ChatLayout.spec.md +41 -36
  521. package/src/Chat/ChatLayoutScrollButton.spec.md +87 -103
  522. package/src/Chat/ChatMessage.spec.md +64 -65
  523. package/src/Chat/ChatSendButton.spec.md +78 -67
  524. package/src/CheckboxInput/CheckboxInput.tsx +6 -5
  525. package/src/Citation/Citation.spec.md +60 -47
  526. package/src/ClickableCard/ClickableCard.test.tsx +96 -0
  527. package/src/ClickableCard/ClickableCard.tsx +48 -4
  528. package/src/CodeBlock/__tests__/CodeBlockCopyClearance.a11y.chromium.spec.ts +38 -3
  529. package/src/Collapsible/Collapsible.spec.md +2 -0
  530. package/src/Collapsible/CollapsibleGroup.spec.md +57 -29
  531. package/src/Collapsible/__tests__/Collapsible.a11y.chromium.spec.ts +18 -7
  532. package/src/Collapsible/__tests__/CollapsibleGroupAccordion.a11y.chromium.spec.ts +18 -6
  533. package/src/ComplexSelector/ComplexSelector.test.tsx +148 -0
  534. package/src/ComplexSelector/ComplexSelector.tsx +41 -4
  535. package/src/ContextMenu/ContextMenu.spec.md +84 -56
  536. package/src/ContextMenu/ContextMenu.test.tsx +35 -0
  537. package/src/ContextMenu/ContextMenuItem.spec.md +132 -0
  538. package/src/DateInput/DateInput.spec.md +74 -57
  539. package/src/DateInput/DateInput.test.tsx +115 -3
  540. package/src/DateInput/DateInput.tsx +29 -14
  541. package/src/DateInput/DateInputTouch.test.tsx +120 -0
  542. package/src/DateInput/MonthScroller.tsx +106 -20
  543. package/src/DateInput/MonthScrollerSettle.test.tsx +165 -0
  544. package/src/DateInput/TouchDateField.tsx +14 -3
  545. package/src/DateInput/Wheel.tsx +9 -6
  546. package/src/DateInput/__tests__/MonthScrollerSteps.interaction.browser.spec.ts +312 -0
  547. package/src/DateInput/usePointerDragScroll.ts +34 -10
  548. package/src/DateInput/useScrollSettle.ts +38 -7
  549. package/src/DateRangeInput/DateRangeInput.test.tsx +46 -3
  550. package/src/DateRangeInput/DateRangeInput.tsx +16 -12
  551. package/src/DateTimeInput/DateTimeInput.doc.mjs +10 -8
  552. package/src/DateTimeInput/DateTimeInput.spec.md +75 -51
  553. package/src/DateTimeInput/DateTimeInput.test.tsx +81 -0
  554. package/src/DateTimeInput/DateTimeInput.tsx +46 -24
  555. package/src/DateTimeInput/NativeTimeSegment.tsx +4 -6
  556. package/src/DateTimeInput/TouchDateTimeField.tsx +3 -7
  557. package/src/Dialog/Dialog.test.tsx +216 -2
  558. package/src/Dialog/Dialog.tsx +42 -9
  559. package/src/Dialog/DialogHeader.tsx +5 -4
  560. package/src/Dialog/__tests__/DialogSecondDocument.a11y.browser.spec.ts +202 -0
  561. package/src/Divider/Divider.tsx +2 -1
  562. package/src/Drawer/Drawer.spec.md +111 -80
  563. package/src/Drawer/Drawer.test.tsx +35 -0
  564. package/src/Drawer/Drawer.tsx +13 -2
  565. package/src/Drawer/DrawerHeader.spec.md +181 -0
  566. package/src/Drawer/__tests__/DrawerNonModalPageScroll.a11y.chromium.spec.ts +267 -0
  567. package/src/DropdownMenu/DropdownMenu.spec.md +198 -218
  568. package/src/DropdownMenu/DropdownMenu.test.tsx +158 -0
  569. package/src/DropdownMenu/DropdownMenu.tsx +43 -21
  570. package/src/DropdownMenu/DropdownMenuCheckboxItem.spec.md +192 -0
  571. package/src/DropdownMenu/DropdownMenuDivider.spec.md +25 -31
  572. package/src/DropdownMenu/DropdownMenuGroup.spec.md +145 -0
  573. package/src/DropdownMenu/DropdownMenuItem.spec.md +48 -42
  574. package/src/DropdownMenu/DropdownMenuRadioGroup.spec.md +34 -37
  575. package/src/DropdownMenu/DropdownMenuRadioItem.spec.md +54 -53
  576. package/src/DropdownMenu/DropdownMenuSubMenu.spec.md +47 -41
  577. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +34 -0
  578. package/src/DropdownMenu/MenuBottomSheetActionList.tsx +6 -5
  579. package/src/DropdownMenu/__tests__/Menu.a11y.chromium.spec.ts +19 -7
  580. package/src/DropdownMenu/__tests__/MenuButton.a11y.chromium.spec.ts +21 -9
  581. package/src/DropdownMenu/lazyMenuBottomSheet.test.ts +60 -0
  582. package/src/DropdownMenu/lazyMenuBottomSheet.ts +36 -7
  583. package/src/DropdownMenu/renderDropdownItems.tsx +24 -19
  584. package/src/EmptyState/EmptyState.spec.md +35 -29
  585. package/src/EmptyState/EmptyState.tsx +2 -1
  586. package/src/Field/FieldLabel.tsx +5 -4
  587. package/src/Field/InputClearButton.tsx +3 -3
  588. package/src/Field/PanelSearchInput.tsx +2 -2
  589. package/src/FieldStatus/FieldStatus.tsx +9 -2
  590. package/src/FieldStatus/__tests__/FieldStatusAudit.a11y.chromium.spec.ts +477 -0
  591. package/src/FileInput/FileInput.test.tsx +159 -0
  592. package/src/FileInput/FileInput.tsx +34 -6
  593. package/src/FileInput/__tests__/FileInputAudit.a11y.chromium.spec.ts +796 -0
  594. package/src/FormLayout/FormLayout.spec.md +11 -20
  595. package/src/Heading/Heading.tsx +1 -1
  596. package/src/Icon/Icon.tsx +1 -1
  597. package/src/Indicator/CheckIndicator.spec.md +47 -42
  598. package/src/Indicator/CheckIndicator.tsx +2 -1
  599. package/src/Indicator/CheckboxIndicator.spec.md +40 -37
  600. package/src/Indicator/CheckboxIndicator.tsx +3 -1
  601. package/src/Indicator/Indicator.spec.md +188 -0
  602. package/src/Indicator/RadioIndicator.tsx +3 -1
  603. package/src/Item/Item.test.tsx +60 -0
  604. package/src/Item/Item.tsx +16 -8
  605. package/src/Item/ItemSwipeLayer.tsx +1 -1
  606. package/src/Kbd/Kbd.spec.md +81 -38
  607. package/src/Layer/__tests__/LayerBrowserHide.a11y.chromium.spec.ts +126 -0
  608. package/src/Layer/popoverOperations.test.ts +205 -0
  609. package/src/Layer/popoverOperations.ts +172 -0
  610. package/src/Layer/useLayer.test.tsx +426 -4
  611. package/src/Layer/useLayer.tsx +145 -77
  612. package/src/Layout/Layout.spec.md +127 -131
  613. package/src/Layout/Layout.tsx +1 -1
  614. package/src/Layout/LayoutContent.spec.md +205 -0
  615. package/src/Layout/LayoutContent.tsx +1 -1
  616. package/src/Layout/LayoutFooter.spec.md +190 -0
  617. package/src/Layout/LayoutFooter.tsx +1 -1
  618. package/src/Layout/LayoutHeader.spec.md +193 -0
  619. package/src/Layout/LayoutHeader.tsx +1 -1
  620. package/src/Layout/LayoutPanel.spec.md +194 -0
  621. package/src/Layout/__tests__/Layout.a11y.chromium.spec.ts +18 -6
  622. package/src/Lightbox/Lightbox.test.tsx +18 -0
  623. package/src/Lightbox/Lightbox.tsx +5 -0
  624. package/src/Lightbox/LightboxViewport.a11y.browser.spec.ts +8 -4
  625. package/src/Link/Link.spec.md +3 -2
  626. package/src/Link/Link.tsx +11 -0
  627. package/src/Link/__tests__/Link.a11y.chromium.spec.ts +18 -8
  628. package/src/Link/__tests__/Link.truncation.a11y.chromium.spec.ts +58 -0
  629. package/src/List/List.tsx +1 -1
  630. package/src/List/ListItem.spec.md +205 -0
  631. package/src/List/ListItem.tsx +3 -3
  632. package/src/Markdown/plugins/headingLinks.a11y.chromium.spec.ts +1 -1
  633. package/src/MetadataList/MetadataList.spec.md +187 -0
  634. package/src/MetadataList/MetadataListItem.spec.md +165 -0
  635. package/src/MobileNav/MobileNav.test.tsx +15 -0
  636. package/src/MobileNav/MobileNav.tsx +9 -1
  637. package/src/MobileNav/__tests__/MobileNavHeader.a11y.chromium.spec.ts +89 -0
  638. package/src/MoreMenu/MoreMenu.test.tsx +11 -0
  639. package/src/MultiSelector/CreateRow.a11y.chromium.spec.ts +7 -8
  640. package/src/MultiSelector/MultiSelector.spec.md +7 -4
  641. package/src/MultiSelector/MultiSelector.test.tsx +270 -1
  642. package/src/MultiSelector/MultiSelector.tsx +25 -10
  643. package/src/MultiSelector/__tests__/MultiSelector.listbox.a11y.test.tsx +8 -1
  644. package/src/NavMenu/NavHeadingMenu.spec.md +1 -5
  645. package/src/NavMenu/NavHeadingMenuItem.tsx +5 -39
  646. package/src/NumberInput/NumberInput.doc.mjs +13 -7
  647. package/src/NumberInput/NumberInput.spec.md +59 -37
  648. package/src/NumberInput/NumberInput.test.tsx +68 -7
  649. package/src/NumberInput/NumberInput.tsx +44 -4
  650. package/src/NumberInput/__tests__/NumberInput.coarse-pointer.a11y.chromium.spec.ts +102 -0
  651. package/src/NumberInput/numberParser.ts +7 -45
  652. package/src/Outline/Outline.spec.md +165 -69
  653. package/src/Outline/Outline.test.tsx +67 -2
  654. package/src/Outline/Outline.tsx +23 -0
  655. package/src/Outline/modules/parseOutlineFromMarkdown.spec.md +3 -4
  656. package/src/OverflowList/OverflowList.spec.md +102 -55
  657. package/src/Pagination/Pagination.spec.md +158 -84
  658. package/src/Popover/Popover.test.tsx +84 -0
  659. package/src/Popover/Popover.tsx +34 -14
  660. package/src/Popover/__tests__/PopoverSecondDocument.a11y.chromium.spec.ts +70 -8
  661. package/src/Popover/usePopover.tsx +10 -3
  662. package/src/PowerSearch/PowerSearch.test.tsx +24 -0
  663. package/src/PowerSearch/PowerSearch.tsx +42 -31
  664. package/src/ProgressBar/ProgressBar.spec.md +143 -119
  665. package/src/ProgressBar/ProgressBar.test.tsx +108 -32
  666. package/src/ProgressBar/ProgressBar.tsx +58 -10
  667. package/src/ProgressBar/__tests__/ProgressBar.a11y.chromium.spec.ts +21 -9
  668. package/src/ProgressBar/__tests__/ProgressBarMotion.a11y.chromium.spec.ts +831 -0
  669. package/src/RadioList/RadioList.spec.md +1 -1
  670. package/src/RadioList/RadioList.test.tsx +84 -0
  671. package/src/RadioList/RadioListItem.tsx +11 -2
  672. package/src/Resizable/Resizable.spec.md +144 -124
  673. package/src/Resizable/__tests__/ResizeHandle.a11y.chromium.spec.ts +21 -14
  674. package/src/Resizable/useResizable.test.ts +43 -0
  675. package/src/Resizable/useResizable.ts +32 -9
  676. package/src/SegmentedControl/SegmentedControl.test.tsx +84 -0
  677. package/src/SegmentedControl/SegmentedControlItem.tsx +11 -5
  678. package/src/Selector/Selector.test.tsx +90 -1
  679. package/src/Selector/__tests__/Selector.combobox.a11y.test.tsx +8 -1
  680. package/src/Selector/__tests__/Selector.listbox.a11y.test.tsx +8 -1
  681. package/src/Selector/useSelectorPresentation.ts +31 -3
  682. package/src/SideNav/SideNav.test.tsx +151 -4
  683. package/src/SideNav/SideNavItem.spec.md +14 -24
  684. package/src/SideNav/SideNavItem.tsx +27 -10
  685. package/src/SideNav/__tests__/SideNavRailMotion.interaction.browser.spec.ts +176 -51
  686. package/src/Skeleton/Skeleton.tsx +3 -1
  687. package/src/Slider/Slider.test.tsx +31 -0
  688. package/src/Slider/Slider.tsx +8 -3
  689. package/src/Slider/__tests__/Slider.a11y.chromium.spec.ts +19 -7
  690. package/src/Spinner/Spinner.tsx +5 -3
  691. package/src/Stack/Stack.tsx +1 -1
  692. package/src/Stack/StackItem.tsx +1 -1
  693. package/src/Stepper/Step.spec.md +36 -42
  694. package/src/Stepper/Step.tsx +33 -6
  695. package/src/Stepper/Stepper.doc.mjs +1 -1
  696. package/src/Stepper/Stepper.test.tsx +69 -7
  697. package/src/Switch/Switch.spec.md +1 -1
  698. package/src/Switch/Switch.test.tsx +66 -1
  699. package/src/Switch/Switch.tsx +11 -3
  700. package/src/TabList/TabList.test.tsx +58 -4
  701. package/src/TabList/TabList.tsx +16 -1
  702. package/src/TabList/__tests__/TabPressPointer.interaction.browser.spec.ts +163 -0
  703. package/src/Table/Table.spec.md +1 -1
  704. package/src/Table/Table.test.tsx +32 -0
  705. package/src/Table/Table.tsx +43 -11
  706. package/src/Table/__tests__/Table.a11y.chromium.spec.ts +18 -6
  707. package/src/Table/__tests__/Table.a11y.states.ts +1 -5
  708. package/src/Table/plugins/columnResize/useTableColumnResize.test.tsx +22 -0
  709. package/src/Table/plugins/columnResize/useTableColumnResize.tsx +8 -4
  710. package/src/Table/plugins/sortable/useTableSortable.test.tsx +73 -144
  711. package/src/Table/plugins/sortable/useTableSortable.tsx +44 -73
  712. package/src/Table/plugins/sortable/useTableSortableState.test.tsx +9 -21
  713. package/src/Text/Text.tsx +1 -1
  714. package/src/Text/useTruncation.test.tsx +128 -1
  715. package/src/Text/useTruncation.ts +23 -5
  716. package/src/TextArea/TextArea.spec.md +10 -13
  717. package/src/TextArea/TextArea.test.tsx +60 -0
  718. package/src/TextArea/TextArea.tsx +15 -1
  719. package/src/TextInput/TextInput.test.tsx +21 -0
  720. package/src/TextInput/TextInput.tsx +3 -1
  721. package/src/TimeInput/NativeTimeInput.test.tsx +17 -0
  722. package/src/TimeInput/Presentation.test.tsx +21 -0
  723. package/src/TimeInput/TimeInput.doc.mjs +8 -8
  724. package/src/TimeInput/TimeInput.spec.md +58 -61
  725. package/src/TimeInput/TimeInput.test.tsx +76 -0
  726. package/src/TimeInput/TimeInput.tsx +36 -18
  727. package/src/TimeInput/TouchTimeField.tsx +4 -6
  728. package/src/Timestamp/Timestamp.spec.md +6 -12
  729. package/src/Timestamp/Timestamp.test.tsx +30 -5
  730. package/src/Timestamp/Timestamp.tsx +6 -2
  731. package/src/Timestamp/TimestampHoverCard.tsx +4 -4
  732. package/src/Toast/Toast.spec.md +15 -24
  733. package/src/Toast/Toast.tsx +104 -36
  734. package/src/Toast/ToastViewport.test.tsx +798 -1
  735. package/src/Toast/ToastViewport.tsx +121 -53
  736. package/src/ToggleButton/ToggleButton.test.tsx +27 -0
  737. package/src/ToggleButton/ToggleButtonGroup.tsx +20 -1
  738. package/src/Token/Token.doc.mjs +2 -2
  739. package/src/Token/Token.spec.md +52 -61
  740. package/src/Token/Token.test.tsx +104 -0
  741. package/src/Token/Token.tsx +75 -5
  742. package/src/Token/TokenLink.tsx +21 -7
  743. package/src/Tokenizer/Tokenizer.spec.md +18 -4
  744. package/src/Tokenizer/Tokenizer.test.tsx +176 -0
  745. package/src/Tokenizer/Tokenizer.tsx +48 -12
  746. package/src/Tokenizer/TokenizerBottomSheet.test.tsx +23 -4
  747. package/src/Tokenizer/TokenizerBottomSheet.tsx +3 -3
  748. package/src/Tokenizer/TokenizerClearAllMessageContext.ts +21 -0
  749. package/src/Toolbar/Toolbar.spec.md +5 -6
  750. package/src/Toolbar/Toolbar.test.tsx +30 -0
  751. package/src/Toolbar/Toolbar.tsx +2 -1
  752. package/src/Toolbar/__tests__/Toolbar.a11y.chromium.spec.ts +18 -6
  753. package/src/Tooltip/__tests__/Tooltip.a11y.chromium.spec.ts +18 -7
  754. package/src/TopNav/TopNav.test.tsx +27 -0
  755. package/src/TopNav/TopNavMegaMenu.test.tsx +21 -0
  756. package/src/TopNav/TopNavMegaMenu.tsx +4 -1
  757. package/src/TopNav/TopNavMenu.test.tsx +17 -0
  758. package/src/TopNav/TopNavMenu.tsx +1 -0
  759. package/src/TreeList/TreeList.test.tsx +26 -0
  760. package/src/TreeList/TreeListItem.tsx +26 -4
  761. package/src/Typeahead/BaseTypeahead.doc.mjs +3 -3
  762. package/src/Typeahead/BaseTypeahead.spec.md +45 -45
  763. package/src/Typeahead/BaseTypeahead.tsx +48 -9
  764. package/src/Typeahead/Typeahead.test.tsx +294 -11
  765. package/src/Typeahead/Typeahead.tsx +51 -9
  766. package/src/__tests__/internalModulesNotExperimental.test.ts +88 -0
  767. package/src/__tests__/pressState.ts +8 -4
  768. package/src/gestures/GestureArena.ts +111 -11
  769. package/src/gestures/GestureHost.ts +2064 -0
  770. package/src/gestures/GestureRecognizer.ts +21 -1
  771. package/src/gestures/LongPressGestureRecognizer.ts +4 -0
  772. package/src/gestures/PanGestureRecognizer.ts +4 -0
  773. package/src/gestures/PinchGestureRecognizer.ts +148 -0
  774. package/src/gestures/PressTracker.ts +386 -0
  775. package/src/gestures/README.md +420 -0
  776. package/src/gestures/ScreenEdgePanGestureRecognizer.ts +256 -0
  777. package/src/gestures/SwipeGestureRecognizer.ts +197 -0
  778. package/src/gestures/TapGestureRecognizer.ts +4 -0
  779. package/src/gestures/VelocityTracker.ts +10 -3
  780. package/src/gestures/__tests__/GestureHost.test.ts +2136 -0
  781. package/src/gestures/__tests__/PanGestureRecognizer.test.ts +4 -4
  782. package/src/gestures/__tests__/PinchGestureRecognizer.test.ts +74 -0
  783. package/src/gestures/__tests__/PressTracker.test.ts +275 -0
  784. package/src/gestures/__tests__/ScreenEdgePanGestureRecognizer.test.ts +498 -0
  785. package/src/gestures/__tests__/SwipeGestureRecognizer.test.ts +108 -0
  786. package/src/gestures/__tests__/arena.test.ts +40 -0
  787. package/src/gestures/__tests__/arenaPinchSwipeEdge.test.ts +193 -0
  788. package/src/gestures/__tests__/browser/build-fixture.mjs +46 -0
  789. package/src/gestures/__tests__/browser/fixture.ts +307 -0
  790. package/src/gestures/__tests__/browser/gestures.interaction.browser.spec.ts +1137 -0
  791. package/src/gestures/__tests__/browserNegotiation.test.ts +293 -0
  792. package/src/gestures/__tests__/haptics.test.ts +187 -0
  793. package/src/gestures/__tests__/harness.ts +52 -4
  794. package/src/gestures/__tests__/pressTrackerFactory.test.ts +59 -0
  795. package/src/gestures/__tests__/recognizerKind.test.ts +78 -0
  796. package/src/gestures/__tests__/touchActionRule.test.ts +54 -0
  797. package/src/gestures/__tests__/useGestureRecognizer.test.tsx +570 -0
  798. package/src/gestures/haptics.ts +203 -0
  799. package/src/gestures/index.ts +79 -0
  800. package/src/gestures/nativeScroll.ts +277 -0
  801. package/src/gestures/touchAction.ts +191 -0
  802. package/src/gestures/touchActionRule.ts +157 -0
  803. package/src/gestures/types.ts +30 -9
  804. package/src/gestures/useGestureRecognizer.ts +305 -0
  805. package/src/hooks/index.ts +5 -1
  806. package/src/hooks/modalAnnounce.ts +129 -72
  807. package/src/hooks/scrollKeyboardDelegation.test.ts +33 -0
  808. package/src/hooks/scrollKeyboardDelegation.ts +9 -3
  809. package/src/hooks/useAdaptivePresentation.ts +4 -1
  810. package/src/hooks/useAnnounce.doc.mjs +35 -3
  811. package/src/hooks/useAnnounce.replace.test.tsx +516 -0
  812. package/src/hooks/useAnnounce.ts +209 -102
  813. package/src/hooks/useAnnounceRenderedText.adopters.test.tsx +10 -1
  814. package/src/hooks/useAnnounceRenderedText.ts +17 -3
  815. package/src/hooks/useBusyAnnouncement.ts +7 -10
  816. package/src/hooks/useMenuHover.ts +15 -1
  817. package/src/hooks/useScrollOverflow.doc.mjs +1 -1
  818. package/src/hooks/useScrollOverflow.test.tsx +102 -1
  819. package/src/hooks/useScrollOverflow.ts +59 -10
  820. package/src/i18n/__tests__/useListFormatter.test.tsx +58 -0
  821. package/src/i18n/generated-locales/af-ZA.generated.ts +0 -7
  822. package/src/i18n/generated-locales/ar-SA.generated.ts +0 -6
  823. package/src/i18n/generated-locales/ca-ES.generated.ts +0 -7
  824. package/src/i18n/generated-locales/cs-CZ.generated.ts +0 -6
  825. package/src/i18n/generated-locales/da-DK.generated.ts +0 -6
  826. package/src/i18n/generated-locales/de-DE.generated.ts +0 -6
  827. package/src/i18n/generated-locales/el-GR.generated.ts +0 -7
  828. package/src/i18n/generated-locales/en.generated.ts +9 -6
  829. package/src/i18n/generated-locales/es-ES.generated.ts +0 -7
  830. package/src/i18n/generated-locales/fi-FI.generated.ts +0 -6
  831. package/src/i18n/generated-locales/fr-FR.generated.ts +0 -7
  832. package/src/i18n/generated-locales/he-IL.generated.ts +0 -6
  833. package/src/i18n/generated-locales/hu-HU.generated.ts +0 -6
  834. package/src/i18n/generated-locales/it-IT.generated.ts +0 -7
  835. package/src/i18n/generated-locales/ja-JP.generated.ts +0 -6
  836. package/src/i18n/generated-locales/ko-KR.generated.ts +0 -6
  837. package/src/i18n/generated-locales/nl-NL.generated.ts +0 -6
  838. package/src/i18n/generated-locales/no-NO.generated.ts +0 -6
  839. package/src/i18n/generated-locales/pl-PL.generated.ts +0 -7
  840. package/src/i18n/generated-locales/pseudo.generated.ts +9 -6
  841. package/src/i18n/generated-locales/pt-BR.generated.ts +0 -7
  842. package/src/i18n/generated-locales/pt-PT.generated.ts +0 -7
  843. package/src/i18n/generated-locales/ro-RO.generated.ts +0 -6
  844. package/src/i18n/generated-locales/ru-RU.generated.ts +0 -7
  845. package/src/i18n/generated-locales/sr-SP.generated.ts +0 -6
  846. package/src/i18n/generated-locales/sv-SE.generated.ts +0 -6
  847. package/src/i18n/generated-locales/tr-TR.generated.ts +0 -7
  848. package/src/i18n/generated-locales/uk-UA.generated.ts +0 -7
  849. package/src/i18n/generated-locales/vi-VN.generated.ts +0 -6
  850. package/src/i18n/generated-locales/zh-CN.generated.ts +0 -6
  851. package/src/i18n/generated-locales/zh-TW.generated.ts +0 -6
  852. package/src/i18n/useListFormatter.ts +42 -0
  853. package/src/motion/MEASUREMENTS.md +2 -1
  854. package/src/motion/README.md +113 -33
  855. package/src/motion/__tests__/animator.interaction.browser.spec.ts +843 -0
  856. package/src/motion/__tests__/motionBundle.ts +14 -0
  857. package/src/motion/animator.test.ts +1125 -32
  858. package/src/motion/animator.ts +932 -130
  859. package/src/motion/coast.ts +10 -3
  860. package/src/motion/reducedMotion.ts +12 -2
  861. package/src/motion/reducedMotionPlan.test.ts +26 -0
  862. package/src/motion/reducedMotionPlan.ts +20 -1
  863. package/src/motion/resolveMotion.test.ts +123 -0
  864. package/src/motion/resolveMotion.ts +68 -0
  865. package/src/motion/roleValue.ts +2 -1
  866. package/src/motion/roles.test.ts +21 -4
  867. package/src/motion/uikit/tableImports.test.ts +293 -0
  868. package/src/motion/uikit/timing.ts +15 -1
  869. package/src/motion/uikit/values.test.ts +23 -12
  870. package/src/theme/Theme.doc.mjs +1 -1
  871. package/src/theme/defineTheme.ts +117 -8
  872. package/src/theme/derivedVarRegistry.test.ts +4 -0
  873. package/src/theme/domainTokens/domainTokenDefaults.ts +19 -0
  874. package/src/theme/domainTokens/index.ts +6 -10
  875. package/src/theme/expandMotionScale.test.ts +1 -1
  876. package/src/theme/index.ts +5 -0
  877. package/src/theme/motionKnobs.ts +82 -0
  878. package/src/theme/resolveThemeValues.ts +68 -6
  879. package/src/theme/themeAdaptations.ts +52 -12
  880. package/src/theme/themeMotion.test.ts +620 -0
  881. package/src/theme/themeMotion.ts +221 -0
  882. package/src/theme/tokenReferences.ts +149 -0
  883. package/src/theme/tokens.stylex.ts +10 -0
  884. package/src/theme/tokens.ts +15 -158
  885. package/src/utils/focusOutline.stylex.ts +28 -0
  886. package/src/utils/focusReturn.ts +17 -6
  887. package/src/utils/getNodeText.test.ts +24 -0
  888. package/src/utils/getNodeText.ts +37 -0
  889. package/src/utils/interactionModality.test.ts +45 -1
  890. package/src/utils/interactionModality.ts +64 -5
  891. package/src/utils/interactionOverlay.stylex.ts +12 -3
  892. package/src/utils/plainDate.test.ts +90 -0
  893. package/src/utils/plainDate.ts +58 -3
  894. package/src/utils/presentation.test.ts +380 -0
  895. package/src/utils/presentation.ts +180 -0
  896. package/src/utils/pressFeedback.test.ts +138 -0
  897. package/src/utils/pressFeedback.ts +34 -11
  898. package/src/utils/pressGesture.test.ts +9 -0
  899. package/src/utils/pressPaintPriority.test.ts +80 -0
  900. package/src/utils/primaryClick.test.ts +38 -0
  901. package/src/utils/primaryClick.ts +28 -0
  902. package/src/utils/sharedMutationObserver.test.ts +54 -1
  903. package/src/utils/sharedMutationObserver.ts +87 -29
  904. package/src/utils/timeFormat.test.ts +204 -0
  905. package/src/utils/timeFormat.ts +314 -0
  906. package/src/utils/touchPresentation.stylex.ts +43 -0
  907. package/src/utils/touchPresentation.test.ts +140 -1
  908. package/src/utils/touchPresentation.ts +13 -8
  909. package/src/utils/touchQueries.ts +15 -0
  910. package/src/utils/unicodeDigits.test.ts +44 -0
  911. package/src/utils/unicodeDigits.ts +71 -0
  912. package/src/utils/useFocusAcrossSwap.ts +55 -0
@@ -9,9 +9,8 @@
9
9
  * back: presentation values and velocities, retarget continuity, stop,
10
10
  * completion, Reduced Motion substitutions, momentum curves (a coast with
11
11
  * edges), late attach
12
- * @position Testing; what the compositor plays (frames advancing through a
13
- * blocked main thread) needs a real browser, which these tests do not
14
- * cover
12
+ * @position Testing; the compositor itself is proven in
13
+ * __tests__/animator.interaction.browser.spec.ts
15
14
  */
16
15
 
17
16
  import {execFileSync} from 'node:child_process';
@@ -67,18 +66,43 @@ const requestFrame = (callback: () => void) => {
67
66
  * properties, so the page is modeled here).
68
67
  */
69
68
  let stylesheet: Record<string, string> = {};
69
+ /** The stylesheet's values for a pseudo-element of the element. */
70
+ let pseudoStylesheet: Record<string, string> = {};
70
71
 
71
72
  beforeEach(() => {
72
73
  clock = 1000;
73
74
  recorded = [];
74
75
  frameQueue = [];
75
76
  stylesheet = {};
77
+ pseudoStylesheet = {};
76
78
  vi.spyOn(window, 'getComputedStyle').mockImplementation(
77
- element =>
79
+ (element, pseudo) =>
78
80
  ({
79
- getPropertyValue: (property: string) =>
80
- (element as HTMLElement).style.getPropertyValue(property) ||
81
- (stylesheet[property] ?? ''),
81
+ getPropertyValue: (property: string) => {
82
+ if (pseudo != null) {
83
+ return pseudoStylesheet[property] ?? '';
84
+ }
85
+ // A live animation wins over the inline value, holding its last
86
+ // keyframe (the end-state tail) until it is cancelled.
87
+ const held = recorded
88
+ .filter(
89
+ animation =>
90
+ animation.element === element &&
91
+ !animation.cancelled &&
92
+ animation.keyframes.some(frame => property in frame),
93
+ )
94
+ .at(-1);
95
+ const last = held?.keyframes
96
+ .filter(frame => property in frame)
97
+ .at(-1) as Record<string, unknown> | undefined;
98
+ if (last != null) {
99
+ return String(last[property]);
100
+ }
101
+ return (
102
+ (element as HTMLElement).style.getPropertyValue(property) ||
103
+ (stylesheet[property] ?? '')
104
+ );
105
+ },
82
106
  }) as unknown as CSSStyleDeclaration,
83
107
  );
84
108
  vi.useFakeTimers();
@@ -261,7 +285,7 @@ function sampleAt(
261
285
  }
262
286
 
263
287
  describe('animate: keyframes on the compositor', () => {
264
- it('bakes the spring into keyframes within a twentieth of a pixel of the curve', async () => {
288
+ it('bakes the spring into keyframes within a quarter of a pixel of the curve', async () => {
265
289
  const {animator} = setup();
266
290
  animator.animate({x: 300}, {spring: springs.settle});
267
291
  const [animation] = live('translate');
@@ -280,7 +304,7 @@ describe('animate: keyframes on the compositor', () => {
280
304
  expect(duration).toBeCloseTo(curve.settleTime(0.05) * 1000, 6);
281
305
  for (let t = 0; t <= duration; t += 0.5) {
282
306
  const [x, y] = sampleAt(animation, 'translate', 1000 + t);
283
- expect(Math.abs(x - curve.value(t / 1000))).toBeLessThan(0.06);
307
+ expect(Math.abs(x - curve.value(t / 1000))).toBeLessThan(0.26);
284
308
  expect(y).toBe(0);
285
309
  expect(animator.get('x', 1000 + t)).toBeCloseTo(curve.value(t / 1000), 9);
286
310
  }
@@ -290,10 +314,41 @@ describe('animate: keyframes on the compositor', () => {
290
314
  ).toEqual([300, 0]);
291
315
  });
292
316
 
317
+ it('on a 3x screen, bakes the spring within a quarter of a device pixel', async () => {
318
+ vi.stubGlobal('devicePixelRatio', 3);
319
+ const {animator} = setup();
320
+ animator.animate({x: 300}, {spring: springs.settle});
321
+ const [animation] = live('translate');
322
+ await commitFrame(1000);
323
+ const curve = solveSpring(toSpring(springs.settle), {from: 0, to: 300});
324
+ const duration = Number(animation.options.duration) - END_HOLD_MS;
325
+ let worst = 0;
326
+ for (let t = 0; t <= duration; t += 0.5) {
327
+ const [x] = sampleAt(animation, 'translate', 1000 + t);
328
+ worst = Math.max(worst, Math.abs(x - curve.value(t / 1000)));
329
+ }
330
+ // In device pixels, as the curve's chords are checked at their quarters.
331
+ expect(worst * 3).toBeLessThan(0.26);
332
+ });
333
+
334
+ it('on a 1x screen, bakes no more keyframes than a quarter of a CSS pixel needs', () => {
335
+ const count = (ratio: number) => {
336
+ vi.stubGlobal('devicePixelRatio', ratio);
337
+ recorded = [];
338
+ const {animator} = setup();
339
+ animator.animate({x: 300}, {spring: springs.settle});
340
+ return live('translate')[0].keyframes.length;
341
+ };
342
+ const desktop = count(1);
343
+ expect(desktop).toBeLessThan(count(3));
344
+ // A ratio below 1 (a zoomed-out page) bakes as 1, never coarser.
345
+ expect(count(0.5)).toBe(desktop);
346
+ });
347
+
293
348
  it('keeps the keyframe count modest (adaptive sampling)', () => {
294
349
  const {animator} = setup();
295
350
  animator.animate({x: 300}, {spring: springs.settle});
296
- expect(live('translate')[0].keyframes.length).toBeLessThan(200);
351
+ expect(live('translate')[0].keyframes.length).toBeLessThan(60);
297
352
  });
298
353
 
299
354
  it('clears an identity transform at rest, so a fixed child stays fixed', async () => {
@@ -302,18 +357,16 @@ describe('animate: keyframes on the compositor', () => {
302
357
  {x: 100, scale: 0.9, rotate: 10},
303
358
  {spring: springs.quickChange},
304
359
  );
305
- expect(element.style.translate).toBe('100px 0px');
360
+ // While the curve runs, the inline value is where it starts.
361
+ expect(['', 'none']).toContain(element.style.translate);
306
362
  await advance(100);
307
363
  animator.animate(
308
364
  {x: 0, scale: 1, rotate: 0},
309
365
  {spring: springs.quickChange},
310
366
  );
311
- // While the motion back runs, the end is never written 0px 0px: none, or
312
- // nothing where the stylesheet agrees.
367
+ // While the motion back runs, the inline value is where it starts.
313
368
  expect(live('translate')).toHaveLength(1);
314
- for (const channel of ['translate', 'scale', 'rotate'] as const) {
315
- expect(['', 'none']).toContain(element.style[channel]);
316
- }
369
+ expect(parse(element.style.translate)[0]).toBeCloseTo(animator.get('x'), 2);
317
370
  await advance(5000);
318
371
  // At rest the identity leaves the element: translate 0px 0px would make
319
372
  // the element a fixed child's containing block, and scale 1 stops a
@@ -350,21 +403,41 @@ describe('animate: keyframes on the compositor', () => {
350
403
  expect(element.style.scale).toBe('none');
351
404
  });
352
405
 
353
- it('writes the end values inline up front, so the last frame needs no main thread', () => {
406
+ it('keeps a fade to 1 inline where the stylesheet rests at 0', async () => {
407
+ const {animator, element} = setup();
408
+ // A scroll edge layer: transparent in its stylesheet, faded in on a
409
+ // spring. At retire the end-state tail still holds 1, so the end can only
410
+ // be judged against the stylesheet once the animation is gone.
411
+ stylesheet.opacity = '0';
412
+ animator.set({opacity: 0});
413
+ animator.animate({opacity: 1}, {spring: springs.quickChange});
414
+ await advance(5000);
415
+ expect(live('opacity')).toHaveLength(0);
416
+ expect(element.style.opacity).toBe('1');
417
+ });
418
+
419
+ it('holds the value on screen inline while a curve runs, and the end once it retires', async () => {
354
420
  const {animator, element} = setup();
421
+ animator.set({x: 10, opacity: 0.5});
355
422
  animator.animate(
356
423
  {x: 120, y: -40, scale: 1.1, rotate: 12, opacity: 0.25},
357
424
  {spring: springs.quickChange},
358
425
  );
426
+ // A frame painted before the compositor has the curve shows where the
427
+ // element is, never where it is going.
428
+ expect(element.style.translate).toBe('10px 0px');
429
+ expect(element.style.opacity).toBe('0.5');
430
+ const animations = ['translate', 'scale', 'rotate', 'opacity'].map(
431
+ channel => live(channel)[0],
432
+ );
433
+ await commitFrame(clock);
434
+ await advance(5000);
359
435
  expect(element.style.translate).toBe('120px -40px');
360
436
  expect(element.style.scale).toBe('1.1 1.1');
361
437
  expect(element.style.rotate).toBe('12deg');
362
438
  expect(element.style.opacity).toBe('0.25');
363
- for (const channel of ['translate', 'scale', 'rotate', 'opacity']) {
364
- const [animation] = live(channel);
365
- expect(animation.keyframes[animation.keyframes.length - 1][channel]).toBe(
366
- (element.style as unknown as Record<string, string>)[channel],
367
- );
439
+ for (const animation of animations) {
440
+ expect(animation.cancelled).toBe(true);
368
441
  }
369
442
  });
370
443
 
@@ -385,6 +458,47 @@ describe('animate: keyframes on the compositor', () => {
385
458
  );
386
459
  });
387
460
 
461
+ it('looks for its end again on the next frame when the engine dates animations per frame (WebKit), not in a spin', async () => {
462
+ // WebKit moves an animation's currentTime once per frame. A retire that
463
+ // looks within a frame reads the frame's time, a fraction of a ms short of
464
+ // the end, and a timer set to that fraction fires again before anything
465
+ // moved: dozens of timers on the main thread after every motion.
466
+ const {animator} = setup();
467
+ const handle = animator.animate({x: 100}, {spring: springs.settle});
468
+ const [animation] = live('translate');
469
+ await commitFrame(clock);
470
+ const start = animation.startTime as number;
471
+ const curve = Number(animation.options.duration) - END_HOLD_MS;
472
+ let frameTime = start;
473
+ Object.defineProperty(animation, 'currentTime', {
474
+ configurable: true,
475
+ get: () => frameTime - start,
476
+ });
477
+ Object.defineProperty(animation, 'effect', {
478
+ configurable: true,
479
+ value: {getTiming: () => ({duration: animation.options.duration})},
480
+ });
481
+ // The last frame before the end dates the animation 0.5 ms short of it.
482
+ frameTime = start + curve - 0.5;
483
+ // Up to just before the retire is due (the injected clock and the timers
484
+ // move together).
485
+ clock += curve - 1;
486
+ await vi.advanceTimersByTimeAsync(curve - 1);
487
+ const timers = vi.spyOn(globalThis, 'setTimeout');
488
+ // A frame's worth of main-thread time with no frame in between.
489
+ for (let ms = 0; ms < 16; ms++) {
490
+ clock += 1;
491
+ await vi.advanceTimersByTimeAsync(1);
492
+ }
493
+ expect(timers.mock.calls.length).toBeLessThanOrEqual(2);
494
+ // The next frame: the engine's time passes the end, and it retires.
495
+ frameTime = start + curve + 16;
496
+ clock += 1;
497
+ await vi.advanceTimersByTimeAsync(17);
498
+ expect(animation.cancelled).toBe(true);
499
+ await expect(handle.finished).resolves.toBe(true);
500
+ });
501
+
388
502
  it('never retires an animation a sampler has paused', async () => {
389
503
  const {animator} = setup();
390
504
  animator.animate({x: 120}, {spring: springs.quickChange});
@@ -450,6 +564,157 @@ describe('a call from rest starts at the commit, as Core Animation does', () =>
450
564
  );
451
565
  });
452
566
 
567
+ it('a start the browser dates before the block (WebKit) moves to after it', async () => {
568
+ // WebKit dates a pending animation at its last frame time before the
569
+ // task that made it, so after a block its first frame would show the
570
+ // motion that far in; the call's first read of that start moves it to
571
+ // the read. Chromium's commit start (the case above) stays.
572
+ Object.defineProperty(document, 'timeline', {
573
+ configurable: true,
574
+ get: () => ({currentTime: clock}),
575
+ });
576
+ try {
577
+ const {animator} = setup();
578
+ const handle = animator.animate({x: 300}, {spring: springs.settle});
579
+ const [animation] = live('translate');
580
+ // The task blocks for 300 ms; the browser dates the animation at 1000.
581
+ clock = 1300;
582
+ animation.startTime = 1000;
583
+ await commitFrame(1300);
584
+ await expect(handle.started).resolves.toBe(1300);
585
+ expect(animation.startTime).toBe(1300);
586
+ expect(animator.get('x', 1300)).toBeCloseTo(0, 9);
587
+ expect(animator.get('x', 1400)).toBeCloseTo(curve.value(0.1), 9);
588
+ } finally {
589
+ Reflect.deleteProperty(document, 'timeline');
590
+ }
591
+ });
592
+
593
+ it('a start the browser dates at the frame before the call stays when the call\u2019s task runs on a few ms', async () => {
594
+ // Browser start times are frame times and a task's end is a live clock:
595
+ // on every call they differ by up to a frame plus the task's own length.
596
+ // A task that runs on 5 ms after the call (a component's effect after a
597
+ // tap) blocks nothing, so the start stays the browser's and a sequence
598
+ // chained on it keeps its clock.
599
+ Object.defineProperty(document, 'timeline', {
600
+ configurable: true,
601
+ get: () => ({currentTime: clock}),
602
+ });
603
+ try {
604
+ const {animator} = setup();
605
+ clock = 1005;
606
+ const handle = animator.animate({x: 300}, {spring: springs.settle});
607
+ const [animation] = live('translate');
608
+ clock = 1010;
609
+ animation.startTime = 1000;
610
+ await commitFrame(1016);
611
+ await expect(handle.started).resolves.toBe(1000);
612
+ expect(animation.startTime).toBe(1000);
613
+ } finally {
614
+ Reflect.deleteProperty(document, 'timeline');
615
+ }
616
+ });
617
+
618
+ it('calls made in one task start together, whichever of them a block follows', async () => {
619
+ // One motion's parts are often separate calls in one task (a
620
+ // component's effect): a block measured per call would move the
621
+ // earlier calls past it and leave a later one on the browser's start,
622
+ // splitting the parts by a frame.
623
+ Object.defineProperty(document, 'timeline', {
624
+ configurable: true,
625
+ get: () => ({currentTime: clock}),
626
+ });
627
+ try {
628
+ const {animator} = setup();
629
+ const first = animator.animate({x: 300}, {spring: springs.settle});
630
+ clock = 1030;
631
+ const second = animator.animate({opacity: 0.5}, {spring: springs.settle});
632
+ clock = 1040;
633
+ for (const animation of recorded) {
634
+ animation.startTime = 1000;
635
+ }
636
+ await commitFrame(1040);
637
+ const [a, b] = await Promise.all([first.started, second.started]);
638
+ expect(a).toBe(b);
639
+ } finally {
640
+ Reflect.deleteProperty(document, 'timeline');
641
+ }
642
+ });
643
+
644
+ it('a block within two frames of the call keeps the browser\u2019s start', async () => {
645
+ // Browser start times are frame times and the task's end a live clock: a
646
+ // 20 ms block cannot be told apart from that quantization, so the start
647
+ // stays where the browser put it.
648
+ Object.defineProperty(document, 'timeline', {
649
+ configurable: true,
650
+ get: () => ({currentTime: clock}),
651
+ });
652
+ try {
653
+ const {animator} = setup();
654
+ const handle = animator.animate({x: 300}, {spring: springs.settle});
655
+ const [animation] = live('translate');
656
+ // The task blocks for 20 ms; the browser dates the animation at 1000.
657
+ clock = 1020;
658
+ animation.startTime = 1000;
659
+ await commitFrame(1020);
660
+ await expect(handle.started).resolves.toBe(1000);
661
+ expect(animation.startTime).toBe(1000);
662
+ } finally {
663
+ Reflect.deleteProperty(document, 'timeline');
664
+ }
665
+ });
666
+
667
+ it('a start the browser dates by the call stays when its own task ended at once, however late the first read', async () => {
668
+ // WebKit under load from other tasks: the call's task ends at once, and
669
+ // another task holds the main thread before the first frame. Nothing in
670
+ // the call's task delayed it, so its start stays the browser's and the
671
+ // calls a sequence chains on it keep their clock (design:reduced-motion
672
+ // DR2's 200 ms cross-fade, a cancel fading on the previous fade's end).
673
+ Object.defineProperty(document, 'timeline', {
674
+ configurable: true,
675
+ get: () => ({currentTime: clock}),
676
+ });
677
+ try {
678
+ const {animator} = setup();
679
+ const handle = animator.animate({x: 300}, {spring: springs.settle});
680
+ const [animation] = live('translate');
681
+ // The call's task ends; then another task runs for 100 ms, and the
682
+ // browser commits the animation after it, dated at the call.
683
+ await Promise.resolve();
684
+ clock = 1100;
685
+ animation.startTime = 1000;
686
+ await commitFrame(1100);
687
+ await expect(handle.started).resolves.toBe(1000);
688
+ expect(animation.startTime).toBe(1000);
689
+ } finally {
690
+ Reflect.deleteProperty(document, 'timeline');
691
+ }
692
+ });
693
+
694
+ it('a start read after the first frame stays the browser\u2019s', async () => {
695
+ Object.defineProperty(document, 'timeline', {
696
+ configurable: true,
697
+ get: () => ({currentTime: clock}),
698
+ });
699
+ try {
700
+ const {animator} = setup();
701
+ const handle = animator.animate({x: 300}, {spring: springs.settle});
702
+ const [animation] = live('translate');
703
+ // Paused before its first frame (a harness), then started late: the
704
+ // first frame passed with no start to read.
705
+ animation.paused = true;
706
+ await commitFrame(1016);
707
+ clock = 1600;
708
+ animation.paused = false;
709
+ animation.startTime = 1000;
710
+ await commitFrame(1600);
711
+ await expect(handle.started).resolves.toBe(1000);
712
+ expect(animation.startTime).toBe(1000);
713
+ } finally {
714
+ Reflect.deleteProperty(document, 'timeline');
715
+ }
716
+ });
717
+
453
718
  it('holds its start until the browser starts it', async () => {
454
719
  const {animator} = setup();
455
720
  animator.animate({x: 300}, {spring: springs.settle});
@@ -793,12 +1058,373 @@ describe('review fixes', () => {
793
1058
  });
794
1059
  });
795
1060
 
1061
+ describe('additive: a retarget adds to the curve on screen', () => {
1062
+ const old = solveSpring(toSpring(springs.settle), {from: 0, to: 300});
1063
+ const oldEnd = old.settleTime(0.05);
1064
+ const oldAt = (t: number) => (t >= oldEnd ? 300 : old.value(t));
1065
+
1066
+ it('starts from the old curve\u2019s value and velocity, and reads as their sum', async () => {
1067
+ const {animator} = setup();
1068
+ animator.animate({x: 300}, {spring: springs.settle});
1069
+ await commitFrame(1000);
1070
+ clock = 1150;
1071
+ const x = animator.get('x');
1072
+ const v = animator.getVelocity('x');
1073
+ animator.animate({x: 100}, {spring: springs.settle}, {additive: true});
1074
+ expect(animator.get('x')).toBeCloseTo(x, 9);
1075
+ expect(animator.getVelocity('x')).toBeCloseTo(v, 6);
1076
+ const delta = solveSpring(toSpring(springs.settle), {from: 0, to: -200});
1077
+ for (const t of [0.02, 0.1, 0.4]) {
1078
+ expect(animator.get('x', 1150 + t * 1000)).toBeCloseTo(
1079
+ oldAt(0.15 + t) + delta.value(t),
1080
+ 6,
1081
+ );
1082
+ }
1083
+ await advance(10_000);
1084
+ expect(animator.get('x')).toBe(100);
1085
+ });
1086
+
1087
+ it('a played reversal keeps the old velocity: a late first frame shows only the reversal\u2019s own start', async () => {
1088
+ // An ease-in-out reversal starts at rest; replacing a curve that moves
1089
+ // at v puts the old curve v·d ahead of it by a first frame d ms late.
1090
+ const ease = cubicBezier(timing.curves.move);
1091
+ const reversal = (from: number, to: number) => ({
1092
+ duration: 0.35,
1093
+ at: (t: number) => from + (to - from) * ease(Math.min(1, t / 0.35)),
1094
+ velocity: (t: number) =>
1095
+ ((to - from) / 0.35) *
1096
+ ((ease(Math.min(1, t / 0.35 + 1e-4)) -
1097
+ ease(Math.max(0, t / 0.35 - 1e-4))) /
1098
+ 2e-4),
1099
+ });
1100
+ const steps = async (additive: boolean) => {
1101
+ const {animator} = setup();
1102
+ animator.animate({x: 300}, {spring: springs.settle}, {at: 1000});
1103
+ clock = 1120;
1104
+ const from = animator.get('x');
1105
+ animator.play({x: reversal(from, 0)}, {at: 1120, additive});
1106
+ return [30, 60, 100].map(d =>
1107
+ Math.abs(animator.get('x', 1120 + d) - oldAt((120 + d) / 1000)),
1108
+ );
1109
+ };
1110
+ const replaced = await steps(false);
1111
+ const added = await steps(true);
1112
+ for (let i = 0; i < added.length; i++) {
1113
+ expect(added[i]).toBeLessThan(replaced[i] / 2);
1114
+ }
1115
+ // What remains is the reversal's own first d ms, scaled to the targets.
1116
+ expect(added[0]).toBeCloseTo(300 * ease(30 / 350), 6);
1117
+ });
1118
+
1119
+ it('nests: a second retarget adds to the first', async () => {
1120
+ const {animator} = setup();
1121
+ animator.animate({x: 300}, {spring: springs.settle});
1122
+ await commitFrame(1000);
1123
+ clock = 1100;
1124
+ animator.animate({x: 100}, {spring: springs.settle}, {additive: true});
1125
+ clock = 1200;
1126
+ const x = animator.get('x');
1127
+ const v = animator.getVelocity('x');
1128
+ animator.animate({x: 250}, {spring: springs.settle}, {additive: true});
1129
+ expect(animator.get('x')).toBeCloseTo(x, 9);
1130
+ expect(animator.getVelocity('x')).toBeCloseTo(v, 6);
1131
+ const first = solveSpring(toSpring(springs.settle), {from: 0, to: -200});
1132
+ const second = solveSpring(toSpring(springs.settle), {from: 0, to: 150});
1133
+ expect(animator.get('x', 1300)).toBeCloseTo(
1134
+ oldAt(0.3) + first.value(0.2) + second.value(0.1),
1135
+ 6,
1136
+ );
1137
+ await advance(10_000);
1138
+ expect(animator.get('x')).toBe(250);
1139
+ });
1140
+
1141
+ it('replaces as before for a property at rest or given a velocity', async () => {
1142
+ const {animator} = setup();
1143
+ animator.animate({x: 300}, {spring: springs.settle}, {additive: true});
1144
+ await commitFrame(1000);
1145
+ const fromRest = solveSpring(toSpring(springs.settle), {from: 0, to: 300});
1146
+ expect(animator.get('x', 1100)).toBeCloseTo(fromRest.value(0.1), 9);
1147
+ clock = 1100;
1148
+ const x = animator.get('x');
1149
+ animator.animate(
1150
+ {x: 0},
1151
+ {spring: springs.settle, velocity: {x: 0}},
1152
+ {additive: true},
1153
+ );
1154
+ const back = solveSpring(toSpring(springs.settle), {from: x, to: 0});
1155
+ expect(animator.get('x', 1200)).toBeCloseTo(back.value(0.1), 9);
1156
+ });
1157
+ });
1158
+
1159
+ describe('start: when a call\u2019s animations start', () => {
1160
+ it('presented: leaves the element at rest, and creates the animations two frames later', async () => {
1161
+ const {animator, element} = setup();
1162
+ animator.set({x: 20});
1163
+ // The set's own promotion ends first.
1164
+ await vi.advanceTimersByTimeAsync(PROMOTION_LINGER_MS + 1);
1165
+ expect(element.style.willChange).toBe('');
1166
+ const handle = animator.animate(
1167
+ {x: 100},
1168
+ {spring: springs.settle},
1169
+ {start: 'presented'},
1170
+ );
1171
+ // At rest, with no layer yet: the next frame commits it where it is.
1172
+ // (A layer at rest for two frames left a stale paint in WebKit on iOS.)
1173
+ expect(live('translate')).toHaveLength(0);
1174
+ expect(element.style.translate).toBe('20px 0px');
1175
+ expect(element.style.willChange).toBe('');
1176
+ expect(animator.get('x')).toBe(20);
1177
+ clock = 1016;
1178
+ await commitFrame(1016);
1179
+ expect(live('translate')).toHaveLength(0);
1180
+ // The frame after it creates them; the browser starts them at its commit.
1181
+ clock = 1033;
1182
+ await commitFrame(1033);
1183
+ expect(live('translate')).toHaveLength(1);
1184
+ expect(element.style.willChange).toContain('translate');
1185
+ clock = 1050;
1186
+ await commitFrame(1050);
1187
+ await expect(handle.started).resolves.toBe(1050);
1188
+ const curve = solveSpring(toSpring(springs.settle), {from: 20, to: 100});
1189
+ expect(animator.get('x', 1150)).toBeCloseTo(curve.value(0.1), 9);
1190
+ expect(sampleAt(live('translate')[0], 'translate', 1150)[0]).toBeCloseTo(
1191
+ curve.value(0.1),
1192
+ 0,
1193
+ );
1194
+ });
1195
+
1196
+ it('presented: a start WebKit dates at the frame that creates the animations keeps it, wherever in its frame the call was made', async () => {
1197
+ // A presented call's animations are created in its second frame's
1198
+ // callback, so its task is that callback's, not the call's: measured
1199
+ // from the call, a call made early in its frame looked like a block
1200
+ // longer than two frames, and a late first read moved the start.
1201
+ Object.defineProperty(document, 'timeline', {
1202
+ configurable: true,
1203
+ get: () => ({currentTime: clock}),
1204
+ });
1205
+ try {
1206
+ const {animator} = setup();
1207
+ const handle = animator.animate(
1208
+ {x: 100},
1209
+ {spring: springs.settle},
1210
+ {start: 'presented'},
1211
+ );
1212
+ clock = 1016;
1213
+ await commitFrame(1016);
1214
+ // The frame that creates the animations; the callback's task runs on
1215
+ // 3 ms.
1216
+ requestFrame(() => {
1217
+ clock = 1036;
1218
+ });
1219
+ clock = 1033;
1220
+ await commitFrame(1033);
1221
+ expect(live('translate')).toHaveLength(1);
1222
+ // Other tasks hold the main thread until the next frame, where WebKit's
1223
+ // start for them, that frame's time, is first read.
1224
+ clock = 1090;
1225
+ live('translate')[0].startTime = 1033;
1226
+ await commitFrame(1090);
1227
+ await expect(handle.started).resolves.toBe(1033);
1228
+ expect(live('translate')[0].startTime).toBe(1033);
1229
+ } finally {
1230
+ Reflect.deleteProperty(document, 'timeline');
1231
+ }
1232
+ });
1233
+
1234
+ it('presented: a block in the frame that creates the animations moves their start past it', async () => {
1235
+ Object.defineProperty(document, 'timeline', {
1236
+ configurable: true,
1237
+ get: () => ({currentTime: clock}),
1238
+ });
1239
+ try {
1240
+ const {animator} = setup();
1241
+ const handle = animator.animate(
1242
+ {x: 100},
1243
+ {spring: springs.settle},
1244
+ {start: 'presented'},
1245
+ );
1246
+ clock = 1016;
1247
+ await commitFrame(1016);
1248
+ // The frame that creates the animations blocks for 300 ms; WebKit's
1249
+ // start for them, that frame's time, is read at the next frame.
1250
+ requestFrame(() => {
1251
+ clock = 1333;
1252
+ });
1253
+ clock = 1033;
1254
+ await commitFrame(1033);
1255
+ clock = 1340;
1256
+ live('translate')[0].startTime = 1033;
1257
+ await commitFrame(1340);
1258
+ await expect(handle.started).resolves.toBe(1340);
1259
+ expect(live('translate')[0].startTime).toBe(1340);
1260
+ } finally {
1261
+ Reflect.deleteProperty(document, 'timeline');
1262
+ }
1263
+ });
1264
+
1265
+ it('presented: a call queued after another change flushed the queue still waits its own two frames', async () => {
1266
+ const {animator} = setup();
1267
+ animator.animate({x: 100}, {spring: springs.settle}, {start: 'presented'});
1268
+ // Another change creates the waiting call's animations first.
1269
+ animator.animate({y: 50}, {spring: springs.settle});
1270
+ clock = 1016;
1271
+ await commitFrame(1016);
1272
+ animator.animate(
1273
+ {opacity: 0.5},
1274
+ {spring: springs.settle},
1275
+ {start: 'presented'},
1276
+ );
1277
+ expect(live('opacity')).toHaveLength(0);
1278
+ // The first call's second frame is this call's first: it still waits.
1279
+ clock = 1033;
1280
+ await commitFrame(1033);
1281
+ expect(live('opacity')).toHaveLength(0);
1282
+ clock = 1050;
1283
+ await commitFrame(1050);
1284
+ expect(live('opacity')).toHaveLength(1);
1285
+ });
1286
+
1287
+ it('presented: a second presented call on the same property a frame later keeps its own two frames', async () => {
1288
+ const {animator} = setup();
1289
+ animator.animate({x: 100}, {spring: springs.settle}, {start: 'presented'});
1290
+ clock = 1016;
1291
+ await commitFrame(1016);
1292
+ animator.animate({x: 200}, {spring: springs.settle}, {start: 'presented'});
1293
+ // The first call's second frame: its property is now the second call's,
1294
+ // so nothing starts yet.
1295
+ clock = 1033;
1296
+ await commitFrame(1033);
1297
+ expect(live('translate')).toHaveLength(0);
1298
+ // The second call's own second frame.
1299
+ clock = 1050;
1300
+ await commitFrame(1050);
1301
+ expect(live('translate')).toHaveLength(1);
1302
+ await commitFrame(1066);
1303
+ expect(animator.get('x', 1066 + 5000)).toBeCloseTo(200, 3);
1304
+ });
1305
+
1306
+ it('presented: a later presented call that takes part of a waiting call\u2019s channel starts the rest of it, from its own start', async () => {
1307
+ const {animator} = setup();
1308
+ const first = animator.animate(
1309
+ {x: 100, y: 100},
1310
+ {spring: springs.settle},
1311
+ {start: 'presented'},
1312
+ );
1313
+ clock = 1016;
1314
+ await commitFrame(1016);
1315
+ animator.animate({x: 200}, {spring: springs.settle}, {start: 'presented'});
1316
+ // The first call's second frame: its channel now holds the second call's
1317
+ // curve, so nothing starts yet.
1318
+ clock = 1033;
1319
+ await commitFrame(1033);
1320
+ expect(live('translate')).toHaveLength(0);
1321
+ // The second call's second frame creates the channel, with the first
1322
+ // call's y in it; the browser starts it at the next commit.
1323
+ clock = 1050;
1324
+ await commitFrame(1050);
1325
+ expect(live('translate')).toHaveLength(1);
1326
+ clock = 1066;
1327
+ await commitFrame(1066);
1328
+ await expect(first.started).resolves.toBe(1066);
1329
+ // y was at rest on screen until then: its curve starts there, and the
1330
+ // model reads what the screen shows.
1331
+ const y = solveSpring(toSpring(springs.settle), {from: 0, to: 100});
1332
+ expect(animator.get('y', 1166)).toBeCloseTo(y.value(0.1), 6);
1333
+ expect(sampleAt(live('translate')[0], 'translate', 1166)[1]).toBeCloseTo(
1334
+ y.value(0.1),
1335
+ 0,
1336
+ );
1337
+ });
1338
+
1339
+ it('presented: another change before then creates the animations first', async () => {
1340
+ const {animator} = setup();
1341
+ animator.animate({x: 100}, {spring: springs.settle}, {start: 'presented'});
1342
+ animator.animate({y: 50}, {spring: springs.settle});
1343
+ expect(live('translate').length).toBeGreaterThan(0);
1344
+ await commitFrame(clock);
1345
+ const x = solveSpring(toSpring(springs.settle), {from: 0, to: 100});
1346
+ expect(animator.get('x', clock + 100)).toBeCloseTo(x.value(0.1), 9);
1347
+ });
1348
+
1349
+ it('presented: a retarget of a moving property starts now, from the value and velocity on screen (FR6)', async () => {
1350
+ const {animator} = setup();
1351
+ animator.animate({x: 300}, {spring: springs.settle});
1352
+ await commitFrame(1000);
1353
+ clock = 1100;
1354
+ const from = animator.get('x');
1355
+ const velocity = animator.getVelocity('x');
1356
+ animator.animate({x: 0}, {spring: springs.settle}, {start: 'presented'});
1357
+ // The element is moving, not at rest: no two frames on the old curve and
1358
+ // no restart at the commit from the call's value. The new curve starts at
1359
+ // the call, from what is on screen, as a retarget does.
1360
+ const [animation] = live('translate');
1361
+ expect(animation.startTime).toBe(1100);
1362
+ expect(animator.get('x', 1100)).toBeCloseTo(from, 6);
1363
+ expect(animator.getVelocity('x', 1100)).toBeCloseTo(velocity, 3);
1364
+ const back = solveSpring(toSpring(springs.settle), {
1365
+ from,
1366
+ to: 0,
1367
+ velocity,
1368
+ });
1369
+ expect(animator.get('x', 1200)).toBeCloseTo(back.value(0.1), 6);
1370
+ });
1371
+
1372
+ it('commit: a retarget is dated by the browser\u2019s commit, from the value on screen', async () => {
1373
+ const {animator} = setup();
1374
+ animator.animate({x: 300}, {spring: springs.settle});
1375
+ await commitFrame(1000);
1376
+ clock = 1100;
1377
+ const from = animator.get('x');
1378
+ const velocity = animator.getVelocity('x');
1379
+ const handle = animator.animate(
1380
+ {x: 0},
1381
+ {spring: springs.settle},
1382
+ {start: 'commit'},
1383
+ );
1384
+ const [animation] = live('translate');
1385
+ expect(animation.startTime).toBeNull();
1386
+ // Held where it is until the browser starts it.
1387
+ expect(animator.get('x')).toBeCloseTo(from, 9);
1388
+ clock = 1130;
1389
+ await commitFrame(1130);
1390
+ await expect(handle.started).resolves.toBe(1130);
1391
+ const back = solveSpring(toSpring(springs.settle), {
1392
+ from,
1393
+ to: 0,
1394
+ velocity,
1395
+ });
1396
+ expect(animator.get('x', 1230)).toBeCloseTo(back.value(0.1), 6);
1397
+ });
1398
+
1399
+ it('a handle hands out the curve it plays, for a companion on the same clock', async () => {
1400
+ const {animator} = setup();
1401
+ const handle = animator.animate({y: 400}, {spring: springs.sheetPresent});
1402
+ const curve = handle.curve('y');
1403
+ expect(curve).not.toBeNull();
1404
+ expect(handle.curve('x')).toBeNull();
1405
+ await commitFrame(1000);
1406
+ const y = solveSpring(toSpring(springs.sheetPresent), {from: 0, to: 400});
1407
+ for (const t of [0, 0.05, 0.2]) {
1408
+ expect(curve?.at(t)).toBeCloseTo(y.value(t), 9);
1409
+ expect(curve?.velocity(t)).toBeCloseTo(y.velocity(t), 6);
1410
+ expect(animator.get('y', 1000 + t * 1000)).toBeCloseTo(y.value(t), 9);
1411
+ }
1412
+ expect(curve?.at(curve.duration)).toBe(400);
1413
+ });
1414
+ });
1415
+
796
1416
  describe('animatorFor: one animator per element', () => {
797
1417
  it('gives every caller the same animator for an element', () => {
798
1418
  const a = document.createElement('div');
799
1419
  const b = document.createElement('div');
800
1420
  expect(animatorFor(a)).toBe(animatorFor(a));
801
1421
  expect(animatorFor(a)).not.toBe(animatorFor(b));
1422
+ expect(animatorFor(a, {pseudoElement: '::backdrop'})).toBe(
1423
+ animatorFor(a, {pseudoElement: '::backdrop'}),
1424
+ );
1425
+ expect(animatorFor(a, {pseudoElement: '::backdrop'})).not.toBe(
1426
+ animatorFor(a),
1427
+ );
802
1428
  });
803
1429
 
804
1430
  it('composes two callers’ axes in one translate track', async () => {
@@ -820,8 +1446,8 @@ describe('animatorFor: one animator per element', () => {
820
1446
  const y = solveSpring(toSpring(springs.settle), {from: 0, to: -44});
821
1447
  expect(swipe.get('y', clock + 30)).toBeCloseTo(y.value(0.03), 9);
822
1448
  const [tx, ty] = sampleAt(tracks[0], 'translate', clock + 30);
823
- expect(tx).toBeCloseTo(x.value(0.08), 1);
824
- expect(ty).toBeCloseTo(y.value(0.03), 1);
1449
+ expect(Math.abs(tx - x.value(0.08))).toBeLessThan(0.26);
1450
+ expect(Math.abs(ty - y.value(0.03))).toBeLessThan(0.26);
825
1451
  });
826
1452
  });
827
1453
 
@@ -890,6 +1516,74 @@ describe('promotion: a compositing layer while it moves', () => {
890
1516
  expect(element.style.willChange).toBe('');
891
1517
  });
892
1518
 
1519
+ it("reads the stylesheet's will-change in a task of its own, so a promotion reads no style in its own task", async () => {
1520
+ // A drag's first move promotes: reading the stylesheet's reasons then
1521
+ // would force a style recalculation in the input handler.
1522
+ stylesheet['will-change'] = 'transform, opacity';
1523
+ const {animator, element} = setup();
1524
+ // In the document, where the read happens.
1525
+ document.body.append(element);
1526
+ await commitFrame(clock);
1527
+ await vi.advanceTimersByTimeAsync(0);
1528
+ const readsOf = () =>
1529
+ vi
1530
+ .mocked(window.getComputedStyle)
1531
+ .mock.calls.filter(([read]) => read === element).length;
1532
+ const before = readsOf();
1533
+ animator.set({y: 10});
1534
+ expect(readsOf()).toBe(before);
1535
+ expect(element.style.willChange).toBe('transform, opacity, translate');
1536
+ await advance(PROMOTION_LINGER_MS + 10);
1537
+ expect(element.style.willChange).toBe('');
1538
+ element.remove();
1539
+ });
1540
+
1541
+ it('reads no style for a node out of the document: a node removed before its read is never read', async () => {
1542
+ // A shared animator (animatorFor) is never detached: its node can leave
1543
+ // the document with the read still waiting. A node out of the document
1544
+ // has no style to read, and reading one costs a style pass for nothing.
1545
+ stubMedia(false, false);
1546
+ const readsOf = (node: Element) =>
1547
+ vi
1548
+ .mocked(window.getComputedStyle)
1549
+ .mock.calls.filter(([read]) => read === node).length;
1550
+ const removed = document.createElement('div');
1551
+ document.body.append(removed);
1552
+ createAnimator(removed, {now: () => clock, requestFrame});
1553
+ removed.remove();
1554
+ const kept = document.createElement('div');
1555
+ document.body.append(kept);
1556
+ createAnimator(kept, {now: () => clock, requestFrame});
1557
+ await vi.advanceTimersByTimeAsync(0);
1558
+ expect(readsOf(removed)).toBe(0);
1559
+ // A node still in the document is read, once, in that task.
1560
+ expect(readsOf(kept)).toBe(1);
1561
+ kept.remove();
1562
+ });
1563
+
1564
+ it("keeps the stylesheet's will-change when a node read nothing before it moves", () => {
1565
+ // Put back after its read was skipped, a node reads its stylesheet's
1566
+ // reasons when a promotion needs them.
1567
+ stylesheet['will-change'] = 'transform, opacity';
1568
+ stubMedia(false, false);
1569
+ const element = document.createElement('div');
1570
+ const animator = createAnimator(element, {
1571
+ now: () => clock,
1572
+ requestFrame,
1573
+ });
1574
+ document.body.append(element);
1575
+ animator.set({y: 10});
1576
+ expect(element.style.willChange).toBe('transform, opacity, translate');
1577
+ element.remove();
1578
+ });
1579
+
1580
+ it("keeps the stylesheet's will-change when an element attached in this task moves at once", () => {
1581
+ stylesheet['will-change'] = 'transform, opacity';
1582
+ const {animator, element} = setup();
1583
+ animator.animate({y: 100}, {spring: springs.settle});
1584
+ expect(element.style.willChange).toBe('transform, opacity, translate');
1585
+ });
1586
+
893
1587
  it('promotes during direct manipulation, until the writes stop', async () => {
894
1588
  const {animator, element} = setup();
895
1589
  animator.set({y: 10});
@@ -902,6 +1596,52 @@ describe('promotion: a compositing layer while it moves', () => {
902
1596
  expect(element.style.willChange).toBe('');
903
1597
  });
904
1598
 
1599
+ it('set with promote: false writes the values without a layer, and leaves other promotion alone', async () => {
1600
+ // A one-off switch (a veil under a sticky header during a programmatic
1601
+ // scroll) must not give its element a layer.
1602
+ const {animator, element} = setup();
1603
+ animator.set({opacity: 0.5}, {promote: false});
1604
+ expect(element.style.opacity).toBe('0.5');
1605
+ expect(element.style.willChange).toBe('');
1606
+ await advance(PROMOTION_LINGER_MS + 10);
1607
+ expect(element.style.willChange).toBe('');
1608
+
1609
+ // A channel a running motion promoted stays promoted, on its own clock.
1610
+ const moving = setup();
1611
+ moving.animator.animate({x: 120}, {spring: springs.settle});
1612
+ expect(moving.element.style.willChange).toBe('translate');
1613
+ moving.animator.set({opacity: 0.25}, {promote: false});
1614
+ expect(moving.element.style.opacity).toBe('0.25');
1615
+ expect(moving.element.style.willChange).toBe('translate');
1616
+ await advance(5000 + PROMOTION_LINGER_MS);
1617
+ expect(moving.element.style.willChange).toBe('');
1618
+
1619
+ // A channel with other motion still running gets its animation's layer.
1620
+ const shared = setup();
1621
+ shared.animator.animate({y: 80}, {spring: springs.settle});
1622
+ shared.animator.set({x: 10}, {promote: false});
1623
+ expect(live('translate').length).toBeGreaterThan(0);
1624
+ expect(shared.element.style.willChange).toBe('translate');
1625
+ });
1626
+
1627
+ it('promotes a pseudo-element through a custom property on its host', async () => {
1628
+ const {animator, element} = setup({pseudoElement: '::backdrop'});
1629
+ animator.set({opacity: 0});
1630
+ // set() promotes a pseudo-element too.
1631
+ expect(
1632
+ element.style.getPropertyValue('--astryx-motion-will-change-backdrop'),
1633
+ ).toBe('opacity');
1634
+ animator.animate({opacity: 1}, {spring: springs.alertPresent});
1635
+ expect(
1636
+ element.style.getPropertyValue('--astryx-motion-will-change-backdrop'),
1637
+ ).toBe('opacity');
1638
+ expect(element.style.willChange).toBe('');
1639
+ await advance(5000 + PROMOTION_LINGER_MS);
1640
+ expect(
1641
+ element.style.getPropertyValue('--astryx-motion-will-change-backdrop'),
1642
+ ).toBe('');
1643
+ });
1644
+
905
1645
  it('ends promotion on the element it leaves', () => {
906
1646
  const {animator, element} = setup();
907
1647
  animator.animate({x: 50}, {spring: springs.settle});
@@ -1000,10 +1740,12 @@ describe('presentation values, retargeting, velocity (spec:AST-094 FR6)', () =>
1000
1740
  animator.animate({x: 0}, {spring: springs.settle});
1001
1741
  const [animation] = live('translate');
1002
1742
  for (let t = clock; t < clock + 400; t += 7) {
1003
- expect(sampleAt(animation, 'translate', t)[1]).toBeCloseTo(
1004
- yCurve.value((t - 1000) / 1000),
1005
- 1,
1006
- );
1743
+ expect(
1744
+ Math.abs(
1745
+ sampleAt(animation, 'translate', t)[1] -
1746
+ yCurve.value((t - 1000) / 1000),
1747
+ ),
1748
+ ).toBeLessThan(0.26);
1007
1749
  expect(animator.get('y', t)).toBeCloseTo(
1008
1750
  yCurve.value((t - 1000) / 1000),
1009
1751
  9,
@@ -1098,6 +1840,52 @@ describe('finished', () => {
1098
1840
  await expect(c.finished).resolves.toBe(false);
1099
1841
  });
1100
1842
 
1843
+ it('waits while a sampler holds the animation past the curve\u2019s end', async () => {
1844
+ const {animator} = setup();
1845
+ const handle = animator.animate({x: 40}, {spring: springs.quickChange});
1846
+ let result: boolean | undefined;
1847
+ void handle.finished.then(value => {
1848
+ result = value;
1849
+ });
1850
+ await advance(16);
1851
+ // A harness pauses it (holdAnimations) and keeps it held well past the
1852
+ // end of the curve on the wall clock: the motion has not ended.
1853
+ const [animation] = live('translate');
1854
+ animation.paused = true;
1855
+ await advance(5000);
1856
+ expect(result).toBeUndefined();
1857
+ });
1858
+
1859
+ it('resolves true once a held animation resumes and reaches its end', async () => {
1860
+ const {animator} = setup();
1861
+ const handle = animator.animate({x: 40}, {spring: springs.quickChange});
1862
+ let result: boolean | undefined;
1863
+ void handle.finished.then(value => {
1864
+ result = value;
1865
+ });
1866
+ await advance(16);
1867
+ const [animation] = live('translate');
1868
+ animation.paused = true;
1869
+ await advance(5000);
1870
+ animation.paused = false;
1871
+ await advance(1000);
1872
+ expect(result).toBe(true);
1873
+ });
1874
+
1875
+ it('resolves on time for an animation nobody holds', async () => {
1876
+ const {animator} = setup();
1877
+ const handle = animator.animate({x: 40}, {spring: springs.quickChange});
1878
+ let result: boolean | undefined;
1879
+ void handle.finished.then(value => {
1880
+ result = value;
1881
+ });
1882
+ const end = (handle.curve('x')?.duration ?? NaN) * 1000;
1883
+ await advance(end - 50);
1884
+ expect(result).toBeUndefined();
1885
+ await advance(100);
1886
+ expect(result).toBe(true);
1887
+ });
1888
+
1101
1889
  it('dispose cancels and settles everything', async () => {
1102
1890
  const {animator} = setup();
1103
1891
  const handle = animator.animate(
@@ -1127,6 +1915,64 @@ describe('Reduced Motion (design:reduced-motion)', () => {
1127
1915
  ),
1128
1916
  );
1129
1917
 
1918
+ it('a call naming the opacity a running dip heads to keeps the dip', async () => {
1919
+ const {animator} = reduced();
1920
+ animator.animate({x: 100}, {spring: springs.navigationPush});
1921
+ await commitFrame(clock);
1922
+ const start = clock;
1923
+ await advance(fade / 4);
1924
+ // Mid fade out: a call names the opacity the dip returns to.
1925
+ const handle = animator.animate({opacity: 1}, {spring: springs.settle});
1926
+ // The dip goes on as it was: at its bottom when it was due, not opening
1927
+ // a second dip from where it had got to.
1928
+ expect(animator.get('opacity', start + fade / 2)).toBeCloseTo(0, 6);
1929
+ await advance(fade / 4 + 1);
1930
+ // Landed at the bottom; the fade back in ends when the dip was due to.
1931
+ expect(animator.get('x')).toBe(100);
1932
+ expect(animator.get('opacity', start + fade)).toBeCloseTo(1, 6);
1933
+ await advance(fade + END_HOLD_MS + 1);
1934
+ expect(animator.get('x')).toBe(100);
1935
+ expect(animator.get('opacity')).toBe(1);
1936
+ expect(await handle.finished).toBe(true);
1937
+ });
1938
+
1939
+ it('a call naming the opacity a dip heads to, in the same task as the dip, hands the browser no second fade', async () => {
1940
+ const {animator} = reduced();
1941
+ animator.animate({x: 100}, {spring: springs.navigationPush});
1942
+ animator.animate({opacity: 1}, {spring: springs.settle});
1943
+ await commitFrame(clock);
1944
+ const fades = recorded.filter(animation =>
1945
+ animation.keyframes.some(keyframe => 'opacity' in keyframe),
1946
+ );
1947
+ expect(fades).toHaveLength(1);
1948
+ expect(fades[0].cancelled).toBe(false);
1949
+ });
1950
+
1951
+ it('a release played as a dip, then its state layer naming the opacity the dip returns to, fades once', async () => {
1952
+ // A swipe row's release: the move's layer plays the travel, then the
1953
+ // state layer lands its opacity on the same element in the same task.
1954
+ const {animator} = reduced();
1955
+ const move = animator.play(
1956
+ {x: {duration: 0.3, at: t => (100 * t) / 0.3, velocity: () => 100 / 0.3}},
1957
+ {touchRelease: true},
1958
+ );
1959
+ const state = animator.animate(
1960
+ {opacity: 1},
1961
+ {spring: springs.settle},
1962
+ {touchRelease: true},
1963
+ );
1964
+ await commitFrame(clock);
1965
+ const fades = recorded.filter(animation =>
1966
+ animation.keyframes.some(keyframe => 'opacity' in keyframe),
1967
+ );
1968
+ expect(fades).toHaveLength(1);
1969
+ await advance(fade + END_HOLD_MS + 1);
1970
+ expect(animator.get('x')).toBe(100);
1971
+ expect(animator.get('opacity')).toBe(1);
1972
+ expect(await move.finished).toBe(true);
1973
+ expect(await state.finished).toBe(true);
1974
+ });
1975
+
1130
1976
  it('an entrance after an outside finish starts from what is on screen', async () => {
1131
1977
  const {animator} = reduced();
1132
1978
  animator.animate(
@@ -1149,6 +1995,24 @@ describe('Reduced Motion (design:reduced-motion)', () => {
1149
1995
  expect(fade?.keyframes[0].opacity).toBe('0');
1150
1996
  });
1151
1997
 
1998
+ it('presented: a call that moves and fades in from transparent lands its travel', async () => {
1999
+ // A control part at its rest offset, transparent, presented in one task
2000
+ // with its own animator (SearchField's close button).
2001
+ const {animator, element} = reduced();
2002
+ animator.set({x: 44, opacity: 0});
2003
+ animator.animate(
2004
+ {x: 0, opacity: 1},
2005
+ {spring: springs.settle},
2006
+ {start: 'presented'},
2007
+ );
2008
+ await commitFrame(clock);
2009
+ await commitFrame(clock);
2010
+ await advance(fade + 16);
2011
+ expect(animator.get('x')).toBe(0);
2012
+ expect(parse(element.style.translate)[0] || 0).toBe(0);
2013
+ expect(animator.get('opacity', clock)).toBeCloseTo(1, 9);
2014
+ });
2015
+
1152
2016
  it('fades for 200 ms at most', () => {
1153
2017
  expect(fade).toBeGreaterThan(0);
1154
2018
  expect(fade).toBeLessThanOrEqual(200);
@@ -1351,10 +2215,13 @@ describe('Reduced Motion (design:reduced-motion)', () => {
1351
2215
 
1352
2216
  it('a node detached mid-dip and attached again in a later task shows the move landed and visible', async () => {
1353
2217
  const {animator, element} = reduced();
1354
- // The call owns its opacity: its dip writes the low opacity inline.
2218
+ // The call owns its opacity: its dip writes opacity inline, the value on
2219
+ // screen as the fade starts (its end is written when the fade retires),
2220
+ // while the model is already partway down.
1355
2221
  animator.animate({x: 240, opacity: 1}, {spring: springs.navigationPush});
1356
2222
  await advance(25);
1357
- expect(Number(element.style.opacity)).toBeLessThan(1);
2223
+ expect(element.style.opacity).not.toBe('');
2224
+ expect(animator.get('opacity')).toBeLessThan(1);
1358
2225
  animator.attach(null);
1359
2226
  await advance(100);
1360
2227
  animator.attach(element);
@@ -1364,6 +2231,24 @@ describe('Reduced Motion (design:reduced-motion)', () => {
1364
2231
  expect(Number(element.style.opacity || '1')).toBe(1);
1365
2232
  });
1366
2233
 
2234
+ it('a node detached mid-dip from a dimmed opacity shows the move landed and visible when attached again', async () => {
2235
+ // With the presented start, a curve writes its starting value inline: a
2236
+ // dip from a dimmed opacity leaves that low value on the node while the
2237
+ // move still waits to land.
2238
+ const {animator, element} = reduced();
2239
+ animator.set({opacity: 0.3});
2240
+ animator.animate({x: 240, opacity: 1}, {spring: springs.navigationPush});
2241
+ await advance(25);
2242
+ expect(Number(element.style.opacity)).toBeLessThan(1);
2243
+ expect(animator.get('x')).toBe(0);
2244
+ animator.attach(null);
2245
+ await advance(100);
2246
+ animator.attach(element);
2247
+ expect(animator.get('x')).toBe(240);
2248
+ expect(animator.get('opacity')).toBe(1);
2249
+ expect(Number(element.style.opacity || '1')).toBe(1);
2250
+ });
2251
+
1367
2252
  it('a node detached mid-dip while another axis moves is left with no animation or promotion', async () => {
1368
2253
  const {animator, element} = reduced();
1369
2254
  animator.set({x: 0, y: 0});
@@ -1642,6 +2527,92 @@ describe('Reduced Motion (design:reduced-motion)', () => {
1642
2527
  }
1643
2528
  });
1644
2529
 
2530
+ it('a landing later than the whole fade back in shows the element at once and settles then', async () => {
2531
+ // A hold released long after the dip was due: the landing\u2019s timer runs
2532
+ // more than the whole fade back in late, so the element appears at once
2533
+ // where it landed. No fade is left for the browser to start a frame
2534
+ // later, so the call\u2019s finished never reports an element still at 0.
2535
+ for (const sheet of ['own', 'stylesheet'] as const) {
2536
+ const {animator, element} = reduced();
2537
+ stylesheet = sheet === 'stylesheet' ? {opacity: '0.6'} : {};
2538
+ const handle = animator.animate(
2539
+ {x: 100},
2540
+ {spring: springs.navigationPush},
2541
+ );
2542
+ for (const animation of recorded) {
2543
+ animation.paused = true;
2544
+ }
2545
+ await commitFrame(clock);
2546
+ clock += fade * 2;
2547
+ for (const animation of recorded) {
2548
+ animation.finish();
2549
+ }
2550
+ const before = recorded.length;
2551
+ let settled: boolean | undefined;
2552
+ void handle.finished.then(value => {
2553
+ settled = value;
2554
+ });
2555
+ // The next frame finds the hold finished: the landing is long due.
2556
+ await commitFrame(clock);
2557
+ await vi.advanceTimersByTimeAsync(0);
2558
+ expect(animator.get('x')).toBe(100);
2559
+ expect(
2560
+ recorded
2561
+ .slice(before)
2562
+ .filter(
2563
+ animation =>
2564
+ !animation.cancelled &&
2565
+ 'opacity' in (animation.keyframes[0] ?? {}),
2566
+ ),
2567
+ ).toHaveLength(0);
2568
+ expect(settled).toBe(true);
2569
+ if (sheet === 'stylesheet') {
2570
+ // Handed back to the stylesheet at once.
2571
+ expect(element.style.opacity).toBe('');
2572
+ } else {
2573
+ expect(animator.get('opacity')).toBe(1);
2574
+ }
2575
+ animator.dispose();
2576
+ recorded = [];
2577
+ }
2578
+ });
2579
+
2580
+ it('a move held before its first frame and finished from outside still lands when its retire runs before the next frame', async () => {
2581
+ // A sampler pauses the call's animations in its own task and finishes
2582
+ // them later. A loaded page can run the retire timer, which cancels the
2583
+ // finished fade, before the frame that would read the call's start: the
2584
+ // call must not be left waiting for a start that no animation can give.
2585
+ const {animator} = reduced();
2586
+ const handle = animator.animate({x: 100}, {spring: springs.navigationPush});
2587
+ for (const animation of recorded) {
2588
+ animation.paused = true;
2589
+ }
2590
+ await commitFrame(clock);
2591
+ clock += 30;
2592
+ await commitFrame(clock);
2593
+ for (const animation of recorded) {
2594
+ animation.finish();
2595
+ }
2596
+ let settled: boolean | undefined;
2597
+ void handle.finished.then(value => {
2598
+ settled = value;
2599
+ });
2600
+ // The retire timer first, with no frame in between: it cancels the
2601
+ // finished fade out.
2602
+ clock += fade * 2;
2603
+ await vi.advanceTimersByTimeAsync(fade * 2);
2604
+ expect(
2605
+ recorded.filter(animation => animation.finished && !animation.cancelled),
2606
+ ).toEqual([]);
2607
+ // Frames keep coming: step the clock through the landing.
2608
+ for (let step = 0; step < 16; step++) {
2609
+ await advance(fade / 4);
2610
+ }
2611
+ expect(animator.get('x')).toBe(100);
2612
+ expect(animator.get('opacity')).toBe(1);
2613
+ expect(settled).toBe(true);
2614
+ });
2615
+
1645
2616
  it('a waiting call whose own opacity a later call retargets settles interrupted', async () => {
1646
2617
  const {animator} = reduced();
1647
2618
  const dismiss = animator.animate(
@@ -1937,6 +2908,75 @@ describe('Reduced Motion (design:reduced-motion)', () => {
1937
2908
  expect(element.style.translate).toBe('100px 0px');
1938
2909
  });
1939
2910
 
2911
+ it("a pseudo-element's dip borrows its own opacity and hands it back without touching the host", async () => {
2912
+ for (const role of [undefined, 'exit'] as const) {
2913
+ const {animator, element} = setup({
2914
+ reduced: true,
2915
+ coarse: true,
2916
+ pseudoElement: '::before',
2917
+ });
2918
+ element.style.opacity = '0.5'; // the host's own, untouched
2919
+ pseudoStylesheet = {opacity: '0.3'};
2920
+ animator.animate(
2921
+ {x: 100},
2922
+ {spring: springs.navigationPush},
2923
+ role == null ? {} : {crossfadeAs: role},
2924
+ );
2925
+ await commitFrame(clock);
2926
+ const [fadeLayer] = live('opacity');
2927
+ const values = fadeLayer.keyframes.map(k => Number(k.opacity));
2928
+ expect(Math.max(...values)).toBeCloseTo(0.3, 9);
2929
+ await advance(fade * 2 + END_HOLD_MS + 1);
2930
+ expect(animator.get('x')).toBe(100);
2931
+ // The pseudo-element's stylesheet owns its opacity again; the host
2932
+ // keeps its own.
2933
+ expect(live('opacity')).toEqual([]);
2934
+ expect(element.style.opacity).toBe('0.5');
2935
+ }
2936
+ });
2937
+
2938
+ it("a pseudo-element's identity hold follows a node replacement", () => {
2939
+ const {animator, element} = setup({pseudoElement: '::backdrop'});
2940
+ pseudoStylesheet = {opacity: '0'};
2941
+ animator.set({opacity: 1});
2942
+ animator.attach(null);
2943
+ const next = document.createElement('div');
2944
+ document.body.append(next);
2945
+ animator.attach(next);
2946
+ const held = live('opacity').filter(
2947
+ animation => animation.element === next,
2948
+ );
2949
+ expect(held).toHaveLength(1);
2950
+ expect(Number(held[0].keyframes.at(-1)!.opacity)).toBe(1);
2951
+ expect(live('opacity').some(a => a.element === element)).toBe(false);
2952
+ next.remove();
2953
+ });
2954
+
2955
+ it('a pseudo-element shown over a hiding stylesheet keeps its own opacity through a move', async () => {
2956
+ for (const role of [undefined, 'entrance'] as const) {
2957
+ const {animator} = setup({
2958
+ reduced: true,
2959
+ coarse: true,
2960
+ pseudoElement: '::backdrop',
2961
+ });
2962
+ pseudoStylesheet = {opacity: '0'};
2963
+ animator.set({opacity: 1});
2964
+ animator.animate(
2965
+ {x: 100},
2966
+ {spring: springs.navigationPush},
2967
+ role == null ? {} : {crossfadeAs: role},
2968
+ );
2969
+ await commitFrame(clock);
2970
+ for (let step = 0; step < 8; step++) {
2971
+ await advance(fade / 4);
2972
+ }
2973
+ expect(animator.get('x')).toBe(100);
2974
+ expect(animator.get('opacity')).toBe(1);
2975
+ const [held] = live('opacity');
2976
+ expect(Number(held.keyframes.at(-1)?.opacity)).toBe(1);
2977
+ }
2978
+ });
2979
+
1940
2980
  it('a move dips: fades out, lands while transparent, fades back in', async () => {
1941
2981
  const {animator, element} = reduced();
1942
2982
  animator.animate({x: 100}, {spring: springs.navigationPush});
@@ -2328,6 +3368,8 @@ describe('play: any closed-form curve', () => {
2328
3368
  const end = press.value(0.266);
2329
3369
  expect(animator.get('scale', 1266)).toBeCloseTo(end, 12);
2330
3370
  expect(animator.getVelocity('scale', 1266)).toBe(0);
3371
+ // Retired, the end is inline.
3372
+ await advance(400);
2331
3373
  for (const axis of parse(element.style.scale)) {
2332
3374
  expect(axis).toBeCloseTo(end, 4);
2333
3375
  }
@@ -2434,7 +3476,7 @@ describe('decay: momentum curves (a coast)', () => {
2434
3476
  Math.abs(
2435
3477
  sampleAt(animation, 'translate', 1000 + t)[1] - glide.value(t / 1000),
2436
3478
  ),
2437
- ).toBeLessThan(0.11);
3479
+ ).toBeLessThan(0.26);
2438
3480
  }
2439
3481
  expect(animator.get('y', 1e7)).toBeCloseTo(glide.rest, 9);
2440
3482
  });
@@ -2555,6 +3597,56 @@ describe('decay: momentum curves (a coast)', () => {
2555
3597
  });
2556
3598
  });
2557
3599
 
3600
+ describe('pseudoElement: a dialog\u2019s ::backdrop', () => {
3601
+ it('targets the pseudo-element, writes no inline style and holds the end state', async () => {
3602
+ const {animator, element} = setup({pseudoElement: '::backdrop'});
3603
+ animator.set({opacity: 0});
3604
+ const [held] = live('opacity');
3605
+ expect(held.options).toMatchObject({
3606
+ fill: 'both',
3607
+ pseudoElement: '::backdrop',
3608
+ });
3609
+ expect(held.keyframes.map(k => k.opacity)).toEqual(['0', '0']);
3610
+ expect(element.style.opacity).toBe('');
3611
+
3612
+ animator.animate({opacity: 1}, {spring: springs.alertPresent});
3613
+ // The new animation exists before the hold goes: no frame in between.
3614
+ expect(held.cancelled).toBe(true);
3615
+ const [fade] = live('opacity');
3616
+ expect(fade.options).toMatchObject({
3617
+ fill: 'both',
3618
+ pseudoElement: '::backdrop',
3619
+ });
3620
+ expect(fade.keyframes[fade.keyframes.length - 1].opacity).toBe('1');
3621
+ expect(element.style.opacity).toBe('');
3622
+ await advance(Number(fade.options.duration) + 1000);
3623
+ // Past the curve's end, a static animation holds the end value.
3624
+ expect(fade.cancelled).toBe(true);
3625
+ const [rest] = live('opacity');
3626
+ expect(rest.options).toMatchObject({
3627
+ fill: 'both',
3628
+ pseudoElement: '::backdrop',
3629
+ });
3630
+ expect(rest.keyframes.map(k => k.opacity)).toEqual(['1', '1']);
3631
+ expect(animator.get('opacity')).toBe(1);
3632
+ });
3633
+
3634
+ it('an exit holds 0 until the next animation replaces it', async () => {
3635
+ const {animator} = setup({
3636
+ pseudoElement: '::backdrop',
3637
+ initial: {opacity: 1},
3638
+ });
3639
+ animator.animate({opacity: 0}, {spring: springs.alertDismiss});
3640
+ const [fade] = live('opacity');
3641
+ await advance(Number(fade.options.duration) + 1000);
3642
+ const [rest] = live('opacity');
3643
+ expect(rest.keyframes.map(k => k.opacity)).toEqual(['0', '0']);
3644
+ animator.animate({opacity: 1}, {spring: springs.alertPresent});
3645
+ expect(rest.cancelled).toBe(true);
3646
+ expect(live('opacity')).toHaveLength(1);
3647
+ });
3648
+ });
3649
+
2558
3650
  describe('attach', () => {
2559
3651
  it('keeps state without an element and resumes the curve on attach', () => {
2560
3652
  const animator = createAnimator(null, {
@@ -2572,7 +3664,8 @@ describe('attach', () => {
2572
3664
  curve.value(0.1),
2573
3665
  1,
2574
3666
  );
2575
- expect(element.style.translate).toBe('300px 0px');
3667
+ // The value on screen, not the end.
3668
+ expect(parse(element.style.translate)[0]).toBeCloseTo(curve.value(0.1), 2);
2576
3669
  });
2577
3670
 
2578
3671
  it('unmounting settles a pending atRest() wait and leaves no poll timer', async () => {