UNPKG

@carbon/react

Version:

React components for the Carbon Design System

441 lines (433 loc) 14.2 kB
/** * Copyright IBM Corp. 2016, 2023 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ 'use strict'; var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js'); var cx = require('classnames'); var PropTypes = require('prop-types'); var React = require('react'); var react = require('@floating-ui/react'); var iconsReact = require('@carbon/icons-react'); var keys = require('../../internal/keyboard/keys.js'); var match = require('../../internal/keyboard/match.js'); var useControllableState = require('../../internal/useControllableState.js'); var useMergedRefs = require('../../internal/useMergedRefs.js'); var usePrefix = require('../../internal/usePrefix.js'); var useId = require('../../internal/useId.js'); var Menu = require('./Menu.js'); var MenuContext = require('./MenuContext.js'); require('../LayoutDirection/LayoutDirection.js'); var useLayoutDirection = require('../LayoutDirection/useLayoutDirection.js'); var Text = require('../Text/Text.js'); require('../Text/TextDirection.js'); var defaultItemToString = require('../../internal/defaultItemToString.js'); var _Checkmark, _CaretLeft, _CaretRight; const MenuItem = /*#__PURE__*/React.forwardRef(function MenuItem({ children, className, dangerDescription = 'danger', disabled, kind = 'default', label, onClick, renderIcon: IconElement, shortcut, ...rest }, forwardRef) { const [submenuOpen, setSubmenuOpen] = React.useState(false); const [rtl, setRtl] = React.useState(false); const { refs, floatingStyles, context: floatingContext } = react.useFloating({ open: submenuOpen, onOpenChange: setSubmenuOpen, placement: rtl ? 'left-start' : 'right-start', whileElementsMounted: react.autoUpdate, middleware: [react.offset({ mainAxis: -6, crossAxis: -6 })], strategy: 'fixed' }); const { getReferenceProps, getFloatingProps } = react.useInteractions([react.useHover(floatingContext, { delay: 100, enabled: true, handleClose: react.safePolygon({ requireIntent: false }) })]); const prefix = usePrefix.usePrefix(); const context = React.useContext(MenuContext.MenuContext); const menuItem = React.useRef(null); const ref = useMergedRefs.useMergedRefs([forwardRef, menuItem, refs.setReference]); const hasChildren = Boolean(children); const isDisabled = disabled && !hasChildren; const isDanger = kind === 'danger' && !hasChildren; function registerItem() { context.dispatch({ type: 'registerItem', payload: { ref: menuItem, disabled: Boolean(disabled) } }); } function openSubmenu() { if (!menuItem.current) { return; } setSubmenuOpen(true); } function closeSubmenu() { setSubmenuOpen(false); } function handleClick(e) { if (!isDisabled) { if (hasChildren) { openSubmenu(); } else { context.state.requestCloseRoot(e); if (onClick) { onClick(e); } } } } // Avoid stray keyup event from MenuButton affecting MenuItem, and vice versa. // Keyboard click is handled differently for <button> vs. <li> and for Enter vs. Space. See // https://www.stefanjudis.com/today-i-learned/keyboard-button-clicks-with-space-and-enter-behave-differently/. const pendingKeyboardClick = React.useRef(false); const keyboardClickEvent = e => match.match(e, keys.Enter) || match.match(e, keys.Space); function handleKeyDown(e) { if (hasChildren && match.match(e, keys.ArrowRight)) { openSubmenu(); requestAnimationFrame(() => { refs.floating.current?.focus(); }); e.stopPropagation(); e.preventDefault(); } pendingKeyboardClick.current = keyboardClickEvent(e); if (rest.onKeyDown) { rest.onKeyDown(e); } } function handleKeyUp(e) { if (pendingKeyboardClick.current && keyboardClickEvent(e)) { handleClick(e); } pendingKeyboardClick.current = false; } 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) React.useEffect(() => { registerItem(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Set RTL based on the document direction or `LayoutDirection` const { direction } = useLayoutDirection.useLayoutDirection(); React.useEffect(() => { if (document?.dir === 'rtl' || direction === 'rtl') { setRtl(true); } else { setRtl(false); } }, [direction]); React.useEffect(() => { if (IconElement && !context.state.hasIcons) { // @ts-expect-error - TODO: Should we be passing payload? context.dispatch({ type: 'enableIcons' }); } }, [IconElement, context.state.hasIcons, context]); React.useEffect(() => { Object.keys(floatingStyles).forEach(style => { if (refs.floating.current && style !== 'position') { refs.floating.current.style[style] = floatingStyles[style]; } }); }, [floatingStyles, refs.floating]); const assistiveId = useId.useId('danger-description'); return /*#__PURE__*/React.createElement(react.FloatingFocusManager, { context: floatingContext, order: ['reference', 'floating'], modal: false }, /*#__PURE__*/React.createElement("li", _rollupPluginBabelHelpers.extends({ role: "menuitem" }, rest, { ref: ref, className: classNames, tabIndex: !disabled ? 0 : -1, "aria-disabled": isDisabled ?? undefined, "aria-haspopup": hasChildren ?? undefined, "aria-expanded": hasChildren ? submenuOpen : undefined, onClick: handleClick, onKeyDown: handleKeyDown, onKeyUp: handleKeyUp, title: label }, getReferenceProps()), /*#__PURE__*/React.createElement("div", { className: `${prefix}--menu-item__selection-icon` }, rest['aria-checked'] && (_Checkmark || (_Checkmark = /*#__PURE__*/React.createElement(iconsReact.Checkmark, null)))), /*#__PURE__*/React.createElement("div", { className: `${prefix}--menu-item__icon` }, IconElement && /*#__PURE__*/React.createElement(IconElement, null)), /*#__PURE__*/React.createElement(Text.Text, { as: "div", className: `${prefix}--menu-item__label` }, label), isDanger && /*#__PURE__*/React.createElement("span", { id: assistiveId, className: `${prefix}--visually-hidden` }, dangerDescription), shortcut && !hasChildren && /*#__PURE__*/React.createElement("div", { className: `${prefix}--menu-item__shortcut` }, shortcut), hasChildren && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", { className: `${prefix}--menu-item__shortcut` }, rtl ? _CaretLeft || (_CaretLeft = /*#__PURE__*/React.createElement(iconsReact.CaretLeft, null)) : _CaretRight || (_CaretRight = /*#__PURE__*/React.createElement(iconsReact.CaretRight, null))), /*#__PURE__*/React.createElement(Menu.Menu, _rollupPluginBabelHelpers.extends({ label: label, open: submenuOpen, onClose: () => { closeSubmenu(); menuItem.current?.focus(); }, ref: refs.setFloating }, getFloatingProps()), 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 the message read by screen readers for the danger menu item variant */ dangerDescription: 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 required label titling the MenuItem. Will be rendered as its text content. */ label: PropTypes.string.isRequired, /** * Provide an optional function to be called when the MenuItem is clicked. */ onClick: PropTypes.func, /** * A component used to render an icon. */ renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), /** * 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.forwardRef(function MenuItemSelectable({ className, defaultSelected, label, onChange, selected, ...rest }, forwardRef) { const prefix = usePrefix.usePrefix(); const context = React.useContext(MenuContext.MenuContext); const [checked, setChecked] = useControllableState.useControllableState({ value: selected, onChange, defaultValue: defaultSelected ?? false }); function handleClick() { setChecked(!checked); } React.useEffect(() => { if (!context.state.hasSelectableItems) { // @ts-expect-error - TODO: Should we be passing payload? context.dispatch({ type: 'enableSelectableItems' }); } }, [context.state.hasSelectableItems, context]); const classNames = cx(className, `${prefix}--menu-item-selectable--selected`); return /*#__PURE__*/React.createElement(MenuItem, _rollupPluginBabelHelpers.extends({}, rest, { ref: forwardRef, label: label, className: classNames, role: "menuitemcheckbox", "aria-checked": checked, onClick: handleClick })); }); MenuItemSelectable.propTypes = { /** * Additional CSS class names. */ className: PropTypes.string, /** * Specify whether the option should be selected by default. */ defaultSelected: PropTypes.bool, /** * A required label titling this option. */ label: PropTypes.string.isRequired, /** * 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.forwardRef(function MenuItemGroup({ children, className, label, ...rest }, forwardRef) { const prefix = usePrefix.usePrefix(); const classNames = cx(className, `${prefix}--menu-item-group`); return /*#__PURE__*/React.createElement("li", { className: classNames, role: "none", ref: forwardRef }, /*#__PURE__*/React.createElement("ul", _rollupPluginBabelHelpers.extends({}, rest, { role: "group", "aria-label": label }), 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.isRequired }; const MenuItemRadioGroup = /*#__PURE__*/React.forwardRef(function MenuItemRadioGroup({ className, defaultSelectedItem, items, itemToString = defaultItemToString.defaultItemToString, label, onChange, selectedItem, ...rest }, forwardRef) { const prefix = usePrefix.usePrefix(); const context = React.useContext(MenuContext.MenuContext); const [selection, setSelection] = useControllableState.useControllableState({ value: selectedItem, onChange, defaultValue: defaultSelectedItem ?? {} }); //eslint-disable-next-line @typescript-eslint/no-unused-vars -- https://github.com/carbon-design-system/carbon/issues/20452 function handleClick(item, e) { setSelection(item); } React.useEffect(() => { if (!context.state.hasSelectableItems) { // @ts-expect-error - TODO: Should we be passing payload? context.dispatch({ type: 'enableSelectableItems' }); } }, [context.state.hasSelectableItems, context]); const classNames = cx(className, `${prefix}--menu-item-radio-group`); return /*#__PURE__*/React.createElement("li", { className: classNames, role: "none", ref: forwardRef }, /*#__PURE__*/React.createElement("ul", _rollupPluginBabelHelpers.extends({}, rest, { role: "group", "aria-label": label }), items.map((item, i) => /*#__PURE__*/React.createElement(MenuItem, { key: i, label: itemToString(item), role: "menuitemradio", "aria-checked": item === selection, onClick: e => { handleClick(item); } })))); }); MenuItemRadioGroup.propTypes = { /** * Additional CSS class names. */ className: PropTypes.string, /** * Specify the default selected item. Must match the type of props.items. */ defaultSelectedItem: PropTypes.any, /** * Converts an item into a string for display. */ 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 required label titling this radio group. */ label: PropTypes.string.isRequired, /** * 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.forwardRef(function MenuItemDivider({ className, ...rest }, forwardRef) { const prefix = usePrefix.usePrefix(); const classNames = cx(className, `${prefix}--menu-item-divider`); return /*#__PURE__*/React.createElement("li", _rollupPluginBabelHelpers.extends({}, rest, { className: classNames, role: "separator", ref: forwardRef })); }); MenuItemDivider.propTypes = { /** * Additional CSS class names. */ className: PropTypes.string }; exports.MenuItem = MenuItem; exports.MenuItemDivider = MenuItemDivider; exports.MenuItemGroup = MenuItemGroup; exports.MenuItemRadioGroup = MenuItemRadioGroup; exports.MenuItemSelectable = MenuItemSelectable;