UNPKG

@carbon/react

Version:

React components for the Carbon Design System

399 lines (378 loc) 16.9 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'; Object.defineProperty(exports, '__esModule', { value: true }); var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js'); var cx = require('classnames'); var PropTypes = require('prop-types'); var deprecateValuesWithin = require('../../prop-types/deprecateValuesWithin.js'); var React = require('react'); var useIsomorphicEffect = require('../../internal/useIsomorphicEffect.js'); var useMergedRefs = require('../../internal/useMergedRefs.js'); var usePrefix = require('../../internal/usePrefix.js'); var useEvent = require('../../internal/useEvent.js'); var mapPopoverAlign = require('../../tools/mapPopoverAlign.js'); var react = require('@floating-ui/react'); var index = require('../FeatureFlags/index.js'); var index$1 = require('../Toggletip/index.js'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var cx__default = /*#__PURE__*/_interopDefaultLegacy(cx); var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes); var React__default = /*#__PURE__*/_interopDefaultLegacy(React); const PopoverContext = /*#__PURE__*/React__default["default"].createContext({ setFloating: { current: null }, caretRef: { current: null }, autoAlign: null }); /** * Deprecated popover alignment values. * @deprecated Use NewPopoverAlignment instead. */ const Popover = /*#__PURE__*/React__default["default"].forwardRef(function PopoverRenderFunction({ isTabTip, align: initialAlign = isTabTip ? 'bottom-start' : 'bottom', as: BaseComponent = 'span', autoAlign = false, autoAlignBoundary, caret = isTabTip ? false : true, className: customClassName, children, dropShadow = true, highContrast = false, onRequestClose, open, alignmentAxisOffset, ...rest }, //this is a workaround, have to come back and fix this. forwardRef) { const prefix = usePrefix.usePrefix(); const floating = React.useRef(null); const caretRef = React.useRef(null); const popover = React.useRef(null); const enableFloatingStyles = index.useFeatureFlag('enable-v12-dynamic-floating-styles') || autoAlign; let align = mapPopoverAlign.mapPopoverAlign(initialAlign); // If the `Popover` is the last focusable item in the tab order, it should also close when the browser window loses focus (#12922) useEvent.useWindowEvent('blur', () => { if (open) { onRequestClose?.(); } }); useEvent.useWindowEvent('click', ({ target }) => { if (open && target instanceof Node && !popover.current?.contains(target)) { onRequestClose?.(); } }); // Slug styling places a border around the popover content so the caret // needs to be placed 1px further outside the popover content. To do so, // we look to see if any of the children has a className containing "slug" const initialCaretHeight = React__default["default"].Children.toArray(children).some(x => { return x?.props?.className?.includes('slug') || x?.props?.className?.includes('ai-label'); }) ? 7 : 6; // These defaults match the defaults defined in packages/styles/scss/components/popover/_popover.scss const popoverDimensions = React.useRef({ offset: 10, caretHeight: initialCaretHeight }); useIsomorphicEffect["default"](() => { // The popover is only offset when a caret is present. Technically, the custom properties // accessed below can be set by a user even if caret=false, but doing so does not follow // the design specification for Popover. if (caret && popover.current) { // Gather the dimensions of the caret and prefer the values set via custom properties. // If a value is not set via a custom property, provide a default value that matches the // default values defined in the sass style file const getStyle = window.getComputedStyle(popover.current, null); const offsetProperty = getStyle.getPropertyValue('--cds-popover-offset'); const caretProperty = getStyle.getPropertyValue('--cds-popover-caret-height'); // Handle if the property values are in px or rem. // We want to store just the base number value without a unit suffix if (offsetProperty) { popoverDimensions.current.offset = offsetProperty.includes('px') ? Number(offsetProperty.split('px', 1)[0]) * 1 : Number(offsetProperty.split('rem', 1)[0]) * 16; } if (caretProperty) { popoverDimensions.current.caretHeight = caretProperty.includes('px') ? Number(caretProperty.split('px', 1)[0]) * 1 : Number(caretProperty.split('rem', 1)[0]) * 16; } } }); const { refs, floatingStyles, placement, middlewareData } = react.useFloating(enableFloatingStyles ? { placement: align, // 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: [react.offset(!isTabTip ? { alignmentAxis: alignmentAxisOffset, mainAxis: popoverDimensions?.current?.offset } : 0), autoAlign && react.flip({ fallbackPlacements: isTabTip ? align.includes('bottom') ? ['bottom-start', 'bottom-end', 'top-start', 'top-end'] : ['top-start', 'top-end', 'bottom-start', 'bottom-end'] : align.includes('bottom') ? ['bottom', 'bottom-start', 'bottom-end', 'right', 'right-start', 'right-end', 'left', 'left-start', 'left-end', 'top', 'top-start', 'top-end'] : ['top', 'top-start', 'top-end', 'left', 'left-start', 'left-end', 'right', 'right-start', 'right-end', 'bottom', 'bottom-start', 'bottom-end'], fallbackStrategy: 'initialPlacement', fallbackAxisSideDirection: 'start', boundary: autoAlignBoundary }), react.arrow({ element: caretRef }), autoAlign && react.hide()], whileElementsMounted: react.autoUpdate } : {} // When autoAlign is turned off & the `enable-v12-dynamic-floating-styles` feature flag is not // enabled, floating-ui will not be used ); const value = React.useMemo(() => { return { floating, setFloating: refs.setFloating, caretRef, autoAlign: autoAlign }; }, [refs.setFloating, autoAlign]); if (isTabTip) { const tabTipAlignments = ['bottom-start', 'bottom-end']; if (!tabTipAlignments.includes(align)) { align = 'bottom-start'; } } React.useEffect(() => { if (enableFloatingStyles) { 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]; } }); if (caret && middlewareData && middlewareData.arrow && caretRef?.current) { const { x, y } = middlewareData.arrow; const staticSide = { top: 'bottom', right: 'left', bottom: 'top', left: 'right' }[placement.split('-')[0]]; caretRef.current.style.left = x != null ? `${x}px` : ''; caretRef.current.style.top = y != null ? `${y}px` : ''; // Ensure the static side gets unset when flipping to other placements' axes. caretRef.current.style.right = ''; caretRef.current.style.bottom = ''; if (staticSide) { caretRef.current.style[staticSide] = `${-popoverDimensions?.current?.caretHeight}px`; } } } }, [floatingStyles, refs.floating, enableFloatingStyles, middlewareData, placement, caret]); const ref = useMergedRefs.useMergedRefs([forwardRef, popover]); const currentAlignment = autoAlign && placement !== align ? placement : align; const className = cx__default["default"]({ [`${prefix}--popover-container`]: true, [`${prefix}--popover--caret`]: caret, [`${prefix}--popover--drop-shadow`]: dropShadow, [`${prefix}--popover--high-contrast`]: highContrast, [`${prefix}--popover--open`]: open, [`${prefix}--popover--auto-align ${prefix}--autoalign`]: enableFloatingStyles, [`${prefix}--popover--${currentAlignment}`]: true, [`${prefix}--popover--tab-tip`]: isTabTip }, customClassName); const mappedChildren = React__default["default"].Children.map(children, child => { const item = child; const displayName = item?.type?.displayName; /** * Only trigger elements (button) or trigger components (ToggletipButton) should be * cloned because these will be decorated with a trigger-specific className and ref. * * There are also some specific components that should not be cloned when autoAlign * is on, even if they are a trigger element. */ const isTriggerElement = item?.type === 'button'; const isTriggerComponent = enableFloatingStyles && displayName && ['ToggletipButton'].includes(displayName); const isAllowedTriggerComponent = enableFloatingStyles && !['ToggletipContent', 'PopoverContent'].includes(displayName); if (/*#__PURE__*/React__default["default"].isValidElement(item) && (isTriggerElement || isTriggerComponent || isAllowedTriggerComponent)) { const className = item?.props?.className; const ref = (item?.props).ref; const tabTipClasses = cx__default["default"](`${prefix}--popover--tab-tip__button`, className); return /*#__PURE__*/React__default["default"].cloneElement(item, { className: isTabTip && item?.type === 'button' ? tabTipClasses : className || '', // With cloneElement, if you pass a `ref`, it overrides the original ref. // https://react.dev/reference/react/cloneElement#parameters // The block below works around this and ensures that the original ref is still // called while allowing the floating-ui reference element to be set as well. // `useMergedRefs` can't be used here because hooks can't be called from within a callback. // More here: https://github.com/facebook/react/issues/8873#issuecomment-489579878 ref: node => { // For a popover, there isn't an explicit trigger component, it's just the first child that's // passed in which should *not* be PopoverContent. // For a toggletip there is a specific trigger component, ToggletipButton. // In either of these cases we want to set this as the reference node for floating-ui autoAlign // positioning. if (enableFloatingStyles && item?.type !== PopoverContent || enableFloatingStyles && item?.type === index$1.ToggletipButton) { // Set the reference element for floating-ui refs.setReference(node); } // Call the original ref, if any if (typeof ref === 'function') { ref(node); } else if (ref !== null && ref !== undefined) { ref.current = node; } } }); } else { return item; } }); const BaseComponentAsAny = BaseComponent; return /*#__PURE__*/React__default["default"].createElement(PopoverContext.Provider, { value: value }, /*#__PURE__*/React__default["default"].createElement(BaseComponentAsAny, _rollupPluginBabelHelpers["extends"]({}, rest, { className: className, ref: ref }), enableFloatingStyles || isTabTip ? mappedChildren : children)); }); // Note: this displayName is temporarily set so that Storybook ArgTable // correctly displays the name of this component if (process.env.NODE_ENV !== 'production') { Popover.displayName = 'Popover'; } Popover.propTypes = { /** * Specify how the popover should align with the trigger element */ align: deprecateValuesWithin["default"](PropTypes__default["default"].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.mapPopoverAlign), /** * Provide a custom element or component to render the top-level node for the * component. */ as: PropTypes__default["default"].oneOfType([PropTypes__default["default"].string, PropTypes__default["default"].elementType]), /** * Will auto-align the popover on first render if it is not visible. This prop is currently experimental and is subject to future changes. */ autoAlign: PropTypes__default["default"].bool, /** * Specify a bounding element to be used for autoAlign calculations. The viewport is used by default. This prop is currently experimental and is subject to future changes. */ autoAlignBoundary: PropTypes__default["default"].oneOfType([PropTypes__default["default"].oneOf(['clippingAncestors']), PropTypes__default["default"].elementType, PropTypes__default["default"].arrayOf(PropTypes__default["default"].elementType), PropTypes__default["default"].exact({ x: PropTypes__default["default"].number.isRequired, y: PropTypes__default["default"].number.isRequired, width: PropTypes__default["default"].number.isRequired, height: PropTypes__default["default"].number.isRequired })]), /** * Specify whether a caret should be rendered */ caret: PropTypes__default["default"].bool, /** * Provide elements to be rendered inside of the component */ children: PropTypes__default["default"].node, /** * Provide a custom class name to be added to the outermost node in the * component */ className: PropTypes__default["default"].string, /** * Specify whether a drop shadow should be rendered on the popover */ dropShadow: PropTypes__default["default"].bool, /** * Render the component using the high-contrast variant */ highContrast: PropTypes__default["default"].bool, /** * Render the component using the tab tip variant */ isTabTip: PropTypes__default["default"].bool, /** * Specify a handler for closing popover. * The handler should take care of closing the popover, e.g. changing the `open` prop. */ onRequestClose: PropTypes__default["default"].func, /** * Specify whether the component is currently open or closed */ open: PropTypes__default["default"].bool.isRequired }; function PopoverContentRenderFunction( // eslint-disable-next-line react/prop-types { className, children, ...rest }, forwardRef) { const prefix = usePrefix.usePrefix(); const { setFloating, caretRef, autoAlign } = React__default["default"].useContext(PopoverContext); const ref = useMergedRefs.useMergedRefs([setFloating, forwardRef]); const enableFloatingStyles = index.useFeatureFlag('enable-v12-dynamic-floating-styles') || autoAlign; return /*#__PURE__*/React__default["default"].createElement("span", _rollupPluginBabelHelpers["extends"]({}, rest, { className: `${prefix}--popover` }), /*#__PURE__*/React__default["default"].createElement("span", { className: cx__default["default"](`${prefix}--popover-content`, className), ref: ref }, children, enableFloatingStyles && /*#__PURE__*/React__default["default"].createElement("span", { className: cx__default["default"]({ [`${prefix}--popover-caret`]: true, [`${prefix}--popover--auto-align`]: true }), ref: caretRef })), !enableFloatingStyles && /*#__PURE__*/React__default["default"].createElement("span", { className: cx__default["default"]({ [`${prefix}--popover-caret`]: true }), ref: caretRef })); } const PopoverContent = /*#__PURE__*/React__default["default"].forwardRef(PopoverContentRenderFunction); PopoverContent.displayName = 'PopoverContent'; PopoverContent.propTypes = { /** * Provide elements to be rendered inside of the component */ children: PropTypes__default["default"].node, /** * Provide a custom class name to be added to the outermost node in the * component */ className: PropTypes__default["default"].string }; exports.Popover = Popover; exports.PopoverContent = PopoverContent;