grommet
Version:
The most advanced UX framework for enterprise applications.
631 lines (540 loc) • 23.9 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 _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _propTypes = require('prop-types');
var _propTypes2 = _interopRequireDefault(_propTypes);
var _reactDom = require('react-dom');
var _classnames3 = require('classnames');
var _classnames4 = _interopRequireDefault(_classnames3);
var _KeyboardAccelerators = require('../utils/KeyboardAccelerators');
var _KeyboardAccelerators2 = _interopRequireDefault(_KeyboardAccelerators);
var _DOM = require('../utils/DOM');
var _Drop = require('../utils/Drop');
var _Drop2 = _interopRequireDefault(_Drop);
var _Intl = require('../utils/Intl');
var _Intl2 = _interopRequireDefault(_Intl);
var _Props = require('../utils/Props');
var _Props2 = _interopRequireDefault(_Props);
var _Responsive = require('../utils/Responsive');
var _Responsive2 = _interopRequireDefault(_Responsive);
var _Box = require('./Box');
var _Box2 = _interopRequireDefault(_Box);
var _Button = require('./Button');
var _Button2 = _interopRequireDefault(_Button);
var _Down = require('./icons/base/Down');
var _Down2 = _interopRequireDefault(_Down);
var _More = require('./icons/base/More');
var _More2 = _interopRequireDefault(_More);
var _CSSClassnames = require('../utils/CSSClassnames');
var _CSSClassnames2 = _interopRequireDefault(_CSSClassnames);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
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; } // (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP
var CLASS_ROOT = _CSSClassnames2.default.MENU;
function isFunction(obj) {
return obj && obj.constructor && obj.call && obj.apply;
}
// We have a separate module for the drop component
// so we can transfer the router context.
var MenuDrop = function (_Component) {
_inherits(MenuDrop, _Component);
function MenuDrop(props, context) {
_classCallCheck(this, MenuDrop);
var _this = _possibleConstructorReturn(this, (MenuDrop.__proto__ || Object.getPrototypeOf(MenuDrop)).call(this, props, context));
_this._onUpKeyPress = _this._onUpKeyPress.bind(_this);
_this._onDownKeyPress = _this._onDownKeyPress.bind(_this);
_this._processTab = _this._processTab.bind(_this);
return _this;
}
_createClass(MenuDrop, [{
key: 'getChildContext',
value: function getChildContext() {
return {
intl: this.props.intl,
history: this.props.history,
router: this.props.router,
store: this.props.store
};
}
}, {
key: 'componentDidMount',
value: function componentDidMount() {
this._originalFocusedElement = document.activeElement;
this._keyboardHandlers = {
tab: this._processTab,
up: this._onUpKeyPress,
left: this._onUpKeyPress,
down: this._onDownKeyPress,
right: this._onDownKeyPress
};
_KeyboardAccelerators2.default.startListeningToKeyboard(this, this._keyboardHandlers);
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
if (this._originalFocusedElement.focus) {
this._originalFocusedElement.focus();
} else if (this._originalFocusedElement.parentNode && this._originalFocusedElement.parentNode.focus) {
// required for IE11 and Edge
this._originalFocusedElement.parentNode.focus();
}
_KeyboardAccelerators2.default.stopListeningToKeyboard(this, this._keyboardHandlers);
}
}, {
key: '_processTab',
value: function _processTab(event) {
var container = (0, _reactDom.findDOMNode)(this.menuDropRef);
var items = container.getElementsByTagName('*');
items = (0, _DOM.filterByFocusable)(items);
if (!items || items.length === 0) {
event.preventDefault();
} else {
if (event.shiftKey) {
if (event.target === items[0]) {
items[items.length - 1].focus();
event.preventDefault();
}
} else if (event.target === items[items.length - 1]) {
items[0].focus();
event.preventDefault();
}
}
}
}, {
key: '_onUpKeyPress',
value: function _onUpKeyPress(event) {
event.preventDefault();
var container = (0, _reactDom.findDOMNode)(this.navContainerRef);
var menuItems = container.childNodes;
if (!this.activeMenuItem) {
var lastMenuItem = menuItems[menuItems.length - 1];
this.activeMenuItem = lastMenuItem;
} else if (this.activeMenuItem.previousSibling) {
this.activeMenuItem = this.activeMenuItem.previousSibling;
}
var classes = this.activeMenuItem.className.split(/\s+/);
var tagName = this.activeMenuItem.tagName.toLowerCase();
// want to skip items of the menu that are not focusable.
if (tagName !== 'button' && tagName !== 'a' && classes.indexOf('check-box') === -1) {
if (this.activeMenuItem === menuItems[0]) {
return true;
} else {
// If this item is not focusable, check the next item.
return this._onUpKeyPress(event);
}
}
this.activeMenuItem.focus();
// Stops KeyboardAccelerators from calling the other listeners.
// Works limilar to event.stopPropagation().
return true;
}
}, {
key: '_onDownKeyPress',
value: function _onDownKeyPress(event) {
event.preventDefault();
var container = (0, _reactDom.findDOMNode)(this.navContainerRef);
var menuItems = container.childNodes;
if (!this.activeMenuItem) {
this.activeMenuItem = menuItems[0];
} else if (this.activeMenuItem.nextSibling) {
this.activeMenuItem = this.activeMenuItem.nextSibling;
}
var classes = this.activeMenuItem.className.split(/\s+/);
var tagName = this.activeMenuItem.tagName.toLowerCase();
// want to skip items of the menu that are not focusable.
if (tagName !== 'button' && tagName !== 'a' && classes.indexOf('check-box') === -1) {
if (this.activeMenuItem === menuItems[menuItems.length - 1]) {
return true;
} else {
// If this item is not focusable, check the next item.
return this._onDownKeyPress(event);
}
}
this.activeMenuItem.focus();
// Stops KeyboardAccelerators from calling the other listeners.
// Works limilar to event.stopPropagation().
return true;
}
}, {
key: 'render',
value: function render() {
var _this2 = this,
_classnames;
var _props = this.props,
dropAlign = _props.dropAlign,
size = _props.size,
children = _props.children,
control = _props.control,
colorIndex = _props.colorIndex,
onClick = _props.onClick,
props = _objectWithoutProperties(_props, ['dropAlign', 'size', 'children', 'control', 'colorIndex', 'onClick']);
var restProps = _Props2.default.omit(props, [].concat(_toConsumableArray(Object.keys(MenuDrop.childContextTypes)), _toConsumableArray(Object.keys(MenuDrop.propTypes))));
// Put nested Menus inline
var menuDropChildren = _react2.default.Children.map(children, function (child) {
var result = child;
if (child && isFunction(child.type) && child.type.prototype._renderMenuDrop) {
result = _react2.default.cloneElement(child, { inline: 'expanded', direction: 'column' });
}
return result;
});
var contents = [_react2.default.createElement(
_Box2.default,
_extends({}, restProps, { key: 'nav', ref: function ref(_ref) {
return _this2.navContainerRef = _ref;
},
tag: 'nav', className: CLASS_ROOT + '__contents',
primary: false }),
menuDropChildren
)];
// do not show the control if menu doesn't overlap with it when expanded
var showControl = ('top' === dropAlign.top || 'bottom' === dropAlign.bottom) && ('left' === dropAlign.left || 'right' === dropAlign.right);
if (showControl) {
contents.unshift(_react2.default.cloneElement(control, { key: 'control', fill: true }));
}
if (dropAlign.bottom) {
contents.reverse();
}
var classes = (0, _classnames4.default)(CLASS_ROOT + '__drop', (_classnames = {}, _defineProperty(_classnames, this.props.className + '__drop', this.props.className), _defineProperty(_classnames, CLASS_ROOT + '__drop--align-right', dropAlign.right), _defineProperty(_classnames, CLASS_ROOT + '__drop--' + size, size), _classnames));
return _react2.default.createElement(
_Box2.default,
{ ref: function ref(_ref2) {
return _this2.menuDropRef = _ref2;
}, className: classes,
colorIndex: colorIndex, onClick: onClick, focusable: false },
contents
);
}
}]);
return MenuDrop;
}(_react.Component);
MenuDrop.displayName = 'MenuDrop';
MenuDrop.propTypes = _extends({
control: _propTypes2.default.node,
dropAlign: _Drop.dropAlignPropType,
dropColorIndex: _propTypes2.default.string,
onClick: _propTypes2.default.func.isRequired,
router: _propTypes2.default.any,
size: _propTypes2.default.oneOf(['small', 'medium', 'large']),
store: _propTypes2.default.any
}, _Box2.default.propTypes);
MenuDrop.childContextTypes = {
history: _propTypes2.default.any,
intl: _propTypes2.default.any,
router: _propTypes2.default.any,
store: _propTypes2.default.any
};
var Menu = function (_Component2) {
_inherits(Menu, _Component2);
function Menu(props, context) {
_classCallCheck(this, Menu);
var _this3 = _possibleConstructorReturn(this, (Menu.__proto__ || Object.getPrototypeOf(Menu)).call(this, props, context));
_this3._onOpen = _this3._onOpen.bind(_this3);
_this3._onClose = _this3._onClose.bind(_this3);
_this3._checkOnClose = _this3._checkOnClose.bind(_this3);
_this3._onSink = _this3._onSink.bind(_this3);
_this3._onResponsive = _this3._onResponsive.bind(_this3);
_this3._onFocusControl = _this3._onFocusControl.bind(_this3);
_this3._onBlurControl = _this3._onBlurControl.bind(_this3);
var inline = void 0;
if (props.hasOwnProperty('inline')) {
inline = props.inline;
} else {
inline = !props.label && !props.icon;
}
var responsive = void 0;
if (props.hasOwnProperty('responsive')) {
responsive = props.responsive;
} else {
responsive = inline && 'row' === props.direction;
}
_this3.state = {
// state may be 'collapsed', 'focused' or 'expanded' (active).
state: 'collapsed',
initialInline: inline,
inline: inline,
responsive: responsive
};
return _this3;
}
_createClass(Menu, [{
key: 'componentDidMount',
value: function componentDidMount() {
if (this.state.responsive) {
this._responsive = _Responsive2.default.start(this._onResponsive);
}
}
}, {
key: 'componentWillReceiveProps',
value: function componentWillReceiveProps(nextProps) {
if (this.props.inline !== nextProps.inline || this.props.icon !== nextProps.icon) {
this.setState({
inline: nextProps.hasOwnProperty('inline') ? nextProps.inline : !nextProps.label && !nextProps.icon
});
}
}
}, {
key: 'componentDidUpdate',
value: function componentDidUpdate(prevProps, prevState) {
var _this4 = this;
if (this.state.state !== prevState.state) {
var activeKeyboardHandlers = {
esc: this._onClose
};
var focusedKeyboardHandlers = {
space: this._onOpen,
down: this._onOpen,
enter: this._onOpen
};
switch (this.state.state) {
case 'collapsed':
_KeyboardAccelerators2.default.stopListeningToKeyboard(this, focusedKeyboardHandlers);
_KeyboardAccelerators2.default.stopListeningToKeyboard(this, activeKeyboardHandlers);
document.removeEventListener('click', this._checkOnClose);
document.removeEventListener('touchstart', this._checkOnClose);
if (this._drop) {
// When Menu is used with Anchor/paths the Drop removes too quickly
// and react looks for a DOM element which is gone. Adding a
// slight delay resolves this issue.
setTimeout(function () {
_this4._drop.remove();
_this4._drop = undefined;
}, 5);
}
break;
case 'focused':
_KeyboardAccelerators2.default.stopListeningToKeyboard(this, activeKeyboardHandlers);
_KeyboardAccelerators2.default.startListeningToKeyboard(this, focusedKeyboardHandlers);
break;
case 'expanded':
// only add the drop again if the instance is not defined
// see https://github.com/grommet/grommet/issues/1431
if (!this._drop) {
_KeyboardAccelerators2.default.stopListeningToKeyboard(this, focusedKeyboardHandlers);
_KeyboardAccelerators2.default.startListeningToKeyboard(this, activeKeyboardHandlers);
document.addEventListener('click', this._checkOnClose);
document.addEventListener('touchstart', this._checkOnClose);
this._drop = new _Drop2.default((0, _reactDom.findDOMNode)(this._controlRef), this._renderMenuDrop(), {
align: this.props.dropAlign,
colorIndex: this.props.dropColorIndex,
focusControl: true
});
}
break;
}
} else if (this.state.state === 'expanded') {
this._drop.render(this._renderMenuDrop());
}
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
document.removeEventListener('click', this._checkOnClose);
document.removeEventListener('touchstart', this._checkOnClose);
_KeyboardAccelerators2.default.stopListeningToKeyboard(this);
if (this._drop) {
this._drop.remove();
}
if (this._responsive) {
this._responsive.stop();
}
}
}, {
key: '_onOpen',
value: function _onOpen() {
if ((0, _reactDom.findDOMNode)(this._controlRef).contains(document.activeElement)) {
this.setState({ state: 'expanded' });
}
}
}, {
key: '_onClose',
value: function _onClose() {
this.setState({ state: 'collapsed' });
}
}, {
key: '_checkOnClose',
value: function _checkOnClose(event) {
var drop = (0, _reactDom.findDOMNode)(this._menuDrop);
var control = (0, _reactDom.findDOMNode)(this._controlRef);
if (drop && !drop.contains(event.target) && !control.contains(event.target)) {
this._onClose();
}
}
}, {
key: '_onSink',
value: function _onSink(event) {
event.stopPropagation();
// need to go native to prevent closing via document
event.nativeEvent.stopImmediatePropagation();
}
}, {
key: '_onResponsive',
value: function _onResponsive(small) {
// deactivate if we change resolutions
var newState = this.state.state;
if (this.state.state === 'expanded') {
newState = 'focused';
}
if (small) {
this.setState({ inline: false, active: newState, controlCollapsed: true });
} else {
this.setState({
inline: this.state.initialInline,
active: newState,
state: 'collapsed',
controlCollapsed: false
});
}
}
}, {
key: '_onFocusControl',
value: function _onFocusControl() {
if (this.state.state !== 'focused') {
this.setState({ state: 'focused' });
}
}
}, {
key: '_onBlurControl',
value: function _onBlurControl() {
if (this.state.state === 'focused') {
this.setState({ state: 'collapsed' });
}
}
}, {
key: '_renderButtonProps',
value: function _renderButtonProps() {
var _props2 = this.props,
icon = _props2.icon,
label = _props2.label;
// Use default icon if no label or icon is provided
if (!label && !icon) {
return { icon: _react2.default.createElement(_More2.default, null) };
}
// Return provided label(if any) and provided icon, or default
// to DropCaretIcon
return {
label: label,
icon: icon || _react2.default.createElement(_Down2.default, { a11yTitle: 'menu-down' })
};
}
}, {
key: '_renderMenuDrop',
value: function _renderMenuDrop() {
var _this5 = this;
var closeLabel = _Intl2.default.getMessage(this.context.intl, 'Close');
var menuLabel = _Intl2.default.getMessage(this.context.intl, 'Menu');
var menuTitle = closeLabel + ' ' + (this.props.a11yTitle || this.props.label || '') + ' ' + ('' + menuLabel);
var control = _react2.default.createElement(_Button2.default, _extends({ className: CLASS_ROOT + '__control', plain: true,
a11yTitle: menuTitle, reverse: true
}, this._renderButtonProps(), { onClick: this._onClose }));
var boxProps = _Props2.default.pick(this.props, Object.keys(_Box2.default.propTypes));
var onClick = this.props.closeOnClick ? this._onClose : this._onSink;
return _react2.default.createElement(
MenuDrop,
_extends({}, boxProps, this.context, {
className: this.props.className,
dropAlign: this.props.dropAlign,
size: this.props.size,
onClick: onClick,
control: control, ref: function ref(_ref3) {
return _this5._menuDrop = _ref3;
} }),
this.props.children
);
}
}, {
key: 'render',
value: function render() {
var _classnames2,
_this6 = this;
var _props3 = this.props,
a11yTitle = _props3.a11yTitle,
children = _props3.children,
className = _props3.className,
direction = _props3.direction,
fill = _props3.fill,
label = _props3.label,
primary = _props3.primary,
size = _props3.size,
pad = _props3.pad,
props = _objectWithoutProperties(_props3, ['a11yTitle', 'children', 'className', 'direction', 'fill', 'label', 'primary', 'size', 'pad']);
delete props.closeOnClick;
delete props.dropColorIndex;
delete props.dropAlign;
delete props.icon;
delete props.inline;
var inline = this.state.inline;
var classes = (0, _classnames4.default)(CLASS_ROOT, (_classnames2 = {}, _defineProperty(_classnames2, CLASS_ROOT + '--' + direction, direction), _defineProperty(_classnames2, CLASS_ROOT + '--' + size, size), _defineProperty(_classnames2, CLASS_ROOT + '--primary', primary), _defineProperty(_classnames2, CLASS_ROOT + '--inline', inline), _defineProperty(_classnames2, CLASS_ROOT + '--controlled', !inline), _defineProperty(_classnames2, CLASS_ROOT + '__control', !inline), _defineProperty(_classnames2, CLASS_ROOT + '--labelled', !inline && label), _defineProperty(_classnames2, CLASS_ROOT + '--fill', fill), _classnames2), className);
if (inline) {
var menuLabel = void 0;
if ('expanded' === inline) {
menuLabel = _react2.default.createElement(
'div',
{ className: CLASS_ROOT + '__label' },
label
);
}
return _react2.default.createElement(
_Box2.default,
_extends({}, props, { pad: pad, direction: direction, tag: 'nav',
className: classes, primary: false }),
menuLabel,
children
);
} else {
var openLabel = _Intl2.default.getMessage(this.context.intl, 'Open');
var _menuLabel = _Intl2.default.getMessage(this.context.intl, 'Menu');
var menuTitle = openLabel + ' ' + (a11yTitle || label || '') + ' ' + ('' + _menuLabel);
return _react2.default.createElement(
_Box2.default,
_extends({ ref: function ref(_ref4) {
return _this6._controlRef = _ref4;
} }, props, { className: classes }),
_react2.default.createElement(_Button2.default, _extends({ plain: true, reverse: true,
a11yTitle: menuTitle }, this._renderButtonProps(), {
onClick: function onClick() {
return _this6.setState({
state: _this6.state.state !== 'expanded' ? 'expanded' : 'collapsed'
});
},
onFocus: this._onFocusControl, onBlur: this._onBlurControl }))
);
}
}
}]);
return Menu;
}(_react.Component);
Menu.displayName = 'Menu';
exports.default = Menu;
Menu.propTypes = _extends({
closeOnClick: _propTypes2.default.bool,
dropAlign: _Drop.dropAlignPropType,
dropColorIndex: _propTypes2.default.string,
icon: _propTypes2.default.node,
id: _propTypes2.default.string,
inline: _propTypes2.default.oneOfType([_propTypes2.default.bool, _propTypes2.default.oneOf(['expand'])]),
fill: _propTypes2.default.bool,
label: _propTypes2.default.string,
size: _propTypes2.default.oneOf(['small', 'medium', 'large'])
}, _Box2.default.propTypes);
Menu.contextTypes = {
history: _propTypes2.default.any,
intl: _propTypes2.default.any,
router: _propTypes2.default.any,
store: _propTypes2.default.any
};
Menu.defaultProps = {
closeOnClick: true,
direction: 'column',
dropAlign: { top: 'top', left: 'left' },
pad: 'none'
};
module.exports = exports['default'];