Skip to content

Instantly share code, notes, and snippets.

@Noitidart
Created June 3, 2021 16:30
Show Gist options
  • Save Noitidart/9715c38eb28ec47fa62fa17153fdab01 to your computer and use it in GitHub Desktop.
Save Noitidart/9715c38eb28ec47fa62fa17153fdab01 to your computer and use it in GitHub Desktop.
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