@mskcc/carbon-react
Version:
Carbon react components for the MSKCC DSM
408 lines (402 loc) • 12.2 kB
JavaScript
/**
* MSKCC 2021, 2024
*/
import { extends as _extends } from '../../_virtual/_rollupPluginBabelHelpers.js';
import cx from 'classnames';
import PropTypes from 'prop-types';
import React__default, { useContext, useRef, useState, useEffect } from 'react';
import { useControllableState } from '../../internal/useControllableState.js';
import { useMergedRefs } from '../../internal/useMergedRefs.js';
import { usePrefix } from '../../internal/usePrefix.js';
import { Menu } from './Menu.js';
import { MenuContext } from './MenuContext.js';
import { match } from '../../internal/keyboard/match.js';
import { ArrowRight, Enter, Space } from '../../internal/keyboard/keys.js';
var _span, _span2, _span3;
const hoverIntentDelay = 150; // in ms
const MenuItem = /*#__PURE__*/React__default.forwardRef(function MenuItem(_ref, forwardRef) {
let {
children,
className,
disabled,
kind = 'default',
label = '',
renderLabel,
onClick,
renderIcon: IconElement,
shortcut,
...rest
} = _ref;
const prefix = usePrefix();
const context = useContext(MenuContext);
const menuItem = useRef();
const ref = useMergedRefs([forwardRef, menuItem]);
const [boundaries, setBoundaries] = useState({
x: -1,
y: -1
});
const hasChildren = Boolean(children);
const [submenuOpen, setSubmenuOpen] = useState(false);
const hoverIntentTimeout = useRef(null);
const isDisabled = disabled && !hasChildren;
const isDanger = kind === 'danger' && !hasChildren;
function registerItem() {
context.dispatch({
type: 'registerItem',
payload: {
ref: menuItem,
disabled: Boolean(disabled)
}
});
}
function openSubmenu() {
const {
x,
y,
width,
height
} = menuItem.current.getBoundingClientRect();
setBoundaries({
x: [x, x + width],
y: [y, y + height]
});
setSubmenuOpen(true);
}
function closeSubmenu() {
setSubmenuOpen(false);
setBoundaries({
x: -1,
y: -1
});
}
function handleClick(e) {
if (!isDisabled) {
if (hasChildren) {
openSubmenu();
} else {
context.state.requestCloseRoot(e);
if (onClick) {
onClick(e);
}
}
}
}
function handleMouseEnter() {
hoverIntentTimeout.current = setTimeout(() => {
openSubmenu();
}, hoverIntentDelay);
}
function handleMouseLeave() {
clearTimeout(hoverIntentTimeout.current);
closeSubmenu();
menuItem.current.focus();
}
function handleKeyDown(e) {
if (hasChildren && match(e, ArrowRight)) {
openSubmenu();
e.stopPropagation();
}
if (match(e, Enter) || match(e, Space)) {
handleClick(e);
}
if (rest.onKeyDown) {
rest.onKeyDown(e);
}
}
const classNames = cx(className, `${prefix}--menu-item`, {
[`${prefix}--menu-item--disabled`]: isDisabled,
[`${prefix}--menu-item--danger`]: isDanger
});
// on first render, register this menuitem in the context's state
// (used for keyboard navigation)
useEffect(() => {
registerItem();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (renderLabel && hasChildren) {
return /*#__PURE__*/React__default.createElement("li", _extends({
role: "menuitem"
}, rest, {
ref: ref,
className: cx(className, `${prefix}--menu-item`, 'msk-menu-item--render-label'),
tabIndex: "-1",
"aria-disabled": isDisabled || null
}), children);
}
return /*#__PURE__*/React__default.createElement("li", _extends({
role: "menuitem"
}, rest, {
ref: ref,
className: classNames,
tabIndex: "-1",
"aria-disabled": isDisabled || null,
"aria-haspopup": hasChildren || null,
"aria-expanded": hasChildren ? submenuOpen : null,
onClick: handleClick,
onMouseEnter: hasChildren ? handleMouseEnter : null,
onMouseLeave: hasChildren ? handleMouseLeave : null,
onKeyDown: handleKeyDown
}), /*#__PURE__*/React__default.createElement("div", {
className: `${prefix}--menu-item__icon`
}, IconElement && /*#__PURE__*/React__default.createElement(IconElement, null)), /*#__PURE__*/React__default.createElement("div", {
className: `${prefix}--menu-item__label`
}, label), shortcut && !hasChildren && /*#__PURE__*/React__default.createElement("div", {
className: `${prefix}--menu-item__shortcut`
}, shortcut), hasChildren && /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("div", {
className: `${prefix}--menu-item__shortcut`
}, _span || (_span = /*#__PURE__*/React__default.createElement("span", {
className: "msk-icon msk--menu-item-shortcut-icon"
}, "arrow_right"))), /*#__PURE__*/React__default.createElement(Menu, {
label: label,
open: submenuOpen,
onClose: () => {
closeSubmenu();
menuItem.current.focus();
},
x: boundaries.x,
y: boundaries.y
}, children)));
});
MenuItem.propTypes = {
/**
* Optionally provide another Menu to create a submenu. props.children can't be used to specify the content of the MenuItem itself. Use props.label instead.
*/
children: PropTypes.node,
/**
* Additional CSS class names.
*/
className: PropTypes.string,
/**
* Specify whether the MenuItem is disabled or not.
*/
disabled: PropTypes.bool,
/**
* Specify the kind of the MenuItem.
*/
kind: PropTypes.oneOf(['default', 'danger']),
/**
* A label titling the MenuItem. Will be rendered as its text content.
*/
label: PropTypes.string,
/**
* Provide an optional function to be called when the MenuItem is clicked.
*/
onClick: PropTypes.func,
/**
* This prop is not intended for use. The only supported icons are Checkmarks to depict single- and multi-selects. This prop is used by MenuItemSelectable and MenuItemRadioGroup automatically.
*/
renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
/**
* If true, renderLabel will be used to display the children of the MenuItem. It can be any valid React node.
*/
renderLabel: PropTypes.bool,
/**
* Provide a shortcut for the action of this MenuItem. Note that the component will only render it as a hint but not actually register the shortcut.
*/
shortcut: PropTypes.string
};
const MenuItemSelectable = /*#__PURE__*/React__default.forwardRef(function MenuItemSelectable(_ref2, forwardRef) {
let {
className,
defaultSelected,
label = '',
onChange,
selected,
...rest
} = _ref2;
const prefix = usePrefix();
const context = useContext(MenuContext);
const [checked, setChecked] = useControllableState({
value: selected,
onChange,
defaultValue: defaultSelected ?? false
});
function handleClick(e) {
setChecked(!checked);
if (onChange) {
onChange(e);
}
}
useEffect(() => {
if (!context.state.hasIcons) {
context.dispatch({
type: 'enableIcons'
});
}
}, [context.state.hasIcons, context]);
const classNames = cx(className, `${prefix}--menu-item-selectable--selected`);
return /*#__PURE__*/React__default.createElement(MenuItem, _extends({}, rest, {
ref: forwardRef,
label: label,
className: classNames,
role: "menuitemcheckbox",
"aria-checked": checked,
renderIcon: () => {
return checked && (_span2 || (_span2 = /*#__PURE__*/React__default.createElement("span", {
className: "msk-icon msk--menu-item-checked-icon"
}, "check")));
},
onClick: handleClick
}));
});
MenuItemSelectable.propTypes = {
/**
* Additional CSS class names.
*/
className: PropTypes.string,
/**
* Specify whether the option should be selected by default.
*/
defaultSelected: PropTypes.bool,
/**
* A label titling this option.
*/
label: PropTypes.string,
/**
* Provide an optional function to be called when the selection state changes.
*/
onChange: PropTypes.func,
/**
* Pass a bool to props.selected to control the state of this option.
*/
selected: PropTypes.bool
};
const MenuItemGroup = /*#__PURE__*/React__default.forwardRef(function MenuItemGroup(_ref3, forwardRef) {
let {
children,
className,
label = '',
...rest
} = _ref3;
const prefix = usePrefix();
const classNames = cx(className, `${prefix}--menu-item-group`);
return /*#__PURE__*/React__default.createElement("li", {
className: classNames,
role: "none",
ref: forwardRef
}, /*#__PURE__*/React__default.createElement("ul", _extends({}, rest, {
role: "group",
"aria-label": label,
className: "msk-menu-item--group-list"
}), children));
});
MenuItemGroup.propTypes = {
/**
* A collection of MenuItems to be rendered within this group.
*/
children: PropTypes.node,
/**
* Additional CSS class names.
*/
className: PropTypes.string,
/**
* A required label titling this group.
*/
label: PropTypes.string
};
const defaultItemToString = item => item.toString();
const MenuItemRadioGroup = /*#__PURE__*/React__default.forwardRef(function MenuItemRadioGroup(_ref4, forwardRef) {
let {
className,
defaultSelectedItem,
items,
itemToString = defaultItemToString,
label = '',
onChange,
selectedItem,
...rest
} = _ref4;
const prefix = usePrefix();
const context = useContext(MenuContext);
const [selection, setSelection] = useControllableState({
value: selectedItem,
onChange,
defaultValue: defaultSelectedItem
});
function handleClick(item, e) {
setSelection(item);
if (onChange) {
onChange(e);
}
}
useEffect(() => {
if (!context.state.hasIcons) {
context.dispatch({
type: 'enableIcons'
});
}
}, [context.state.hasIcons, context]);
const classNames = cx(className, `${prefix}--menu-item-radio-group`);
return /*#__PURE__*/React__default.createElement("li", {
className: classNames,
role: "none",
ref: forwardRef
}, /*#__PURE__*/React__default.createElement("ul", _extends({}, rest, {
role: "group",
"aria-label": label,
className: `msk-menu-item--radio-group-list`
}), items.map((item, i) => /*#__PURE__*/React__default.createElement(MenuItem, {
key: i,
label: itemToString(item),
role: "menuitemradio",
"aria-checked": item === selection,
renderIcon: () => {
return item === selection && (_span3 || (_span3 = /*#__PURE__*/React__default.createElement("span", {
className: "msk-icon msk--menu-item-selection-icon"
}, "check")));
},
onClick: e => {
handleClick(item, e);
}
}))));
});
MenuItemRadioGroup.propTypes = {
/**
* Additional CSS class names.
*/
className: PropTypes.string,
/**
* Specify the default selected item. Must match the type of props.items.
*/
defaultSelectedItem: PropTypes.any,
/**
* Provide a function to convert an item to the string that will be rendered. Defaults to item.toString().
*/
itemToString: PropTypes.func,
/**
* Provide the options for this radio group. Can be of any type, as long as you provide an appropriate props.itemToString function.
*/
items: PropTypes.array,
/**
* A label titling this radio group.
*/
label: PropTypes.string,
/**
* Provide an optional function to be called when the selection changes.
*/
onChange: PropTypes.func,
/**
* Provide props.selectedItem to control the state of this radio group. Must match the type of props.items.
*/
selectedItem: PropTypes.any
};
const MenuItemDivider = /*#__PURE__*/React__default.forwardRef(function MenuItemDivider(_ref5, forwardRef) {
let {
className,
...rest
} = _ref5;
const prefix = usePrefix();
const classNames = cx(className, `${prefix}--menu-item-divider`);
return /*#__PURE__*/React__default.createElement("li", _extends({}, rest, {
className: classNames,
role: "separator",
ref: forwardRef
}));
});
MenuItemDivider.propTypes = {
/**
* Additional CSS class names.
*/
className: PropTypes.string
};
export { MenuItem, MenuItemDivider, MenuItemGroup, MenuItemRadioGroup, MenuItemSelectable };