@astryxdesign/core 0.7.5 → 0.7.6-canary.5cd7a15

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 (463) hide show
  1. package/dist/AlertDialog/AlertDialog.d.ts +1 -1
  2. package/dist/AlertDialog/AlertDialog.d.ts.map +1 -1
  3. package/dist/AlertDialog/AlertDialog.js +5 -5
  4. package/dist/Avatar/Avatar.d.ts.map +1 -1
  5. package/dist/Avatar/Avatar.js +12 -3
  6. package/dist/Badge/Badge.d.ts.map +1 -1
  7. package/dist/Badge/Badge.js +7 -1
  8. package/dist/BottomSheet/BottomSheet.js +1 -1
  9. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  10. package/dist/BottomSheet/BottomSheetPanel.js +2 -1
  11. package/dist/BottomSheet/useSheetGestures.d.ts +12 -0
  12. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
  13. package/dist/BottomSheet/useSheetGestures.js +30 -3
  14. package/dist/Button/Button.js +4 -4
  15. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  16. package/dist/CheckboxInput/CheckboxInput.js +6 -5
  17. package/dist/DateInput/DateInput.d.ts.map +1 -1
  18. package/dist/DateInput/DateInput.js +12 -4
  19. package/dist/DateInput/MonthScroller.d.ts.map +1 -1
  20. package/dist/DateInput/MonthScroller.js +101 -15
  21. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  22. package/dist/DateInput/TouchDateField.js +8 -3
  23. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  24. package/dist/DateRangeInput/DateRangeInput.js +1 -1
  25. package/dist/DateTimeInput/DateTimeInput.d.ts +5 -2
  26. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  27. package/dist/DateTimeInput/DateTimeInput.js +23 -12
  28. package/dist/DateTimeInput/NativeTimeSegment.d.ts.map +1 -1
  29. package/dist/DateTimeInput/NativeTimeSegment.js +5 -4
  30. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  31. package/dist/DateTimeInput/TouchDateTimeField.js +4 -3
  32. package/dist/Dialog/Dialog.d.ts.map +1 -1
  33. package/dist/Dialog/Dialog.js +34 -9
  34. package/dist/Dialog/DialogHeader.d.ts.map +1 -1
  35. package/dist/Dialog/DialogHeader.js +5 -4
  36. package/dist/Divider/Divider.d.ts.map +1 -1
  37. package/dist/Divider/Divider.js +2 -1
  38. package/dist/Drawer/Drawer.d.ts +1 -0
  39. package/dist/Drawer/Drawer.d.ts.map +1 -1
  40. package/dist/Drawer/Drawer.js +7 -2
  41. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  42. package/dist/DropdownMenu/DropdownMenu.js +4 -1
  43. package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts.map +1 -1
  44. package/dist/DropdownMenu/MenuBottomSheetActionList.js +6 -5
  45. package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts +10 -1
  46. package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts.map +1 -1
  47. package/dist/DropdownMenu/lazyMenuBottomSheet.js +16 -2
  48. package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -1
  49. package/dist/DropdownMenu/renderDropdownItems.js +21 -16
  50. package/dist/EmptyState/EmptyState.d.ts.map +1 -1
  51. package/dist/EmptyState/EmptyState.js +2 -1
  52. package/dist/Field/FieldLabel.d.ts +1 -1
  53. package/dist/Field/FieldLabel.d.ts.map +1 -1
  54. package/dist/Field/FieldLabel.js +4 -4
  55. package/dist/Field/InputClearButton.js +3 -3
  56. package/dist/Field/PanelSearchInput.js +2 -2
  57. package/dist/FieldStatus/FieldStatus.js +2 -2
  58. package/dist/FileInput/FileInput.d.ts.map +1 -1
  59. package/dist/FileInput/FileInput.js +7 -1
  60. package/dist/Heading/Heading.js +1 -1
  61. package/dist/Icon/Icon.js +1 -1
  62. package/dist/Indicator/CheckIndicator.d.ts.map +1 -1
  63. package/dist/Indicator/CheckIndicator.js +2 -1
  64. package/dist/Indicator/CheckboxIndicator.d.ts.map +1 -1
  65. package/dist/Indicator/CheckboxIndicator.js +3 -1
  66. package/dist/Indicator/RadioIndicator.d.ts.map +1 -1
  67. package/dist/Indicator/RadioIndicator.js +3 -1
  68. package/dist/Item/Item.d.ts.map +1 -1
  69. package/dist/Item/Item.js +2 -1
  70. package/dist/Item/ItemSwipeLayer.js +1 -1
  71. package/dist/Layout/Layout.js +1 -1
  72. package/dist/Layout/LayoutContent.js +1 -1
  73. package/dist/Layout/LayoutFooter.js +1 -1
  74. package/dist/Layout/LayoutHeader.js +1 -1
  75. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  76. package/dist/Lightbox/Lightbox.js +5 -0
  77. package/dist/Lightbox/LightboxViewport.a11y.browser.spec.js +5 -4
  78. package/dist/List/List.js +1 -1
  79. package/dist/List/ListItem.js +3 -3
  80. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  81. package/dist/MobileNav/MobileNav.js +7 -2
  82. package/dist/MultiSelector/MultiSelector.d.ts +1 -1
  83. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  84. package/dist/MultiSelector/MultiSelector.js +5 -2
  85. package/dist/NumberInput/NumberInput.d.ts +3 -1
  86. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  87. package/dist/NumberInput/NumberInput.js +3 -3
  88. package/dist/NumberInput/numberParser.d.ts +3 -0
  89. package/dist/NumberInput/numberParser.d.ts.map +1 -1
  90. package/dist/NumberInput/numberParser.js +7 -44
  91. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  92. package/dist/ProgressBar/ProgressBar.js +16 -12
  93. package/dist/Resizable/useResizable.d.ts.map +1 -1
  94. package/dist/Resizable/useResizable.js +28 -2
  95. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  96. package/dist/SegmentedControl/SegmentedControlItem.js +4 -1
  97. package/dist/Selector/useSelectorPresentation.d.ts +8 -1
  98. package/dist/Selector/useSelectorPresentation.d.ts.map +1 -1
  99. package/dist/Selector/useSelectorPresentation.js +20 -4
  100. package/dist/Slider/Slider.d.ts.map +1 -1
  101. package/dist/Slider/Slider.js +7 -1
  102. package/dist/Spinner/Spinner.js +2 -2
  103. package/dist/Stack/Stack.js +1 -1
  104. package/dist/Stack/StackItem.js +1 -1
  105. package/dist/TabList/TabList.d.ts.map +1 -1
  106. package/dist/TabList/TabList.js +16 -1
  107. package/dist/Table/plugins/columnResize/useTableColumnResize.d.ts.map +1 -1
  108. package/dist/Table/plugins/columnResize/useTableColumnResize.js +9 -1
  109. package/dist/Text/Text.js +1 -1
  110. package/dist/Text/useTruncation.d.ts.map +1 -1
  111. package/dist/Text/useTruncation.js +23 -5
  112. package/dist/TextArea/TextArea.d.ts +1 -1
  113. package/dist/TextArea/TextArea.d.ts.map +1 -1
  114. package/dist/TextArea/TextArea.js +13 -2
  115. package/dist/TextInput/TextInput.d.ts.map +1 -1
  116. package/dist/TextInput/TextInput.js +3 -3
  117. package/dist/TimeInput/TimeInput.d.ts +6 -2
  118. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  119. package/dist/TimeInput/TimeInput.js +21 -14
  120. package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
  121. package/dist/TimeInput/TouchTimeField.js +5 -4
  122. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  123. package/dist/Timestamp/Timestamp.js +2 -2
  124. package/dist/Timestamp/TimestampHoverCard.js +4 -4
  125. package/dist/Toast/Toast.d.ts.map +1 -1
  126. package/dist/Toast/Toast.js +60 -20
  127. package/dist/Toast/ToastViewport.d.ts.map +1 -1
  128. package/dist/Toast/ToastViewport.js +82 -51
  129. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  130. package/dist/Tokenizer/Tokenizer.js +13 -2
  131. package/dist/Tokenizer/TokenizerBottomSheet.js +3 -3
  132. package/dist/Toolbar/Toolbar.d.ts.map +1 -1
  133. package/dist/Toolbar/Toolbar.js +2 -1
  134. package/dist/Typeahead/Typeahead.d.ts +1 -1
  135. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  136. package/dist/Typeahead/Typeahead.js +9 -1
  137. package/dist/astryx.css +15 -1
  138. package/dist/gestures/GestureArena.d.ts +6 -0
  139. package/dist/gestures/GestureArena.d.ts.map +1 -1
  140. package/dist/gestures/GestureArena.js +39 -0
  141. package/dist/gestures/GestureHost.d.ts +352 -0
  142. package/dist/gestures/GestureHost.d.ts.map +1 -0
  143. package/dist/gestures/GestureHost.js +1773 -0
  144. package/dist/gestures/GestureRecognizer.d.ts +10 -0
  145. package/dist/gestures/GestureRecognizer.d.ts.map +1 -1
  146. package/dist/gestures/GestureRecognizer.js +16 -0
  147. package/dist/gestures/LongPressGestureRecognizer.d.ts +1 -0
  148. package/dist/gestures/LongPressGestureRecognizer.d.ts.map +1 -1
  149. package/dist/gestures/LongPressGestureRecognizer.js +3 -0
  150. package/dist/gestures/PanGestureRecognizer.d.ts +1 -0
  151. package/dist/gestures/PanGestureRecognizer.d.ts.map +1 -1
  152. package/dist/gestures/PanGestureRecognizer.js +3 -0
  153. package/dist/gestures/PinchGestureRecognizer.d.ts +31 -0
  154. package/dist/gestures/PinchGestureRecognizer.d.ts.map +1 -0
  155. package/dist/gestures/PinchGestureRecognizer.js +128 -0
  156. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts +71 -0
  157. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts.map +1 -0
  158. package/dist/gestures/ScreenEdgePanGestureRecognizer.js +223 -0
  159. package/dist/gestures/SwipeGestureRecognizer.d.ts +37 -0
  160. package/dist/gestures/SwipeGestureRecognizer.d.ts.map +1 -0
  161. package/dist/gestures/SwipeGestureRecognizer.js +148 -0
  162. package/dist/gestures/TapGestureRecognizer.d.ts +1 -0
  163. package/dist/gestures/TapGestureRecognizer.d.ts.map +1 -1
  164. package/dist/gestures/TapGestureRecognizer.js +3 -0
  165. package/dist/gestures/VelocityTracker.d.ts +6 -2
  166. package/dist/gestures/VelocityTracker.d.ts.map +1 -1
  167. package/dist/gestures/VelocityTracker.js +7 -3
  168. package/dist/gestures/haptics.d.ts +65 -0
  169. package/dist/gestures/haptics.d.ts.map +1 -0
  170. package/dist/gestures/haptics.js +182 -0
  171. package/dist/gestures/nativeScroll.d.ts +42 -0
  172. package/dist/gestures/nativeScroll.d.ts.map +1 -0
  173. package/dist/gestures/nativeScroll.js +228 -0
  174. package/dist/gestures/touchAction.d.ts +60 -0
  175. package/dist/gestures/touchAction.d.ts.map +1 -0
  176. package/dist/gestures/touchAction.js +157 -0
  177. package/dist/gestures/touchActionRule.d.ts +34 -0
  178. package/dist/gestures/touchActionRule.d.ts.map +1 -0
  179. package/dist/gestures/touchActionRule.js +140 -0
  180. package/dist/gestures/types.d.ts +18 -7
  181. package/dist/gestures/types.d.ts.map +1 -1
  182. package/dist/gestures/types.js +22 -2
  183. package/dist/hooks/useAdaptivePresentation.d.ts.map +1 -1
  184. package/dist/hooks/useAdaptivePresentation.js +4 -1
  185. package/dist/hooks/useMenuHover.d.ts.map +1 -1
  186. package/dist/hooks/useMenuHover.js +12 -1
  187. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  188. package/dist/i18n/generated-locales/en.generated.js +3 -0
  189. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  190. package/dist/i18n/generated-locales/pseudo.generated.js +3 -0
  191. package/dist/motion/animator.d.ts +7 -1
  192. package/dist/motion/animator.d.ts.map +1 -1
  193. package/dist/motion/animator.js +119 -27
  194. package/dist/theme/defineTheme.js +1 -1
  195. package/dist/theme/domainTokens/domainTokenDefaults.d.ts +3 -0
  196. package/dist/theme/domainTokens/domainTokenDefaults.d.ts.map +1 -0
  197. package/dist/theme/domainTokens/domainTokenDefaults.js +19 -0
  198. package/dist/theme/domainTokens/index.d.ts +5 -4
  199. package/dist/theme/domainTokens/index.d.ts.map +1 -1
  200. package/dist/theme/domainTokens/index.js +5 -9
  201. package/dist/utils/focusReturn.d.ts +17 -1
  202. package/dist/utils/focusReturn.d.ts.map +1 -1
  203. package/dist/utils/focusReturn.js +11 -4
  204. package/dist/utils/getNodeText.d.ts +8 -0
  205. package/dist/utils/getNodeText.d.ts.map +1 -0
  206. package/dist/utils/getNodeText.js +37 -0
  207. package/dist/utils/interactionModality.d.ts +11 -2
  208. package/dist/utils/interactionModality.d.ts.map +1 -1
  209. package/dist/utils/interactionModality.js +58 -5
  210. package/dist/utils/plainDate.d.ts +2 -0
  211. package/dist/utils/plainDate.d.ts.map +1 -1
  212. package/dist/utils/plainDate.js +46 -3
  213. package/dist/utils/presentation.d.ts +50 -0
  214. package/dist/utils/presentation.d.ts.map +1 -0
  215. package/dist/utils/presentation.js +125 -0
  216. package/dist/utils/primaryClick.d.ts +11 -0
  217. package/dist/utils/primaryClick.d.ts.map +1 -0
  218. package/dist/utils/primaryClick.js +25 -0
  219. package/dist/utils/timeFormat.d.ts +47 -0
  220. package/dist/utils/timeFormat.d.ts.map +1 -0
  221. package/dist/utils/timeFormat.js +240 -0
  222. package/dist/utils/touchPresentation.d.ts.map +1 -1
  223. package/dist/utils/touchPresentation.js +5 -3
  224. package/dist/utils/touchPresentation.stylex.d.ts +15 -0
  225. package/dist/utils/touchPresentation.stylex.d.ts.map +1 -0
  226. package/dist/utils/touchPresentation.stylex.js +33 -0
  227. package/dist/utils/unicodeDigits.d.ts +20 -0
  228. package/dist/utils/unicodeDigits.d.ts.map +1 -0
  229. package/dist/utils/unicodeDigits.js +70 -0
  230. package/locales/en.json +15 -3
  231. package/locales/pseudo.json +9 -0
  232. package/package.json +5 -4
  233. package/src/AlertDialog/AlertDialog.tsx +6 -5
  234. package/src/AlertDialog/__tests__/AlertDialog.a11y.chromium.spec.ts +18 -6
  235. package/src/Avatar/Avatar.spec.md +22 -22
  236. package/src/Avatar/Avatar.tsx +18 -1
  237. package/src/Avatar/__tests__/AvatarForcedColors.a11y.chromium.spec.ts +126 -0
  238. package/src/Badge/Badge.doc.mjs +1 -1
  239. package/src/Badge/Badge.tsx +19 -2
  240. package/src/Badge/__tests__/BadgeLargerText.a11y.chromium.spec.ts +260 -0
  241. package/src/BottomSheet/BottomSheet.test.tsx +46 -0
  242. package/src/BottomSheet/BottomSheet.tsx +1 -1
  243. package/src/BottomSheet/BottomSheetPanel.tsx +2 -1
  244. package/src/BottomSheet/useSheetGestures.test.ts +52 -0
  245. package/src/BottomSheet/useSheetGestures.ts +37 -6
  246. package/src/Breadcrumbs/__tests__/Breadcrumbs.a11y.chromium.spec.ts +18 -6
  247. package/src/Button/Button.tsx +4 -4
  248. package/src/Calendar/Calendar.spec.md +87 -48
  249. package/src/Calendar/__tests__/Calendar.a11y.chromium.spec.ts +21 -7
  250. package/src/Carousel/__tests__/Carousel.a11y.chromium.spec.ts +21 -7
  251. package/src/CheckboxInput/CheckboxInput.tsx +6 -5
  252. package/src/Collapsible/Collapsible.spec.md +2 -0
  253. package/src/Collapsible/CollapsibleGroup.spec.md +57 -29
  254. package/src/Collapsible/__tests__/Collapsible.a11y.chromium.spec.ts +18 -7
  255. package/src/Collapsible/__tests__/CollapsibleGroupAccordion.a11y.chromium.spec.ts +18 -6
  256. package/src/ComplexSelector/ComplexSelector.test.tsx +37 -0
  257. package/src/ContextMenu/ContextMenu.test.tsx +35 -0
  258. package/src/DateInput/DateInput.test.tsx +115 -3
  259. package/src/DateInput/DateInput.tsx +29 -14
  260. package/src/DateInput/DateInputTouch.test.tsx +76 -0
  261. package/src/DateInput/MonthScroller.tsx +106 -20
  262. package/src/DateInput/MonthScrollerSettle.test.tsx +165 -0
  263. package/src/DateInput/TouchDateField.tsx +14 -3
  264. package/src/DateInput/__tests__/MonthScrollerSteps.interaction.browser.spec.ts +312 -0
  265. package/src/DateRangeInput/DateRangeInput.test.tsx +46 -3
  266. package/src/DateRangeInput/DateRangeInput.tsx +16 -12
  267. package/src/DateTimeInput/DateTimeInput.doc.mjs +10 -8
  268. package/src/DateTimeInput/DateTimeInput.spec.md +75 -51
  269. package/src/DateTimeInput/DateTimeInput.test.tsx +81 -0
  270. package/src/DateTimeInput/DateTimeInput.tsx +46 -24
  271. package/src/DateTimeInput/NativeTimeSegment.tsx +4 -6
  272. package/src/DateTimeInput/TouchDateTimeField.tsx +3 -7
  273. package/src/Dialog/Dialog.test.tsx +216 -2
  274. package/src/Dialog/Dialog.tsx +42 -9
  275. package/src/Dialog/DialogHeader.tsx +5 -4
  276. package/src/Dialog/__tests__/DialogSecondDocument.a11y.browser.spec.ts +202 -0
  277. package/src/Divider/Divider.tsx +2 -1
  278. package/src/Drawer/Drawer.spec.md +111 -80
  279. package/src/Drawer/Drawer.test.tsx +35 -0
  280. package/src/Drawer/Drawer.tsx +13 -2
  281. package/src/Drawer/DrawerHeader.spec.md +181 -0
  282. package/src/Drawer/__tests__/DrawerNonModalPageScroll.a11y.chromium.spec.ts +267 -0
  283. package/src/DropdownMenu/DropdownMenu.test.tsx +41 -0
  284. package/src/DropdownMenu/DropdownMenu.tsx +7 -1
  285. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +34 -0
  286. package/src/DropdownMenu/MenuBottomSheetActionList.tsx +6 -5
  287. package/src/DropdownMenu/__tests__/Menu.a11y.chromium.spec.ts +19 -7
  288. package/src/DropdownMenu/__tests__/MenuButton.a11y.chromium.spec.ts +21 -9
  289. package/src/DropdownMenu/lazyMenuBottomSheet.test.ts +53 -0
  290. package/src/DropdownMenu/lazyMenuBottomSheet.ts +23 -6
  291. package/src/DropdownMenu/renderDropdownItems.tsx +24 -19
  292. package/src/EmptyState/EmptyState.tsx +2 -1
  293. package/src/Field/FieldLabel.tsx +5 -4
  294. package/src/Field/InputClearButton.tsx +3 -3
  295. package/src/Field/PanelSearchInput.tsx +2 -2
  296. package/src/FieldStatus/FieldStatus.tsx +2 -2
  297. package/src/FileInput/FileInput.test.tsx +21 -0
  298. package/src/FileInput/FileInput.tsx +4 -1
  299. package/src/FormLayout/FormLayout.spec.md +7 -8
  300. package/src/Heading/Heading.tsx +1 -1
  301. package/src/Icon/Icon.tsx +1 -1
  302. package/src/Indicator/CheckIndicator.spec.md +47 -42
  303. package/src/Indicator/CheckIndicator.tsx +2 -1
  304. package/src/Indicator/CheckboxIndicator.spec.md +40 -37
  305. package/src/Indicator/CheckboxIndicator.tsx +3 -1
  306. package/src/Indicator/Indicator.spec.md +188 -0
  307. package/src/Indicator/RadioIndicator.tsx +3 -1
  308. package/src/Item/Item.tsx +2 -1
  309. package/src/Item/ItemSwipeLayer.tsx +1 -1
  310. package/src/Layout/Layout.spec.md +127 -131
  311. package/src/Layout/Layout.tsx +1 -1
  312. package/src/Layout/LayoutContent.spec.md +205 -0
  313. package/src/Layout/LayoutContent.tsx +1 -1
  314. package/src/Layout/LayoutFooter.spec.md +190 -0
  315. package/src/Layout/LayoutFooter.tsx +1 -1
  316. package/src/Layout/LayoutHeader.spec.md +193 -0
  317. package/src/Layout/LayoutHeader.tsx +1 -1
  318. package/src/Layout/LayoutPanel.spec.md +194 -0
  319. package/src/Layout/__tests__/Layout.a11y.chromium.spec.ts +18 -6
  320. package/src/Lightbox/Lightbox.test.tsx +18 -0
  321. package/src/Lightbox/Lightbox.tsx +5 -0
  322. package/src/Lightbox/LightboxViewport.a11y.browser.spec.ts +8 -4
  323. package/src/Link/__tests__/Link.a11y.chromium.spec.ts +18 -8
  324. package/src/List/List.tsx +1 -1
  325. package/src/List/ListItem.spec.md +205 -0
  326. package/src/List/ListItem.tsx +3 -3
  327. package/src/MobileNav/MobileNav.test.tsx +15 -0
  328. package/src/MobileNav/MobileNav.tsx +9 -1
  329. package/src/MobileNav/__tests__/MobileNavHeader.a11y.chromium.spec.ts +89 -0
  330. package/src/MoreMenu/MoreMenu.test.tsx +11 -0
  331. package/src/MultiSelector/MultiSelector.spec.md +7 -4
  332. package/src/MultiSelector/MultiSelector.test.tsx +118 -1
  333. package/src/MultiSelector/MultiSelector.tsx +6 -2
  334. package/src/MultiSelector/__tests__/MultiSelector.listbox.a11y.test.tsx +8 -1
  335. package/src/NavMenu/NavHeadingMenu.spec.md +1 -5
  336. package/src/NumberInput/NumberInput.doc.mjs +13 -7
  337. package/src/NumberInput/NumberInput.spec.md +59 -37
  338. package/src/NumberInput/NumberInput.tsx +13 -3
  339. package/src/NumberInput/__tests__/NumberInput.coarse-pointer.a11y.chromium.spec.ts +102 -0
  340. package/src/NumberInput/numberParser.ts +7 -45
  341. package/src/ProgressBar/ProgressBar.test.tsx +108 -32
  342. package/src/ProgressBar/ProgressBar.tsx +58 -10
  343. package/src/ProgressBar/__tests__/ProgressBar.a11y.chromium.spec.ts +21 -9
  344. package/src/ProgressBar/__tests__/ProgressBarMotion.a11y.chromium.spec.ts +831 -0
  345. package/src/Resizable/__tests__/ResizeHandle.a11y.chromium.spec.ts +21 -14
  346. package/src/Resizable/useResizable.test.ts +43 -0
  347. package/src/Resizable/useResizable.ts +32 -9
  348. package/src/SegmentedControl/SegmentedControl.test.tsx +84 -0
  349. package/src/SegmentedControl/SegmentedControlItem.tsx +11 -5
  350. package/src/Selector/Selector.test.tsx +90 -1
  351. package/src/Selector/__tests__/Selector.combobox.a11y.test.tsx +8 -1
  352. package/src/Selector/__tests__/Selector.listbox.a11y.test.tsx +8 -1
  353. package/src/Selector/useSelectorPresentation.ts +31 -3
  354. package/src/SideNav/SideNav.test.tsx +27 -0
  355. package/src/SideNav/__tests__/SideNavRailMotion.interaction.browser.spec.ts +176 -51
  356. package/src/Slider/Slider.test.tsx +12 -0
  357. package/src/Slider/Slider.tsx +4 -1
  358. package/src/Slider/__tests__/Slider.a11y.chromium.spec.ts +19 -7
  359. package/src/Spinner/Spinner.tsx +2 -2
  360. package/src/Stack/Stack.tsx +1 -1
  361. package/src/Stack/StackItem.tsx +1 -1
  362. package/src/TabList/TabList.test.tsx +58 -4
  363. package/src/TabList/TabList.tsx +16 -1
  364. package/src/TabList/__tests__/TabPressPointer.interaction.browser.spec.ts +163 -0
  365. package/src/Table/__tests__/Table.a11y.chromium.spec.ts +18 -6
  366. package/src/Table/plugins/columnResize/useTableColumnResize.test.tsx +22 -0
  367. package/src/Table/plugins/columnResize/useTableColumnResize.tsx +8 -4
  368. package/src/Text/Text.tsx +1 -1
  369. package/src/Text/useTruncation.test.tsx +128 -1
  370. package/src/Text/useTruncation.ts +23 -5
  371. package/src/TextArea/TextArea.spec.md +10 -13
  372. package/src/TextArea/TextArea.test.tsx +60 -0
  373. package/src/TextArea/TextArea.tsx +15 -1
  374. package/src/TextInput/TextInput.test.tsx +21 -0
  375. package/src/TextInput/TextInput.tsx +3 -1
  376. package/src/TimeInput/NativeTimeInput.test.tsx +17 -0
  377. package/src/TimeInput/Presentation.test.tsx +21 -0
  378. package/src/TimeInput/TimeInput.doc.mjs +8 -8
  379. package/src/TimeInput/TimeInput.spec.md +45 -48
  380. package/src/TimeInput/TimeInput.test.tsx +76 -0
  381. package/src/TimeInput/TimeInput.tsx +36 -18
  382. package/src/TimeInput/TouchTimeField.tsx +4 -6
  383. package/src/Timestamp/Timestamp.spec.md +6 -12
  384. package/src/Timestamp/Timestamp.test.tsx +30 -5
  385. package/src/Timestamp/Timestamp.tsx +6 -2
  386. package/src/Timestamp/TimestampHoverCard.tsx +4 -4
  387. package/src/Toast/Toast.spec.md +14 -20
  388. package/src/Toast/Toast.tsx +104 -36
  389. package/src/Toast/ToastViewport.test.tsx +719 -1
  390. package/src/Toast/ToastViewport.tsx +100 -50
  391. package/src/Tokenizer/Tokenizer.spec.md +5 -3
  392. package/src/Tokenizer/Tokenizer.test.tsx +68 -0
  393. package/src/Tokenizer/Tokenizer.tsx +14 -2
  394. package/src/Tokenizer/TokenizerBottomSheet.test.tsx +15 -4
  395. package/src/Tokenizer/TokenizerBottomSheet.tsx +3 -3
  396. package/src/Toolbar/Toolbar.spec.md +5 -6
  397. package/src/Toolbar/Toolbar.test.tsx +30 -0
  398. package/src/Toolbar/Toolbar.tsx +2 -1
  399. package/src/Toolbar/__tests__/Toolbar.a11y.chromium.spec.ts +18 -6
  400. package/src/Tooltip/__tests__/Tooltip.a11y.chromium.spec.ts +18 -7
  401. package/src/TopNav/TopNav.test.tsx +27 -0
  402. package/src/Typeahead/Typeahead.test.tsx +40 -0
  403. package/src/Typeahead/Typeahead.tsx +12 -1
  404. package/src/gestures/GestureArena.ts +35 -0
  405. package/src/gestures/GestureHost.ts +2065 -0
  406. package/src/gestures/GestureRecognizer.ts +18 -0
  407. package/src/gestures/LongPressGestureRecognizer.ts +4 -0
  408. package/src/gestures/PanGestureRecognizer.ts +4 -0
  409. package/src/gestures/PinchGestureRecognizer.ts +148 -0
  410. package/src/gestures/ScreenEdgePanGestureRecognizer.ts +256 -0
  411. package/src/gestures/SwipeGestureRecognizer.ts +197 -0
  412. package/src/gestures/TapGestureRecognizer.ts +4 -0
  413. package/src/gestures/VelocityTracker.ts +10 -3
  414. package/src/gestures/__tests__/GestureHost.test.ts +2087 -0
  415. package/src/gestures/__tests__/PanGestureRecognizer.test.ts +4 -4
  416. package/src/gestures/__tests__/PinchGestureRecognizer.test.ts +74 -0
  417. package/src/gestures/__tests__/ScreenEdgePanGestureRecognizer.test.ts +498 -0
  418. package/src/gestures/__tests__/SwipeGestureRecognizer.test.ts +108 -0
  419. package/src/gestures/__tests__/arena.test.ts +5 -0
  420. package/src/gestures/__tests__/arenaPinchSwipeEdge.test.ts +193 -0
  421. package/src/gestures/__tests__/browser/build-fixture.mjs +46 -0
  422. package/src/gestures/__tests__/browser/fixture.ts +307 -0
  423. package/src/gestures/__tests__/browser/gestures.interaction.browser.spec.ts +1137 -0
  424. package/src/gestures/__tests__/browserNegotiation.test.ts +293 -0
  425. package/src/gestures/__tests__/haptics.test.ts +187 -0
  426. package/src/gestures/__tests__/harness.ts +21 -2
  427. package/src/gestures/__tests__/recognizerKind.test.ts +78 -0
  428. package/src/gestures/__tests__/touchActionRule.test.ts +54 -0
  429. package/src/gestures/haptics.ts +203 -0
  430. package/src/gestures/nativeScroll.ts +277 -0
  431. package/src/gestures/touchAction.ts +191 -0
  432. package/src/gestures/touchActionRule.ts +157 -0
  433. package/src/gestures/types.ts +30 -9
  434. package/src/hooks/useAdaptivePresentation.ts +4 -1
  435. package/src/hooks/useMenuHover.ts +15 -1
  436. package/src/i18n/generated-locales/en.generated.ts +4 -0
  437. package/src/i18n/generated-locales/pseudo.generated.ts +4 -0
  438. package/src/motion/README.md +11 -1
  439. package/src/motion/animator.test.ts +151 -3
  440. package/src/motion/animator.ts +149 -33
  441. package/src/theme/defineTheme.ts +1 -1
  442. package/src/theme/derivedVarRegistry.test.ts +4 -0
  443. package/src/theme/domainTokens/domainTokenDefaults.ts +19 -0
  444. package/src/theme/domainTokens/index.ts +6 -10
  445. package/src/utils/focusReturn.ts +17 -6
  446. package/src/utils/getNodeText.test.ts +24 -0
  447. package/src/utils/getNodeText.ts +37 -0
  448. package/src/utils/interactionModality.test.ts +45 -1
  449. package/src/utils/interactionModality.ts +64 -5
  450. package/src/utils/plainDate.test.ts +90 -0
  451. package/src/utils/plainDate.ts +58 -3
  452. package/src/utils/presentation.test.ts +380 -0
  453. package/src/utils/presentation.ts +180 -0
  454. package/src/utils/pressPaintPriority.test.ts +80 -0
  455. package/src/utils/primaryClick.test.ts +38 -0
  456. package/src/utils/primaryClick.ts +28 -0
  457. package/src/utils/timeFormat.test.ts +204 -0
  458. package/src/utils/timeFormat.ts +314 -0
  459. package/src/utils/touchPresentation.stylex.ts +43 -0
  460. package/src/utils/touchPresentation.test.ts +140 -1
  461. package/src/utils/touchPresentation.ts +5 -3
  462. package/src/utils/unicodeDigits.test.ts +44 -0
  463. package/src/utils/unicodeDigits.ts +71 -0
