Skip to content

feat: added native modal support #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions example/src/components/SimpleNativeModal.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<TouchableWithoutFeedback onPress={onPress} style={styles.buttonContainer}>
<View style={styles.backdropContainer}>
<View style={styles.modalContainer}>
<Text>Current Screen: {name}</Text>
</View>
</View>
</TouchableWithoutFeedback>
);
};

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;
47 changes: 47 additions & 0 deletions example/src/screens/NativeModalScreen.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<View style={styles.container}>
<TouchableOpacity style={styles.button} onPress={handleOnModalPress}>
<Text style={styles.text}>
{showModal ? 'Hide' : 'Show'} Native Modal
</Text>
</TouchableOpacity>
{showModal && (
<Portal name="modal" contained={false}>
<SimpleNativeModal onPress={handleOnModalPress} />
</Portal>
)}
</View>
);
};

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;
5 changes: 5 additions & 0 deletions example/src/screens/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
59 changes: 53 additions & 6 deletions src/components/portal/Portal.tsx
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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 {
Expand All @@ -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 (
<Modal
animationType={animationType}
transparent={transparent}
hardwareAccelerated={hardwareAccelerated}
statusBarTranslucent={statusBarTranslucent}
>
{children}
</Modal>
);
//#endregion
};

const Portal = memo(PortalComponent);
Expand Down
17 changes: 16 additions & 1 deletion src/components/portal/types.d.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
Expand Down
20 changes: 20 additions & 0 deletions src/components/portalContainer/PortalContainer.tsx
Original file line number Diff line number Diff line change
@@ -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
) : (
<Modal transparent={true} animationType="none">
{children}
</Modal>
);
};

const PortalContainer = memo(PortalContainerComponent);

export default PortalContainer;
1 change: 1 addition & 0 deletions src/components/portalContainer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './PortalContainer';
10 changes: 10 additions & 0 deletions src/components/portalContainer/types.d.ts
Original file line number Diff line number Diff line change
@@ -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;
}
9 changes: 7 additions & 2 deletions src/components/portalHost/PortalHost.tsx
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -19,7 +20,11 @@ const PortalHostComponent = ({ name }: PortalHostProps) => {
//#endregion

//#region render
return <>{state.map(item => item.node)}</>;
return (
<PortalContainer contained={contained}>
{state.map(item => item.node)}
</PortalContainer>
);
//#endregion
};

Expand Down
7 changes: 7 additions & 0 deletions src/components/portalHost/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
66 changes: 35 additions & 31 deletions src/hooks/usePortal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down