UNPKG

terra-menu

Version:

The menu is a popup component that displays a list of items, item groups, and dividers. Menu Items can be actionable, have toggle-style selection, or have nested submenu items. Menu Item groups are a single-select grouping of menu items. The Menu will det

513 lines (508 loc) 21.7 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _typeof = require("@babel/runtime/helpers/typeof"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties")); var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass")); var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn")); var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf")); var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits")); var _react = _interopRequireDefault(require("react")); var _reactIntl = require("react-intl"); var _propTypes = _interopRequireDefault(require("prop-types")); var _terraList = _interopRequireDefault(require("terra-list")); var _IconLeft = _interopRequireDefault(require("terra-icon/lib/icon/IconLeft")); var _terraContentContainer = _interopRequireDefault(require("terra-content-container")); var _IconClose = _interopRequireDefault(require("terra-icon/lib/icon/IconClose")); var _terraArrange = _interopRequireDefault(require("terra-arrange")); var _bind = _interopRequireDefault(require("classnames/bind")); var KeyCode = _interopRequireWildcard(require("keycode-js")); var _terraThemeContext = _interopRequireDefault(require("terra-theme-context")); var _uuid = require("uuid"); var _MenuUtils = _interopRequireDefault(require("./_MenuUtils")); var _MenuItem = _interopRequireDefault(require("./MenuItem")); var _MenuModule = _interopRequireDefault(require("./Menu.module.scss")); var _excluded = ["children", "isToggleable"]; function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != _typeof(e) && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } function _callSuper(t, o, e) { return o = (0, _getPrototypeOf2.default)(o), (0, _possibleConstructorReturn2.default)(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], (0, _getPrototypeOf2.default)(t).constructor) : o.apply(t, e)); } function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); } /* eslint-disable-next-line import/no-extraneous-dependencies */ var cx = _bind.default.bind(_MenuModule.default); var propTypes = { /** * The intl object to be injected for translations. Provided by the injectIntl function. */ intl: _propTypes.default.shape({ formatMessage: _propTypes.default.func }).isRequired, /** * Title the should be displayed in header. */ title: _propTypes.default.string, /** * Callback function for when back button is clicked. */ onRequestBack: _propTypes.default.func, /** * Callback function for when close button is clicked. */ onRequestClose: _propTypes.default.func, /** * Callback function that takes the content to be displayed next and is called when an item with nested content is clicked. */ onRequestNext: _propTypes.default.func.isRequired, /** * Menu Items/Menu Groups/Menu Dividers to be displayed. */ children: _propTypes.default.node.isRequired, /** * Index within the Menu Stack. */ index: _propTypes.default.number.isRequired, /** * Bounding container for the menu, will use window if no value provided. */ boundingRef: _propTypes.default.func, /** * Indicates if the menu content should set default focus on itself. */ isFocused: _propTypes.default.bool, /** * Indicates if menu's height has been constrained by bounding container. */ isHeightBounded: _propTypes.default.bool, /** * Indicates if menu's width has been constrained by bounding container. */ isWidthBounded: _propTypes.default.bool, /** * Fixed height for content. */ fixedHeight: _propTypes.default.number, /** * Fixed width for content. */ fixedWidth: _propTypes.default.number, /** * Width for content. */ contentWidth: _propTypes.default.number, /** * Indicates if the content should be hidden. */ isHidden: _propTypes.default.bool, /** * Ref callback function to be applied to content container. */ refCallback: _propTypes.default.func, /** * Header Title for main-menu(first-tier). * Header Title will only be visible if the main-menu contains at least one sub-menu. */ headerTitle: _propTypes.default.string, /** * @private * Should the menu display Header Title (first-tier). */ showHeader: _propTypes.default.bool }; var defaultProps = { isFocused: false, title: '', isWidthBounded: false, isHeightBounded: false, headerTitle: '', isHidden: false }; var childContextTypes = { isToggleableMenu: _propTypes.default.bool, shouldReserveSpaceForIcon: _propTypes.default.bool }; var MenuContent = /*#__PURE__*/function (_React$Component) { function MenuContent(props) { var _this; (0, _classCallCheck2.default)(this, MenuContent); _this = _callSuper(this, MenuContent, [props]); _this.wrapOnClick = _this.wrapOnClick.bind(_this); _this.wrapOnKeyDown = _this.wrapOnKeyDown.bind(_this); _this.buildHeader = _this.buildHeader.bind(_this); _this.isToggleable = _this.isToggleable.bind(_this); _this.shouldReserveSpaceForIcon = _this.shouldReserveSpaceForIcon.bind(_this); _this.onKeyDown = _this.onKeyDown.bind(_this); _this.onKeyDownBackButton = _this.onKeyDownBackButton.bind(_this); _this.validateFocus = _this.validateFocus.bind(_this); _this.ariaDescribedByHandle = _this.ariaDescribedByHandle.bind(_this); _this.needsFocus = props.isFocused; _this.handleContainerRef = _this.handleContainerRef.bind(_this); _this.getSubmenuHeight = _this.getSubmenuHeight.bind(_this); _this.setListNode = _this.setListNode.bind(_this); _this.menuHeaderId = "terra-menu-headertitle-".concat((0, _uuid.v4)()); _this.menuTopHeaderId = "terra-menu-headertitle-".concat((0, _uuid.v4)()); _this.state = { focusIndex: -1 }; return _this; } (0, _inherits2.default)(MenuContent, _React$Component); return (0, _createClass2.default)(MenuContent, [{ key: "getChildContext", value: function getChildContext() { return { isToggleableMenu: this.isToggleable(), shouldReserveSpaceForIcon: this.shouldReserveSpaceForIcon() }; } }, { key: "componentDidMount", value: function componentDidMount() { // Set focus to first focusable menu item var items = this.contentNode.querySelectorAll('[data-terra-menu-interactive-item="true"]'); if (items.length) { items[0].focus(); } } }, { key: "componentDidUpdate", value: function componentDidUpdate(prevProps) { if (this.props.isFocused) { this.needsFocus = this.needsFocus || this.props.isFocused !== prevProps.isFocused; } else { this.needsFocus = false; } this.validateFocus(this.contentNode); } }, { key: "handleContainerRef", value: function handleContainerRef(node) { if (this.props.refCallback) { this.props.refCallback(node); } this.contentNode = node; this.validateFocus(node); } }, { key: "onKeyDown", value: function onKeyDown(event) { // stop event propagation in case Menu oppened inside the layout component that has its own key navigation. // removing next line would affect Menu Button support in `terra-compact-interactive-list` event.stopPropagation(); var focusableMenuItems = this.contentNode.querySelectorAll('li[tabindex="0"]'); if (event.nativeEvent.keyCode === KeyCode.KEY_UP || event.nativeEvent.keyCode === KeyCode.KEY_END) { // Shift focus to last focusable menu item focusableMenuItems[focusableMenuItems.length - 1].focus(); } if (event.nativeEvent.keyCode === KeyCode.KEY_DOWN || event.nativeEvent.keyCode === KeyCode.KEY_HOME) { // Shift focus to first focusable menu item focusableMenuItems[0].focus(); } } }, { key: "onKeyDownBackButton", value: function onKeyDownBackButton(event) { if (event.nativeEvent.keyCode === KeyCode.KEY_RETURN || event.nativeEvent.keyCode === KeyCode.KEY_SPACE || event.nativeEvent.keyCode === KeyCode.KEY_LEFT) { event.preventDefault(); this.props.onRequestBack(); } } }, { key: "getSubmenuHeight", value: function getSubmenuHeight() { if (this.props.index > 0 && this.listNode) { var bufHeight = (this.context.name || this.context.className) === 'orion-fusion-theme' || _MenuUtils.default.isSafari() ? 20 : 10; var submenuHeight = this.listNode.clientHeight + this.listNode.parentNode.parentNode.parentNode.firstChild.clientHeight + bufHeight; return submenuHeight > window.innerHeight ? window.innerHeight : submenuHeight; } return 0; } }, { key: "setListNode", value: function setListNode(node) { if (node) { this.listNode = node; } } }, { key: "validateFocus", value: function validateFocus(node) { if (this.needsFocus && node) { node.focus(); this.needsFocus = document.activeElement !== node; // If nested menu is open if (this.props.index > 0) { // Shift focus to the back button node.querySelector('[role="button"][tabIndex="0"]').focus(); } } } }, { key: "isToggleable", value: function isToggleable() { var isToggleableValue = false; _react.default.Children.forEach(this.props.children, function (child) { var _child$props = child.props, children = _child$props.children, isToggleable = _child$props.isToggleable, customProps = (0, _objectWithoutProperties2.default)(_child$props, _excluded); // child is a group menu item that needs space reserved for the checkmark _react.default.Children.forEach(children, function (subchild) { if (subchild.type === _MenuItem.default) { isToggleableValue = true; } }); if (isToggleable || customProps.isSelectable) { isToggleableValue = true; } }); return isToggleableValue; } }, { key: "shouldReserveSpaceForIcon", value: function shouldReserveSpaceForIcon() { var shouldReserveSpaceForIcon = false; _react.default.Children.forEach(this.props.children, function (child) { var _child$props2 = child.props, icon = _child$props2.icon, isInstructionsForUse = _child$props2.isInstructionsForUse; // reserve space for when there is a custom icon or instructions icon to be shown if (icon || isInstructionsForUse) { shouldReserveSpaceForIcon = true; } }); return shouldReserveSpaceForIcon; } }, { key: "wrapOnClick", value: function wrapOnClick(item) { var _this2 = this; var onClick = item.props.onClick; return function (event) { event.preventDefault(); if (_this2.state.focusIndex !== -1) { _this2.setState({ focusIndex: -1 }); } if (item.props.subMenuItems && item.props.subMenuItems.length > 0) { // Avoid keydown "click" event from enter / space key triggering stack increase here // We handle increasing stack with keydown events in a separate handler below // Fixes: https://github.com/cerner/terra-core/issues/2015 if (event.type !== 'keydown') { _this2.props.onRequestNext(item); } } if (onClick) { onClick(event); } }; } }, { key: "wrapOnKeyDown", value: function wrapOnKeyDown(item, index, isDisabled) { var _this3 = this; var onKeyDown = item.props.onKeyDown; return function (event) { var shiftTabClicked = event.shiftKey && event.nativeEvent.keyCode === KeyCode.KEY_TAB; var tabClicked = event.nativeEvent.keyCode === KeyCode.KEY_TAB; if (!(shiftTabClicked || tabClicked)) { event.preventDefault(); } if (!isDisabled && (event.nativeEvent.keyCode === KeyCode.KEY_RETURN || event.nativeEvent.keyCode === KeyCode.KEY_SPACE)) { if (item.props.subMenuItems && item.props.subMenuItems.length > 0) { _this3.props.onRequestNext(item); _this3.setState({ focusIndex: index }); } } else if (!isDisabled && event.nativeEvent.keyCode === KeyCode.KEY_RIGHT) { if (item.props.subMenuItems && item.props.subMenuItems.length > 0) { _this3.props.onRequestNext(item); } } else if (!isDisabled && event.nativeEvent.keyCode === KeyCode.KEY_LEFT) { _this3.props.onRequestBack(); } else if (event.nativeEvent.keyCode === KeyCode.KEY_UP) { _this3.setState({ focusIndex: index - 1 }); } else if (event.nativeEvent.keyCode === KeyCode.KEY_DOWN) { _this3.setState({ focusIndex: index + 1 }); } if (onKeyDown) { onKeyDown(event); } }; } }, { key: "buildHeader", value: function buildHeader(isFullScreen) { var intl = this.props.intl; var backBtnText = intl.formatMessage({ id: 'Terra.menu.back' }); var closeBtnText = intl.formatMessage({ id: 'Terra.menu.close' }); var closeIcon = /*#__PURE__*/_react.default.createElement(_IconClose.default, null); var closeButton = /*#__PURE__*/_react.default.createElement("div", null); if (this.props.onRequestClose && isFullScreen) { closeButton = /*#__PURE__*/_react.default.createElement("button", { type: "button", className: cx('header-button'), onClick: this.props.onRequestClose, "aria-label": closeBtnText }, closeIcon); } var backIcon = /*#__PURE__*/_react.default.createElement(_IconLeft.default, null); var header = /*#__PURE__*/_react.default.createElement("div", null); if (this.props.index > 0) { header = /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("div", { className: cx('header-container') }, /*#__PURE__*/_react.default.createElement("div", { className: cx('header-button'), role: "button", onClick: this.props.onRequestBack, onKeyDown: this.onKeyDownBackButton, tabIndex: "0", "aria-label": backBtnText, "aria-describedby": this.menuHeaderId }, backIcon), /*#__PURE__*/_react.default.createElement("h2", { id: this.menuHeaderId, className: cx('header-title') }, this.props.title))); } else if (this.props.headerTitle && this.props.headerTitle.length > 0) { header = /*#__PURE__*/_react.default.createElement("h1", { id: this.menuTopHeaderId, className: cx(['header-title', 'main-header-title']) }, this.props.headerTitle); } return /*#__PURE__*/_react.default.createElement(_terraArrange.default, { className: cx('header'), fitEnd: closeButton, fill: header, align: "center" }); } }, { key: "ariaDescribedByHandle", value: function ariaDescribedByHandle(value, index) { if (!_MenuUtils.default.isMac() && !this.props.index && this.props.showHeader && index === 0) { var ariaDescribedByValue = value.props.ariaDescribedBy ? " ".concat(value.props.ariaDescribedBy) : ''; return "".concat(this.menuTopHeaderId).concat(ariaDescribedByValue); } return value.props.ariaDescribedBy; } }, { key: "render", value: function render() { var _this4 = this; var index = -1; var totalItems = _MenuUtils.default.totalItems(this.props.children); var items = this.props.children ? [] : undefined; _react.default.Children.map(this.props.children, function (item) { var onClick = _this4.wrapOnClick(item); var newItem = item; // Check if child is an enabled Menu.Item if (item.props.text) { index += 1; var onKeyDown = _this4.wrapOnKeyDown(item, index, item.props.isDisabled); var isActive = _this4.state.focusIndex === index; var ariaDescribedByHandleValue = _this4.ariaDescribedByHandle(item, index); newItem = /*#__PURE__*/_react.default.cloneElement(item, { onClick: onClick, onKeyDown: onKeyDown, isActive: isActive, totalItems: totalItems, index: index, intl: _this4.props.intl, 'aria-describedby': ariaDescribedByHandleValue }); // If the child has children then it is an item group, so iterate through it's children } else if (item.props.children) { var children = item.props.children ? [] : undefined; _react.default.Children.forEach(item.props.children, function (child) { index += 1; var ariaDescribedByHandleValue = _this4.ariaDescribedByHandle(child, index); var clonedElement = /*#__PURE__*/_react.default.cloneElement(child, { onKeyDown: _this4.wrapOnKeyDown(child, index, child.props.isDisabled), isActive: index === _this4.state.focusIndex, totalItems: totalItems, index: index, intl: _this4.props.intl, 'aria-describedby': ariaDescribedByHandleValue }); children.push(clonedElement); }); newItem = /*#__PURE__*/_react.default.cloneElement(item, {}, children); } items.push(newItem); }); var boundingFrame = this.props.boundingRef ? this.props.boundingRef() : undefined; var isFullScreen = _MenuUtils.default.isFullScreen(this.props.isHeightBounded, this.props.isWidthBounded, boundingFrame, this.props.contentWidth); var theme = this.context; var isSubMenu = this.props.index > 0; var contentClass = cx('content', { submenu: isSubMenu }, { 'hidden-page': this.props.isHidden }, { fullscreen: isFullScreen }, theme.className); var header; if (this.props.showHeader || isSubMenu) { header = this.buildHeader(isFullScreen); } var contentHeight; if (this.props.isHeightBounded) { contentHeight = '100%'; } else if (!this.props.boundingRef) { contentHeight = undefined; } else { contentHeight = this.props.fixedHeight; } var menuHeight = isSubMenu && !this.props.boundingRef && !this.props.isHeightBounded ? this.getSubmenuHeight() : contentHeight; var contentPosition = this.props.isHeightBounded ? 'relative' : 'static'; var contentWidth = this.props.isWidthBounded ? undefined : this.props.fixedWidth; /* eslint-disable jsx-a11y/no-noninteractive-element-interactions, react/forbid-dom-props */ return ( /*#__PURE__*/ // eslint-disable-next-line jsx-a11y/no-static-element-interactions _react.default.createElement("div", { ref: this.handleContainerRef, className: contentClass, style: { height: menuHeight, width: contentWidth, position: contentPosition }, tabIndex: "-1", onKeyDown: this.onKeyDown // stop event propagation in case Menu oppened inside the layout component that has its own event handler for that event. // added for Menu Button support in terra-compact-interactive-list. , onFocus: function onFocus(event) { return event.stopPropagation(); } }, /*#__PURE__*/_react.default.createElement(_terraContentContainer.default, { header: header, fill: this.props.isHeightBounded || this.props.index > 0 }, /*#__PURE__*/_react.default.createElement(_terraList.default, { className: cx('list'), role: "menu", "data-submenu": isSubMenu, refCallback: this.setListNode }, items))) ); /* eslint-enable jsx-a11y/no-noninteractive-element-interactions, react/forbid-dom-props */ } }]); }(_react.default.Component); MenuContent.propTypes = propTypes; MenuContent.defaultProps = defaultProps; MenuContent.childContextTypes = childContextTypes; MenuContent.contextType = _terraThemeContext.default; var _default = exports.default = (0, _reactIntl.injectIntl)(MenuContent);