UNPKG

3.71 kBTypeScriptView Raw
1import * as React from 'react';
2import { View, StyleSheet } from 'react-native';
3import PortalManager from './PortalManager';
4
5type Props = {
6 children: React.ReactNode;
7};
8
9type Operation =
10 | { type: 'mount'; key: number; children: React.ReactNode }
11 | { type: 'update'; key: number; children: React.ReactNode }
12 | { type: 'unmount'; key: number };
13
14export type PortalMethods = {
15 mount: (children: React.ReactNode) => number;
16 update: (key: number, children: React.ReactNode) => void;
17 unmount: (key: number) => void;
18};
19
20export const PortalContext = React.createContext<PortalMethods>(null as any);
21
22/**
23 * Portal host renders all of its children `Portal` elements.
24 * For example, you can wrap a screen in `Portal.Host` to render items above the screen.
25 * If you're using the `Provider` component, it already includes `Portal.Host`.
26 *
27 * ## Usage
28 * ```js
29 * import * as React from 'react';
30 * import { Text } from 'react-native';
31 * import { Portal } from 'react-native-paper';
32 *
33 * const MyComponent = () => (
34 * <Portal.Host>
35 * <Text>Content of the app</Text>
36 * </Portal.Host>
37 * );
38 *
39 * export default MyComponent;
40 * ```
41 *
42 * Here any `Portal` elements under `<App />` are rendered alongside `<App />` and will appear above `<App />` like a `Modal`.
43 */
44export default class PortalHost extends React.Component<Props> {
45 static displayName = 'Portal.Host';
46
47 componentDidMount() {
48 const manager = this.manager;
49 const queue = this.queue;
50
51 while (queue.length && manager) {
52 const action = queue.pop();
53 if (action) {
54 // eslint-disable-next-line default-case
55 switch (action.type) {
56 case 'mount':
57 manager.mount(action.key, action.children);
58 break;
59 case 'update':
60 manager.update(action.key, action.children);
61 break;
62 case 'unmount':
63 manager.unmount(action.key);
64 break;
65 }
66 }
67 }
68 }
69
70 private setManager = (manager: PortalManager | undefined | null) => {
71 this.manager = manager;
72 };
73
74 private mount = (children: React.ReactNode) => {
75 const key = this.nextKey++;
76
77 if (this.manager) {
78 this.manager.mount(key, children);
79 } else {
80 this.queue.push({ type: 'mount', key, children });
81 }
82
83 return key;
84 };
85
86 private update = (key: number, children: React.ReactNode) => {
87 if (this.manager) {
88 this.manager.update(key, children);
89 } else {
90 const op: Operation = { type: 'mount', key, children };
91 const index = this.queue.findIndex(
92 (o) => o.type === 'mount' || (o.type === 'update' && o.key === key)
93 );
94
95 if (index > -1) {
96 this.queue[index] = op;
97 } else {
98 this.queue.push(op as Operation);
99 }
100 }
101 };
102
103 private unmount = (key: number) => {
104 if (this.manager) {
105 this.manager.unmount(key);
106 } else {
107 this.queue.push({ type: 'unmount', key });
108 }
109 };
110
111 private nextKey = 0;
112 private queue: Operation[] = [];
113 private manager: PortalManager | null | undefined;
114
115 render() {
116 return (
117 <PortalContext.Provider
118 value={{
119 mount: this.mount,
120 update: this.update,
121 unmount: this.unmount,
122 }}
123 >
124 {/* Need collapsable=false here to clip the elevations, otherwise they appear above Portal components */}
125 <View
126 style={styles.container}
127 collapsable={false}
128 pointerEvents="box-none"
129 >
130 {this.props.children}
131 </View>
132 <PortalManager ref={this.setManager} />
133 </PortalContext.Provider>
134 );
135 }
136}
137
138const styles = StyleSheet.create({
139 container: {
140 flex: 1,
141 },
142});