react-native-menu
Version:
A flexible dropdown menu component for Android and iOS that is similar to Android's Spinner.
204 lines (189 loc) • 6.03 kB
JavaScript
const TimerMixin = require('react-timer-mixin');
const PropTypes = require('prop-types');
const createClass = require('create-react-class');
let nextID = 1;
module.exports = (React, ReactNative, { model, styles }) => {
const {
UIManager,
TouchableWithoutFeedback,
ScrollView,
View,
BackHandler
} = ReactNative;
const AnimatedOptionsContainer = require('./makeAnimatedOptionsContainer')(React, ReactNative);
// Calls a function once, then never again.
const once = (fn) => {
let called = false;
return (...args) => {
if (!called) {
called = true;
fn(...args);
}
};
};
const defaultOptionsContainerRenderer = (options) => (
<ScrollView>
{options}
</ScrollView>
);
const makeOptions = (options, { top, right }) => {
const { optionsContainerStyle, renderOptionsContainer = defaultOptionsContainerRenderer} = options.props;
return (
<AnimatedOptionsContainer style={[ styles.optionsContainer, { top, right }, optionsContainerStyle]}>
{ renderOptionsContainer(options) }
</AnimatedOptionsContainer>
);
};
const NULL_HOOKS = {
didOpen: () => {},
didClose: () => {}
}
/*
* The MenuContext provides a tunnel for descendant menu components to access
* top-level methods. It also allows the <MenuOptions/> element to be placed
* properly.
*/
return createClass({
displayName: 'MenuContext',
propTypes: {
detectBackHandler: PropTypes.bool,
},
getDefaultProps() {
return {
detectBackHandler: true,
};
},
mixins: [TimerMixin],
// Public methods
isMenuOpen() {
return this.state.openedMenu
},
openMenu(name) {
const handle = ReactNative.findNodeHandle(this._menus[name].ref);
UIManager.measure(handle, (x, y, w, h, px, py) => {
this._menus[name].measurements = { x, y, w, h, px, py };
this.setState({
openedMenu: name,
menuOptions: this._makeAndPositionOptions(name, this._menus[name].measurements),
backdropWidth: this._ownMeasurements.w
});
this._activeMenuHooks = this._menus[name];
this._activeMenuHooks && this._activeMenuHooks.didOpen();
});
},
closeMenu() {
if (this.props.onCloseMenu) {
this.props.onCloseMenu(this.state)
}
this.setState({
openedMenu: '',
menuOptions: null
});
this._activeMenuHooks && this._activeMenuHooks.didClose();
this._activeMenuHooks = NULL_HOOKS;
},
toggleMenu(name) {
if (this.state.openedMenu === name) {
this.closeMenu(name);
} else {
this.openMenu(name);
}
},
// Private and lifecycle methods
getInitialState() {
return {
openedMenu: '',
menuOptions: null,
optionsTop: 0,
optionsRight: 0,
backdropWidth: 0
};
},
childContextTypes: {
menuController: model.IMenuController
},
getChildContext() {
const menuController = {
open: this.openMenu,
close: this.closeMenu,
toggle: this.toggleMenu,
registerMenu: this._registerMenu,
unregisterMenu: this._unregisterMenu,
registerOptionsElement: this._registerOptionsElement,
makeName: this._makeName
};
return { menuController };
},
componentWillMount() {
this._menus = {};
this._options = {};
// Only do this once on initial layout.
this.onLayout = once(this.onLayout);
},
handleBackHandler() {
if (this.isMenuOpen()){
this.closeMenu();
return true;
}
return false;
},
onLayout() {
const handle = ReactNative.findNodeHandle(this.refs.Container);
UIManager.measure(handle, (x, y, w, h, px, py) => {
this._ownMeasurements = {x, y, w, h, px, py};
});
},
_registerMenu(name, hooks) {
if (this._menus[name]) {
console.warn(`Menu ${name} has already been registered in this context. Please provide a different name.`);
}
if (this.props.detectBackHandler){
BackHandler.removeEventListener('hardwareBackPress', this.handleBackHandler); //Override previous listener
BackHandler.addEventListener('hardwareBackPress', this.handleBackHandler);
}
this._menus[name] = hooks;
},
_unregisterMenu(name) {
delete this._menus[name];
delete this._options[name];
},
_registerOptionsElement(name, options) {
// If options are already set and visible, skip the update.
if (this.state.menuOptions === options) {
return;
}
this._options[name] = options;
// If the menu is already open, re-render the options.
this.setTimeout(() => {
if (this.state.openedMenu === name) {
this.setState({ menuOptions: this._makeAndPositionOptions(name, this._menus[name].measurements) });
}
}, 16);
},
_makeName() {
return `menu-${nextID++}`;
},
_makeAndPositionOptions(name, menuMeasurements) {
const options = this._options[name];
const { w: menuWidth, px: menuPX, py: menuPY } = menuMeasurements;
const { w: ownWidth, px: ownPX, py: ownPY } = this._ownMeasurements;
const optionsTop = menuPY - ownPY;
const optionsRight = ownWidth + ownPX - menuPX - menuWidth;
return makeOptions(options, { top: optionsTop, right: optionsRight });
},
render() {
return (
<View ref="Container" onLayout={this.onLayout} style={{ flex: 1 }}>
<View style={this.props.style}>
{ this.props.children }
</View>
<TouchableWithoutFeedback onPress={this.closeMenu}>
<View style={[ styles.backdrop
, this.state.openedMenu ? { width: this.state.backdropWidth, top: 0, bottom: 0 }: null ]}/>
</TouchableWithoutFeedback>
{ this.state.menuOptions }
</View>
)
}
});
};