@astryxdesign/core 0.5.1 → 0.5.2-canary.0f46244
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/CHANGELOG.md +19 -0
- package/dist/BottomSheet/BottomSheet.d.ts +3 -1
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +4 -3
- package/dist/BottomSheet/BottomSheetEdgeTint.d.ts +2 -2
- package/dist/BottomSheet/BottomSheetEdgeTint.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetEdgeTint.js +7 -10
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +1 -1
- package/dist/Button/Button.d.ts +2 -1
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +11 -4
- package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
- package/dist/Chat/ChatToolCalls.js +34 -17
- package/dist/ContextMenu/ContextMenu.d.ts +14 -3
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +149 -26
- package/dist/ContextMenu/index.d.ts +1 -0
- package/dist/ContextMenu/index.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.d.ts +24 -5
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +340 -26
- package/dist/DropdownMenu/DropdownMenuContext.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuItem.js +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +6 -2
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +43 -14
- package/dist/DropdownMenu/MenuBottomSheet.d.ts +20 -0
- package/dist/DropdownMenu/MenuBottomSheet.d.ts.map +1 -0
- package/dist/DropdownMenu/MenuBottomSheet.js +36 -0
- package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts +10 -0
- package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts.map +1 -0
- package/dist/DropdownMenu/MenuBottomSheetActionList.js +111 -0
- package/dist/DropdownMenu/index.d.ts +2 -1
- package/dist/DropdownMenu/index.d.ts.map +1 -1
- package/dist/DropdownMenu/menuWidth.d.ts +14 -0
- package/dist/DropdownMenu/menuWidth.d.ts.map +1 -0
- package/dist/DropdownMenu/menuWidth.js +35 -0
- package/dist/DropdownMenu/useMenuOverflow.d.ts +10 -0
- package/dist/DropdownMenu/useMenuOverflow.d.ts.map +1 -0
- package/dist/DropdownMenu/useMenuOverflow.js +60 -0
- package/dist/MoreMenu/MoreMenu.d.ts +8 -2
- package/dist/MoreMenu/MoreMenu.d.ts.map +1 -1
- package/dist/MoreMenu/MoreMenu.js +2 -0
- package/dist/MultiSelector/MultiSelector.d.ts +11 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +97 -68
- package/dist/MultiSelector/index.d.ts +1 -1
- package/dist/MultiSelector/index.d.ts.map +1 -1
- package/dist/Popover/Popover.d.ts +10 -6
- package/dist/Popover/Popover.d.ts.map +1 -1
- package/dist/Popover/Popover.js +141 -16
- package/dist/Popover/usePopover.d.ts +4 -0
- package/dist/Popover/usePopover.d.ts.map +1 -1
- package/dist/Popover/usePopover.js +52 -5
- package/dist/Selector/Selector.d.ts +10 -0
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +106 -80
- package/dist/Selector/SelectorBottomSheet.d.ts +21 -0
- package/dist/Selector/SelectorBottomSheet.d.ts.map +1 -0
- package/dist/Selector/SelectorBottomSheet.js +83 -0
- package/dist/Selector/index.d.ts +1 -1
- package/dist/Selector/index.d.ts.map +1 -1
- package/dist/Selector/selectorPresentation.stylex.d.ts +16 -0
- package/dist/Selector/selectorPresentation.stylex.d.ts.map +1 -0
- package/dist/Selector/selectorPresentation.stylex.js +21 -0
- package/dist/Selector/useSelectorPresentation.d.ts +32 -0
- package/dist/Selector/useSelectorPresentation.d.ts.map +1 -0
- package/dist/Selector/useSelectorPresentation.js +90 -0
- package/dist/Toast/useToastGesture.d.ts.map +1 -1
- package/dist/Toast/useToastGesture.js +46 -9
- package/dist/astryx.css +17 -1
- package/dist/hooks/useAdaptivePresentation.d.ts +5 -0
- package/dist/hooks/useAdaptivePresentation.d.ts.map +1 -0
- package/dist/hooks/useAdaptivePresentation.js +19 -0
- package/dist/hooks/useFocusReturnVisibility.d.ts +7 -0
- package/dist/hooks/useFocusReturnVisibility.d.ts.map +1 -0
- package/dist/hooks/useFocusReturnVisibility.js +35 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +12 -0
- package/dist/i18n/InternationalizationContext.d.ts +2 -0
- package/dist/i18n/InternationalizationContext.d.ts.map +1 -1
- package/dist/i18n/InternationalizationContext.js +3 -1
- package/dist/i18n/InternationalizationProvider.d.ts.map +1 -1
- package/dist/i18n/InternationalizationProvider.js +3 -1
- package/dist/i18n/resolve.d.ts +1 -1
- package/dist/i18n/resolve.d.ts.map +1 -1
- package/dist/i18n/resolve.js +37 -38
- package/dist/i18n/useTranslator.d.ts.map +1 -1
- package/dist/i18n/useTranslator.js +2 -3
- package/locales/af-ZA.json +0 -4
- package/locales/ar-SA.json +0 -4
- package/locales/ca-ES.json +0 -4
- package/locales/cs-CZ.json +0 -4
- package/locales/da-DK.json +0 -4
- package/locales/de-DE.json +0 -4
- package/locales/el-GR.json +0 -4
- package/locales/en.json +20 -0
- package/locales/es-ES.json +0 -4
- package/locales/fi-FI.json +0 -4
- package/locales/fr-FR.json +0 -4
- package/locales/he-IL.json +0 -4
- package/locales/hu-HU.json +0 -4
- package/locales/it-IT.json +0 -4
- package/locales/ja-JP.json +0 -4
- package/locales/ko-KR.json +0 -4
- package/locales/nl-NL.json +0 -4
- package/locales/no-NO.json +0 -4
- package/locales/pl-PL.json +0 -4
- package/locales/pseudo.json +15 -0
- package/locales/pt-BR.json +0 -4
- package/locales/pt-PT.json +0 -4
- package/locales/ro-RO.json +0 -4
- package/locales/ru-RU.json +0 -4
- package/locales/sr-SP.json +0 -4
- package/locales/sv-SE.json +0 -4
- package/locales/tr-TR.json +0 -4
- package/locales/uk-UA.json +0 -4
- package/locales/vi-VN.json +0 -4
- package/locales/zh-CN.json +0 -4
- package/locales/zh-TW.json +0 -4
- package/package.json +3 -3
- package/src/BottomSheet/BottomSheet.doc.mjs +6 -0
- package/src/BottomSheet/BottomSheet.test.tsx +29 -0
- package/src/BottomSheet/BottomSheet.tsx +7 -4
- package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +4 -6
- package/src/BottomSheet/BottomSheetEdgeTint.tsx +7 -10
- package/src/BottomSheet/BottomSheetSwitcher.tsx +1 -2
- package/src/Button/Button.test.tsx +10 -0
- package/src/Button/Button.tsx +13 -5
- package/src/Calendar/Calendar.doc.mjs +1 -1
- package/src/Chat/ChatToolCalls.test.tsx +44 -0
- package/src/Chat/ChatToolCalls.tsx +36 -15
- package/src/CheckboxInput/CheckboxInput.doc.mjs +1 -1
- package/src/CheckboxList/CheckboxList.doc.mjs +1 -1
- package/src/ComplexSelector/ComplexSelector.doc.mjs +2 -2
- package/src/ContextMenu/ContextMenu.doc.mjs +13 -1
- package/src/ContextMenu/ContextMenu.test.tsx +131 -1
- package/src/ContextMenu/ContextMenu.tsx +199 -32
- package/src/ContextMenu/index.ts +1 -0
- package/src/DateInput/DateInput.doc.mjs +1 -1
- package/src/DateRangeInput/DateRangeInput.doc.mjs +1 -1
- package/src/DateTimeInput/DateTimeInput.doc.mjs +1 -1
- package/src/DropdownMenu/DropdownMenu.doc.mjs +201 -40
- package/src/DropdownMenu/DropdownMenu.test.tsx +518 -7
- package/src/DropdownMenu/DropdownMenu.tsx +459 -28
- package/src/DropdownMenu/DropdownMenuContext.tsx +1 -1
- package/src/DropdownMenu/DropdownMenuItem.tsx +2 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +4 -2
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +124 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +72 -17
- package/src/DropdownMenu/MenuBottomSheet.tsx +46 -0
- package/src/DropdownMenu/MenuBottomSheetActionList.tsx +141 -0
- package/src/DropdownMenu/index.ts +2 -0
- package/src/DropdownMenu/menuWidth.ts +51 -0
- package/src/DropdownMenu/useMenuOverflow.ts +77 -0
- package/src/Field/Field.doc.mjs +1 -1
- package/src/FieldStatus/FieldStatus.doc.mjs +1 -1
- package/src/FileInput/FileInput.doc.mjs +1 -1
- package/src/Indicator/Indicator.doc.mjs +1 -1
- package/src/InputGroup/InputGroup.doc.mjs +1 -1
- package/src/MoreMenu/MoreMenu.doc.mjs +17 -0
- package/src/MoreMenu/MoreMenu.test.tsx +47 -0
- package/src/MoreMenu/MoreMenu.tsx +13 -1
- package/src/MultiSelector/MultiSelector.doc.mjs +17 -1
- package/src/MultiSelector/MultiSelector.test.tsx +127 -0
- package/src/MultiSelector/MultiSelector.tsx +121 -73
- package/src/MultiSelector/index.ts +1 -0
- package/src/NumberInput/NumberInput.doc.mjs +1 -1
- package/src/Popover/Popover.doc.mjs +11 -11
- package/src/Popover/Popover.test.tsx +397 -2
- package/src/Popover/Popover.tsx +228 -21
- package/src/Popover/usePopover.tsx +92 -15
- package/src/PowerSearch/PowerSearch.doc.mjs +1 -1
- package/src/RadioList/RadioList.doc.mjs +1 -1
- package/src/Selector/Selector.doc.mjs +13 -1
- package/src/Selector/Selector.test.tsx +135 -0
- package/src/Selector/Selector.tsx +141 -82
- package/src/Selector/SelectorBottomSheet.tsx +97 -0
- package/src/Selector/index.ts +1 -0
- package/src/Selector/selectorPresentation.stylex.ts +20 -0
- package/src/Selector/useSelectorPresentation.ts +147 -0
- package/src/Slider/Slider.doc.mjs +2 -2
- package/src/Switch/Switch.doc.mjs +1 -1
- package/src/TextArea/TextArea.doc.mjs +1 -1
- package/src/TextInput/TextInput.doc.mjs +1 -1
- package/src/TimeInput/TimeInput.doc.mjs +1 -1
- package/src/Toast/ToastViewport.test.tsx +118 -4
- package/src/Toast/useToastGesture.ts +61 -8
- package/src/Tokenizer/Tokenizer.doc.mjs +1 -1
- package/src/Typeahead/Typeahead.doc.mjs +1 -1
- package/src/hooks/useAdaptivePresentation.ts +35 -0
- package/src/hooks/useFocusReturnVisibility.ts +45 -0
- package/src/hooks/useFocusTrap.test.tsx +40 -0
- package/src/hooks/useFocusTrap.ts +13 -0
- package/src/i18n/InternationalizationContext.ts +3 -0
- package/src/i18n/InternationalizationProvider.tsx +8 -1
- package/src/i18n/__tests__/resolve.test.ts +23 -1
- package/src/i18n/resolve.ts +46 -49
- package/src/i18n/useTranslator.ts +2 -7
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file selectorPresentation.stylex.ts
|
|
5
|
+
* @input Uses field border tokens
|
|
6
|
+
* @output Shared focus-return styles for Selector and MultiSelector
|
|
7
|
+
* @position Internal visual policy for selector presentation changes
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as stylex from '@stylexjs/stylex';
|
|
11
|
+
import {colorVars} from '../theme/tokens.stylex';
|
|
12
|
+
|
|
13
|
+
export const selectorPresentationStyles = stylex.create({
|
|
14
|
+
pointerRestoredFocus: {
|
|
15
|
+
borderColor: colorVars['--color-border-emphasized'],
|
|
16
|
+
boxShadow: 'none',
|
|
17
|
+
outlineWidth: '0',
|
|
18
|
+
outlineStyle: 'none',
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file useSelectorPresentation.ts
|
|
7
|
+
* @input Uses the shared adaptive presentation policy and usePopover
|
|
8
|
+
* @output Coordinates popover and bottom-sheet disclosure state
|
|
9
|
+
* @position Internal presentation controller shared by Selector and MultiSelector
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
useCallback,
|
|
14
|
+
useRef,
|
|
15
|
+
useState,
|
|
16
|
+
type FocusEvent,
|
|
17
|
+
type RefObject,
|
|
18
|
+
} from 'react';
|
|
19
|
+
import {
|
|
20
|
+
usePopoverInternal,
|
|
21
|
+
type UsePopoverOptions,
|
|
22
|
+
type UsePopoverReturn,
|
|
23
|
+
} from '../Popover/usePopover';
|
|
24
|
+
import {
|
|
25
|
+
useAdaptivePresentation,
|
|
26
|
+
type AdaptivePresentation,
|
|
27
|
+
type ResolvedAdaptivePresentation,
|
|
28
|
+
} from '../hooks/useAdaptivePresentation';
|
|
29
|
+
import {useFocusReturnVisibility} from '../hooks/useFocusReturnVisibility';
|
|
30
|
+
|
|
31
|
+
interface UseSelectorPresentationOptions {
|
|
32
|
+
presentation: AdaptivePresentation;
|
|
33
|
+
onHide: () => void;
|
|
34
|
+
popoverOptions: Omit<UsePopoverOptions, 'onHide'>;
|
|
35
|
+
triggerRef: RefObject<HTMLElement | null>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface SelectorPresentationController {
|
|
39
|
+
activePresentation: ResolvedAdaptivePresentation;
|
|
40
|
+
hide: () => void;
|
|
41
|
+
isOpen: boolean;
|
|
42
|
+
isSheetOpen: boolean;
|
|
43
|
+
isTriggerFocusRingSuppressed: boolean;
|
|
44
|
+
onSheetOpenChange: (isOpen: boolean) => void;
|
|
45
|
+
onTriggerFocus: (event: FocusEvent<HTMLElement>) => void;
|
|
46
|
+
popover: UsePopoverReturn & {wasJustDismissed: () => boolean};
|
|
47
|
+
show: () => void;
|
|
48
|
+
wasJustDismissed: () => boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function useSelectorPresentation({
|
|
52
|
+
presentation,
|
|
53
|
+
onHide,
|
|
54
|
+
popoverOptions,
|
|
55
|
+
triggerRef,
|
|
56
|
+
}: UseSelectorPresentationOptions): SelectorPresentationController {
|
|
57
|
+
const resolvedPresentation = useAdaptivePresentation(presentation);
|
|
58
|
+
const activePresentationRef =
|
|
59
|
+
useRef<ResolvedAdaptivePresentation>(resolvedPresentation);
|
|
60
|
+
const [isSheetOpen, setIsSheetOpen] = useState(false);
|
|
61
|
+
const isSheetOpenRef = useRef(false);
|
|
62
|
+
const onHideRef = useRef(onHide);
|
|
63
|
+
const {
|
|
64
|
+
isFocusRingSuppressed,
|
|
65
|
+
onFocusReturnTargetFocus,
|
|
66
|
+
prepareFocusReturn,
|
|
67
|
+
resetFocusReturn,
|
|
68
|
+
} = useFocusReturnVisibility();
|
|
69
|
+
onHideRef.current = onHide;
|
|
70
|
+
|
|
71
|
+
const handlePopoverHide = useCallback(() => {
|
|
72
|
+
prepareFocusReturn();
|
|
73
|
+
onHideRef.current();
|
|
74
|
+
triggerRef.current?.focus();
|
|
75
|
+
}, [prepareFocusReturn, triggerRef]);
|
|
76
|
+
const popover = usePopoverInternal({
|
|
77
|
+
...popoverOptions,
|
|
78
|
+
onHide: handlePopoverHide,
|
|
79
|
+
});
|
|
80
|
+
const {
|
|
81
|
+
hide: hidePopover,
|
|
82
|
+
show: showPopover,
|
|
83
|
+
wasJustDismissed: wasPopoverJustDismissed,
|
|
84
|
+
} = popover;
|
|
85
|
+
|
|
86
|
+
const show = useCallback(() => {
|
|
87
|
+
resetFocusReturn();
|
|
88
|
+
activePresentationRef.current = resolvedPresentation;
|
|
89
|
+
if (resolvedPresentation === 'bottom-sheet') {
|
|
90
|
+
isSheetOpenRef.current = true;
|
|
91
|
+
setIsSheetOpen(true);
|
|
92
|
+
} else {
|
|
93
|
+
showPopover();
|
|
94
|
+
}
|
|
95
|
+
}, [resetFocusReturn, resolvedPresentation, showPopover]);
|
|
96
|
+
|
|
97
|
+
const hide = useCallback(() => {
|
|
98
|
+
if (isSheetOpenRef.current) {
|
|
99
|
+
prepareFocusReturn();
|
|
100
|
+
isSheetOpenRef.current = false;
|
|
101
|
+
setIsSheetOpen(false);
|
|
102
|
+
onHideRef.current();
|
|
103
|
+
} else {
|
|
104
|
+
hidePopover();
|
|
105
|
+
}
|
|
106
|
+
}, [hidePopover, prepareFocusReturn]);
|
|
107
|
+
|
|
108
|
+
const handleTriggerFocus = useCallback(
|
|
109
|
+
(_event: FocusEvent<HTMLElement>) => onFocusReturnTargetFocus(),
|
|
110
|
+
[onFocusReturnTargetFocus],
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const handleSheetOpenChange = useCallback(
|
|
114
|
+
(nextIsOpen: boolean) => {
|
|
115
|
+
if (nextIsOpen) {
|
|
116
|
+
show();
|
|
117
|
+
} else {
|
|
118
|
+
hide();
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
[hide, show],
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const isOpen = popover.isOpen || isSheetOpen;
|
|
125
|
+
const activePresentation = isOpen
|
|
126
|
+
? activePresentationRef.current
|
|
127
|
+
: resolvedPresentation;
|
|
128
|
+
|
|
129
|
+
const wasJustDismissed = useCallback(
|
|
130
|
+
() =>
|
|
131
|
+
activePresentationRef.current === 'popover' && wasPopoverJustDismissed(),
|
|
132
|
+
[wasPopoverJustDismissed],
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
activePresentation,
|
|
137
|
+
hide,
|
|
138
|
+
isOpen,
|
|
139
|
+
isSheetOpen,
|
|
140
|
+
isTriggerFocusRingSuppressed: isFocusRingSuppressed,
|
|
141
|
+
onSheetOpenChange: handleSheetOpenChange,
|
|
142
|
+
onTriggerFocus: handleTriggerFocus,
|
|
143
|
+
popover,
|
|
144
|
+
show,
|
|
145
|
+
wasJustDismissed,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
export const docs = {
|
|
6
6
|
name: 'Slider',
|
|
7
7
|
displayName: 'Slider',
|
|
8
|
-
category: '
|
|
8
|
+
category: 'Form Controls',
|
|
9
9
|
keywords: ["slider","range","slidebar","trackbar","scrubber","knob","thumb","rangeslider"],
|
|
10
10
|
playground: {
|
|
11
11
|
defaults: {
|
|
@@ -364,4 +364,4 @@ export const docsDense = {
|
|
|
364
364
|
labelTooltip: 'Tooltip text for info icon next to label.',
|
|
365
365
|
xstyle: 'StyleX layout styles; must be stylex.create() value.',
|
|
366
366
|
},
|
|
367
|
-
};
|
|
367
|
+
};
|
|
@@ -793,6 +793,11 @@ describe('Toast swipe dismissal', () => {
|
|
|
793
793
|
expect(visualToast.style.getPropertyValue('--_toast-swipe-exit-y')).toBe(
|
|
794
794
|
'120%',
|
|
795
795
|
);
|
|
796
|
+
expect(visualToast.style.getPropertyValue('--_toast-swipe-y')).toBe('');
|
|
797
|
+
expect(visualToast.style.getPropertyValue('--_toast-swipe-opacity')).toBe(
|
|
798
|
+
'',
|
|
799
|
+
);
|
|
800
|
+
expect(visualToast.style.getPropertyValue('--_toast-swipe-scale')).toBe('');
|
|
796
801
|
expect(getComputedStyle(visualToast).transform).not.toContain('translateX');
|
|
797
802
|
});
|
|
798
803
|
|
|
@@ -1147,11 +1152,9 @@ describe('Toast swipe dismissal', () => {
|
|
|
1147
1152
|
|
|
1148
1153
|
expect(onHide).toHaveBeenCalledWith('manual');
|
|
1149
1154
|
expect(visualToast.style.getPropertyValue('--_toast-swipe-opacity')).toBe(
|
|
1150
|
-
'
|
|
1151
|
-
);
|
|
1152
|
-
expect(visualToast.style.getPropertyValue('--_toast-swipe-scale')).toBe(
|
|
1153
|
-
'0.985',
|
|
1155
|
+
'',
|
|
1154
1156
|
);
|
|
1157
|
+
expect(visualToast.style.getPropertyValue('--_toast-swipe-scale')).toBe('');
|
|
1155
1158
|
expect(visualToast.style.getPropertyValue('--_toast-swipe-exit-y')).toBe(
|
|
1156
1159
|
'calc(-1 * 120%)',
|
|
1157
1160
|
);
|
|
@@ -1248,6 +1251,117 @@ describe('Toast swipe dismissal', () => {
|
|
|
1248
1251
|
);
|
|
1249
1252
|
});
|
|
1250
1253
|
|
|
1254
|
+
it('abandons a one-finger swipe when a second touch starts', () => {
|
|
1255
|
+
const {visualToast, onHide} = renderSwipeToast();
|
|
1256
|
+
const dispatchTouch = (
|
|
1257
|
+
type: 'touchstart' | 'touchmove',
|
|
1258
|
+
touches: {identifier: number; clientX: number; clientY: number}[],
|
|
1259
|
+
changedTouches = touches,
|
|
1260
|
+
) => {
|
|
1261
|
+
const event = new Event(type, {bubbles: true, cancelable: true});
|
|
1262
|
+
Object.defineProperty(event, 'touches', {value: touches});
|
|
1263
|
+
Object.defineProperty(event, 'changedTouches', {value: changedTouches});
|
|
1264
|
+
visualToast.dispatchEvent(event);
|
|
1265
|
+
return event;
|
|
1266
|
+
};
|
|
1267
|
+
const first = {identifier: 51, clientX: 10, clientY: 0};
|
|
1268
|
+
const second = {identifier: 52, clientX: 30, clientY: 40};
|
|
1269
|
+
|
|
1270
|
+
act(() => {
|
|
1271
|
+
dispatchTouch('touchstart', [first]);
|
|
1272
|
+
dispatchTouch('touchmove', [{...first, clientY: 40}]);
|
|
1273
|
+
});
|
|
1274
|
+
expect(visualToast.style.getPropertyValue('--_toast-swipe-y')).toBe('40px');
|
|
1275
|
+
|
|
1276
|
+
act(() => {
|
|
1277
|
+
dispatchTouch('touchstart', [first, second], [second]);
|
|
1278
|
+
});
|
|
1279
|
+
const multitouchMove = dispatchTouch('touchmove', [first, second]);
|
|
1280
|
+
|
|
1281
|
+
expect(multitouchMove.defaultPrevented).toBe(false);
|
|
1282
|
+
expect(visualToast.style.getPropertyValue('--_toast-swipe-y')).toBe('');
|
|
1283
|
+
expect(visualToast.style.getPropertyValue('--_toast-swipe-opacity')).toBe(
|
|
1284
|
+
'',
|
|
1285
|
+
);
|
|
1286
|
+
expect(onHide).not.toHaveBeenCalled();
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
it('does not dismiss when another touch remains outside the Toast', () => {
|
|
1290
|
+
const {visualToast, onHide} = renderSwipeToast();
|
|
1291
|
+
const first = {identifier: 61, clientX: 10, clientY: 0};
|
|
1292
|
+
const second = {identifier: 62, clientX: 200, clientY: 200};
|
|
1293
|
+
const dispatchTouch = (
|
|
1294
|
+
type: 'touchstart' | 'touchmove' | 'touchend',
|
|
1295
|
+
touches: (typeof first)[],
|
|
1296
|
+
changedTouches: (typeof first)[],
|
|
1297
|
+
) => {
|
|
1298
|
+
const event = new Event(type, {bubbles: true, cancelable: true});
|
|
1299
|
+
Object.defineProperty(event, 'touches', {value: touches});
|
|
1300
|
+
Object.defineProperty(event, 'changedTouches', {value: changedTouches});
|
|
1301
|
+
visualToast.dispatchEvent(event);
|
|
1302
|
+
return event;
|
|
1303
|
+
};
|
|
1304
|
+
|
|
1305
|
+
act(() => {
|
|
1306
|
+
dispatchTouch('touchstart', [first], [first]);
|
|
1307
|
+
dispatchTouch(
|
|
1308
|
+
'touchmove',
|
|
1309
|
+
[{...first, clientY: 60}],
|
|
1310
|
+
[{...first, clientY: 60}],
|
|
1311
|
+
);
|
|
1312
|
+
});
|
|
1313
|
+
expect(visualToast.style.getPropertyValue('--_toast-swipe-y')).toBe('60px');
|
|
1314
|
+
|
|
1315
|
+
// The second contact began outside the Toast, so its touchstart did not
|
|
1316
|
+
// reach the Toast listener. Ending the tracked finger must still cancel
|
|
1317
|
+
// while that other contact remains active.
|
|
1318
|
+
act(() => {
|
|
1319
|
+
dispatchTouch('touchend', [second], [{...first, clientY: 60}]);
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
expect(onHide).not.toHaveBeenCalled();
|
|
1323
|
+
expect(visualToast.style.getPropertyValue('--_toast-swipe-y')).toBe('');
|
|
1324
|
+
expect(visualToast.style.getPropertyValue('--_toast-swipe-opacity')).toBe(
|
|
1325
|
+
'',
|
|
1326
|
+
);
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
it('does not let a pen event with the same numeric ID control a touch gesture', () => {
|
|
1330
|
+
const {visualToast, onHide} = renderSwipeToast();
|
|
1331
|
+
const touch = {identifier: 7, clientX: 10, clientY: 0};
|
|
1332
|
+
const touchStart = new Event('touchstart', {
|
|
1333
|
+
bubbles: true,
|
|
1334
|
+
cancelable: true,
|
|
1335
|
+
});
|
|
1336
|
+
Object.defineProperty(touchStart, 'touches', {value: [touch]});
|
|
1337
|
+
Object.defineProperty(touchStart, 'changedTouches', {value: [touch]});
|
|
1338
|
+
|
|
1339
|
+
act(() => {
|
|
1340
|
+
visualToast.dispatchEvent(touchStart);
|
|
1341
|
+
fireEvent.pointerMove(visualToast, {
|
|
1342
|
+
pointerId: 7,
|
|
1343
|
+
pointerType: 'pen',
|
|
1344
|
+
clientX: 10,
|
|
1345
|
+
clientY: 60,
|
|
1346
|
+
});
|
|
1347
|
+
fireEvent.pointerUp(visualToast, {
|
|
1348
|
+
pointerId: 7,
|
|
1349
|
+
pointerType: 'pen',
|
|
1350
|
+
clientX: 10,
|
|
1351
|
+
clientY: 60,
|
|
1352
|
+
});
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
expect(onHide).not.toHaveBeenCalled();
|
|
1356
|
+
expect(visualToast.style.getPropertyValue('--_toast-swipe-y')).toBe('');
|
|
1357
|
+
|
|
1358
|
+
act(() => {
|
|
1359
|
+
visualToast.dispatchEvent(
|
|
1360
|
+
new Event('touchcancel', {bubbles: true, cancelable: true}),
|
|
1361
|
+
);
|
|
1362
|
+
});
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1251
1365
|
it('removes native touch listeners when a Toast unmounts', () => {
|
|
1252
1366
|
const {visualToast, unmount} = renderSwipeToast();
|
|
1253
1367
|
const removeListener = vi.spyOn(visualToast, 'removeEventListener');
|
|
@@ -25,12 +25,14 @@ const SWIPE_ACTIVE_SCALE_MAX = 0.02;
|
|
|
25
25
|
export type ToastGestureDirection = 1 | -1;
|
|
26
26
|
|
|
27
27
|
interface GesturePoint {
|
|
28
|
+
source: 'touch' | 'pen';
|
|
28
29
|
pointerId: number;
|
|
29
30
|
clientX: number;
|
|
30
31
|
clientY: number;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
interface GestureState {
|
|
35
|
+
source: GesturePoint['source'];
|
|
34
36
|
pointerId: number;
|
|
35
37
|
startX: number;
|
|
36
38
|
startY: number;
|
|
@@ -126,6 +128,7 @@ export function useToastGesture({
|
|
|
126
128
|
}
|
|
127
129
|
const surfaceSize = Math.max(root.getBoundingClientRect().height, 1);
|
|
128
130
|
gestureRef.current = {
|
|
131
|
+
source: point.source,
|
|
129
132
|
pointerId: point.pointerId,
|
|
130
133
|
startX: point.clientX,
|
|
131
134
|
startY: point.clientY,
|
|
@@ -159,7 +162,12 @@ export function useToastGesture({
|
|
|
159
162
|
) => {
|
|
160
163
|
const state = gestureRef.current;
|
|
161
164
|
const root = rootRef.current;
|
|
162
|
-
if (
|
|
165
|
+
if (
|
|
166
|
+
!state ||
|
|
167
|
+
!root ||
|
|
168
|
+
point.source !== state.source ||
|
|
169
|
+
point.pointerId !== state.pointerId
|
|
170
|
+
) {
|
|
163
171
|
return;
|
|
164
172
|
}
|
|
165
173
|
const deltaX = point.clientX - state.startX;
|
|
@@ -209,7 +217,12 @@ export function useToastGesture({
|
|
|
209
217
|
(point: GesturePoint) => {
|
|
210
218
|
const state = gestureRef.current;
|
|
211
219
|
const root = rootRef.current;
|
|
212
|
-
if (
|
|
220
|
+
if (
|
|
221
|
+
!state ||
|
|
222
|
+
!root ||
|
|
223
|
+
point.source !== state.source ||
|
|
224
|
+
point.pointerId !== state.pointerId
|
|
225
|
+
) {
|
|
213
226
|
return;
|
|
214
227
|
}
|
|
215
228
|
const travel = Math.max(
|
|
@@ -225,6 +238,10 @@ export function useToastGesture({
|
|
|
225
238
|
gestureRef.current = null;
|
|
226
239
|
root.style.removeProperty('transition-duration');
|
|
227
240
|
if (isDismissed) {
|
|
241
|
+
// The exit animation owns the final translation. Clear the live-drag
|
|
242
|
+
// transform/fade/scale first so a standalone Toast does not remain
|
|
243
|
+
// partially translated or faded after its dismiss callback fires.
|
|
244
|
+
clearTransientStyles(root);
|
|
228
245
|
root.style.setProperty(
|
|
229
246
|
'--_toast-swipe-exit-y',
|
|
230
247
|
state.direction === 1
|
|
@@ -247,7 +264,15 @@ export function useToastGesture({
|
|
|
247
264
|
if (
|
|
248
265
|
event.pointerType === 'pen' &&
|
|
249
266
|
(event.button == null || event.button === 0) &&
|
|
250
|
-
beginGesture(
|
|
267
|
+
beginGesture(
|
|
268
|
+
{
|
|
269
|
+
source: 'pen',
|
|
270
|
+
pointerId: event.pointerId,
|
|
271
|
+
clientX: event.clientX,
|
|
272
|
+
clientY: event.clientY,
|
|
273
|
+
},
|
|
274
|
+
event.target,
|
|
275
|
+
)
|
|
251
276
|
) {
|
|
252
277
|
rootRef.current?.setPointerCapture?.(event.pointerId);
|
|
253
278
|
}
|
|
@@ -261,7 +286,12 @@ export function useToastGesture({
|
|
|
261
286
|
return;
|
|
262
287
|
}
|
|
263
288
|
moveGesture(
|
|
264
|
-
|
|
289
|
+
{
|
|
290
|
+
source: 'pen',
|
|
291
|
+
pointerId: event.pointerId,
|
|
292
|
+
clientX: event.clientX,
|
|
293
|
+
clientY: event.clientY,
|
|
294
|
+
},
|
|
265
295
|
() => event.preventDefault(),
|
|
266
296
|
() => rootRef.current?.releasePointerCapture?.(event.pointerId),
|
|
267
297
|
);
|
|
@@ -275,7 +305,12 @@ export function useToastGesture({
|
|
|
275
305
|
return;
|
|
276
306
|
}
|
|
277
307
|
rootRef.current?.releasePointerCapture?.(event.pointerId);
|
|
278
|
-
endGesture(
|
|
308
|
+
endGesture({
|
|
309
|
+
source: 'pen',
|
|
310
|
+
pointerId: event.pointerId,
|
|
311
|
+
clientX: event.clientX,
|
|
312
|
+
clientY: event.clientY,
|
|
313
|
+
});
|
|
279
314
|
},
|
|
280
315
|
[endGesture],
|
|
281
316
|
);
|
|
@@ -284,6 +319,7 @@ export function useToastGesture({
|
|
|
284
319
|
(event: ReactPointerEvent<HTMLDivElement>) => {
|
|
285
320
|
if (
|
|
286
321
|
event.pointerType === 'pen' &&
|
|
322
|
+
gestureRef.current?.source === 'pen' &&
|
|
287
323
|
gestureRef.current?.pointerId === event.pointerId
|
|
288
324
|
) {
|
|
289
325
|
resetGesture(true);
|
|
@@ -298,12 +334,14 @@ export function useToastGesture({
|
|
|
298
334
|
return;
|
|
299
335
|
}
|
|
300
336
|
const point = (touch: Touch): GesturePoint => ({
|
|
337
|
+
source: 'touch',
|
|
301
338
|
pointerId: touch.identifier,
|
|
302
339
|
clientX: touch.clientX,
|
|
303
340
|
clientY: touch.clientY,
|
|
304
341
|
});
|
|
305
342
|
const changedTouch = (event: TouchEvent) => {
|
|
306
|
-
const
|
|
343
|
+
const state = gestureRef.current;
|
|
344
|
+
const pointerId = state?.source === 'touch' ? state.pointerId : null;
|
|
307
345
|
return pointerId == null
|
|
308
346
|
? undefined
|
|
309
347
|
: [...event.changedTouches].find(
|
|
@@ -311,12 +349,23 @@ export function useToastGesture({
|
|
|
311
349
|
);
|
|
312
350
|
};
|
|
313
351
|
const handleTouchStart = (event: TouchEvent) => {
|
|
314
|
-
|
|
315
|
-
|
|
352
|
+
if (event.touches?.length !== 1) {
|
|
353
|
+
// A second contact means the browser may be handling pinch zoom or
|
|
354
|
+
// two-finger scrolling. Abandon any accepted one-finger swipe before
|
|
355
|
+
// touchmove can suppress that native gesture.
|
|
356
|
+
resetGesture(true);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const touch = event.changedTouches[0];
|
|
360
|
+
if (touch != null) {
|
|
316
361
|
beginGesture(point(touch), event.target);
|
|
317
362
|
}
|
|
318
363
|
};
|
|
319
364
|
const handleTouchMove = (event: TouchEvent) => {
|
|
365
|
+
if (event.touches?.length !== 1) {
|
|
366
|
+
resetGesture(true);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
320
369
|
const touch = changedTouch(event);
|
|
321
370
|
if (touch) {
|
|
322
371
|
moveGesture(point(touch), () => {
|
|
@@ -327,6 +376,10 @@ export function useToastGesture({
|
|
|
327
376
|
}
|
|
328
377
|
};
|
|
329
378
|
const handleTouchEnd = (event: TouchEvent) => {
|
|
379
|
+
if (event.touches.length > 0) {
|
|
380
|
+
resetGesture(true);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
330
383
|
const touch = changedTouch(event);
|
|
331
384
|
if (touch) {
|
|
332
385
|
endGesture(point(touch));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file useAdaptivePresentation.ts
|
|
7
|
+
* @input Uses the shared SSR-safe media-query hook
|
|
8
|
+
* @output Resolves popover/bottom-sheet presentation policy
|
|
9
|
+
* @position Internal overlay policy shared across component families
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {useMediaQuery} from './useMediaQuery';
|
|
13
|
+
|
|
14
|
+
export type AdaptivePresentation = 'popover' | 'bottom-sheet' | 'adaptive';
|
|
15
|
+
export type ResolvedAdaptivePresentation = Exclude<
|
|
16
|
+
AdaptivePresentation,
|
|
17
|
+
'adaptive'
|
|
18
|
+
>;
|
|
19
|
+
|
|
20
|
+
// Width keeps desktop-class tablets and large touch displays anchored by
|
|
21
|
+
// default. Pointer capability, rather than `hover: none`, avoids excluding
|
|
22
|
+
// touch browsers that also report hover capability.
|
|
23
|
+
export const COMPACT_TOUCH_PRESENTATION_QUERY =
|
|
24
|
+
'(max-width: 768px) and (pointer: coarse)';
|
|
25
|
+
|
|
26
|
+
export function useAdaptivePresentation(
|
|
27
|
+
presentation: AdaptivePresentation,
|
|
28
|
+
): ResolvedAdaptivePresentation {
|
|
29
|
+
const isCompactTouch = useMediaQuery(COMPACT_TOUCH_PRESENTATION_QUERY);
|
|
30
|
+
return presentation === 'adaptive'
|
|
31
|
+
? isCompactTouch
|
|
32
|
+
? 'bottom-sheet'
|
|
33
|
+
: 'popover'
|
|
34
|
+
: presentation;
|
|
35
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file useFocusReturnVisibility.ts
|
|
7
|
+
* @input Uses the shared interaction-modality tracker
|
|
8
|
+
* @output Preserves returned focus while suppressing pointer-only focus rings
|
|
9
|
+
* @position Internal focus-return policy for adaptive overlays
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {useCallback, useEffect, useState} from 'react';
|
|
13
|
+
import {
|
|
14
|
+
getInteractionModality,
|
|
15
|
+
trackInteractionModality,
|
|
16
|
+
} from '../utils/interactionModality';
|
|
17
|
+
|
|
18
|
+
export function useFocusReturnVisibility() {
|
|
19
|
+
const [isFocusRingSuppressed, setIsFocusRingSuppressed] = useState(false);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
trackInteractionModality();
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
const prepareFocusReturn = useCallback(() => {
|
|
26
|
+
setIsFocusRingSuppressed(getInteractionModality() === 'pointer');
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
const resetFocusReturn = useCallback(() => {
|
|
30
|
+
setIsFocusRingSuppressed(false);
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
const onFocusReturnTargetFocus = useCallback(() => {
|
|
34
|
+
if (getInteractionModality() === 'keyboard') {
|
|
35
|
+
setIsFocusRingSuppressed(false);
|
|
36
|
+
}
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
isFocusRingSuppressed,
|
|
41
|
+
onFocusReturnTargetFocus,
|
|
42
|
+
prepareFocusReturn,
|
|
43
|
+
resetFocusReturn,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -193,6 +193,46 @@ describe('useFocusTrap tabbable model (infra-8)', () => {
|
|
|
193
193
|
expect(screen.getByTestId('first')).toHaveFocus();
|
|
194
194
|
});
|
|
195
195
|
|
|
196
|
+
it('moves from a programmatic container focus target into the tabbable cycle', () => {
|
|
197
|
+
render(
|
|
198
|
+
<Trap>
|
|
199
|
+
<button type="button" data-testid="first">
|
|
200
|
+
First
|
|
201
|
+
</button>
|
|
202
|
+
<button type="button" data-testid="last">
|
|
203
|
+
Last
|
|
204
|
+
</button>
|
|
205
|
+
</Trap>,
|
|
206
|
+
);
|
|
207
|
+
const trap = screen.getByTestId('trap');
|
|
208
|
+
trap.tabIndex = -1;
|
|
209
|
+
trap.focus();
|
|
210
|
+
|
|
211
|
+
fireEvent.keyDown(document, {key: 'Tab'});
|
|
212
|
+
|
|
213
|
+
expect(screen.getByTestId('first')).toHaveFocus();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('moves backward from a programmatic container focus target to the last tabbable control', () => {
|
|
217
|
+
render(
|
|
218
|
+
<Trap>
|
|
219
|
+
<button type="button" data-testid="first">
|
|
220
|
+
First
|
|
221
|
+
</button>
|
|
222
|
+
<button type="button" data-testid="last">
|
|
223
|
+
Last
|
|
224
|
+
</button>
|
|
225
|
+
</Trap>,
|
|
226
|
+
);
|
|
227
|
+
const trap = screen.getByTestId('trap');
|
|
228
|
+
trap.tabIndex = -1;
|
|
229
|
+
trap.focus();
|
|
230
|
+
|
|
231
|
+
fireEvent.keyDown(document, {key: 'Tab', shiftKey: true});
|
|
232
|
+
|
|
233
|
+
expect(screen.getByTestId('last')).toHaveFocus();
|
|
234
|
+
});
|
|
235
|
+
|
|
196
236
|
it('keeps a programmatic focus target when the trap has no tabbable controls', () => {
|
|
197
237
|
render(
|
|
198
238
|
<Trap>
|