d2-ui
Version:
686 lines (579 loc) • 24.1 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _simpleAssign = require('simple-assign');
var _simpleAssign2 = _interopRequireDefault(_simpleAssign);
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _reactDom = require('react-dom');
var _reactDom2 = _interopRequireDefault(_reactDom);
var _reactAddonsUpdate = require('react-addons-update');
var _reactAddonsUpdate2 = _interopRequireDefault(_reactAddonsUpdate);
var _shallowEqual = require('recompose/shallowEqual');
var _shallowEqual2 = _interopRequireDefault(_shallowEqual);
var _ClickAwayListener = require('../internal/ClickAwayListener');
var _ClickAwayListener2 = _interopRequireDefault(_ClickAwayListener);
var _autoPrefix = require('../utils/autoPrefix');
var _autoPrefix2 = _interopRequireDefault(_autoPrefix);
var _transitions = require('../styles/transitions');
var _transitions2 = _interopRequireDefault(_transitions);
var _keycode = require('keycode');
var _keycode2 = _interopRequireDefault(_keycode);
var _propTypes = require('../utils/propTypes');
var _propTypes2 = _interopRequireDefault(_propTypes);
var _List = require('../List/List');
var _List2 = _interopRequireDefault(_List);
var _deprecatedPropType = require('../utils/deprecatedPropType');
var _deprecatedPropType2 = _interopRequireDefault(_deprecatedPropType);
var _warning = require('warning');
var _warning2 = _interopRequireDefault(_warning);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
function getStyles(props, context) {
var animated = props.animated;
var desktop = props.desktop;
var maxHeight = props.maxHeight;
var _props$openDirection = props.openDirection;
var openDirection = _props$openDirection === undefined ? 'bottom-left' : _props$openDirection;
var width = props.width;
var openDown = openDirection.split('-')[0] === 'bottom';
var openLeft = openDirection.split('-')[1] === 'left';
var muiTheme = context.muiTheme;
var styles = {
root: {
// Nested div bacause the List scales x faster than it scales y
transition: animated ? _transitions2.default.easeOut('250ms', 'transform') : null,
zIndex: muiTheme.zIndex.menu,
top: openDown ? 0 : null,
bottom: !openDown ? 0 : null,
left: !openLeft ? 0 : null,
right: openLeft ? 0 : null,
transform: animated ? 'scaleX(0)' : null,
transformOrigin: openLeft ? 'right' : 'left',
opacity: 0,
maxHeight: maxHeight,
overflowY: maxHeight ? 'auto' : null
},
divider: {
marginTop: 7,
marginBottom: 8
},
list: {
display: 'table-cell',
paddingBottom: desktop ? 16 : 8,
paddingTop: desktop ? 16 : 8,
userSelect: 'none',
width: width
},
menuItemContainer: {
transition: animated ? _transitions2.default.easeOut(null, 'opacity') : null,
opacity: 0
},
selectedMenuItem: {
color: muiTheme.baseTheme.palette.accent1Color
}
};
return styles;
}
var Menu = function (_Component) {
_inherits(Menu, _Component);
function Menu(props, context) {
_classCallCheck(this, Menu);
var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(Menu).call(this, props, context));
_initialiseProps.call(_this);
var filteredChildren = _this.getFilteredChildren(props.children);
var selectedIndex = _this.getSelectedIndex(props, filteredChildren);
_this.state = {
focusIndex: props.disableAutoFocus ? -1 : selectedIndex >= 0 ? selectedIndex : 0,
isKeyboardFocused: props.initiallyKeyboardFocused,
keyWidth: props.desktop ? 64 : 56
};
return _this;
}
_createClass(Menu, [{
key: 'componentDidMount',
value: function componentDidMount() {
if (this.props.autoWidth) this.setWidth();
if (!this.props.animated) this.animateOpen();
this.setScollPosition();
}
}, {
key: 'componentWillReceiveProps',
value: function componentWillReceiveProps(nextProps) {
var filteredChildren = this.getFilteredChildren(nextProps.children);
var selectedIndex = this.getSelectedIndex(nextProps, filteredChildren);
this.setState({
focusIndex: nextProps.disableAutoFocus ? -1 : selectedIndex >= 0 ? selectedIndex : 0,
keyWidth: nextProps.desktop ? 64 : 56
});
}
}, {
key: 'shouldComponentUpdate',
value: function shouldComponentUpdate(nextProps, nextState) {
return !(0, _shallowEqual2.default)(this.props, nextProps) || !(0, _shallowEqual2.default)(this.state, nextState);
}
}, {
key: 'componentDidUpdate',
value: function componentDidUpdate() {
if (this.props.autoWidth) this.setWidth();
}
}, {
key: 'getValueLink',
// Do not use outside of this component, it will be removed once valueLink is deprecated
value: function getValueLink(props) {
return props.valueLink || {
value: props.value,
requestChange: props.onChange
};
}
}, {
key: 'setKeyboardFocused',
value: function setKeyboardFocused(keyboardFocused) {
this.setState({
isKeyboardFocused: keyboardFocused
});
}
}, {
key: 'getFilteredChildren',
value: function getFilteredChildren(children) {
var filteredChildren = [];
_react2.default.Children.forEach(children, function (child) {
if (child) {
filteredChildren.push(child);
}
});
return filteredChildren;
}
}, {
key: 'animateOpen',
value: function animateOpen() {
var rootStyle = _reactDom2.default.findDOMNode(this).style;
var scrollContainerStyle = _reactDom2.default.findDOMNode(this.refs.scrollContainer).style;
var menuContainers = _reactDom2.default.findDOMNode(this.refs.list).childNodes;
_autoPrefix2.default.set(rootStyle, 'transform', 'scaleX(1)');
_autoPrefix2.default.set(scrollContainerStyle, 'transform', 'scaleY(1)');
scrollContainerStyle.opacity = 1;
for (var i = 0; i < menuContainers.length; ++i) {
menuContainers[i].style.opacity = 1;
}
}
}, {
key: 'cloneMenuItem',
value: function cloneMenuItem(child, childIndex, styles, index) {
var _this2 = this;
var _props = this.props;
var desktop = _props.desktop;
var selectedMenuItemStyle = _props.selectedMenuItemStyle;
var selected = this.isChildSelected(child, this.props);
var selectedChildrenStyles = {};
if (selected) {
selectedChildrenStyles = (0, _simpleAssign2.default)(styles.selectedMenuItem, selectedMenuItemStyle);
}
var mergedChildrenStyles = (0, _simpleAssign2.default)({}, child.props.style, selectedChildrenStyles);
var isFocused = childIndex === this.state.focusIndex;
var focusState = 'none';
if (isFocused) {
focusState = this.state.isKeyboardFocused ? 'keyboard-focused' : 'focused';
}
return _react2.default.cloneElement(child, {
desktop: desktop,
focusState: focusState,
onTouchTap: function onTouchTap(event) {
_this2.handleMenuItemTouchTap(event, child, index);
if (child.props.onTouchTap) child.props.onTouchTap(event);
},
ref: isFocused ? 'focusedMenuItem' : null,
style: mergedChildrenStyles
});
}
}, {
key: 'decrementKeyboardFocusIndex',
value: function decrementKeyboardFocusIndex() {
var index = this.state.focusIndex;
index--;
if (index < 0) index = 0;
this.setFocusIndex(index, true);
}
}, {
key: 'getCascadeChildrenCount',
value: function getCascadeChildrenCount(filteredChildren) {
var _props2 = this.props;
var desktop = _props2.desktop;
var maxHeight = _props2.maxHeight;
var count = 1;
var currentHeight = desktop ? 16 : 8;
var menuItemHeight = desktop ? 32 : 48;
// MaxHeight isn't set - cascade all of the children
if (!maxHeight) return filteredChildren.length;
// Count all the children that will fit inside the max menu height
filteredChildren.forEach(function (child) {
if (currentHeight < maxHeight) {
var childIsADivider = child.type && child.type.muiName === 'Divider';
currentHeight += childIsADivider ? 16 : menuItemHeight;
count++;
}
});
return count;
}
}, {
key: 'getMenuItemCount',
value: function getMenuItemCount(filteredChildren) {
var menuItemCount = 0;
filteredChildren.forEach(function (child) {
var childIsADivider = child.type && child.type.muiName === 'Divider';
var childIsDisabled = child.props.disabled;
if (!childIsADivider && !childIsDisabled) menuItemCount++;
});
return menuItemCount;
}
}, {
key: 'getSelectedIndex',
value: function getSelectedIndex(props, filteredChildren) {
var _this3 = this;
var selectedIndex = -1;
var menuItemIndex = 0;
filteredChildren.forEach(function (child) {
var childIsADivider = child.type && child.type.muiName === 'Divider';
if (_this3.isChildSelected(child, props)) selectedIndex = menuItemIndex;
if (!childIsADivider) menuItemIndex++;
});
return selectedIndex;
}
}, {
key: 'handleMenuItemTouchTap',
value: function handleMenuItemTouchTap(event, item, index) {
var children = this.props.children;
var multiple = this.props.multiple;
var valueLink = this.getValueLink(this.props);
var menuValue = valueLink.value;
var itemValue = item.props.value;
var focusIndex = _react2.default.isValidElement(children) ? 0 : children.indexOf(item);
this.setFocusIndex(focusIndex, false);
if (multiple) {
var itemIndex = menuValue.indexOf(itemValue);
var newMenuValue = itemIndex === -1 ? (0, _reactAddonsUpdate2.default)(menuValue, { $push: [itemValue] }) : (0, _reactAddonsUpdate2.default)(menuValue, { $splice: [[itemIndex, 1]] });
valueLink.requestChange(event, newMenuValue);
} else if (!multiple && itemValue !== menuValue) {
valueLink.requestChange(event, itemValue);
}
this.props.onItemTouchTap(event, item, index);
}
}, {
key: 'incrementKeyboardFocusIndex',
value: function incrementKeyboardFocusIndex(filteredChildren) {
var index = this.state.focusIndex;
var maxIndex = this.getMenuItemCount(filteredChildren) - 1;
index++;
if (index > maxIndex) index = maxIndex;
this.setFocusIndex(index, true);
}
}, {
key: 'isChildSelected',
value: function isChildSelected(child, props) {
var menuValue = this.getValueLink(props).value;
var childValue = child.props.value;
if (props.multiple) {
return menuValue.length && menuValue.indexOf(childValue) !== -1;
} else {
return child.props.hasOwnProperty('value') && menuValue === childValue;
}
}
}, {
key: 'setFocusIndex',
value: function setFocusIndex(newIndex, isKeyboardFocused) {
this.setState({
focusIndex: newIndex,
isKeyboardFocused: isKeyboardFocused
});
}
}, {
key: 'setScollPosition',
value: function setScollPosition() {
var desktop = this.props.desktop;
var focusedMenuItem = this.refs.focusedMenuItem;
var menuItemHeight = desktop ? 32 : 48;
if (focusedMenuItem) {
var selectedOffSet = _reactDom2.default.findDOMNode(focusedMenuItem).offsetTop;
// Make the focused item be the 2nd item in the list the user sees
var scrollTop = selectedOffSet - menuItemHeight;
if (scrollTop < menuItemHeight) scrollTop = 0;
_reactDom2.default.findDOMNode(this.refs.scrollContainer).scrollTop = scrollTop;
}
}
}, {
key: 'setWidth',
value: function setWidth() {
var el = _reactDom2.default.findDOMNode(this);
var listEl = _reactDom2.default.findDOMNode(this.refs.list);
var elWidth = el.offsetWidth;
var keyWidth = this.state.keyWidth;
var minWidth = keyWidth * 1.5;
var keyIncrements = elWidth / keyWidth;
var newWidth = void 0;
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';
}
}, {
key: 'render',
value: function render() {
var _this4 = this;
var _props3 = this.props;
var animated = _props3.animated;
var autoWidth = _props3.autoWidth;
var // eslint-disable-line no-unused-vars
children = _props3.children;
var desktop = _props3.desktop;
var initiallyKeyboardFocused = _props3.initiallyKeyboardFocused;
var // eslint-disable-line no-unused-vars
listStyle = _props3.listStyle;
var maxHeight = _props3.maxHeight;
var // eslint-disable-line no-unused-vars
multiple = _props3.multiple;
var _props3$openDirection = _props3.openDirection;
var // eslint-disable-line no-unused-vars
openDirection = _props3$openDirection === undefined ? 'bottom-left' : _props3$openDirection;
var selectedMenuItemStyle = _props3.selectedMenuItemStyle;
var // eslint-disable-line no-unused-vars
style = _props3.style;
var value = _props3.value;
var // eslint-disable-line no-unused-vars
valueLink = _props3.valueLink;
var // eslint-disable-line no-unused-vars
width = _props3.width;
var // eslint-disable-line no-unused-vars
zDepth = _props3.zDepth;
var other = _objectWithoutProperties(_props3, ['animated', 'autoWidth', 'children', 'desktop', 'initiallyKeyboardFocused', 'listStyle', 'maxHeight', 'multiple', 'openDirection', 'selectedMenuItemStyle', 'style', 'value', 'valueLink', 'width', 'zDepth']);
process.env.NODE_ENV !== "production" ? (0, _warning2.default)(typeof zDepth === 'undefined', 'Menu no longer supports `zDepth`. Instead, wrap it in `Paper` ' + 'or another component that provides `zDepth`.') : void 0;
var focusIndex = this.state.focusIndex;
var prepareStyles = this.context.muiTheme.prepareStyles;
var styles = getStyles(this.props, this.context);
var mergedRootStyles = (0, _simpleAssign2.default)(styles.root, style);
var mergedListStyles = (0, _simpleAssign2.default)(styles.list, listStyle);
var openDown = openDirection.split('-')[0] === 'bottom';
var filteredChildren = this.getFilteredChildren(children);
// Cascade children opacity
var cumulativeDelay = openDown ? 175 : 325;
var cascadeChildrenCount = this.getCascadeChildrenCount(filteredChildren);
var cumulativeDelayIncrement = Math.ceil(150 / cascadeChildrenCount);
var menuItemIndex = 0;
var newChildren = _react2.default.Children.map(filteredChildren, function (child, index) {
var childIsADivider = child.type && child.type.muiName === 'Divider';
var childIsDisabled = child.props.disabled;
var childrenContainerStyles = {};
if (animated) {
var transitionDelay = 0;
// Only cascade the visible menu items
if (menuItemIndex >= focusIndex - 1 && menuItemIndex <= focusIndex + cascadeChildrenCount - 1) {
cumulativeDelay = openDown ? cumulativeDelay + cumulativeDelayIncrement : cumulativeDelay - cumulativeDelayIncrement;
transitionDelay = cumulativeDelay;
}
childrenContainerStyles = (0, _simpleAssign2.default)({}, styles.menuItemContainer, {
transitionDelay: transitionDelay + 'ms'
});
}
var clonedChild = childIsADivider ? _react2.default.cloneElement(child, { style: styles.divider }) : childIsDisabled ? _react2.default.cloneElement(child, { desktop: desktop }) : _this4.cloneMenuItem(child, menuItemIndex, styles, index);
if (!childIsADivider && !childIsDisabled) menuItemIndex++;
return animated ? _react2.default.createElement(
'div',
{ style: prepareStyles(childrenContainerStyles) },
clonedChild
) : clonedChild;
});
return _react2.default.createElement(
_ClickAwayListener2.default,
{ onClickAway: this.handleClickAway },
_react2.default.createElement(
'div',
{
onKeyDown: this.handleKeyDown,
style: prepareStyles(mergedRootStyles),
ref: 'scrollContainer'
},
_react2.default.createElement(
_List2.default,
_extends({}, other, {
ref: 'list',
style: mergedListStyles
}),
newChildren
)
)
);
}
}]);
return Menu;
}(_react.Component);
Menu.propTypes = {
/**
* If true, the menu will apply transitions when it
* is added to the DOM. In order for transitions to
* work, wrap the menu inside a `ReactTransitionGroup`.
*/
animated: (0, _deprecatedPropType2.default)(_react.PropTypes.bool, 'Instead, use a [Popover](/#/components/popover).'),
/**
* If true, the width of the menu will be set automatically
* according to the widths of its children,
* using proper keyline increments (64px for desktop,
* 56px otherwise).
*/
autoWidth: _react.PropTypes.bool,
/**
* The content of the menu. This is usually used to pass `MenuItem`
* elements.
*/
children: _react.PropTypes.node,
/**
* If true, the menu item will render with compact desktop styles.
*/
desktop: _react.PropTypes.bool,
/**
* If true, the menu will not be auto-focused.
*/
disableAutoFocus: _react.PropTypes.bool,
/**
* If true, the menu will be keyboard-focused initially.
*/
initiallyKeyboardFocused: _react.PropTypes.bool,
/**
* Override the inline-styles of the underlying `List` element.
*/
listStyle: _react.PropTypes.object,
/**
* The maximum height of the menu in pixels. If specified,
* the menu will be scrollable if it is taller than the provided
* height.
*/
maxHeight: _react.PropTypes.number,
/**
* If true, `value` must be an array and the menu will support
* multiple selections.
*/
multiple: _react.PropTypes.bool,
/**
* Callback function fired when a menu item with `value` not
* equal to the current `value` of the menu is touch-tapped.
*
* @param {object} event TouchTap event targeting the menu item.
* @param {any} value If `multiple` is true, the menu's `value`
* array with either the menu item's `value` added (if
* it wasn't already selected) or omitted (if it was already selected).
* Otherwise, the `value` of the menu item.
*/
onChange: _react.PropTypes.func,
/**
* Callback function fired when the menu is focused and the *Esc* key
* is pressed.
*
* @param {object} event `keydown` event targeting the menu.
*/
onEscKeyDown: _react.PropTypes.func,
/**
* Callback function fired when a menu item is touch-tapped.
*
* @param {object} event TouchTap event targeting the menu item.
* @param {object} menuItem The menu item.
* @param {number} index The index of the menu item.
*/
onItemTouchTap: _react.PropTypes.func,
/**
* Callback function fired when the menu is focused and a key
* is pressed.
*
* @param {object} event `keydown` event targeting the menu.
*/
onKeyDown: _react.PropTypes.func,
/**
* This is the placement of the menu relative to the `IconButton`.
*/
openDirection: (0, _deprecatedPropType2.default)(_propTypes2.default.corners, 'Instead, use a [Popover](/#/components/popover).'),
/**
* Override the inline-styles of selected menu items.
*/
selectedMenuItemStyle: _react.PropTypes.object,
/**
* Override the inline-styles of the root element.
*/
style: _react.PropTypes.object,
/**
* If `multiple` is true, an array of the `value`s of the selected
* menu items. Otherwise, the `value` of the selected menu item.
* If provided, the menu will be a controlled component.
* This component also supports valueLink.
*/
value: _react.PropTypes.any,
/**
* ValueLink for the menu's `value`.
*/
valueLink: _react.PropTypes.object,
/**
* The width of the menu. If not specified, the menu's width
* will be set according to the widths of its children, using
* proper keyline increments (64px for desktop, 56px otherwise).
*/
width: _propTypes2.default.stringOrNumber,
/**
* @ignore
* Menu no longer supports `zDepth`. Instead, wrap it in `Paper`
* or another component that provides zDepth.
*/
zDepth: _propTypes2.default.zDepth
};
Menu.defaultProps = {
autoWidth: true,
desktop: false,
disableAutoFocus: false,
initiallyKeyboardFocused: false,
maxHeight: null,
multiple: false,
onChange: function onChange() {},
onEscKeyDown: function onEscKeyDown() {},
onItemTouchTap: function onItemTouchTap() {},
onKeyDown: function onKeyDown() {}
};
Menu.contextTypes = {
muiTheme: _react.PropTypes.object.isRequired
};
var _initialiseProps = function _initialiseProps() {
var _this5 = this;
this.handleClickAway = function (event) {
if (event.defaultPrevented) {
return;
}
_this5.setFocusIndex(-1, false);
};
this.handleKeyDown = function (event) {
var filteredChildren = _this5.getFilteredChildren(_this5.props.children);
switch ((0, _keycode2.default)(event)) {
case 'down':
event.preventDefault();
_this5.incrementKeyboardFocusIndex(filteredChildren);
break;
case 'esc':
_this5.props.onEscKeyDown(event);
break;
case 'tab':
event.preventDefault();
if (event.shiftKey) {
_this5.decrementKeyboardFocusIndex();
} else {
_this5.incrementKeyboardFocusIndex(filteredChildren);
}
break;
case 'up':
event.preventDefault();
_this5.decrementKeyboardFocusIndex();
break;
}
_this5.props.onKeyDown(event);
};
};
exports.default = Menu;