1import { Button } from 'expo-dev-client-components'; 2import * as React from 'react'; 3import { Animated, StyleSheet, useWindowDimensions, Pressable } from 'react-native'; 4 5import { 6 createAsyncStack, 7 StackItem, 8 StackItemComponent, 9 useStackItems, 10} from '../functions/createAsyncStack'; 11 12type ModalStackContextProps = { 13 push: (element: StackItemComponent) => StackItem; 14 pop: (amount?: number) => StackItem[]; 15}; 16 17const ModalStackContext = React.createContext<ModalStackContextProps | null>(null); 18export const useModalStack = () => React.useContext(ModalStackContext); 19const defaultModalStack = createAsyncStack(); 20 21export function ModalStackProvider({ children, modalStack = defaultModalStack }) { 22 const modals = useStackItems(modalStack); 23 24 const animatedValue = React.useRef(new Animated.Value(0)); 25 26 const hasModal = modals.some((m) => m.status === 'settled' || m.status === 'pushing'); 27 28 React.useEffect(() => { 29 if (hasModal) { 30 Animated.spring(animatedValue.current, { 31 toValue: 1, 32 useNativeDriver: false, 33 }).start(); 34 } else { 35 Animated.spring(animatedValue.current, { 36 toValue: 0, 37 useNativeDriver: false, 38 }).start(); 39 } 40 }, [hasModal]); 41 42 const backgroundColor = animatedValue.current.interpolate({ 43 inputRange: [0, 1], 44 outputRange: ['rgba(0,0,0,0)', 'rgba(0,0,0,0.75)'], 45 }); 46 47 function push(element: StackItemComponent) { 48 return modalStack.push({ element }); 49 } 50 51 function pop(amount: number = 1) { 52 return modalStack.pop(amount); 53 } 54 55 return ( 56 <ModalStackContext.Provider value={{ push, pop }}> 57 {children} 58 <Animated.View 59 style={[StyleSheet.absoluteFillObject, { backgroundColor }]} 60 pointerEvents={hasModal ? 'box-none' : 'none'}> 61 <Pressable 62 onPress={() => { 63 modalStack.pop(); 64 }} 65 style={[StyleSheet.absoluteFillObject]}> 66 {modals.map((item) => ( 67 <ModalScreen 68 key={item.key} 69 {...item} 70 onClose={item.pop} 71 onPopEnd={item.onPopEnd} 72 onPushEnd={item.onPushEnd} 73 /> 74 ))} 75 </Pressable> 76 </Animated.View> 77 </ModalStackContext.Provider> 78 ); 79} 80 81type ModalScreenProps = StackItem & { 82 onPushEnd: () => void; 83 onPopEnd: () => void; 84 onClose: () => void; 85}; 86 87function ModalScreen({ status, data, onPopEnd, onPushEnd }: ModalScreenProps) { 88 const { element } = data; 89 const { height } = useWindowDimensions(); 90 91 const animatedValue = React.useRef(new Animated.Value(status === 'settled' ? 1 : 0)); 92 93 React.useEffect(() => { 94 if (status === 'pushing') { 95 Animated.spring(animatedValue.current, { 96 toValue: 1, 97 stiffness: 1000, 98 damping: 500, 99 mass: 3, 100 overshootClamping: true, 101 useNativeDriver: true, 102 }).start(() => onPushEnd()); 103 } 104 105 if (status === 'popping') { 106 Animated.spring(animatedValue.current, { 107 toValue: 0, 108 stiffness: 1000, 109 damping: 500, 110 mass: 3, 111 overshootClamping: true, 112 useNativeDriver: true, 113 }).start(() => onPopEnd()); 114 } 115 }, [status]); 116 117 const translateY = animatedValue.current.interpolate({ 118 inputRange: [0, 1], 119 outputRange: [height, 0], 120 }); 121 122 return ( 123 <Animated.View 124 pointerEvents={status === 'popping' ? 'none' : 'box-none'} 125 style={[ 126 StyleSheet.absoluteFillObject, 127 { justifyContent: 'center', transform: [{ translateY }] }, 128 ]}> 129 <Button.Container>{element}</Button.Container> 130 </Animated.View> 131 ); 132} 133