├── .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 | [](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 |
--------------------------------------------------------------------------------