@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.
- package/dist/AlertDialog/AlertDialog.d.ts +4 -1
- package/dist/AlertDialog/AlertDialog.d.ts.map +1 -1
- package/dist/AlertDialog/AlertDialog.js +36 -6
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +76 -6
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +23 -4
- package/dist/BottomSheet/bottomSheetStack.d.ts.map +1 -1
- package/dist/BottomSheet/bottomSheetStack.js +5 -4
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
- package/dist/BottomSheet/useSheetGestures.js +3 -1
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +47 -3
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +13 -2
- package/dist/ComplexSelector/ComplexSelector.d.ts +4 -1
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +14 -4
- package/dist/ContextMenu/ContextMenu.js +2 -2
- package/dist/DateInput/MonthScroller.d.ts.map +1 -1
- package/dist/DateInput/MonthScroller.js +13 -27
- package/dist/DateInput/Wheel.d.ts.map +1 -1
- package/dist/DateInput/Wheel.js +22 -41
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +7 -31
- package/dist/Field/Field.js +2 -2
- package/dist/HoverCard/HoverCard.d.ts +4 -3
- package/dist/HoverCard/HoverCard.d.ts.map +1 -1
- package/dist/HoverCard/HoverCard.js +27 -2
- package/dist/HoverCard/useHoverCard.d.ts +5 -4
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +17 -31
- package/dist/Icon/Icon.d.ts +5 -1
- package/dist/Icon/Icon.d.ts.map +1 -1
- package/dist/Icon/Icon.js +5 -2
- package/dist/Layer/layerAnimations.stylex.d.ts +7 -8
- package/dist/Layer/layerAnimations.stylex.d.ts.map +1 -1
- package/dist/Layer/layerAnimations.stylex.js +25 -20
- package/dist/Layer/usePressPin.d.ts +17 -5
- package/dist/Layer/usePressPin.d.ts.map +1 -1
- package/dist/Layer/usePressPin.js +62 -12
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +18 -0
- package/dist/PowerSearch/PowerSearch.d.ts +36 -3
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +522 -269
- package/dist/PowerSearch/PowerSearchBottomSheet.d.ts +100 -0
- package/dist/PowerSearch/PowerSearchBottomSheet.d.ts.map +1 -0
- package/dist/PowerSearch/PowerSearchBottomSheet.js +1913 -0
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts +32 -1
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +17 -6
- package/dist/PowerSearch/PowerSearchValueEditor.d.ts +38 -3
- package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchValueEditor.js +170 -61
- package/dist/PowerSearch/formatFilterValue.d.ts +7 -0
- package/dist/PowerSearch/formatFilterValue.d.ts.map +1 -1
- package/dist/PowerSearch/formatFilterValue.js +11 -1
- package/dist/PowerSearch/formatTokenValue.d.ts +28 -0
- package/dist/PowerSearch/formatTokenValue.d.ts.map +1 -0
- package/dist/PowerSearch/formatTokenValue.js +160 -0
- package/dist/PowerSearch/powerSearchSheetUtils.d.ts +65 -0
- package/dist/PowerSearch/powerSearchSheetUtils.d.ts.map +1 -0
- package/dist/PowerSearch/powerSearchSheetUtils.js +168 -0
- package/dist/RadioList/RadioList.d.ts.map +1 -1
- package/dist/RadioList/RadioList.js +3 -2
- package/dist/RadioList/RadioListItem.d.ts +1 -1
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +5 -3
- package/dist/SegmentedControl/useSegmentedSelectionMotion.d.ts.map +1 -1
- package/dist/SegmentedControl/useSegmentedSelectionMotion.js +3 -3
- package/dist/Slider/sliderTouchController.js +2 -2
- package/dist/Spinner/Spinner.d.ts +1 -1
- package/dist/Spinner/Spinner.d.ts.map +1 -1
- package/dist/Spinner/Spinner.js +60 -58
- package/dist/Switch/useSwitchTouch.js +4 -4
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +29 -8
- package/dist/TextInput/TextInput.d.ts +2 -2
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +10 -7
- package/dist/Toast/Toast.d.ts +11 -1
- package/dist/Toast/Toast.d.ts.map +1 -1
- package/dist/Toast/Toast.js +25 -15
- package/dist/Toast/ToastViewport.d.ts.map +1 -1
- package/dist/Toast/ToastViewport.js +94 -34
- package/dist/Toast/types.d.ts +9 -3
- package/dist/Toast/types.d.ts.map +1 -1
- package/dist/Token/Token.d.ts +2 -2
- package/dist/Token/Token.d.ts.map +1 -1
- package/dist/Token/Token.js +12 -4
- package/dist/Token/TokenNamingContext.d.ts +14 -0
- package/dist/Token/TokenNamingContext.d.ts.map +1 -0
- package/dist/Token/TokenNamingContext.js +23 -0
- package/dist/Tooltip/useTooltip.d.ts +19 -1
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +34 -53
- package/dist/Typeahead/TypeaheadItem.d.ts +2 -1
- package/dist/Typeahead/TypeaheadItem.d.ts.map +1 -1
- package/dist/animator/createAnimator.d.ts +98 -0
- package/dist/animator/createAnimator.d.ts.map +1 -0
- package/dist/animator/createAnimator.js +259 -0
- package/dist/astryx.css +22 -6
- package/dist/gestures/GestureHost.d.ts +27 -1
- package/dist/gestures/GestureHost.d.ts.map +1 -1
- package/dist/gestures/GestureHost.js +90 -8
- package/dist/gestures/PressTracker.d.ts.map +1 -1
- package/dist/gestures/PressTracker.js +4 -3
- package/dist/hooks/actionGroupTracker.d.ts +4 -1
- package/dist/hooks/actionGroupTracker.d.ts.map +1 -1
- package/dist/hooks/actionGroupTracker.js +13 -3
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/pressGroupContext.d.ts +6 -0
- package/dist/hooks/pressGroupContext.d.ts.map +1 -0
- package/dist/hooks/pressGroupContext.js +20 -0
- package/dist/hooks/useActionGroupTracking.d.ts +53 -0
- package/dist/hooks/useActionGroupTracking.d.ts.map +1 -0
- package/dist/hooks/useActionGroupTracking.js +174 -0
- package/dist/hooks/useAnimator.d.ts +41 -0
- package/dist/hooks/useAnimator.d.ts.map +1 -0
- package/dist/hooks/useAnimator.js +70 -0
- package/dist/hooks/useAttachment.d.ts +1 -1
- package/dist/hooks/useAttachment.js +1 -1
- package/dist/hooks/useInputStatusIcon.d.ts +1 -1
- package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
- package/dist/hooks/useInputStatusIcon.js +2 -2
- package/dist/hooks/useLongPress.d.ts +6 -2
- package/dist/hooks/useLongPress.d.ts.map +1 -1
- package/dist/hooks/useLongPress.js +95 -15
- package/dist/hooks/useMenuPress.d.ts.map +1 -1
- package/dist/hooks/useMenuPress.js +4 -2
- package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts +8 -0
- package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts.map +1 -0
- package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.js +42 -0
- package/dist/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.d.ts.map +1 -1
- package/dist/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.js +6 -0
- package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.d.ts +8 -0
- package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.d.ts.map +1 -0
- package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.js +14 -0
- package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/en.generated.js +30 -0
- package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/pseudo.generated.js +30 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/internal/indicatorPress.stylex.d.ts +20 -0
- package/dist/internal/indicatorPress.stylex.d.ts.map +1 -0
- package/dist/internal/indicatorPress.stylex.js +26 -0
- package/dist/motion/animator.d.ts +14 -14
- package/dist/motion/animator.d.ts.map +1 -1
- package/dist/motion/animator.js +41 -18
- package/dist/motion/reducedMotion.d.ts +24 -0
- package/dist/motion/reducedMotion.d.ts.map +1 -1
- package/dist/motion/reducedMotion.js +78 -1
- package/dist/motion/uikit/alert.d.ts +41 -0
- package/dist/motion/uikit/alert.d.ts.map +1 -0
- package/dist/motion/uikit/alert.js +43 -0
- package/dist/privateAttributes/pressGroupItem.stylex.d.ts +15 -0
- package/dist/privateAttributes/pressGroupItem.stylex.d.ts.map +1 -0
- package/dist/privateAttributes/pressGroupItem.stylex.js +23 -0
- package/dist/utils/clickGuard.d.ts +8 -1
- package/dist/utils/clickGuard.d.ts.map +1 -1
- package/dist/utils/clickGuard.js +24 -1
- package/dist/utils/dateParser.d.ts.map +1 -1
- package/dist/utils/dateParser.js +12 -0
- package/dist/utils/focusReturn.d.ts +15 -0
- package/dist/utils/focusReturn.d.ts.map +1 -1
- package/dist/utils/focusReturn.js +28 -0
- package/dist/utils/interactionOverlay.stylex.d.ts +3 -2
- package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
- package/dist/utils/interactionOverlay.stylex.js +3 -2
- package/dist/utils/pressFeedback.d.ts.map +1 -1
- package/dist/utils/pressFeedback.js +19 -6
- package/dist/utils/triggerFocus.d.ts +36 -0
- package/dist/utils/triggerFocus.d.ts.map +1 -0
- package/dist/utils/triggerFocus.js +65 -0
- package/locales/en.json +120 -0
- package/locales/pseudo.json +90 -0
- package/package.json +9 -4
- package/src/AlertDialog/AlertDialog.doc.mjs +2 -2
- package/src/AlertDialog/AlertDialog.tsx +37 -5
- package/src/BottomSheet/BottomSheet.tsx +85 -5
- package/src/BottomSheet/BottomSheetPanel.tsx +40 -5
- package/src/BottomSheet/bottomSheetStack.ts +8 -4
- package/src/BottomSheet/useSheetGestures.ts +3 -1
- package/src/Button/Button.tsx +63 -5
- package/src/CheckboxInput/CheckboxInput.doc.mjs +18 -0
- package/src/CheckboxInput/CheckboxInput.tsx +36 -9
- package/src/ComplexSelector/ComplexSelector.tsx +20 -5
- package/src/ContextMenu/ContextMenu.tsx +2 -2
- package/src/DateInput/MonthScroller.tsx +18 -22
- package/src/DateInput/Wheel.tsx +30 -32
- package/src/Dialog/Dialog.tsx +10 -31
- package/src/Field/Field.tsx +2 -2
- package/src/HoverCard/HoverCard.doc.mjs +4 -2
- package/src/HoverCard/HoverCard.tsx +35 -4
- package/src/HoverCard/useHoverCard.doc.mjs +178 -36
- package/src/HoverCard/useHoverCard.tsx +46 -52
- package/src/Icon/Icon.doc.mjs +49 -8
- package/src/Icon/Icon.tsx +15 -7
- package/src/Layer/layerAnimations.stylex.ts +24 -16
- package/src/Layer/usePressPin.ts +81 -15
- package/src/Lightbox/Lightbox.tsx +18 -0
- package/src/PowerSearch/PowerSearch.doc.mjs +54 -5
- package/src/PowerSearch/PowerSearch.tsx +734 -288
- package/src/PowerSearch/PowerSearchBottomSheet.tsx +2515 -0
- package/src/PowerSearch/PowerSearchEditPopover.tsx +28 -6
- package/src/PowerSearch/PowerSearchValueEditor.tsx +203 -38
- package/src/PowerSearch/formatFilterValue.ts +18 -1
- package/src/PowerSearch/formatTokenValue.ts +215 -0
- package/src/PowerSearch/powerSearchSheetUtils.ts +224 -0
- package/src/RadioList/RadioList.tsx +3 -2
- package/src/RadioList/RadioListItem.tsx +5 -3
- package/src/SegmentedControl/useSegmentedSelectionMotion.ts +6 -3
- package/src/Slider/sliderTouchController.ts +2 -2
- package/src/Spinner/Spinner.tsx +77 -63
- package/src/Switch/useSwitchTouch.ts +4 -4
- package/src/Table/BaseTable.tsx +46 -12
- package/src/TextInput/TextInput.tsx +11 -10
- package/src/Toast/Toast.doc.mjs +4 -3
- package/src/Toast/Toast.tsx +52 -32
- package/src/Toast/ToastViewport.tsx +121 -35
- package/src/Toast/types.ts +9 -3
- package/src/Token/Token.tsx +13 -4
- package/src/Token/TokenNamingContext.ts +28 -0
- package/src/Tooltip/useTooltip.doc.mjs +24 -0
- package/src/Tooltip/useTooltip.tsx +61 -56
- package/src/Typeahead/TypeaheadItem.doc.mjs +6 -3
- package/src/Typeahead/TypeaheadItem.tsx +2 -1
- package/src/animator/createAnimator.doc.mjs +153 -0
- package/src/animator/createAnimator.ts +324 -0
- package/src/gestures/GestureHost.ts +105 -10
- package/src/gestures/PressTracker.ts +4 -3
- package/src/gestures/README.md +7 -1
- package/src/hooks/actionGroupTracker.ts +20 -5
- package/src/hooks/index.ts +2 -0
- package/src/hooks/pressGroupContext.ts +21 -0
- package/src/hooks/useActionGroupTracking.ts +224 -0
- package/src/hooks/useAnimator.doc.mjs +109 -0
- package/src/hooks/useAnimator.ts +67 -0
- package/src/hooks/useAttachment.ts +1 -1
- package/src/hooks/useInputStatusIcon.tsx +3 -2
- package/src/hooks/useLongPress.doc.mjs +2 -2
- package/src/hooks/useLongPress.ts +136 -15
- package/src/hooks/useMenuPress.ts +8 -2
- package/src/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.ts +43 -0
- package/src/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.ts +6 -0
- package/src/i18n/generated-english/PowerSearch/formatTokenValue.generated.ts +15 -0
- package/src/i18n/generated-locales/en.generated.ts +41 -0
- package/src/i18n/generated-locales/pseudo.generated.ts +42 -0
- package/src/index.ts +13 -0
- package/src/internal/indicatorPress.stylex.ts +24 -0
- package/src/motion/README.md +8 -5
- package/src/motion/animator.ts +61 -29
- package/src/motion/reducedMotion.ts +79 -1
- package/src/motion/uikit/alert.ts +43 -0
- package/src/privateAttributes/pressGroupItem.stylex.ts +23 -0
- package/src/utils/clickGuard.ts +37 -1
- package/src/utils/dateParser.ts +16 -0
- package/src/utils/focusReturn.ts +30 -0
- package/src/utils/interactionOverlay.stylex.ts +3 -2
- package/src/utils/pressFeedback.ts +19 -6
- package/src/utils/triggerFocus.ts +80 -0
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
useMemo,
|
|
22
22
|
useRef,
|
|
23
23
|
useState,
|
|
24
|
+
useSyncExternalStore,
|
|
24
25
|
} from 'react';
|
|
25
26
|
import type {ReactNode} from 'react';
|
|
26
27
|
import {createPortal} from 'react-dom';
|
|
@@ -28,6 +29,11 @@ import * as stylex from '@stylexjs/stylex';
|
|
|
28
29
|
import {spacingVars, durationVars, easeVars} from '../theme/tokens.stylex';
|
|
29
30
|
import {mergeProps} from '../utils';
|
|
30
31
|
import {getNodeText} from '../utils/getNodeText';
|
|
32
|
+
import {
|
|
33
|
+
moveFocusLeftBehind,
|
|
34
|
+
noteFocusWithin,
|
|
35
|
+
type FocusWithin,
|
|
36
|
+
} from '../utils/focusReturn';
|
|
31
37
|
import {INTERACTIVE_SELECTORS} from '../hooks/useClickableContainer';
|
|
32
38
|
import {useAnnounce} from '../hooks/useAnnounce';
|
|
33
39
|
import {layerTextReset} from '../Layer/layerTextReset.stylex';
|
|
@@ -224,6 +230,8 @@ interface ToastRowProps {
|
|
|
224
230
|
isReversed: boolean;
|
|
225
231
|
onExited: (id: string) => void;
|
|
226
232
|
onDismiss: (id: string, reason: ToastDismissReason) => void;
|
|
233
|
+
/** Whether the card carries its own live region (see ToastViewport). */
|
|
234
|
+
isLiveRegion: boolean;
|
|
227
235
|
}
|
|
228
236
|
|
|
229
237
|
/**
|
|
@@ -246,6 +254,7 @@ function ToastRow({
|
|
|
246
254
|
isReversed,
|
|
247
255
|
onExited,
|
|
248
256
|
onDismiss,
|
|
257
|
+
isLiveRegion,
|
|
249
258
|
}: ToastRowProps) {
|
|
250
259
|
const o = entry.options;
|
|
251
260
|
const type = o.type ?? 'info';
|
|
@@ -296,6 +305,8 @@ function ToastRow({
|
|
|
296
305
|
gestureDirection={isReversed ? -1 : 1}
|
|
297
306
|
onDismiss={reason => onDismiss(entry.id, reason)}
|
|
298
307
|
renderContent={o.renderContent}
|
|
308
|
+
timerKey={entry.id}
|
|
309
|
+
isLiveRegion={isLiveRegion}
|
|
299
310
|
/>
|
|
300
311
|
</div>
|
|
301
312
|
</div>
|
|
@@ -303,6 +314,9 @@ function ToastRow({
|
|
|
303
314
|
}
|
|
304
315
|
ToastRow.displayName = 'ToastRow';
|
|
305
316
|
|
|
317
|
+
const hasActiveModalOutlet = (): boolean => getActiveModalOutlet() != null;
|
|
318
|
+
const hasNoModalOutlet = (): boolean => false;
|
|
319
|
+
|
|
306
320
|
/**
|
|
307
321
|
* Container that renders and manages toast notifications. Place at the root
|
|
308
322
|
* of your app to enable useToast(). Toasts stack with enter/exit
|
|
@@ -363,6 +377,13 @@ export function ToastViewport({
|
|
|
363
377
|
// The next toast id that should receive focus once the dismissed toast has
|
|
364
378
|
// unmounted, or 'restore' to fall back to the previously-focused element.
|
|
365
379
|
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);
|
|
366
387
|
|
|
367
388
|
// Collect a focusable control within a toast node, if any.
|
|
368
389
|
// Reuses the canonical INTERACTIVE_SELECTORS list (native controls plus
|
|
@@ -392,13 +413,9 @@ export function ToastViewport({
|
|
|
392
413
|
[],
|
|
393
414
|
);
|
|
394
415
|
|
|
395
|
-
// Announce toasts through the persistent singleton live regions.
|
|
396
|
-
//
|
|
397
|
-
//
|
|
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
|
|
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
|
|
402
419
|
// dispatch path invoked once per useToast() call from an event handler,
|
|
403
420
|
// never from render — so each toast is announced exactly once by
|
|
404
421
|
// construction, independent of the React render lifecycle (StrictMode
|
|
@@ -406,6 +423,16 @@ export function ToastViewport({
|
|
|
406
423
|
// re-renders never re-announce). It is client-only (addToast never runs
|
|
407
424
|
// during SSR), so it is SSR-safe.
|
|
408
425
|
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;
|
|
409
436
|
|
|
410
437
|
const addToast = useCallback(
|
|
411
438
|
(entry: ToastEntry) => {
|
|
@@ -441,9 +468,29 @@ export function ToastViewport({
|
|
|
441
468
|
if (overwritten && renderedToastsRef.current.includes(overwritten)) {
|
|
442
469
|
replacedRef.current.set(overwritten.id, overwritten);
|
|
443
470
|
}
|
|
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
|
+
}
|
|
444
491
|
const projected = overwritten
|
|
445
|
-
? committed.map(t => (t === overwritten ?
|
|
446
|
-
: [...committed,
|
|
492
|
+
? committed.map(t => (t === overwritten ? placed : t))
|
|
493
|
+
: [...committed, placed];
|
|
447
494
|
// `maxVisible={0}` shows every toast (FR7): nothing leaves the window.
|
|
448
495
|
const firstVisible = maxVisible > 0 ? projected.length - maxVisible : 0;
|
|
449
496
|
const cutIds = new Set(
|
|
@@ -470,7 +517,7 @@ export function ToastViewport({
|
|
|
470
517
|
}
|
|
471
518
|
}
|
|
472
519
|
}
|
|
473
|
-
pending.set(entry.id,
|
|
520
|
+
pending.set(entry.id, placed);
|
|
474
521
|
// An overwrite swaps a new entry, with a new id, into the replaced
|
|
475
522
|
// toast's place, so the old row leaves the DOM without ever being
|
|
476
523
|
// dismissed. Its settled id is dropped by the prune below, which covers
|
|
@@ -482,8 +529,8 @@ export function ToastViewport({
|
|
|
482
529
|
// overwrite into an append. An ignored collision already returned.
|
|
483
530
|
const kept = prev.filter(t => !cutIds.has(t.id));
|
|
484
531
|
return overwritten && kept.includes(overwritten)
|
|
485
|
-
? kept.map(t => (t === overwritten ?
|
|
486
|
-
: [...kept,
|
|
532
|
+
? kept.map(t => (t === overwritten ? placed : t))
|
|
533
|
+
: [...kept, placed];
|
|
487
534
|
});
|
|
488
535
|
},
|
|
489
536
|
[announce, maxVisible],
|
|
@@ -512,14 +559,23 @@ export function ToastViewport({
|
|
|
512
559
|
// focused before the user entered the viewport) rather than <body>.
|
|
513
560
|
const el = viewportRef.current;
|
|
514
561
|
const active = document.activeElement;
|
|
515
|
-
|
|
516
|
-
|
|
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);
|
|
517
572
|
if (
|
|
518
573
|
dismissedNode &&
|
|
519
574
|
active instanceof Node &&
|
|
520
575
|
dismissedNode.contains(active)
|
|
521
576
|
) {
|
|
522
577
|
focusHandoffIdRef.current = id;
|
|
578
|
+
dismissFocusNoteRef.current = noteFocusWithin(dismissedNode);
|
|
523
579
|
// Pick the toast to receive focus while the DOM is still intact: the
|
|
524
580
|
// nearest one after it, then before it, that shows once it is gone
|
|
525
581
|
// (FR10), else restore. The list may already run ahead of the render
|
|
@@ -587,6 +643,41 @@ export function ToastViewport({
|
|
|
587
643
|
setToasts(prev => prev.filter(t => t.id !== id));
|
|
588
644
|
}, []);
|
|
589
645
|
|
|
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
|
+
|
|
590
681
|
// After a dismissed toast unmounts, hand focus off so it never falls to
|
|
591
682
|
// <body>. Runs in a layout effect once the toast list no longer contains
|
|
592
683
|
// the dismissed toast.
|
|
@@ -602,27 +693,21 @@ export function ToastViewport({
|
|
|
602
693
|
}
|
|
603
694
|
focusHandoffIdRef.current = null;
|
|
604
695
|
pendingFocusRef.current = null;
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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();
|
|
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;
|
|
624
707
|
}
|
|
625
|
-
|
|
708
|
+
overwriteFocusRef.current = null;
|
|
709
|
+
handOffFocus(overwrite.note, overwrite.id);
|
|
710
|
+
}, [toasts, handOffFocus]);
|
|
626
711
|
|
|
627
712
|
const findByUniqueID = useCallback((uid: string) => {
|
|
628
713
|
return toastsRef.current.find(t => t.options.uniqueID === uid);
|
|
@@ -830,12 +915,13 @@ export function ToastViewport({
|
|
|
830
915
|
<LayerContentBoundary>
|
|
831
916
|
{visibleToasts.map(entry => (
|
|
832
917
|
<ToastRow
|
|
833
|
-
key={entry.id}
|
|
918
|
+
key={entry.rowKey ?? entry.id}
|
|
834
919
|
entry={entry}
|
|
835
920
|
isExiting={exitingIds.has(entry.id)}
|
|
836
921
|
isReversed={isReversed}
|
|
837
922
|
onExited={handleExited}
|
|
838
923
|
onDismiss={removeToast}
|
|
924
|
+
isLiveRegion={isModalHosted}
|
|
839
925
|
/>
|
|
840
926
|
))}
|
|
841
927
|
</LayerContentBoundary>
|
package/src/Toast/types.ts
CHANGED
|
@@ -38,9 +38,10 @@ 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
|
|
42
|
-
* the
|
|
43
|
-
*
|
|
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
|
|
44
45
|
* `dismiss` from the control that should close the toast. Astryx does not
|
|
45
46
|
* inject a fallback control into custom content.
|
|
46
47
|
*
|
|
@@ -100,4 +101,9 @@ export interface ToastEntry {
|
|
|
100
101
|
id: string;
|
|
101
102
|
options: ToastOptions;
|
|
102
103
|
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;
|
|
103
109
|
}
|
package/src/Token/Token.tsx
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* - /packages/cli/assets/templates/blocks/components/Token/ (showcase blocks)
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import type
|
|
20
|
+
import {use, type ReactNode} from 'react';
|
|
21
21
|
import * as stylex from '@stylexjs/stylex';
|
|
22
22
|
import {
|
|
23
23
|
colorVars,
|
|
@@ -41,6 +41,7 @@ import {interactionOverlayStyles} from '../utils/interactionOverlay.stylex';
|
|
|
41
41
|
import {usePressFeedback} from '../hooks/usePressFeedback';
|
|
42
42
|
import type {TokenColorMap} from './index';
|
|
43
43
|
import {disabledInteractionProps} from '../utils/disabledInteraction';
|
|
44
|
+
import {TokenNamingContext} from './TokenNamingContext';
|
|
44
45
|
import {
|
|
45
46
|
registerEnglishFallback,
|
|
46
47
|
useComponentTranslator,
|
|
@@ -309,6 +310,8 @@ export function Token({
|
|
|
309
310
|
'data-testid': testId,
|
|
310
311
|
'aria-labelledby': ariaLabelledBy,
|
|
311
312
|
'aria-describedby': ariaDescribedBy,
|
|
313
|
+
'aria-haspopup': ariaHasPopup,
|
|
314
|
+
'aria-expanded': ariaExpanded,
|
|
312
315
|
ref,
|
|
313
316
|
...rest
|
|
314
317
|
}: TokenProps) {
|
|
@@ -319,6 +322,7 @@ export function Token({
|
|
|
319
322
|
const t = useComponentTranslator();
|
|
320
323
|
const LinkComponent = useLinkComponent();
|
|
321
324
|
const role = useInteractiveRole({href, onClick, isDisabled});
|
|
325
|
+
const naming = use(TokenNamingContext);
|
|
322
326
|
|
|
323
327
|
// When role is 'button' via context (no explicit onClick), treat as
|
|
324
328
|
// if onClick was provided — the popover attaches its own handler.
|
|
@@ -327,7 +331,9 @@ export function Token({
|
|
|
327
331
|
const removeButton = onRemove != null && (
|
|
328
332
|
<button
|
|
329
333
|
type="button"
|
|
330
|
-
aria-label={t('@astryx.token.remove', {
|
|
334
|
+
aria-label={t('@astryx.token.remove', {
|
|
335
|
+
label: naming?.removeName ?? label,
|
|
336
|
+
})}
|
|
331
337
|
onClick={e => {
|
|
332
338
|
e.stopPropagation();
|
|
333
339
|
onRemove(e);
|
|
@@ -352,12 +358,14 @@ export function Token({
|
|
|
352
358
|
</>
|
|
353
359
|
);
|
|
354
360
|
|
|
355
|
-
// Naming inputs reach the element that owns the token's action:
|
|
356
|
-
// button of a clickable token, otherwise the root
|
|
361
|
+
// Naming and popup inputs reach the element that owns the token's action:
|
|
362
|
+
// the inner button of a clickable token, otherwise the root
|
|
357
363
|
// (architecture:public-component-api INV5).
|
|
358
364
|
const labellingProps = {
|
|
359
365
|
'aria-labelledby': ariaLabelledBy,
|
|
360
366
|
'aria-describedby': ariaDescribedBy,
|
|
367
|
+
'aria-haspopup': ariaHasPopup,
|
|
368
|
+
'aria-expanded': ariaExpanded,
|
|
361
369
|
};
|
|
362
370
|
|
|
363
371
|
const sharedProps = {
|
|
@@ -532,6 +540,7 @@ export function Token({
|
|
|
532
540
|
<button
|
|
533
541
|
type="button"
|
|
534
542
|
onClick={effectiveOnClick}
|
|
543
|
+
aria-label={ariaLabelledBy == null ? naming?.actionName : undefined}
|
|
535
544
|
{...labellingProps}
|
|
536
545
|
aria-disabled={isDisabled || undefined}
|
|
537
546
|
{...(isDisabled ? disabledInteractionProps() : null)}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file TokenNamingContext.ts
|
|
7
|
+
* @input Naming parts from a caller whose token value renders outside its label
|
|
8
|
+
* @output Internal context: the names of a token's action and remove button
|
|
9
|
+
* @position Internal to Core; not exported from the Token entry point
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {createContext} from 'react';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Names for a Token whose visible text runs past its label. PowerSearch
|
|
16
|
+
* provides them: its tokens show the value as `endContent`.
|
|
17
|
+
*
|
|
18
|
+
* - `actionName` names a clickable token's inner button in place of `label`.
|
|
19
|
+
* - `removeName` names the remove button in place of `label`. The remove word
|
|
20
|
+
* still comes from the catalog, so each locale keeps its word order.
|
|
21
|
+
*/
|
|
22
|
+
export interface TokenNaming {
|
|
23
|
+
actionName?: string;
|
|
24
|
+
removeName?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const TokenNamingContext = createContext<TokenNaming | null>(null);
|
|
28
|
+
TokenNamingContext.displayName = 'TokenNamingContext';
|
|
@@ -110,6 +110,18 @@ 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
|
+
},
|
|
113
125
|
],
|
|
114
126
|
usage: {
|
|
115
127
|
description:
|
|
@@ -125,6 +137,11 @@ export const docs = {
|
|
|
125
137
|
description:
|
|
126
138
|
'Prefer the Tooltip component for standard wrapping; use the hook when the trigger is not a simple child.',
|
|
127
139
|
},
|
|
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
|
+
},
|
|
128
145
|
{
|
|
129
146
|
guidance: false,
|
|
130
147
|
description:
|
|
@@ -167,6 +184,8 @@ export const docsDense = {
|
|
|
167
184
|
anchorId: 'CSS anchor name.',
|
|
168
185
|
describedBy: 'ID for aria-describedby.',
|
|
169
186
|
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.',
|
|
170
189
|
},
|
|
171
190
|
usage: {
|
|
172
191
|
description:
|
|
@@ -182,6 +201,11 @@ export const docsDense = {
|
|
|
182
201
|
description:
|
|
183
202
|
'Prefer Tooltip for standard wrapping; use hook when trigger is not simple child.',
|
|
184
203
|
},
|
|
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
|
+
},
|
|
185
209
|
{
|
|
186
210
|
guidance: false,
|
|
187
211
|
description:
|
|
@@ -5,7 +5,8 @@
|
|
|
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
|
|
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,
|
|
9
10
|
* and the module-only useTooltipWithTriggerBehavior that InfoTip's press-to-pin uses
|
|
10
11
|
* @position Layer hook; builds on useLayer for tooltip behavior
|
|
11
12
|
*
|
|
@@ -17,9 +18,11 @@ import {
|
|
|
17
18
|
useCallback,
|
|
18
19
|
useEffect,
|
|
19
20
|
useRef,
|
|
21
|
+
useState,
|
|
20
22
|
type ReactNode,
|
|
21
23
|
type RefCallback,
|
|
22
24
|
} from 'react';
|
|
25
|
+
import {createTriggerFocusGate} from '../utils/triggerFocus';
|
|
23
26
|
import * as stylex from '@stylexjs/stylex';
|
|
24
27
|
import {
|
|
25
28
|
useLayerInternal,
|
|
@@ -216,29 +219,25 @@ export interface TooltipReturn {
|
|
|
216
219
|
children: ReactNode,
|
|
217
220
|
props?: Omit<ContextRenderProps, 'positioning'>,
|
|
218
221
|
) => ReactNode;
|
|
219
|
-
}
|
|
220
222
|
|
|
221
|
-
/**
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return false;
|
|
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;
|
|
242
241
|
}
|
|
243
242
|
|
|
244
243
|
/**
|
|
@@ -375,6 +374,9 @@ export function useTooltipWithTriggerBehavior(
|
|
|
375
374
|
isHeldOpen: isPressHeldOpen,
|
|
376
375
|
attach: attachPressPin,
|
|
377
376
|
unpin: unpinPress,
|
|
377
|
+
hold: holdPress,
|
|
378
|
+
release: releasePress,
|
|
379
|
+
isAppHeld,
|
|
378
380
|
isPinShown,
|
|
379
381
|
touch: {
|
|
380
382
|
isTouchPointerRef,
|
|
@@ -485,32 +487,13 @@ export function useTooltipWithTriggerBehavior(
|
|
|
485
487
|
[isTouchPointerRef, layerId, scheduleHide],
|
|
486
488
|
);
|
|
487
489
|
|
|
488
|
-
// Under `auto`, whether focus counts is decided when it arrives,
|
|
489
|
-
//
|
|
490
|
-
|
|
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);
|
|
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);
|
|
509
493
|
|
|
510
494
|
const handleFocusIn = useCallback(
|
|
511
495
|
(e: Event) => {
|
|
512
|
-
|
|
513
|
-
if (!isEnabled || !focusCountedRef.current) {
|
|
496
|
+
if (!focusGate.focusIn(e, focusTrigger) || !isEnabled) {
|
|
514
497
|
return;
|
|
515
498
|
}
|
|
516
499
|
// A tap focuses the trigger it activates, and `:focus-visible` does not
|
|
@@ -531,8 +514,9 @@ export function useTooltipWithTriggerBehavior(
|
|
|
531
514
|
showLayer();
|
|
532
515
|
},
|
|
533
516
|
[
|
|
517
|
+
focusGate,
|
|
518
|
+
focusTrigger,
|
|
534
519
|
isEnabled,
|
|
535
|
-
isFocusForTrigger,
|
|
536
520
|
isTouchInteraction,
|
|
537
521
|
clearTimeouts,
|
|
538
522
|
showLayer,
|
|
@@ -544,13 +528,12 @@ export function useTooltipWithTriggerBehavior(
|
|
|
544
528
|
// which a held tooltip skips.
|
|
545
529
|
const handleFocusOut = useCallback(
|
|
546
530
|
(e: Event) => {
|
|
547
|
-
if (!
|
|
531
|
+
if (!focusGate.focusOut(e, focusTrigger)) {
|
|
548
532
|
return;
|
|
549
533
|
}
|
|
550
|
-
focusCountedRef.current = false;
|
|
551
534
|
scheduleHide();
|
|
552
535
|
},
|
|
553
|
-
[
|
|
536
|
+
[focusGate, focusTrigger, scheduleHide],
|
|
554
537
|
);
|
|
555
538
|
|
|
556
539
|
// Pressing the trigger hides its own tooltip: once the control is activated
|
|
@@ -562,9 +545,10 @@ export function useTooltipWithTriggerBehavior(
|
|
|
562
545
|
// tooltip at all), so the touch path answers it first.
|
|
563
546
|
const handlePointerDown = useCallback(
|
|
564
547
|
(event: PointerEvent) => {
|
|
565
|
-
// A
|
|
566
|
-
//
|
|
567
|
-
|
|
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()) {
|
|
568
552
|
return;
|
|
569
553
|
}
|
|
570
554
|
if (handleTouchPointerDown(event)) {
|
|
@@ -576,7 +560,14 @@ export function useTooltipWithTriggerBehavior(
|
|
|
576
560
|
clearTimeouts();
|
|
577
561
|
hideLayer();
|
|
578
562
|
},
|
|
579
|
-
[
|
|
563
|
+
[
|
|
564
|
+
isToggleTrigger,
|
|
565
|
+
isAppHeld,
|
|
566
|
+
handleTouchPointerDown,
|
|
567
|
+
isOpen,
|
|
568
|
+
clearTimeouts,
|
|
569
|
+
hideLayer,
|
|
570
|
+
],
|
|
580
571
|
);
|
|
581
572
|
|
|
582
573
|
// Interaction ref that handles event listeners only
|
|
@@ -609,7 +600,7 @@ export function useTooltipWithTriggerBehavior(
|
|
|
609
600
|
el.addEventListener('pointerdown', handlePointerDown);
|
|
610
601
|
|
|
611
602
|
// Focus listeners unless focus never opens the tooltip; `auto` decides
|
|
612
|
-
// per event (
|
|
603
|
+
// per event (utils/triggerFocus.ts).
|
|
613
604
|
if (focusTrigger !== 'never') {
|
|
614
605
|
el.addEventListener('focusin', handleFocusIn);
|
|
615
606
|
el.addEventListener('focusout', handleFocusOut);
|
|
@@ -665,9 +656,11 @@ export function useTooltipWithTriggerBehavior(
|
|
|
665
656
|
showLayer();
|
|
666
657
|
} else {
|
|
667
658
|
clearTimeouts();
|
|
659
|
+
// A pin or app hold ends when the consumer closes the tooltip.
|
|
660
|
+
unpinPress();
|
|
668
661
|
hideLayer();
|
|
669
662
|
}
|
|
670
|
-
}, [isOpen, clearTimeouts, showLayer, hideLayer]);
|
|
663
|
+
}, [isOpen, clearTimeouts, showLayer, hideLayer, unpinPress]);
|
|
671
664
|
|
|
672
665
|
// Dismiss on Escape (WCAG 1.4.13 — dismissible) through the shared layer
|
|
673
666
|
// stack. A visible tip is the top-most layer, so it takes the press and
|
|
@@ -763,6 +756,16 @@ export function useTooltipWithTriggerBehavior(
|
|
|
763
756
|
],
|
|
764
757
|
);
|
|
765
758
|
|
|
759
|
+
const show = useCallback(() => {
|
|
760
|
+
clearTimeouts();
|
|
761
|
+
holdPress();
|
|
762
|
+
}, [clearTimeouts, holdPress]);
|
|
763
|
+
|
|
764
|
+
const hide = useCallback(() => {
|
|
765
|
+
clearTimeouts();
|
|
766
|
+
releasePress();
|
|
767
|
+
}, [clearTimeouts, releasePress]);
|
|
768
|
+
|
|
766
769
|
return {
|
|
767
770
|
ref,
|
|
768
771
|
positionRef: layerRef,
|
|
@@ -770,5 +773,7 @@ export function useTooltipWithTriggerBehavior(
|
|
|
770
773
|
anchorId: layerAnchorId,
|
|
771
774
|
describedBy: layerId,
|
|
772
775
|
renderTooltip,
|
|
776
|
+
show,
|
|
777
|
+
hide,
|
|
773
778
|
};
|
|
774
779
|
}
|