UNPKG

material-ui

Version:

Material Design UI components built with React

434 lines (357 loc) 11.7 kB
let React = require('react/addons'); let update = React.addons.update; let Controllable = require('../mixins/controllable'); let StylePropable = require('../mixins/style-propable'); let AutoPrefix = require('../styles/auto-prefix'); let Transitions = require('../styles/transitions'); let KeyCode = require('../utils/key-code'); let List = require('../lists/list'); let Paper = require('../paper'); let Menu = React.createClass({ mixins: [StylePropable, Controllable], contextTypes: { muiTheme: React.PropTypes.object, }, propTypes: { autoWidth: React.PropTypes.bool, desktop: React.PropTypes.bool, initiallyKeyboardFocused: React.PropTypes.bool, listStyle: React.PropTypes.object, maxHeight: React.PropTypes.number, multiple: React.PropTypes.bool, onEscKeyDown: React.PropTypes.func, onItemTouchTap: React.PropTypes.func, onKeyDown: React.PropTypes.func, openDirection: React.PropTypes.oneOf([ 'bottom-left', 'bottom-right', 'top-left', 'top-right', ]), selectedMenuItemStyle: React.PropTypes.object, width: React.PropTypes.oneOfType([ React.PropTypes.string, React.PropTypes.number, ]), zDepth: React.PropTypes.oneOf([0, 1, 2, 3, 4, 5]), }, getDefaultProps() { return { autoWidth: true, maxHeight: null, onEscKeyDown: () => {}, onItemTouchTap: () => {}, onKeyDown: () => {}, openDirection: 'bottom-left', zDepth: 1, }; }, getInitialState() { let selectedIndex = this._getSelectedIndex(); return { focusIndex: selectedIndex >= 0 ? selectedIndex : 0, isKeyboardFocused: this.props.initiallyKeyboardFocused, keyWidth: this.props.desktop ? 64 : 56, componentEntered: false, }; }, componentDidAppear() { this.setState({ componentEntered: true, }, this._setScollPosition); }, componentDidEnter() { this.componentDidAppear(); }, componentDidMount() { if (this.props.autoWidth) this._setWidth(); }, componentWillLeave(callback) { let rootStyle = React.findDOMNode(this).style; AutoPrefix.set(rootStyle, 'transition', Transitions.easeOut('250ms', ['opacity', 'transform'])); AutoPrefix.set(rootStyle, 'transform', 'translate3d(0,-8px,0)'); rootStyle.opacity = 0; setTimeout(callback, 250); }, render() { let { autoWidth, children, desktop, initiallyKeyboardFocused, listStyle, maxHeight, multiple, openDirection, selectedMenuItemStyle, style, value, valueLink, width, zDepth, ...other } = this.props; let componentEntered = this.state.componentEntered; let openDown = openDirection.split('-')[0] === 'bottom'; let openLeft = openDirection.split('-')[1] === 'left'; let styles = { root: { //Nested div bacause the List scales x faster than //it scales y transition: Transitions.easeOut('250ms', 'transform'), position: 'absolute', zIndex: 10, top: openDown ? 0 : null, bottom: !openDown ? 0 : null, left: !openLeft ? 0 : null, right: openLeft ? 0 : null, transform: componentEntered ? 'scaleX(1)' : 'scaleX(0)', transformOrigin: openLeft ? 'right' : 'left', }, list: { display: 'table-cell', paddingBottom: desktop ? 16 : 8, paddingTop: desktop ? 16 : 8, userSelect: 'none', width: width, }, menuItem: { transition: Transitions.easeOut(null, 'opacity'), opacity: componentEntered ? 1 : 0, }, paper: { transition: Transitions.easeOut('500ms', ['transform', 'opacity']), transform: componentEntered ? 'scaleY(1)' : 'scaleY(0)', transformOrigin: openDown ? 'top' : 'bottom', opacity: componentEntered ? 1 : 0, maxHeight: maxHeight, overflowY: maxHeight ? 'scroll' : null, }, selectedMenuItem: { color: this.context.muiTheme.palette.accent1Color, }, }; let mergedRootStyles = this.mergeAndPrefix(styles.root, style); let mergedListStyles = this.mergeStyles(styles.list, listStyle); //Cascade children opacity let cumulativeDelay = openDown ? 175 : 325; let cascadeChildrenCount = this._getCascadeChildrenCount(); let cumulativeDelayIncrement = Math.ceil(150/cascadeChildrenCount); let menuItemIndex = 0; let newChildren = React.Children.map(children, (child) => { let childIsADivider = child.type.displayName === 'MenuDivider'; let childIsDisabled = child.props.disabled; let focusIndex = this.state.focusIndex; let transitionDelay = 0; //Only cascade the visible menu items if (componentEntered && (menuItemIndex >= focusIndex - 1) && (menuItemIndex <= focusIndex + cascadeChildrenCount - 1)) { cumulativeDelay = openDown ? cumulativeDelay + cumulativeDelayIncrement : cumulativeDelay - cumulativeDelayIncrement; transitionDelay = cumulativeDelay; } let childrenContainerStyles = this.mergeStyles(styles.menuItem, { transitionDelay: transitionDelay + 'ms', }); let clonedChild = childIsADivider ? child : childIsDisabled ? React.cloneElement(child, {desktop: desktop}) : this._cloneMenuItem(child, menuItemIndex, styles); if (!childIsADivider && !childIsDisabled) menuItemIndex++; return <div style={childrenContainerStyles}>{clonedChild}</div>; }.bind(this)); return ( <div onKeyDown={this._handleKeyDown} style={mergedRootStyles}> <Paper ref="scrollContainer" style={styles.paper} zDepth={zDepth}> <List {...other} ref="list" style={mergedListStyles}> {newChildren} </List> </Paper> </div> ); }, setKeyboardFocused(keyboardFocused) { this.setState({ isKeyboardFocused: keyboardFocused, }); }, _cloneMenuItem(child, childIndex, styles) { let { desktop, selectedMenuItemStyle } = this.props; let selected = this._isChildSelected(child); let selectedChildrenStyles = {}; if (selected) { selectedChildrenStyles = this.mergeStyles(styles.selectedMenuItem, selectedMenuItemStyle); } let mergedChildrenStyles = this.mergeStyles( child.props.style || {}, selectedChildrenStyles ); let isFocused = childIndex === this.state.focusIndex; let focusState = 'none'; if (isFocused) { focusState = this.state.isKeyboardFocused ? 'keyboard-focused' : 'focused'; } return React.cloneElement(child, { desktop: desktop, focusState: focusState, onTouchTap: (e) => { this._handleMenuItemTouchTap(e, child); if (child.props.onTouchTap) child.props.onTouchTap(e); }, ref: isFocused ? 'focusedMenuItem' : null, style: mergedChildrenStyles, }); }, _decrementKeyboardFocusIndex() { let index = this.state.focusIndex; index--; if (index < 0) index = 0; this._setFocusIndex(index, true); }, _getCascadeChildrenCount() { let { children, desktop, maxHeight } = this.props; let count = 1; let currentHeight = desktop ? 16 : 8; let menuItemHeight = desktop ? 32 : 48; //MaxHeight isn't set - cascade all of the children if (!maxHeight) return React.Children.count(children); //Count all the children that will fit inside the //max menu height React.Children.forEach(children, (child) => { if (currentHeight < maxHeight) { let childIsADivider = child.type.displayName === 'MenuDivider'; currentHeight += childIsADivider ? 16 : menuItemHeight; count++; } }); return count; }, _getMenuItemCount() { let menuItemCount = 0; React.Children.forEach(this.props.children, (child) => { let childIsADivider = child.type.displayName === 'MenuDivider'; let childIsDisabled = child.props.disabled; if (!childIsADivider && !childIsDisabled) menuItemCount++; }); return menuItemCount; }, _getSelectedIndex() { let { children } = this.props; let selectedIndex = -1; let menuItemIndex = 0; React.Children.forEach(children, (child) => { let childIsADivider = child.type.displayName === 'MenuDivider'; if (this._isChildSelected(child)) selectedIndex = menuItemIndex; if (!childIsADivider) menuItemIndex++; }.bind(this)); return selectedIndex; }, _handleKeyDown(e) { switch (e.keyCode) { case KeyCode.DOWN: e.preventDefault(); this._incrementKeyboardFocusIndex(); break; case KeyCode.ESC: this.props.onEscKeyDown(e); break; case KeyCode.TAB: e.preventDefault(); if (e.shiftKey) { this._decrementKeyboardFocusIndex(); } else { this._incrementKeyboardFocusIndex(); } break; case KeyCode.UP: e.preventDefault(); this._decrementKeyboardFocusIndex(); break; } this.props.onKeyDown(e); }, _handleMenuItemTouchTap(e, item) { let multiple = this.props.multiple; let valueLink = this.getValueLink(this.props); let menuValue = valueLink.value; let itemValue = item.props.value; if (multiple) { let index = menuValue.indexOf(itemValue); let newMenuValue = index === -1 ? update(menuValue, {$push: [itemValue]}) : update(menuValue, {$splice: [[index, 1]]}); valueLink.requestChange(e, newMenuValue); } else if (!multiple && itemValue !== menuValue) { valueLink.requestChange(e, itemValue); } this.props.onItemTouchTap(e, item); }, _incrementKeyboardFocusIndex() { let index = this.state.focusIndex; let maxIndex = this._getMenuItemCount() - 1; index++; if (index > maxIndex) index = maxIndex; this._setFocusIndex(index, true); }, _isChildSelected(child) { let multiple = this.props.multiple; let menuValue = this.getValueLink(this.props).value; let childValue = child.props.value; return (multiple && menuValue.length && menuValue.indexOf(childValue) !== -1) || (!multiple && menuValue && menuValue === childValue); }, _setFocusIndex(newIndex, isKeyboardFocused) { this.setState({ focusIndex: newIndex, isKeyboardFocused: isKeyboardFocused, }); }, _setScollPosition() { let desktop = this.props.desktop; let focusedMenuItem = this.refs.focusedMenuItem; let menuItemHeight = desktop ? 32 : 48; if (focusedMenuItem) { let selectedOffSet = React.findDOMNode(focusedMenuItem).offsetTop; //Make the focused item be the 2nd item in the list the //user sees let scrollTop = selectedOffSet - menuItemHeight; if (scrollTop < menuItemHeight) scrollTop = 0; React.findDOMNode(this.refs.scrollContainer).scrollTop = scrollTop; } }, _setWidth() { let el = React.findDOMNode(this); let listEl = React.findDOMNode(this.refs.list); let elWidth = el.offsetWidth; let keyWidth = this.state.keyWidth; let minWidth = keyWidth * 1.5; let keyIncrements = elWidth / keyWidth; let newWidth; keyIncrements = keyIncrements <= 1.5 ? 1.5 : Math.ceil(keyIncrements); newWidth = keyIncrements * keyWidth; if (newWidth < minWidth) newWidth = minWidth; el.style.width = newWidth + 'px'; listEl.style.width = newWidth + 'px'; }, }); module.exports = Menu;