@carbon/react
Version:
React components for the Carbon Design System
399 lines (378 loc) • 16.9 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.
*/
;
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;