UNPKG

@mskcc/carbon-react

Version:

Carbon react components for the MSKCC DSM

541 lines (528 loc) 19.5 kB
/** * MSKCC 2021, 2024 */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js'); var invariant = require('invariant'); var PropTypes = require('prop-types'); var React = require('react'); var cx = require('classnames'); var ClickListener = require('../../internal/ClickListener.js'); var FloatingMenu = require('../../internal/FloatingMenu.js'); var iconsReact = require('@carbon/icons-react'); var mergeRefs = require('../../tools/mergeRefs.js'); var usePrefix = require('../../internal/usePrefix.js'); var deprecate = require('../../prop-types/deprecate.js'); var IconButton = require('../IconButton/IconButton.js'); var setupGetInstanceId = require('../../tools/setupGetInstanceId.js'); var noopFn = require('../../internal/noopFn.js'); var match = require('../../internal/keyboard/match.js'); var keys = require('../../internal/keyboard/keys.js'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var invariant__default = /*#__PURE__*/_interopDefaultLegacy(invariant); var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes); var React__default = /*#__PURE__*/_interopDefaultLegacy(React); var cx__default = /*#__PURE__*/_interopDefaultLegacy(cx); const getInstanceId = setupGetInstanceId["default"](); const on = function (element) { for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { args[_key - 1] = arguments[_key]; } element.addEventListener(...args); return { release() { element.removeEventListener(...args); return null; } }; }; /** * The CSS property names of the arrow keyed by the floating menu direction. * @type {[key: string]: string} */ const triggerButtonPositionProps = { [FloatingMenu.DIRECTION_TOP]: 'bottom', [FloatingMenu.DIRECTION_BOTTOM]: 'top' }; /** * Determines how the position of arrow should affect the floating menu position. * @type {[key: string]: number} */ const triggerButtonPositionFactors = { [FloatingMenu.DIRECTION_TOP]: -2, [FloatingMenu.DIRECTION_BOTTOM]: -1 }; /** * @param {Element} menuBody The menu body with the menu arrow. * @param {string} direction The floating menu direction. * @returns {FloatingMenu~offset} The adjustment of the floating menu position, upon the position of the menu arrow. * @private */ const getMenuOffset = (menuBody, direction, trigger, flip) => { const triggerButtonPositionProp = triggerButtonPositionProps[direction]; const triggerButtonPositionFactor = triggerButtonPositionFactors[direction]; if (process.env.NODE_ENV !== "production") { !(triggerButtonPositionProp && triggerButtonPositionFactor) ? process.env.NODE_ENV !== "production" ? invariant__default["default"](false, '[OverflowMenu] wrong floating menu direction: `%s`', direction) : invariant__default["default"](false) : void 0; } const { offsetWidth: menuWidth, offsetHeight: menuHeight } = menuBody; switch (triggerButtonPositionProp) { case 'top': case 'bottom': { // TODO: Ensure `trigger` is there for `<OverflowMenu open>` const triggerWidth = !trigger ? 0 : trigger.offsetWidth; return { left: (!flip ? 1 : -1) * (menuWidth / 2 - triggerWidth / 2), top: 0 }; } case 'left': case 'right': { // TODO: Ensure `trigger` is there for `<OverflowMenu open>` const triggerHeight = !trigger ? 0 : trigger.offsetHeight; return { left: 0, top: (!flip ? 1 : -1) * (menuHeight / 2 - triggerHeight / 2) }; } } }; class OverflowMenu extends React__default["default"].Component { constructor() { super(...arguments); _rollupPluginBabelHelpers.defineProperty(this, "state", { open: false, // Set a default value for 'open' hasMountedTrigger: false, // Set a default value for 'hasMountedTrigger' click: false // Set a default value for 'click' }); _rollupPluginBabelHelpers.defineProperty(this, "instanceId", getInstanceId()); /** * The handle of `onfocusin` or `focus` event handler. * @private */ _rollupPluginBabelHelpers.defineProperty(this, "_hFocusIn", null); /** * The timeout handle for handling `blur` event. * @private */ _rollupPluginBabelHelpers.defineProperty(this, "_hBlurTimeout", void 0); /** * The element ref of the tooltip's trigger button. * @type {React.RefObject<Element>} * @private */ _rollupPluginBabelHelpers.defineProperty(this, "_triggerRef", /*#__PURE__*/React__default["default"].createRef()); _rollupPluginBabelHelpers.defineProperty(this, "handleClick", evt => { const { onClick = noopFn.noopFn } = this.props; this.setState({ click: true }); if (!this._menuBody || !this._menuBody.contains(evt.target)) { this.setState({ open: !this.state.open }); onClick(evt); } }); _rollupPluginBabelHelpers.defineProperty(this, "closeMenuAndFocus", () => { const wasClicked = this.state.click; const wasOpen = this.state.open; this.closeMenu(() => { if (wasOpen && !wasClicked) { this.focusMenuEl(); } }); }); _rollupPluginBabelHelpers.defineProperty(this, "closeMenuOnEscape", () => { const wasOpen = this.state.open; this.closeMenu(() => { if (wasOpen) { this.focusMenuEl(); } }); }); _rollupPluginBabelHelpers.defineProperty(this, "handleKeyPress", evt => { if (this.state.open && match.matches(evt, [keys.ArrowUp, keys.ArrowRight, keys.ArrowDown, keys.ArrowLeft])) { evt.preventDefault(); } // Close the overflow menu on escape if (match.matches(evt, [keys.Escape])) { this.closeMenuOnEscape(); // Stop the esc keypress from bubbling out and closing something it shouldn't evt.stopPropagation(); } }); _rollupPluginBabelHelpers.defineProperty(this, "handleClickOutside", evt => { if (this.state.open && (!this._menuBody || !this._menuBody.contains(evt.target))) { this.closeMenu(); } }); _rollupPluginBabelHelpers.defineProperty(this, "closeMenu", onCloseMenu => { const { onClose = noopFn.noopFn } = this.props; this.setState({ open: false }, () => { // Optional callback to be executed after the state as been set to close if (onCloseMenu) { onCloseMenu(); } onClose(); }); }); _rollupPluginBabelHelpers.defineProperty(this, "focusMenuEl", () => { const { current: triggerEl } = this._triggerRef; if (triggerEl) { triggerEl.focus(); } }); /** * Focuses the next enabled overflow menu item given the currently focused * item index and direction to move * @param {object} params * @param {number} params.currentIndex - the index of the currently focused * overflow menu item in the list of overflow menu items * @param {number} params.direction - number denoting the direction to move * focus (1 for forwards, -1 for backwards) */ _rollupPluginBabelHelpers.defineProperty(this, "handleOverflowMenuItemFocus", _ref => { let { currentIndex, direction } = _ref; const enabledIndices = React__default["default"].Children.toArray(this.props.children).reduce((acc, curr, i) => { if ( /*#__PURE__*/React__default["default"].isValidElement(curr) && !curr.props.disabled) { acc.push(i); } return acc; }, []); const nextValidIndex = (() => { const nextIndex = enabledIndices.indexOf(currentIndex) + direction; switch (nextIndex) { case -1: return enabledIndices.length - 1; case enabledIndices.length: return 0; default: return nextIndex; } })(); const overflowMenuItem = this[`overflowMenuItem${enabledIndices[nextValidIndex]}`]; overflowMenuItem?.focus(); }); /** * Handles the floating menu being unmounted or non-floating menu being * mounted or unmounted. * @param {Element} menuBody The DOM element of the menu body. * @private */ _rollupPluginBabelHelpers.defineProperty(this, "_menuBody", null); _rollupPluginBabelHelpers.defineProperty(this, "_bindMenuBody", menuBody => { if (!menuBody) { this._menuBody = menuBody; } if (!menuBody && this._hFocusIn) { this._hFocusIn = this._hFocusIn.release(); } }); /** * Handles the floating menu being placed. * @param {Element} menuBody The DOM element of the menu body. * @private */ _rollupPluginBabelHelpers.defineProperty(this, "_handlePlace", menuBody => { const { onOpen = noopFn.noopFn } = this.props; if (menuBody) { this._menuBody = menuBody; const hasFocusin = ('onfocusin' in window); const focusinEventName = hasFocusin ? 'focusin' : 'focus'; this._hFocusIn = on(menuBody.ownerDocument, focusinEventName, event => { const target = ClickListener["default"].getEventTarget(event); const { current: triggerEl } = this._triggerRef; if (typeof target.matches === 'function') { if (!menuBody.contains(target) && triggerEl && !target.matches(`.${this.context}--overflow-menu:first-child,.${this.context}--overflow-menu-options:first-child`)) { this.closeMenuAndFocus(); } } }, !hasFocusin); onOpen(); } }); /** * @returns {Element} The DOM element where the floating menu is placed in. */ _rollupPluginBabelHelpers.defineProperty(this, "_getTarget", () => { const { current: triggerEl } = this._triggerRef; return triggerEl instanceof Element && triggerEl.closest('[data-floating-menu-container]') || document.body; }); } componentDidUpdate(_, prevState) { const { onClose = noopFn.noopFn } = this.props; if (!this.state.open && prevState.open) { onClose(); } } componentDidMount() { // ensure that if open=true on first render, we wait // to render the floating menu until the trigger ref is not null if (this._triggerRef.current) { this.setState({ hasMountedTrigger: true }); } } static getDerivedStateFromProps(_ref2, state) { let { open } = _ref2; const { prevOpen } = state; return prevOpen === open ? null : { open, prevOpen: open }; } componentWillUnmount() { if (typeof this._hBlurTimeout === 'number') { clearTimeout(this._hBlurTimeout); this._hBlurTimeout = undefined; } } render() { const prefix = this.context; const { id, ['aria-label']: ariaLabel = null, ariaLabel: deprecatedAriaLabel, children, iconDescription = 'Options', direction = FloatingMenu.DIRECTION_BOTTOM, flipped = false, focusTrap = true, menuOffset = getMenuOffset, menuOffsetFlip = getMenuOffset, iconClass, onClick = noopFn.noopFn, // eslint-disable-line onOpen = noopFn.noopFn, // eslint-disable-line selectorPrimaryFocus = '[data-floating-menu-primary-focus]', // eslint-disable-line renderIcon: IconElement = iconsReact.OverflowMenuVertical, // eslint-disable-next-line react/prop-types innerRef: ref, menuOptionsClass, light, size = 'md', disableTooltip = false, ...other } = this.props; const { open = false } = this.state; const overflowMenuClasses = cx__default["default"](this.props.className, `${prefix}--overflow-menu`, { [`${prefix}--overflow-menu--open`]: open, [`${prefix}--overflow-menu--light`]: light, [`${prefix}--overflow-menu--${size}`]: size }); const overflowMenuOptionsClasses = cx__default["default"](menuOptionsClass, `${prefix}--overflow-menu-options`, { [`${prefix}--overflow-menu--flip`]: this.props.flipped, [`${prefix}--overflow-menu-options--open`]: open, [`${prefix}--overflow-menu-options--light`]: light, [`${prefix}--overflow-menu-options--${size}`]: size }); const overflowMenuIconClasses = cx__default["default"](`${prefix}--overflow-menu__icon`, iconClass); const childrenWithProps = React__default["default"].Children.toArray(children).map((child, index) => /*#__PURE__*/React__default["default"].isValidElement(child) ? /*#__PURE__*/React__default["default"].cloneElement(child, { // @ts-expect-error: PropTypes are not expressive enough to cover this case closeMenu: child.props.closeMenu || this.closeMenuAndFocus, handleOverflowMenuItemFocus: this.handleOverflowMenuItemFocus, ref: e => { this[`overflowMenuItem${index}`] = e; }, index }) : null); const menuBodyId = `overflow-menu-${this.instanceId}__menu-body`; const menuBody = /*#__PURE__*/React__default["default"].createElement("ul", { className: overflowMenuOptionsClasses, tabIndex: -1, role: "menu", "aria-label": ariaLabel || deprecatedAriaLabel, onKeyDown: this.handleKeyPress, id: menuBodyId }, childrenWithProps); const wrappedMenuBody = /*#__PURE__*/React__default["default"].createElement(FloatingMenu["default"], { focusTrap: focusTrap, triggerRef: this._triggerRef, menuDirection: direction, menuOffset: flipped ? menuOffsetFlip : menuOffset, menuRef: this._bindMenuBody, flipped: this.props.flipped, target: this._getTarget, onPlace: this._handlePlace, selectorPrimaryFocus: this.props.selectorPrimaryFocus }, /*#__PURE__*/React__default["default"].cloneElement(menuBody, { 'data-floating-menu-direction': direction })); const iconProps = { className: overflowMenuIconClasses, 'aria-label': iconDescription }; return /*#__PURE__*/React__default["default"].createElement(ClickListener["default"], { onClickOutside: this.handleClickOutside }, /*#__PURE__*/React__default["default"].createElement("span", { className: `${prefix}--overflow-menu__wrapper`, "aria-owns": open ? menuBodyId : undefined }, /*#__PURE__*/React__default["default"].createElement(IconButton.IconButton, _rollupPluginBabelHelpers["extends"]({}, other, { type: "button", "aria-haspopup": true, "aria-expanded": open, "aria-controls": open ? menuBodyId : undefined, className: overflowMenuClasses, onClick: this.handleClick, id: id, ref: mergeRefs["default"](this._triggerRef, ref), size: size, label: iconDescription, kind: "ghost", disableTooltip: disableTooltip }), /*#__PURE__*/React__default["default"].createElement(IconElement, iconProps)), open && this.state.hasMountedTrigger && wrappedMenuBody)); } } _rollupPluginBabelHelpers.defineProperty(OverflowMenu, "propTypes", { /** * Specify a label to be read by screen readers on the container node */ ['aria-label']: PropTypes__default["default"].string, /** * Deprecated, please use `aria-label` instead. * Specify a label to be read by screen readers on the container note. */ ariaLabel: deprecate["default"](PropTypes__default["default"].string, 'This prop syntax has been deprecated. Please use the new `aria-label`.'), /** * The child nodes. */ children: PropTypes__default["default"].node, /** * The CSS class names. */ className: PropTypes__default["default"].string, /** * The menu direction. */ direction: PropTypes__default["default"].oneOf([FloatingMenu.DIRECTION_TOP, FloatingMenu.DIRECTION_BOTTOM]), /** * Specify whether the tooltip should be disabled */ disableTooltip: PropTypes__default["default"].bool, /** * `true` if the menu alignment should be flipped. */ flipped: PropTypes__default["default"].bool, /** * Enable or disable focus trap behavior */ focusTrap: PropTypes__default["default"].bool, /** * The CSS class for the icon. */ iconClass: PropTypes__default["default"].string, /** * The icon description. */ iconDescription: PropTypes__default["default"].string, /** * The element ID. */ id: PropTypes__default["default"].string, /** * `true` to use the light version. For use on $ui-01 backgrounds only. * Don't use this to make OverflowMenu background color same as container background color. */ light: deprecate["default"](PropTypes__default["default"].bool, 'The `light` prop for `OverflowMenu` is no longer needed and has been deprecated. It will be removed in the next major release. Use the Layer component instead.'), /** * The adjustment in position applied to the floating menu. */ menuOffset: PropTypes__default["default"].oneOfType([PropTypes__default["default"].shape({ top: PropTypes__default["default"].number, left: PropTypes__default["default"].number }), PropTypes__default["default"].func]), /** * The adjustment in position applied to the floating menu. */ menuOffsetFlip: PropTypes__default["default"].oneOfType([PropTypes__default["default"].shape({ top: PropTypes__default["default"].number, left: PropTypes__default["default"].number }), PropTypes__default["default"].func]), /** * The class to apply to the menu options */ menuOptionsClass: PropTypes__default["default"].string, /** * The event handler for the `click` event. */ onClick: PropTypes__default["default"].func, /** * Function called when menu is closed */ onClose: PropTypes__default["default"].func, /** * The event handler for the `focus` event. */ onFocus: PropTypes__default["default"].func, /** * The event handler for the `keydown` event. */ onKeyDown: PropTypes__default["default"].func, /** * Function called when menu is opened */ onOpen: PropTypes__default["default"].func, /** * `true` if the menu should be open. */ open: PropTypes__default["default"].bool, /** * Function called to override icon rendering. */ renderIcon: PropTypes__default["default"].oneOfType([PropTypes__default["default"].func, PropTypes__default["default"].object]), /** * Specify a CSS selector that matches the DOM element that should * be focused when the OverflowMenu opens */ selectorPrimaryFocus: PropTypes__default["default"].string, /** * Specify the size of the OverflowMenu. Currently supports either `sm`, 'md' (default) or 'lg` as an option. */ size: PropTypes__default["default"].oneOf(['sm', 'md', 'lg']) }); _rollupPluginBabelHelpers.defineProperty(OverflowMenu, "contextType", usePrefix.PrefixContext); (() => { const forwardRef = (props, ref) => /*#__PURE__*/React__default["default"].createElement(OverflowMenu, _rollupPluginBabelHelpers["extends"]({}, props, { innerRef: ref })); forwardRef.displayName = 'OverflowMenu'; return /*#__PURE__*/React__default["default"].forwardRef(forwardRef); })(); exports.OverflowMenu = OverflowMenu; exports.getMenuOffset = getMenuOffset;