From faf763143df6233a847408c1d22bf2581ce1f08b Mon Sep 17 00:00:00 2001 From: Mo Gorhom Date: Sat, 31 Jul 2021 13:06:13 +0100 Subject: [PATCH 1/2] feat: added contained prop and native modal implementation --- src/components/portal/Portal.tsx | 59 +++++++++++++++-- src/components/portal/types.d.ts | 17 ++++- .../portalContainer/PortalContainer.tsx | 20 ++++++ src/components/portalContainer/index.ts | 1 + src/components/portalContainer/types.d.ts | 10 +++ src/components/portalHost/PortalHost.tsx | 9 ++- src/components/portalHost/types.d.ts | 7 ++ src/hooks/usePortal.ts | 66 ++++++++++--------- 8 files changed, 149 insertions(+), 40 deletions(-) create mode 100644 src/components/portalContainer/PortalContainer.tsx create mode 100644 src/components/portalContainer/index.ts create mode 100644 src/components/portalContainer/types.d.ts diff --git a/src/components/portal/Portal.tsx b/src/components/portal/Portal.tsx index 06a8b6a..27602e3 100644 --- a/src/components/portal/Portal.tsx +++ b/src/components/portal/Portal.tsx @@ -1,14 +1,22 @@ -import { memo, useEffect, useMemo } from 'react'; +import React, { memo, useEffect, useMemo } from 'react'; import { nanoid } from 'nanoid/non-secure'; import { usePortal } from '../../hooks'; import type { PortalProps } from './types'; +import { Modal } from 'react-native'; const PortalComponent = ({ name: _providedName, hostName, + contained = true, + children, handleOnMount, handleOnUnmount, - children, + + // modal props + animationType = 'none', + transparent = true, + hardwareAccelerated, + statusBarTranslucent, }: PortalProps) => { //#region hooks const { addPortal, removePortal, updatePortal } = usePortal(hostName); @@ -20,15 +28,35 @@ const PortalComponent = ({ //#region effects useEffect(() => { + /** + * if portal is not contained, then + * we skip adding portal to the host. + */ + if (!contained) { + if (handleOnMount) { + handleOnMount(); + } + return; + } + if (handleOnMount) { handleOnMount(() => addPortal(name, children)); } else { addPortal(name, children); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - useEffect(() => { + return () => { + /** + * if portal is not contained, then + * we skip removing portal to the host. + */ + if (!contained) { + if (handleOnUnmount) { + handleOnUnmount(); + } + return; + } + if (handleOnUnmount) { handleOnUnmount(() => removePortal(name)); } else { @@ -38,12 +66,31 @@ const PortalComponent = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { + if (!contained) { + return; + } + updatePortal(name, children); // eslint-disable-next-line react-hooks/exhaustive-deps }, [children]); //#endregion - return null; + //#region render + if (contained) { + return null; + } + + return ( + + {children} + + ); + //#endregion }; const Portal = memo(PortalComponent); diff --git a/src/components/portal/types.d.ts b/src/components/portal/types.d.ts index 0f53b70..31a2ac6 100644 --- a/src/components/portal/types.d.ts +++ b/src/components/portal/types.d.ts @@ -1,6 +1,14 @@ import type { ReactNode } from 'react'; +import type { ModalProps } from 'react-native'; -export interface PortalProps { +export interface PortalProps + extends Pick< + ModalProps, + | 'animationType' + | 'transparent' + | 'hardwareAccelerated' + | 'statusBarTranslucent' + > { /** * Portal's key or name to be used as an identifer. * @type string @@ -13,6 +21,13 @@ export interface PortalProps { * @default 'root' */ hostName?: string; + /** + * Determines whether the portal will be rendered under the + * react native root view or native root view. + * @type boolean + * @default true + */ + contained?: boolean; /** * Override internal mounting functionality, this is useful * if you want to trigger any action before mounting the portal content. diff --git a/src/components/portalContainer/PortalContainer.tsx b/src/components/portalContainer/PortalContainer.tsx new file mode 100644 index 0000000..3661cfb --- /dev/null +++ b/src/components/portalContainer/PortalContainer.tsx @@ -0,0 +1,20 @@ +import React, { memo } from 'react'; +import { Modal } from 'react-native'; +import type { PortalContainerProps } from './types'; + +const PortalContainerComponent = ({ + contained = true, + children, +}: PortalContainerProps) => { + return contained ? ( + children + ) : ( + + {children} + + ); +}; + +const PortalContainer = memo(PortalContainerComponent); + +export default PortalContainer; diff --git a/src/components/portalContainer/index.ts b/src/components/portalContainer/index.ts new file mode 100644 index 0000000..48a30bb --- /dev/null +++ b/src/components/portalContainer/index.ts @@ -0,0 +1 @@ +export { default } from './PortalContainer'; diff --git a/src/components/portalContainer/types.d.ts b/src/components/portalContainer/types.d.ts new file mode 100644 index 0000000..c47bc39 --- /dev/null +++ b/src/components/portalContainer/types.d.ts @@ -0,0 +1,10 @@ +export interface PortalContainerProps { + /** + * Determines whether the portal host will be rendered under the + * react native root view or native root view. + * @type boolean + * @default true + */ + contained?: boolean; + children: any; +} diff --git a/src/components/portalHost/PortalHost.tsx b/src/components/portalHost/PortalHost.tsx index 07fbbd0..f593244 100644 --- a/src/components/portalHost/PortalHost.tsx +++ b/src/components/portalHost/PortalHost.tsx @@ -1,8 +1,9 @@ import React, { memo, useEffect } from 'react'; import { usePortal, usePortalState } from '../../hooks'; +import PortalContainer from '../portalContainer'; import type { PortalHostProps } from './types'; -const PortalHostComponent = ({ name }: PortalHostProps) => { +const PortalHostComponent = ({ name, contained = true }: PortalHostProps) => { //#region hooks const state = usePortalState(name); const { registerHost, deregisterHost } = usePortal(name); @@ -19,7 +20,11 @@ const PortalHostComponent = ({ name }: PortalHostProps) => { //#endregion //#region render - return <>{state.map(item => item.node)}; + return ( + + {state.map(item => item.node)} + + ); //#endregion }; diff --git a/src/components/portalHost/types.d.ts b/src/components/portalHost/types.d.ts index fbd436c..d3c6114 100644 --- a/src/components/portalHost/types.d.ts +++ b/src/components/portalHost/types.d.ts @@ -4,4 +4,11 @@ export interface PortalHostProps { * @type string */ name: string; + /** + * Determines whether the container will be rendered under the + * react native root view or native root view. + * @type boolean + * @default true + */ + contained?: boolean; } diff --git a/src/hooks/usePortal.ts b/src/hooks/usePortal.ts index 3ce1db8..08bbc69 100644 --- a/src/hooks/usePortal.ts +++ b/src/hooks/usePortal.ts @@ -17,45 +17,49 @@ export const usePortal = (hostName: string = 'root') => { type: ACTIONS.REGISTER_HOST, hostName: hostName, }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [dispatch, hostName]); const deregisterHost = useCallback(() => { dispatch({ type: ACTIONS.DEREGISTER_HOST, - hostName: hostName, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const addPortal = useCallback((name: string, node: ReactNode) => { - dispatch({ - type: ACTIONS.ADD_PORTAL, hostName, - portalName: name, - node, }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [dispatch, hostName]); - const updatePortal = useCallback((name: string, node: ReactNode) => { - dispatch({ - type: ACTIONS.UPDATE_PORTAL, - hostName, - portalName: name, - node, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const addPortal = useCallback( + (name: string, node: ReactNode) => { + dispatch({ + type: ACTIONS.ADD_PORTAL, + hostName, + portalName: name, + node, + }); + }, + [dispatch, hostName] + ); - const removePortal = useCallback((name: string) => { - dispatch({ - type: ACTIONS.REMOVE_PORTAL, - hostName, - portalName: name, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const updatePortal = useCallback( + (name: string, node: ReactNode) => { + dispatch({ + type: ACTIONS.UPDATE_PORTAL, + hostName, + portalName: name, + node, + }); + }, + [dispatch, hostName] + ); + + const removePortal = useCallback( + (name: string) => { + dispatch({ + type: ACTIONS.REMOVE_PORTAL, + hostName, + portalName: name, + }); + }, + [dispatch, hostName] + ); //#endregion return { From 5cd0816fbff652d5803b8b6c18f28bb189979f78 Mon Sep 17 00:00:00 2001 From: Mo Gorhom Date: Sat, 31 Jul 2021 13:07:01 +0100 Subject: [PATCH 2/2] chore: added native modal example --- example/src/components/SimpleNativeModal.tsx | 46 +++++++++++++++++++ example/src/screens/NativeModalScreen.tsx | 47 ++++++++++++++++++++ example/src/screens/index.ts | 5 +++ 3 files changed, 98 insertions(+) create mode 100644 example/src/components/SimpleNativeModal.tsx create mode 100644 example/src/screens/NativeModalScreen.tsx diff --git a/example/src/components/SimpleNativeModal.tsx b/example/src/components/SimpleNativeModal.tsx new file mode 100644 index 0000000..1d68df1 --- /dev/null +++ b/example/src/components/SimpleNativeModal.tsx @@ -0,0 +1,46 @@ +import { useRoute } from '@react-navigation/native'; +import React from 'react'; +import { + GestureResponderEvent, + StyleSheet, + Text, + TouchableWithoutFeedback, + View, +} from 'react-native'; + +interface SimpleNativeModalProps { + onPress: (event: GestureResponderEvent) => void; +} + +const SimpleNativeModal = ({ onPress }: SimpleNativeModalProps) => { + const { name } = useRoute(); + return ( + + + + Current Screen: {name} + + + + ); +}; + +const styles = StyleSheet.create({ + backdropContainer: { + ...StyleSheet.absoluteFillObject, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + justifyContent: 'center', + alignItems: 'center', + }, + buttonContainer: { + ...StyleSheet.absoluteFillObject, + justifyContent: 'center', + alignItems: 'center', + }, + modalContainer: { + padding: 24, + backgroundColor: 'white', + }, +}); + +export default SimpleNativeModal; diff --git a/example/src/screens/NativeModalScreen.tsx b/example/src/screens/NativeModalScreen.tsx new file mode 100644 index 0000000..00ca963 --- /dev/null +++ b/example/src/screens/NativeModalScreen.tsx @@ -0,0 +1,47 @@ +import React, { useCallback, useState } from 'react'; +import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { Portal } from '@gorhom/portal'; +import SimpleNativeModal from '../components/SimpleNativeModal'; + +const NativeModalScreen = () => { + const [showModal, setShowModal] = useState(false); + + const handleOnModalPress = useCallback(() => { + setShowModal(state => !state); + }, []); + + return ( + + + + {showModal ? 'Hide' : 'Show'} Native Modal + + + {showModal && ( + + + + )} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + alignContent: 'center', + }, + button: { + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 24, + backgroundColor: '#333', + }, + text: { + color: 'white', + }, +}); + +export default NativeModalScreen; diff --git a/example/src/screens/index.ts b/example/src/screens/index.ts index 20ee62d..ff95696 100644 --- a/example/src/screens/index.ts +++ b/example/src/screens/index.ts @@ -12,6 +12,11 @@ export const screens = [ slug: 'modal', getScreen: () => require('./ModalScreen').default, }, + { + name: 'Native Modal', + slug: 'native-modal', + getScreen: () => require('./NativeModalScreen').default, + }, { name: 'Popover', slug: 'popover',