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',
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 {