Created
June 3, 2021 16:30
-
-
Save Noitidart/9715c38eb28ec47fa62fa17153fdab01 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { | |
Children, | |
createContext, | |
useContext, | |
useEffect, | |
useRef, | |
useState | |
} from 'react'; | |
import { | |
Animated, | |
Modal, | |
requireNativeComponent, | |
StyleSheet, | |
View, | |
ViewPropTypes | |
} from 'react-native'; | |
import { platform } from 'config'; | |
import type { ReactNode } from 'react'; | |
type ConfirmValue = any; | |
interface IModalApi { | |
animationDuration: number; | |
cancel: () => void; | |
confirm: (value?: ConfirmValue) => void; | |
open: <T extends ConfirmValue = void>( | |
node: ReactNode | |
) => Promise<{ value: T; confirmed: boolean; canceled: boolean }>; | |
anim: Animated.Value; | |
} | |
// TODO: Switch to RNGH export once expo fixes it - | |
// https://forums.expo.io/t/in-android-modal-wrapping-content-in-gesturehandlerrootview-not-fixing-react-native-gesture-handler/51009 | |
const GestureHandlerRootViewNative = platform.isAndroid | |
? requireNativeComponent( | |
'GestureHandlerRootView', | |
// @ts-note: TS is saying only one arg supported. I tested one arg, and it | |
// works. But I copied this from | |
// `node_modules/react-native-gesture-handler/GestureHandlerRootView.android.js` | |
// and they use this 2nd argument, so I'm keeping it. | |
{ name: 'GestureHandlerRootView', propTypes: { ...ViewPropTypes } } | |
) | |
: View; | |
const ModalContext = | |
// @ts-ignore | |
// its always used within a context, otherwise throws, so defaultValue | |
// is never used, but a value is always set - | |
// https://kentcdodds.com/blog/how-to-use-react-context-effectively | |
createContext<IModalApi>(); | |
interface IModalProviderProps { | |
children: ReactNode; | |
animationDuration?: number; | |
} | |
ModalProvider.defaultProps = { | |
animationDuration: 300 | |
}; | |
export function ModalProvider(props: IModalProviderProps) { | |
const [dialog, setDialog] = useState(null); | |
const opened = Boolean(dialog); | |
const stableFinalizers = useRef(null); | |
const stableUpdaters = useRef<IModalApi>({ | |
animationDuration: props.animationDuration, | |
anim: new Animated.Value(0), | |
cancel: () => { | |
const opened = Boolean(stableFinalizers.current); | |
if (!opened) { | |
return; | |
} | |
stableFinalizers.current.resolve({ canceled: true, confirmed: false }); | |
stableFinalizers.current = null; | |
setDialog(null); | |
}, | |
confirm: value => { | |
const opened = Boolean(stableFinalizers.current); | |
if (!opened) { | |
return; | |
} | |
stableFinalizers.current.resolve({ | |
value, | |
canceled: false, | |
confirmed: true | |
}); | |
stableFinalizers.current = null; | |
setDialog(null); | |
}, | |
open: async dialog => { | |
const opened = Boolean(stableFinalizers.current); | |
if (opened) { | |
setDialog(dialog); | |
return stableFinalizers.current.promise; | |
} | |
let resolve; | |
const promise = new Promise(r => { | |
resolve = r; | |
}); | |
stableFinalizers.current = { promise, resolve }; | |
setDialog(dialog); | |
return promise; | |
} | |
}); | |
return ( | |
<ModalContext.Provider value={stableUpdaters.current}> | |
<> | |
{props.children} | |
<ModalContainer opened={opened} dialog={dialog} /> | |
</> | |
</ModalContext.Provider> | |
); | |
} | |
export function useModal() { | |
const context = useContext(ModalContext); | |
if (context === undefined) { | |
throw new Error('useModal must be used within a ModalProvider'); | |
} | |
return context; | |
} | |
interface IModalContainerProps { | |
opened: boolean; | |
dialog: ReactNode; | |
} | |
function ModalContainer(props: IModalContainerProps) { | |
const modal = useModal(); | |
const [visible, setVisible] = useState(props.opened); | |
useEffect(() => { | |
if (props.opened) { | |
setVisible(true); | |
Animated.timing(modal.anim, { | |
toValue: 1, | |
duration: modal.animationDuration, | |
useNativeDriver: true | |
}).start(); | |
} else { | |
Animated.timing(modal.anim, { | |
toValue: 0, | |
duration: modal.animationDuration, | |
useNativeDriver: true | |
}).start(endResult => { | |
if (endResult.finished) { | |
setVisible(false); | |
} | |
}); | |
} | |
}, [props.opened]); | |
const invisible = visible === false; | |
return invisible ? null : ( | |
<Modal | |
statusBarTranslucent | |
animationType="none" | |
onRequestClose={modal.cancel} | |
onDismiss={modal.cancel} | |
hardwareAccelerated | |
transparent | |
> | |
<GestureHandlerRootViewNative style={styles.gesuteHandlerRoot}> | |
<AnimatePresence>{props.dialog}</AnimatePresence> | |
</GestureHandlerRootViewNative> | |
</Modal> | |
); | |
} | |
function AnimatePresence(props) { | |
const [child, setChild] = useState(() => { | |
if (Children.count(props.children)) { | |
return props.children; | |
} else { | |
return null; | |
} | |
}); | |
useEffect(() => { | |
const cnt = Children.count(props.children); | |
if (cnt >= 1) { | |
setChild(props.children); | |
} else if (cnt === 0) { | |
// never unmount | |
} | |
}, [props.children]); | |
return child; | |
} | |
const styles = StyleSheet.create({ | |
gesuteHandlerRoot: { | |
flex: 1 | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment