@carbon/react
Version:
React components for the Carbon Design System
222 lines (212 loc) • 7.42 kB
JavaScript
/**
* 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, { useRef, useLayoutEffect } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { ChevronDown } from '@carbon/icons-react';
import { IconButton } from '../IconButton/index.js';
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, hide, size, useFloating, autoUpdate } from '@floating-ui/react';
import { useFeatureFlag } from '../FeatureFlags/index.js';
import mergeRefs from '../../tools/mergeRefs.js';
import deprecateValuesWithin from '../../prop-types/deprecateValuesWithin.js';
import { mapPopoverAlign } from '../../tools/mapPopoverAlign.js';
var _ChevronDown;
const defaultTranslations = {
'carbon.combo-button.additional-actions': 'Additional actions'
};
/**
* Message ids that will be passed to translateWithId().
*/
function defaultTranslateWithId(messageId) {
return defaultTranslations[messageId];
}
const ComboButton = /*#__PURE__*/React.forwardRef(function ComboButton({
children,
className,
disabled,
label,
onClick,
size: size$1 = 'lg',
menuAlignment = 'bottom',
tooltipAlignment,
translateWithId: t = defaultTranslateWithId,
...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('combobutton');
const prefix = usePrefix();
const containerRef = useRef(null);
let middlewares = [];
if (!enableOnlyFloatingStyles) {
middlewares = [flip({
crossAxis: false
}), hide()];
}
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',
// Middleware order matters, arrow should be last
middleware: middlewares,
whileElementsMounted: autoUpdate
});
const ref = mergeRefs(forwardRef, containerRef, refs.setReference);
const {
open,
handleClick: hookOnClick,
handleMousedown: handleTriggerMousedown,
handleClose
} = useAttachedMenu(containerRef);
useLayoutEffect(() => {
const updatedFloatingStyles = {
...floatingStyles,
visibility: middlewareData.hide?.referenceHidden ? 'hidden' : 'visible'
};
Object.keys(updatedFloatingStyles).forEach(style => {
if (refs.floating.current) {
refs.floating.current.style[style] = updatedFloatingStyles[style];
}
});
}, [floatingStyles, refs.floating, middlewareData, placement, open]);
function handleTriggerClick() {
if (containerRef.current) {
hookOnClick();
}
}
function handlePrimaryActionClick(e) {
if (onClick) {
onClick(e);
}
}
const containerClasses = cx(`${prefix}--combo-button__container`, `${prefix}--combo-button__container--${size$1}`, {
[`${prefix}--combo-button__container--open`]: open
}, className);
const menuClasses = cx(`${prefix}--combo-button__${menuAlignment}`);
const primaryActionClasses = cx(`${prefix}--combo-button__primary-action`);
const triggerClasses = cx(`${prefix}--combo-button__trigger`);
return /*#__PURE__*/React.createElement("div", _extends({}, rest, {
className: containerClasses,
ref: ref,
"aria-owns": open ? id : undefined
}), /*#__PURE__*/React.createElement("div", {
className: primaryActionClasses
}, /*#__PURE__*/React.createElement(Button, {
title: label,
size: size$1,
disabled: disabled,
onClick: handlePrimaryActionClick
}, label)), /*#__PURE__*/React.createElement(IconButton, {
ref: refs.setReference,
className: triggerClasses,
label: t('carbon.combo-button.additional-actions'),
size: size$1,
disabled: disabled,
align: tooltipAlignment,
"aria-haspopup": true,
"aria-expanded": open,
onClick: handleTriggerClick,
onMouseDown: handleTriggerMousedown,
"aria-controls": open ? id : undefined
}, _ChevronDown || (_ChevronDown = /*#__PURE__*/React.createElement(ChevronDown, null))), /*#__PURE__*/React.createElement(Menu, {
containerRef: containerRef,
menuAlignment: menuAlignment,
className: menuClasses,
ref: refs.setFloating,
id: id,
label: t('carbon.combo-button.additional-actions'),
size: size$1,
open: open,
onClose: handleClose
}, children));
});
ComboButton.propTypes = {
/**
* A collection of MenuItems to be rendered as additional actions for this ComboButton.
*/
children: PropTypes.node.isRequired,
/**
* Additional CSS class names.
*/
className: PropTypes.string,
/**
* Specify whether the ComboButton should be disabled, or not.
*/
disabled: PropTypes.bool,
/**
* Provide the label to be rendered on the primary action 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']),
/**
* Provide an optional function to be called when the primary action element is clicked.
*/
onClick: PropTypes.func,
/**
* Specify the size of the buttons and menu.
*/
size: PropTypes.oneOf(['sm', 'md', 'lg']),
/**
* Specify how the trigger tooltip should be aligned.
*/
tooltipAlignment: deprecateValuesWithin(PropTypes.oneOf(['top', 'top-left',
// deprecated use top-start instead
'top-right',
// deprecated use top-end instead
'bottom', 'bottom-left',
// deprecated use bottom-start instead
'bottom-right',
// deprecated use bottom-end instead
'left', 'left-bottom',
// deprecated use left-end instead
'left-top',
// deprecated use left-start instead
'right', 'right-bottom',
// deprecated use right-end instead
'right-top',
// deprecated use right-start instead
// new values to match floating-ui
'top-start', 'top-end', 'bottom-start', 'bottom-end', 'left-end', 'left-start', 'right-end', 'right-start']), ['top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'left-start', 'left-end', 'right', 'right-start', 'right-end'], mapPopoverAlign),
/**
* Optional method that takes in a message id and returns an
* internationalized string.
*/
translateWithId: PropTypes.func
};
export { ComboButton };