@mskcc/carbon-react
Version:
Carbon react components for the MSKCC DSM
160 lines (155 loc) • 5.88 kB
JavaScript
/**
* MSKCC 2021, 2024
*/
import { extends as _extends } from '../../../_virtual/_rollupPluginBabelHelpers.js';
import React__default, { useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { useFloating, flip, autoUpdate } from '@floating-ui/react';
import { IconButton } from '../../IconButton/IconButton.js';
import { Menu } from '../../Menu/Menu.js';
import mergeRefs from '../../../tools/mergeRefs.js';
import { useId } from '../../../internal/useId.js';
import { usePrefix } from '../../../internal/usePrefix.js';
import { useAttachedMenu } from '../../../internal/useAttachedMenu.js';
const defaultSize = 'md';
const OverflowMenu = /*#__PURE__*/React__default.forwardRef(function OverflowMenu(_ref, forwardRef) {
var _IconElement;
let {
autoAlign = false,
children,
className,
label = 'Options',
renderIcon: IconElement = () => /*#__PURE__*/React__default.createElement("span", {
className: `msk-icon ${prefix}--overflow-menu__icon`
}, "vert"),
size = defaultSize,
menuAlignment = 'bottom-start',
tooltipAlignment,
...rest
} = _ref;
return function () {
const {
refs,
floatingStyles,
placement,
middlewareData
} = useFloating(autoAlign ? {
// Computing the position starts with initial positioning
// via `placement`.
placement: menuAlignment,
// The floating element is positioned relative to its nearest
// containing block (usually the viewport). It will in many cases
// also “break” the floating element out of a clipping ancestor.
// https://floating-ui.com/docs/misc#clipping
strategy: 'fixed',
// Middleware are executed as an in-between “middle” step of the
// initial `placement` computation and eventual return of data for
// rendering. Each middleware is executed in order.
middleware: [flip({
// An explicit array of placements to try if the initial
// `placement` doesn’t fit on the axes in which overflow
// is checked.
fallbackPlacements: menuAlignment.includes('bottom') ? ['bottom-start', 'bottom-end', 'top-start', 'top-end'] : ['top-start', 'top-end', 'bottom-start', 'bottom-end']
})],
whileElementsMounted: autoUpdate
} : {} // When autoAlign is turned off, floating-ui will not be used
);
const id = useId('overflowmenu');
const prefix = usePrefix();
const triggerRef = useRef(null);
const {
open,
x,
y,
handleClick: hookOnClick,
handleMousedown,
handleClose
} = useAttachedMenu(triggerRef);
useEffect(() => {
if (autoAlign) {
Object.keys(floatingStyles).forEach(style => {
if (refs.floating.current) {
refs.floating.current.style[style] = floatingStyles[style];
}
});
}
}, [floatingStyles, autoAlign, refs.floating, open, placement, middlewareData]);
function handleTriggerClick() {
if (triggerRef.current) {
hookOnClick();
}
}
const containerClasses = cx(className, `${prefix}--overflow-menu__container`);
const menuClasses = cx(`${prefix}--overflow-menu__${menuAlignment}`);
const triggerClasses = cx(`${prefix}--overflow-menu`, {
[`${prefix}--overflow-menu--open`]: open
}, size !== defaultSize && `${prefix}--overflow-menu--${size}`);
const floatingRef = mergeRefs(triggerRef, refs.setReference);
return /*#__PURE__*/React__default.createElement("div", _extends({}, rest, {
className: containerClasses,
"aria-owns": open ? id : undefined,
ref: forwardRef
}), /*#__PURE__*/React__default.createElement(IconButton, {
"aria-controls": open ? id : undefined,
"aria-haspopup": true,
"aria-expanded": open,
className: triggerClasses,
onClick: handleTriggerClick,
onMouseDown: event => handleMousedown(event),
ref: floatingRef,
label: label,
align: tooltipAlignment,
kind: "ghost"
}, _IconElement || (_IconElement = /*#__PURE__*/React__default.createElement(IconElement, null))), /*#__PURE__*/React__default.createElement(Menu, {
containerRef: triggerRef,
ref: refs.setFloating,
menuAlignment: menuAlignment,
className: menuClasses,
id: id,
size: size,
legacyAutoalign: !autoAlign,
open: open,
onClose: handleClose,
x: x,
y: y,
label: label
}, children));
}();
});
OverflowMenu.propTypes = {
/**
* **Experimental**: Will attempt to automatically align the floating element to avoid collisions with the viewport and being clipped by ancestor elements.
*/
autoAlign: PropTypes.bool,
/**
* A collection of MenuItems to be rendered within this OverflowMenu.
*/
children: PropTypes.node,
/**
* Additional CSS class names for the trigger button.
*/
className: PropTypes.string,
/**
* A label describing the options available. Is used in the trigger tooltip and as the menu's accessible label.
*/
label: PropTypes.string,
/**
* Experimental property. Specify how the menu should align with the button element
*/
menuAlignment: PropTypes.oneOf(['top-start', 'top-end', 'bottom-start', 'bottom-end']),
/**
* Optionally provide a custom icon to be rendered on the trigger button.
*/
// @ts-expect-error: PropTypes are not expressive enough to cover this case
renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
/**
* Specify the size of the menu, from a list of available sizes.
*/
size: PropTypes.oneOf(['sm', 'md', 'lg']),
/**
* Specify how the trigger tooltip should be aligned.
*/
tooltipAlignment: PropTypes.oneOf(['top', 'top-left', 'top-right', 'bottom', 'bottom-left', 'bottom-right', 'left', 'right'])
};
export { OverflowMenu };