@astryxdesign/core 0.7.7-canary.fc68123 → 0.7.7

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 (240) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/dist/AlertDialog/AlertDialog.d.ts +1 -4
  3. package/dist/AlertDialog/AlertDialog.d.ts.map +1 -1
  4. package/dist/AlertDialog/AlertDialog.js +6 -36
  5. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  6. package/dist/BottomSheet/BottomSheetPanel.js +4 -23
  7. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
  8. package/dist/BottomSheet/useSheetGestures.js +1 -3
  9. package/dist/Button/Button.d.ts.map +1 -1
  10. package/dist/Button/Button.js +1 -17
  11. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  12. package/dist/CheckboxInput/CheckboxInput.js +2 -13
  13. package/dist/ComplexSelector/ComplexSelector.d.ts +1 -4
  14. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  15. package/dist/ComplexSelector/ComplexSelector.js +4 -14
  16. package/dist/ContextMenu/ContextMenu.js +2 -2
  17. package/dist/Dialog/Dialog.d.ts.map +1 -1
  18. package/dist/Dialog/Dialog.js +31 -7
  19. package/dist/Field/Field.js +2 -2
  20. package/dist/HoverCard/HoverCard.d.ts +3 -4
  21. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  22. package/dist/HoverCard/HoverCard.js +2 -27
  23. package/dist/HoverCard/useHoverCard.d.ts +4 -5
  24. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  25. package/dist/HoverCard/useHoverCard.js +31 -17
  26. package/dist/Icon/Icon.d.ts +1 -5
  27. package/dist/Icon/Icon.d.ts.map +1 -1
  28. package/dist/Icon/Icon.js +2 -5
  29. package/dist/Layer/usePressPin.d.ts +5 -17
  30. package/dist/Layer/usePressPin.d.ts.map +1 -1
  31. package/dist/Layer/usePressPin.js +12 -62
  32. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  33. package/dist/Lightbox/Lightbox.js +0 -18
  34. package/dist/PowerSearch/PowerSearch.d.ts +3 -36
  35. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  36. package/dist/PowerSearch/PowerSearch.js +127 -433
  37. package/dist/PowerSearch/PowerSearchEditPopover.d.ts +1 -32
  38. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  39. package/dist/PowerSearch/PowerSearchEditPopover.js +6 -17
  40. package/dist/PowerSearch/PowerSearchValueEditor.d.ts +3 -38
  41. package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
  42. package/dist/PowerSearch/PowerSearchValueEditor.js +61 -170
  43. package/dist/PowerSearch/formatFilterValue.d.ts +0 -7
  44. package/dist/PowerSearch/formatFilterValue.d.ts.map +1 -1
  45. package/dist/PowerSearch/formatFilterValue.js +1 -11
  46. package/dist/RadioList/RadioList.d.ts.map +1 -1
  47. package/dist/RadioList/RadioList.js +2 -3
  48. package/dist/RadioList/RadioListItem.d.ts +1 -1
  49. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  50. package/dist/RadioList/RadioListItem.js +3 -5
  51. package/dist/Resizable/ResizeHandle.d.ts +1 -2
  52. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  53. package/dist/Resizable/ResizeHandle.js +77 -99
  54. package/dist/SegmentedControl/useSegmentedSelectionMotion.d.ts.map +1 -1
  55. package/dist/SegmentedControl/useSegmentedSelectionMotion.js +3 -3
  56. package/dist/Slider/sliderTouchController.js +2 -2
  57. package/dist/Spinner/Spinner.d.ts +1 -1
  58. package/dist/Spinner/Spinner.d.ts.map +1 -1
  59. package/dist/Spinner/Spinner.js +58 -60
  60. package/dist/Switch/useSwitchTouch.js +4 -4
  61. package/dist/Table/BaseTable.d.ts.map +1 -1
  62. package/dist/Table/BaseTable.js +8 -29
  63. package/dist/TextInput/TextInput.d.ts +2 -2
  64. package/dist/TextInput/TextInput.d.ts.map +1 -1
  65. package/dist/TextInput/TextInput.js +7 -10
  66. package/dist/Toast/Toast.d.ts +1 -11
  67. package/dist/Toast/Toast.d.ts.map +1 -1
  68. package/dist/Toast/Toast.js +15 -25
  69. package/dist/Toast/ToastViewport.d.ts.map +1 -1
  70. package/dist/Toast/ToastViewport.js +34 -94
  71. package/dist/Toast/types.d.ts +3 -9
  72. package/dist/Toast/types.d.ts.map +1 -1
  73. package/dist/Tooltip/useTooltip.d.ts +1 -19
  74. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  75. package/dist/Tooltip/useTooltip.js +53 -34
  76. package/dist/Typeahead/TypeaheadItem.d.ts +1 -2
  77. package/dist/Typeahead/TypeaheadItem.d.ts.map +1 -1
  78. package/dist/astryx.css +6 -35
  79. package/dist/gestures/PressTracker.d.ts.map +1 -1
  80. package/dist/gestures/PressTracker.js +3 -4
  81. package/dist/hooks/actionGroupTracker.d.ts +1 -4
  82. package/dist/hooks/actionGroupTracker.d.ts.map +1 -1
  83. package/dist/hooks/actionGroupTracker.js +3 -13
  84. package/dist/hooks/index.d.ts +0 -1
  85. package/dist/hooks/index.d.ts.map +1 -1
  86. package/dist/hooks/index.js +0 -1
  87. package/dist/hooks/useAttachment.d.ts +1 -1
  88. package/dist/hooks/useAttachment.js +1 -1
  89. package/dist/hooks/useInputStatusIcon.d.ts +1 -1
  90. package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
  91. package/dist/hooks/useInputStatusIcon.js +2 -2
  92. package/dist/hooks/useLongPress.d.ts +2 -6
  93. package/dist/hooks/useLongPress.d.ts.map +1 -1
  94. package/dist/hooks/useLongPress.js +15 -95
  95. package/dist/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.d.ts.map +1 -1
  96. package/dist/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.js +0 -6
  97. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  98. package/dist/i18n/generated-locales/en.generated.js +0 -30
  99. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  100. package/dist/i18n/generated-locales/pseudo.generated.js +0 -30
  101. package/dist/index.d.ts +0 -1
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js +0 -4
  104. package/dist/motion/animatedLayout.d.ts.map +1 -1
  105. package/dist/motion/animatedLayout.js +6 -5
  106. package/dist/motion/animator.d.ts +14 -14
  107. package/dist/motion/animator.d.ts.map +1 -1
  108. package/dist/motion/animator.js +18 -41
  109. package/dist/utils/dateParser.d.ts.map +1 -1
  110. package/dist/utils/dateParser.js +0 -12
  111. package/dist/utils/focusReturn.d.ts +0 -15
  112. package/dist/utils/focusReturn.d.ts.map +1 -1
  113. package/dist/utils/focusReturn.js +0 -28
  114. package/dist/utils/interactionOverlay.stylex.d.ts +2 -3
  115. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
  116. package/dist/utils/interactionOverlay.stylex.js +2 -3
  117. package/dist/utils/pressFeedback.d.ts.map +1 -1
  118. package/dist/utils/pressFeedback.js +6 -19
  119. package/locales/en.json +0 -120
  120. package/locales/pseudo.json +0 -90
  121. package/package.json +5 -10
  122. package/src/AlertDialog/AlertDialog.doc.mjs +2 -2
  123. package/src/AlertDialog/AlertDialog.tsx +5 -37
  124. package/src/BottomSheet/BottomSheetPanel.tsx +3 -37
  125. package/src/BottomSheet/useSheetGestures.ts +1 -3
  126. package/src/Button/Button.tsx +4 -26
  127. package/src/CheckboxInput/CheckboxInput.doc.mjs +0 -18
  128. package/src/CheckboxInput/CheckboxInput.tsx +9 -36
  129. package/src/ComplexSelector/ComplexSelector.tsx +5 -20
  130. package/src/ContextMenu/ContextMenu.tsx +2 -2
  131. package/src/Dialog/Dialog.tsx +31 -10
  132. package/src/Field/Field.tsx +2 -2
  133. package/src/HoverCard/HoverCard.doc.mjs +2 -4
  134. package/src/HoverCard/HoverCard.tsx +4 -35
  135. package/src/HoverCard/useHoverCard.doc.mjs +36 -178
  136. package/src/HoverCard/useHoverCard.tsx +52 -46
  137. package/src/Icon/Icon.doc.mjs +8 -49
  138. package/src/Icon/Icon.tsx +7 -15
  139. package/src/Layer/usePressPin.ts +15 -81
  140. package/src/Lightbox/Lightbox.tsx +0 -18
  141. package/src/PowerSearch/PowerSearch.doc.mjs +5 -54
  142. package/src/PowerSearch/PowerSearch.tsx +155 -609
  143. package/src/PowerSearch/PowerSearchEditPopover.tsx +6 -28
  144. package/src/PowerSearch/PowerSearchValueEditor.tsx +38 -203
  145. package/src/PowerSearch/formatFilterValue.ts +1 -18
  146. package/src/RadioList/RadioList.tsx +2 -3
  147. package/src/RadioList/RadioListItem.tsx +3 -5
  148. package/src/Resizable/Resizable.doc.mjs +4 -5
  149. package/src/Resizable/ResizeHandle.tsx +109 -167
  150. package/src/SegmentedControl/useSegmentedSelectionMotion.ts +3 -6
  151. package/src/Slider/sliderTouchController.ts +2 -2
  152. package/src/Spinner/Spinner.tsx +63 -77
  153. package/src/Switch/useSwitchTouch.ts +4 -4
  154. package/src/Table/BaseTable.tsx +12 -46
  155. package/src/TextInput/TextInput.tsx +10 -11
  156. package/src/Toast/Toast.doc.mjs +3 -4
  157. package/src/Toast/Toast.tsx +32 -52
  158. package/src/Toast/ToastViewport.tsx +35 -121
  159. package/src/Toast/types.ts +3 -9
  160. package/src/Tooltip/useTooltip.doc.mjs +0 -24
  161. package/src/Tooltip/useTooltip.tsx +56 -61
  162. package/src/Typeahead/TypeaheadItem.doc.mjs +3 -6
  163. package/src/Typeahead/TypeaheadItem.tsx +1 -2
  164. package/src/gestures/PressTracker.ts +3 -4
  165. package/src/gestures/README.md +1 -7
  166. package/src/hooks/actionGroupTracker.ts +5 -20
  167. package/src/hooks/index.ts +0 -2
  168. package/src/hooks/useAttachment.ts +1 -1
  169. package/src/hooks/useInputStatusIcon.tsx +2 -3
  170. package/src/hooks/useLongPress.doc.mjs +2 -2
  171. package/src/hooks/useLongPress.ts +15 -136
  172. package/src/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.ts +0 -6
  173. package/src/i18n/generated-locales/en.generated.ts +0 -41
  174. package/src/i18n/generated-locales/pseudo.generated.ts +0 -42
  175. package/src/index.ts +0 -13
  176. package/src/motion/README.md +5 -9
  177. package/src/motion/animatedLayout.ts +4 -5
  178. package/src/motion/animator.ts +29 -61
  179. package/src/utils/dateParser.ts +0 -16
  180. package/src/utils/focusReturn.ts +0 -30
  181. package/src/utils/interactionOverlay.stylex.ts +2 -3
  182. package/src/utils/pressFeedback.ts +6 -19
  183. package/dist/PowerSearch/PowerSearchBottomSheet.d.ts +0 -100
  184. package/dist/PowerSearch/PowerSearchBottomSheet.d.ts.map +0 -1
  185. package/dist/PowerSearch/PowerSearchBottomSheet.js +0 -1913
  186. package/dist/PowerSearch/formatTokenValue.d.ts +0 -28
  187. package/dist/PowerSearch/formatTokenValue.d.ts.map +0 -1
  188. package/dist/PowerSearch/formatTokenValue.js +0 -160
  189. package/dist/PowerSearch/powerSearchSheetUtils.d.ts +0 -65
  190. package/dist/PowerSearch/powerSearchSheetUtils.d.ts.map +0 -1
  191. package/dist/PowerSearch/powerSearchSheetUtils.js +0 -168
  192. package/dist/animator/createAnimator.d.ts +0 -98
  193. package/dist/animator/createAnimator.d.ts.map +0 -1
  194. package/dist/animator/createAnimator.js +0 -259
  195. package/dist/hooks/pressGroupContext.d.ts +0 -6
  196. package/dist/hooks/pressGroupContext.d.ts.map +0 -1
  197. package/dist/hooks/pressGroupContext.js +0 -20
  198. package/dist/hooks/useActionGroupTracking.d.ts +0 -53
  199. package/dist/hooks/useActionGroupTracking.d.ts.map +0 -1
  200. package/dist/hooks/useActionGroupTracking.js +0 -174
  201. package/dist/hooks/useAnimator.d.ts +0 -41
  202. package/dist/hooks/useAnimator.d.ts.map +0 -1
  203. package/dist/hooks/useAnimator.js +0 -70
  204. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts +0 -8
  205. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts.map +0 -1
  206. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.js +0 -42
  207. package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.d.ts +0 -8
  208. package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.d.ts.map +0 -1
  209. package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.js +0 -14
  210. package/dist/internal/indicatorPress.stylex.d.ts +0 -20
  211. package/dist/internal/indicatorPress.stylex.d.ts.map +0 -1
  212. package/dist/internal/indicatorPress.stylex.js +0 -26
  213. package/dist/motion/progress.d.ts +0 -88
  214. package/dist/motion/progress.d.ts.map +0 -1
  215. package/dist/motion/progress.js +0 -128
  216. package/dist/motion/uikit/alert.d.ts +0 -41
  217. package/dist/motion/uikit/alert.d.ts.map +0 -1
  218. package/dist/motion/uikit/alert.js +0 -43
  219. package/dist/privateAttributes/pressGroupItem.stylex.d.ts +0 -15
  220. package/dist/privateAttributes/pressGroupItem.stylex.d.ts.map +0 -1
  221. package/dist/privateAttributes/pressGroupItem.stylex.js +0 -23
  222. package/dist/utils/triggerFocus.d.ts +0 -36
  223. package/dist/utils/triggerFocus.d.ts.map +0 -1
  224. package/dist/utils/triggerFocus.js +0 -65
  225. package/src/PowerSearch/PowerSearchBottomSheet.tsx +0 -2515
  226. package/src/PowerSearch/formatTokenValue.ts +0 -215
  227. package/src/PowerSearch/powerSearchSheetUtils.ts +0 -224
  228. package/src/animator/createAnimator.doc.mjs +0 -153
  229. package/src/animator/createAnimator.ts +0 -324
  230. package/src/hooks/pressGroupContext.ts +0 -21
  231. package/src/hooks/useActionGroupTracking.ts +0 -224
  232. package/src/hooks/useAnimator.doc.mjs +0 -109
  233. package/src/hooks/useAnimator.ts +0 -67
  234. package/src/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.ts +0 -43
  235. package/src/i18n/generated-english/PowerSearch/formatTokenValue.generated.ts +0 -15
  236. package/src/internal/indicatorPress.stylex.ts +0 -24
  237. package/src/motion/progress.ts +0 -155
  238. package/src/motion/uikit/alert.ts +0 -43
  239. package/src/privateAttributes/pressGroupItem.stylex.ts +0 -23
  240. package/src/utils/triggerFocus.ts +0 -80
