@mskcc/carbon-react
Version:
Carbon react components for the MSKCC DSM
541 lines (528 loc) • 19.5 kB
JavaScript
/**
* 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;