UNPKG

@carbon/react

Version:

React components for the Carbon Design System

209 lines (204 loc) 7.57 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 React = require('react'); var PropTypes = require('prop-types'); var cx = require('classnames'); var iconsReact = require('@carbon/icons-react'); var Button = require('../Button/Button.js'); require('../Button/Button.Skeleton.js'); var Menu = require('../Menu/Menu.js'); require('../Menu/MenuItem.js'); var useAttachedMenu = require('../../internal/useAttachedMenu.js'); var useId = require('../../internal/useId.js'); var usePrefix = require('../../internal/usePrefix.js'); var useIsomorphicEffect = require('../../internal/useIsomorphicEffect.js'); var react = require('@floating-ui/react'); var index = require('../FeatureFlags/index.js'); var mergeRefs = require('../../tools/mergeRefs.js'); const validButtonKinds = ['primary', 'tertiary', 'ghost']; const defaultButtonKind = 'primary'; // eslint-disable-next-line react/display-name -- https://github.com/carbon-design-system/carbon/issues/20452 const MenuButton = /*#__PURE__*/React.forwardRef(({ children, className, disabled, kind = defaultButtonKind, label, menuBackgroundToken = 'layer', menuBorder = false, size = 'lg', menuAlignment = 'bottom', tabIndex = 0, menuTarget, ...rest }, forwardRef) => { // feature flag utilized to separate out only the dynamic styles from @floating-ui // flag is turned on when collision detection (ie. flip, hide) logic is not desired const enableOnlyFloatingStyles = index.useFeatureFlag('enable-v12-dynamic-floating-styles'); const id = useId.useId('MenuButton'); const prefix = usePrefix.usePrefix(); const triggerRef = React.useRef(null); // eslint-disable-next-line @typescript-eslint/no-explicit-any -- https://github.com/carbon-design-system/carbon/issues/20452 let middlewares = []; if (!enableOnlyFloatingStyles) { middlewares = [react.flip({ crossAxis: false })]; } if (menuAlignment === 'bottom' || menuAlignment === 'top') { middlewares.push(react.size({ apply({ rects, elements }) { Object.assign(elements.floating.style, { width: `${rects.reference.width}px` }); } })); } const { refs, floatingStyles, placement, middlewareData } = react.useFloating({ 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', // Submenus are using a fixed position to break out of the parent menu's // box avoiding clipping while allowing for vertical scroll. When an // element is using transform it establishes a new containing block // block for all of its descendants. Therefore, its padding box will be // used for fixed-positioned descendants. This would cause the submenu // to be clipped by its parent menu. // Reference: https://www.w3.org/TR/2019/CR-css-transforms-1-20190214/#current-transformation-matrix-computation // Reference: https://github.com/carbon-design-system/carbon/pull/18153#issuecomment-2498548835 transform: false, // Middleware order matters, arrow should be last middleware: middlewares, whileElementsMounted: react.autoUpdate }); const ref = mergeRefs.mergeRefs(forwardRef, triggerRef); const { open, handleClick: hookOnClick, handleMousedown, handleClose } = useAttachedMenu.useAttachedMenu(triggerRef); useIsomorphicEffect.default(() => { Object.keys(floatingStyles).forEach(style => { if (refs.floating.current) { let value = floatingStyles[style]; if (['top', 'right', 'bottom', 'left'].includes(style) && Number(value)) { value += 'px'; } refs.floating.current.style[style] = value; } }); }, [floatingStyles, refs.floating, middlewareData, placement, open]); function handleClick() { if (triggerRef.current) { hookOnClick(); } } const containerClasses = cx(`${prefix}--menu-button__container`, className); const triggerClasses = cx(`${prefix}--menu-button__trigger`, { [`${prefix}--menu-button__trigger--open`]: open }); const menuClasses = cx(`${prefix}--menu-button__${menuAlignment}`); return /*#__PURE__*/React.createElement("div", _rollupPluginBabelHelpers.extends({}, rest, { ref: ref, "aria-owns": open ? id : undefined, className: containerClasses }), /*#__PURE__*/React.createElement(Button.default, { ref: refs.setReference, className: triggerClasses, size: size, tabIndex: tabIndex, kind: kind, renderIcon: iconsReact.ChevronDown, disabled: disabled, "aria-haspopup": true, "aria-expanded": open, onClick: handleClick, onMouseDown: handleMousedown, "aria-controls": open ? id : undefined }, label), /*#__PURE__*/React.createElement(Menu.Menu, { containerRef: triggerRef, menuAlignment: menuAlignment, className: menuClasses, ref: refs.setFloating, id: id, legacyAutoalign: false, label: label, size: size, open: open, onClose: handleClose, target: menuTarget, backgroundToken: menuBackgroundToken, border: menuBorder }, children)); }); MenuButton.propTypes = { /** * A collection of MenuItems to be rendered as actions for this MenuButton. */ children: PropTypes.node.isRequired, /** * Additional CSS class names. */ className: PropTypes.string, /** * Specify whether the MenuButton should be disabled, or not. */ disabled: PropTypes.bool, /** * Specify the type of button to be used as the base for the trigger button. */ // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- https://github.com/carbon-design-system/carbon/issues/20452 // @ts-ignore-next-line -- avoid spurious (?) TS2322 error kind: PropTypes.oneOf(validButtonKinds), /** * Provide the label to be rendered on the trigger button. */ label: PropTypes.string.isRequired, /** * Experimental property. Specify how the menu should align with the button element */ menuAlignment: PropTypes.oneOf(['top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end']), /** * Specify the size of the button and menu. */ // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- https://github.com/carbon-design-system/carbon/issues/20452 // @ts-ignore-next-line -- avoid spurious (?) TS2322 error size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']), /** * Specify the tabIndex of the button. */ // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- https://github.com/carbon-design-system/carbon/issues/20452 // @ts-ignore-next-line -- avoid spurious (?) TS2322 error tabIndex: PropTypes.number, /** * Specify the background token to use for the menu. Default is 'layer'. */ menuBackgroundToken: PropTypes.oneOf(['layer', 'background']), /** * Specify whether a border should be rendered on the menu */ menuBorder: PropTypes.bool, /** * Specify a DOM node where the Menu should be rendered in. Defaults to document.body. */ menuTarget: PropTypes.instanceOf(typeof Element !== 'undefined' ? Element : Object) }; exports.MenuButton = MenuButton;