@@ -21,7 +21,6 @@ import {
21
21
  useMemo,
22
22
  useRef,
23
23
  useState,
24
- useSyncExternalStore,
25
24
  } from 'react';
26
25
  import type {ReactNode} from 'react';
27
26
  import {createPortal} from 'react-dom';
@@ -29,11 +28,6 @@ import * as stylex from '@stylexjs/stylex';
29
28
  import {spacingVars, durationVars, easeVars} from '../theme/tokens.stylex';
30
29
  import {mergeProps} from '../utils';
31
30
  import {getNodeText} from '../utils/getNodeText';
32
- import {
33
- moveFocusLeftBehind,
34
- noteFocusWithin,
35
- type FocusWithin,
36
- } from '../utils/focusReturn';
37
31
  import {INTERACTIVE_SELECTORS} from '../hooks/useClickableContainer';
38
32
  import {useAnnounce} from '../hooks/useAnnounce';
39
33
  import {layerTextReset} from '../Layer/layerTextReset.stylex';
@@ -230,8 +224,6 @@ interface ToastRowProps {
230
224
  isReversed: boolean;
231
225
  onExited: (id: string) => void;
232
226
  onDismiss: (id: string, reason: ToastDismissReason) => void;
233
- /** Whether the card carries its own live region (see ToastViewport). */
234
- isLiveRegion: boolean;
235
227
  }
