Merge branch 'closing-animation'

This commit is contained in:
Stanislav Miklik 2017-05-12 20:57:18 +02:00
commit 4941e1ad3d
12 changed files with 143 additions and 100 deletions

View File

@ -25,5 +25,6 @@
"comma-dangle": 0, "comma-dangle": 0,
"react/prop-types": 0, "react/prop-types": 0,
"react/no-did-mount-set-state": 0, "react/no-did-mount-set-state": 0,
"react/no-deprecated": 0
} }
} }

View File

@ -10,5 +10,8 @@
"jest": true, "jest": true,
"expect": true, "expect": true,
"jasmine": true "jasmine": true
},
"rules": {
"react/jsx-key": 0,
} }
} }

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { View } from 'react-native'; import { View, Text } from 'react-native';
import { render } from './helpers'; import { render } from './helpers';
import { MenuTrigger, MenuOptions } from '../src/index'; import { MenuTrigger, MenuOptions } from '../src/index';

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { View } from 'react-native'; import { View, Text } from 'react-native';
import { render } from './helpers'; import { render } from './helpers';
import { MenuOptions, MenuTrigger } from '../src/index'; import { MenuOptions, MenuTrigger } from '../src/index';
import MenuOutside from '../src/renderers/MenuOutside'; import MenuOutside from '../src/renderers/MenuOutside';
@ -106,7 +106,7 @@ describe('MenuContext', () => {
); );
const { menuRegistry, menuActions } = instance.getChildContext(); const { menuRegistry, menuActions } = instance.getChildContext();
menuRegistry.subscribe(menu1); menuRegistry.subscribe(menu1);
menuActions.openMenu('menu1'); menuActions.openMenu('menu1').then(() => {
expect(menuActions.isMenuOpen()).toEqual(true); expect(menuActions.isMenuOpen()).toEqual(true);
expect(menu1._getOpened()).toEqual(true); expect(menu1._getOpened()).toEqual(true);
initOutput.props.onLayout(defaultLayout); initOutput.props.onLayout(defaultLayout);
@ -120,6 +120,7 @@ describe('MenuContext', () => {
// on open was called only once // on open was called only once
expect(menu1.props.onOpen.calls.count()).toEqual(1); expect(menu1.props.onOpen.calls.count()).toEqual(1);
}); });
});
it('should close menu', () => { it('should close menu', () => {
const { output: initOutput, instance, renderer } = render( const { output: initOutput, instance, renderer } = render(
@ -166,7 +167,7 @@ describe('MenuContext', () => {
const { menuRegistry, menuActions } = instance.getChildContext(); const { menuRegistry, menuActions } = instance.getChildContext();
initOutput.props.onLayout(defaultLayout); initOutput.props.onLayout(defaultLayout);
menuRegistry.subscribe(menu1); menuRegistry.subscribe(menu1);
menuActions.openMenu('menu_not_existing'); return menuActions.openMenu('menu_not_existing').then(() => {
expect(menuActions.isMenuOpen()).toEqual(false); expect(menuActions.isMenuOpen()).toEqual(false);
const output = renderer.getRenderOutput(); const output = renderer.getRenderOutput();
const [ components, backdrop, options ] = output.props.children; const [ components, backdrop, options ] = output.props.children;
@ -174,6 +175,7 @@ describe('MenuContext', () => {
expect(backdrop).toBeFalsy(); expect(backdrop).toBeFalsy();
expect(options).toBeFalsy(); expect(options).toBeFalsy();
}); });
});
it('should not open menu if not initialized', () => { it('should not open menu if not initialized', () => {
const { output, instance } = render( const { output, instance } = render(
@ -181,7 +183,7 @@ describe('MenuContext', () => {
); );
const { menuRegistry, menuActions } = instance.getChildContext(); const { menuRegistry, menuActions } = instance.getChildContext();
menuRegistry.subscribe(menu1); menuRegistry.subscribe(menu1);
menuActions.openMenu('menu1'); menuActions.openMenu('menu1').then(() => {
expect(menuActions.isMenuOpen()).toEqual(true); expect(menuActions.isMenuOpen()).toEqual(true);
const [ components, backdrop, options ] = output.props.children; const [ components, backdrop, options ] = output.props.children;
// on layout has not been not called // on layout has not been not called
@ -189,6 +191,7 @@ describe('MenuContext', () => {
expect(backdrop).toBeFalsy(); expect(backdrop).toBeFalsy();
expect(options).toBeFalsy(); expect(options).toBeFalsy();
}); });
});
it('should update options layout', () => { it('should update options layout', () => {
const { output: initOutput, instance, renderer } = render( const { output: initOutput, instance, renderer } = render(
@ -197,7 +200,7 @@ describe('MenuContext', () => {
const { menuRegistry, menuActions } = instance.getChildContext(); const { menuRegistry, menuActions } = instance.getChildContext();
initOutput.props.onLayout(defaultLayout); initOutput.props.onLayout(defaultLayout);
menuRegistry.subscribe(menu1); menuRegistry.subscribe(menu1);
menuActions.openMenu('menu1'); menuActions.openMenu('menu1').then(() => {
const output = renderer.getRenderOutput(); const output = renderer.getRenderOutput();
expect(output.props.children.length).toEqual(3); expect(output.props.children.length).toEqual(3);
const options = output.props.children[2]; const options = output.props.children[2];
@ -218,6 +221,7 @@ describe('MenuContext', () => {
} }
})); }));
}); });
});
it('should render backdrop that will trigger onBackdropPress', () => { it('should render backdrop that will trigger onBackdropPress', () => {
const { output: initOutput, instance, renderer } = render( const { output: initOutput, instance, renderer } = render(
@ -226,7 +230,7 @@ describe('MenuContext', () => {
const { menuRegistry, menuActions } = instance.getChildContext(); const { menuRegistry, menuActions } = instance.getChildContext();
initOutput.props.onLayout(defaultLayout); initOutput.props.onLayout(defaultLayout);
menuRegistry.subscribe(menu1); menuRegistry.subscribe(menu1);
menuActions.openMenu('menu1'); menuActions.openMenu('menu1').then(() => {
const output = renderer.getRenderOutput(); const output = renderer.getRenderOutput();
expect(output.props.children.length).toEqual(3); expect(output.props.children.length).toEqual(3);
const backdrop = output.props.children[1]; const backdrop = output.props.children[1];
@ -234,5 +238,6 @@ describe('MenuContext', () => {
backdrop.props.onPress(); backdrop.props.onPress();
expect(menu1.props.onBackdropPress).toHaveBeenCalled(); expect(menu1.props.onBackdropPress).toHaveBeenCalled();
}); });
});
}); });

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Animated } from 'react-native'; import { Animated, Text } from 'react-native';
import { render } from '../helpers'; import { render } from '../helpers';
jest.dontMock('../../src/renderers/ContextMenu'); jest.dontMock('../../src/renderers/ContextMenu');

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { View } from 'react-native'; import { View, Text } from 'react-native';
import { render } from '../helpers'; import { render } from '../helpers';
jest.dontMock('../../src/renderers/MenuOutside'); jest.dontMock('../../src/renderers/MenuOutside');

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { View } from 'react-native'; import { View, Text } from 'react-native';
import { render } from '../helpers'; import { render } from '../helpers';
jest.dontMock('../../src/renderers/NotAnimatedContextMenu'); jest.dontMock('../../src/renderers/NotAnimatedContextMenu');

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Animated } from 'react-native'; import { Animated, Text } from 'react-native';
import { render } from '../helpers'; import { render } from '../helpers';
jest.dontMock('../../src/renderers/SlideInMenu'); jest.dontMock('../../src/renderers/SlideInMenu');

View File

@ -12,19 +12,23 @@ export default class ControlledExample extends Component {
onOptionSelect(value) { onOptionSelect(value) {
alert(`Selected number: ${value}`); alert(`Selected number: ${value}`);
if (value === 1) { if (value === 1) {
this.refs.menu.close(); this.menu.close();
} }
return false; return false;
} }
openMenu() { openMenu() {
this.refs.menu.open(); this.menu.open();
}
onRef = r => {
this.menu = r;
} }
render() { render() {
return ( return (
<MenuContext style={{flexDirection: 'column', padding: 30}}> <MenuContext style={{flexDirection: 'column', padding: 30}}>
<Menu onSelect={value => this.onOptionSelect(value)} ref='menu'> <Menu onSelect={value => this.onOptionSelect(value)} ref={this.onRef}>
<MenuTrigger text='Select option'/> <MenuTrigger text='Select option'/>
<MenuOptions> <MenuOptions>
<MenuOption value={1} text='One' /> <MenuOption value={1} text='One' />

View File

@ -46,7 +46,7 @@ const OriginalExample = React.createClass({
}); });
}, },
render() { render() {
return ( return ( // eslint-disable-next-line react/no-string-refs
<MenuContext style={{ flex: 1 }} ref="MenuContext"> <MenuContext style={{ flex: 1 }} ref="MenuContext">
<View style={styles.topbar}> <View style={styles.topbar}>
<Menu onSelect={this.setMessage}> <Menu onSelect={this.setMessage}>

View File

@ -21,10 +21,10 @@
}, },
"homepage": "https://github.com/instea/react-native-popup-menu", "homepage": "https://github.com/instea/react-native-popup-menu",
"jest": { "jest": {
"scriptPreprocessor": "<rootDir>/node_modules/babel-jest", "transform": {
"testFileExtensions": [ ".*": "<rootDir>/node_modules/babel-jest"
"js" },
], "testRegex": ".*-test.js",
"moduleFileExtensions": [ "moduleFileExtensions": [
"js" "js"
], ],
@ -41,18 +41,20 @@
}, },
"devDependencies": { "devDependencies": {
"babel-core": "^6.8.0", "babel-core": "^6.8.0",
"babel-eslint": "^6.0.4", "babel-eslint": "^7.2.3",
"babel-jest": "^12.0.2", "babel-jest": "^20.0.1",
"babel-polyfill": "^6.23.0",
"babel-preset-react": "^6.5.0", "babel-preset-react": "^6.5.0",
"babel-preset-react-native": "^1.7.0", "babel-preset-react-native": "^1.9.2",
"chai": "^3.5.0", "chai": "^3.5.0",
"eslint": "^2.9.0", "eslint": "^3.19.0",
"eslint-plugin-react": "^5.1.1", "eslint-plugin-react": "^7.0.0",
"jasmine-reporters": "^2.1.1", "jasmine-reporters": "^2.1.1",
"jest-cli": "^12.0.2", "jest-cli": "^20.0.1",
"mocha": "^2.4.5", "mocha": "^3.3.0",
"react": "^15.0.2", "react": "^15.5.4",
"react-addons-test-utils": "^15.0.2", "react-addons-test-utils": "^15.5.1",
"sinon": "^1.17.4" "react-dom": "^15.5.4",
"sinon": "^2.2.0"
} }
} }

View File

@ -40,37 +40,47 @@ export default class MenuContext extends Component {
openMenu(name) { openMenu(name) {
const menu = this._menuRegistry.getMenu(name); const menu = this._menuRegistry.getMenu(name);
if (!menu) { if (!menu) {
return console.warn(`menu with name ${name} does not exist`); console.warn(`menu with name ${name} does not exist`);
return Promise.resolve();
} }
debug('open menu', name); debug('open menu', name);
menu.instance._setOpened(true); menu.instance._setOpened(true);
this._notify(); return this._notify();
return Promise.resolve();
} }
closeMenu() { closeMenu() { // has no effect on controlled menus
debug('close menu'); debug('close menu');
const hideMenu = (this.refs.menuOptions this._menuRegistry.getAll()
&& this.refs.menuOptions.close .filter(menu => menu.instance._getOpened())
&& this.refs.menuOptions.close()) || Promise.resolve(); .forEach(menu => menu.instance._setOpened(false));
const hideBackdrop = this.refs.backdrop && this.refs.backdrop.close(); return this._notify();
const closePromise = Promise.all([hideMenu, hideBackdrop]);
return closePromise.then(() => {
this._menuRegistry.getAll().forEach(menu => {
if (menu.instance._getOpened()) {
menu.instance._setOpened(false);
// invalidate trigger layout
this._menuRegistry.updateLayoutInfo(menu.name, { triggerLayout: undefined });
} }
_invalidateTriggerLayouts() {
// invalidate layouts for closed menus,
// both controlled and uncontrolled menus
this._menuRegistry.getAll()
.filter(menu => !menu.instance._isOpen())
.forEach(menu => {
this._menuRegistry.updateLayoutInfo(menu.name, { triggerLayout: undefined });
}); });
this._notify(); }
}).catch(console.error);
_beforeClose(menu) {
debug('before close', menu.name);
const hideMenu = (this.optionsRef
&& this.optionsRef.close
&& this.optionsRef.close()) || Promise.resolve();
const hideBackdrop = this.backdropRef && this.backdropRef.close();
this._invalidateTriggerLayouts();
return Promise.all([hideMenu, hideBackdrop]);
} }
toggleMenu(name) { toggleMenu(name) {
const menu = this._menuRegistry.getMenu(name); const menu = this._menuRegistry.getMenu(name);
if (!menu) { if (!menu) {
return console.warn(`menu with name ${name} does not exist`); console.warn(`menu with name ${name} does not exist`);
return Promise.resolve();
} }
debug('toggle menu', name); debug('toggle menu', name);
if (menu.instance._getOpened()) { if (menu.instance._getOpened()) {
@ -87,19 +97,25 @@ export default class MenuContext extends Component {
// set newly opened menu before any callbacks are called // set newly opened menu before any callbacks are called
this.openedMenu = next === NULL ? undefined : next; this.openedMenu = next === NULL ? undefined : next;
if (!forceUpdate && !this._isRenderNeeded(prev, next)) { if (!forceUpdate && !this._isRenderNeeded(prev, next)) {
return; return Promise.resolve();
} }
debug('notify: next menu:', next.name, ' prev menu:', prev.name); debug('notify: next menu:', next.name, ' prev menu:', prev.name);
let afterSetState = undefined; let afterSetState = undefined;
let beforeSetState = () => Promise.resolve();
if (prev.name !== next.name) { if (prev.name !== next.name) {
prev.instance && prev.instance.props.onClose(); if (prev !== NULL && !prev.instance._isOpen()) {
if (next.name) { beforeSetState = () => this._beforeClose(prev)
.then(() => prev.instance.props.onClose());
}
if (next !== NULL) {
next.instance.props.onOpen(); next.instance.props.onOpen();
afterSetState = () => this._initOpen(next); afterSetState = () => this._initOpen(next);
} }
} }
return beforeSetState().then(() => {
this.setState({ openedMenu: this.openedMenu }, afterSetState); this.setState({ openedMenu: this.openedMenu }, afterSetState);
debug('notify ended'); debug('notify ended');
});
} }
/** /**
@ -131,7 +147,11 @@ export default class MenuContext extends Component {
{ this.props.children } { this.props.children }
</View> </View>
{shouldRenderMenu && {shouldRenderMenu &&
<Backdrop onPress={() => this._onBackdropPress()} style={customStyles.backdrop} ref='backdrop' /> <Backdrop
onPress={() => this._onBackdropPress()}
style={customStyles.backdrop}
ref={this.onBackdropRef}
/>
} }
{shouldRenderMenu && {shouldRenderMenu &&
this._makeOptions(this.state.openedMenu) this._makeOptions(this.state.openedMenu)
@ -140,6 +160,14 @@ export default class MenuContext extends Component {
); );
} }
onBackdropRef = r => {
this.backdropRef = r;
}
onOptionsRef = r => {
this.optionsRef = r;
}
_onBackdropPress() { _onBackdropPress() {
debug('on backdrop press'); debug('on backdrop press');
this.state.openedMenu.instance.props.onBackdropPress(); this.state.openedMenu.instance.props.onBackdropPress();
@ -181,7 +209,7 @@ export default class MenuContext extends Component {
const props = { style, onLayout, layouts }; const props = { style, onLayout, layouts };
const optionsType = isOutside ? MenuOutside : renderer; const optionsType = isOutside ? MenuOutside : renderer;
if (!isFunctional(optionsType)) { if (!isFunctional(optionsType)) {
props.ref = 'menuOptions'; props.ref = this.onOptionsRef;
} }
return React.createElement(optionsType, props, optionsRenderer(options)); return React.createElement(optionsType, props, optionsRenderer(options));
} }