@astryxdesign/core 0.7.7-canary.bf14580 → 0.7.7-canary.c19ca12

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 (266) hide show
  1. package/dist/AlertDialog/AlertDialog.d.ts +4 -1
  2. package/dist/AlertDialog/AlertDialog.d.ts.map +1 -1
  3. package/dist/AlertDialog/AlertDialog.js +36 -6
  4. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  5. package/dist/BottomSheet/BottomSheet.js +76 -6
  6. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  7. package/dist/BottomSheet/BottomSheetPanel.js +23 -4
  8. package/dist/BottomSheet/bottomSheetStack.d.ts.map +1 -1
  9. package/dist/BottomSheet/bottomSheetStack.js +5 -4
  10. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
  11. package/dist/BottomSheet/useSheetGestures.js +3 -1
  12. package/dist/Button/Button.d.ts.map +1 -1
  13. package/dist/Button/Button.js +47 -3
  14. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  15. package/dist/CheckboxInput/CheckboxInput.js +13 -2
  16. package/dist/ComplexSelector/ComplexSelector.d.ts +4 -1
  17. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  18. package/dist/ComplexSelector/ComplexSelector.js +14 -4
  19. package/dist/ContextMenu/ContextMenu.js +2 -2
  20. package/dist/DateInput/MonthScroller.d.ts.map +1 -1
  21. package/dist/DateInput/MonthScroller.js +13 -27
  22. package/dist/DateInput/Wheel.d.ts.map +1 -1
  23. package/dist/DateInput/Wheel.js +22 -41
  24. package/dist/Dialog/Dialog.d.ts.map +1 -1
  25. package/dist/Dialog/Dialog.js +7 -31
  26. package/dist/Field/Field.js +2 -2
  27. package/dist/HoverCard/HoverCard.d.ts +4 -3
  28. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  29. package/dist/HoverCard/HoverCard.js +27 -2
  30. package/dist/HoverCard/useHoverCard.d.ts +5 -4
  31. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  32. package/dist/HoverCard/useHoverCard.js +17 -31
  33. package/dist/Icon/Icon.d.ts +5 -1
  34. package/dist/Icon/Icon.d.ts.map +1 -1
  35. package/dist/Icon/Icon.js +5 -2
  36. package/dist/Layer/layerAnimations.stylex.d.ts +7 -8
  37. package/dist/Layer/layerAnimations.stylex.d.ts.map +1 -1
  38. package/dist/Layer/layerAnimations.stylex.js +25 -20
  39. package/dist/Layer/usePressPin.d.ts +17 -5
  40. package/dist/Layer/usePressPin.d.ts.map +1 -1
  41. package/dist/Layer/usePressPin.js +62 -12
  42. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  43. package/dist/Lightbox/Lightbox.js +18 -0
  44. package/dist/PowerSearch/PowerSearch.d.ts +36 -3
  45. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  46. package/dist/PowerSearch/PowerSearch.js +522 -269
  47. package/dist/PowerSearch/PowerSearchBottomSheet.d.ts +100 -0
  48. package/dist/PowerSearch/PowerSearchBottomSheet.d.ts.map +1 -0
  49. package/dist/PowerSearch/PowerSearchBottomSheet.js +1913 -0
  50. package/dist/PowerSearch/PowerSearchEditPopover.d.ts +32 -1
  51. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  52. package/dist/PowerSearch/PowerSearchEditPopover.js +17 -6
  53. package/dist/PowerSearch/PowerSearchValueEditor.d.ts +38 -3
  54. package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
  55. package/dist/PowerSearch/PowerSearchValueEditor.js +170 -61
  56. package/dist/PowerSearch/formatFilterValue.d.ts +7 -0
  57. package/dist/PowerSearch/formatFilterValue.d.ts.map +1 -1
  58. package/dist/PowerSearch/formatFilterValue.js +11 -1
  59. package/dist/PowerSearch/formatTokenValue.d.ts +28 -0
  60. package/dist/PowerSearch/formatTokenValue.d.ts.map +1 -0
  61. package/dist/PowerSearch/formatTokenValue.js +160 -0
  62. package/dist/PowerSearch/powerSearchSheetUtils.d.ts +65 -0
  63. package/dist/PowerSearch/powerSearchSheetUtils.d.ts.map +1 -0
  64. package/dist/PowerSearch/powerSearchSheetUtils.js +168 -0
  65. package/dist/RadioList/RadioList.d.ts.map +1 -1
  66. package/dist/RadioList/RadioList.js +3 -2
  67. package/dist/RadioList/RadioListItem.d.ts +1 -1
  68. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  69. package/dist/RadioList/RadioListItem.js +5 -3
  70. package/dist/SegmentedControl/useSegmentedSelectionMotion.d.ts.map +1 -1
  71. package/dist/SegmentedControl/useSegmentedSelectionMotion.js +3 -3
  72. package/dist/Slider/sliderTouchController.js +2 -2
  73. package/dist/Spinner/Spinner.d.ts +1 -1
  74. package/dist/Spinner/Spinner.d.ts.map +1 -1
  75. package/dist/Spinner/Spinner.js +60 -58
  76. package/dist/Switch/useSwitchTouch.js +4 -4
  77. package/dist/Table/BaseTable.d.ts.map +1 -1
  78. package/dist/Table/BaseTable.js +29 -8
  79. package/dist/TextInput/TextInput.d.ts +2 -2
  80. package/dist/TextInput/TextInput.d.ts.map +1 -1
  81. package/dist/TextInput/TextInput.js +10 -7
  82. package/dist/Toast/Toast.d.ts +11 -1
  83. package/dist/Toast/Toast.d.ts.map +1 -1
  84. package/dist/Toast/Toast.js +25 -15
  85. package/dist/Toast/ToastViewport.d.ts.map +1 -1
  86. package/dist/Toast/ToastViewport.js +94 -34
  87. package/dist/Toast/types.d.ts +9 -3
  88. package/dist/Toast/types.d.ts.map +1 -1
  89. package/dist/Token/Token.d.ts +2 -2
  90. package/dist/Token/Token.d.ts.map +1 -1
  91. package/dist/Token/Token.js +12 -4
  92. package/dist/Token/TokenNamingContext.d.ts +14 -0
  93. package/dist/Token/TokenNamingContext.d.ts.map +1 -0
  94. package/dist/Token/TokenNamingContext.js +23 -0
  95. package/dist/Tooltip/useTooltip.d.ts +19 -1
  96. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  97. package/dist/Tooltip/useTooltip.js +34 -53
  98. package/dist/Typeahead/TypeaheadItem.d.ts +2 -1
  99. package/dist/Typeahead/TypeaheadItem.d.ts.map +1 -1
  100. package/dist/animator/createAnimator.d.ts +98 -0
  101. package/dist/animator/createAnimator.d.ts.map +1 -0
  102. package/dist/animator/createAnimator.js +259 -0
  103. package/dist/astryx.css +22 -6
  104. package/dist/gestures/GestureHost.d.ts +27 -1
  105. package/dist/gestures/GestureHost.d.ts.map +1 -1
  106. package/dist/gestures/GestureHost.js +90 -8
  107. package/dist/gestures/PressTracker.d.ts.map +1 -1
  108. package/dist/gestures/PressTracker.js +4 -3
  109. package/dist/hooks/actionGroupTracker.d.ts +4 -1
  110. package/dist/hooks/actionGroupTracker.d.ts.map +1 -1
  111. package/dist/hooks/actionGroupTracker.js +13 -3
  112. package/dist/hooks/index.d.ts +1 -0
  113. package/dist/hooks/index.d.ts.map +1 -1
  114. package/dist/hooks/index.js +1 -0
  115. package/dist/hooks/pressGroupContext.d.ts +6 -0
  116. package/dist/hooks/pressGroupContext.d.ts.map +1 -0
  117. package/dist/hooks/pressGroupContext.js +20 -0
  118. package/dist/hooks/useActionGroupTracking.d.ts +53 -0
  119. package/dist/hooks/useActionGroupTracking.d.ts.map +1 -0
  120. package/dist/hooks/useActionGroupTracking.js +174 -0
  121. package/dist/hooks/useAnimator.d.ts +41 -0
  122. package/dist/hooks/useAnimator.d.ts.map +1 -0
  123. package/dist/hooks/useAnimator.js +70 -0
  124. package/dist/hooks/useAttachment.d.ts +1 -1
  125. package/dist/hooks/useAttachment.js +1 -1
  126. package/dist/hooks/useInputStatusIcon.d.ts +1 -1
  127. package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
  128. package/dist/hooks/useInputStatusIcon.js +2 -2
  129. package/dist/hooks/useLongPress.d.ts +6 -2
  130. package/dist/hooks/useLongPress.d.ts.map +1 -1
  131. package/dist/hooks/useLongPress.js +95 -15
  132. package/dist/hooks/useMenuPress.d.ts.map +1 -1
  133. package/dist/hooks/useMenuPress.js +4 -2
  134. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts +8 -0
  135. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts.map +1 -0
  136. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.js +42 -0
  137. package/dist/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.d.ts.map +1 -1
  138. package/dist/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.js +6 -0
  139. package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.d.ts +8 -0
  140. package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.d.ts.map +1 -0
  141. package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.js +14 -0
  142. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  143. package/dist/i18n/generated-locales/en.generated.js +30 -0
  144. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  145. package/dist/i18n/generated-locales/pseudo.generated.js +30 -0
  146. package/dist/index.d.ts +1 -0
  147. package/dist/index.d.ts.map +1 -1
  148. package/dist/index.js +4 -0
  149. package/dist/internal/indicatorPress.stylex.d.ts +20 -0
  150. package/dist/internal/indicatorPress.stylex.d.ts.map +1 -0
  151. package/dist/internal/indicatorPress.stylex.js +26 -0
  152. package/dist/motion/animator.d.ts +14 -14
  153. package/dist/motion/animator.d.ts.map +1 -1
  154. package/dist/motion/animator.js +41 -18
  155. package/dist/motion/reducedMotion.d.ts +24 -0
  156. package/dist/motion/reducedMotion.d.ts.map +1 -1
  157. package/dist/motion/reducedMotion.js +78 -1
  158. package/dist/motion/uikit/alert.d.ts +41 -0
  159. package/dist/motion/uikit/alert.d.ts.map +1 -0
  160. package/dist/motion/uikit/alert.js +43 -0
  161. package/dist/privateAttributes/pressGroupItem.stylex.d.ts +15 -0
  162. package/dist/privateAttributes/pressGroupItem.stylex.d.ts.map +1 -0
  163. package/dist/privateAttributes/pressGroupItem.stylex.js +23 -0
  164. package/dist/utils/clickGuard.d.ts +8 -1
  165. package/dist/utils/clickGuard.d.ts.map +1 -1
  166. package/dist/utils/clickGuard.js +24 -1
  167. package/dist/utils/dateParser.d.ts.map +1 -1
  168. package/dist/utils/dateParser.js +12 -0
  169. package/dist/utils/focusReturn.d.ts +15 -0
  170. package/dist/utils/focusReturn.d.ts.map +1 -1
  171. package/dist/utils/focusReturn.js +28 -0
  172. package/dist/utils/interactionOverlay.stylex.d.ts +3 -2
  173. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
  174. package/dist/utils/interactionOverlay.stylex.js +3 -2
  175. package/dist/utils/pressFeedback.d.ts.map +1 -1
  176. package/dist/utils/pressFeedback.js +19 -6
  177. package/dist/utils/triggerFocus.d.ts +36 -0
  178. package/dist/utils/triggerFocus.d.ts.map +1 -0
  179. package/dist/utils/triggerFocus.js +65 -0
  180. package/locales/en.json +120 -0
  181. package/locales/pseudo.json +90 -0
  182. package/package.json +9 -4
  183. package/src/AlertDialog/AlertDialog.doc.mjs +2 -2
  184. package/src/AlertDialog/AlertDialog.tsx +37 -5
  185. package/src/BottomSheet/BottomSheet.tsx +85 -5
  186. package/src/BottomSheet/BottomSheetPanel.tsx +40 -5
  187. package/src/BottomSheet/bottomSheetStack.ts +8 -4
  188. package/src/BottomSheet/useSheetGestures.ts +3 -1
  189. package/src/Button/Button.tsx +63 -5
  190. package/src/CheckboxInput/CheckboxInput.doc.mjs +18 -0
  191. package/src/CheckboxInput/CheckboxInput.tsx +36 -9
  192. package/src/ComplexSelector/ComplexSelector.tsx +20 -5
  193. package/src/ContextMenu/ContextMenu.tsx +2 -2
  194. package/src/DateInput/MonthScroller.tsx +18 -22
  195. package/src/DateInput/Wheel.tsx +30 -32
  196. package/src/Dialog/Dialog.tsx +10 -31
  197. package/src/Field/Field.tsx +2 -2
  198. package/src/HoverCard/HoverCard.doc.mjs +4 -2
  199. package/src/HoverCard/HoverCard.tsx +35 -4
  200. package/src/HoverCard/useHoverCard.doc.mjs +178 -36
  201. package/src/HoverCard/useHoverCard.tsx +46 -52
  202. package/src/Icon/Icon.doc.mjs +49 -8
  203. package/src/Icon/Icon.tsx +15 -7
  204. package/src/Layer/layerAnimations.stylex.ts +24 -16
  205. package/src/Layer/usePressPin.ts +81 -15
  206. package/src/Lightbox/Lightbox.tsx +18 -0
  207. package/src/PowerSearch/PowerSearch.doc.mjs +54 -5
  208. package/src/PowerSearch/PowerSearch.tsx +734 -288
  209. package/src/PowerSearch/PowerSearchBottomSheet.tsx +2515 -0
  210. package/src/PowerSearch/PowerSearchEditPopover.tsx +28 -6
  211. package/src/PowerSearch/PowerSearchValueEditor.tsx +203 -38
  212. package/src/PowerSearch/formatFilterValue.ts +18 -1
  213. package/src/PowerSearch/formatTokenValue.ts +215 -0
  214. package/src/PowerSearch/powerSearchSheetUtils.ts +224 -0
  215. package/src/RadioList/RadioList.tsx +3 -2
  216. package/src/RadioList/RadioListItem.tsx +5 -3
  217. package/src/SegmentedControl/useSegmentedSelectionMotion.ts +6 -3
  218. package/src/Slider/sliderTouchController.ts +2 -2
  219. package/src/Spinner/Spinner.tsx +77 -63
  220. package/src/Switch/useSwitchTouch.ts +4 -4
  221. package/src/Table/BaseTable.tsx +46 -12
  222. package/src/TextInput/TextInput.tsx +11 -10
  223. package/src/Toast/Toast.doc.mjs +4 -3
  224. package/src/Toast/Toast.tsx +52 -32
  225. package/src/Toast/ToastViewport.tsx +121 -35
  226. package/src/Toast/types.ts +9 -3
  227. package/src/Token/Token.tsx +13 -4
  228. package/src/Token/TokenNamingContext.ts +28 -0
  229. package/src/Tooltip/useTooltip.doc.mjs +24 -0
  230. package/src/Tooltip/useTooltip.tsx +61 -56
  231. package/src/Typeahead/TypeaheadItem.doc.mjs +6 -3
  232. package/src/Typeahead/TypeaheadItem.tsx +2 -1
  233. package/src/animator/createAnimator.doc.mjs +153 -0
  234. package/src/animator/createAnimator.ts +324 -0
  235. package/src/gestures/GestureHost.ts +105 -10
  236. package/src/gestures/PressTracker.ts +4 -3
  237. package/src/gestures/README.md +7 -1
  238. package/src/hooks/actionGroupTracker.ts +20 -5
  239. package/src/hooks/index.ts +2 -0
  240. package/src/hooks/pressGroupContext.ts +21 -0
  241. package/src/hooks/useActionGroupTracking.ts +224 -0
  242. package/src/hooks/useAnimator.doc.mjs +109 -0
  243. package/src/hooks/useAnimator.ts +67 -0
  244. package/src/hooks/useAttachment.ts +1 -1
  245. package/src/hooks/useInputStatusIcon.tsx +3 -2
  246. package/src/hooks/useLongPress.doc.mjs +2 -2
  247. package/src/hooks/useLongPress.ts +136 -15
  248. package/src/hooks/useMenuPress.ts +8 -2
  249. package/src/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.ts +43 -0
  250. package/src/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.ts +6 -0
  251. package/src/i18n/generated-english/PowerSearch/formatTokenValue.generated.ts +15 -0
  252. package/src/i18n/generated-locales/en.generated.ts +41 -0
  253. package/src/i18n/generated-locales/pseudo.generated.ts +42 -0
  254. package/src/index.ts +13 -0
  255. package/src/internal/indicatorPress.stylex.ts +24 -0
  256. package/src/motion/README.md +8 -5
  257. package/src/motion/animator.ts +61 -29
  258. package/src/motion/reducedMotion.ts +79 -1
  259. package/src/motion/uikit/alert.ts +43 -0
  260. package/src/privateAttributes/pressGroupItem.stylex.ts +23 -0
  261. package/src/utils/clickGuard.ts +37 -1
  262. package/src/utils/dateParser.ts +16 -0
  263. package/src/utils/focusReturn.ts +30 -0
  264. package/src/utils/interactionOverlay.stylex.ts +3 -2
  265. package/src/utils/pressFeedback.ts +19 -6
  266. package/src/utils/triggerFocus.ts +80 -0
