@elastic/eui
Version:
Elastic UI Component Library
458 lines (447 loc) • 26.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.SIZES = exports.EuiContextMenuPanel = void 0;
var _react = _interopRequireWildcard(require("react"));
var _propTypes = _interopRequireDefault(require("prop-types"));
var _classnames = _interopRequireDefault(require("classnames"));
var _tabbable = require("tabbable");
var _common = require("../common");
var _icon = require("../icon");
var _resize_observer = require("../observer/resize_observer");
var _services = require("../../services");
var _context_menu_item = require("./context_menu_item");
var _react2 = require("@emotion/react");
var _excluded = ["children", "className", "onClose", "title", "onHeightChange", "transitionType", "transitionDirection", "onTransitionComplete", "onUseKeyboardToNavigate", "items", "initialFocusedItemIndex", "showNextPanel", "showPreviousPanel", "size"];
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
function _extends() { _extends = Object.assign ? Object.assign.bind() : 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; }; return _extends.apply(this, arguments); }
function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a 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, _toPropertyKey(descriptor.key), descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); }
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); }
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); }
function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
* 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 titleSizeToClassNameMap = {
s: 'euiContextMenuPanelTitle--small',
m: null
};
var SIZES = (0, _common.keysOf)(titleSizeToClassNameMap);
exports.SIZES = SIZES;
var transitionDirectionAndTypeToClassNameMap = {
next: {
in: 'euiContextMenuPanel-txInLeft',
out: 'euiContextMenuPanel-txOutLeft'
},
previous: {
in: 'euiContextMenuPanel-txInRight',
out: 'euiContextMenuPanel-txOutRight'
}
};
var EuiContextMenuPanel = /*#__PURE__*/function (_Component) {
_inherits(EuiContextMenuPanel, _Component);
var _super = _createSuper(EuiContextMenuPanel);
function EuiContextMenuPanel(props) {
var _this;
_classCallCheck(this, EuiContextMenuPanel);
_this = _super.call(this, props);
_defineProperty(_assertThisInitialized(_this), "_isMounted", false);
_defineProperty(_assertThisInitialized(_this), "backButton", null);
_defineProperty(_assertThisInitialized(_this), "panel", null);
_defineProperty(_assertThisInitialized(_this), "initialPopoverParent", null);
// Find all tabbable menu items on both panel init and
// whenever `menuItems` resets when `props.items` changes
_defineProperty(_assertThisInitialized(_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
});
}
});
_defineProperty(_assertThisInitialized(_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 ? void 0 : _this$state$menuItems.focus();
});
_defineProperty(_assertThisInitialized(_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.cascadingMenuKeys.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.cascadingMenuKeys.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.cascadingMenuKeys.ARROW_UP:
event.preventDefault();
_this.focusMenuItem('up');
if (_this.props.onUseKeyboardToNavigate) {
_this.props.onUseKeyboardToNavigate();
}
break;
case _services.cascadingMenuKeys.ARROW_DOWN:
event.preventDefault();
_this.focusMenuItem('down');
if (_this.props.onUseKeyboardToNavigate) {
_this.props.onUseKeyboardToNavigate();
}
break;
case _services.cascadingMenuKeys.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;
}
}
});
_defineProperty(_assertThisInitialized(_this), "reclaimPopoverFocus", function () {
_this.setState({
waitingForInitialPopover: false
});
_this.takeInitialFocus();
});
_defineProperty(_assertThisInitialized(_this), "onTransitionComplete", function () {
if (_this.props.onTransitionComplete) {
_this.props.onTransitionComplete();
}
});
_defineProperty(_assertThisInitialized(_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;
}
_createClass(EuiContextMenuPanel, [{
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 ? 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 ? 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 ? 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,
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 = _objectWithoutProperties(_this$props2, _excluded);
var panelTitle;
if (title) {
var titleClasses = (0, _classnames.default)('euiContextMenuPanelTitle', size && titleSizeToClassNameMap[size]);
if (Boolean(onClose)) {
panelTitle = (0, _react2.jsx)("button", {
className: titleClasses,
type: "button",
onClick: onClose,
ref: function ref(node) {
_this3.backButton = node;
},
"data-test-subj": "contextMenuPanelTitleButton"
}, (0, _react2.jsx)("span", {
className: "euiContextMenu__itemLayout"
}, (0, _react2.jsx)(_icon.EuiIcon, {
type: "arrowLeft",
size: "m",
className: "euiContextMenu__icon"
}), (0, _react2.jsx)("span", {
className: "euiContextMenu__text"
}, title)));
} else {
panelTitle = (0, _react2.jsx)("div", {
className: titleClasses
}, (0, _react2.jsx)("span", {
className: "euiContextMenu__itemLayout"
}, title));
}
}
var classes = (0, _classnames.default)('euiContextMenuPanel', className, transitionDirection && transitionType && transitionDirectionAndTypeToClassNameMap[transitionDirection] ? transitionDirectionAndTypeToClassNameMap[transitionDirection][transitionType] : undefined);
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", _extends({
ref: this.panelRef,
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;
}
}]);
return EuiContextMenuPanel;
}(_react.Component);
exports.EuiContextMenuPanel = EuiContextMenuPanel;
_defineProperty(EuiContextMenuPanel, "defaultProps", {
items: []
});
EuiContextMenuPanel.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
};