initial replacement with new context API - WIP

This commit is contained in:
Stanislav Miklik 2018-04-06 10:30:47 +02:00
parent 37d0a0f8e1
commit 1bfab39fa2
8 changed files with 102 additions and 66 deletions

View File

@ -57,7 +57,7 @@ describe('MenuTrigger', () => {
<MenuTrigger menuName='menu1' /> <MenuTrigger menuName='menu1' />
); );
const menuActions = { openMenu: createSpy() }; const menuActions = { openMenu: createSpy() };
instance.context = { menuActions }; instance.props.ctx = { menuActions };
nthChild(output, 1).props.onPress(); nthChild(output, 1).props.onPress();
expect(menuActions.openMenu).toHaveBeenCalledWith('menu1'); expect(menuActions.openMenu).toHaveBeenCalledWith('menu1');
expect(menuActions.openMenu.calls.count()).toEqual(1); expect(menuActions.openMenu.calls.count()).toEqual(1);
@ -68,7 +68,7 @@ describe('MenuTrigger', () => {
<MenuTrigger menuName='menu1' disabled={true} /> <MenuTrigger menuName='menu1' disabled={true} />
); );
const menuActions = { openMenu: createSpy() }; const menuActions = { openMenu: createSpy() };
instance.context = { menuActions }; instance.props.ctx = { menuActions };
nthChild(output, 1).props.onPress(); nthChild(output, 1).props.onPress();
expect(menuActions.openMenu).not.toHaveBeenCalled(); expect(menuActions.openMenu).not.toHaveBeenCalled();
}); });

View File

@ -1,6 +1,6 @@
{ {
"name": "react-native-popup-menu", "name": "react-native-popup-menu",
"version": "0.12.3", "version": "0.13.0-rc",
"description": "extensible popup/context menu for react native", "description": "extensible popup/context menu for react native",
"main": "src/index.js", "main": "src/index.js",
"directories": { "directories": {
@ -61,5 +61,6 @@
"react-dom": "^15.5.4", "react-dom": "^15.5.4",
"react-test-renderer": "^15.6.1", "react-test-renderer": "^15.6.1",
"sinon": "^2.2.0" "sinon": "^2.2.0"
} },
"dependencies": {}
} }

View File

