@carbon/react
Version:
React components for the Carbon Design System
482 lines (461 loc) • 15.6 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, useState, useLayoutEffect, useMemo, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { usePrefix } from '../../internal/usePrefix.js';
import { breakpoints } from '@carbon/layout';
import { useMatchMedia } from '../../internal/useMatchMedia.js';
import '../Text/index.js';
import { MenuButton } from '../MenuButton/index.js';
import '../Menu/Menu.js';
import { MenuItem } from '../Menu/MenuItem.js';
import { DefinitionTooltip } from '../Tooltip/DefinitionTooltip.js';
import '../Tooltip/Tooltip.js';
import AspectRatio from '../AspectRatio/AspectRatio.js';
import { createOverflowHandler } from '@carbon/utilities';
import Tag from '../Tag/Tag.js';
import '../Tag/DismissibleTag.js';
import OperationalTag from '../Tag/OperationalTag.js';
import '../Tag/SelectableTag.js';
import '../Tag/Tag.Skeleton.js';
import useOverflowItems from '../../internal/useOverflowItems.js';
import { Popover, PopoverContent } from '../Popover/index.js';
import { useId } from '../../internal/useId.js';
import '../Grid/FlexGrid.js';
import { Grid as GridAsGridComponent } from '../Grid/Grid.js';
import '../Grid/Row.js';
import Column from '../Grid/Column.js';
import '../Grid/ColumnHang.js';
import '../Grid/GridContext.js';
import { Text } from '../Text/Text.js';
/**
* ----------
* PageHeader
* ----------
*/
const PageHeader = /*#__PURE__*/React.forwardRef(function PageHeader({
className,
children,
...other
}, ref) {
const prefix = usePrefix();
const classNames = cx({
[`${prefix}--page-header`]: true
}, className);
return /*#__PURE__*/React.createElement("div", _extends({
className: classNames,
ref: ref
}, other), children);
});
PageHeader.displayName = 'PageHeader';
/**
* -----------------------
* PageHeaderBreadcrumbBar
* -----------------------
*/
const PageHeaderBreadcrumbBar = /*#__PURE__*/React.forwardRef(function PageHeaderBreadcrumbBar({
border = true,
className,
children,
renderIcon: IconElement,
contentActions,
contentActionsFlush,
pageActions,
pageActionsFlush,
...other
}, ref) {
const prefix = usePrefix();
const classNames = cx({
[`${prefix}--page-header__breadcrumb-bar`]: true,
[`${prefix}--page-header__breadcrumb-bar-border`]: border,
[`${prefix}--page-header__breadcrumb__actions-flush`]: pageActionsFlush
}, className);
const contentActionsClasses = cx({
[`${prefix}--page-header__breadcrumb__content-actions`]: !contentActionsFlush
});
return /*#__PURE__*/React.createElement("div", _extends({
className: classNames,
ref: ref
}, other), /*#__PURE__*/React.createElement(GridAsGridComponent, null, /*#__PURE__*/React.createElement(Column, {
lg: 16,
md: 8,
sm: 4
}, /*#__PURE__*/React.createElement("div", {
className: `${prefix}--page-header__breadcrumb-container`
}, /*#__PURE__*/React.createElement("div", {
className: `${prefix}--page-header__breadcrumb-wrapper`
}, IconElement && /*#__PURE__*/React.createElement("div", {
className: `${prefix}--page-header__breadcrumb__icon`
}, /*#__PURE__*/React.createElement(IconElement, null)), children), /*#__PURE__*/React.createElement("div", {
className: `${prefix}--page-header__breadcrumb__actions`
}, /*#__PURE__*/React.createElement("div", {
className: contentActionsClasses
}, contentActions), pageActions)))));
});
PageHeaderBreadcrumbBar.displayName = 'PageHeaderBreadcrumbBar';
/**
* -----------------
* PageHeaderContent
* -----------------
*/
const PageHeaderContent = /*#__PURE__*/React.forwardRef(function PageHeaderContent({
className,
children,
title,
renderIcon: IconElement,
contextualActions,
pageActions,
...other
}, ref) {
const prefix = usePrefix();
const classNames = cx({
[`${prefix}--page-header__content`]: true
}, className);
const titleRef = useRef(null);
const [isEllipsisApplied, setIsEllipsisApplied] = useState(false);
const isEllipsisActive = element => {
setIsEllipsisApplied(element.offsetHeight < element.scrollHeight);
return element.offsetHeight < element.scrollHeight;
};
useLayoutEffect(() => {
titleRef.current && isEllipsisActive(titleRef.current);
}, [title]);
return /*#__PURE__*/React.createElement("div", _extends({
className: classNames,
ref: ref
}, other), /*#__PURE__*/React.createElement(GridAsGridComponent, null, /*#__PURE__*/React.createElement(Column, {
lg: 16,
md: 8,
sm: 4
}, /*#__PURE__*/React.createElement("div", {
className: `${prefix}--page-header__content__title-wrapper`
}, /*#__PURE__*/React.createElement("div", {
className: `${prefix}--page-header__content__start`
}, /*#__PURE__*/React.createElement("div", {
className: `${prefix}--page-header__content__title-container`
}, IconElement && /*#__PURE__*/React.createElement("div", {
className: `${prefix}--page-header__content__icon`
}, /*#__PURE__*/React.createElement(IconElement, null)), isEllipsisApplied ? /*#__PURE__*/React.createElement(DefinitionTooltip, {
definition: title
}, /*#__PURE__*/React.createElement(Text, {
ref: titleRef,
as: "h4",
className: `${prefix}--page-header__content__title`
}, title)) : /*#__PURE__*/React.createElement(Text, {
ref: titleRef,
as: "h4",
className: `${prefix}--page-header__content__title`
}, title)), contextualActions && /*#__PURE__*/React.createElement("div", {
className: `${prefix}--page-header__content__contextual-actions`
}, contextualActions)), pageActions), children)));
});
PageHeaderContent.displayName = 'PageHeaderContent';
PageHeaderContent.propTypes = {
/**
* Provide child elements to be rendered inside PageHeaderContent.
*/
children: PropTypes.node,
/**
* Specify an optional className to be added to your PageHeaderContent
*/
className: PropTypes.string,
/**
* Provide an optional icon to render in front of the PageHeaderContent's title.
*/
renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
/**
* The PageHeaderContent's title
*/
title: PropTypes.string.isRequired,
/**
* The PageHeaderContent's subtitle
*/
subtitle: PropTypes.string,
/**
* The PageHeaderContent's contextual actions
*/
contextualActions: PropTypes.node,
/**
* The PageHeaderContent's page actions
*/
pageActions: PropTypes.node
};
/**
* ----------------
* PageHeaderContentPageActions
* ----------------
*/
const PageHeaderContentPageActions = ({
className,
children,
menuButtonLabel = 'Actions',
actions,
...other
}) => {
const prefix = usePrefix();
const classNames = cx({
[`${prefix}--page-header__content__page-actions`]: true
}, className);
const containerRef = useRef(null);
const offsetRef = useRef(null);
const [menuButtonVisibility, setMenuButtonVisibility] = useState(false);
const [hiddenItems, setHiddenItems] = useState([]);
// need to set the grid columns width based on the menu button's width
// to avoid overlapping when resizing
useLayoutEffect(() => {
if (menuButtonVisibility && offsetRef.current) {
const width = offsetRef.current.offsetWidth;
document.documentElement.style.setProperty('--pageheader-title-grid-width', `${width}px`);
}
}, [menuButtonVisibility]);
useEffect(() => {
if (!containerRef.current || !Array.isArray(actions)) return;
createOverflowHandler({
container: containerRef.current,
// exclude the hidden menu button from children
maxVisibleItems: containerRef.current.children.length - 1,
onChange: (visible, hidden) => {
setHiddenItems(actions?.slice(visible.length));
if (hidden.length > 0) {
setMenuButtonVisibility(true);
}
}
});
}, []);
return /*#__PURE__*/React.createElement("div", _extends({
className: classNames,
ref: containerRef
}, other), actions && /*#__PURE__*/React.createElement(React.Fragment, null, Array.isArray(actions) && /*#__PURE__*/React.createElement(React.Fragment, null, actions.map(action => /*#__PURE__*/React.createElement("div", {
key: action.id
}, /*#__PURE__*/React.cloneElement(action.body, {
...action.body.props,
onClick: action.onClick
}))), /*#__PURE__*/React.createElement("span", {
"data-offset": true,
"data-hidden": true,
ref: offsetRef
}, /*#__PURE__*/React.createElement(MenuButton, {
menuAlignment: "bottom-end",
label: menuButtonLabel,
size: "md"
}, [...hiddenItems].reverse().map(item => /*#__PURE__*/React.createElement(MenuItem, _extends({
key: item.id,
onClick: item.onClick
}, item.menuItem))))))));
};
PageHeaderContentPageActions.displayName = 'PageHeaderContentPageActions';
PageHeaderContentPageActions.propTypes = {
/**
* Provide child elements to be rendered inside PageHeaderContentPageActions.
*/
children: PropTypes.node,
/**
* Specify an optional className to be added to your PageHeaderContentPageActions
*/
className: PropTypes.string,
/**
* The PageHeaderContent's collapsible Menu button label
*/
menuButtonLabel: PropTypes.string,
/**
* The PageHeaderContent's page actions
*/
actions: PropTypes.oneOfType([PropTypes.node, PropTypes.array])
};
/**
* ----------------
* PageHeaderContentText
* ----------------
*/
const PageHeaderContentText = ({
className,
children,
subtitle,
...other
}) => {
const prefix = usePrefix();
const classNames = cx({
[`${prefix}--page-header__content__body`]: true
}, className);
return /*#__PURE__*/React.createElement("div", _extends({
className: classNames
}, other), subtitle && /*#__PURE__*/React.createElement(Text, {
as: "h3",
className: `${prefix}--page-header__content__subtitle`
}, subtitle), children);
};
PageHeaderContentText.displayName = 'PageHeaderContentText';
PageHeaderContentText.propTypes = {
/**
* Provide child elements to be rendered inside PageHeaderContentText.
*/
children: PropTypes.node,
/**
* Specify an optional className to be added to your PageHeaderContentText
*/
className: PropTypes.string,
/**
* The PageHeaderContent's subtitle
*/
subtitle: PropTypes.string
};
/**
* ----------------
* PageHeaderHeroImage
* ----------------
*/
const PageHeaderHeroImage = ({
className,
children,
...other
}) => {
const prefix = usePrefix();
const classNames = cx({
[`${prefix}--page-header__hero-image`]: true
}, className);
const lgMediaQuery = `(min-width: ${breakpoints.lg.width})`;
const isLg = useMatchMedia(lgMediaQuery);
return /*#__PURE__*/React.createElement(AspectRatio, _extends({
className: classNames
}, other, {
ratio: isLg ? '2x1' : '3x2'
}), children);
};
PageHeaderHeroImage.displayName = 'PageHeaderHeroImage';
PageHeaderHeroImage.propTypes = {
/**
* Provide child elements to be rendered inside PageHeaderHeroImage.
*/
children: PropTypes.node,
/**
* Specify an optional className to be added to your PageHeaderHeroImage
*/
className: PropTypes.string
};
/**
* ----------------
* PageHeaderTabBar
* ----------------
*/
const PageHeaderTabBar = /*#__PURE__*/React.forwardRef(function PageHeaderTabBar({
className,
children,
tags = [],
...other
}, ref) {
const prefix = usePrefix();
const classNames = cx({
[`${prefix}--page-header__tab-bar`]: true
}, className);
// Early return if no tags are provided
if (!tags.length) {
return /*#__PURE__*/React.createElement("div", _extends({
className: classNames,
ref: ref
}, other), /*#__PURE__*/React.createElement(GridAsGridComponent, null, /*#__PURE__*/React.createElement(Column, {
lg: 16,
md: 8,
sm: 4
}, children)));
}
const [openPopover, setOpenPopover] = useState(false);
const tagSize = tags[0]?.size || 'md';
const instanceId = useId('PageHeaderTabBar');
const tagsWithIds = useMemo(() => {
return tags.map((tag, index) => ({
...tag,
id: tag.id || `tag-${index}-${instanceId}`
}));
}, [tags]);
const tagsContainerRef = useRef(null);
const offsetRef = useRef(null);
// To close popover when window resizes
useEffect(() => {
const handleResize = () => {
// Close the popover when window resizes to prevent unwanted opens
setOpenPopover(false);
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
// overflow items hook
const {
visibleItems = [],
hiddenItems = [],
itemRefHandler = () => {}
} = useOverflowItems(tagsWithIds, tagsContainerRef, offsetRef) || {
visibleItems: [],
hiddenItems: [],
itemRefHandler: () => {}
};
const handleOverflowClick = useCallback(event => {
event.stopPropagation();
setOpenPopover(prev => !prev);
}, []);
// Function to render tags
const renderTags = () => /*#__PURE__*/React.createElement("div", {
className: `${prefix}--page-header__tags`,
ref: tagsContainerRef
}, visibleItems.map(tag => /*#__PURE__*/React.createElement(Tag, {
key: tag.id,
ref: node => itemRefHandler(tag.id, node),
type: tag.type,
size: tag.size,
className: `${prefix}--page-header__tag-item`
}, tag.text)), hiddenItems.length > 0 && /*#__PURE__*/React.createElement(Popover, {
open: openPopover,
onRequestClose: () => setOpenPopover(false)
}, /*#__PURE__*/React.createElement(OperationalTag, {
onClick: handleOverflowClick,
"aria-expanded": openPopover,
text: `+${hiddenItems.length}`,
size: tagSize
}), /*#__PURE__*/React.createElement(PopoverContent, {
className: "tag-popover-content"
}, /*#__PURE__*/React.createElement("div", {
className: `${prefix}--page-header__tags-popover-list`
}, hiddenItems.map(tag => /*#__PURE__*/React.createElement(Tag, {
key: tag.id,
type: tag.type,
size: tag.size
}, tag.text))))));
return /*#__PURE__*/React.createElement("div", _extends({
className: classNames,
ref: ref
}, other), /*#__PURE__*/React.createElement(GridAsGridComponent, null, /*#__PURE__*/React.createElement(Column, {
lg: 16,
md: 8,
sm: 4
}, /*#__PURE__*/React.createElement("div", {
className: `${prefix}--page-header__tab-bar--tablist`
}, children, tags.length > 0 && renderTags()))));
});
PageHeaderTabBar.displayName = 'PageHeaderTabBar';
/**
* -------
* Exports
* -------
*/
const Root = PageHeader;
Root.displayName = 'PageHeader.Root';
const BreadcrumbBar = PageHeaderBreadcrumbBar;
BreadcrumbBar.displayName = 'PageHeaderBreadcrumbBar';
const Content = PageHeaderContent;
Content.displayName = 'PageHeaderContent';
const ContentPageActions = PageHeaderContentPageActions;
ContentPageActions.displayName = 'PageHeaderContentPageActions';
const ContentText = PageHeaderContentText;
ContentText.displayName = 'PageHeaderContentText';
const HeroImage = PageHeaderHeroImage;
HeroImage.displayName = 'PageHeaderHeroImage';
const TabBar = PageHeaderTabBar;
TabBar.displayName = 'PageHeaderTabBar';
export { BreadcrumbBar, Content, ContentPageActions, ContentText, HeroImage, PageHeader, PageHeaderBreadcrumbBar, PageHeaderContent, PageHeaderContentPageActions, PageHeaderContentText, PageHeaderHeroImage, PageHeaderTabBar, Root, TabBar };