236
228
 
237
229
  /**
@@ -254,7 +246,6 @@ function ToastRow({
254
246
  isReversed,
255
247
  onExited,
256
248
  onDismiss,
257
- isLiveRegion,
258
249
  }: ToastRowProps) {
259
250
  const o = entry.options;
260
251
  const type = o.type ?? 'info';
@@ -305,8 +296,6 @@ function ToastRow({
305
296
  gestureDirection={isReversed ? -1 : 1}
306
297
  onDismiss={reason => onDismiss(entry.id, reason)}
307
298
  renderContent={o.renderContent}
308
- timerKey={entry.id}
309
- isLiveRegion={isLiveRegion}
310
299
  />
311
300
  </div>
312
301
  </div>
@@ -314,9 +303,6 @@ function ToastRow({
314
303
  }
315
304
  ToastRow.displayName = 'ToastRow';
316
305
 
317
- const hasActiveModalOutlet = (): boolean => getActiveModalOutlet() != null;
318
- const hasNoModalOutlet = (): boolean => false;
319
-
320
306
  /**
321
307
  * Container that renders and manages toast notifications. Place at the root
322
308
  * of your app to enable useToast(). Toasts stack with enter/exit
@@ -377,13 +363,6 @@ export function ToastViewport({
377
363
  // The next toast id that should receive focus once the dismissed toast has
378
364
  // unmounted, or 'restore' to fall back to the previously-focused element.
379
365
  const pendingFocusRef = useRef<string | 'restore' | null>(null);
380
- // The id of a toast whose overwrite updated a row that held focus. If the
381
- // update removed the focused control, the commit hands focus off.
382
- const overwriteFocusRef = useRef<{id: string; note: FocusWithin} | null>(
383
- null,
384
- );
385
- // Where focus was inside a toast whose dismissal hands focus off.
386
- const dismissFocusNoteRef = useRef<FocusWithin | null>(null);
387
366
 
388
367
  // Collect a focusable control within a toast node, if any.
389
368
  // Reuses the canonical INTERACTIVE_SELECTORS list (native controls plus
@@ -413,9 +392,13 @@ export function ToastViewport({
413
392
  [],
414
393
  );
415
394
 
416
- // Announce toasts through the persistent singleton live regions. They are
417
- // mounted empty and only mutated afterwards, which screen readers announce
418
- // (see useAnnounce.ts). The announcement happens in addToast — the imperative
395
+ // Announce toasts through the persistent singleton live regions. Each
396
+ // <Toast> also renders its own role="status"/"alert" region, but that region
397
+ // is "born with content" — mounted together with its text — which many
398
+ // screen readers do not announce (see useAnnounce.ts); the singleton regions
399
+ // are mounted empty and only mutated afterwards, so they are what actually
400
+ // guarantees the announcement (the per-toast markup is kept for browse-mode
401
+ // discoverability). The announcement happens in addToast — the imperative
419
402
  // dispatch path invoked once per useToast() call from an event handler,
420
403
  // never from render — so each toast is announced exactly once by
421
404
  // construction, independent of the React render lifecycle (StrictMode
@@ -423,16 +406,6 @@ export function ToastViewport({
423
406
  // re-renders never re-announce). It is client-only (addToast never runs
424
407
  // during SSR), so it is SSR-safe.
425
408
  const announce = useAnnounce();
426
- // An open native modal hides the body-level channel above from assistive
427
- // technology, so while the viewport is hosted in one, each card announces
428
- // itself through its own live region instead. Outside a modal the cards
429
- // carry none, so a card updated in place is announced once.
430
- const hasModalOutlet = useSyncExternalStore(
431
- subscribeModalOutlets,
432
- hasActiveModalOutlet,
433
- hasNoModalOutlet,
434
- );
435
- const isModalHosted = isTopLayer && hasModalOutlet;
436
409
 
437
410
  const addToast = useCallback(
438
411
  (entry: ToastEntry) => {
@@ -468,29 +441,9 @@ export function ToastViewport({
468
441
  if (overwritten && renderedToastsRef.current.includes(overwritten)) {
469
442
  replacedRef.current.set(overwritten.id, overwritten);
470
443
  }
471
- // An overwrite of a toast that is not leaving keeps its row, so the card
472
- // updates in place and focus inside it stays. A leaving namesake
473
- // is replaced by a new row, which enters again.
474
- const keepsRow =
475
- overwritten != null && !exitingIdsRef.current.has(overwritten.id);
476
- const placed: ToastEntry = keepsRow
477
- ? {...entry, rowKey: overwritten.rowKey ?? overwritten.id}
478
- : entry;
479
- if (keepsRow) {
480
- // If the update removes the focused control, the commit hands focus
481
- // off as a dismissal does.
482
- const note = noteFocusWithin(
483
- viewportRef.current?.querySelector<HTMLElement>(
484
- `[data-toast-id="${overwritten.id}"]`,
485
- ),
486
- );
487
- if (note != null) {
488
- overwriteFocusRef.current = {id: placed.id, note};
489
- }
490
- }
491
444
  const projected = overwritten
492
- ? committed.map(t => (t === overwritten ? placed : t))
493
- : [...committed, placed];
445
+ ? committed.map(t => (t === overwritten ? entry : t))
446
+ : [...committed, entry];
494
447
  // `maxVisible={0}` shows every toast (FR7): nothing leaves the window.
495
448
  const firstVisible = maxVisible > 0 ? projected.length - maxVisible : 0;
496
449
  const cutIds = new Set(
@@ -517,7 +470,7 @@ export function ToastViewport({
517
470
  }
518
471
  }
519
472
  }
520
- pending.set(entry.id, placed);
473
+ pending.set(entry.id, entry);
521
474
  // An overwrite swaps a new entry, with a new id, into the replaced
522
475
  // toast's place, so the old row leaves the DOM without ever being
523
476
  // dismissed. Its settled id is dropped by the prune below, which covers
@@ -529,8 +482,8 @@ export function ToastViewport({
529
482
  // overwrite into an append. An ignored collision already returned.
530
483
  const kept = prev.filter(t => !cutIds.has(t.id));
531
484
  return overwritten && kept.includes(overwritten)
532
- ? kept.map(t => (t === overwritten ? placed : t))
533
- : [...kept, placed];
485
+ ? kept.map(t => (t === overwritten ? entry : t))
486
+ : [...kept, entry];
534
487
  });
535
488
  },
536
489
  [announce, maxVisible],
@@ -559,23 +512,14 @@ export function ToastViewport({
559
512
  // focused before the user entered the viewport) rather than <body>.
560
513
  const el = viewportRef.current;
561
514
  const active = document.activeElement;
562
- // A toast an overwrite replaced in place no longer owns its row: the
563
- // replacement does, so dismissing the replaced toast leaves the row, and
564
- // focus in it, to the replacement.
565
- const rowKey = entry.rowKey ?? entry.id;
566
- const isReplacedInPlace =
567
- !toastsRef.current.some(t => t.id === id) &&
568
- toastsRef.current.some(t => t.rowKey === rowKey);
569
- const dismissedNode = isReplacedInPlace
570
- ? null
571
- : (el?.querySelector<HTMLElement>(`[data-toast-id="${id}"]`) ?? null);
515
+ const dismissedNode =
516
+ el?.querySelector<HTMLElement>(`[data-toast-id="${id}"]`) ?? null;
572
517
  if (
573
518
  dismissedNode &&
574
519
  active instanceof Node &&
575
520
  dismissedNode.contains(active)
576
521
  ) {
577
522
  focusHandoffIdRef.current = id;
578
- dismissFocusNoteRef.current = noteFocusWithin(dismissedNode);
579
523
  // Pick the toast to receive focus while the DOM is still intact: the
580
524
  // nearest one after it, then before it, that shows once it is gone
581
525
  // (FR10), else restore. The list may already run ahead of the render
@@ -643,41 +587,6 @@ export function ToastViewport({
643
587
  setToasts(prev => prev.filter(t => t.id !== id));
644
588
  }, []);
645
589
 
646
- // Moves focus to a control in the target toast, else in another toast that
647
- // shows (newest first), else back to the element focused before focus
648
- // entered the viewport, so a handoff never drops focus to <body>.
649
- const handOffFocus = useCallback(
650
- (note: FocusWithin | null, targetId: string | null) => {
651
- const el = viewportRef.current;
652
- const rowFor = (id: string) =>
653
- el?.querySelector<HTMLElement>(`[data-toast-id="${id}"]`) ?? null;
654
- const shownIds = toastsRef.current
655
- .slice(maxVisibleRef.current > 0 ? -maxVisibleRef.current : 0)
656
- .map(t => t.id)
657
- .reverse();
658
- const candidates =
659
- targetId == null
660
- ? []
661
- : [targetId, ...shownIds.filter(id => id !== targetId)];
662
- let target: HTMLElement | null = null;
663
- for (const id of candidates) {
664
- target = getFocusable(rowFor(id));
665
- if (target != null) {
666
- break;
667
- }
668
- }
669
- const prev = prevFocusRef.current;
670
- const isReturning = target == null && prev != null && prev.isConnected;
671
- if (target == null) {
672
- target = isReturning ? prev : el;
673
- }
674
- if (moveFocusLeftBehind(note, target) && isReturning) {
675
- prevFocusRef.current = null;
676
- }
677
- },
678
- [getFocusable],
679
- );
680
-
681
590
  // After a dismissed toast unmounts, hand focus off so it never falls to
682
591
  // <body>. Runs in a layout effect once the toast list no longer contains
683
592
  // the dismissed toast.
@@ -693,21 +602,27 @@ export function ToastViewport({
693
602
  }
694
603
  focusHandoffIdRef.current = null;
695
604
  pendingFocusRef.current = null;
696
- const note = dismissFocusNoteRef.current;
697
- dismissFocusNoteRef.current = null;
698
- handOffFocus(note, target === 'restore' ? null : target);
699
- }, [toasts, handOffFocus]);
700
-
701
- // After an overwrite updated a row that held focus: focus stays on the same
702
- // control, or, if the update removed it, moves as a dismissal hands it off.
703
- useLayoutEffect(() => {
704
- const overwrite = overwriteFocusRef.current;
705
- if (overwrite == null) {
706
- return;
605
+ const el = viewportRef.current;
606
+ if (target !== 'restore' && el) {
607
+ const nextNode = el.querySelector<HTMLElement>(
608
+ `[data-toast-id="${target}"]`,
609
+ );
610
+ const focusable = getFocusable(nextNode) ?? nextNode;
611
+ if (focusable) {
612
+ focusable.focus();
613
+ return;
614
+ }
615
+ }
616
+ // No remaining toast to receive focus — restore the previously-focused
617
+ // element if it's still connected, else fall back to the container.
618
+ const prev = prevFocusRef.current;
619
+ prevFocusRef.current = null;
620
+ if (prev && prev.isConnected) {
621
+ prev.focus();
622
+ } else if (el) {
623
+ el.focus();
707
624
  }
708
- overwriteFocusRef.current = null;
709
- handOffFocus(overwrite.note, overwrite.id);
710
- }, [toasts, handOffFocus]);
625
+ }, [toasts, getFocusable]);
711
626
 
712
627
  const findByUniqueID = useCallback((uid: string) => {
713
628
  return toastsRef.current.find(t => t.options.uniqueID === uid);
@@ -915,13 +830,12 @@ export function ToastViewport({
915
830
  <LayerContentBoundary>
916
831
  {visibleToasts.map(entry => (
917
832
  <ToastRow
918
- key={entry.rowKey ?? entry.id}
833
+ key={entry.id}
919
834
  entry={entry}
920
835
  isExiting={exitingIds.has(entry.id)}
921
836
  isReversed={isReversed}
922
837
  onExited={handleExited}
923
838
  onDismiss={removeToast}
924
- isLiveRegion={isModalHosted}
925
839
  />
926
840
  ))}
927
841
  </LayerContentBoundary>
@@ -38,10 +38,9 @@ export interface ToastOptions {
38
38
  /**
39
39
  * Replaces the content of this toast's card with your own layout.
40
40
  *
41
- * Astryx keeps the card and its auto-hide behavior (inside a
42
- * `ToastViewport` the viewport announces the toast, and while a modal hosts
43
- * the viewport the card announces itself), then hands the
44
- * renderer this toast's content, resolved settings and `dismiss` callback. The renderer owns every control inside its layout; call
41
+ * Astryx keeps the card, live-region role and auto-hide behavior, then hands
42
+ * the renderer this toast's content, resolved settings and `dismiss`
43
+ * callback. The renderer owns every control inside its layout; call
45
44
  * `dismiss` from the control that should close the toast. Astryx does not
46
45
  * inject a fallback control into custom content.
47
46
  *
@@ -101,9 +100,4 @@ export interface ToastEntry {
101
100
  id: string;
102
101
  options: ToastOptions;
103
102
  createdAt: number;
104
- /**
105
- * The row's identity in the stack. A uniqueID overwrite keeps the replaced
106
- * toast's, so the card updates in place and focus inside it stays.
107
- */
108
- rowKey?: string;
109
103
  }
