feat: improvements to portal (#90)

- layer management according to position
- preserve theme context via ThemedPortal
This commit is contained in:
Satyajit Sahoo
2016-11-28 11:27:31 +05:30
committed by Ahmed Elhanafy
parent 4732b02a0f
commit a5519eefbd
8 changed files with 209 additions and 121 deletions

View File

@@ -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 (
<ThemeProvider>
<PaperProvider>
<NavigationProvider router={Router}>
<Drawer
onOpen={this._handleOpenDrawer}
@@ -73,7 +77,7 @@ class App extends Component {
/>
</Drawer>
</NavigationProvider>
</ThemeProvider>
</PaperProvider>
);
}
}

View File

@@ -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<void, Props, void> {
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<void, Props, void> {
'You need to wrap your root component in \'<PortalHost />\''
);
}
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() {

View File

@@ -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<void, Props, State> {
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 (
<View {...this.props}>
{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 }) => (
<View key={position} style={StyleSheet.absoluteFill}>
{items}
</View>
))
}
</View>
);
}
}

View File

@@ -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 <void, Props, void> {
static propTypes = {
children: PropTypes.element.isRequired,
theme: PropTypes.object.isRequired,
};
render() {
return (
<Portal {...this.props}>
<ThemeProvider theme={this.props.theme}>
{Children.only(this.props.children)}
</ThemeProvider>
</Portal>
);
}
}
export default withTheme(ThemedPortal);

View File

@@ -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<void, Props, State> {
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 (
<View style={[ styles.container, this.props.style ]}>
{this.props.children}
<View style={StyleSheet.absoluteFill}>
{Object.keys(portals).map(key =>
portals[key]
)}
</View>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
});

32
src/core/Provider.js Normal file
View File

@@ -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<void, Props, void> {
static propTypes = {
children: PropTypes.element.isRequired,
theme: PropTypes.object,
};
render() {
return (
<PortalHost>
<ThemeProvider theme={this.props.theme}>
{Children.only(this.props.children)}
</ThemeProvider>
</PortalHost>
);
}
}

View File

@@ -21,7 +21,7 @@ export const theme = 'react-native-paper$theme';
export default class ThemeProvider extends PureComponent<DefaultProps, Props, void> {
static propTypes = {
children: PropTypes.node.isRequired,
children: PropTypes.element.isRequired,
theme: PropTypes.object,
};

View File

@@ -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';