@@ -6,7 +6,9 @@
6
6
  * @file interactionModality.ts
7
7
  * @input Global pointerdown/keydown events
8
8
  * @output `useInteractionModalityTracking()` activates document-lifetime tracking;
9
- * `getInteractionModality()` reads how the user last interacted
9
+ * `getInteractionModality()` reads how the user last interacted;
10
+ * `trackInteractionModalityIn(doc)` and `getInteractionModality(doc)` do the
11
+ * same for a second document
10
12
  * @position Internal document-wide singleton; used where `:focus-visible` alone
11
13
  * is too broad
12
14
  *
@@ -26,8 +28,12 @@
26
28
  * The store lives on `document`, so duplicate Astryx bundles share one state
27
29
  * value and one listener pair. The first mounted consumer activates tracking;
28
30
  * the listeners then remain for the document lifetime so input during a gap
29
- * between consumers is not lost. Importing this module has no DOM side effects,
30
- * and server rendering keeps the keyboard-safe default.
31
+ * between consumers is not lost. A second document (a same-origin iframe, a
32
+ * Document Picture-in-Picture window) gets its own store when a component
33
+ * tracks it, since input there never reaches the page's listeners; its
34
+ * listeners stay only while a consumer there is mounted. Importing this module
35
+ * has no DOM side effects, and server rendering keeps the keyboard-safe
36
+ * default.
31
37
  */