@@ -110,18 +110,6 @@ export const docs = {
110
110
  description:
111
111
  'Render function for the anchor-positioned tooltip content. The positioning opt-out is excluded: the tooltip always derives its position from placement/alignment.',
112
112
  },
113
- {
114
- name: 'show',
115
- type: '() => void',
116
- description:
117
- 'Opens the tooltip now and keeps it open until hide(), Escape, a press outside, or focus leaving its trigger. Its text is announced once, and onShow fires. Under a controlled isOpen it only calls onShow. Does nothing while isEnabled is false.',
118
- },
119
- {
120
- name: 'hide',
121
- type: '() => void',
122
- description:
123
- 'Closes the tooltip now, and onHide fires. Under a controlled isOpen it only calls onHide.',
124
- },
125
113
  ],
126
114
  usage: {
127
115
  description:
@@ -137,11 +125,6 @@ export const docs = {
137
125
  description:
138
126
  'Prefer the Tooltip component for standard wrapping; use the hook when the trigger is not a simple child.',
139
127
  },
140
- {
141
- guidance: true,
142
- description:
143
- 'For a toggle-style trigger, have your own button call show() and hide() on each press. Give it aria-describedby and aria-controls set to describedBy, and aria-expanded (true while show() holds the tooltip, false after hide() or onHide), and keep its own accessible name. For an info icon, use InfoTip instead.',
144
- },
145
128
  {
146
129
  guidance: false,
147
130
  description:
@@ -184,8 +167,6 @@ export const docsDense = {
184
167
  anchorId: 'CSS anchor name.',
185
168
  describedBy: 'ID for aria-describedby.',
186
169
  renderTooltip: 'renders anchor-positioned tooltip content.',
187
- show: 'opens + holds until hide()/Escape/outside press/focus leave; announces once; onShow. Controlled: onShow only. No-op when disabled.',
188
- hide: 'closes; onHide. Controlled: onHide only.',
189
170
  },
190
171
  usage: {
191
172
  description:
@@ -201,11 +182,6 @@ export const docsDense = {
201
182
  description:
202
183
  'Prefer Tooltip for standard wrapping; use hook when trigger is not simple child.',
203
184
  },
204
- {
205
- guidance: true,
206
- description:
207
- 'Toggle-style trigger: own button calls show()/hide() per press, sets aria-describedby + aria-controls={describedBy}, aria-expanded (true while held), keeps its own name. Info icon: use InfoTip.',
208
- },
209
185
  {
210
186
  guidance: false,
211
187
  description:
@@ -5,8 +5,7 @@
5
5
  /**
6
6
  * @file useTooltip.tsx
7
7
  * @input Uses useLayer, usePressPin, React hooks
8
- * @output Exports useTooltip hook with stable trigger refs for hover/focus/tap tooltips
9
- * and imperative show() and hide() for an app-built toggle trigger,
8
+ * @output Exports useTooltip hook with stable trigger refs for hover/focus/tap tooltips,
10
9
  * and the module-only useTooltipWithTriggerBehavior that InfoTip's press-to-pin uses
11
10
  * @position Layer hook; builds on useLayer for tooltip behavior
12
11
  *
@@ -18,11 +17,9 @@ import {
18
17
  useCallback,
19
18
  useEffect,
20
19
  useRef,
21
- useState,
22
20
  type ReactNode,
23
21
  type RefCallback,
24
22
  } from 'react';
25
- import {createTriggerFocusGate} from '../utils/triggerFocus';
26
23
  import * as stylex from '@stylexjs/stylex';
27
24
  import {
28
25
  useLayerInternal,
@@ -219,25 +216,29 @@ export interface TooltipReturn {
219
216
  children: ReactNode,
220
217
  props?: Omit<ContextRenderProps, 'positioning'>,
221
218
  ) => ReactNode;
219
+ }
222
220
 
223
- /**
224
- * Opens the tooltip now and keeps it open until `hide()`, Escape, a press
225
- * outside, or focus leaving its trigger, as a pinned tooltip does. Its text
226
- * is announced once. Reports through `onShow`. Under a controlled `isOpen`,
227
- * it calls `onShow` and does not change `isOpen`. Does nothing while
228
- * `isEnabled` is false.
229
- *
230
- * Use `show()` and `hide()` to build a toggle-style trigger: your own button
231
- * calls them, sets `aria-describedby` and `aria-controls` to `describedBy`
232
- * and `aria-expanded` (true while shown), and keeps its own accessible name.
233
- */
234
- show: () => void;
235
-
236
- /**
237
- * Closes the tooltip now. Reports through `onHide`. Under a controlled
238
- * `isOpen`, it calls `onHide` and does not change `isOpen`.
239
- */
240
- hide: () => void;
221
+ /**
222
+ * Check if an element is naturally focusable
223
+ */
224
+ function isFocusable(element: HTMLElement): boolean {
225
+ // Elements with explicit tabindex
226
+ if (element.hasAttribute('tabindex')) {
227
+ return element.tabIndex >= 0;
228
+ }
229
+
230
+ // Naturally focusable elements
231
+ const focusableTags = ['A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA'];
232
+ if (focusableTags.includes(element.tagName)) {
233
+ return !(element as HTMLButtonElement).disabled;
234
+ }
235
+
236
+ // Elements with contenteditable
237
+ if (element.isContentEditable) {
238
+ return true;
239
+ }
240
+
241
+ return false;
241
242
  }
242
243
 
243
244
  /**
@@ -374,9 +375,6 @@ export function useTooltipWithTriggerBehavior(
374
375
  isHeldOpen: isPressHeldOpen,
375
376
  attach: attachPressPin,
376
377
  unpin: unpinPress,
377
- hold: holdPress,
378
- release: releasePress,
379
- isAppHeld,
380
378
  isPinShown,
381
379
  touch: {
382
380
  isTouchPointerRef,
@@ -487,13 +485,32 @@ export function useTooltipWithTriggerBehavior(
487
485
  [isTouchPointerRef, layerId, scheduleHide],
488
486
  );
489
487
 
490
- // Under `auto`, whether focus counts is decided when it arrives, and a
491
- // focus that counted still hides on blur (utils/triggerFocus.ts).
492
- const [focusGate] = useState(createTriggerFocusGate);
488
+ // Under `auto`, whether focus counts is decided when it arrives, not when
489
+ // the listeners attach: a roving-tabindex member that was `tabindex="-1"`
490
+ // then can be the tab stop now. A trigger that receives focus is focusable
491
+ // by definition; focus inside it counts only while the trigger is
492
+ // focusable too, so a plain wrapper still ignores its children's focus.
493
+ const isFocusForTrigger = useCallback(
494
+ (e: Event): boolean => {
495
+ if (focusTrigger === 'always') {
496
+ return true;
497
+ }
498
+ const triggerEl = e.currentTarget as HTMLElement | null;
499
+ return (
500
+ triggerEl != null && (e.target === triggerEl || isFocusable(triggerEl))
501
+ );
502
+ },
503
+ [focusTrigger],
504
+ );
505
+
506
+ // Whether the trigger's current focus counted when it arrived, so its blur
507
+ // hides the tooltip even if the trigger has since left the tab order.
508
+ const focusCountedRef = useRef(false);
493
509
 
494
510
  const handleFocusIn = useCallback(
495
511
  (e: Event) => {
496
- if (!focusGate.focusIn(e, focusTrigger) || !isEnabled) {
512
+ focusCountedRef.current = isFocusForTrigger(e);
513
+ if (!isEnabled || !focusCountedRef.current) {
497
514
  return;
498
515
  }
499
516
  // A tap focuses the trigger it activates, and `:focus-visible` does not
@@ -514,9 +531,8 @@ export function useTooltipWithTriggerBehavior(
514
531
  showLayer();
515
532
  },
516
533
  [
517
- focusGate,
518
- focusTrigger,
519
534
  isEnabled,
535
+ isFocusForTrigger,
520
536
  isTouchInteraction,
521
537
  clearTimeouts,
522
538
  showLayer,
@@ -528,12 +544,13 @@ export function useTooltipWithTriggerBehavior(
528
544
  // which a held tooltip skips.
529
545
  const handleFocusOut = useCallback(
530
546
  (e: Event) => {
531
- if (!focusGate.focusOut(e, focusTrigger)) {
547
+ if (!focusCountedRef.current && !isFocusForTrigger(e)) {
532
548
  return;
533
549
  }
550
+ focusCountedRef.current = false;
534
551
  scheduleHide();
535
552
  },
536
- [focusGate, focusTrigger, scheduleHide],
553
+ [isFocusForTrigger, scheduleHide],
537
554
  );
538
555
 
539
556
  // Pressing the trigger hides its own tooltip: once the control is activated
@@ -545,10 +562,9 @@ export function useTooltipWithTriggerBehavior(
545
562
  // tooltip at all), so the touch path answers it first.
546
563
  const handlePointerDown = useCallback(
547
564
  (event: PointerEvent) => {
548
- // A toggle trigger's press pins or closes through its click. The app's
549
- // hide() closes a held tooltip. Neither path hides here or waits for a
550
- // tap verdict.
551
- if (isToggleTrigger(triggerRef.current) || isAppHeld()) {
565
+ // A press on a toggle trigger pins or closes through its click. It
566
+ // neither hides the preview here nor waits for a tap verdict.
567
+ if (isToggleTrigger(triggerRef.current)) {
552
568
  return;
553
569
  }
554
570
  if (handleTouchPointerDown(event)) {
@@ -560,14 +576,7 @@ export function useTooltipWithTriggerBehavior(
560
576
  clearTimeouts();
561
577
  hideLayer();
562
578
  },
563
- [
564
- isToggleTrigger,
565
- isAppHeld,
566
- handleTouchPointerDown,
567
- isOpen,
568
- clearTimeouts,
569
- hideLayer,
570
- ],
579
+ [isToggleTrigger, handleTouchPointerDown, isOpen, clearTimeouts, hideLayer],
571
580
  );
572
581
 
573
582
  // Interaction ref that handles event listeners only
@@ -600,7 +609,7 @@ export function useTooltipWithTriggerBehavior(
600
609
  el.addEventListener('pointerdown', handlePointerDown);
601
610
 
602
611
  // Focus listeners unless focus never opens the tooltip; `auto` decides
603
- // per event (utils/triggerFocus.ts).
612
+ // per event (isFocusForTrigger).
604
613
  if (focusTrigger !== 'never') {
605
614
  el.addEventListener('focusin', handleFocusIn);
606
615
  el.addEventListener('focusout', handleFocusOut);
@@ -656,11 +665,9 @@ export function useTooltipWithTriggerBehavior(
656
665
  showLayer();
657
666
  } else {
658
667
  clearTimeouts();
659
- // A pin or app hold ends when the consumer closes the tooltip.
660
- unpinPress();
661
668
  hideLayer();
662
669
  }
663
- }, [isOpen, clearTimeouts, showLayer, hideLayer, unpinPress]);
670
+ }, [isOpen, clearTimeouts, showLayer, hideLayer]);
664
671
 
665
672
  // Dismiss on Escape (WCAG 1.4.13 — dismissible) through the shared layer
666
673
  // stack. A visible tip is the top-most layer, so it takes the press and
@@ -756,16 +763,6 @@ export function useTooltipWithTriggerBehavior(
756
763
  ],
757
764
  );
758
765
 
759
- const show = useCallback(() => {
760
- clearTimeouts();
761
- holdPress();
762
- }, [clearTimeouts, holdPress]);
763
-
764
- const hide = useCallback(() => {
765
- clearTimeouts();
766
- releasePress();
767
- }, [clearTimeouts, releasePress]);
768
-
769
766
  return {
770
767
  ref,
771
768
  positionRef: layerRef,
@@ -773,7 +770,5 @@ export function useTooltipWithTriggerBehavior(
773
770
  anchorId: layerAnchorId,
774
771
  describedBy: layerId,
775
772
  renderTooltip,
776
- show,
777
- hide,
778
773
  };
779
774
  }
@@ -43,8 +43,7 @@ export const docs = {
43
43
  {
44
44
  name: 'group',
45
45
  type: 'string',
46
- description:
47
- 'This prop has no effect. Set `item.auxiliaryData.group` on search-source items to group results in Typeahead or BaseTypeahead.',
46
+ description: 'Group label for grouping items visually.',
48
47
  },
49
48
  ],
50
49
  };
@@ -80,8 +79,7 @@ export const docsZh = {
80
79
  {
81
80
  name: 'group',
82
81
  type: 'string',
83
- description:
84
- '此属性不影响渲染。要在 Typeahead 或 BaseTypeahead 中分组,请在搜索源返回的结果项上设置 `item.auxiliaryData.group`。',
82
+ description: '用于视觉分组的分组标签。',
85
83
  },
86
84
  ],
87
85
  };
@@ -96,7 +94,6 @@ export const docsDense = {
96
94
  icon: 'Icon/avatar before label.',
97
95
  description: 'Text below label.',
98
96
  isDisabled: 'Visually disabled.',
99
- group:
100
- 'No effect. Group results via `item.auxiliaryData.group` in the search source.',
97
+ group: 'Group label for visual grouping.',
101
98
  },
102
99
  };
@@ -54,8 +54,7 @@ export interface TypeaheadItemProps<
54
54
  isDisabled?: boolean;
55
55
 
56
56
  /**
57
- * This prop has no effect. Set `item.auxiliaryData.group` on search-source
58
- * items to group results in Typeahead or BaseTypeahead.
57
+ * Group label for grouping items visually.
59
58
  */
60
59
  group?: string;
61
60
  }
@@ -47,8 +47,7 @@
47
47
  */
48
48
 
49
49
  import {UIKIT_CONTROL, UIKIT_PAN, UIKIT_PRESS} from '../motion/uikit/gestures';
50
- // Importing the whole table bundles its curves into each press-tracking control.
51
- import {press as pressClocks} from '../motion/uikit/timing';
50
+ import {timing} from '../motion/uikit/timing';
52
51
  import {travel} from './geometry';
53
52
  import type {GestureRecognizer, SelectionLock} from './GestureRecognizer';
54
53
  import type {TapGestureRecognizer} from './TapGestureRecognizer';
@@ -155,7 +154,7 @@ export const PRESS_PROFILES = {
155
154
  // The shared press styles release every surface on the deselect clock
156
155
  // until a control runs its own release (UIButton's 250 ms), so a
157
156
  // painter's `fading` never outlasts the fade it shows.
158
- releaseMs: pressClocks.highlightOutMs,
157
+ releaseMs: timing.press.highlightOutMs,
159
158
  },
160
159
  /**
161
160
  * A table or collection cell: inside a scroll view it highlights after the
@@ -166,7 +165,7 @@ export const PRESS_PROFILES = {
166
165
  highlightDelay: 'inScrollers',
167
166
  cancelTravel: UIKIT_PAN.hysteresis.value,
168
167
  quickTapFlash: 0,
169
- releaseMs: pressClocks.highlightOutMs,
168
+ releaseMs: timing.press.highlightOutMs,
170
169
  },
171
170
  } as const satisfies Record<string, PressProfile>;
172
171
 
@@ -96,10 +96,6 @@ 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
-
103
99
  A recognizer created with `claimsTouches: false` is a watcher (a tab bar
104
100
  that minimizes while the page scrolls): it never claims, so its element gets
105
101
  only passive listeners and a scroll never waits for it; it prevents no other
@@ -178,9 +174,7 @@ innerPan.requireToFail(edgePan);
178
174
  ## Touches in the view: `createPressTracker`
179
175
 
180
176
  A pressable control's view-level touch handling, with `UIControl`
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):
177
+ semantics:
184
178
 
185
179
  - `touchDown`, `touchDragInside` / `touchDragOutside`, `touchDragEnter` /
186
180
  `touchDragExit`, `touchUpInside` / `touchUpOutside`, `touchCancel`.