├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .prettierignore ├── LICENSE ├── README.md ├── package.json ├── renovate.json ├── src ├── Consumer.tsx ├── Host.tsx ├── Manager.tsx ├── Portal.tsx ├── hooks │ └── useKey.ts └── index.tsx └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | }, 6 | extends: [ 7 | 'plugin:@typescript-eslint/recommended', 8 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 9 | 'plugin:react/recommended', 10 | 'prettier/@typescript-eslint', 11 | ], 12 | parser: '@typescript-eslint/parser', 13 | parserOptions: { 14 | project: 'tsconfig.json', 15 | sourceType: 'module', 16 | ecmaFeatures: { jsx: true }, 17 | }, 18 | plugins: ['@typescript-eslint', 'react', 'react-native'], 19 | rules: { 20 | camelcase: 'off', 21 | '@typescript-eslint/camelcase': 'off', 22 | '@typescript-eslint/interface-name-prefix': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | '@typescript-eslint/ban-ts-ignore': 'off', 25 | '@typescript-eslint/no-use-before-define': 'off', 26 | 'react/display-name': 'off', 27 | }, 28 | settings: { 29 | react: { 30 | pragma: 'React', 31 | version: 'detect', 32 | }, 33 | }, 34 | ignorePatterns: ['node_modules/**/*', 'lib/**/*'], 35 | }; 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # node.js 6 | node_modules/ 7 | npm-debug.log 8 | yarn-error.log 9 | yarn.lock 10 | 11 | # misc 12 | # 13 | .expo/ 14 | .vscode/ 15 | lib/ 16 | .watchmanconfig 17 | package-lock.json 18 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Node 6 | # 7 | node_modules/ 8 | npm-debug.log 9 | yarn-error.log 10 | yarn.lock 11 | 12 | # Misc 13 | # 14 | .expo 15 | .vscode/ 16 | src/ 17 | .eslintrc.js 18 | .prettierignore 19 | .watchmanconfig 20 | README.md 21 | renovate.json 22 | tsconfig.json 23 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .expo 2 | .vscode 3 | docs 4 | lib 5 | node_modules 6 | android 7 | ios 8 | tsconfig.json 9 | package.json 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jérémy Barbet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Portalize 2 | 3 | [![npm version](https://badge.fury.io/js/react-native-portalize.svg)](https://badge.fury.io/js/react-native-portalize) 4 | 5 | The simplest way to render anything on top of the rest. 6 | 7 | This component is extracted from [`react-native-paper`](https://github.com/callstack/react-native-paper/tree/master/src/components/Portal) and has been simplified for the purpose of [`react-native-modalize`](https://github.com/jeremybarbet/react-native-modalize). 8 | 9 | ## Installation 10 | 11 | ```bash 12 | yarn add react-native-portalize 13 | ``` 14 | 15 | ## Usage 16 | 17 | ```tsx 18 | import React from 'react'; 19 | import { View, Text } from 'react-native'; 20 | import { Host, Portal } from 'react-native-portalize'; 21 | 22 | export const MyApp = () => ( 23 | 24 | 25 | Some copy here and there... 26 | 27 | 28 | A portal on top of the rest 29 | 30 | 31 | 32 | ); 33 | ``` 34 | 35 | **Example with `react-native-modalize` and `react-navigation`** 36 | 37 | ```tsx 38 | import React from 'react'; 39 | import { View, TouchableOpacity, Text } from 'react-native'; 40 | import { NavigationContainer } from '@react-navigation/native'; 41 | import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; 42 | import { Modalize } from 'react-native-modalize'; 43 | import { Host, Portal } from 'react-native-portalize'; 44 | 45 | const Tab = createBottomTabNavigator(); 46 | 47 | const ExamplesScreen = () => { 48 | const modalRef = useRef(null); 49 | 50 | const onOpen = () => { 51 | modalRef.current?.open(); 52 | }; 53 | 54 | return ( 55 | <> 56 | 57 | Open the modal 58 | 59 | 60 | 61 | ...your content 62 | 63 | 64 | ); 65 | }; 66 | 67 | const SettingsScreen = () => ( 68 | 69 | Settings screen 70 | 71 | ); 72 | 73 | export const App = () => ( 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | ); 83 | ``` 84 | 85 | ## Props 86 | 87 | ### Host 88 | 89 | - `children` 90 | 91 | A React node that will be most likely wrapping your whole app. 92 | 93 | | Type | Required | 94 | | ---- | -------- | 95 | | node | Yes | 96 | 97 | - `style` 98 | 99 | Optional props to define the style of the Host component. 100 | 101 | | Type | Required | 102 | | ----- | -------- | 103 | | style | No | 104 | 105 | ### Portal 106 | 107 | - `children` 108 | 109 | The React node you want to display on top of the rest. 110 | 111 | | Type | Required | 112 | | ---- | -------- | 113 | | node | Yes | 114 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-portalize", 3 | "version": "1.0.7", 4 | "description": "Render anything on top of the rest", 5 | "main": "lib/index.js", 6 | "types": "./lib/index.d.ts", 7 | "scripts": { 8 | "build": "rm -rf ./lib/* && yarn lint && tsc", 9 | "prepare": "yarn build", 10 | "lint": "eslint 'src/**/*.ts?(x)' && prettier --list-different \"**/*.{json,md,js,jsx,ts,tsx}\"", 11 | "prettier": "prettier --write \"src/**/*.{json,md,js,jsx,ts,tsx}\"" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/jeremybarbet/react-native-portalize.git" 16 | }, 17 | "keywords": [ 18 | "portal", 19 | "react-native", 20 | "react", 21 | "native", 22 | "ios", 23 | "android", 24 | "above", 25 | "top" 26 | ], 27 | "author": "Jérémy Barbet", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/jeremybarbet/react-native-portalize/issues" 31 | }, 32 | "homepage": "https://github.com/jeremybarbet/react-native-portalize#readme", 33 | "lint-staged": { 34 | "*.{ts,tsx,js,jsx,json,md}": [ 35 | "prettier --write" 36 | ], 37 | "*.{ts,tsx}": [ 38 | "eslint --fix" 39 | ] 40 | }, 41 | "prettier": { 42 | "singleQuote": true, 43 | "trailingComma": "all", 44 | "printWidth": 100, 45 | "arrowParens": "avoid", 46 | "semi": true 47 | }, 48 | "commitlint": { 49 | "extends": [ 50 | "@commitlint/config-conventional" 51 | ] 52 | }, 53 | "husky": { 54 | "hooks": { 55 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 56 | "pre-commit": "lint-staged" 57 | } 58 | }, 59 | "devDependencies": { 60 | "@commitlint/cli": "8.3.5", 61 | "@commitlint/config-conventional": "8.3.4", 62 | "@types/react": "16.9.34", 63 | "@types/react-native": "0.62.2", 64 | "@typescript-eslint/eslint-plugin": "2.29.0", 65 | "@typescript-eslint/parser": "2.29.0", 66 | "eslint": "6.8.0", 67 | "eslint-config-prettier": "6.11.0", 68 | "eslint-plugin-react": "7.19.0", 69 | "eslint-plugin-react-native": "3.8.1", 70 | "husky": "4.2.5", 71 | "lint-staged": "10.1.3", 72 | "prettier": "2.0.4", 73 | "react": "16.13.1", 74 | "react-native": "0.62.2", 75 | "typescript": "3.8.3" 76 | }, 77 | "peerDependencies": { 78 | "react": "> 15.0.0", 79 | "react-native": "> 0.50.0" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "groupName": "all dependencies", 4 | "separateMajorMinor": true, 5 | "groupSlug": "all", 6 | "automerge": false, 7 | "packageRules": [ 8 | { 9 | "packagePatterns": ["*"], 10 | "groupName": "all dependencies", 11 | "groupSlug": "all" 12 | } 13 | ], 14 | "lockFileMaintenance": { 15 | "enabled": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Consumer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { IProvider } from './Host'; 4 | 5 | interface IConsumerProps { 6 | children: React.ReactNode; 7 | manager: IProvider | null; 8 | } 9 | 10 | export const Consumer = ({ children, manager }: IConsumerProps): null => { 11 | const key = React.useRef(undefined); 12 | 13 | const checkManager = (): void => { 14 | if (!manager) { 15 | throw new Error('No portal manager defined'); 16 | } 17 | }; 18 | 19 | const handleInit = (): void => { 20 | checkManager(); 21 | key.current = manager?.mount(children); 22 | }; 23 | 24 | React.useEffect(() => { 25 | checkManager(); 26 | manager?.update(key.current, children); 27 | }, [children, manager]); 28 | 29 | React.useEffect(() => { 30 | handleInit(); 31 | 32 | return (): void => { 33 | checkManager(); 34 | manager?.unmount(key.current); 35 | }; 36 | }, []); 37 | 38 | return null; 39 | }; 40 | -------------------------------------------------------------------------------- /src/Host.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { View, ViewStyle } from 'react-native'; 3 | 4 | import { useKey } from './hooks/useKey'; 5 | import { Manager, IManagerHandles } from './Manager'; 6 | 7 | interface IHostProps { 8 | children: React.ReactNode; 9 | style?: ViewStyle; 10 | } 11 | 12 | export interface IProvider { 13 | mount(children: React.ReactNode): string; 14 | update(key?: string, children?: React.ReactNode): void; 15 | unmount(key?: string): void; 16 | } 17 | 18 | export const Context = React.createContext(null); 19 | 20 | export const Host = ({ children, style }: IHostProps): JSX.Element => { 21 | const managerRef = React.useRef(null); 22 | const queue: { 23 | type: 'mount' | 'update' | 'unmount'; 24 | key: string; 25 | children?: React.ReactNode; 26 | }[] = []; 27 | const { generateKey, removeKey } = useKey(); 28 | 29 | React.useEffect(() => { 30 | while (queue.length && managerRef.current) { 31 | const action = queue.pop(); 32 | 33 | if (action) { 34 | switch (action.type) { 35 | case 'mount': 36 | managerRef.current?.mount(action.key, action.children); 37 | break; 38 | case 'update': 39 | managerRef.current?.update(action.key, action.children); 40 | break; 41 | case 'unmount': 42 | managerRef.current?.unmount(action.key); 43 | break; 44 | } 45 | } 46 | } 47 | }, []); 48 | 49 | const mount = (children: React.ReactNode): string => { 50 | const key = generateKey(); 51 | 52 | if (managerRef.current) { 53 | managerRef.current.mount(key, children); 54 | } else { 55 | queue.push({ type: 'mount', key, children }); 56 | } 57 | 58 | return key; 59 | }; 60 | 61 | const update = (key: string, children: React.ReactNode): void => { 62 | if (managerRef.current) { 63 | managerRef.current.update(key, children); 64 | } else { 65 | const op = { type: 'mount' as 'mount', key, children }; 66 | const index = queue.findIndex( 67 | o => o.type === 'mount' || (o.type === 'update' && o.key === key), 68 | ); 69 | 70 | if (index > -1) { 71 | queue[index] = op; 72 | } else { 73 | queue.push(op); 74 | } 75 | } 76 | }; 77 | 78 | const unmount = (key: string): void => { 79 | if (managerRef.current) { 80 | managerRef.current.unmount(key); 81 | removeKey(key); 82 | } else { 83 | queue.push({ type: 'unmount', key }); 84 | } 85 | }; 86 | 87 | return ( 88 | 89 | 90 | {children} 91 | 92 | 93 | 94 | 95 | ); 96 | }; 97 | -------------------------------------------------------------------------------- /src/Manager.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { View, StyleSheet } from 'react-native'; 3 | 4 | export interface IManagerHandles { 5 | mount(key: string, children: React.ReactNode): void; 6 | update(key?: string, children?: React.ReactNode): void; 7 | unmount(key?: string): void; 8 | } 9 | 10 | export const Manager = React.forwardRef((_, ref): any => { 11 | const [portals, setPortals] = React.useState<{ key: string; children: React.ReactNode }[]>([]); 12 | 13 | React.useImperativeHandle( 14 | ref, 15 | (): IManagerHandles => ({ 16 | mount(key: string, children: React.ReactNode): void { 17 | setPortals(prev => [...prev, { key, children }]); 18 | }, 19 | 20 | update(key: string, children: React.ReactNode): void { 21 | setPortals(prev => 22 | prev.map(item => { 23 | if (item.key === key) { 24 | return { ...item, children }; 25 | } 26 | 27 | return item; 28 | }), 29 | ); 30 | }, 31 | 32 | unmount(key: string): void { 33 | setPortals(prev => prev.filter(item => item.key !== key)); 34 | }, 35 | }), 36 | ); 37 | 38 | return portals.map(({ key, children }, index: number) => ( 39 | 45 | {children} 46 | 47 | )); 48 | }); 49 | -------------------------------------------------------------------------------- /src/Portal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { Consumer } from './Consumer'; 4 | import { Context } from './Host'; 5 | 6 | interface IPortalProps { 7 | children: React.ReactNode; 8 | } 9 | 10 | export const Portal = ({ children }: IPortalProps): JSX.Element => ( 11 | 12 | {(manager): JSX.Element => {children}} 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /src/hooks/useKey.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface IUseKey { 4 | generateKey(): string; 5 | removeKey(key: string): void; 6 | } 7 | 8 | // Generates a random key 9 | const keyGenerator = (): string => { 10 | return `portalize_${Math.random().toString(36).substr(2, 16)}-${Math.random() 11 | .toString(36) 12 | .substr(2, 16)}-${Math.random().toString(36).substr(2, 16)}`; 13 | }; 14 | 15 | // Custom hook that checks for uniqueness and retries if clashes 16 | export const useKey = (): IUseKey => { 17 | const usedKeys = React.useRef>([]); 18 | 19 | const generateKey = (): string => { 20 | let foundUniqueKey = false; 21 | let newKey = ''; 22 | let tries = 0; 23 | 24 | while (!foundUniqueKey && tries < 3) { 25 | // limit number of tries to stop endless loop of pain 26 | tries++; 27 | newKey = keyGenerator(); 28 | 29 | if (!usedKeys.current.includes(newKey)) { 30 | foundUniqueKey = true; 31 | } 32 | } 33 | 34 | // will only run if exited while loop without finding a unique key 35 | if (!foundUniqueKey) { 36 | newKey = `portalize_${Date.now()}_${Math.floor(Math.random() * 1000)}`; // fallback method 37 | } 38 | 39 | usedKeys.current.push(newKey); 40 | return newKey; 41 | }; 42 | 43 | // Removes our key to make it 'available' again 44 | const removeKey = (key: string): void => { 45 | usedKeys.current = usedKeys.current.filter(k => k !== key); 46 | }; 47 | 48 | return { generateKey, removeKey }; 49 | }; 50 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | export { Consumer } from './Consumer'; 2 | export { Host } from './Host'; 3 | export { Manager } from './Manager'; 4 | export { Portal } from './Portal'; 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "lib/", 4 | "allowSyntheticDefaultImports": true, 5 | "declaration": true, 6 | "jsx": "react", 7 | "lib": ["es2017", "dom"], 8 | "target": "es2015", 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "noEmitOnError": true, 12 | "noImplicitAny": false, 13 | "esModuleInterop": true, 14 | "skipLibCheck": true, 15 | "strict": true, 16 | "pretty": true, 17 | "removeComments": false, 18 | "sourceMap": false, 19 | "strictPropertyInitialization": false, 20 | "noUnusedLocals": true, 21 | "suppressImplicitAnyIndexErrors": true, 22 | "noUnusedParameters": true 23 | }, 24 | "include": ["src"] 25 | } 26 | --------------------------------------------------------------------------------