@@ -731,6 +731,12 @@ export class GestureHost {
731
731
  * lift click (`spec:AST-094` FR28: nothing acts).
732
732
  */
733
733
  private readonly cancelledFingers = new Set<number>();
734
+ /**
735
+ * The pens and pointer-read fingers that the app's lost focus cancelled,
736
+ * by pointer id. Each is kept until its lift, which must not click either,
737
+ * or until a new press.
738
+ */
739
+ private readonly cancelledPointers = new Map<number, PointerKind>();
734
740
  /** Touch.identifier → the element the finger landed on. */
735
741
  private readonly touchTargets = new Map<number, EventTarget>();
736
742
  /**
@@ -755,6 +761,8 @@ export class GestureHost {
755
761
  private readonly processed = new WeakSet<Event>();
756
762
  /** pointerId → arena id, for live mouse and pen contacts. */
757
763
  private readonly pointerIds = new Map<number, number>();
764
+ /** Each contact's kind: a blur refuses a pen's or a pointer-read finger's lift, never a mouse's. */
765
+ private readonly pointerKinds = new Map<number, PointerKind>();
758
766
  /**
759
767
  * The boxes the host watches for screen-edge pans (syncLayoutWatch): each
760
768
  * element with an enabled one, and the root for one whose band moves with
@@ -1820,13 +1828,36 @@ export class GestureHost {
1820
1828
  }
1821
1829
  }
1822
1830
 
1823
- /** After a contact ends: an armed press that is over stops listening. */
1831
+ /**
1832
+ * A new press is a new gesture, as the click guard reads one, so the lifts
1833
+ * the blur cancelled owe nothing after it. A second finger keeps the other
1834
+ * fingers' refusals, since those fingers may still be down.
1835
+ */
1836
+ private dropCancelledLifts(event: PointerEvent): void {
1837
+ const isSecond = event.pointerType === 'touch' && event.isPrimary === false;
1838
+ for (const [pointerId, kind] of [...this.cancelledPointers]) {
1839
+ if (pointerId === event.pointerId || !(isSecond && kind === 'touch')) {
1840
+ this.cancelledPointers.delete(pointerId);
1841
+ }
1842
+ }
1843
+ }
1844
+
1845
+ /**
1846
+ * After a contact ends: an armed press that is over stops listening. A
1847
+ * contact the lost focus cancelled still owes its lift, so the press is
1848
+ * not over until that lift comes. A lift the page never hears ends it at
1849
+ * the next press, or at a cancel.
1850
+ */
1824
1851
  private releaseArmedPress(): void {
1825
1852
  if (!this.isArmedPress) {
1826
1853
  return;
1827
1854
  }
1828
1855
  queueMicrotask(() => {
1829
- if (this.isArmedPress && this.arena.touches.length === 0) {
1856
+ if (
1857
+ this.isArmedPress &&
1858
+ this.arena.touches.length === 0 &&
1859
+ this.cancelledPointers.size === 0
1860
+ ) {
1830
1861
  this.isArmedPress = false;
1831
1862
  this.syncKeptPresses();
1832
1863
  this.syncListeners();
@@ -1857,6 +1888,7 @@ export class GestureHost {
1857
1888
  return;
1858
1889
  }
1859
1890
  this.cancelledFingers.clear();
1891
+ this.cancelledPointers.clear();
1860
1892
  this.clearClaimedLifts();
1861
1893
  this.cancelCallerRead?.();
1862
1894
  this.cancelCallerRead = null;
@@ -2934,13 +2966,19 @@ export class GestureHost {
2934
2966
  if (locksSelectionFor('touch')) {
2935
2967
  toLock.push({id, target, consumerElement});
2936
2968
  }
2937
- // The finger's events keep going to what the engine hit.
2969
+ // The finger's events keep going to what the engine hit. A disabled
2970
+ // participant can never claim, so a scroll never waits on it.
2938
2971
  this.watchTarget(
2939
2972
  touch.identifier,
2940
2973
  native,
2941
- recognizers.some(claimsWhileMoving) || consumer?.keepsTouch != null,
2942
- consumer != null ||
2943
- recognizers.some(recognizer => recognizer.claimsTouches),
2974
+ recognizers.some(
2975
+ recognizer => recognizer.isEnabled && claimsWhileMoving(recognizer),
2976
+ ) ||
2977
+ (consumer?.keepsTouch != null && consumer.isEnabled?.() !== false),
2978
+ (consumer != null && consumer.isEnabled?.() !== false) ||
2979
+ recognizers.some(
2980
+ recognizer => recognizer.isEnabled && recognizer.claimsTouches,
2981
+ ),
2944
2982
  );
2945
2983
  this.lastFed.set(id, {x: touch.clientX, y: touch.clientY, time});
2946
2984
  this.pairTouch(id, {x: touch.clientX, y: touch.clientY, target: native});
@@ -2975,6 +3013,11 @@ export class GestureHost {
2975
3013
  for (const input of untracked) {
2976
3014
  this.arena.untrackedBegan(input);
2977
3015
  }
3016
+ // Again once the new fingers are delivered: a pinch's second finger may
3017
+ // have landed on another one-finger pan, the next row.
3018
+ if (this.fingerTargets.length > 1 && !this.isFingerClaimed()) {
3019
+ this.failUnclaimedLoneFingerPans();
3020
+ }
2978
3021
  // Stamped after the consumers judged the touch: the brake asks whether
2979
3022
  // an EARLIER finger is still driving a scroll.
2980
3023
  const landed = landedOn(event);
@@ -3570,6 +3613,7 @@ export class GestureHost {
3570
3613
  if (!isPointerKind(event.pointerType)) {
3571
3614
  return;
3572
3615
  }
3616
+ this.dropCancelledLifts(event);
3573
3617
  if (event.pointerType === 'touch' && this.usesTouchEvents) {
3574
3618
  this.pairPointer(event.pointerId, {
3575
3619
  x: event.clientX,
@@ -3578,6 +3622,13 @@ export class GestureHost {
3578
3622
  });
3579
3623
  return;
3580
3624
  }
3625
+ if (event.pointerType === 'touch' && event.isPrimary === false) {
3626
+ // Fingers from pointer events: a second one, wherever either lands,
3627
+ // leaves the two to the page's pinch, as onTouchStart does. A finger
3628
+ // that is not the primary pointer is a second finger, even where the
3629
+ // host never heard the first.
3630
+ this.failUnclaimedLoneFingerPans();
3631
+ }
3581
3632
  // A right click, a pen's barrel button, or a ctrl-click on macOS (its
3582
3633
  // context click) starts no press (FR43).
3583
3634
  if (event.pointerType !== 'touch' && !isPrimaryClick(event)) {
@@ -3599,6 +3650,7 @@ export class GestureHost {
3599
3650
  }
3600
3651
  const id = this.nextId++;
3601
3652
  this.pointerIds.set(event.pointerId, id);
3653
+ this.pointerKinds.set(event.pointerId, event.pointerType);
3602
3654
  if (target !== landed && target != null) {
3603
3655
  this.retargeted.set(id, {x: event.clientX, y: event.clientY, target});
3604
3656
  }
@@ -3620,6 +3672,15 @@ export class GestureHost {
3620
3672
  consumer,
3621
3673
  },
3622
3674
  ]);
3675
+ if (
3676
+ event.pointerType === 'touch' &&
3677
+ event.isPrimary === false &&
3678
+ !this.isFingerClaimed()
3679
+ ) {
3680
+ // Again once this finger is delivered: it may have landed on another
3681
+ // one-finger pan.
3682
+ this.failUnclaimedLoneFingerPans();
3683
+ }
3623
3684
  if (locksSelectionFor(event.pointerType)) {
3624
3685
  // The presented target: what the pen is on, where the engine's own
3625
3686
  // hit test may still see the motion's start.
@@ -3678,9 +3739,14 @@ export class GestureHost {
3678
3739
  }
3679
3740
  const id = this.pointerIds.get(event.pointerId);
3680
3741
  if (id == null) {
3742
+ // A contact the lost focus cancelled: its lift clicks nothing.
3743
+ if (this.cancelledPointers.delete(event.pointerId)) {
3744
+ this.refuseCancelledLift(event);
3745
+ }
3681
3746
  return;
3682
3747
  }
3683
3748
  this.pointerIds.delete(event.pointerId);
3749
+ this.pointerKinds.delete(event.pointerId);
3684
3750
  this.unlockSelection(id);
3685
3751
  const dispositions = this.arena.ended([
3686
3752
  {id, x: event.clientX, y: event.clientY, time: eventTime(event)},
@@ -3784,9 +3850,12 @@ export class GestureHost {
3784
3850
  }
3785
3851
  return;
3786
3852
  }
3853
+ // A contact that ends in a cancel owes no refused lift.
3854
+ this.cancelledPointers.delete(event.pointerId);
3787
3855
  const id = this.pointerIds.get(event.pointerId);
3788
3856
  if (id != null) {
3789
3857
  this.pointerIds.delete(event.pointerId);
3858
+ this.pointerKinds.delete(event.pointerId);
3790
3859
  this.unlockSelection(id, true);
3791
3860
  this.retargeted.delete(id);
3792
3861
  this.arena.cancelled([id], eventTime(event));
@@ -3991,20 +4060,26 @@ export class GestureHost {
3991
4060
  }
3992
4061
  }
3993
4062
  }
4063
+ /** Whether a finger's drag is claimed: then two fingers make no pinch. */
4064
+ private isFingerClaimed(): boolean {
4065
+ return this.arena.touches.some(
4066
+ touch => touch.pointerType === 'touch' && this.arena.isClaimed(touch.id),
4067
+ );
4068
+ }
3994
4069
 
3995
4070
  private readonly onVisibilityChange = (): void => {
3996
4071
  if (this.doc.visibilityState === 'hidden') {
3997
4072
  this.cancelEverything(this.arena.now());
3998
- // As at a blur: the press sends no further pointer event, so an armed
3999
- // press's page listeners go now (FR24).
4073
+ // As at a blur: an armed press's page listeners go once nothing is left
4074
+ // to hear.
4000
4075
  this.releaseArmedPress();
4001
4076
  }
4002
4077
  };
4003
4078
 
4004
4079
  private readonly onBlur = (): void => {
4005
4080
  this.cancelEverything(this.arena.now());
4006
- // The press a blur ends sends no further pointer event: an armed press's
4007
- // page listeners go now, as at its lift (FR24).
4081
+ // An armed press's page listeners go once nothing is left to hear. A pen
4082
+ // or a pointer-read finger the blur cancelled still owes its lift.
4008
4083
  this.releaseArmedPress();
4009
4084
  };
4010
4085
 
@@ -4027,8 +4102,27 @@ export class GestureHost {
4027
4102
  );
4028
4103
  }
4029
4104
 
4105
+ /**
4106
+ * The lift of a contact the lost focus cancelled: its click is refused,
4107
+ * and a finger's touchend after it is cancelled.
4108
+ */
4109
+ private refuseCancelledLift(event: PointerEvent): void {
4110
+ armClickGuard(this.doc, {takesLateMouse: event.pointerType === 'touch'});
4111
+ if (event.pointerType === 'touch') {
4112
+ // Noted as a claimed lift is: capped, and gone after its time.
4113
+ this.noteClaimedLift(event);
4114
+ }
4115
+ }
4116
+
4030
4117
  /** The app lost focus: UIKit cancels every touch in flight. */
4031
4118
  private cancelEverything(time: number): void {
4119
+ // The browser may keep a pen or a pointer-read finger down: its lift must
4120
+ // not click. A mouse button held through the blur keeps its click.
4121
+ for (const [pointerId, kind] of this.pointerKinds) {
4122
+ if (kind !== 'mouse') {
4123
+ this.cancelledPointers.set(pointerId, kind);
4124
+ }
4125
+ }
4032
4126
  const ids = [...this.pointerIds.values()];
4033
4127
  for (const id of ids) {
4034
4128
  this.unlockSelection(id, true);
@@ -4042,6 +4136,7 @@ export class GestureHost {
4042
4136
  }
4043
4137
  }
4044
4138
  this.pointerIds.clear();
4139
+ this.pointerKinds.clear();
4045
4140
  this.retargeted.clear();
4046
4141
  this.fingerPointers.clear();
4047
4142
  this.pendingPointers.clear();
@@ -47,7 +47,8 @@
47
47
  */
48
48
 
49
49
  import {UIKIT_CONTROL, UIKIT_PAN, UIKIT_PRESS} from '../motion/uikit/gestures';
50
- import {timing} from '../motion/uikit/timing';
50
+ // Importing the whole table bundles its curves into each press-tracking control.
51
+ import {press as pressClocks} from '../motion/uikit/timing';
51
52
  import {travel} from './geometry';
52
53
  import type {GestureRecognizer, SelectionLock} from './GestureRecognizer';
53
54
  import type {TapGestureRecognizer} from './TapGestureRecognizer';
@@ -154,7 +155,7 @@ export const PRESS_PROFILES = {
154
155
  // The shared press styles release every surface on the deselect clock
155
156
  // until a control runs its own release (UIButton's 250 ms), so a
156
157
  // painter's `fading` never outlasts the fade it shows.
157
- releaseMs: timing.press.highlightOutMs,
158
+ releaseMs: pressClocks.highlightOutMs,
158
159
  },
159
160
  /**
160
161
  * A table or collection cell: inside a scroll view it highlights after the
@@ -165,7 +166,7 @@ export const PRESS_PROFILES = {
165
166
  highlightDelay: 'inScrollers',
166
167
  cancelTravel: UIKIT_PAN.hysteresis.value,
167
168
  quickTapFlash: 0,
168
- releaseMs: timing.press.highlightOutMs,
169
+ releaseMs: pressClocks.highlightOutMs,
169
170
  },
170
171
  } as const satisfies Record<string, PressProfile>;
171
172
 
@@ -96,6 +96,10 @@ Once a drag has claimed its finger, a second finger never breaks it.
96
96
  | `PinchGestureRecognizer` | `UIPinchGestureRecognizer` | continuous | `scale` (settable, to re-base it), `velocity` in scale per second, `focalPoint()` |
97
97
  | `ScreenEdgePanGestureRecognizer` | `UIScreenEdgePanGestureRecognizer` | continuous | `edges`: `leading` / `trailing`, resolved against the view's writing direction; attached to an edge zone the view lies within; fails as soon as the touch lands outside every requested edge band; a finger's only |
98
98
 
99
+ Each recognizer reports what it is in `kind` (`'tap'`, `'longPress'`,
100
+ `'pan'`, `'screenEdgePan'`, `'swipe'`, `'pinch'`): code tells the family apart by it, never by `instanceof`
101
+ (`architecture:react-component-runtime` INV13).
102
+
99
103
  A recognizer created with `claimsTouches: false` is a watcher (a tab bar
100
104
  that minimizes while the page scrolls): it never claims, so its element gets
101
105
  only passive listeners and a scroll never waits for it; it prevents no other
@@ -174,7 +178,9 @@ innerPan.requireToFail(edgePan);
174
178
  ## Touches in the view: `createPressTracker`
175
179
 
176
180
  A pressable control's view-level touch handling, with `UIControl`
177
- semantics:
181
+ semantics, built by `createPressTracker(options)`: a factory, as
182
+ `architecture:react-component-runtime` INV13 asks of the touch modules outside
183
+ the recognizer family (`VelocityTracker` is still a class, its one listed gap):
178
184
 
179
185
  - `touchDown`, `touchDragInside` / `touchDragOutside`, `touchDragEnter` /
180
186
  `touchDragExit`, `touchUpInside` / `touchUpOutside`, `touchCancel`.
@@ -57,7 +57,8 @@ export interface ActionGroupTrackerOptions {
57
57
  * target past the group's ends commits the lift there wherever the finger
58
58
  * lifts.
59
59
  * @default the target whose rect, grown by `highlightExpansion`, holds the
60
- * point; none between or outside them
60
+ * point, the one the press landed on where several do; none between or
61
+ * outside them
61
62
  */
62
63
  resolveTarget?: (
63
64
  x: number,
@@ -83,8 +84,13 @@ export interface ActionGroupTracker {
83
84
  /**
84
85
  * A press began at viewport (x, y): read the actions' rects once and
85
86
  * highlight the one under it. An inert press is taken but tracks nothing.
87
+ * `landed` is the node the press landed on.
86
88
  */
87
- begin(x: number, y: number, options?: {inert?: boolean}): void;
89
+ begin(
90
+ x: number,
91
+ y: number,
92
+ options?: {inert?: boolean; landed?: Node | null},
93
+ ): void;
88
94
  /** The finger moved to viewport (x, y). */
89
95
  move(x: number, y: number): void;
90
96
  /**
@@ -150,6 +156,7 @@ export function createActionGroupTracker(
150
156
  let actions: TrackedAction[] = [];
151
157
  let highlighted: HTMLElement | null = null;
152
158
  let beganOn: HTMLElement | null = null;
159
+ let landedOn: Node | null = null;
153
160
  let isTracking = false;
154
161
  let isInert = false;
155
162
  // The finger's last position: `remeasure` resolves it again.
@@ -241,6 +248,9 @@ export function createActionGroupTracker(
241
248
  if (resolveTarget != null) {
242
249
  return actions.length === 0 ? null : resolveTarget(x, y, actions);
243
250
  }
251
+ // Rects that share an edge, or that have no layout (a test renderer), can
252
+ // both hold the point: the one the press landed on wins.
253
+ let first: HTMLElement | null = null;
244
254
  for (const action of actions) {
245
255
  if (
246
256
  x >= action.left - margin &&
@@ -248,22 +258,26 @@ export function createActionGroupTracker(
248
258
  y >= action.top - margin &&
249
259
  y <= action.bottom + margin
250
260
  ) {
251
- return action.element;
261
+ if (landedOn != null && action.element.contains(landedOn)) {
262
+ return action.element;
263
+ }
264
+ first ??= action.element;
252
265
  }
253
266
  }
254
- return null;
267
+ return first;
255
268
  };
256
269
 
257
270
  const reset = () => {
258
271
  actions = [];
259
272
  highlighted = null;
260
273
  beganOn = null;
274
+ landedOn = null;
261
275
  isTracking = false;
262
276
  isInert = false;
263
277
  };
264
278
 
265
279
  return {
266
- begin(x, y, {inert = false} = {}) {
280
+ begin(x, y, {inert = false, landed = null} = {}) {
267
281
  // A press can begin while another is tracked, when a second recognizer
268
282
  // takes over the finger (a trigger's held press, then the menu's): the
269
283
  // action the first one lit loses its paint.
@@ -276,6 +290,7 @@ export function createActionGroupTracker(
276
290
  highlighted = null;
277
291
  isTracking = true;
278
292
  isInert = inert;
293
+ landedOn = landed;
279
294
  lastX = x;
280
295
  lastY = y;
281
296
  getOptions().onMove?.(x, y);
@@ -133,6 +133,8 @@ export type {
133
133
  UseInteractiveRoleOptions,
134
134
  } from './useInteractiveRole';
135
135
 
136
+ export {useAnimator} from './useAnimator';
137
+
136
138
  export {useLongPress} from './useLongPress';
137
139
  export type {UseLongPressOptions, UseLongPressHandlers} from './useLongPress';
138
140
 
@@ -0,0 +1,21 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file pressGroupContext.ts
7
+ * @input Nothing
8
+ * @output PressGroupContext: true inside a group whose own tracker paints
9
+ * its actions' press
10
+ * @position Internal; not exported from the hooks entry. AlertDialog
11
+ * provides it around its tracked actions, and Button reads it.
12
+ */
13
+
14
+ import {createContext} from 'react';
15
+
16
+ /**
17
+ * True inside a group whose tracker paints a pointer's press on its actions.
18
+ * Those actions take no `:active` paint, so one press never lights two.
19
+ */
20
+ export const PressGroupContext = createContext(false);
21
+ PressGroupContext.displayName = 'PressGroupContext';
@@ -0,0 +1,224 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file useActionGroupTracking.ts
7
+ * @input A ref to a group of sibling actions, the actions' selector, and the
8
+ * component's tracking constants
9
+ * @output Exports useActionGroupTracking and ActionGroupTrackingOptions
10
+ * @position Internal hook, not exported from the hooks entry. A long press
11
+ * drives the shared action-group tracker (hooks/actionGroupTracker.ts) for
12
+ * a group that a held pointer slides across. AlertDialog consumes it.
13
+ *
14
+ * SYNC: When modified, update useActionGroupTracking.test.tsx
15
+ */
16
+
17
+ import {useEffect, useRef, useState, type RefObject} from 'react';
18
+ import {isActive} from '../gestures/GestureRecognizer';
19
+ import {LongPressGestureRecognizer} from '../gestures/LongPressGestureRecognizer';
20
+ import {lockSelection} from '../utils/selectionLock';
21
+ import type {GestureTouch, PointerKind} from '../gestures/types';
22
+ import {useGestureRecognizer} from '../gestures/useGestureRecognizer';
23
+ import {UIKIT_PRESS} from '../motion/uikit/gestures';
24
+ import {isIosWebKit} from '../utils/touchQueries';
25
+ import {createActionGroupTracker} from './actionGroupTracker';
26
+ import {useIsomorphicLayoutEffect} from './useIsomorphicLayoutEffect';
27
+
28
+ export interface ActionGroupTrackingOptions {
29
+ /** Selects the actions inside the tracked element. */
30
+ itemSelector: string;
31
+ /** How long after touch-down the touched action highlights, ms. */
32
+ highlightOnsetMs: number;
33
+ /**
34
+ * How far outside an action a finger keeps it highlighted, px. A mouse or a
35
+ * pen hit-tests the actions' exact rects.
36
+ */
37
+ highlightExpansion: number;
38
+ /** Pointers that are tracked; others keep the browser's click. @default every pointer */
39
+ pointerTypes?: ReadonlyArray<PointerKind>;
40
+ /** A press whose target this rejects is not tracked. */
41
+ shouldTrack?: (target: unknown) => boolean;
42
+ /** Off, nothing is attached and nothing is tracked. @default true */
43
+ isEnabled?: boolean;
44
+ /**
45
+ * Let the gesture host write the group's `touch-action` and refuse a text
46
+ * selection while a finger or a pen holds it.
47
+ * @default true
48
+ */
49
+ manageStyle?: boolean;
50
+ /** Runs the action under the finger at the lift. @default its own click */
51
+ onActivate?: (item: HTMLElement) => void;
52
+ }
53
+
54
+ const PRESS = UIKIT_PRESS;
55
+
56
+ /** The ref handed to the recognizer while tracking is off: no element. */
57
+ const DETACHED: RefObject<Element | null> = {current: null};
58
+
59
+ const EVERY_POINTER: ReadonlyArray<PointerKind> = ['touch', 'pen', 'mouse'];
60
+
61
+ /**
62
+ * The group's press: a long press that a second finger on the group ends with
63
+ * nothing acting. A plain long press ignores a second finger.
64
+ */
65
+ class ActionGroupPress extends LongPressGestureRecognizer {
66
+ protected override acceptsTouch(touch: GestureTouch): boolean {
67
+ if (isActive(this.state)) {
68
+ this.setState('cancelled');
69
+ return false;
70
+ }
71
+ return super.acceptsTouch(touch);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Track a held pointer on the group behind `ref` across the actions inside
77
+ * it, through the shared action-group tracker.
78
+ */
79
+ export function useActionGroupTracking(
80
+ ref: RefObject<HTMLElement | null>,
81
+ options: ActionGroupTrackingOptions,
82
+ ): ActionGroupPress {
83
+ const optionsRef = useRef(options);
84
+ // Keeps the recognizer's delegate and the tracker, which run outside
85
+ // render, reading the latest options.
86
+ useIsomorphicLayoutEffect(() => {
87
+ optionsRef.current = options;
88
+ });
89
+ // The pointer that started the current press. Only a finger gets the hit
90
+ // expansion; a mouse or a pen hit-tests the actions' exact rects.
91
+ const pointerRef = useRef<PointerKind>('touch');
92
+ const landedRef = useRef<Node | null>(null);
93
+ const [tracker] = useState(() =>
94
+ createActionGroupTracker(() => ({
95
+ roots: () => [ref.current],
96
+ itemSelector: optionsRef.current.itemSelector,
97
+ highlightExpansion:
98
+ pointerRef.current === 'touch'
99
+ ? optionsRef.current.highlightExpansion
100
+ : 0,
101
+ onActivate: optionsRef.current.onActivate,
102
+ // The browser's own click for a press the group ran is taken, so the
103
+ // action runs once. The gesture host swallows it for a press it claims.
104
+ // This covers one it does not claim, a mouse under a coarse pointer.
105
+ swallowsNativeClick: true,
106
+ })),
107
+ );
108
+ const isEnabled = options.isEnabled ?? true;
109
+ // Synchronizes the tracker with tracking on and mounted. Turning tracking
110
+ // off, or unmounting, drops a tracked press and ends its release fades.
111
+ // The host reports no cancellation to a disabled recognizer, and the lift
112
+ // that runs an action often closes the owner before its fade ends.
113
+ useEffect(
114
+ () => () => {
115
+ tracker.cancel();
116
+ tracker.dispose();
117
+ },
118
+ [tracker, isEnabled],
119
+ );
120
+
121
+ return useGestureRecognizer(
122
+ isEnabled ? ref : DETACHED,
123
+ () =>
124
+ new ActionGroupPress({
125
+ name: 'ActionGroupTracking',
126
+ minimumPressDuration: options.highlightOnsetMs,
127
+ allowableMovement: Number.POSITIVE_INFINITY,
128
+ allowedPointerTypes: options.pointerTypes ?? EVERY_POINTER,
129
+ delaysTouchesBegan: true,
130
+ // The hold picks an action, so a held finger or pen starts no text
131
+ // selection and opens no callout while down. A long press brings this
132
+ // lock by default; this hold has no minimum duration, so it names it.
133
+ selectionLock: lockSelection,
134
+ shouldReceiveTouch: (recognizer, touch) => {
135
+ if (recognizer.numberOfTouches === 0) {
136
+ pointerRef.current = touch.pointerType;
137
+ const target = touch.target as Node | null;
138
+ landedRef.current =
139
+ typeof target?.nodeType === 'number' ? target : null;
140
+ }
141
+ return receiveTouch(recognizer, touch, optionsRef.current);
142
+ },
143
+ }),
144
+ recognizer => {
145
+ switch (recognizer.state) {
146
+ case 'began': {
147
+ const {x, y} = recognizer.location();
148
+ tracker.begin(x, y, {landed: landedRef.current});
149
+ break;
150
+ }
151
+ case 'changed': {
152
+ const {x, y} = recognizer.location();
153
+ tracker.move(x, y);
154
+ break;
155
+ }
156
+ case 'ended': {
157
+ const doc = ref.current?.ownerDocument;
158
+ const focusedBefore = doc?.activeElement ?? null;
159
+ const beganOn = tracker.beganOn;
160
+ // Whether a click on the lifted action would focus it. A mouse's or
161
+ // a pen's pointerdown already gave the engine's answer, on the action
162
+ // it began on. A tap focuses on a later mousedown, which the claimed
163
+ // touch cancels, so the platform answers for a finger.
164
+ // iOS WebKit focuses no tapped button; Chromium and Firefox do.
165
+ const clickFocuses =
166
+ pointerRef.current === 'touch'
167
+ ? !isIosWebKit(doc?.defaultView)
168
+ : beganOn != null && focusedBefore === beganOn;
169
+ const lifted = tracker.end();
170
+ // The lift runs the action's own click(), which moves no focus. So
171
+ // focus moves to the lifted action, unless its click moved focus.
172
+ if (
173
+ clickFocuses &&
174
+ lifted != null &&
175
+ lifted.isConnected &&
176
+ doc?.activeElement === focusedBefore &&
177
+ focusedBefore !== lifted
178
+ ) {
179
+ lifted.focus({preventScroll: true, focusVisible: false});
180
+ }
181
+ break;
182
+ }
183
+ case 'cancelled':
184
+ tracker.cancel();
185
+ break;
186
+ case 'possible':
187
+ case 'failed':
188
+ // Listeners are not called for these.
189
+ break;
190
+ }
191
+ },
192
+ {
193
+ isEnabled,
194
+ manageStyle: options.manageStyle ?? true,
195
+ actionGroup: true,
196
+ },
197
+ );
198
+ }
199
+
200
+ /**
201
+ * The recognizer's should-receive delegate. It also picks the press's clock.
202
+ * Inside a scroller the press waits the content-touch delay and fails past
203
+ * the slop, so the scroller takes a flick. Elsewhere it begins at once, with
204
+ * no movement limit.
205
+ */
206
+ function receiveTouch(
207
+ recognizer: LongPressGestureRecognizer,
208
+ touch: GestureTouch,
209
+ options: ActionGroupTrackingOptions,
210
+ ): boolean {
211
+ if (options.shouldTrack?.(touch.target) === false) {
212
+ return false;
213
+ }
214
+ if (recognizer.numberOfTouches === 0) {
215
+ const isInsideScroller = touch.scroll?.isInsideScroller === true;
216
+ recognizer.minimumPressDuration = isInsideScroller
217
+ ? PRESS.contentTouchDelay.value
218
+ : options.highlightOnsetMs;
219
+ recognizer.allowableMovement = isInsideScroller
220
+ ? PRESS.slop.value
221
+ : Number.POSITIVE_INFINITY;
222
+ }
223
+ return true;
224
+ }