From a5519eefbd6b5ccd66ea2661fad110aef2a455cb Mon Sep 17 00:00:00 2001 From: Satyajit Sahoo Date: Mon, 28 Nov 2016 11:27:31 +0530 Subject: [PATCH] feat: improvements to portal (#90) - layer management according to position - preserve theme context via ThemedPortal --- example/main.js | 10 ++- src/components/{ => Portal}/Portal.js | 20 +++-- src/components/Portal/PortalHost.js | 115 ++++++++++++++++++++++++++ src/components/Portal/ThemedPortal.js | 38 +++++++++ src/core/PortalHost.js | 111 ------------------------- src/core/Provider.js | 32 +++++++ src/core/ThemeProvider.js | 2 +- src/index.js | 2 +- 8 files changed, 209 insertions(+), 121 deletions(-) rename src/components/{ => Portal}/Portal.js (63%) create mode 100644 src/components/Portal/PortalHost.js create mode 100644 src/components/Portal/ThemedPortal.js delete mode 100644 src/core/PortalHost.js create mode 100644 src/core/Provider.js diff --git a/example/main.js b/example/main.js index 5fb36cf..d1bdd1f 100644 --- a/example/main.js +++ b/example/main.js @@ -11,7 +11,11 @@ import { NavigationProvider, StackNavigation, } from '@exponent/ex-navigation'; -import { Colors, ThemeProvider, Drawer } from 'react-native-paper'; +import { + Colors, + Drawer, + Provider as PaperProvider, +} from 'react-native-paper'; import Router from './src/Router'; @@ -53,7 +57,7 @@ class App extends Component { render() { return ( - + - + ); } } diff --git a/src/components/Portal.js b/src/components/Portal/Portal.js similarity index 63% rename from src/components/Portal.js rename to src/components/Portal/Portal.js index 54be528..af202a9 100644 --- a/src/components/Portal.js +++ b/src/components/Portal/Portal.js @@ -4,14 +4,24 @@ import { PureComponent, PropTypes, } from 'react'; -import { manager } from '../core/PortalHost'; +import { manager } from './PortalHost'; -type Props = { - children?: any; +export type PortalProps = { + children: any; + position: number; } +type Props = PortalProps; + +/** + * Portal allows to render a component at a different place in the parent tree. + */ export default class Portal extends PureComponent { static propTypes = { + /** + * Position of the element in the z-axis + */ + position: PropTypes.number, children: PropTypes.node.isRequired, }; @@ -26,11 +36,11 @@ export default class Portal extends PureComponent { 'You need to wrap your root component in \'\'' ); } - this._key = this.context[manager].mount(this.props.children); + this._key = this.context[manager].mount(this.props); } componentDidUpdate() { - this.context[manager].update(this._key, this.props.children); + this.context[manager].update(this._key, this.props); } componentWillUnmount() { diff --git a/src/components/Portal/PortalHost.js b/src/components/Portal/PortalHost.js new file mode 100644 index 0000000..7d2298b --- /dev/null +++ b/src/components/Portal/PortalHost.js @@ -0,0 +1,115 @@ +/* @flow */ + +import React, { + PureComponent, + PropTypes, +} from 'react'; +import { + View, + StyleSheet, +} from 'react-native'; +import type { PortalProps } from './Portal'; + +type Props = { + children?: any; +} + +type State = { + portals: Array<{ + key: number; + props: PortalProps; + }>; +} + +export const manager = 'react-native-paper$portal-manager'; + +/** + * Portal host is the component which actually renders all Portals. + */ +export default class Portals extends PureComponent { + static propTypes = { + children: PropTypes.node.isRequired, + }; + + static childContextTypes = { + [manager]: PropTypes.object, + }; + + state = { + portals: [], + }; + + getChildContext() { + return { + [manager]: { + mount: this._mountPortal, + unmount: this._unmountPortal, + update: this._updatePortal, + }, + }; + } + + _nextId = 0; + + _mountPortal = (props: PortalProps) => { + const portals = this.state.portals; + this.setState({ + portals: portals.concat({ key: this._nextId, props }), + }); + return this._nextId++; + }; + + _unmountPortal = (key: number) => { + const portals = this.state.portals; + this.setState({ + portals: portals.filter(item => item.key !== key), + }); + }; + + _updatePortal = (key: number, props: PortalProps) => { + const portals = this.state.portals; + this.setState({ + portals: portals.map(item => { + if (item.key === key) { + return { ...item, props }; + } + return item; + }), + }); + }; + + render() { + const { portals } = this.state; + return ( + + {this.props.children} + {portals + .reduce((acc, curr) => { + const { position = 0, children } = curr.props; + let group = acc.find(it => it.position === position); + if (group) { + group = { + position, + items: group.items.concat([ children ]), + }; + return acc.map(g => { + if (group && g.position === position) { + return group; + } + return g; + }); + } else { + group = { position, items: [ children ] }; + return [ ...acc, group ]; + } + }, []) + .map(({ position, items }) => ( + + {items} + + )) + } + + ); + } +} diff --git a/src/components/Portal/ThemedPortal.js b/src/components/Portal/ThemedPortal.js new file mode 100644 index 0000000..152f2f2 --- /dev/null +++ b/src/components/Portal/ThemedPortal.js @@ -0,0 +1,38 @@ +/* @flow */ + +import React, { + Component, + Children, + PropTypes, +} from 'react'; +import Portal from './Portal'; +import ThemeProvider from '../../core/ThemeProvider'; +import withTheme from '../../core/withTheme'; +import type { Theme } from '../../types/Theme'; + +type Props = { + children?: any; + theme: Theme; +} + +/** + * Themed portal is a special portal which preserves the theme in the context. + */ +class ThemedPortal extends Component { + static propTypes = { + children: PropTypes.element.isRequired, + theme: PropTypes.object.isRequired, + }; + + render() { + return ( + + + {Children.only(this.props.children)} + + + ); + } +} + +export default withTheme(ThemedPortal); diff --git a/src/core/PortalHost.js b/src/core/PortalHost.js deleted file mode 100644 index 1c1027b..0000000 --- a/src/core/PortalHost.js +++ /dev/null @@ -1,111 +0,0 @@ -/* @flow */ - -import React, { - PureComponent, - isValidElement, - PropTypes, -} from 'react'; -import { - View, - StyleSheet, -} from 'react-native'; - -type Props = { - children?: any; - style?: any; -} - -type State = { - portals: { [key: string]: ?React.Element<*> }; -} - -export const manager = 'react-native-paper$portal-manager'; - -export default class PortalHost extends PureComponent { - static propTypes = { - children: PropTypes.node.isRequired, - style: View.propTypes.style, - }; - - static childContextTypes = { - [manager]: PropTypes.object, - }; - - state = { - portals: {}, - }; - - getChildContext() { - return { - [manager]: { - mount: this._mountPortal, - unmount: this._unmountPortal, - update: this._updatePortal, - }, - }; - } - - _nextId = 0; - - _mountPortal = (portal: ?React.Element<*>) => { - const { portals } = this.state; - - if (isValidElement(portal)) { - this.setState({ - portals: { - ...portals, - [this._nextId]: portal, - }, - }); - return this._nextId++; - } - - return null; - }; - - _unmountPortal = (key: string) => { - let { portals } = this.state; - - if (portals.hasOwnProperty(key)) { - portals = { ...portals }; - delete portals[key]; - - this.setState({ - portals, - }); - } - }; - - _updatePortal = (key: string, portal: ?React.Element<*>) => { - const { portals } = this.state; - - if (isValidElement(portal)) { - this.setState({ - portals: { - ...portals, - [key]: portal, - }, - }); - } - }; - - render() { - const { portals } = this.state; - return ( - - {this.props.children} - - {Object.keys(portals).map(key => - portals[key] - )} - - - ); - } -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, -}); diff --git a/src/core/Provider.js b/src/core/Provider.js new file mode 100644 index 0000000..ce363dc --- /dev/null +++ b/src/core/Provider.js @@ -0,0 +1,32 @@ +/* @flow */ + +import React, { + PureComponent, + PropTypes, + Children, +} from 'react'; +import ThemeProvider from './ThemeProvider'; +import PortalHost from '../components/Portal/PortalHost'; +import type { Theme } from '../types/Theme'; + +type Props = { + children?: any; + theme?: Theme +} + +export default class Provider extends PureComponent { + static propTypes = { + children: PropTypes.element.isRequired, + theme: PropTypes.object, + }; + + render() { + return ( + + + {Children.only(this.props.children)} + + + ); + } +} diff --git a/src/core/ThemeProvider.js b/src/core/ThemeProvider.js index 4617f95..7a6c406 100644 --- a/src/core/ThemeProvider.js +++ b/src/core/ThemeProvider.js @@ -21,7 +21,7 @@ export const theme = 'react-native-paper$theme'; export default class ThemeProvider extends PureComponent { static propTypes = { - children: PropTypes.node.isRequired, + children: PropTypes.element.isRequired, theme: PropTypes.object, }; diff --git a/src/index.js b/src/index.js index 2cc0fbb..890c436 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,7 @@ export { default as withTheme } from './core/withTheme'; export { default as ThemeProvider } from './core/ThemeProvider'; -export { default as PortalHost } from './core/PortalHost'; +export { default as Provider } from './core/Provider'; export * as Colors from './styles/colors';