@cdx-ui/components 0.0.1-beta.112 → 0.0.1-beta.114
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/README.md +2 -2
- package/lib/commonjs/animation/hooks/index.js +33 -0
- package/lib/commonjs/animation/hooks/index.js.map +1 -1
- package/lib/commonjs/animation/hooks/usePopupDialogContentAnimation.js +173 -0
- package/lib/commonjs/animation/hooks/usePopupDialogContentAnimation.js.map +1 -0
- package/lib/commonjs/animation/hooks/usePopupOverlayAnimation.js +67 -0
- package/lib/commonjs/animation/hooks/usePopupOverlayAnimation.js.map +1 -0
- package/lib/commonjs/animation/hooks/usePopupRootAnimation.js +26 -0
- package/lib/commonjs/animation/hooks/usePopupRootAnimation.js.map +1 -0
- package/lib/commonjs/components/AlertDialog/index.js +15 -1
- package/lib/commonjs/components/AlertDialog/index.js.map +1 -1
- package/lib/commonjs/components/Dialog/animation.js +13 -0
- package/lib/commonjs/components/Dialog/animation.js.map +1 -0
- package/lib/commonjs/components/Dialog/index.js +267 -24
- package/lib/commonjs/components/Dialog/index.js.map +1 -1
- package/lib/commonjs/components/Dialog/styles.js +7 -3
- package/lib/commonjs/components/Dialog/styles.js.map +1 -1
- package/lib/module/animation/hooks/index.js +3 -0
- package/lib/module/animation/hooks/index.js.map +1 -1
- package/lib/module/animation/hooks/usePopupDialogContentAnimation.js +169 -0
- package/lib/module/animation/hooks/usePopupDialogContentAnimation.js.map +1 -0
- package/lib/module/animation/hooks/usePopupOverlayAnimation.js +63 -0
- package/lib/module/animation/hooks/usePopupOverlayAnimation.js.map +1 -0
- package/lib/module/animation/hooks/usePopupRootAnimation.js +22 -0
- package/lib/module/animation/hooks/usePopupRootAnimation.js.map +1 -0
- package/lib/module/components/AlertDialog/index.js +15 -1
- package/lib/module/components/AlertDialog/index.js.map +1 -1
- package/lib/module/components/Dialog/animation.js +7 -0
- package/lib/module/components/Dialog/animation.js.map +1 -0
- package/lib/module/components/Dialog/index.js +271 -30
- package/lib/module/components/Dialog/index.js.map +1 -1
- package/lib/module/components/Dialog/styles.js +6 -2
- package/lib/module/components/Dialog/styles.js.map +1 -1
- package/lib/typescript/animation/hooks/index.d.ts +3 -0
- package/lib/typescript/animation/hooks/index.d.ts.map +1 -1
- package/lib/typescript/animation/hooks/usePopupDialogContentAnimation.d.ts +47 -0
- package/lib/typescript/animation/hooks/usePopupDialogContentAnimation.d.ts.map +1 -0
- package/lib/typescript/animation/hooks/usePopupOverlayAnimation.d.ts +29 -0
- package/lib/typescript/animation/hooks/usePopupOverlayAnimation.d.ts.map +1 -0
- package/lib/typescript/animation/hooks/usePopupRootAnimation.d.ts +16 -0
- package/lib/typescript/animation/hooks/usePopupRootAnimation.d.ts.map +1 -0
- package/lib/typescript/components/AlertDialog/index.d.ts +5 -2
- package/lib/typescript/components/AlertDialog/index.d.ts.map +1 -1
- package/lib/typescript/components/Dialog/animation.d.ts +11 -0
- package/lib/typescript/components/Dialog/animation.d.ts.map +1 -0
- package/lib/typescript/components/Dialog/index.d.ts +6 -2
- package/lib/typescript/components/Dialog/index.d.ts.map +1 -1
- package/lib/typescript/components/Dialog/styles.d.ts +1 -0
- package/lib/typescript/components/Dialog/styles.d.ts.map +1 -1
- package/package.json +13 -5
- package/src/animation/hooks/index.ts +3 -0
- package/src/animation/hooks/usePopupDialogContentAnimation.ts +256 -0
- package/src/animation/hooks/usePopupOverlayAnimation.ts +84 -0
- package/src/animation/hooks/usePopupRootAnimation.ts +27 -0
- package/src/components/AlertDialog/index.tsx +16 -3
- package/src/components/Dialog/animation.ts +13 -0
- package/src/components/Dialog/index.tsx +331 -31
- package/src/components/Dialog/styles.ts +22 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useSharedValue } from 'react-native-reanimated';
|
|
2
|
+
import type { AnimationRootDisableAll } from '../types';
|
|
3
|
+
import { useCombinedAnimationDisabledState } from './useCombinedAnimationDisabledState';
|
|
4
|
+
|
|
5
|
+
export interface UsePopupRootAnimationOptions {
|
|
6
|
+
animation?: AnimationRootDisableAll;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Root animation hook for popup-like components (Dialog, Select, BottomSheet, Popover, etc.)
|
|
11
|
+
* Manages component state transitions and animation coordination via a shared
|
|
12
|
+
* `progress` value: 0 = idle/closed, 1 = fully open, 2 = dismissed.
|
|
13
|
+
*/
|
|
14
|
+
export function usePopupRootAnimation(options: UsePopupRootAnimationOptions) {
|
|
15
|
+
const isAllAnimationsDisabled = useCombinedAnimationDisabledState(options.animation);
|
|
16
|
+
|
|
17
|
+
const progress = useSharedValue(0);
|
|
18
|
+
const isDragging = useSharedValue(false);
|
|
19
|
+
const isGestureReleaseAnimationRunning = useSharedValue(false);
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
isAllAnimationsDisabled,
|
|
23
|
+
progress,
|
|
24
|
+
isDragging,
|
|
25
|
+
isGestureReleaseAnimationRunning,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -3,7 +3,7 @@ import { View } from 'react-native';
|
|
|
3
3
|
import { Slot, useDialog } from '@cdx-ui/primitives';
|
|
4
4
|
import { composeEventHandlers } from '@cdx-ui/utils';
|
|
5
5
|
import { Button, type ButtonProps } from '../Button';
|
|
6
|
-
import { Dialog, type DialogRootProps } from '../Dialog';
|
|
6
|
+
import { Dialog, type DialogRootProps, type DialogPopupProps } from '../Dialog';
|
|
7
7
|
|
|
8
8
|
// =============================================================================
|
|
9
9
|
// ALERT DIALOG ROOT (Dialog with non-dismissible defaults + alertdialog role)
|
|
@@ -35,6 +35,19 @@ const AlertDialogRoot = forwardRef<View, AlertDialogRootProps>(
|
|
|
35
35
|
);
|
|
36
36
|
AlertDialogRoot.displayName = 'AlertDialog';
|
|
37
37
|
|
|
38
|
+
// =============================================================================
|
|
39
|
+
// ALERT DIALOG POPUP (swipe disabled by default)
|
|
40
|
+
// =============================================================================
|
|
41
|
+
|
|
42
|
+
export interface AlertDialogPopupProps extends DialogPopupProps {}
|
|
43
|
+
|
|
44
|
+
const AlertDialogPopup = forwardRef<View, AlertDialogPopupProps>(
|
|
45
|
+
({ isSwipeable = false, ...props }, ref) => (
|
|
46
|
+
<Dialog.Popup ref={ref} isSwipeable={isSwipeable} {...props} />
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
AlertDialogPopup.displayName = 'AlertDialog.Popup';
|
|
50
|
+
|
|
38
51
|
// =============================================================================
|
|
39
52
|
// ALERT DIALOG ACTION
|
|
40
53
|
// =============================================================================
|
|
@@ -98,7 +111,7 @@ AlertDialogCancel.displayName = 'AlertDialog.Cancel';
|
|
|
98
111
|
|
|
99
112
|
type AlertDialogCompoundComponent = typeof AlertDialogRoot & {
|
|
100
113
|
Trigger: typeof Dialog.Trigger;
|
|
101
|
-
Popup: typeof
|
|
114
|
+
Popup: typeof AlertDialogPopup;
|
|
102
115
|
Header: typeof Dialog.Header;
|
|
103
116
|
Title: typeof Dialog.Title;
|
|
104
117
|
Description: typeof Dialog.Description;
|
|
@@ -112,7 +125,7 @@ type AlertDialogCompoundComponent = typeof AlertDialogRoot & {
|
|
|
112
125
|
|
|
113
126
|
export const AlertDialog = Object.assign(AlertDialogRoot, {
|
|
114
127
|
Trigger: Dialog.Trigger,
|
|
115
|
-
Popup:
|
|
128
|
+
Popup: AlertDialogPopup,
|
|
116
129
|
Header: Dialog.Header,
|
|
117
130
|
Title: Dialog.Title,
|
|
118
131
|
Description: Dialog.Description,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SharedValue } from 'react-native-reanimated';
|
|
2
|
+
import { createContext } from '@cdx-ui/utils';
|
|
3
|
+
|
|
4
|
+
export interface DialogAnimationContextValue {
|
|
5
|
+
progress: SharedValue<number>;
|
|
6
|
+
isDragging: SharedValue<boolean>;
|
|
7
|
+
isGestureReleaseAnimationRunning: SharedValue<boolean>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const [DialogAnimationProvider, useDialogAnimation] =
|
|
11
|
+
createContext<DialogAnimationContextValue>('DialogAnimationContext', {
|
|
12
|
+
strict: false,
|
|
13
|
+
});
|
|
@@ -1,22 +1,54 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useEffect,
|
|
4
|
+
useLayoutEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import {
|
|
11
|
+
KeyboardAvoidingView,
|
|
12
|
+
Platform,
|
|
13
|
+
StyleSheet,
|
|
14
|
+
Text,
|
|
15
|
+
View,
|
|
16
|
+
type TextProps,
|
|
17
|
+
type ViewProps,
|
|
18
|
+
} from 'react-native';
|
|
19
|
+
import { GestureDetector } from 'react-native-gesture-handler';
|
|
20
|
+
import Animated, { useSharedValue } from 'react-native-reanimated';
|
|
3
21
|
import { CheckCircle, Close as CloseIcon, EmergencyHome, Warning } from '@cdx-ui/icons';
|
|
4
22
|
import {
|
|
5
23
|
DialogClose as DialogClosePrimitive,
|
|
6
|
-
|
|
24
|
+
DialogContent as DialogContentPrimitive,
|
|
25
|
+
DialogOverlay as DialogOverlayPrimitive,
|
|
26
|
+
DialogPortal as DialogPortalPrimitive,
|
|
7
27
|
DialogRoot as DialogRootPrimitive,
|
|
8
28
|
DialogTrigger as DialogTriggerPrimitive,
|
|
29
|
+
FullWindowOverlay,
|
|
9
30
|
Slot,
|
|
10
31
|
dataAttributes,
|
|
11
32
|
mergeDataAttributes,
|
|
12
33
|
useDialog,
|
|
13
34
|
type IDialogCloseProps,
|
|
14
|
-
type
|
|
35
|
+
type IDialogContentProps,
|
|
15
36
|
type IDialogRootProps,
|
|
16
37
|
type IDialogTriggerProps,
|
|
17
38
|
} from '@cdx-ui/primitives';
|
|
18
|
-
import { cn, ParentContext, useParentContext, useStyleContext } from '@cdx-ui/utils';
|
|
39
|
+
import { cn, mergeRefs, ParentContext, useParentContext, useStyleContext } from '@cdx-ui/utils';
|
|
40
|
+
import {
|
|
41
|
+
usePopupDialogContentAnimation,
|
|
42
|
+
usePopupOverlayAnimation,
|
|
43
|
+
usePopupRootAnimation,
|
|
44
|
+
type AnimationRootDisableAll,
|
|
45
|
+
} from '../../animation';
|
|
46
|
+
import {
|
|
47
|
+
AnimationSettingsProvider,
|
|
48
|
+
useAnimationSettings,
|
|
49
|
+
} from '../../providers/animation-settings';
|
|
19
50
|
import { Icon } from '../Icon';
|
|
51
|
+
import { DialogAnimationProvider, useDialogAnimation } from './animation';
|
|
20
52
|
import {
|
|
21
53
|
type DialogTitleVariantProps,
|
|
22
54
|
type DialogVariantProps,
|
|
@@ -28,6 +60,7 @@ import {
|
|
|
28
60
|
dialogHeaderVariants,
|
|
29
61
|
dialogOverlayVariants,
|
|
30
62
|
dialogPopupVariants,
|
|
63
|
+
dialogPortalVariants,
|
|
31
64
|
dialogTextVariants,
|
|
32
65
|
dialogTitleIconVariants,
|
|
33
66
|
dialogTitleVariants,
|
|
@@ -35,6 +68,11 @@ import {
|
|
|
35
68
|
|
|
36
69
|
export { useDialog } from '@cdx-ui/primitives';
|
|
37
70
|
|
|
71
|
+
const IS_WEB = Platform.OS === 'web';
|
|
72
|
+
const WEB_TRANSITION_SAFETY_MS = 300;
|
|
73
|
+
|
|
74
|
+
type DialogDataState = 'entering' | 'open' | 'closed';
|
|
75
|
+
|
|
38
76
|
// =============================================================================
|
|
39
77
|
// STYLE CONTEXT
|
|
40
78
|
// =============================================================================
|
|
@@ -43,6 +81,12 @@ const SCOPE = 'DIALOG';
|
|
|
43
81
|
|
|
44
82
|
const useDialogStyleContext = () => useStyleContext(SCOPE) as DialogVariantProps;
|
|
45
83
|
|
|
84
|
+
// =============================================================================
|
|
85
|
+
// ANIMATED PRIMITIVES
|
|
86
|
+
// =============================================================================
|
|
87
|
+
|
|
88
|
+
const AnimatedOverlay = Animated.createAnimatedComponent(DialogOverlayPrimitive);
|
|
89
|
+
|
|
46
90
|
// =============================================================================
|
|
47
91
|
// STYLED ROOT
|
|
48
92
|
// =============================================================================
|
|
@@ -50,28 +94,46 @@ const useDialogStyleContext = () => useStyleContext(SCOPE) as DialogVariantProps
|
|
|
50
94
|
export interface DialogRootProps
|
|
51
95
|
extends Omit<ViewProps, 'children' | 'role'>, IDialogRootProps, DialogVariantProps {
|
|
52
96
|
className?: string;
|
|
97
|
+
animation?: AnimationRootDisableAll;
|
|
53
98
|
}
|
|
54
99
|
|
|
55
100
|
const DialogRoot = forwardRef<View, DialogRootProps>(
|
|
56
|
-
({ size = 'md', fullBleed = false, className, children, style, ...props }, ref) => {
|
|
101
|
+
({ size = 'md', fullBleed = false, animation, className, children, style, ...props }, ref) => {
|
|
57
102
|
const parent = useParentContext();
|
|
58
103
|
const ctx = useMemo(
|
|
59
104
|
() => ({ ...parent, [SCOPE]: { size, fullBleed } }),
|
|
60
105
|
[parent, size, fullBleed],
|
|
61
106
|
);
|
|
62
107
|
|
|
108
|
+
const { progress, isDragging, isGestureReleaseAnimationRunning, isAllAnimationsDisabled } =
|
|
109
|
+
usePopupRootAnimation({ animation });
|
|
110
|
+
|
|
111
|
+
const animationContextValue = useMemo(
|
|
112
|
+
() => ({ progress, isDragging, isGestureReleaseAnimationRunning }),
|
|
113
|
+
[progress, isDragging, isGestureReleaseAnimationRunning],
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const animationSettingsContextValue = useMemo(
|
|
117
|
+
() => ({ isAllAnimationsDisabled }),
|
|
118
|
+
[isAllAnimationsDisabled],
|
|
119
|
+
);
|
|
120
|
+
|
|
63
121
|
return (
|
|
64
|
-
<
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
122
|
+
<AnimationSettingsProvider value={animationSettingsContextValue}>
|
|
123
|
+
<DialogAnimationProvider value={animationContextValue}>
|
|
124
|
+
<ParentContext.Provider value={ctx}>
|
|
125
|
+
<DialogRootPrimitive
|
|
126
|
+
ref={ref}
|
|
127
|
+
className={className}
|
|
128
|
+
style={style}
|
|
129
|
+
{...props}
|
|
130
|
+
{...mergeDataAttributes(props, dataAttributes({ slot: 'dialog' }))}
|
|
131
|
+
>
|
|
132
|
+
{children}
|
|
133
|
+
</DialogRootPrimitive>
|
|
134
|
+
</ParentContext.Provider>
|
|
135
|
+
</DialogAnimationProvider>
|
|
136
|
+
</AnimationSettingsProvider>
|
|
75
137
|
);
|
|
76
138
|
},
|
|
77
139
|
);
|
|
@@ -104,32 +166,255 @@ const DialogTrigger = forwardRef<View, DialogTriggerProps>(
|
|
|
104
166
|
DialogTrigger.displayName = 'Dialog.Trigger';
|
|
105
167
|
|
|
106
168
|
// =============================================================================
|
|
107
|
-
// STYLED POPUP
|
|
169
|
+
// STYLED POPUP (portal + overlay + content composition)
|
|
108
170
|
// =============================================================================
|
|
109
171
|
|
|
110
|
-
export interface DialogPopupProps
|
|
172
|
+
export interface DialogPopupProps
|
|
173
|
+
extends Omit<IDialogContentProps, 'asChild'>, Pick<DialogVariantProps, 'size'> {
|
|
111
174
|
className?: string;
|
|
112
175
|
children?: ReactNode;
|
|
176
|
+
disableFullWindowOverlay?: boolean;
|
|
177
|
+
isSwipeable?: boolean;
|
|
113
178
|
}
|
|
114
179
|
|
|
115
180
|
const DialogPopup = forwardRef<View, DialogPopupProps>(
|
|
116
|
-
(
|
|
181
|
+
(
|
|
182
|
+
{
|
|
183
|
+
size: sizeProp,
|
|
184
|
+
className,
|
|
185
|
+
children,
|
|
186
|
+
style,
|
|
187
|
+
forceMount = false,
|
|
188
|
+
avoidKeyboard = true,
|
|
189
|
+
keyboardVerticalOffset = 0,
|
|
190
|
+
disableFullWindowOverlay = false,
|
|
191
|
+
isSwipeable = true,
|
|
192
|
+
...props
|
|
193
|
+
},
|
|
194
|
+
ref,
|
|
195
|
+
) => {
|
|
117
196
|
const { size: contextSize } = useDialogStyleContext();
|
|
118
197
|
const size = sizeProp ?? contextSize;
|
|
119
|
-
const
|
|
198
|
+
const { open, onOpenChange } = useDialog();
|
|
199
|
+
const parentContext = useParentContext();
|
|
200
|
+
|
|
201
|
+
const animationContext = useDialogAnimation();
|
|
202
|
+
const parentAnimationSettings = useAnimationSettings();
|
|
203
|
+
|
|
204
|
+
const fallbackProgress = useSharedValue(0);
|
|
205
|
+
const fallbackIsDragging = useSharedValue(false);
|
|
206
|
+
const fallbackIsGestureReleaseAnimationRunning = useSharedValue(false);
|
|
207
|
+
|
|
208
|
+
const progress = animationContext?.progress ?? fallbackProgress;
|
|
209
|
+
const isDragging = animationContext?.isDragging ?? fallbackIsDragging;
|
|
210
|
+
const isGestureReleaseAnimationRunning =
|
|
211
|
+
animationContext?.isGestureReleaseAnimationRunning ??
|
|
212
|
+
fallbackIsGestureReleaseAnimationRunning;
|
|
213
|
+
|
|
214
|
+
const {
|
|
215
|
+
rContainerStyle,
|
|
216
|
+
entering: overlayEntering,
|
|
217
|
+
exiting: overlayExiting,
|
|
218
|
+
} = usePopupOverlayAnimation({
|
|
219
|
+
progress,
|
|
220
|
+
isDragging,
|
|
221
|
+
isGestureReleaseAnimationRunning,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const {
|
|
225
|
+
contentY,
|
|
226
|
+
contentHeight,
|
|
227
|
+
panGesture,
|
|
228
|
+
rDragContainerStyle,
|
|
229
|
+
entering: contentEntering,
|
|
230
|
+
exiting: contentExiting,
|
|
231
|
+
} = usePopupDialogContentAnimation({
|
|
232
|
+
isOpen: open,
|
|
233
|
+
progress,
|
|
234
|
+
isDragging,
|
|
235
|
+
isGestureReleaseAnimationRunning,
|
|
236
|
+
onOpenChange,
|
|
237
|
+
isSwipeable,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const dragContainerRef = useRef<View>(null);
|
|
241
|
+
|
|
242
|
+
useLayoutEffect(() => {
|
|
243
|
+
if (!open || IS_WEB) return;
|
|
244
|
+
dragContainerRef.current?.measure((_x, _y, _width, height, _pageX, pageY) => {
|
|
245
|
+
contentY.set(pageY);
|
|
246
|
+
contentHeight.set(height);
|
|
247
|
+
});
|
|
248
|
+
}, [open, contentY, contentHeight]);
|
|
249
|
+
|
|
250
|
+
// ---- Web enter/exit lifecycle ----
|
|
251
|
+
// Mirrors the Toast pattern: mount → 'entering' → (rAF) → 'open' → (close)
|
|
252
|
+
// → 'closed' → (transitionend) → unmount.
|
|
253
|
+
//
|
|
254
|
+
// Critical: `isExiting` is derived synchronously (via ref tracking the previous
|
|
255
|
+
// `open` value) so the component stays mounted in the SAME render that sees
|
|
256
|
+
// `open` go false — if we used useEffect the component would return null before
|
|
257
|
+
// the effect fires.
|
|
258
|
+
//
|
|
259
|
+
// The transitionend listener targets the popup element (via popupRef) instead of
|
|
260
|
+
// the portal wrapper. CSS exit transitions live on the overlay and popup children,
|
|
261
|
+
// not the wrapper — listening on the wrapper would unmount on the first bubbled
|
|
262
|
+
// event, potentially cutting the other child's animation short.
|
|
263
|
+
const wrapperRef = useRef<View | null>(null);
|
|
264
|
+
const popupRef = useRef<View | null>(null);
|
|
265
|
+
const prevOpenRef = useRef(open);
|
|
266
|
+
const [isExiting, setIsExiting] = useState(false);
|
|
267
|
+
const [webState, setWebState] = useState<DialogDataState>(open ? 'entering' : 'closed');
|
|
268
|
+
|
|
269
|
+
// Detect open → false transition synchronously during render
|
|
270
|
+
if (IS_WEB && prevOpenRef.current && !open && !isExiting) {
|
|
271
|
+
setIsExiting(true);
|
|
272
|
+
setWebState('closed');
|
|
273
|
+
}
|
|
274
|
+
// Detect closed → open transition synchronously during render
|
|
275
|
+
if (IS_WEB && !prevOpenRef.current && open) {
|
|
276
|
+
setIsExiting(false);
|
|
277
|
+
setWebState('entering');
|
|
278
|
+
}
|
|
279
|
+
prevOpenRef.current = open;
|
|
280
|
+
|
|
281
|
+
// Entrance: commit 'entering' on first paint, then transition to 'open'
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
if (!IS_WEB || !open) return;
|
|
284
|
+
const raf = requestAnimationFrame(() => {
|
|
285
|
+
setWebState('open');
|
|
286
|
+
});
|
|
287
|
+
return () => cancelAnimationFrame(raf);
|
|
288
|
+
}, [open]);
|
|
289
|
+
|
|
290
|
+
// Exit: wait for CSS transition to finish, then unmount.
|
|
291
|
+
// Listens on the popup element directly — its exit transitions (opacity +
|
|
292
|
+
// transform) are at least as long as the overlay's, so when the popup
|
|
293
|
+
// finishes both children are done animating.
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
if (!IS_WEB || !isExiting) return;
|
|
296
|
+
|
|
297
|
+
const node = popupRef.current as unknown as HTMLElement | null;
|
|
298
|
+
let done = false;
|
|
299
|
+
|
|
300
|
+
const finish = () => {
|
|
301
|
+
if (done) return;
|
|
302
|
+
done = true;
|
|
303
|
+
setIsExiting(false);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const safetyTimer = setTimeout(finish, WEB_TRANSITION_SAFETY_MS);
|
|
307
|
+
|
|
308
|
+
if (!node || typeof node.addEventListener !== 'function') {
|
|
309
|
+
return () => {
|
|
310
|
+
clearTimeout(safetyTimer);
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const handler = (event: Event) => {
|
|
315
|
+
if (event.target !== node) return;
|
|
316
|
+
const propertyName = (event as unknown as { propertyName?: string }).propertyName ?? '';
|
|
317
|
+
if (propertyName === 'opacity' || propertyName === 'transform') {
|
|
318
|
+
finish();
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
node.addEventListener('transitionend', handler);
|
|
322
|
+
|
|
323
|
+
return () => {
|
|
324
|
+
clearTimeout(safetyTimer);
|
|
325
|
+
node.removeEventListener('transitionend', handler);
|
|
326
|
+
};
|
|
327
|
+
}, [isExiting]);
|
|
328
|
+
|
|
329
|
+
const dataState: DialogDataState = webState;
|
|
330
|
+
const shouldMount = IS_WEB ? open || isExiting : open;
|
|
331
|
+
|
|
332
|
+
if (!forceMount && !shouldMount) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const portalClassName = cn(dialogPortalVariants());
|
|
120
337
|
const overlayClassName = cn(dialogOverlayVariants());
|
|
338
|
+
const popupClassName = cn(dialogPopupVariants({ size }), className);
|
|
339
|
+
|
|
340
|
+
const keyboardBehavior = avoidKeyboard
|
|
341
|
+
? Platform.select<'padding' | 'height'>({ ios: 'padding', default: 'height' })
|
|
342
|
+
: undefined;
|
|
121
343
|
|
|
122
344
|
return (
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
345
|
+
<DialogPortalPrimitive forceMount>
|
|
346
|
+
<ParentContext.Provider value={parentContext}>
|
|
347
|
+
<AnimationSettingsProvider value={parentAnimationSettings}>
|
|
348
|
+
<DialogAnimationProvider value={animationContext}>
|
|
349
|
+
<FullWindowOverlay
|
|
350
|
+
disableFullWindowOverlay={disableFullWindowOverlay}
|
|
351
|
+
unstable_accessibilityContainerViewIsModal
|
|
352
|
+
>
|
|
353
|
+
<Animated.View
|
|
354
|
+
ref={wrapperRef}
|
|
355
|
+
className={portalClassName}
|
|
356
|
+
pointerEvents="box-none"
|
|
357
|
+
{...(IS_WEB ? dataAttributes({ state: dataState }) : undefined)}
|
|
358
|
+
>
|
|
359
|
+
{/* Overlay */}
|
|
360
|
+
<Animated.View
|
|
361
|
+
entering={overlayEntering}
|
|
362
|
+
exiting={overlayExiting}
|
|
363
|
+
style={StyleSheet.absoluteFill}
|
|
364
|
+
>
|
|
365
|
+
<AnimatedOverlay
|
|
366
|
+
forceMount
|
|
367
|
+
className={overlayClassName}
|
|
368
|
+
style={IS_WEB ? undefined : rContainerStyle}
|
|
369
|
+
{...(IS_WEB ? dataAttributes({ state: dataState }) : undefined)}
|
|
370
|
+
/>
|
|
371
|
+
</Animated.View>
|
|
372
|
+
|
|
373
|
+
{/* Content with gesture + animation */}
|
|
374
|
+
<GestureDetector gesture={panGesture}>
|
|
375
|
+
<Animated.View
|
|
376
|
+
ref={dragContainerRef}
|
|
377
|
+
style={internalStyles.fill}
|
|
378
|
+
pointerEvents="box-none"
|
|
379
|
+
entering={contentEntering}
|
|
380
|
+
exiting={contentExiting}
|
|
381
|
+
>
|
|
382
|
+
<Animated.View
|
|
383
|
+
style={
|
|
384
|
+
IS_WEB ? internalStyles.fill : [internalStyles.fill, rDragContainerStyle]
|
|
385
|
+
}
|
|
386
|
+
pointerEvents="box-none"
|
|
387
|
+
>
|
|
388
|
+
<KeyboardAvoidingView
|
|
389
|
+
style={internalStyles.center}
|
|
390
|
+
pointerEvents="box-none"
|
|
391
|
+
behavior={keyboardBehavior}
|
|
392
|
+
keyboardVerticalOffset={keyboardVerticalOffset}
|
|
393
|
+
>
|
|
394
|
+
<DialogContentPrimitive
|
|
395
|
+
ref={mergeRefs(ref, popupRef)}
|
|
396
|
+
forceMount
|
|
397
|
+
className={popupClassName}
|
|
398
|
+
style={style}
|
|
399
|
+
{...props}
|
|
400
|
+
{...mergeDataAttributes(
|
|
401
|
+
props,
|
|
402
|
+
dataAttributes({ slot: 'dialog-popup' }),
|
|
403
|
+
IS_WEB ? dataAttributes({ state: dataState }) : undefined,
|
|
404
|
+
)}
|
|
405
|
+
>
|
|
406
|
+
{children}
|
|
407
|
+
</DialogContentPrimitive>
|
|
408
|
+
</KeyboardAvoidingView>
|
|
409
|
+
</Animated.View>
|
|
410
|
+
</Animated.View>
|
|
411
|
+
</GestureDetector>
|
|
412
|
+
</Animated.View>
|
|
413
|
+
</FullWindowOverlay>
|
|
414
|
+
</DialogAnimationProvider>
|
|
415
|
+
</AnimationSettingsProvider>
|
|
416
|
+
</ParentContext.Provider>
|
|
417
|
+
</DialogPortalPrimitive>
|
|
133
418
|
);
|
|
134
419
|
},
|
|
135
420
|
);
|
|
@@ -408,3 +693,18 @@ export const Dialog = Object.assign(DialogRoot, {
|
|
|
408
693
|
Footer: DialogFooter,
|
|
409
694
|
Close: DialogClose,
|
|
410
695
|
}) as DialogCompoundComponent;
|
|
696
|
+
|
|
697
|
+
// =============================================================================
|
|
698
|
+
// INTERNAL STYLES
|
|
699
|
+
// =============================================================================
|
|
700
|
+
|
|
701
|
+
const internalStyles = StyleSheet.create({
|
|
702
|
+
fill: {
|
|
703
|
+
flex: 1,
|
|
704
|
+
},
|
|
705
|
+
center: {
|
|
706
|
+
flex: 1,
|
|
707
|
+
alignItems: 'center',
|
|
708
|
+
justifyContent: 'center',
|
|
709
|
+
},
|
|
710
|
+
});
|
|
@@ -2,9 +2,25 @@ import { Platform } from 'react-native';
|
|
|
2
2
|
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
3
|
import { TRANSITION_COLORS } from '../../styles/constants';
|
|
4
4
|
|
|
5
|
+
// ── Portal container ────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export const dialogPortalVariants = cva([
|
|
8
|
+
'flex-1',
|
|
9
|
+
'absolute inset-0',
|
|
10
|
+
'web:fixed web:inset-0 web:z-50',
|
|
11
|
+
]);
|
|
12
|
+
|
|
5
13
|
// ── Overlay ─────────────────────────────────────────────────
|
|
6
14
|
|
|
7
|
-
export const dialogOverlayVariants = cva([
|
|
15
|
+
export const dialogOverlayVariants = cva([
|
|
16
|
+
'absolute inset-0',
|
|
17
|
+
'bg-surface-backdrop',
|
|
18
|
+
'web:transition-opacity web:duration-200 web:ease-out',
|
|
19
|
+
'web:data-[state=entering]:opacity-0',
|
|
20
|
+
'web:data-[state=open]:opacity-100',
|
|
21
|
+
'web:data-[state=closed]:opacity-0 web:data-[state=closed]:duration-150',
|
|
22
|
+
'web:motion-reduce:transition-none web:motion-reduce:duration-0',
|
|
23
|
+
]);
|
|
8
24
|
|
|
9
25
|
// ── Popup (renamed from dialogContentVariants) ──────────────
|
|
10
26
|
|
|
@@ -14,6 +30,11 @@ export const dialogPopupVariants = cva(
|
|
|
14
30
|
'rounded-lg',
|
|
15
31
|
Platform.select({ web: 'shadow-lg', default: '' }),
|
|
16
32
|
'max-h-[85vh] overflow-hidden',
|
|
33
|
+
'web:transition-[opacity,transform] web:duration-200 web:ease-out',
|
|
34
|
+
'web:data-[state=entering]:opacity-0 web:data-[state=entering]:scale-[0.96]',
|
|
35
|
+
'web:data-[state=open]:opacity-100 web:data-[state=open]:scale-100',
|
|
36
|
+
'web:data-[state=closed]:opacity-0 web:data-[state=closed]:scale-[0.96] web:data-[state=closed]:duration-150',
|
|
37
|
+
'web:motion-reduce:transition-none web:motion-reduce:duration-0',
|
|
17
38
|
],
|
|
18
39
|
{
|
|
19
40
|
variants: {
|