32
38
 
33
39
  import {useEffect} from 'react';
@@ -37,6 +43,8 @@ export type InteractionModality = 'keyboard' | 'pointer';
37
43
  type InteractionModalityStore = {
38
44
  modality: InteractionModality;
39
45
  isListening: boolean;
46
+ /** Mounted consumers holding a second document's listeners. */
47
+ consumers?: number;
40
48
  onPointerDown: () => void;
41
49
  onKeyDown: (event: KeyboardEvent) => void;
42
50
  };
@@ -107,8 +115,59 @@ export function useInteractionModalityTracking(): void {
107
115
  useEffect(() => trackInteractionModality(), []);
108
116
  }
109
117
 
110
- /** How the user last interacted with the page. */
111
- export function getInteractionModality(): InteractionModality {
118
+ /**
119
+ * Track a second document while a consumer there is mounted: a component an
120
+ * app renders into a same-origin iframe or a Document Picture-in-Picture
121
+ * window gets input there that never reaches the page's listeners. The first
122
+ * consumer installs the document's listener pair and the last one's cleanup
123
+ * removes it. Read the value with `getInteractionModality(doc)`. The page's
124
+ * own document keeps its document-lifetime listeners.
125
+ */
126
+ export function trackInteractionModalityIn(
127
+ targetDocument: Document,
128
+ ): () => void {
129
+ if (typeof document !== 'undefined' && targetDocument === document) {
130
+ return trackInteractionModality();
131
+ }
132
+ const store = getStore(targetDocument);
133
+ store.consumers = (store.consumers ?? 0) + 1;
134
+ if (store.consumers === 1) {
135
+ store.isListening = true;
136
+ targetDocument.addEventListener('pointerdown', store.onPointerDown, {
137
+ capture: true,
138
+ passive: true,
139
+ });
140
+ targetDocument.addEventListener('keydown', store.onKeyDown, {
141
+ capture: true,
142
+ passive: true,
143
+ });
144
+ }
145
+ let isReleased = false;
146
+ return () => {
147
+ if (isReleased) {
148
+ return;
149
+ }
150
+ isReleased = true;
151
+ store.consumers = (store.consumers ?? 1) - 1;
152
+ if (store.consumers === 0) {
153
+ store.isListening = false;
154
+ targetDocument.removeEventListener('pointerdown', store.onPointerDown, {
155
+ capture: true,
156
+ });
157
+ targetDocument.removeEventListener('keydown', store.onKeyDown, {
158
+ capture: true,
159
+ });
160
+ }
161
+ };
162
+ }
163
+
164
+ /** How the user last interacted with the page, or with `targetDocument`. */
165
+ export function getInteractionModality(
166
+ targetDocument?: Document,
167
+ ): InteractionModality {
168
+ if (targetDocument != null) {
169
+ return getStore(targetDocument).modality;
170
+ }
112
171
  return typeof document === 'undefined'
113
172
  ? 'keyboard'
114
173
  : getStore(document).modality;
@@ -7,6 +7,7 @@ import {
7
7
  plainDateFromISO,
8
8
  plainDateToISO,
9
9
  plainDateToDate,
10
+ dateTimeFormatFor,
10
11
  plainDateFromDate,
11
12
  plainDateToday,
12
13
  getDaysInMonth,
@@ -28,6 +29,7 @@ import {
28
29
  plainDateGetWeekNumber,
29
30
  plainDateFormat,
30
31
  formatSharedDate,
32
+ DATE_FORMAT_LONG,
31
33
  DATE_FORMAT_WITH_WEEKDAY,
32
34
  DATE_FORMAT_MONTH_YEAR,
33
35
  plainDateWeekFieldName,
@@ -754,6 +756,94 @@ describe('plainDateGetWeekNumber', () => {
754
756
  });
755
757
 
756
758
  describe('plainDateFormat', () => {
759
+ it('keeps one formatter per locale and options, whatever order the options are in', () => {
760
+ // A month of day labels, as a calendar renders them, reuses one formatter.
761
+ const first = dateTimeFormatFor('en-NZ', DATE_FORMAT_WITH_WEEKDAY);
762
+ expect(dateTimeFormatFor('en-NZ', DATE_FORMAT_WITH_WEEKDAY)).toBe(first);
763
+ expect(
764
+ dateTimeFormatFor('en-NZ', {
765
+ year: 'numeric',
766
+ day: 'numeric',
767
+ month: 'long',
768
+ weekday: 'long',
769
+ }),
770
+ ).toBe(first);
771
+ // Other options, or another locale, get their own.
772
+ expect(dateTimeFormatFor('en-NZ', DATE_FORMAT_MONTH_YEAR)).not.toBe(first);
773
+ expect(dateTimeFormatFor('en-ZA', DATE_FORMAT_WITH_WEEKDAY)).not.toBe(
774
+ first,
775
+ );
776
+ });
777
+
778
+ it('formats the same date after the host zone moves', () => {
779
+ // The page stays open while the device travels east: a formatter cached
780
+ // in the first zone must not move the date.
781
+ const zone = process.env.TZ;
782
+ try {
783
+ process.env.TZ = 'America/Los_Angeles';
784
+ expect(
785
+ plainDateFormat(
786
+ {year: 2026, month: 2, day: 1},
787
+ DATE_FORMAT_MONTH_YEAR,
788
+ 'en-IE',
789
+ ),
790
+ ).toBe('February 2026');
791
+ process.env.TZ = 'America/New_York';
792
+ expect(
793
+ plainDateFormat(
794
+ {year: 2026, month: 2, day: 1},
795
+ DATE_FORMAT_MONTH_YEAR,
796
+ 'en-IE',
797
+ ),
798
+ ).toBe('February 2026');
799
+ process.env.TZ = 'Pacific/Kiritimati';
800
+ expect(
801
+ plainDateFormat(
802
+ {year: 2026, month: 2, day: 1},
803
+ DATE_FORMAT_WITH_WEEKDAY,
804
+ 'en-IE',
805
+ ),
806
+ ).toBe('Sunday 1 February 2026');
807
+ } finally {
808
+ if (zone === undefined) {
809
+ delete process.env.TZ;
810
+ } else {
811
+ process.env.TZ = zone;
812
+ }
813
+ }
814
+ });
815
+
816
+ it('prints a zone name or a time of day in the host zone, as before', () => {
817
+ // A PlainDate has neither: such fields read local midnight in the zone
818
+ // current at the call, not the UTC the date-only path formats in.
819
+ const zone = process.env.TZ;
820
+ try {
821
+ process.env.TZ = 'America/Los_Angeles';
822
+ const withZone = plainDateFormat(
823
+ {year: 2026, month: 1, day: 25},
824
+ {...DATE_FORMAT_LONG, timeZoneName: 'short'},
825
+ 'en-US',
826
+ );
827
+ expect(withZone).toContain('January 25, 2026');
828
+ expect(withZone).toContain('PST');
829
+ expect(withZone).not.toContain('UTC');
830
+ process.env.TZ = 'America/New_York';
831
+ expect(
832
+ plainDateFormat(
833
+ {year: 2026, month: 1, day: 25},
834
+ {...DATE_FORMAT_LONG, timeZoneName: 'short'},
835
+ 'en-US',
836
+ ),
837
+ ).toContain('EST');
838
+ } finally {
839
+ if (zone === undefined) {
840
+ delete process.env.TZ;
841
+ } else {
842
+ process.env.TZ = zone;
843
+ }
844
+ }
845
+ });
846
+
757
847
  it('returns a human-readable date string with weekday', () => {
758
848
  const result = plainDateFormat(
759
849
  {year: 2026, month: 1, day: 25},
@@ -374,10 +374,65 @@ export function plainDateFormat(
374
374
  options: Intl.DateTimeFormatOptions,
375
375
  locale: Locale = 'en',
376
376
  ): string {
377
- return new Intl.DateTimeFormat(locale, {
377
+ const calendar = options.calendar ?? 'gregory';
378
+ if (options.timeZone != null) {
379
+ return dateTimeFormatFor(locale, {...options, calendar}).format(
380
+ plainDateToDate(pd),
381
+ );
382
+ }
383
+ if (TIME_OF_DAY_FIELDS.some(field => options[field] != null)) {
384
+ // A zone name or a time of day reads local midnight in the zone current
385
+ // at the call. A cached formatter would keep the zone it was built in,
386
+ // so this one is built for the call.
387
+ return new Intl.DateTimeFormat(locale, {...options, calendar}).format(
388
+ plainDateToDate(pd),
389
+ );
390
+ }
391
+ // Date fields alone: UTC midnight formatted in UTC, so neither the cached
392
+ // formatter nor the date depends on the host's zone, which can change while
393
+ // a page is open.
394
+ return dateTimeFormatFor(locale, {
378
395
  ...options,
379
- calendar: options.calendar ?? 'gregory',
380
- }).format(plainDateToDate(pd));
396
+ calendar,
397
+ timeZone: 'UTC',
398
+ }).format(utcTime(pd.year, pd.month - 1, pd.day));
399
+ }
400
+
401
+ /** Options that print a time of day or a zone, which a PlainDate lacks. */
402
+ const TIME_OF_DAY_FIELDS = [
403
+ 'timeZoneName',
404
+ 'hour',
405
+ 'minute',
406
+ 'second',
407
+ 'fractionalSecondDigits',
408
+ 'dayPeriod',
409
+ 'timeStyle',
410
+ ] as const satisfies ReadonlyArray<keyof Intl.DateTimeFormatOptions>;
411
+
412
+ /**
413
+ * One `Intl.DateTimeFormat` per locale and options, built on first use.
414
+ * Building a formatter costs far more than formatting with it, and a calendar
415
+ * formats each of a month's 42 day labels with the same options on every
416
+ * render. The options are keyed in a fixed order, so the same options written
417
+ * in another order share a formatter.
418
+ */
419
+ const dateTimeFormats = new Map<string, Intl.DateTimeFormat>();
420
+
421
+ /** @internal plainDateFormat's formatter cache; not part of the public API. */
422
+ export function dateTimeFormatFor(
423
+ locale: Locale,
424
+ options: Intl.DateTimeFormatOptions,
425
+ ): Intl.DateTimeFormat {
426
+ const key = JSON.stringify([
427
+ locale,
428
+ Object.entries(options).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
429
+ ]);
430
+ let format = dateTimeFormats.get(key);
431
+ if (format == null) {
432
+ format = new Intl.DateTimeFormat(locale, options);
433
+ dateTimeFormats.set(key, format);
434
+ }
435
+ return format;
381
436
  }
382
437
 
383
438
  /**
@@ -0,0 +1,380 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file presentation.test.ts
5
+ * @input Uses vitest, React Testing Library, react-dom/server and a
6
+ * query-aware matchMedia and CSS.supports stub
7
+ * @output A caller's explicit presentation wins: `'popover'` pins the fine
8
+ * surface and input modes on an iPhone; an omitted value follows the
9
+ * component's default (`'popover'` or `'touch'`), distinguishable from an
10
+ * explicit `'popover'`; `'adaptive'` and `'bottom-sheet'` keep their sheet
11
+ * meaning with touch modes under a finger, the same on every touch device;
12
+ * the surface equals useAdaptivePresentation's for every
13
+ * value on every device; live pointer changes; the server and hydrating
14
+ * render, which no hint can put into touch modes; the one-moment read
15
+ * @position Unit tests of packages/core/src/utils/presentation.ts
16
+ */
17
+
18
+ import {act, renderHook} from '@testing-library/react';
19
+ import {createElement, type ComponentProps, type ReactNode} from 'react';
20
+ import {hydrateRoot, type Root} from 'react-dom/client';
21
+ import {renderToString} from 'react-dom/server';
22
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
23
+ import {
24
+ useAdaptivePresentation,
25
+ type AdaptivePresentation,
26
+ } from '../hooks/useAdaptivePresentation';
27
+ import {
28
+ readPresentation,
29
+ usePresentation,
30
+ type PresentationDefault,
31
+ type UsePresentationOptions,
32
+ } from './presentation';
33
+ import {Theme, defineTheme} from '../theme';
34
+ import {IOS_SUPPORTS_QUERY} from './touchPresentation';
35
+ import {
36
+ compactDefinition,
37
+ DEFAULT_COMPACT_DEFINITION,
38
+ } from '../theme/themeAdaptations';
39
+
40
+ const device = {coarse: false, width: 1280, height: 800, iosWebKit: false};
41
+ const listeners = new Set<() => void>();
42
+
43
+ /** Answers a media query list: `,` is or, `and` is and. */
44
+ function answer(query: string): boolean {
45
+ return query.split(', ').some(alternative =>
46
+ alternative.split(' and ').every(part => {
47
+ if (part === '(pointer: coarse)') {
48
+ return device.coarse;
49
+ }
50
+ const range = /^\((width|height) < (\d+)px\)$/.exec(part);
51
+ if (range != null) {
52
+ return device[range[1] as 'width' | 'height'] < Number(range[2]);
53
+ }
54
+ throw new Error(`unexpected query ${query}`);
55
+ }),
56
+ );
57
+ }
58
+
59
+ function setDevice(next: Partial<typeof device>): void {
60
+ Object.assign(device, next);
61
+ for (const listener of [...listeners]) {
62
+ listener();
63
+ }
64
+ }
65
+
66
+ const IPHONE = {coarse: true, width: 393, height: 852, iosWebKit: true};
67
+ const IPAD = {coarse: true, width: 820, height: 1180, iosWebKit: true};
68
+ const ANDROID = {coarse: true, width: 412, height: 915, iosWebKit: false};
69
+ const DESKTOP = {coarse: false, width: 1280, height: 800, iosWebKit: false};
70
+ const MAC_SAFARI = {coarse: false, width: 1440, height: 900, iosWebKit: false};
71
+ /** A narrow window under a fine pointer: compact by size, never by touch. */
72
+ const FINE_NARROW = {coarse: false, width: 600, height: 800, iosWebKit: false};
73
+ /** A phone in landscape: compact by its height alone. */
74
+ const PHONE_LANDSCAPE = {
75
+ coarse: true,
76
+ width: 852,
77
+ height: 393,
78
+ iosWebKit: true,
79
+ };
80
+ /** The compact edges at the default md point (768 wide, 500 high). */
81
+ const COMPACT_EDGES = [
82
+ {coarse: true, width: 767, height: 800, iosWebKit: false},
83
+ {coarse: true, width: 768, height: 800, iosWebKit: false},
84
+ {coarse: true, width: 1024, height: 499, iosWebKit: false},
85
+ {coarse: true, width: 1024, height: 500, iosWebKit: false},
86
+ ];
87
+
88
+ beforeEach(() => {
89
+ setDevice(DESKTOP);
90
+ vi.stubGlobal('CSS', {
91
+ supports: (condition: string) =>
92
+ condition === IOS_SUPPORTS_QUERY && device.iosWebKit,
93
+ });
94
+ vi.stubGlobal(
95
+ 'matchMedia',
96
+ vi.fn((query: string) => ({
97
+ get matches() {
98
+ return answer(query);
99
+ },
100
+ media: query,
101
+ addEventListener: (_type: string, listener: () => void) =>
102
+ listeners.add(listener),
103
+ removeEventListener: (_type: string, listener: () => void) =>
104
+ listeners.delete(listener),
105
+ })),
106
+ );
107
+ });
108
+
109
+ afterEach(() => {
110
+ listeners.clear();
111
+ vi.unstubAllGlobals();
112
+ });
113
+
114
+ const resolve = (
115
+ presentation: AdaptivePresentation | undefined,
116
+ whenOmitted?: PresentationDefault,
117
+ ) =>
118
+ renderHook(() => usePresentation(presentation, {whenOmitted})).result.current;
119
+
120
+ /** A component whose omitted presentation is the touch default. */
121
+ const TOUCH_WHEN_OMITTED: UsePresentationOptions = {whenOmitted: 'touch'};
122
+
123
+ describe('presentation: a caller choice wins over touch defaults', () => {
124
+ it('pins the fine surface and input modes with an explicit popover, even on an iPhone', () => {
125
+ setDevice(IPHONE);
126
+ for (const whenOmitted of ['popover', 'touch'] as const) {
127
+ expect(resolve('popover', whenOmitted)).toEqual({
128
+ surface: 'popover',
129
+ touchModes: false,
130
+ });
131
+ }
132
+ });
133
+
134
+ it("tells an omitted value from an explicit popover: the component's touch default applies only when omitted", () => {
135
+ setDevice(IPHONE);
136
+ expect(resolve(undefined, 'touch')).toEqual({
137
+ surface: 'popover',
138
+ touchModes: true,
139
+ });
140
+ // Today's components: omitted is popover.
141
+ expect(resolve(undefined)).toEqual(resolve('popover'));
142
+ expect(resolve(undefined, 'popover')).toEqual(resolve('popover'));
143
+ });
144
+
145
+ it('turns touch modes on under a finger only, the same on every touch device', () => {
146
+ setDevice(ANDROID);
147
+ expect(resolve(undefined, 'touch')).toEqual({
148
+ surface: 'popover',
149
+ touchModes: true,
150
+ });
151
+ for (const desk of [DESKTOP, MAC_SAFARI]) {
152
+ setDevice(desk);
153
+ expect(resolve(undefined, 'touch')).toMatchObject({
154
+ touchModes: false,
155
+ });
156
+ }
157
+ });
158
+
159
+ it('keeps the sheet meaning of adaptive and bottom-sheet, with touch modes under a finger', () => {
160
+ setDevice(IPHONE);
161
+ expect(resolve('adaptive')).toEqual({
162
+ surface: 'bottom-sheet',
163
+ touchModes: true,
164
+ });
165
+ setDevice(IPAD);
166
+ expect(resolve('adaptive')).toMatchObject({
167
+ surface: 'popover',
168
+ touchModes: true,
169
+ });
170
+ setDevice(DESKTOP);
171
+ expect(resolve('adaptive')).toMatchObject({
172
+ surface: 'popover',
173
+ touchModes: false,
174
+ });
175
+ expect(resolve('bottom-sheet')).toMatchObject({
176
+ surface: 'bottom-sheet',
177
+ touchModes: false,
178
+ });
179
+ });
180
+
181
+ it("renders useAdaptivePresentation's surface for every value on every device", () => {
182
+ for (const next of [
183
+ IPHONE,
184
+ IPAD,
185
+ ANDROID,
186
+ DESKTOP,
187
+ MAC_SAFARI,
188
+ FINE_NARROW,
189
+ PHONE_LANDSCAPE,
190
+ ...COMPACT_EDGES,
191
+ ]) {
192
+ setDevice(next);
193
+ for (const value of ['popover', 'bottom-sheet', 'adaptive'] as const) {
194
+ const {result} = renderHook(() => ({
195
+ surface: usePresentation(value).surface,
196
+ adaptive: useAdaptivePresentation(value),
197
+ }));
198
+ expect(result.current.surface).toBe(result.current.adaptive);
199
+ }
200
+ }
201
+ });
202
+
203
+ it("answers 'adaptive' with the sheet only on a compact touch screen: a fine narrow window, a phone in landscape and each compact edge (spec:AST-094 FR13, spec:AST-012 FR9)", () => {
204
+ for (const [next, surface] of [
205
+ [FINE_NARROW, 'popover'],
206
+ [PHONE_LANDSCAPE, 'bottom-sheet'],
207
+ [COMPACT_EDGES[0], 'bottom-sheet'],
208
+ [COMPACT_EDGES[1], 'popover'],
209
+ [COMPACT_EDGES[2], 'bottom-sheet'],
210
+ [COMPACT_EDGES[3], 'popover'],
211
+ ] as const) {
212
+ setDevice(next);
213
+ expect({...next, surface: resolve('adaptive').surface}).toEqual({
214
+ ...next,
215
+ surface,
216
+ });
217
+ }
218
+ });
219
+
220
+ it("follows the active theme's md point for 'adaptive' (spec:AST-012 FR9)", () => {
221
+ const wideMd = defineTheme({
222
+ name: 'wide-md-presentation',
223
+ adaptations: {widthBreakpoints: {md: 1000, lg: 1100}},
224
+ });
225
+ const wrapper = ({children}: {children: ReactNode}) =>
226
+ createElement(
227
+ Theme,
228
+ {theme: wideMd} as ComponentProps<typeof Theme>,
229
+ children,
230
+ );
231
+ setDevice({coarse: true, width: 900, height: 1024});
232
+ // Under the default md point (768) this screen is not compact.
233
+ const plain = renderHook(() => usePresentation('adaptive'));
234
+ expect(plain.result.current.surface).toBe('popover');
235
+ plain.unmount();
236
+ // Under a theme whose md is 1000, the same screen is compact.
237
+ const themed = renderHook(() => usePresentation('adaptive'), {wrapper});
238
+ expect(themed.result.current).toEqual({
239
+ surface: 'bottom-sheet',
240
+ touchModes: true,
241
+ });
242
+ });
243
+
244
+ it('follows the pointer when it changes (an iPad docking a trackpad)', () => {
245
+ setDevice(IPAD);
246
+ const {result} = renderHook(() =>
247
+ usePresentation(undefined, {whenOmitted: 'touch'}),
248
+ );
249
+ expect(result.current.touchModes).toBe(true);
250
+ act(() => setDevice({coarse: false}));
251
+ // A fine pointer in this context, checked before the claim.
252
+ expect(window.matchMedia('(pointer: coarse)').matches).toBe(false);
253
+ expect(result.current).toMatchObject({touchModes: false});
254
+ act(() => setDevice({coarse: true}));
255
+ expect(result.current).toMatchObject({touchModes: true});
256
+ });
257
+
258
+ it('renders no touch surface or input mode on the server or while hydrating, and takes no hint that would (spec:AST-094 FR13)', () => {
259
+ // On a phone, where a live read would answer touch modes.
260
+ setDevice(IPHONE);
261
+ function Probe({
262
+ presentation,
263
+ options = TOUCH_WHEN_OMITTED,
264
+ }: {
265
+ presentation?: AdaptivePresentation;
266
+ options?: UsePresentationOptions;
267
+ }) {
268
+ const resolved = usePresentation(presentation, options);
269
+ return `${resolved.surface}/${String(resolved.touchModes)}`;
270
+ }
271
+ expect(renderToString(createElement(Probe))).toContain('popover/false');
272
+ expect(
273
+ renderToString(createElement(Probe, {presentation: 'adaptive'})),
274
+ ).toContain('popover/false');
275
+ expect(
276
+ renderToString(createElement(Probe, {presentation: 'bottom-sheet'})),
277
+ ).toContain('bottom-sheet/false');
278
+ // A server hint would put touch modes into the server render, which
279
+ // hydration then contradicts: usePresentation takes none.
280
+ const hinted = {
281
+ whenOmitted: 'touch',
282
+ // @ts-expect-error usePresentation takes no serverDefault.
283
+ serverDefault: {isTouch: true, isCompact: true},
284
+ } satisfies UsePresentationOptions;
285
+ expect(
286
+ renderToString(
287
+ createElement(Probe, {presentation: 'adaptive', options: hinted}),
288
+ ),
289
+ ).toContain('popover/false');
290
+ });
291
+
292
+ it('hydrates on a phone from the server render, then follows the device (spec:AST-094 FR13)', async () => {
293
+ setDevice(IPHONE);
294
+ const renders: string[] = [];
295
+ function Probe({presentation}: {presentation: AdaptivePresentation}) {
296
+ const resolved = usePresentation(presentation, TOUCH_WHEN_OMITTED);
297
+ const text = `${resolved.surface}/${String(resolved.touchModes)}`;
298
+ renders.push(text);
299
+ return text;
300
+ }
301
+ const html = renderToString(
302
+ createElement(Probe, {presentation: 'adaptive'}),
303
+ );
304
+ expect(html).toContain('popover/false');
305
+ const container = document.createElement('div');
306
+ container.innerHTML = html;
307
+ document.body.append(container);
308
+ renders.length = 0;
309
+ const recoverable = vi.fn();
310
+ let root: Root | null = null;
311
+ await act(async () => {
312
+ root = hydrateRoot(
313
+ container,
314
+ createElement(Probe, {presentation: 'adaptive'}),
315
+ {onRecoverableError: recoverable},
316
+ );
317
+ });
318
+ // The hydrating render is the server's; the phone's answer follows it.
319
+ expect(renders[0]).toBe('popover/false');
320
+ expect(renders.at(-1)).toBe('bottom-sheet/true');
321
+ expect(recoverable).not.toHaveBeenCalled();
322
+ act(() => root?.unmount());
323
+ container.remove();
324
+ });
325
+
326
+ it('subscribes only to the queries a request reads (architecture:react-component-runtime INV5)', () => {
327
+ setDevice(IPHONE);
328
+ for (const [presentation, whenOmitted, subscriptions] of [
329
+ ['popover', 'touch', 0],
330
+ [undefined, 'popover', 0],
331
+ ['bottom-sheet', 'popover', 1],
332
+ [undefined, 'touch', 1],
333
+ ['adaptive', 'popover', 2],
334
+ ] as const) {
335
+ listeners.clear();
336
+ const {unmount} = renderHook(() =>
337
+ usePresentation(presentation, {whenOmitted}),
338
+ );
339
+ expect({
340
+ presentation,
341
+ whenOmitted,
342
+ subscriptions: listeners.size,
343
+ }).toEqual({presentation, whenOmitted, subscriptions});
344
+ unmount();
345
+ expect(listeners.size).toBe(0);
346
+ }
347
+ });
348
+
349
+ it('answers at one moment as the hook does, and as the server would with no window', () => {
350
+ setDevice(IPHONE);
351
+ const compact = DEFAULT_COMPACT_DEFINITION;
352
+ expect(
353
+ readPresentation(undefined, {whenOmitted: 'touch', compact}),
354
+ ).toEqual(resolve(undefined, 'touch'));
355
+ expect(
356
+ readPresentation('popover', {whenOmitted: 'touch', compact}),
357
+ ).toEqual(resolve('popover', 'touch'));
358
+ expect(readPresentation('adaptive', {compact})).toEqual(
359
+ resolve('adaptive'),
360
+ );
361
+ expect(
362
+ readPresentation(undefined, {whenOmitted: 'touch', view: null, compact}),
363
+ ).toEqual({
364
+ surface: 'popover',
365
+ touchModes: false,
366
+ });
367
+ });
368
+
369
+ it('reads the compact definition it is given, so a theme that moves md moves the sheet (spec:AST-012 FR9)', () => {
370
+ setDevice({...IPHONE, width: 900, height: 1024});
371
+ expect(
372
+ readPresentation('adaptive', {compact: DEFAULT_COMPACT_DEFINITION})
373
+ .surface,
374
+ ).toBe('popover');
375
+ expect(
376
+ readPresentation('adaptive', {compact: compactDefinition({md: 1000})})
377
+ .surface,
378
+ ).toBe('bottom-sheet');
379
+ });
380
+ });