@elastic/eui
Version:
Elastic UI Component Library
419 lines (408 loc) • 19.6 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _typeof = require("@babel/runtime/helpers/typeof");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.SIZES = exports.EuiContextMenuPanelClass = exports.EuiContextMenuPanel = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
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 _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _react = _interopRequireWildcard(require("react"));
var _propTypes = _interopRequireDefault(require("prop-types"));
var _classnames = _interopRequireDefault(require("classnames"));
var _tabbable = require("tabbable");
var _services = require("../../services");
var _resize_observer = require("../observer/resize_observer");
var _context_menu_item = require("./context_menu_item");
var _context_menu_panel = require("./context_menu_panel.styles");
var _react2 = require("@emotion/react");
var _excluded = ["stylesMemoizer", "children", "className", "onClose", "title", "onHeightChange", "transitionType", "transitionDirection", "onTransitionComplete", "onUseKeyboardToNavigate", "items", "initialFocusedItemIndex", "showNextPanel", "showPreviousPanel", "size"];
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; })(); } /*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
var SIZES = exports.SIZES = ['s', 'm'];
var EuiContextMenuPanelClass = exports.EuiContextMenuPanelClass = /*#__PURE__*/function (_Component) {
function EuiContextMenuPanelClass(props) {
var _this;
(0, _classCallCheck2.default)(this, EuiContextMenuPanelClass);
_this = _callSuper(this, EuiContextMenuPanelClass, [props]);
(0, _defineProperty2.default)(_this, "_isMounted", false);
(0, _defineProperty2.default)(_this, "backButton", null);
(0, _defineProperty2.default)(_this, "panel", null);
(0, _defineProperty2.default)(_this, "initialPopoverParent", null);
// Find all tabbable menu items on both panel init and
// whenever `menuItems` resets when `props.items` changes
(0, _defineProperty2.default)(_this, "findMenuItems", function () {
var _this$props$items;
if (!_this.panel) return;
if (!((_this$props$items = _this.props.items) !== null && _this$props$items !== void 0 && _this$props$items.length)) return; // We only need menu items/arrow key navigation for the `items` API
if (_this.state.menuItems.length) return; // If we already have menu items, no need to continue
var tabbableItems = (0, _tabbable.tabbable)(_this.panel);
if (tabbableItems.length) {
_this.setState({
menuItems: tabbableItems
});
}
});
(0, _defineProperty2.default)(_this, "focusMenuItem", function (direction) {
var _this$state$menuItems;
var indexOffset = direction === 'up' ? -1 : 1;
var nextFocusedItemIndex;
if (_this.state.focusedItemIndex === undefined) {
// If this is the beginning of the user's keyboard navigation of the menu, then we'll focus
// either the first or last item.
nextFocusedItemIndex = direction === 'up' ? _this.state.menuItems.length - 1 : 0;
} else {
nextFocusedItemIndex = _this.state.focusedItemIndex + indexOffset;
if (nextFocusedItemIndex < 0) {
nextFocusedItemIndex = _this.state.menuItems.length - 1;
} else if (nextFocusedItemIndex === _this.state.menuItems.length) {
nextFocusedItemIndex = 0;
}
}
_this.setState({
focusedItemIndex: nextFocusedItemIndex
});
(_this$state$menuItems = _this.state.menuItems[nextFocusedItemIndex]) === null || _this$state$menuItems === void 0 || _this$state$menuItems.focus();
});
(0, _defineProperty2.default)(_this, "onKeyDown", function (event) {
// If this panel contains items you can use the left arrow key to go back at any time.
// But if it doesn't contain items, then you have to focus on the back button specifically,
// since there could be content inside the panel which requires use of the left arrow key,
// e.g. text inputs.
var _this$props = _this.props,
items = _this$props.items,
onClose = _this$props.onClose,
showPreviousPanel = _this$props.showPreviousPanel;
if (onClose && (items !== null && items !== void 0 && items.length || document.activeElement === _this.backButton || document.activeElement === _this.panel)) {
if (event.key === _services.keys.ARROW_LEFT) {
if (showPreviousPanel) {
event.preventDefault();
event.stopPropagation();
showPreviousPanel();
if (_this.props.onUseKeyboardToNavigate) {
_this.props.onUseKeyboardToNavigate();
}
}
}
}
if (items !== null && items !== void 0 && items.length) {
switch (event.key) {
case _services.keys.TAB:
requestAnimationFrame(function () {
// NOTE: document.activeElement is stale if not wrapped in requestAnimationFrame
var focusedItemIndex = _this.state.menuItems.indexOf(document.activeElement);
// We need to sync our internal state with the user tabbing through items
_this.setState({
focusedItemIndex: focusedItemIndex >= 0 && focusedItemIndex < _this.state.menuItems.length ? focusedItemIndex : undefined
});
});
break;
case _services.keys.ARROW_UP:
event.preventDefault();
_this.focusMenuItem('up');
if (_this.props.onUseKeyboardToNavigate) {
_this.props.onUseKeyboardToNavigate();
}
break;
case _services.keys.ARROW_DOWN:
event.preventDefault();
_this.focusMenuItem('down');
if (_this.props.onUseKeyboardToNavigate) {
_this.props.onUseKeyboardToNavigate();
}
break;
case _services.keys.ARROW_RIGHT:
if (_this.props.showNextPanel) {
event.preventDefault();
_this.props.showNextPanel(onClose && _this.state.focusedItemIndex ? _this.state.focusedItemIndex - 1 // Account for panel title back button
: _this.state.focusedItemIndex);
if (_this.props.onUseKeyboardToNavigate) {
_this.props.onUseKeyboardToNavigate();
}
}
break;
default:
break;
}
}
});
(0, _defineProperty2.default)(_this, "reclaimPopoverFocus", function () {
_this.setState({
waitingForInitialPopover: false
});
_this.takeInitialFocus();
});
(0, _defineProperty2.default)(_this, "onTransitionComplete", function () {
if (_this.props.onTransitionComplete) {
_this.props.onTransitionComplete();
}
});
(0, _defineProperty2.default)(_this, "panelRef", function (node) {
_this.panel = node;
_this.updateHeight();
_this.getInitialPopoverParent();
_this.findMenuItems();
});
_this.state = {
prevProps: {
items: _this.props.items
},
menuItems: [],
focusedItemIndex: props.onClose && props.initialFocusedItemIndex != null ? props.initialFocusedItemIndex + 1 // Account for panel title back button
: props.initialFocusedItemIndex,
currentHeight: undefined,
waitingForInitialPopover: false,
tookInitialFocus: false
};
return _this;
}
(0, _inherits2.default)(EuiContextMenuPanelClass, _Component);
return (0, _createClass2.default)(EuiContextMenuPanelClass, [{
key: "takeInitialFocus",
value: function takeInitialFocus() {
var _this2 = this;
// Give positioning time to render before focus is applied. Otherwise page jumps.
requestAnimationFrame(function () {
if (!_this2._isMounted) {
return;
}
// Don't take focus yet if EuiContextMenu is in a popover
// and the popover is initially opening/transitioning in
if (_this2.initialPopoverParent && _this2.state.waitingForInitialPopover) {
return;
}
// Setting focus while transitioning causes the animation to glitch, so we have to wait
// until it's finished before we focus anything.
if (_this2.props.transitionType) {
var _this2$panel;
// If the panel is transitioning, set focus to the panel so that users using
// arrow keys that are fast clickers don't accidentally get stranded focus
// or trigger keystrokes when it shouldn't
(_this2$panel = _this2.panel) === null || _this2$panel === void 0 || _this2$panel.focus({
preventScroll: true
});
return;
}
// Initial focus has already been handled, no need to continue and potentially hijack/focus fight
if (_this2.state.tookInitialFocus) {
return;
}
// If an item should be focused, focus it (if it exists)
if (_this2.state.focusedItemIndex != null && _this2.state.menuItems.length) {
var focusedItem = _this2.state.menuItems[_this2.state.focusedItemIndex];
if (focusedItem) {
focusedItem.focus();
return _this2.setState({
tookInitialFocus: true
});
}
}
// Otherwise, if the back button panel title is present, focus it
if (_this2.backButton) {
// Focus the back button for both `items` and `children` APIs
_this2.backButton.focus();
// If `items`, ensure our focused item index is correct
if (_this2.state.menuItems.length) {
_this2.setState({
focusedItemIndex: 0
});
}
return _this2.setState({
tookInitialFocus: true
});
}
// Focus on the panel as a last resort.
if (_this2.panel && !_this2.panel.contains(document.activeElement)) {
_this2.panel.focus();
_this2.setState({
tookInitialFocus: true
});
}
});
}
}, {
key: "componentDidUpdate",
value: function componentDidUpdate(_, prevState) {
if (prevState.menuItems !== this.state.menuItems) {
this.findMenuItems();
}
// Focus isn't always ready to be taken on mount, so we need to call it
// on update as well just in case
this.takeInitialFocus();
}
}, {
key: "componentDidMount",
value: function componentDidMount() {
// If EuiContextMenu is used within an EuiPopover, we need to wait for EuiPopover to:
// 1. Correctly set its `returnFocus` to the toggling button,
// so focus is correctly restored to the popover toggle on close
// 2. Finish its react-focus-on `autoFocus` behavior after transitioning in,
// so the panel can handle its own focus without focus fighting
if (this.initialPopoverParent) {
this.initialPopoverParent.addEventListener('focus', this.reclaimPopoverFocus, {
once: true
});
} else {
this.takeInitialFocus();
}
this._isMounted = true;
}
}, {
key: "componentWillUnmount",
value: function componentWillUnmount() {
var _this$initialPopoverP;
(_this$initialPopoverP = this.initialPopoverParent) === null || _this$initialPopoverP === void 0 || _this$initialPopoverP.removeEventListener('focus', this.reclaimPopoverFocus);
this._isMounted = false;
}
}, {
key: "updateHeight",
value: function updateHeight() {
var currentHeight = this.panel ? this.panel.clientHeight : 0;
if (this.state.height !== currentHeight) {
if (this.props.onHeightChange) {
this.props.onHeightChange(currentHeight);
this.setState({
height: currentHeight
});
}
}
}
}, {
key: "getInitialPopoverParent",
value: function getInitialPopoverParent() {
var _parent$parentNode;
// If `transitionType` exists, that means we're navigating between panels
// and the initial popover has already loaded, so we shouldn't need this logic
if (this.props.transitionType) return;
if (!this.panel) return;
var parent = this.panel.parentNode;
if (!parent) return;
var hasEuiContextMenuParent = parent.classList.contains('euiContextMenu');
// It's possible to use an EuiContextMenuPanel directly in a popover without
// an EuiContextMenu, so we need to account for that when searching parent nodes
var popoverParent = hasEuiContextMenuParent ? parent === null || parent === void 0 || (_parent$parentNode = parent.parentNode) === null || _parent$parentNode === void 0 ? void 0 : _parent$parentNode.parentNode : parent === null || parent === void 0 ? void 0 : parent.parentNode;
if (!popoverParent) return;
var hasPopoverParent = !!popoverParent.dataset.popoverPanel;
if (!hasPopoverParent) return;
this.initialPopoverParent = popoverParent;
this.setState({
waitingForInitialPopover: true
});
}
}, {
key: "render",
value: function render() {
var _this3 = this;
var _this$props2 = this.props,
stylesMemoizer = _this$props2.stylesMemoizer,
children = _this$props2.children,
className = _this$props2.className,
onClose = _this$props2.onClose,
title = _this$props2.title,
onHeightChange = _this$props2.onHeightChange,
transitionType = _this$props2.transitionType,
transitionDirection = _this$props2.transitionDirection,
onTransitionComplete = _this$props2.onTransitionComplete,
onUseKeyboardToNavigate = _this$props2.onUseKeyboardToNavigate,
items = _this$props2.items,
initialFocusedItemIndex = _this$props2.initialFocusedItemIndex,
showNextPanel = _this$props2.showNextPanel,
showPreviousPanel = _this$props2.showPreviousPanel,
size = _this$props2.size,
rest = (0, _objectWithoutProperties2.default)(_this$props2, _excluded);
var classes = (0, _classnames.default)('euiContextMenuPanel', className);
var styles = stylesMemoizer(_context_menu_panel.euiContextMenuPanelStyles);
var cssStyles = [styles.euiContextMenuPanel, transitionDirection && transitionType && styles[transitionDirection][transitionType]];
var panelTitle = title && (0, _react2.jsx)(_context_menu_item.EuiContextMenuItem, {
css: styles.euiContextMenuPanel__title,
className: "euiContextMenuPanel__title",
onClick: onClose,
buttonRef: function buttonRef(node) {
if (onClose) _this3.backButton = node;
},
"data-test-subj": onClose ? 'contextMenuPanelTitleButton' : 'contextMenuPanelTitle',
icon: onClose && 'arrowLeft'
}, title);
var content = items && items.length ? items.map(function (MenuItem) {
var cloneProps = {};
if (size) {
cloneProps.size = size;
}
return MenuItem.type === _context_menu_item.EuiContextMenuItem ? /*#__PURE__*/(0, _react.cloneElement)(MenuItem, cloneProps) : MenuItem;
}) : children;
return (0, _react2.jsx)("div", (0, _extends2.default)({
ref: this.panelRef,
css: cssStyles,
className: classes,
onKeyDown: this.onKeyDown,
tabIndex: -1,
onAnimationEnd: this.onTransitionComplete
}, rest), panelTitle, (0, _react2.jsx)(_resize_observer.EuiResizeObserver, {
onResize: function onResize() {
return _this3.updateHeight();
}
}, function (resizeRef) {
return (0, _react2.jsx)("div", {
ref: resizeRef
}, content);
}));
}
}], [{
key: "getDerivedStateFromProps",
value: function getDerivedStateFromProps(nextProps, prevState) {
var needsUpdate = false;
var nextState = {};
// Clear refs to menuItems if we're getting new ones.
if (nextProps.items !== prevState.prevProps.items) {
needsUpdate = true;
nextState.menuItems = [];
nextState.prevProps = {
items: nextProps.items
};
}
if (needsUpdate) {
return nextState;
}
return null;
}
}]);
}(_react.Component);
(0, _defineProperty2.default)(EuiContextMenuPanelClass, "defaultProps", {
items: []
});
EuiContextMenuPanelClass.propTypes = {
className: _propTypes.default.string,
"aria-label": _propTypes.default.string,
"data-test-subj": _propTypes.default.string,
css: _propTypes.default.any,
initialFocusedItemIndex: _propTypes.default.number,
items: _propTypes.default.arrayOf(_propTypes.default.element.isRequired),
onClose: _propTypes.default.func,
onHeightChange: _propTypes.default.func,
onTransitionComplete: _propTypes.default.func,
onUseKeyboardToNavigate: _propTypes.default.func,
showNextPanel: _propTypes.default.func,
showPreviousPanel: _propTypes.default.func,
title: _propTypes.default.node,
transitionDirection: _propTypes.default.oneOf(["next", "previous"]),
transitionType: _propTypes.default.oneOf(["in", "out"]),
/**
* Alters the size of the items and the title
*/
size: _propTypes.default.any
};
var EuiContextMenuPanel = exports.EuiContextMenuPanel = (0, _services.withEuiStylesMemoizer)(EuiContextMenuPanelClass);