@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
|
@@ -16,9 +16,11 @@ import {
|
|
|
16
16
|
useCallback,
|
|
17
17
|
useEffect,
|
|
18
18
|
useRef,
|
|
19
|
+
useState,
|
|
19
20
|
type ReactNode,
|
|
20
21
|
type RefCallback,
|
|
21
22
|
} from 'react';
|
|
23
|
+
import {createTriggerFocusGate} from '../utils/triggerFocus';
|
|
22
24
|
import * as stylex from '@stylexjs/stylex';
|
|
23
25
|
import {
|
|
24
26
|
useLayer,
|
|
@@ -60,7 +62,7 @@ const styles = stylex.create({
|
|
|
60
62
|
});
|
|
61
63
|
|
|
62
64
|
/**
|
|
63
|
-
*
|
|
65
|
+
* When focus on the trigger opens the hover card
|
|
64
66
|
*/
|
|
65
67
|
export type HoverCardFocusTrigger = 'auto' | 'always' | 'never';
|
|
66
68
|
|
|
@@ -96,9 +98,10 @@ export interface HoverCardOptions {
|
|
|
96
98
|
|
|
97
99
|
/**
|
|
98
100
|
* When to trigger on focus:
|
|
99
|
-
* - `auto`:
|
|
100
|
-
*
|
|
101
|
-
* - `
|
|
101
|
+
* - `auto`: focus on the trigger itself, or within a trigger that is
|
|
102
|
+
* focusable when focus arrives
|
|
103
|
+
* - `always`: focus on the trigger or anything inside it
|
|
104
|
+
* - `never`: never on focus (for composite widgets)
|
|
102
105
|
*
|
|
103
106
|
* @default 'auto'
|
|
104
107
|
*/
|
|
@@ -235,29 +238,6 @@ export interface HoverCardReturn {
|
|
|
235
238
|
hide: () => void;
|
|
236
239
|
}
|
|
237
240
|
|
|
238
|
-
/**
|
|
239
|
-
* Check if an element is naturally focusable
|
|
240
|
-
*/
|
|
241
|
-
function isFocusable(element: HTMLElement): boolean {
|
|
242
|
-
// Elements with explicit tabindex
|
|
243
|
-
if (element.hasAttribute('tabindex')) {
|
|
244
|
-
return element.tabIndex >= 0;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Naturally focusable elements
|
|
248
|
-
const focusableTags = ['A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA'];
|
|
249
|
-
if (focusableTags.includes(element.tagName)) {
|
|
250
|
-
return !(element as HTMLButtonElement).disabled;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Elements with contenteditable
|
|
254
|
-
if (element.isContentEditable) {
|
|
255
|
-
return true;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return false;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
241
|
/**
|
|
262
242
|
* Hook for hover card behavior with hover/focus triggers.
|
|
263
243
|
*
|
|
@@ -426,27 +406,44 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
|
|
|
426
406
|
[handleTouchPointerDown],
|
|
427
407
|
);
|
|
428
408
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
409
|
+
// Under `auto`, whether focus counts is decided when it arrives, and a
|
|
410
|
+
// focus that counted still hides on blur (utils/triggerFocus.ts).
|
|
411
|
+
const [focusGate] = useState(createTriggerFocusGate);
|
|
412
|
+
|
|
413
|
+
const handleFocusIn = useCallback(
|
|
414
|
+
(e: Event) => {
|
|
415
|
+
if (!focusGate.focusIn(e, focusTrigger) || !isEnabled) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
// A tap focuses the trigger it activates. Opening on that focus would
|
|
419
|
+
// reinstate exactly the behavior the touch path just decided against —
|
|
420
|
+
// and on an action trigger it covers the thing the user tapped.
|
|
421
|
+
if (isTouchInteraction()) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
// Skip showing if we're in the middle of an Escape dismiss
|
|
425
|
+
if (isEscapeDismissingRef.current) {
|
|
426
|
+
isEscapeDismissingRef.current = false;
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
clearTimeouts();
|
|
430
|
+
showLayer();
|
|
431
|
+
},
|
|
432
|
+
[
|
|
433
|
+
focusGate,
|
|
434
|
+
focusTrigger,
|
|
435
|
+
isEnabled,
|
|
436
|
+
isTouchInteraction,
|
|
437
|
+
clearTimeouts,
|
|
438
|
+
showLayer,
|
|
439
|
+
],
|
|
440
|
+
);
|
|
447
441
|
|
|
448
442
|
const handleFocusOut = useCallback(
|
|
449
443
|
(e: FocusEvent) => {
|
|
444
|
+
if (!focusGate.focusOut(e, focusTrigger)) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
450
447
|
// Check if focus is moving to the hover card content
|
|
451
448
|
const relatedTarget = e.relatedTarget as HTMLElement | null;
|
|
452
449
|
const popoverElement = document.getElementById(layerId);
|
|
@@ -458,7 +455,7 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
|
|
|
458
455
|
|
|
459
456
|
scheduleHide();
|
|
460
457
|
},
|
|
461
|
-
[layerId, scheduleHide],
|
|
458
|
+
[focusGate, focusTrigger, layerId, scheduleHide],
|
|
462
459
|
);
|
|
463
460
|
|
|
464
461
|
// Escape dismissal (WCAG 1.4.13) goes through the shared layer stack: a
|
|
@@ -558,12 +555,9 @@ export function useHoverCard(options: HoverCardOptions = {}): HoverCardReturn {
|
|
|
558
555
|
el.addEventListener('mouseleave', handleMouseLeave);
|
|
559
556
|
el.addEventListener('pointerdown', handlePointerDown);
|
|
560
557
|
|
|
561
|
-
//
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
(focusTrigger === 'auto' && isFocusable(el));
|
|
565
|
-
|
|
566
|
-
if (shouldAttachFocus) {
|
|
558
|
+
// Focus listeners unless focus never opens the card; `auto` decides
|
|
559
|
+
// per event (utils/triggerFocus.ts).
|
|
560
|
+
if (focusTrigger !== 'never') {
|
|
567
561
|
el.addEventListener('focusin', handleFocusIn);
|
|
568
562
|
el.addEventListener('focusout', handleFocusOut as EventListener);
|
|
569
563
|
}
|
package/src/Icon/Icon.doc.mjs
CHANGED
|
@@ -71,15 +71,16 @@ export const docs = {
|
|
|
71
71
|
props: [
|
|
72
72
|
{
|
|
73
73
|
name: 'icon',
|
|
74
|
-
type: 'IconName | ComponentType<SVGProps>',
|
|
74
|
+
type: 'IconName | NamespacedIconName | ComponentType<SVGProps>',
|
|
75
75
|
description:
|
|
76
|
-
'Semantic icon name or SVG component. Valid semantic names: close, chevronDown, chevronLeft, chevronRight, chevronsLeft, chevronsRight, check, success, error, warning, info, calendar, clock, externalLink, menu, moreHorizontal, search, upload, arrowUp, arrowDown, arrowsUpDown, funnel, eyeSlash, viewColumns, copy, checkDouble, wrench, stop, microphone. For any icon not in this list, pass an SVG component directly (e.g. import from lucide-react or @heroicons/react). Note: this prop is called `icon`, not `name`.',
|
|
76
|
+
'Semantic icon name or SVG component. Valid semantic names: close, chevronDown, chevronLeft, chevronRight, chevronsLeft, chevronsRight, check, success, error, warning, info, calendar, clock, externalLink, menu, moreHorizontal, search, upload, arrowUp, arrowDown, arrowsUpDown, funnel, eyeSlash, viewColumns, copy, checkDouble, wrench, stop, microphone. A namespaced key such as `richtext:bold` names a glyph that one component or library registers, and a theme can replace it by key; a namespaced key that nothing registers renders nothing. For any icon not in this list, pass an SVG component directly (e.g. import from lucide-react or @heroicons/react). Note: this prop is called `icon`, not `name`.',
|
|
77
77
|
required: true,
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
80
|
name: 'color',
|
|
81
81
|
type: "'primary' | 'secondary' | 'tertiary' | 'disabled' | 'accent' | 'info' | 'success' | 'error' | 'warning' | 'inherit'",
|
|
82
|
-
description:
|
|
82
|
+
description:
|
|
83
|
+
'Color variant mapped to Astryx icon color tokens. Semantic values (primary through warning) follow the role, and inherit uses the surrounding text color. On the web, Icon also accepts the palette values blue, red, green, gray, cyan, teal, yellow, orange, pink, and purple, which pick a non-semantic hue.',
|
|
83
84
|
default: "'inherit'",
|
|
84
85
|
},
|
|
85
86
|
{
|
|
@@ -107,6 +108,24 @@ export const docs = {
|
|
|
107
108
|
description:
|
|
108
109
|
"StyleX styles for customization (color, size, opacity). Folded into the icon's own stylex.props() call so it composes with the base color/size styles. Must be a stylex.create() value, not an inline style object like style={{}}.",
|
|
109
110
|
},
|
|
111
|
+
{
|
|
112
|
+
name: 'className',
|
|
113
|
+
type: 'string',
|
|
114
|
+
description:
|
|
115
|
+
'Class name added to the rendered glyph, beside the astryx-icon target class.',
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'style',
|
|
119
|
+
type: 'CSSProperties',
|
|
120
|
+
description:
|
|
121
|
+
"Inline styles merged onto the rendered glyph after Icon's own size and color styles.",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'ref',
|
|
125
|
+
type: 'Ref<SVGSVGElement>',
|
|
126
|
+
description:
|
|
127
|
+
'Ref to the rendered SVG element when icon is an icon component. A semantic or namespaced name renders inside an internal wrapper, and the ref is not attached.',
|
|
128
|
+
},
|
|
110
129
|
],
|
|
111
130
|
theming: {
|
|
112
131
|
targets: [{className: 'astryx-icon', visualProps: ['color', 'size']}],
|
|
@@ -207,15 +226,16 @@ export const docsZh = {
|
|
|
207
226
|
props: [
|
|
208
227
|
{
|
|
209
228
|
name: 'icon',
|
|
210
|
-
type: 'IconName | ComponentType<SVGProps>',
|
|
229
|
+
type: 'IconName | NamespacedIconName | ComponentType<SVGProps>',
|
|
211
230
|
description:
|
|
212
|
-
'语义图标名称或 SVG 组件。有效语义名称:close, chevronDown, chevronLeft, chevronRight, chevronsLeft, chevronsRight, check, success, error, warning, info, calendar, clock, externalLink, menu, moreHorizontal, search, upload, arrowUp, arrowDown, arrowsUpDown, funnel, eyeSlash, viewColumns, copy, checkDouble, wrench, stop, microphone
|
|
231
|
+
'语义图标名称或 SVG 组件。有效语义名称:close, chevronDown, chevronLeft, chevronRight, chevronsLeft, chevronsRight, check, success, error, warning, info, calendar, clock, externalLink, menu, moreHorizontal, search, upload, arrowUp, arrowDown, arrowsUpDown, funnel, eyeSlash, viewColumns, copy, checkDouble, wrench, stop, microphone。命名空间键(如 `richtext:bold`)指向某个组件或库注册的图标,主题可以按键替换它;没有注册的命名空间键不渲染任何内容。列表之外的图标请直接传入 SVG 组件。',
|
|
213
232
|
required: true,
|
|
214
233
|
},
|
|
215
234
|
{
|
|
216
235
|
name: 'color',
|
|
217
236
|
type: "'primary' | 'secondary' | 'tertiary' | 'disabled' | 'accent' | 'info' | 'success' | 'error' | 'warning' | 'inherit'",
|
|
218
|
-
description:
|
|
237
|
+
description:
|
|
238
|
+
'映射到 Astryx 图标颜色令牌的颜色变体。语义值(primary 到 warning)跟随角色,inherit 使用周围文字的颜色。在 Web 上,Icon 还接受调色板值 blue、red、green、gray、cyan、teal、yellow、orange、pink 和 purple,它们选择非语义色相。',
|
|
219
239
|
default: "'inherit'",
|
|
220
240
|
},
|
|
221
241
|
{
|
|
@@ -243,6 +263,23 @@ export const docsZh = {
|
|
|
243
263
|
description:
|
|
244
264
|
'用于自定义的 StyleX 样式(颜色、尺寸、不透明度)。会并入图标自身的 stylex.props() 调用,从而与基础的颜色/尺寸样式组合。必须是 stylex.create() 的值,而不是像 style={{}} 这样的内联样式对象。',
|
|
245
265
|
},
|
|
266
|
+
{
|
|
267
|
+
name: 'className',
|
|
268
|
+
type: 'string',
|
|
269
|
+
description: '添加到渲染图标上的类名,与 astryx-icon 目标类名并列。',
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: 'style',
|
|
273
|
+
type: 'CSSProperties',
|
|
274
|
+
description:
|
|
275
|
+
'合并到渲染图标上的内联样式,位于 Icon 自身的尺寸和颜色样式之后。',
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: 'ref',
|
|
279
|
+
type: 'Ref<SVGSVGElement>',
|
|
280
|
+
description:
|
|
281
|
+
'icon 为图标组件时,指向渲染的 SVG 元素的 ref。语义名称或命名空间名称在内部包装元素中渲染,不会附加 ref。',
|
|
282
|
+
},
|
|
246
283
|
],
|
|
247
284
|
theming: {
|
|
248
285
|
targets: [{className: 'astryx-icon', visualProps: ['color', 'size']}],
|
|
@@ -390,8 +427,9 @@ export const docsDense = {
|
|
|
390
427
|
],
|
|
391
428
|
},
|
|
392
429
|
propDescriptions: {
|
|
393
|
-
icon: 'Semantic icon name or SVG component. Valid names: close, chevronDown, chevronLeft, chevronRight, chevronsLeft, chevronsRight, check, success, error, warning, info, calendar, clock, externalLink, menu, moreHorizontal, search, upload, arrowUp, arrowDown, arrowsUpDown, funnel, eyeSlash, viewColumns, copy, checkDouble, wrench, stop, microphone. For others, pass an SVG component.',
|
|
394
|
-
color:
|
|
430
|
+
icon: 'Semantic icon name or SVG component. Valid names: close, chevronDown, chevronLeft, chevronRight, chevronsLeft, chevronsRight, check, success, error, warning, info, calendar, clock, externalLink, menu, moreHorizontal, search, upload, arrowUp, arrowDown, arrowsUpDown, funnel, eyeSlash, viewColumns, copy, checkDouble, wrench, stop, microphone. A namespaced key (`richtext:bold`) names a glyph a component or library registers; an unregistered one renders nothing. For others, pass an SVG component.',
|
|
431
|
+
color:
|
|
432
|
+
'Color variant mapped to Astryx icon color tokens; inherit = surrounding text color. Web also accepts the non-semantic palette: blue, red, green, gray, cyan, teal, yellow, orange, pink, purple.',
|
|
395
433
|
size: "xsm/sm/md/lg = 12/16/20/24px at a 16px root, scale w/ root font size; theme sets a size's glyph dimension via width + height (px or rem) on components.icon['size:sm'] etc. Explicit Icon size; otherwise nearest owning-component default, then 'md' when no contextual default exists",
|
|
396
434
|
variant:
|
|
397
435
|
"Requests the active theme's alternate artwork (e.g. 'filled'); names from augmentable IconVariantMap. Reaches the entry only if the active theme's iconVariants lists it (and an inherited entry's defining theme does too); else normal artwork. Ignored by a directly passed component; never in the DOM; no theming selector.",
|
|
@@ -399,5 +437,8 @@ export const docsDense = {
|
|
|
399
437
|
'Accessible name for a meaningful, standalone icon. Sets role="img" + aria-label and drops the default aria-hidden. Omit (default) for decorative icons (stays aria-hidden). Empty string = decorative. The accessible-name/alt-text prop for icons.',
|
|
400
438
|
xstyle:
|
|
401
439
|
'StyleX styles (color, size, opacity) via stylex.create(); composes with the base color/size styles.',
|
|
440
|
+
className: 'Class added to the rendered glyph beside astryx-icon.',
|
|
441
|
+
style: "Inline styles merged after Icon's size and color styles.",
|
|
442
|
+
ref: 'Reaches the SVG only when icon is a component; a semantic or namespaced name renders in a wrapper and gets no ref.',
|
|
402
443
|
},
|
|
403
444
|
};
|
package/src/Icon/Icon.tsx
CHANGED
|
@@ -176,7 +176,11 @@ export interface IconProps extends Omit<
|
|
|
176
176
|
SVGProps<SVGSVGElement>,
|
|
177
177
|
'ref' | 'color'
|
|
178
178
|
> {
|
|
179
|
-
/**
|
|
179
|
+
/**
|
|
180
|
+
* Ref to the rendered SVG element when `icon` is an icon component. A
|
|
181
|
+
* semantic or namespaced name renders inside an internal wrapper, and the
|
|
182
|
+
* ref is not attached.
|
|
183
|
+
*/
|
|
180
184
|
ref?: React.Ref<SVGSVGElement>;
|
|
181
185
|
/**
|
|
182
186
|
* Icon to render. Can be:
|
|
@@ -267,22 +271,26 @@ export interface IconProps extends Omit<
|
|
|
267
271
|
xstyle?: StyleXStyles;
|
|
268
272
|
}
|
|
269
273
|
|
|
274
|
+
type IconA11yProps =
|
|
275
|
+
| {role: 'img'; 'aria-label': string; 'aria-hidden': undefined}
|
|
276
|
+
| {'aria-hidden': 'true'};
|
|
277
|
+
|
|
270
278
|
/**
|
|
271
279
|
* Derives the ARIA attributes for an icon from its `label` prop.
|
|
272
280
|
*
|
|
273
281
|
* - Non-empty `label` → meaningful image: `role="img"` + `aria-label`, and no
|
|
274
282
|
* `aria-hidden` (an `aria-hidden` element is removed from the accessibility
|
|
275
|
-
* tree, so its accessible name would be ignored).
|
|
283
|
+
* tree, so its accessible name would be ignored). Icon passes
|
|
284
|
+
* `aria-hidden: undefined`. Many icon components, such as Heroicons, write
|
|
285
|
+
* `aria-hidden="true"` unless a prop replaces it.
|
|
276
286
|
* - Omitted or empty `label` → decorative default: `aria-hidden="true"`.
|
|
277
287
|
*
|
|
278
288
|
* The result is spread BEFORE `{...props}` in both render modes so an explicit
|
|
279
289
|
* `aria-hidden` / `role` / `aria-label` from the consumer always wins.
|
|
280
290
|
*/
|
|
281
|
-
function getIconA11yProps(
|
|
282
|
-
label: string | undefined,
|
|
283
|
-
): {role: 'img'; 'aria-label': string} | {'aria-hidden': 'true'} {
|
|
291
|
+
function getIconA11yProps(label: string | undefined): IconA11yProps {
|
|
284
292
|
return label != null && label !== ''
|
|
285
|
-
? {role: 'img', 'aria-label': label}
|
|
293
|
+
? {role: 'img', 'aria-label': label, 'aria-hidden': undefined}
|
|
286
294
|
: {'aria-hidden': 'true'};
|
|
287
295
|
}
|
|
288
296
|
|
|
@@ -392,7 +400,7 @@ function IconFromRegistry({
|
|
|
392
400
|
color: IconColor;
|
|
393
401
|
size: IconSize;
|
|
394
402
|
variant: IconVariant | undefined;
|
|
395
|
-
a11yProps:
|
|
403
|
+
a11yProps: IconA11yProps;
|
|
396
404
|
className?: string;
|
|
397
405
|
style?: React.CSSProperties;
|
|
398
406
|
xstyle?: StyleXStyles;
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @file layerAnimations.stylex.ts
|
|
5
|
-
* @input Uses StyleX, theme duration/easing tokens
|
|
5
|
+
* @input Uses StyleX, theme duration/easing tokens, the Reduce Motion fade
|
|
6
|
+
* constants
|
|
6
7
|
* @output Exports shared entry animation styles for layer-based components
|
|
7
8
|
* @position Shared animation primitives; consumed by DropdownMenu, Popover,
|
|
8
9
|
* HoverCard, Tooltip, Selector, and any component using useLayer
|
|
@@ -27,6 +28,7 @@
|
|
|
27
28
|
*/
|
|
28
29
|
|
|
29
30
|
import * as stylex from '@stylexjs/stylex';
|
|
31
|
+
import {reducedMotionFadeConsts} from '../motion/reducedMotion.stylex';
|
|
30
32
|
import {durationVars, easeVars, spacingVars} from '../theme/tokens.stylex';
|
|
31
33
|
|
|
32
34
|
const enterBelow = stylex.keyframes({
|
|
@@ -84,17 +86,21 @@ const enterStartRtl = stylex.keyframes({
|
|
|
84
86
|
|
|
85
87
|
// Reduce Motion under a finger: the same entrance without its travel. A touch
|
|
86
88
|
// presentation substitutes a cross-fade for motion, never a jump.
|
|
89
|
+
// The fade's curve rides in its first keyframe, so the published class type
|
|
90
|
+
// of `animationTimingFunction` stays the theme ease's.
|
|
87
91
|
const fadeIn = stylex.keyframes({
|
|
88
|
-
from: {opacity: 0},
|
|
92
|
+
from: {opacity: 0, animationTimingFunction: reducedMotionFadeConsts.easing},
|
|
89
93
|
to: {opacity: 1},
|
|
90
94
|
});
|
|
91
95
|
|
|
96
|
+
// No theme value lengthens the Reduce Motion fade, so its values are constants.
|
|
97
|
+
// The media keys stay literal: StyleX gives an imported at-rule key a later
|
|
98
|
+
// priority layer (see reducedMotionKeys).
|
|
92
99
|
const animationBase = {
|
|
93
100
|
animationDuration: {
|
|
94
101
|
default: durationVars['--duration-fast-max'],
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
'@media (prefers-reduced-motion: reduce) and (pointer: coarse)': `min(${durationVars['--duration-fast-max']}, 200ms)`,
|
|
102
|
+
'@media (prefers-reduced-motion: reduce) and (pointer: coarse)':
|
|
103
|
+
reducedMotionFadeConsts.duration,
|
|
98
104
|
},
|
|
99
105
|
animationTimingFunction: easeVars['--ease-standard'],
|
|
100
106
|
animationFillMode: 'backwards' as const,
|
|
@@ -106,19 +112,18 @@ const animationBase = {
|
|
|
106
112
|
* Keyed by LayerPlacement ('above' | 'below' | 'start' | 'end')
|
|
107
113
|
* for easy lookup: `layerAnimations[placement]`.
|
|
108
114
|
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* (design:reduced-motion DR2, DR3), as iOS does for its menus, popovers and
|
|
115
|
-
* tips.
|
|
115
|
+
* Under `prefers-reduced-motion: reduce` each entry drops its travel and
|
|
116
|
+
* scale. Under a finger (the primary pointer is coarse) the layer fades in,
|
|
117
|
+
* opacity only, in 200 ms on easeInOut: on a touch screen Reduce Motion
|
|
118
|
+
* replaces motion with a cross-fade rather than a jump; under any other
|
|
119
|
+
* pointer it appears at once.
|
|
116
120
|
*/
|
|
117
121
|
export const layerAnimations = stylex.create({
|
|
118
122
|
below: {
|
|
119
123
|
animationName: {
|
|
120
124
|
default: enterBelow,
|
|
121
|
-
'@media (prefers-reduced-motion: reduce)
|
|
125
|
+
'@media (prefers-reduced-motion: reduce) and (not (pointer: coarse))':
|
|
126
|
+
'none',
|
|
122
127
|
'@media (prefers-reduced-motion: reduce) and (pointer: coarse)': fadeIn,
|
|
123
128
|
},
|
|
124
129
|
...animationBase,
|
|
@@ -126,7 +131,8 @@ export const layerAnimations = stylex.create({
|
|
|
126
131
|
above: {
|
|
127
132
|
animationName: {
|
|
128
133
|
default: enterAbove,
|
|
129
|
-
'@media (prefers-reduced-motion: reduce)
|
|
134
|
+
'@media (prefers-reduced-motion: reduce) and (not (pointer: coarse))':
|
|
135
|
+
'none',
|
|
130
136
|
'@media (prefers-reduced-motion: reduce) and (pointer: coarse)': fadeIn,
|
|
131
137
|
},
|
|
132
138
|
...animationBase,
|
|
@@ -135,7 +141,8 @@ export const layerAnimations = stylex.create({
|
|
|
135
141
|
animationName: {
|
|
136
142
|
default: enterEnd,
|
|
137
143
|
':is([dir="rtl"] *)': enterEndRtl,
|
|
138
|
-
'@media (prefers-reduced-motion: reduce)
|
|
144
|
+
'@media (prefers-reduced-motion: reduce) and (not (pointer: coarse))':
|
|
145
|
+
'none',
|
|
139
146
|
'@media (prefers-reduced-motion: reduce) and (pointer: coarse)': fadeIn,
|
|
140
147
|
},
|
|
141
148
|
...animationBase,
|
|
@@ -144,7 +151,8 @@ export const layerAnimations = stylex.create({
|
|
|
144
151
|
animationName: {
|
|
145
152
|
default: enterStart,
|
|
146
153
|
':is([dir="rtl"] *)': enterStartRtl,
|
|
147
|
-
'@media (prefers-reduced-motion: reduce)
|
|
154
|
+
'@media (prefers-reduced-motion: reduce) and (not (pointer: coarse))':
|
|
155
|
+
'none',
|
|
148
156
|
'@media (prefers-reduced-motion: reduce) and (pointer: coarse)': fadeIn,
|
|
149
157
|
},
|
|
150
158
|
...animationBase,
|
package/src/Layer/usePressPin.ts
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* @file usePressPin.ts
|
|
7
7
|
* @input A hover layer's trigger, open state, show and hide, and its
|
|
8
8
|
* `triggerBehavior` and `touchTrigger` options
|
|
9
|
-
* @output usePressPin, the press toggle that pins a hover layer open
|
|
10
|
-
*
|
|
9
|
+
* @output usePressPin, the press toggle that pins a hover layer open, and
|
|
10
|
+
* the app hold behind useTooltip's show() and hide(); isPressPinTrigger;
|
|
11
|
+
* stopPressPinClick; the LayerTriggerBehavior type
|
|
11
12
|
* @position Core-internal: one call per hover layer, useTooltip's. It owns
|
|
12
13
|
* the layer's touch trigger, so the host makes no second touch-trigger call.
|
|
13
14
|
*
|
|
@@ -103,8 +104,18 @@ export interface UsePressPinReturn {
|
|
|
103
104
|
isHeldOpen: () => boolean;
|
|
104
105
|
/** Wires the trigger element, or unwires it with `null`. */
|
|
105
106
|
attach: (element: HTMLElement | null) => void;
|
|
106
|
-
/** Drops the pin: Escape, or a hide
|
|
107
|
+
/** Drops the pin and the app hold: Escape, or a hide from elsewhere. */
|
|
107
108
|
unpin: () => void;
|
|
109
|
+
/**
|
|
110
|
+
* The app's `show()`: shows the layer and holds it open, as a pin does,
|
|
111
|
+
* until `release`, Escape, a press outside, or focus leaving the trigger
|
|
112
|
+
* and the layer. A controlled layer reports the request instead.
|
|
113
|
+
*/
|
|
114
|
+
hold: () => void;
|
|
115
|
+
/** The app's `hide()`: drops the hold and hides the layer. */
|
|
116
|
+
release: () => void;
|
|
117
|
+
/** Whether the app's `show()` holds the layer open now. */
|
|
118
|
+
isAppHeld: () => boolean;
|
|
108
119
|
/**
|
|
109
120
|
* While pinned, focus moving onto an element outside the trigger and the
|
|
110
121
|
* layer closes it. Returns whether the pin decided. The pin calls it for its
|
|
@@ -112,8 +123,9 @@ export interface UsePressPinReturn {
|
|
|
112
123
|
*/
|
|
113
124
|
handleFocusLeave: (event: FocusEvent) => boolean;
|
|
114
125
|
/**
|
|
115
|
-
* Whether the layer shows pinned on a wired trigger
|
|
116
|
-
* its rendered text, unless the layer has
|
|
126
|
+
* Whether the layer shows pinned on a wired trigger, or held by the app's
|
|
127
|
+
* `show()`. The host then announces its rendered text, unless the layer has
|
|
128
|
+
* a `label`.
|
|
117
129
|
*/
|
|
118
130
|
isPinShown: boolean;
|
|
119
131
|
/** The layer's touch trigger (useTouchTriggerInternal), wired to the pin. */
|
|
@@ -175,8 +187,19 @@ export function usePressPin(options: UsePressPinOptions): UsePressPinReturn {
|
|
|
175
187
|
setIsPinnedState(next);
|
|
176
188
|
}, []);
|
|
177
189
|
|
|
190
|
+
// The app's show() holds the layer on any trigger, in any mode.
|
|
191
|
+
const isAppHeldRef = useRef(false);
|
|
192
|
+
const [isAppHeldState, setIsAppHeldState] = useState(false);
|
|
193
|
+
const setAppHeld = useCallback((next: boolean) => {
|
|
194
|
+
isAppHeldRef.current = next;
|
|
195
|
+
setIsAppHeldState(next);
|
|
196
|
+
}, []);
|
|
197
|
+
|
|
178
198
|
// The last offered element lets a `triggerBehavior` change rewire it.
|
|
179
199
|
const triggerRef = useRef<HTMLElement | null>(null);
|
|
200
|
+
// Every offered trigger, in any mode, so focus leaving ends a pin or an
|
|
201
|
+
// app hold.
|
|
202
|
+
const focusTargetRef = useRef<HTMLElement | null>(null);
|
|
180
203
|
const offeredRef = useRef<HTMLElement | null>(null);
|
|
181
204
|
// A primitive: re-attaching the same trigger renders nothing.
|
|
182
205
|
const [offeredKind, setOfferedKind] = useState<'none' | 'other' | 'button'>(
|
|
@@ -192,6 +215,13 @@ export function usePressPin(options: UsePressPinOptions): UsePressPinReturn {
|
|
|
192
215
|
// a pinned layer through the pin, so the pin drops with it.
|
|
193
216
|
const hideFromTouch = useCallback(() => {
|
|
194
217
|
const current = optionsRef.current;
|
|
218
|
+
if (isAppHeldRef.current) {
|
|
219
|
+
setAppHeld(false);
|
|
220
|
+
if (current.isOpen !== undefined) {
|
|
221
|
+
current.report(false);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
195
225
|
if (current.triggerBehavior === 'toggle' && triggerRef.current != null) {
|
|
196
226
|
if (current.isOpen !== undefined) {
|
|
197
227
|
current.report(false);
|
|
@@ -202,7 +232,7 @@ export function usePressPin(options: UsePressPinOptions): UsePressPinReturn {
|
|
|
202
232
|
}
|
|
203
233
|
}
|
|
204
234
|
current.hide();
|
|
205
|
-
}, [setPinned]);
|
|
235
|
+
}, [setPinned, setAppHeld]);
|
|
206
236
|
|
|
207
237
|
const {
|
|
208
238
|
isTouchPointerRef,
|
|
@@ -233,13 +263,14 @@ export function usePressPin(options: UsePressPinOptions): UsePressPinReturn {
|
|
|
233
263
|
const close = useCallback(() => {
|
|
234
264
|
const current = optionsRef.current;
|
|
235
265
|
clearTapOpen();
|
|
266
|
+
setAppHeld(false);
|
|
236
267
|
if (current.isOpen !== undefined) {
|
|
237
268
|
current.report(false);
|
|
238
269
|
return;
|
|
239
270
|
}
|
|
240
271
|
setPinned(false);
|
|
241
272
|
current.hide();
|
|
242
|
-
}, [setPinned, clearTapOpen]);
|
|
273
|
+
}, [setPinned, setAppHeld, clearTapOpen]);
|
|
243
274
|
|
|
244
275
|
const announceName = useCallback(() => {
|
|
245
276
|
const current = optionsRef.current;
|
|
@@ -348,6 +379,9 @@ export function usePressPin(options: UsePressPinOptions): UsePressPinReturn {
|
|
|
348
379
|
|
|
349
380
|
const isHeldOpen = useCallback(() => {
|
|
350
381
|
const current = optionsRef.current;
|
|
382
|
+
if (isAppHeldRef.current && current.isLayerOpen) {
|
|
383
|
+
return current.isOpen !== false;
|
|
384
|
+
}
|
|
351
385
|
if (current.triggerBehavior !== 'toggle' || triggerRef.current == null) {
|
|
352
386
|
return false;
|
|
353
387
|
}
|
|
@@ -364,7 +398,7 @@ export function usePressPin(options: UsePressPinOptions): UsePressPinReturn {
|
|
|
364
398
|
return false;
|
|
365
399
|
}
|
|
366
400
|
const next = event.relatedTarget;
|
|
367
|
-
const trigger =
|
|
401
|
+
const trigger = focusTargetRef.current;
|
|
368
402
|
// Focus that leaves for no element is a press's to decide. The trigger
|
|
369
403
|
// can sit in another document, so the check uses its own window.
|
|
370
404
|
const view = trigger?.ownerDocument.defaultView;
|
|
@@ -387,6 +421,8 @@ export function usePressPin(options: UsePressPinOptions): UsePressPinReturn {
|
|
|
387
421
|
);
|
|
388
422
|
|
|
389
423
|
const unwire = useCallback(() => {
|
|
424
|
+
focusTargetRef.current?.removeEventListener('focusout', handleFocusLeave);
|
|
425
|
+
focusTargetRef.current = null;
|
|
390
426
|
const previous = triggerRef.current;
|
|
391
427
|
if (previous == null) {
|
|
392
428
|
return;
|
|
@@ -394,7 +430,6 @@ export function usePressPin(options: UsePressPinOptions): UsePressPinReturn {
|
|
|
394
430
|
previous.removeEventListener('click', handlePress);
|
|
395
431
|
previous.removeEventListener('keydown', handleKeyDown);
|
|
396
432
|
previous.removeEventListener('keyup', handleKeyUp);
|
|
397
|
-
previous.removeEventListener('focusout', handleFocusLeave);
|
|
398
433
|
const own = ownStateRef.current;
|
|
399
434
|
if (own != null) {
|
|
400
435
|
restoreAttribute(previous, 'aria-expanded', own.expanded);
|
|
@@ -423,10 +458,14 @@ export function usePressPin(options: UsePressPinOptions): UsePressPinReturn {
|
|
|
423
458
|
}
|
|
424
459
|
settledRef.current = {element, mode: current.triggerBehavior};
|
|
425
460
|
unwire();
|
|
426
|
-
if (element == null
|
|
461
|
+
if (element == null) {
|
|
427
462
|
return;
|
|
428
463
|
}
|
|
429
|
-
|
|
464
|
+
// Its own focus-leave listener, in any mode: `focusTrigger="never"`
|
|
465
|
+
// attaches none, and an app hold needs it on any trigger.
|
|
466
|
+
element.addEventListener('focusout', handleFocusLeave);
|
|
467
|
+
focusTargetRef.current = element;
|
|
468
|
+
if (current.triggerBehavior !== 'toggle' || !isPressPinTrigger(element)) {
|
|
430
469
|
return;
|
|
431
470
|
}
|
|
432
471
|
element.addEventListener('click', handlePress);
|
|
@@ -434,8 +473,6 @@ export function usePressPin(options: UsePressPinOptions): UsePressPinReturn {
|
|
|
434
473
|
element.addEventListener('keydown', handleKeyDown);
|
|
435
474
|
element.addEventListener('keyup', handleKeyUp);
|
|
436
475
|
}
|
|
437
|
-
// Its own focus-leave listener: `focusTrigger="never"` attaches none.
|
|
438
|
-
element.addEventListener('focusout', handleFocusLeave);
|
|
439
476
|
ownStateRef.current = {
|
|
440
477
|
expanded: element.getAttribute('aria-expanded'),
|
|
441
478
|
controls: element.getAttribute('aria-controls'),
|
|
@@ -473,7 +510,31 @@ export function usePressPin(options: UsePressPinOptions): UsePressPinReturn {
|
|
|
473
510
|
if (isPinnedRef.current) {
|
|
474
511
|
setPinned(false);
|
|
475
512
|
}
|
|
476
|
-
|
|
513
|
+
if (isAppHeldRef.current) {
|
|
514
|
+
setAppHeld(false);
|
|
515
|
+
}
|
|
516
|
+
}, [setPinned, setAppHeld]);
|
|
517
|
+
|
|
518
|
+
const hold = useCallback(() => {
|
|
519
|
+
const current = optionsRef.current;
|
|
520
|
+
if (!current.isEnabled) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
setAppHeld(true);
|
|
524
|
+
armOutsideDismiss();
|
|
525
|
+
announceName();
|
|
526
|
+
if (current.isOpen !== undefined) {
|
|
527
|
+
current.report(true);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
current.show();
|
|
531
|
+
}, [setAppHeld, armOutsideDismiss, announceName]);
|
|
532
|
+
|
|
533
|
+
const release = useCallback(() => {
|
|
534
|
+
close();
|
|
535
|
+
}, [close]);
|
|
536
|
+
|
|
537
|
+
const isAppHeld = useCallback(() => isAppHeldRef.current, []);
|
|
477
538
|
|
|
478
539
|
// Each path that hides a pinned layer drops the pin itself, so a layer that
|
|
479
540
|
// does not show is never reported expanded.
|
|
@@ -490,13 +551,18 @@ export function usePressPin(options: UsePressPinOptions): UsePressPinReturn {
|
|
|
490
551
|
writeAttribute(trigger, 'aria-controls', isLayerOpen ? layerId : null);
|
|
491
552
|
});
|
|
492
553
|
|
|
493
|
-
const isPinShown =
|
|
554
|
+
const isPinShown =
|
|
555
|
+
(isToggleMode && isWired && isExpanded) ||
|
|
556
|
+
(isAppHeldState && isLayerOpen && isOpen !== false);
|
|
494
557
|
|
|
495
558
|
return {
|
|
496
559
|
isToggleTrigger,
|
|
497
560
|
isHeldOpen,
|
|
498
561
|
attach,
|
|
499
562
|
unpin,
|
|
563
|
+
hold,
|
|
564
|
+
release,
|
|
565
|
+
isAppHeld,
|
|
500
566
|
handleFocusLeave,
|
|
501
567
|
isPinShown,
|
|
502
568
|
touch: {
|
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
} from '../i18n/useComponentTranslator';
|
|
56
56
|
import english from '../i18n/generated-english/Lightbox/Lightbox.generated';
|
|
57
57
|
import {isRenderable} from '../utils/isRenderable';
|
|
58
|
+
import {prepareFocusReturnOnRemoval} from '../utils/focusReturn';
|
|
58
59
|
|
|
59
60
|
registerEnglishFallback(english);
|
|
60
61
|
/**
|
|
@@ -445,6 +446,23 @@ export function Lightbox({
|
|
|
445
446
|
}
|
|
446
447
|
}, [isOpen, hasItem]);
|
|
447
448
|
|
|
449
|
+
// A viewer that unmounts while open (rendered only while open, or removed
|
|
450
|
+
// with the rest of a view) never runs the close branch above. Focus the
|
|
451
|
+
// removal lost goes back to the opener while the opener is still on the
|
|
452
|
+
// page.
|
|
453
|
+
useEffect(() => {
|
|
454
|
+
const dialog = dialogRef.current;
|
|
455
|
+
if (dialog == null) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const returnFocusOnRemoval = prepareFocusReturnOnRemoval(dialog);
|
|
459
|
+
return () => {
|
|
460
|
+
if (dialog.open) {
|
|
461
|
+
returnFocusOnRemoval(triggerElementRef.current);
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
}, [hasItem]);
|
|
465
|
+
|
|
448
466
|
const handleClose = useCallback(() => {
|
|
449
467
|
onOpenChange(false);
|
|
450
468
|
}, [onOpenChange]);
|