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