@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
@@ -38,6 +38,7 @@ import {
38
38
  useRef,
39
39
  useState,
40
40
  } from 'react';
41
+ import {flushSync} from 'react-dom';
41
42
  import * as stylex from '@stylexjs/stylex';
42
43
  import {useCalendarDays} from '../Calendar';
43
44
  import {useDirection, useLocale} from '../i18n';
@@ -62,7 +63,11 @@ import {
62
63
  plainDateToISO,
63
64
  DATE_FORMAT_WITH_WEEKDAY,
64
65
  } from '../utils';
65
- import {reducedMotionFade} from '../motion/reducedMotion';
66
+ import {
67
+ playReducedMotionDip,
68
+ reducedMotionFade,
69
+ type ReducedMotionDip,
70
+ } from '../motion/reducedMotion';
66
71
  import {dateInputTouchSizes, dateInputTouchGeometry} from './tokens.stylex';
67
72
  import {useOwnScrollGesture} from './useOwnScrollGesture';
68
73
  import {useScrollSettle} from './useScrollSettle';
@@ -434,17 +439,12 @@ export function MonthScroller({
434
439
 
435
440
  // The Reduced Motion dip of a step on a touch screen, owned here so a
436
441
  // second step or the unmount clears it.
437
- const dipRef = useRef<{animation: Animation; timer: number} | null>(null);
442
+ const dipRef = useRef<ReducedMotionDip | null>(null);
438
443
  const stopDip = useCallback(() => {
439
- const dip = dipRef.current;
440
- if (dip != null) {
441
- window.clearTimeout(dip.timer);
442
- dip.animation.cancel();
443
- dipRef.current = null;
444
- }
444
+ dipRef.current?.cancel();
445
+ dipRef.current = null;
445
446
  }, []);
446
- // Synchronizes with the dip's timer and animation: neither outlives the
447
- // scroller.
447
+ // Synchronizes with the dip's animations: they do not outlive the scroller.
448
448
  useEffect(() => stopDip, [stopDip]);
449
449
 
450
450
  const scrollToMonth = useCallback(
@@ -505,18 +505,14 @@ export function MonthScroller({
505
505
  land();
506
506
  return;
507
507
  }
508
- // A second step within the dip has replaced the first (above): only
509
- // its scroll lands, at its own dip's middle.
510
- dipRef.current = {
511
- animation: scroller.animate(
512
- [{opacity: 1}, {opacity: 0}, {opacity: 1}],
513
- {duration: fade.duration, easing: fade.easing},
514
- ),
515
- timer: window.setTimeout(() => {
516
- dipRef.current = null;
517
- land();
518
- }, fade.duration / 2),
519
- };
508
+ // A newer step replaces this one: only its scroll lands.
509
+ dipRef.current = playReducedMotionDip(scroller, fade, () => {
510
+ dipRef.current = null;
511
+ // The fade-in starts as soon as this returns. So render now: the
512
+ // scroll lands in the layout effect, before the fade-in.
513
+ // eslint-disable-next-line @eslint-react/dom-no-flush-sync -- the month must change before the dip's fade-in starts
514
+ flushSync(land);
515
+ });
520
516
  return;
521
517
  }
522
518
  steeredRowRef.current = row;
@@ -49,7 +49,11 @@ import {
49
49
  durationVars,
50
50
  } from '../theme/tokens.stylex';
51
51
  import {focusOutlineStyles} from '../utils';
52
- import {reducedMotionFade} from '../motion/reducedMotion';
52
+ import {
53
+ playReducedMotionDip,
54
+ reducedMotionFade,
55
+ type ReducedMotionDip,
56
+ } from '../motion/reducedMotion';
53
57
  import {dateInputTouchSizes, dateInputTouchGeometry} from './tokens.stylex';
54
58
  import {useOwnScrollGesture} from './useOwnScrollGesture';
55
59
  import {usePointerDragScroll} from './usePointerDragScroll';
@@ -331,19 +335,19 @@ export function Wheel({
331
335
  // once on a fine one.
332
336
  const lastPointerRef = useRef<string | null>(null);
333
337
 
334
- // A reduced-motion dip whose rows have not landed yet: its timer and the
335
- // row it lands on. While one is pending the rows stay put for it, so they
336
- // never move at full opacity, and a further step retargets it.
337
- const pendingFadeRef = useRef<{timer: number; top: number} | null>(null);
338
+ // A reduced-motion dip whose rows have not landed yet: the row they land
339
+ // on. While one is pending the rows stay put for it, so they never move at
340
+ // full opacity, and a further step retargets it.
341
+ const pendingFadeRef = useRef<{top: number} | null>(null);
338
342
  // The running dip, so a step during its fade-in starts the next one where
339
343
  // the column is.
340
- const dipRef = useRef<Animation | null>(null);
344
+ const dipRef = useRef<ReducedMotionDip | null>(null);
345
+ // Synchronizes with the dip's animations: an unmounted wheel lands nothing.
341
346
  useEffect(
342
347
  () => () => {
343
- if (pendingFadeRef.current != null) {
344
- window.clearTimeout(pendingFadeRef.current.timer);
345
- pendingFadeRef.current = null;
346
- }
348
+ pendingFadeRef.current = null;
349
+ dipRef.current?.cancel();
350
+ dipRef.current = null;
347
351
  },
348
352
  [],
349
353
  );
@@ -368,19 +372,15 @@ export function Wheel({
368
372
  ? reducedMotionFade(scroller, {touchRelease})
369
373
  : null;
370
374
  const pending = pendingFadeRef.current;
375
+ // A change that lands now replaces a pending landing. Its dip goes on
376
+ // and fades back in.
371
377
  if (fadeTiming == null) {
372
- if (pending != null) {
373
- window.clearTimeout(pending.timer);
374
- pendingFadeRef.current = null;
375
- }
378
+ pendingFadeRef.current = null;
376
379
  scroller.scrollTo({top, behavior});
377
380
  return;
378
381
  }
379
382
  if (fadeTiming.duration === 0 || typeof scroller.animate !== 'function') {
380
- if (pending != null) {
381
- window.clearTimeout(pending.timer);
382
- pendingFadeRef.current = null;
383
- }
383
+ pendingFadeRef.current = null;
384
384
  scroller.scrollTo({top, behavior: 'auto'});
385
385
  return;
386
386
  }
@@ -389,26 +389,24 @@ export function Wheel({
389
389
  pending.top = top;
390
390
  return;
391
391
  }
392
- const {duration, easing} = fadeTiming;
393
392
  // A step during the last dip's fade-in starts the new dip at the
394
393
  // column's current opacity, not at full opacity. A leaving element fades
395
394
  // out from where it is.
396
395
  const shown = Number.parseFloat(getComputedStyle(scroller).opacity);
397
396
  dipRef.current?.cancel();
398
- dipRef.current = scroller.animate(
399
- [
400
- {opacity: Number.isFinite(shown) ? shown : 1, easing},
401
- {opacity: 0, easing},
402
- {opacity: 1},
403
- ],
404
- {duration, easing: 'linear'},
405
- );
406
- const landing = {top, timer: 0};
407
- landing.timer = window.setTimeout(() => {
408
- pendingFadeRef.current = null;
409
- scroller.scrollTo({top: landing.top, behavior: 'auto'});
410
- }, duration / 2);
397
+ const landing = {top};
411
398
  pendingFadeRef.current = landing;
399
+ dipRef.current = playReducedMotionDip(
400
+ scroller,
401
+ fadeTiming,
402
+ () => {
403
+ if (pendingFadeRef.current === landing) {
404
+ pendingFadeRef.current = null;
405
+ scroller.scrollTo({top: landing.top, behavior: 'auto'});
406
+ }
407
+ },
408
+ Number.isFinite(shown) ? shown : 1,
409
+ );
412
410
  },
413
411
  [itemBlockSize],
414
412
  );
@@ -81,7 +81,10 @@ import {themeProps} from '../utils/themeProps';
81
81
  import type {DialogVariantMap} from './index';
82
82
  import {focusOutlineProps} from '../utils/focusOutline.stylex';
83
83
  import {useInteractionModalityTracking} from '../utils/interactionModality';
84
- import {focusForLastInput, isFocusLost} from '../utils/focusReturn';
84
+ import {
85
+ focusForLastInput,
86
+ prepareFocusReturnOnRemoval,
87
+ } from '../utils/focusReturn';
85
88
  import {ModalAnnounceProvider} from '../hooks/ModalAnnounceProvider';
86
89
 
87
90
  import {useMergedRefs} from '../hooks/useMergedRefs';
@@ -653,39 +656,15 @@ export function Dialog({
653
656
  }, [isOpen, isInline]);
654
657
 
655
658
  // A dialog that unmounts while open (rendered only while open, or removed
656
- // with the rest of a view) never runs the close branch above, and removing a
657
- // <dialog> does not run the browser's own focus return. Focus left inside
658
- // it, or dropped to the body by the removal, goes back to the opener.
659
+ // with the rest of a view) never runs the close branch above. Focus the
660
+ // removal lost goes back to the opener.
659
661
  useEffect(() => {
660
- if (isInline) {
662
+ const dialog = dialogRef.current;
663
+ if (isInline || dialog == null) {
661
664
  return;
662
665
  }
663
- const dialog = dialogRef.current;
664
- // Read now: a removed iframe's document has no window, so its frame
665
- // element can no longer be found once the dialog is gone.
666
- const host = dialog?.ownerDocument.defaultView?.frameElement ?? null;
667
- return () => {
668
- const trigger = triggerElementRef.current;
669
- if (dialog == null || trigger == null) {
670
- return;
671
- }
672
- // Checked once the commit has finished: StrictMode's simulated unmount
673
- // leaves the dialog in the document, and only a real removal returns
674
- // focus.
675
- queueMicrotask(() => {
676
- if (dialog.isConnected || !trigger.isConnected) {
677
- return;
678
- }
679
- // isFocusLost reads the dialog's own document. For a dialog in an
680
- // iframe, the document that hosts the frame must also have lost focus
681
- // or keep it on that frame, wherever the opener is: a control the
682
- // user pressed outside the frame to remove the dialog, or the frame
683
- // itself, keeps its focus.
684
- if (isFocusLost(dialog) && (host == null || isFocusLost(host))) {
685
- focusForLastInput(trigger);
686
- }
687
- });
688
- };
666
+ const returnFocusOnRemoval = prepareFocusReturnOnRemoval(dialog);
667
+ return () => returnFocusOnRemoval(triggerElementRef.current);
689
668
  }, [isInline]);
690
669
 
691
670
  // Host app-global surfaces (toasts) inside this modal while it is open, so
@@ -25,11 +25,11 @@ import {FieldStatus} from '../FieldStatus/FieldStatus';
25
25
  import {useFieldStatusAnnouncement} from '../FieldStatus/useFieldStatusAnnouncement';
26
26
  import {spacingVars, borderVars, sizeVars} from '../theme/tokens.stylex';
27
27
  import type {IconType} from '../Icon';
28
- import {mergeProps} from '../utils';
28
+ import {mergeProps} from '../utils/mergeProps';
29
29
  import {useDevWarning} from '../hooks/useDevWarning';
30
30
  import {FormLayoutContext} from '../FormLayout/FormLayoutContext';
31
31
  import {Text} from '../Text/Text';
32
- import {VisuallyHidden} from '../VisuallyHidden';
32
+ import {VisuallyHidden} from '../VisuallyHidden/VisuallyHidden';
33
33
  import {themeProps} from '../utils/themeProps';
34
34
  import type {FieldStatusPlacement} from './fieldStatusPlacement';
35
35
 
@@ -98,7 +98,8 @@ export const docs = {
98
98
  {
99
99
  name: 'focusTrigger',
100
100
  type: "'auto' | 'always' | 'never'",
101
- description: 'Controls when focus events trigger the hover card.',
101
+ description:
102
+ 'When focus should open the hover card. auto opens when the trigger itself takes focus, or focus lands inside a trigger that is focusable at that moment, so a roving-tabindex member opens when the arrow keys reach it.',
102
103
  default: "'auto'",
103
104
  },
104
105
  {
@@ -303,7 +304,8 @@ export const docsZh = {
303
304
  {
304
305
  name: 'focusTrigger',
305
306
  type: "'auto' | 'always' | 'never'",
306
- description: '控制焦点事件何时触发悬浮卡片。',
307
+ description:
308
+ '焦点何时打开悬浮卡片。auto 在触发元素自身获得焦点,或焦点落在当时可聚焦的触发元素内部时打开,因此方向键到达漫游 tabindex 成员时也会打开。',
307
309
  default: "'auto'",
308
310
  },
309
311
  {
@@ -16,7 +16,14 @@
16
16
  * - /packages/cli/assets/templates/blocks/components/HoverCard/ (showcase blocks)
17
17
  */
18
18
 
19
- import {useCallback, useRef, type ReactElement, type ReactNode} from 'react';
19
+ import {
20
+ useCallback,
21
+ useRef,
22
+ useState,
23
+ type ReactElement,
24
+ type ReactNode,
25
+ } from 'react';
26
+ import {observeMutations} from '../utils/sharedMutationObserver';
20
27
  import {useIsomorphicLayoutEffect} from '../hooks/useIsomorphicLayoutEffect';
21
28
  import * as stylex from '@stylexjs/stylex';
22
29
  import {
@@ -148,9 +155,10 @@ export interface HoverCardProps extends Pick<
148
155
 
149
156
  /**
150
157
  * When to trigger on focus:
151
- * - `auto`: Only if element is naturally focusable
152
- * - `always`: Always attach focus listeners
153
- * - `never`: Never attach focus listeners (for composite widgets)
158
+ * - `auto`: focus on the trigger itself, or within a trigger that is
159
+ * focusable when focus arrives
160
+ * - `always`: focus on the trigger or anything inside it
161
+ * - `never`: never on focus (for composite widgets)
154
162
  *
155
163
  * @default 'auto'
156
164
  */
@@ -303,6 +311,28 @@ export function HoverCard({
303
311
  onHide: handleHide,
304
312
  });
305
313
 
314
+ // The wrapper's first element is the trigger, and it can be replaced after
315
+ // mount: by new children, or by a child component that swaps its own root
316
+ // without a render here. Syncs with the DOM: on a replacement the wiring
317
+ // below re-runs, so the old element is released before the new one is wired.
318
+ const [triggerReplacements, setTriggerReplacements] = useState(0);
319
+ const observedTriggerRef = useRef<Element | null>(null);
320
+ useIsomorphicLayoutEffect(() => {
321
+ const wrapper = wrapperRef.current;
322
+ if (textOnly || wrapper == null) {
323
+ return;
324
+ }
325
+ observedTriggerRef.current = wrapper.firstElementChild;
326
+ return observeMutations(wrapper, {childList: true}, () => {
327
+ const trigger = wrapper.firstElementChild;
328
+ if (trigger === observedTriggerRef.current) {
329
+ return;
330
+ }
331
+ observedTriggerRef.current = trigger;
332
+ setTriggerReplacements(count => count + 1);
333
+ });
334
+ }, [textOnly]);
335
+
306
336
  // For element children with display:contents, attach ref to first child
307
337
  useIsomorphicLayoutEffect(() => {
308
338
  if (textOnly) {
@@ -407,6 +437,7 @@ export function HoverCard({
407
437
  hoverCard.id,
408
438
  hoverCard.isOpen,
409
439
  hoverCard.describedBy,
440
+ triggerReplacements,
410
441
  ]);
411
442
 
412
443
  // While closed, useLayer leaves only an inert <template> marker at this JSX
@@ -5,37 +5,159 @@ export const docs = {
5
5
  name: 'useHoverCard',
6
6
  displayName: 'useHoverCard',
7
7
  group: 'HoverCard',
8
- keywords: ['hovercard', 'hover', 'preview', 'card', 'tooltip', 'popup', 'floating', 'anchor'],
8
+ keywords: [
9
+ 'hovercard',
10
+ 'hover',
11
+ 'preview',
12
+ 'card',
13
+ 'tooltip',
14
+ 'popup',
15
+ 'floating',
16
+ 'anchor',
17
+ ],
9
18
  params: [
10
- {name: 'placement', type: "'above' | 'below' | 'start' | 'end'", description: "Position relative to the trigger. Logical: start/end resolve against the popover\'s own inherited direction (RTL mirrors in pure CSS).", default: "'above'"},
11
- {name: 'alignment', type: "'start' | 'center' | 'end'", description: "Alignment along the placement axis. Logical: start/end resolve against the popover\'s own inherited direction (RTL mirrors in pure CSS).", default: "'center'"},
12
- {name: 'delay', type: 'number', description: 'Delay before showing the hover card on hover, in milliseconds.', default: '300'},
13
- {name: 'hideDelay', type: 'number', description: 'Delay before hiding after mouse or focus leaves, in milliseconds.', default: '200'},
14
- {name: 'focusTrigger', type: "'auto' | 'always' | 'never'", description: 'When focus should open the hover card. auto only attaches focus listeners to naturally focusable elements.', default: "'auto'"},
15
- {name: 'touchTrigger', type: "'auto' | 'tap' | 'none'", description: 'What a tap does where there is no hover. auto opens on tap unless the trigger performs an action of its own (an aria-disabled control performs none); tap always opens; none never opens on touch.', default: "'auto'"},
16
- {name: 'isEnabled', type: 'boolean', description: 'Whether hover and focus triggers are enabled.', default: 'true'},
17
- {name: 'label', type: 'string', description: 'Accessible name for the hover card popup. When provided, the popup is exposed as a named role="dialog"; when omitted, it falls back to role="group" (a group may validly be unnamed).'},
18
- {name: 'isOpen', type: 'boolean', description: 'Controlled open state. true force-shows, false force-hides, undefined lets hover/focus manage visibility.'},
19
- {name: 'isDefaultOpen', type: 'boolean', description: 'Whether the hover card should be shown on mount.', default: 'false'},
20
- {name: 'onShow', type: '() => void', description: 'Callback fired when the hover card becomes visible.'},
21
- {name: 'onHide', type: '() => void', description: 'Callback fired when the hover card is hidden.'},
19
+ {
20
+ name: 'placement',
21
+ type: "'above' | 'below' | 'start' | 'end'",
22
+ description:
23
+ "Position relative to the trigger. Logical: start/end resolve against the popover\'s own inherited direction (RTL mirrors in pure CSS).",
24
+ default: "'above'",
25
+ },
26
+ {
27
+ name: 'alignment',
28
+ type: "'start' | 'center' | 'end'",
29
+ description:
30
+ "Alignment along the placement axis. Logical: start/end resolve against the popover\'s own inherited direction (RTL mirrors in pure CSS).",
31
+ default: "'center'",
32
+ },
33
+ {
34
+ name: 'delay',
35
+ type: 'number',
36
+ description:
37
+ 'Delay before showing the hover card on hover, in milliseconds.',
38
+ default: '300',
39
+ },
40
+ {
41
+ name: 'hideDelay',
42
+ type: 'number',
43
+ description:
44
+ 'Delay before hiding after mouse or focus leaves, in milliseconds.',
45
+ default: '200',
46
+ },
47
+ {
48
+ name: 'focusTrigger',
49
+ type: "'auto' | 'always' | 'never'",
50
+ description:
51
+ 'When focus should open the hover card. auto opens when the trigger itself takes focus, or focus lands inside a trigger that is focusable at that moment, so a roving-tabindex member opens when the arrow keys reach it.',
52
+ default: "'auto'",
53
+ },
54
+ {
55
+ name: 'touchTrigger',
56
+ type: "'auto' | 'tap' | 'none'",
57
+ description:
58
+ 'What a tap does where there is no hover. auto opens on tap unless the trigger performs an action of its own (an aria-disabled control performs none); tap always opens; none never opens on touch.',
59
+ default: "'auto'",
60
+ },
61
+ {
62
+ name: 'isEnabled',
63
+ type: 'boolean',
64
+ description: 'Whether hover and focus triggers are enabled.',
65
+ default: 'true',
66
+ },
67
+ {
68
+ name: 'label',
69
+ type: 'string',
70
+ description:
71
+ 'Accessible name for the hover card popup. When provided, the popup is exposed as a named role="dialog"; when omitted, it falls back to role="group" (a group may validly be unnamed).',
72
+ },
73
+ {
74
+ name: 'isOpen',
75
+ type: 'boolean',
76
+ description:
77
+ 'Controlled open state. true force-shows, false force-hides, undefined lets hover/focus manage visibility.',
78
+ },
79
+ {
80
+ name: 'isDefaultOpen',
81
+ type: 'boolean',
82
+ description: 'Whether the hover card should be shown on mount.',
83
+ default: 'false',
84
+ },
85
+ {
86
+ name: 'onShow',
87
+ type: '() => void',
88
+ description: 'Callback fired when the hover card becomes visible.',
89
+ },
90
+ {
91
+ name: 'onHide',
92
+ type: '() => void',
93
+ description: 'Callback fired when the hover card is hidden.',
94
+ },
22
95
  ],
23
96
  returns: [
24
- {name: 'ref', type: 'RefCallback<HTMLElement>', description: 'Combined ref that sets both position and interaction on the same trigger element.'},
25
- {name: 'positionRef', type: 'RefCallback<HTMLElement>', description: 'Ref for the positioning anchor element. Use when position and interaction live on different elements.'},
26
- {name: 'interactionRef', type: 'RefCallback<HTMLElement>', description: 'Ref for the hover/focus interaction element. Use with positionRef for split trigger patterns.'},
27
- {name: 'anchorId', type: 'string', description: 'CSS anchor name for advanced positioning cases.'},
28
- {name: 'describedBy', type: 'string', description: 'ID to compose into aria-describedby on the trigger.'},
29
- {name: 'renderHoverCard', type: "(children: ReactNode, props?: Omit<ContextRenderProps, 'positioning'>) => ReactNode", description: 'Render function for the anchor-positioned hover card content. The positioning opt-out is excluded: the hover card always derives its position from placement/alignment.'},
30
- {name: 'show', type: '() => void', description: 'Imperatively show the hover card immediately.'},
31
- {name: 'hide', type: '() => void', description: 'Imperatively hide the hover card immediately.'},
97
+ {
98
+ name: 'ref',
99
+ type: 'RefCallback<HTMLElement>',
100
+ description:
101
+ 'Combined ref that sets both position and interaction on the same trigger element.',
102
+ },
103
+ {
104
+ name: 'positionRef',
105
+ type: 'RefCallback<HTMLElement>',
106
+ description:
107
+ 'Ref for the positioning anchor element. Use when position and interaction live on different elements.',
108
+ },
109
+ {
110
+ name: 'interactionRef',
111
+ type: 'RefCallback<HTMLElement>',
112
+ description:
113
+ 'Ref for the hover/focus interaction element. Use with positionRef for split trigger patterns.',
114
+ },
115
+ {
116
+ name: 'anchorId',
117
+ type: 'string',
118
+ description: 'CSS anchor name for advanced positioning cases.',
119
+ },
120
+ {
121
+ name: 'describedBy',
122
+ type: 'string',
123
+ description: 'ID to compose into aria-describedby on the trigger.',
124
+ },
125
+ {
126
+ name: 'renderHoverCard',
127
+ type: "(children: ReactNode, props?: Omit<ContextRenderProps, 'positioning'>) => ReactNode",
128
+ description:
129
+ 'Render function for the anchor-positioned hover card content. The positioning opt-out is excluded: the hover card always derives its position from placement/alignment.',
130
+ },
131
+ {
132
+ name: 'show',
133
+ type: '() => void',
134
+ description: 'Imperatively show the hover card immediately.',
135
+ },
136
+ {
137
+ name: 'hide',
138
+ type: '() => void',
139
+ description: 'Imperatively hide the hover card immediately.',
140
+ },
32
141
  ],
33
142
  usage: {
34
- description: 'Headless hook for hover-triggered floating cards. Builds on useLayer with hover/focus intent detection, configurable delays, safe hover behavior, and accessible aria-describedby linking. Use for rich previews on hover when you need full control over the trigger or rendered content.',
143
+ description:
144
+ 'Headless hook for hover-triggered floating cards. Builds on useLayer with hover/focus intent detection, configurable delays, safe hover behavior, and accessible aria-describedby linking. Use for rich previews on hover when you need full control over the trigger or rendered content.',
35
145
  bestPractices: [
36
- {guidance: true, description: 'Use for rich content previews such as user profiles, entity summaries, and link previews.'},
37
- {guidance: true, description: 'Prefer the HoverCard component for standard trigger-content pairs; use the hook for custom trigger patterns.'},
38
- {guidance: false, description: 'Use for simple text hints: use Tooltip or useTooltip instead.'},
146
+ {
147
+ guidance: true,
148
+ description:
149
+ 'Use for rich content previews such as user profiles, entity summaries, and link previews.',
150
+ },
151
+ {
152
+ guidance: true,
153
+ description:
154
+ 'Prefer the HoverCard component for standard trigger-content pairs; use the hook for custom trigger patterns.',
155
+ },
156
+ {
157
+ guidance: false,
158
+ description:
159
+ 'Use for simple text hints: use Tooltip or useTooltip instead.',
160
+ },
39
161
  ],
40
162
  },
41
163
  relatedComponents: ['HoverCard', 'Tooltip', 'Popover'],
@@ -46,17 +168,24 @@ export const docs = {
46
168
 
47
169
  /** @type {import('@astryxdesign/cli/authoring').HookTranslationDoc} */
48
170
  export const docsDense = {
49
- description: 'Headless hover-triggered floating cards. Builds on useLayer w/ hover/focus intent, delays, safe hover behavior, aria-describedby. Use for rich previews when trigger/content rendering needs full control.',
171
+ description:
172
+ 'Headless hover-triggered floating cards. Builds on useLayer w/ hover/focus intent, delays, safe hover behavior, aria-describedby. Use for rich previews when trigger/content rendering needs full control.',
50
173
  paramDescriptions: {
51
- placement: 'position relative to trigger. logical: start/end follow the popover\'s inherited direction (RTL mirrors).',
52
- alignment: 'alignment along placement axis. logical: start/end follow the popover\'s inherited direction (RTL mirrors).',
174
+ placement:
175
+ "position relative to trigger. logical: start/end follow the popover's inherited direction (RTL mirrors).",
176
+ alignment:
177
+ "alignment along placement axis. logical: start/end follow the popover's inherited direction (RTL mirrors).",
53
178
  delay: 'show delay in ms.',
54
179
  hideDelay: 'hide delay after mouse/focus leave in ms.',
55
- focusTrigger: 'when focus opens hover card; auto = naturally focusable elements only.',
56
- touchTrigger: 'tap behavior with no hover; auto = tap unless trigger acts, tap = always, none = never.',
180
+ focusTrigger:
181
+ 'when focus opens hover card; auto = focus on the trigger, or inside a trigger focusable when focus arrives.',
182
+ touchTrigger:
183
+ 'tap behavior with no hover; auto = tap unless trigger acts, tap = always, none = never.',
57
184
  isEnabled: 'whether hover/focus triggers are enabled.',
58
- label: 'accessible name for popup. with label: named role="dialog"; without: role="group".',
59
- isOpen: 'controlled open state: true force-show, false force-hide, undefined unmanaged.',
185
+ label:
186
+ 'accessible name for popup. with label: named role="dialog"; without: role="group".',
187
+ isOpen:
188
+ 'controlled open state: true force-show, false force-hide, undefined unmanaged.',
60
189
  isDefaultOpen: 'show on mount.',
61
190
  onShow: 'fires when hover card becomes visible.',
62
191
  onHide: 'fires when hover card hides.',
@@ -72,11 +201,24 @@ export const docsDense = {
72
201
  hide: 'hide immediately.',
73
202
  },
74
203
  usage: {
75
- description: 'Headless hover-triggered floating cards. Builds on useLayer w/ hover/focus intent, delays, safe hover behavior, aria-describedby. Use for rich previews when trigger/content rendering needs full control.',
204
+ description:
205
+ 'Headless hover-triggered floating cards. Builds on useLayer w/ hover/focus intent, delays, safe hover behavior, aria-describedby. Use for rich previews when trigger/content rendering needs full control.',
76
206
  bestPractices: [
77
- {guidance: true, description: 'Use for rich previews: profiles, entity summaries, link previews.'},
78
- {guidance: true, description: 'Prefer HoverCard for standard trigger-content pairs; use hook for custom trigger patterns.'},
79
- {guidance: false, description: 'Use for simple text hints: use Tooltip / useTooltip instead.'},
207
+ {
208
+ guidance: true,
209
+ description:
210
+ 'Use for rich previews: profiles, entity summaries, link previews.',
211
+ },
212
+ {
213
+ guidance: true,
214
+ description:
215
+ 'Prefer HoverCard for standard trigger-content pairs; use hook for custom trigger patterns.',
216
+ },
217
+ {
218
+ guidance: false,
219
+ description:
220
+ 'Use for simple text hints: use Tooltip / useTooltip instead.',
221
+ },
80
222
  ],
81
223
  },
82
224
  };