UNPKG

@carbon/react

Version:

React components for the Carbon Design System

482 lines (461 loc) 15.6 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, { 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 };