UNPKG

@carbon/ibm-products

Version:
221 lines (219 loc) 10 kB
/** * Copyright IBM Corp. 2020, 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 { __toESM } from "../../_virtual/_rolldown/runtime.js"; import { require_classnames } from "../../node_modules/classnames/index.js"; import { pkg } from "../../settings.js"; import "../../global/js/utils/props-helper.js"; import uuidv4 from "../../global/js/utils/uuidv4.js"; import { useResizeObserver } from "../../global/js/hooks/useResizeObserver.js"; import { TooltipTrigger } from "../TooltipTrigger/TooltipTrigger.js"; import React, { useEffect, useRef, useState } from "react"; import PropTypes from "prop-types"; import { Breadcrumb, BreadcrumbItem, Link, MenuItem, OverflowMenu, Tooltip, unstable_FeatureFlags, usePrefix } from "@carbon/react"; import { ArrowLeft, OverflowMenuHorizontal } from "@carbon/react/icons"; //#region src/components/BreadcrumbWithOverflow/BreadcrumbWithOverflow.jsx var import_classnames = /* @__PURE__ */ __toESM(require_classnames()); const blockClass = `${pkg.prefix}--breadcrumb-with-overflow`; const componentName = "BreadcrumbWithOverflow"; /** * The BreadcrumbWithOverflow is used internally by the PageHeader to wrap BreadcrumbItems. */ let BreadcrumbWithOverflow = ({ breadcrumbs, className, label, maxVisible, noTrailingSlash, overflowAriaLabel, overflowTooltipAlign, ...other }) => { const carbonPrefix = usePrefix(); const [displayCount, setDisplayCount] = useState(3); const [displayedBreadcrumbItems, setDisplayedBreadcrumbItems] = useState([]); const breadcrumbItemWithOverflow = useRef(null); const sizingContainerRef = useRef(null); const internalId = useRef(uuidv4()); const [hiddenSizingItems, setHiddenSizingItems] = useState([]); const BreadcrumbOverflowMenu = ({ overflowItems }) => { const handleClick = (evt, item) => { if (item?.props?.href) window.location.href = item.props.href; item?.props?.onClick?.(evt); }; return /* @__PURE__ */ React.createElement(BreadcrumbItem, { key: `breadcrumb-overflow-${internalId.current}` }, /* @__PURE__ */ React.createElement(unstable_FeatureFlags, { enableV12Overflowmenu: true }, /* @__PURE__ */ React.createElement(OverflowMenu, { role: "navigation", "aria-label": overflowAriaLabel, label: overflowAriaLabel, renderIcon: (props) => /* @__PURE__ */ React.createElement(OverflowMenuHorizontal, { size: 32, ...props }), className: `${blockClass}__overflow-menu`, tooltipAlignment: overflowTooltipAlign }, overflowItems.map((item, index) => /* @__PURE__ */ React.createElement(MenuItem, { key: `breadcrumb-overflow-menu-item-${internalId.current}-${index}`, onClick: (evt) => handleClick(evt, item), label: item.props.children }))))); }; useEffect(() => { setHiddenSizingItems(/* @__PURE__ */ React.createElement("div", { className: `${blockClass}__breadcrumb-container ${blockClass}__breadcrumb-container--hidden`, "aria-hidden": true, ref: sizingContainerRef }, /* @__PURE__ */ React.createElement(Breadcrumb, { "aria-label": `${label}-hidden` }, /* @__PURE__ */ React.createElement(BreadcrumbItem, { key: `${blockClass}-hidden-overflow-${internalId}` }, /* @__PURE__ */ React.createElement(OverflowMenu, { role: "navigation", "aria-label": overflowAriaLabel, renderIcon: (props) => /* @__PURE__ */ React.createElement(OverflowMenuHorizontal, { size: 32, ...props }) })), breadcrumbs.map(({ label, key, title, id, shortTitle, ...rest }) => /* @__PURE__ */ React.createElement(BreadcrumbItem, { key, ...rest, "data-original-id": id, title: title ?? label }, label))))); }, [ breadcrumbs, label, overflowAriaLabel ]); useEffect(() => { /* istanbul ignore if */ if (breadcrumbs.length === 0) { setDisplayedBreadcrumbItems([]); return; } const newDisplayedBreadcrumbItems = breadcrumbs.map(({ className, key, label, shortTitle, title, ...rest }, index) => /* @__PURE__ */ React.createElement(BreadcrumbItem, { key, className: index > 0 || displayCount > 1 ? (0, import_classnames.default)([className, `${blockClass}__displayed-breadcrumb`]) : className, title: index + 1 === breadcrumbs.length ? title : null, ...rest }, shortTitle || label)); const overflowPosition = displayCount > 1 ? 1 : 0; let newOverflowBreadcrumbItems = newDisplayedBreadcrumbItems.splice(overflowPosition, breadcrumbs.length - displayCount); if (newOverflowBreadcrumbItems.length) newDisplayedBreadcrumbItems.splice(overflowPosition, 0, /* @__PURE__ */ React.createElement(BreadcrumbOverflowMenu, { overflowItems: newOverflowBreadcrumbItems, key: `displayed-breadcrumb-${internalId}-overflow` })); setDisplayedBreadcrumbItems(newDisplayedBreadcrumbItems); }, [breadcrumbs, displayCount]); const checkFullyVisibleBreadcrumbItems = () => { if (!breadcrumbItemWithOverflow.current) return; const displayItemIndex = (itemCount, index) => { if (index === 0) return itemCount - 1; else if (index === 1) return 1; else return itemCount - index; }; if (maxVisible <= 1) setDisplayCount(1); else { let willFit = 0; let spaceAvailable = breadcrumbItemWithOverflow.current.offsetWidth; /* istanbul ignore next */ if (sizingContainerRef.current) { const sizingBreadcrumbItems = sizingContainerRef.current.querySelectorAll(`.${carbonPrefix}--breadcrumb-item`); const breadcrumbWidthsIncludingMargin = []; for (let item of sizingBreadcrumbItems) { const computedStyle = window ? window.getComputedStyle(sizingBreadcrumbItems[0]) : null; const marginWidths = computedStyle ? parseFloat(computedStyle.marginLeft || 0, 10) + parseFloat(computedStyle.marginRight || 0, 10) : 0; breadcrumbWidthsIncludingMargin.push(item.offsetWidth + marginWidths); } let overflowWidth = breadcrumbWidthsIncludingMargin[0]; for (let i = 0; i < breadcrumbWidthsIncludingMargin.length - 1; i++) { const index = displayItemIndex(breadcrumbWidthsIncludingMargin.length, i); if (spaceAvailable >= breadcrumbWidthsIncludingMargin[index]) { spaceAvailable -= breadcrumbWidthsIncludingMargin[index]; willFit += 1; } else break; } if (willFit < breadcrumbWidthsIncludingMargin.length - 1) { if (spaceAvailable < overflowWidth) while (willFit > 0 && spaceAvailable < overflowWidth) { willFit -= 1; const itemToRemove = displayItemIndex(breadcrumbWidthsIncludingMargin.length, willFit); spaceAvailable += breadcrumbWidthsIncludingMargin[itemToRemove]; } } } if (willFit <= 1) setDisplayCount(1); else setDisplayCount(maxVisible ? Math.min(willFit, maxVisible) : willFit); } }; useEffect(() => { checkFullyVisibleBreadcrumbItems(); }, [hiddenSizingItems, maxVisible]); /* istanbul ignore next */ const handleResize = () => { /* istanbul ignore next */ checkFullyVisibleBreadcrumbItems(); }; let backItem = breadcrumbs[breadcrumbs.length - 1]; /* istanbul ignore if */ if (backItem.isCurrentPage) backItem = breadcrumbs[breadcrumbs.length - 2]; useResizeObserver(sizingContainerRef, handleResize); useResizeObserver(breadcrumbItemWithOverflow, handleResize); return /* @__PURE__ */ React.createElement("div", { className: (0, import_classnames.default)(blockClass, className, { [`${blockClass}__with-items`]: displayedBreadcrumbItems.length > 1 }), ref: breadcrumbItemWithOverflow }, /* @__PURE__ */ React.createElement("div", { className: (0, import_classnames.default)([`${blockClass}__space`]) }, hiddenSizingItems, /* @__PURE__ */ React.createElement(Breadcrumb, { "aria-label": label, className: (0, import_classnames.default)(`${blockClass}__breadcrumb-container`, { [`${blockClass}__breadcrumb-container-with-items`]: displayedBreadcrumbItems.length > 1 }), noTrailingSlash, ...other }, backItem?.href && (backItem?.label || backItem?.title) && /* @__PURE__ */ React.createElement(BreadcrumbItem, { className: (0, import_classnames.default)(`${blockClass}__breadcrumb-back`) }, /* @__PURE__ */ React.createElement(Link, { href: backItem.href, renderIcon: () => /* @__PURE__ */ React.createElement(Tooltip, { align: "right", label: backItem.title || backItem.label, className: `${blockClass}__back__button ${carbonPrefix}--icon-tooltip` }, /* @__PURE__ */ React.createElement(TooltipTrigger, null, /* @__PURE__ */ React.createElement(ArrowLeft, { size: 16 }))) })), displayedBreadcrumbItems))); }; BreadcrumbWithOverflow = pkg.checkComponentEnabled(BreadcrumbWithOverflow, componentName); BreadcrumbWithOverflow.propTypes = { breadcrumbs: PropTypes.arrayOf(PropTypes.shape({ /** * Optional string representing the link location for the BreadcrumbItem */ href: PropTypes.string, /** * Provide if this breadcrumb item represents the current page */ isCurrentPage: PropTypes.bool, /** * Key required to render array efficiently */ key: PropTypes.string.isRequired, /** * Pass in content that will be inside of the BreadcrumbItem */ label: PropTypes.node, /** * An optional title label for extra long breadcrumb */ shortTitle: PropTypes.string, /** * A string based alternative to the children, required only if children is not of type string */ title: PropTypes.string })), /** * className */ className: PropTypes.string, /** * Label for the Breadcrumb component */ label: PropTypes.string, /** * maxVisible: maximum visible breadcrumb-items before overflow is used (values less than 1 are treated as 1) */ maxVisible: PropTypes.number, /** * noTrailing slash - same as for Carbon */ noTrailingSlash: PropTypes.bool, /** * overflowAriaLabel label for open close button overflow used for breadcrumb items that do not fit. */ overflowAriaLabel: PropTypes.string, /** * overflowTooltipAlign: align tooltip position */ overflowTooltipAlign: Tooltip.propTypes.align }; BreadcrumbWithOverflow.displayName = componentName; //#endregion export { BreadcrumbWithOverflow };