@ -5,17 +5,19 @@ import { MenuOptions, MenuTrigger } from './index';
import ContextMenu from './renderers/ContextMenu'; import ContextMenu from './renderers/ContextMenu';
import { makeName } from './helpers'; import { makeName } from './helpers';
import { debug } from './logger'; import { debug } from './logger';
import { withCtx } from './MenuProvider';
const isRegularComponent = c => c.type !== MenuOptions && c.type !== MenuTrigger; const isRegularComponent = c => c.type !== MenuOptions && c.type !== MenuTrigger;
const isTrigger = c => c.type === MenuTrigger; const isTrigger = c => c.type === MenuTrigger;
const isMenuOptions = c => c.type === MenuOptions; const isMenuOptions = c => c.type === MenuOptions;
export default class Menu extends Component { class Menu extends Component {
constructor(props, ctx) { constructor(props) {
super(props, ctx); super(props);
this._name = this.props.name || makeName(); this._name = this.props.name || makeName();
this._forceClose = false; this._forceClose = false;
const { ctx } = props;
if(!(ctx && ctx.menuActions)) { if(!(ctx && ctx.menuActions)) {
throw new Error("Menu component must be ancestor of MenuProvider"); throw new Error("Menu component must be ancestor of MenuProvider");
} }
@ -26,24 +28,24 @@ export default class Menu extends Component {
return; return;
} }
debug('subscribing menu', this._name); debug('subscribing menu', this._name);
this.context.menuRegistry.subscribe(this); this.props.ctx.menuRegistry.subscribe(this);
this.context.menuActions._notify(); this.props.ctx.menuActions._notify();
} }
componentDidUpdate() { componentDidUpdate() {
// force update if menu is opened as its content might have changed // force update if menu is opened as its content might have changed
const force = this._isOpen(); const force = this._isOpen();
debug('component did update', this._name, force); debug('component did update', this._name, force);
this.context.menuActions._notify(force); this.props.ctx.menuActions._notify(force);
} }
componentWillUnmount() { componentWillUnmount() {
debug('unsubscribing menu', this._name); debug('unsubscribing menu', this._name);
if (this._isOpen()) { if (this._isOpen()) {
this._forceClose = true; this._forceClose = true;
this.context.menuActions._notify(); this.props.ctx.menuActions._notify();
} }
this.context.menuRegistry.unsubscribe(this); this.props.ctx.menuRegistry.unsubscribe(this);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
@ -53,11 +55,11 @@ export default class Menu extends Component {
} }
open() { open() {
return this.context.menuActions.openMenu(this._name); return this.props.ctx.menuActions.openMenu(this._name);
} }
close() { close() {
return this.context.menuActions.closeMenu(); return this.props.ctx.menuActions.closeMenu();
} }
getName() { getName() {
@ -156,7 +158,4 @@ Menu.defaultProps = {
onBackdropPress: () => {}, onBackdropPress: () => {},
}; };
Menu.contextTypes = { export default withCtx(Menu);
menuRegistry: PropTypes.object,
menuActions: PropTypes.object,
};

View File

@ -3,8 +3,10 @@ import PropTypes from 'prop-types';
import { View, StyleSheet, Text } from 'react-native'; import { View, StyleSheet, Text } from 'react-native';
import { debug } from './logger'; import { debug } from './logger';
import { makeTouchable } from './helpers'; import { makeTouchable } from './helpers';
import { withCtx } from './MenuProvider';
export default class MenuOption extends Component {
class MenuOption extends Component {
_onSelect() { _onSelect() {
const { value } = this.props; const { value } = this.props;
@ -12,17 +14,17 @@ export default class MenuOption extends Component {
const shouldClose = onSelect(value) !== false; const shouldClose = onSelect(value) !== false;
debug('select option', value, shouldClose); debug('select option', value, shouldClose);
if (shouldClose) { if (shouldClose) {
this.context.menuActions.closeMenu(); this.props.ctx.menuActions.closeMenu();
} }
} }
_getMenusOnSelect() { _getMenusOnSelect() {
const menu = this.context.menuActions._getOpenedMenu(); const menu = this.props.ctx.menuActions._getOpenedMenu();
return menu.instance.props.onSelect; return menu.instance.props.onSelect;
} }
_getCustomStyles() { _getCustomStyles() {
const { optionsCustomStyles } = this.context.menuActions._getOpenedMenu(); const { optionsCustomStyles } = this.props.ctx.menuActions._getOpenedMenu();
return { return {
...optionsCustomStyles, ...optionsCustomStyles,
...this.props.customStyles, ...this.props.customStyles,
@ -81,10 +83,6 @@ MenuOption.defaultProps = {
customStyles: {}, customStyles: {},
}; };
MenuOption.contextTypes = {
menuActions: PropTypes.object,
};
const defaultStyles = StyleSheet.create({ const defaultStyles = StyleSheet.create({
option: { option: {
padding: 5, padding: 5,
@ -94,3 +92,5 @@ const defaultStyles = StyleSheet.create({
color: '#ccc', color: '#ccc',
}, },
}); });
export default withCtx(MenuOption);

View File

@ -1,14 +1,15 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View } from 'react-native'; import { View } from 'react-native';
import { withCtx } from './MenuProvider';
class MenuOptions extends React.Component { class _MenuOptions extends React.Component {
updateCustomStyles(_props) { updateCustomStyles(_props) {
const { customStyles } = _props const { customStyles } = _props
const menu = this.context.menuActions._getOpenedMenu() const menu = this.props.ctx.menuActions._getOpenedMenu()
const menuName = menu.instance.getName() const menuName = menu.instance.getName()
this.context.menuRegistry.setOptionsCustomStyles(menuName, customStyles) this.props.ctx.menuRegistry.setOptionsCustomStyles(menuName, customStyles)
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
@ -29,6 +30,8 @@ class MenuOptions extends React.Component {
} }
} }
const MenuOptions = withCtx(_MenuOptions)
MenuOptions.propTypes = { MenuOptions.propTypes = {
customStyles: PropTypes.object, customStyles: PropTypes.object,
renderOptionsContainer: PropTypes.func, renderOptionsContainer: PropTypes.func,
@ -43,9 +46,4 @@ MenuOptions.defaultProps = {
customStyles: {}, customStyles: {},
}; };
MenuOptions.contextTypes = {
menuRegistry: PropTypes.object,
menuActions: PropTypes.object,
};
export default MenuOptions; export default MenuOptions;

View File

@ -1,6 +1,8 @@
import React, { Component } from 'react'; import React, { Component, createContext } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, BackHandler } from 'react-native'; import { View, BackHandler } from 'react-native';
import { withContext } from './with-context';
import makeMenuRegistry from './menuRegistry'; import makeMenuRegistry from './menuRegistry';
import MenuPlaceholder from './MenuPlaceholder'; import MenuPlaceholder from './MenuPlaceholder';
import { measure } from './helpers'; import { measure } from './helpers';
@ -14,15 +16,18 @@ const layoutsEqual = (a, b) => (
const isFunctional = Component => !Component.prototype.render; const isFunctional = Component => !Component.prototype.render;
if (!createContext) {
console.warn('New React context API not available - are you using RN 0.55+ ?')
}
const PopupMenuContext = createContext({})
export const withCtx = withContext(PopupMenuContext, "ctx");
export default class MenuProvider extends Component { export default class MenuProvider extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this._menuRegistry = makeMenuRegistry(); this._menuRegistry = makeMenuRegistry();
this._isMenuClosing = false; this._isMenuClosing = false;
}
getChildContext() {
const menuActions = { const menuActions = {
openMenu: name => this.openMenu(name), openMenu: name => this.openMenu(name),
closeMenu: () => this.closeMenu(), closeMenu: () => this.closeMenu(),
@ -31,8 +36,7 @@ export default class MenuProvider extends Component {
_getOpenedMenu: () => this._getOpenedMenu(), _getOpenedMenu: () => this._getOpenedMenu(),
_notify: force => this._notify(force), _notify: force => this._notify(force),
}; };
const menuRegistry = this._menuRegistry; this.menuCtx = { menuRegistry: this._menuRegistry, menuActions }
return { menuRegistry, menuActions };
} }
_handleBackButton = () => { _handleBackButton = () => {
@ -196,21 +200,23 @@ export default class MenuProvider extends Component {
const { style, customStyles } = this.props; const { style, customStyles } = this.props;
debug('render menu', this.isMenuOpen(), this._ownLayout); debug('render menu', this.isMenuOpen(), this._ownLayout);
return ( return (
<View style={{flex:1}} onLayout={this._onLayout}> <PopupMenuContext.Provider value={this.menuCtx}>
<View style={[ <View style={{flex:1}} onLayout={this._onLayout}>
{flex:1}, <View style={[
customStyles.menuContextWrapper, {flex:1},
customStyles.menuProviderWrapper, customStyles.menuContextWrapper,
style, customStyles.menuProviderWrapper,
]}> style,
{ this.props.children } ]}>
{ this.props.children }
</View>
<MenuPlaceholder
ctx={this}
backdropStyles={customStyles.backdrop}
ref={this._onPlaceholderRef}
/>
</View> </View>
<MenuPlaceholder </PopupMenuContext.Provider>
ctx={this}
backdropStyles={customStyles.backdrop}
ref={this._onPlaceholderRef}
/>
</View>
); );
} }
@ -235,7 +241,6 @@ export default class MenuProvider extends Component {
if (menu) { if (menu) {
menu.instance.props.onBackdropPress(); menu.instance.props.onBackdropPress();
} }
this.closeMenu(); this.closeMenu();
} }
@ -311,8 +316,3 @@ MenuProvider.defaultProps = {
customStyles: {}, customStyles: {},
backHandler: false, backHandler: false,
}; };
MenuProvider.childContextTypes = {
menuRegistry: PropTypes.object,
menuActions: PropTypes.object,
};

View File

@ -3,13 +3,14 @@ import PropTypes from 'prop-types';
import { View, Text } from 'react-native'; import { View, Text } from 'react-native';
import { debug } from './logger.js'; import { debug } from './logger.js';
import { makeTouchable } from './helpers'; import { makeTouchable } from './helpers';
import { withCtx } from './MenuProvider';
export default class MenuTrigger extends Component { class MenuTrigger extends Component {
_onPress() { _onPress() {
debug('trigger onPress'); debug('trigger onPress');
this.props.onPress && this.props.onPress(); this.props.onPress && this.props.onPress();
this.context.menuActions.openMenu(this.props.menuName); this.props.ctx.menuActions.openMenu(this.props.menuName);
} }
render() { render() {
@ -45,7 +46,4 @@ MenuTrigger.defaultProps = {
customStyles: {}, customStyles: {},
}; };
MenuTrigger.contextTypes = { export default withCtx(MenuTrigger)
menuActions: PropTypes.object,
};

40
src/with-context.js Normal file
View File

@ -0,0 +1,40 @@
import React from "react";
export function withContext(Context, propName = "context") {
return function wrap(Component) {
class EnhanceContext extends React.Component {
render() {
const { forwardedRef, ...rest } = this.props;
return (
<Context.Consumer>
{value => {
const custom = {
[propName]: value,
ref: forwardedRef
};
return <Component {...custom} {...rest} />;
}}
</Context.Consumer>
);
}
}
const name = Component.displayName || Component.name || "Component";
const consumerName =
Context.Consumer.displayName ||
Context.Consumer.name ||
"Context.Consumer";
function enhanceForwardRef(props, ref) {
return <EnhanceContext {...props} forwardedRef={ref} />;
}
enhanceForwardRef.displayName = `enhanceContext-${consumerName}(${name})`;
enhanceForwardRef.defaultProps = Component.defaultProps;
enhanceForwardRef.propTypes = Component.propTypes;
return React.forwardRef(enhanceForwardRef);
};
}