@carbon/react
Version:
React components for the Carbon Design System
328 lines (326 loc) • 12.5 kB
JavaScript
/**
* Copyright IBM Corp. 2016, 2026
*
* 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 { usePrefix } from "../../internal/usePrefix.js";
import { Text } from "../Text/Text.js";
import useIsomorphicEffect from "../../internal/useIsomorphicEffect.js";
import { useId } from "../../internal/useId.js";
import AspectRatio from "../AspectRatio/AspectRatio.js";
import { Popover, PopoverContent } from "../Popover/index.js";
import { DefinitionTooltip } from "../Tooltip/DefinitionTooltip.js";
import { MenuItem } from "../Menu/MenuItem.js";
import { GridAsGridComponent } from "../Grid/Grid.js";
import Column from "../Grid/Column.js";
import { MenuButton } from "../MenuButton/index.js";
import { useMatchMedia } from "../../internal/useMatchMedia.js";
import Tag from "../Tag/Tag.js";
import OperationalTag from "../Tag/OperationalTag.js";
import useOverflowItems from "../../internal/useOverflowItems.js";
import classNames from "classnames";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import PropTypes from "prop-types";
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
import { createOverflowHandler } from "@carbon/utilities";
import { breakpoints } from "@carbon/layout";
//#region src/components/PageHeader/PageHeader.tsx
/**
* Copyright IBM Corp. 2025, 2026
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
const PageHeader = React.forwardRef(({ className, children, ...other }, ref) => {
return /* @__PURE__ */ jsx("div", {
className: classNames({ [`${usePrefix()}--page-header`]: true }, className),
ref,
...other,
children
});
});
PageHeader.displayName = "PageHeader";
const PageHeaderBreadcrumbBar = React.forwardRef(({ border = true, className, children, renderIcon: IconElement, contentActions, contentActionsFlush, pageActions, pageActionsFlush, ...other }, ref) => {
const prefix = usePrefix();
const classNames$1 = classNames({
[`${prefix}--page-header__breadcrumb-bar`]: true,
[`${prefix}--page-header__breadcrumb-bar-border`]: border,
[`${prefix}--page-header__breadcrumb__actions-flush`]: pageActionsFlush
}, className);
const contentActionsClasses = classNames({ [`${prefix}--page-header__breadcrumb__content-actions`]: !contentActionsFlush });
return /* @__PURE__ */ jsx("div", {
className: classNames$1,
ref,
...other,
children: /* @__PURE__ */ jsx(GridAsGridComponent, { children: /* @__PURE__ */ jsx(Column, {
lg: 16,
md: 8,
sm: 4,
children: /* @__PURE__ */ jsxs("div", {
className: `${prefix}--page-header__breadcrumb-container`,
children: [/* @__PURE__ */ jsxs("div", {
className: `${prefix}--page-header__breadcrumb-wrapper`,
children: [IconElement && /* @__PURE__ */ jsx("div", {
className: `${prefix}--page-header__breadcrumb__icon`,
children: /* @__PURE__ */ jsx(IconElement, {})
}), children]
}), /* @__PURE__ */ jsxs("div", {
className: `${prefix}--page-header__breadcrumb__actions`,
children: [/* @__PURE__ */ jsx("div", {
className: contentActionsClasses,
children: contentActions
}), pageActions]
})]
})
}) })
});
});
PageHeaderBreadcrumbBar.displayName = "PageHeaderBreadcrumbBar";
const PageHeaderContent = React.forwardRef(({ className, children, title, renderIcon: IconElement, contextualActions, pageActions, ...other }, ref) => {
const prefix = usePrefix();
const classNames$2 = classNames({ [`${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;
};
useIsomorphicEffect(() => {
if (titleRef.current) isEllipsisActive(titleRef.current);
}, [title]);
return /* @__PURE__ */ jsx("div", {
className: classNames$2,
ref,
...other,
children: /* @__PURE__ */ jsx(GridAsGridComponent, { children: /* @__PURE__ */ jsxs(Column, {
lg: 16,
md: 8,
sm: 4,
children: [/* @__PURE__ */ jsxs("div", {
className: `${prefix}--page-header__content__title-wrapper`,
children: [/* @__PURE__ */ jsxs("div", {
className: `${prefix}--page-header__content__start`,
children: [/* @__PURE__ */ jsxs("div", {
className: `${prefix}--page-header__content__title-container`,
children: [IconElement && /* @__PURE__ */ jsx("div", {
className: `${prefix}--page-header__content__icon`,
children: /* @__PURE__ */ jsx(IconElement, {})
}), isEllipsisApplied ? /* @__PURE__ */ jsx(DefinitionTooltip, {
definition: title,
children: /* @__PURE__ */ jsx(Text, {
ref: titleRef,
as: "h4",
className: `${prefix}--page-header__content__title`,
children: title
})
}) : /* @__PURE__ */ jsx(Text, {
ref: titleRef,
as: "h4",
className: `${prefix}--page-header__content__title`,
children: title
})]
}), contextualActions && /* @__PURE__ */ jsx("div", {
className: `${prefix}--page-header__content__contextual-actions`,
children: contextualActions
})]
}), pageActions]
}), children]
}) })
});
});
PageHeaderContent.displayName = "PageHeaderContent";
PageHeaderContent.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
title: PropTypes.string.isRequired,
subtitle: PropTypes.string,
contextualActions: PropTypes.node,
pageActions: PropTypes.node
};
const PageHeaderContentPageActions = ({ className, children, menuButtonLabel = "Actions", actions, ...other }) => {
const classNames$3 = classNames({ [`${usePrefix()}--page-header__content__page-actions`]: true }, className);
const containerRef = useRef(null);
const offsetRef = useRef(null);
const [menuButtonVisibility, setMenuButtonVisibility] = useState(false);
const [hiddenItems, setHiddenItems] = useState([]);
useIsomorphicEffect(() => {
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,
maxVisibleItems: containerRef.current.children.length - 1,
onChange: (visible, hidden) => {
setHiddenItems(actions?.slice(visible.length));
if (hidden.length > 0) setMenuButtonVisibility(true);
}
});
}, []);
return /* @__PURE__ */ jsx("div", {
className: classNames$3,
ref: containerRef,
...other,
children: actions && /* @__PURE__ */ jsx(Fragment, { children: Array.isArray(actions) && /* @__PURE__ */ jsxs(Fragment, { children: [actions.map((action) => /* @__PURE__ */ jsx("div", { children: React.cloneElement(action.body, {
...action.body.props,
onClick: action.onClick
}) }, action.id)), /* @__PURE__ */ jsx("span", {
"data-offset": true,
"data-hidden": true,
ref: offsetRef,
children: /* @__PURE__ */ jsx(MenuButton, {
menuAlignment: "bottom-end",
label: menuButtonLabel,
size: "md",
children: [...hiddenItems].reverse().map((item) => /* @__PURE__ */ jsx(MenuItem, {
onClick: item.onClick,
...item.menuItem
}, item.id))
})
})] }) })
});
};
PageHeaderContentPageActions.displayName = "PageHeaderContentPageActions";
PageHeaderContentPageActions.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
menuButtonLabel: PropTypes.string,
actions: PropTypes.oneOfType([PropTypes.node, PropTypes.array])
};
const PageHeaderContentText = ({ className, children, subtitle, ...other }) => {
const prefix = usePrefix();
return /* @__PURE__ */ jsxs("div", {
className: classNames({ [`${prefix}--page-header__content__body`]: true }, className),
...other,
children: [subtitle && /* @__PURE__ */ jsx(Text, {
as: "h3",
className: `${prefix}--page-header__content__subtitle`,
children: subtitle
}), children]
});
};
PageHeaderContentText.displayName = "PageHeaderContentText";
PageHeaderContentText.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
subtitle: PropTypes.string
};
const PageHeaderHeroImage = ({ className, children, ...other }) => {
const classNames$4 = classNames({ [`${usePrefix()}--page-header__hero-image`]: true }, className);
const isLg = useMatchMedia(`(min-width: ${breakpoints.lg.width})`);
return /* @__PURE__ */ jsx(AspectRatio, {
className: classNames$4,
...other,
ratio: isLg ? "2x1" : "3x2",
children
});
};
PageHeaderHeroImage.displayName = "PageHeaderHeroImage";
PageHeaderHeroImage.propTypes = {
children: PropTypes.node,
className: PropTypes.string
};
const PageHeaderTabBar = React.forwardRef(({ className, children, tags = [], ...other }, ref) => {
const prefix = usePrefix();
const classNames$5 = classNames({ [`${prefix}--page-header__tab-bar`]: true }, className);
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}`
}));
}, [instanceId, tags]);
const tagsContainerRef = useRef(null);
const offsetRef = useRef(null);
useEffect(() => {
const handleResize = () => {
setOpenPopover(false);
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
const { visibleItems = [], hiddenItems = [], itemRefHandler = () => {} } = useOverflowItems(tagsWithIds, tagsContainerRef, offsetRef) || {
visibleItems: [],
hiddenItems: [],
itemRefHandler: () => {}
};
const handleOverflowClick = useCallback((event) => {
event.stopPropagation();
setOpenPopover((prev) => !prev);
}, []);
const renderTags = () => /* @__PURE__ */ jsxs("div", {
className: `${prefix}--page-header__tags`,
ref: tagsContainerRef,
children: [visibleItems.map((tag) => /* @__PURE__ */ jsx(Tag, {
ref: (node) => itemRefHandler(tag.id, node),
type: tag.type,
size: tag.size,
className: `${prefix}--page-header__tag-item`,
children: tag.text
}, tag.id)), hiddenItems.length > 0 && /* @__PURE__ */ jsxs(Popover, {
open: openPopover,
onRequestClose: () => setOpenPopover(false),
children: [/* @__PURE__ */ jsx(OperationalTag, {
onClick: handleOverflowClick,
"aria-expanded": openPopover,
text: `+${hiddenItems.length}`,
size: tagSize
}), /* @__PURE__ */ jsx(PopoverContent, {
className: "tag-popover-content",
children: /* @__PURE__ */ jsx("div", {
className: `${prefix}--page-header__tags-popover-list`,
children: hiddenItems.map((tag) => /* @__PURE__ */ jsx(Tag, {
type: tag.type,
size: tag.size,
children: tag.text
}, tag.id))
})
})]
})]
});
return /* @__PURE__ */ jsx("div", {
className: classNames$5,
ref,
...other,
children: /* @__PURE__ */ jsx(GridAsGridComponent, { children: /* @__PURE__ */ jsx(Column, {
lg: 16,
md: 8,
sm: 4,
children: /* @__PURE__ */ jsxs("div", {
className: `${prefix}--page-header__tab-bar--tablist`,
children: [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";
//#endregion
export { BreadcrumbBar, Content, ContentPageActions, ContentText, HeroImage, PageHeader, PageHeaderBreadcrumbBar, PageHeaderContent, PageHeaderContentPageActions, PageHeaderContentText, PageHeaderHeroImage, PageHeaderTabBar, Root, TabBar };