UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

451 lines (443 loc) 14.7 kB
import { c } from 'react-compiler-runtime'; import { clsx } from 'clsx'; import React, { useRef, useCallback, useState, useMemo, useEffect } from 'react'; import classes from './Breadcrumbs.module.css.js'; import { ActionList } from '../ActionList/index.js'; import { IconButton } from '../Button/IconButton.js'; import { KebabHorizontalIcon } from '@primer/octicons-react'; import { useResizeObserver } from '../hooks/useResizeObserver.js'; import { useOnEscapePress } from '../hooks/useOnEscapePress.js'; import { useOnOutsideClick } from '../hooks/useOnOutsideClick.js'; import { fixedForwardRef } from '../utils/modern-polymorphic.js'; import { jsx, jsxs } from 'react/jsx-runtime'; import Details from '../Details/Details.js'; const BreadcrumbsList = t0 => { const $ = c(2); const { children } = t0; let t1; if ($[0] !== children) { t1 = /*#__PURE__*/jsx("ol", { className: classes.BreadcrumbsList, children: children }); $[0] = children; $[1] = t1; } else { t1 = $[1]; } return t1; }; const BreadcrumbsMenuItem = /*#__PURE__*/React.forwardRef((t0, menuRefCallback) => { const $ = c(25); let ariaLabel; let items; let rest; if ($[0] !== t0) { ({ items, "aria-label": ariaLabel, ...rest } = t0); $[0] = t0; $[1] = ariaLabel; $[2] = items; $[3] = rest; } else { ariaLabel = $[1]; items = $[2]; rest = $[3]; } const [isOpen, setIsOpen] = useState(false); const detailsRef = useRef(null); const menuButtonRef = useRef(null); const menuContainerRef = useRef(null); let t1; if ($[4] !== menuRefCallback) { t1 = element => { detailsRef.current = element; if (typeof menuRefCallback === "function") { menuRefCallback(element); } }; $[4] = menuRefCallback; $[5] = t1; } else { t1 = $[5]; } const detailsRefCallback = t1; let t2; if ($[6] === Symbol.for("react.memo_cache_sentinel")) { t2 = event => { event.preventDefault(); if (detailsRef.current) { const newOpenState = !detailsRef.current.open; detailsRef.current.open = newOpenState; setIsOpen(newOpenState); } }; $[6] = t2; } else { t2 = $[6]; } const handleSummaryClick = t2; let t3; if ($[7] === Symbol.for("react.memo_cache_sentinel")) { t3 = () => { if (detailsRef.current) { detailsRef.current.open = false; setIsOpen(false); } }; $[7] = t3; } else { t3 = $[7]; } const closeOverlay = t3; let t4; if ($[8] === Symbol.for("react.memo_cache_sentinel")) { t4 = () => { var _menuButtonRef$curren; (_menuButtonRef$curren = menuButtonRef.current) === null || _menuButtonRef$curren === void 0 ? void 0 : _menuButtonRef$curren.focus(); }; $[8] = t4; } else { t4 = $[8]; } const focusOnMenuButton = t4; let t5; let t6; if ($[9] !== isOpen) { t5 = event_0 => { if (isOpen) { event_0.preventDefault(); closeOverlay(); focusOnMenuButton(); } }; t6 = [isOpen]; $[9] = isOpen; $[10] = t5; $[11] = t6; } else { t5 = $[10]; t6 = $[11]; } useOnEscapePress(t5, t6); let t7; if ($[12] === Symbol.for("react.memo_cache_sentinel")) { t7 = { onClickOutside: closeOverlay, containerRef: menuContainerRef, ignoreClickRefs: [menuButtonRef] }; $[12] = t7; } else { t7 = $[12]; } useOnOutsideClick(t7); const t8 = ariaLabel || `${items.length} more breadcrumb items`; const t9 = isOpen ? "true" : "false"; let t10; if ($[13] !== rest || $[14] !== t8 || $[15] !== t9) { t10 = /*#__PURE__*/jsx(IconButton, { as: "summary", role: "button", ref: menuButtonRef, "aria-label": t8, "aria-expanded": t9, onClick: handleSummaryClick, variant: "invisible", size: "small", icon: KebabHorizontalIcon, tooltipDirection: "e", ...rest }); $[13] = rest; $[14] = t8; $[15] = t9; $[16] = t10; } else { t10 = $[16]; } let t11; if ($[17] !== items) { t11 = items.map(_temp); $[17] = items; $[18] = t11; } else { t11 = $[18]; } let t12; if ($[19] !== t11) { t12 = /*#__PURE__*/jsx("div", { ref: menuContainerRef, className: classes.MenuOverlay, children: /*#__PURE__*/jsx(ActionList, { children: t11 }) }); $[19] = t11; $[20] = t12; } else { t12 = $[20]; } let t13; if ($[21] !== detailsRefCallback || $[22] !== t10 || $[23] !== t12) { t13 = /*#__PURE__*/jsxs(Details, { ref: detailsRefCallback, className: classes.MenuDetails, "data-component": "Breadcrumbs.MenuItem", children: [t10, t12] }); $[21] = detailsRefCallback; $[22] = t10; $[23] = t12; $[24] = t13; } else { t13 = $[24]; } return t13; }); BreadcrumbsMenuItem.displayName = 'Breadcrumbs.MenuItem'; const getValidChildren = children => { // eslint-disable-next-line @typescript-eslint/no-explicit-any return React.Children.toArray(children).filter(child => /*#__PURE__*/React.isValidElement(child)); }; function Breadcrumbs({ className, children, style, overflow = 'wrap', variant = 'normal' }) { const containerRef = useRef(null); const measureMenuButton = useCallback(element => { if (element) { const iconButtonElement = element.querySelector('button[data-component="IconButton"]'); if (iconButtonElement) { const measuredWidth = iconButtonElement.offsetWidth; // eslint-disable-next-line react-hooks/immutability setMenuButtonWidth(measuredWidth); } } }, []); const hideRoot = !(overflow === 'menu-with-root'); const [effectiveHideRoot, setEffectiveHideRoot] = useState(hideRoot); const childArray = useMemo(() => getValidChildren(children), [children]); const rootItem = childArray[0]; // eslint-disable-next-line @typescript-eslint/no-explicit-any const [visibleItems, setVisibleItems] = useState(() => childArray); const [childArrayWidths, setChildArrayWidths] = useState(() => []); // eslint-disable-next-line @typescript-eslint/no-explicit-any const [menuItems, setMenuItems] = useState([]); const [rootItemWidth, setRootItemWidth] = useState(0); const MENU_BUTTON_FALLBACK_WIDTH = 32; // Design system small IconButton const [menuButtonWidth, setMenuButtonWidth] = useState(MENU_BUTTON_FALLBACK_WIDTH); useEffect(() => { var _containerRef$current; const listElement = (_containerRef$current = containerRef.current) === null || _containerRef$current === void 0 ? void 0 : _containerRef$current.querySelector('ol'); if (listElement && listElement.children.length > 0 && listElement.children.length === childArray.length) { const listElementArray = Array.from(listElement.children); const widths = listElementArray.map(child => child.offsetWidth); setChildArrayWidths(widths); setRootItemWidth(listElementArray[0].offsetWidth); } }, [childArray]); const calculateOverflow = useCallback(availableWidth => { let eHideRoot = effectiveHideRoot; const MENU_BUTTON_WIDTH = menuButtonWidth; const NARROW_BREAKPOINT = 544; const isNarrow = availableWidth < NARROW_BREAKPOINT; let MIN_VISIBLE_ITEMS = 4; if (!eHideRoot) { MIN_VISIBLE_ITEMS = 3; } else if (isNarrow && childArray.length > 2) { MIN_VISIBLE_ITEMS = 1; } const calculateVisibleItemsWidth = w => { const widths_0 = w.reduce((sum, width) => sum + width + 16, 0); return !eHideRoot ? rootItemWidth + widths_0 : widths_0; }; let currentVisibleItems = [...childArray]; let currentVisibleItemWidths = [...childArrayWidths]; // eslint-disable-next-line @typescript-eslint/no-explicit-any let currentMenuItems = []; let currentMenuItemsWidths = []; if (availableWidth > 0 && currentVisibleItemWidths.length > 0) { let visibleItemsWidthTotal = calculateVisibleItemsWidth(currentVisibleItemWidths); if (currentMenuItems.length > 0) { visibleItemsWidthTotal += MENU_BUTTON_WIDTH; } while ((overflow === 'menu' || overflow === 'menu-with-root') && (visibleItemsWidthTotal > availableWidth || currentVisibleItems.length > MIN_VISIBLE_ITEMS)) { const itemToHide = currentVisibleItems[0]; const itemToHideWidth = currentVisibleItemWidths[0]; currentMenuItems = [...currentMenuItems, itemToHide]; currentMenuItemsWidths = [...currentMenuItemsWidths, itemToHideWidth]; currentVisibleItems = currentVisibleItems.slice(1); currentVisibleItemWidths = currentVisibleItemWidths.slice(1); visibleItemsWidthTotal = calculateVisibleItemsWidth(currentVisibleItemWidths); if (currentMenuItems.length > 0) { visibleItemsWidthTotal += MENU_BUTTON_WIDTH; } if (currentVisibleItems.length === 1 && visibleItemsWidthTotal > availableWidth) { eHideRoot = true; break; } else { eHideRoot = hideRoot; } } } return { visibleItems: currentVisibleItems, menuItems: currentMenuItems, effectiveHideRoot: eHideRoot }; }, [childArray, childArrayWidths, effectiveHideRoot, hideRoot, overflow, rootItemWidth, menuButtonWidth]); const handleResize = useCallback(entries => { if (entries[0]) { const containerWidth = entries[0].contentRect.width; const result = calculateOverflow(containerWidth); if (visibleItems.length !== result.visibleItems.length && menuItems.length !== result.menuItems.length || result.effectiveHideRoot !== effectiveHideRoot) { setVisibleItems(result.visibleItems); setMenuItems(result.menuItems); setEffectiveHideRoot(result.effectiveHideRoot); } } }, [calculateOverflow, effectiveHideRoot, menuItems.length, visibleItems.length]); useResizeObserver(handleResize, containerRef); useEffect(() => { if ((overflow === 'menu' || overflow === 'menu-with-root') && childArray.length > 5 && menuItems.length === 0) { var _containerRef$current2; const containerWidth_0 = ((_containerRef$current2 = containerRef.current) === null || _containerRef$current2 === void 0 ? void 0 : _containerRef$current2.offsetWidth) || 800; const result_0 = calculateOverflow(containerWidth_0); setVisibleItems(result_0.visibleItems); setMenuItems(result_0.menuItems); setEffectiveHideRoot(result_0.effectiveHideRoot); } }, [overflow, childArray, calculateOverflow, menuItems.length]); const finalChildren = React.useMemo(() => { if (overflow === 'wrap' || menuItems.length === 0) { return React.Children.map(children, child_0 => /*#__PURE__*/jsx("li", { className: classes.ItemWrapper, children: child_0 })); } let effectiveMenuItems = [...menuItems]; // In 'menu-with-root' mode, include the root item inside the menu even if it's visible in the breadcrumbs if (!effectiveHideRoot) { effectiveMenuItems = [...menuItems.slice(1)]; } const menuElement = /*#__PURE__*/jsxs("li", { className: classes.BreadcrumbsItem, children: [/*#__PURE__*/jsx(BreadcrumbsMenuItem, { ref: measureMenuButton, items: effectiveMenuItems, "aria-label": `${effectiveMenuItems.length} more breadcrumb items` }), /*#__PURE__*/jsx(ItemSeparator, {})] }, "breadcrumbs-menu"); const visibleElements = visibleItems.map((child_1, index) => /*#__PURE__*/jsxs("li", { className: classes.BreadcrumbsItem, children: [child_1, /*#__PURE__*/jsx(ItemSeparator, {})] }, `visible + ${index}`)); const rootElement = /*#__PURE__*/jsxs("li", { className: classes.BreadcrumbsItem, children: [rootItem, /*#__PURE__*/jsx(ItemSeparator, {})] }, `rootElement`); if (effectiveHideRoot) { // Show: [overflow menu, leaf breadcrumb] return [menuElement, ...visibleElements]; } else { // Show: [root breadcrumb, overflow menu, leaf breadcrumb] return [rootElement, menuElement, ...visibleElements]; } }, [overflow, menuItems, effectiveHideRoot, measureMenuButton, visibleItems, rootItem, children]); return /*#__PURE__*/jsx("nav", { className: clsx(className, classes.BreadcrumbsBase), "aria-label": "Breadcrumbs", style: style, ref: containerRef, "data-overflow": overflow, "data-variant": variant, "data-component": "Breadcrumbs", children: /*#__PURE__*/jsx(BreadcrumbsList, { children: finalChildren }) }); } Breadcrumbs.displayName = "Breadcrumbs"; const ItemSeparator = () => { const $ = c(1); let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { t0 = /*#__PURE__*/jsx("span", { className: classes.ItemSeparator, children: /*#__PURE__*/jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", children: /*#__PURE__*/jsx("path", { d: "M10.956 1.27994L6.06418 14.7201L5 14.7201L9.89181 1.27994L10.956 1.27994Z", fill: "currentcolor" }) }) }); $[0] = t0; } else { t0 = $[0]; } return t0; }; const BreadcrumbsItem = fixedForwardRef((props, ref) => { const { as: Component = 'a', selected, className, ...rest } = props; return /*#__PURE__*/jsx(Component, { className: clsx(className, classes.Item, selected && 'selected'), "aria-current": selected ? 'page' : undefined, ref: ref, "data-component": "Breadcrumbs.Item", ...rest }); }); Breadcrumbs.displayName = 'Breadcrumbs'; const BreadcrumbsItemWithDisplayName = Object.assign(BreadcrumbsItem, { displayName: 'Breadcrumbs.Item' }); var Breadcrumbs_default = Object.assign(Breadcrumbs, { Item: BreadcrumbsItemWithDisplayName }); /** * @deprecated Use the `Breadcrumbs` component instead (i.e. `<Breadcrumb>` → `<Breadcrumbs>`) */ const Breadcrumb = Object.assign(Breadcrumbs, { Item: BreadcrumbsItem }); /** * @deprecated Use the `BreadcrumbsProps` type instead */ /** * @deprecated Use the `BreadcrumbsItemProps` type instead */ function _temp(item, index) { const { children, selected, as: Component, ...itemProps } = item.props; return /*#__PURE__*/jsx(ActionList.LinkItem, { as: Component, ...itemProps, "aria-current": selected ? "page" : undefined, children: children }, index); } _temp.displayName = "_temp"; export { Breadcrumb, Breadcrumbs_default as default };