UNPKG

@carbon/react

Version:

React components for the Carbon Design System

190 lines (185 loc) 6.42 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. */ import { extends as _extends } from '../../_virtual/_rollupPluginBabelHelpers.js'; import React, { forwardRef, useRef, useLayoutEffect } from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; import { ChevronDown } from '@carbon/icons-react'; import Button from '../Button/Button.js'; import '../Button/Button.Skeleton.js'; import { Menu } from '../Menu/Menu.js'; import '../Menu/MenuItem.js'; import { useAttachedMenu } from '../../internal/useAttachedMenu.js'; import { useId } from '../../internal/useId.js'; import { usePrefix } from '../../internal/usePrefix.js'; import { flip, size, useFloating, autoUpdate } from '@floating-ui/react'; import { useFeatureFlag } from '../FeatureFlags/index.js'; import mergeRefs from '../../tools/mergeRefs.js'; const validButtonKinds = ['primary', 'tertiary', 'ghost']; const defaultButtonKind = 'primary'; const MenuButton = /*#__PURE__*/forwardRef(function MenuButton({ children, className, disabled, kind = defaultButtonKind, label, size: size$1 = '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 = useFeatureFlag('enable-v12-dynamic-floating-styles'); const id = useId('MenuButton'); const prefix = usePrefix(); const triggerRef = useRef(null); let middlewares = []; if (!enableOnlyFloatingStyles) { middlewares = [flip({ crossAxis: false })]; } if (menuAlignment === 'bottom' || menuAlignment === 'top') { middlewares.push(size({ apply({ rects, elements }) { Object.assign(elements.floating.style, { width: `${rects.reference.width}px` }); } })); } const { refs, floatingStyles, placement, middlewareData } = 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: autoUpdate }); const ref = mergeRefs(forwardRef, triggerRef); const { open, handleClick: hookOnClick, handleMousedown, handleClose } = useAttachedMenu(triggerRef); useLayoutEffect(() => { 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", _extends({}, rest, { ref: ref, "aria-owns": open ? id : undefined, className: containerClasses }), /*#__PURE__*/React.createElement(Button, { ref: refs.setReference, className: triggerClasses, size: size$1, tabIndex: tabIndex, kind: kind, renderIcon: ChevronDown, disabled: disabled, "aria-haspopup": true, "aria-expanded": open, onClick: handleClick, onMouseDown: handleMousedown, "aria-controls": open ? id : undefined }, label), /*#__PURE__*/React.createElement(Menu, { containerRef: triggerRef, menuAlignment: menuAlignment, className: menuClasses, ref: refs.setFloating, id: id, legacyAutoalign: false, label: label, size: size$1, open: open, onClose: handleClose, target: menuTarget }, 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. */ // @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. */ // @ts-ignore-next-line -- avoid spurious (?) TS2322 error size: PropTypes.oneOf(['sm', 'md', 'lg']), /** * Specify the tabIndex of the button. */ // @ts-ignore-next-line -- avoid spurious (?) TS2322 error tabIndex: PropTypes.number, /** * Specify a DOM node where the Menu should be rendered in. Defaults to document.body. */ menuTarget: PropTypes.instanceOf(typeof Element !== 'undefined' ? Element : Object) }; export { MenuButton };