UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

495 lines (485 loc) 15.9 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 { jsx, jsxs } from 'react/jsx-runtime'; import { useFeatureFlag } from '../FeatureFlags/useFeatureFlag.js'; 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, 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 overflowMenuEnabled = useFeatureFlag('primer_react_breadcrumbs_overflow_menu'); const wrappedChildren = React.Children.map(children, child => /*#__PURE__*/jsx("li", { className: classes.ItemWrapper, children: child })); 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 (overflowMenuEnabled && listElement && listElement.children.length > 0 && listElement.children.length === childArray.length) { const listElementArray = Array.from(listElement.children); const widths = listElementArray.map(child_0 => child_0.offsetWidth); setChildArrayWidths(widths); setRootItemWidth(listElementArray[0].offsetWidth); } }, [childArray, overflowMenuEnabled]); const calculateOverflow = useCallback(availableWidth => { let eHideRoot = effectiveHideRoot; const MENU_BUTTON_WIDTH = menuButtonWidth; const MIN_VISIBLE_ITEMS = !eHideRoot ? 3 : 4; 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 (overflowMenuEnabled && 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, overflowMenuEnabled, visibleItems.length]); useResizeObserver(handleResize, containerRef); useEffect(() => { if (overflowMenuEnabled && (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, overflowMenuEnabled]); const finalChildren = React.useMemo(() => { if (overflowMenuEnabled) { if (overflow === 'wrap' || menuItems.length === 0) { return React.Children.map(children, child_1 => /*#__PURE__*/jsx("li", { className: classes.ItemWrapper, children: child_1 })); } 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_2, index) => /*#__PURE__*/jsxs("li", { className: classes.BreadcrumbsItem, children: [child_2, /*#__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]; } } }, [overflowMenuEnabled, overflow, menuItems, effectiveHideRoot, measureMenuButton, visibleItems, rootItem, children]); return overflowMenuEnabled ? /*#__PURE__*/jsx("nav", { className: clsx(className, classes.BreadcrumbsBase), "aria-label": "Breadcrumbs", style: style, ref: containerRef, "data-overflow": overflow, "data-variant": variant, children: /*#__PURE__*/jsx(BreadcrumbsList, { children: finalChildren }) }) : /*#__PURE__*/jsx("nav", { className: clsx(className, classes.BreadcrumbsBase), "aria-label": "Breadcrumbs", style: style, "data-variant": variant, children: /*#__PURE__*/jsx(BreadcrumbsList, { children: wrappedChildren }) }); } 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; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any function BreadcrumbsItemComponent(props, ref) { const $ = c(14); let className; let rest; let selected; let t0; if ($[0] !== props) { ({ as: t0, selected, className, ...rest } = props); $[0] = props; $[1] = className; $[2] = rest; $[3] = selected; $[4] = t0; } else { className = $[1]; rest = $[2]; selected = $[3]; t0 = $[4]; } const Component = t0 === undefined ? "a" : t0; const t1 = selected && "selected"; let t2; if ($[5] !== className || $[6] !== t1) { t2 = clsx(className, classes.Item, t1); $[5] = className; $[6] = t1; $[7] = t2; } else { t2 = $[7]; } const t3 = selected ? "page" : undefined; let t4; if ($[8] !== Component || $[9] !== ref || $[10] !== rest || $[11] !== t2 || $[12] !== t3) { t4 = /*#__PURE__*/jsx(Component, { className: t2, "aria-current": t3, ref: ref, ...rest }); $[8] = Component; $[9] = ref; $[10] = rest; $[11] = t2; $[12] = t3; $[13] = t4; } else { t4 = $[13]; } return t4; } BreadcrumbsItemComponent.displayName = 'Breadcrumbs.Item'; const BreadcrumbsItem = /*#__PURE__*/React.forwardRef(BreadcrumbsItemComponent); Breadcrumbs.displayName = 'Breadcrumbs'; var Breadcrumbs_default = Object.assign(Breadcrumbs, { Item: BreadcrumbsItem }); /** * @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 href = item.props.href; const children = item.props.children; const selected = item.props.selected; return /*#__PURE__*/jsx(ActionList.LinkItem, { href: href, "aria-current": selected ? "page" : undefined, className: classes.MenuItem, children: children }, index); } _temp.displayName = "_temp"; export { Breadcrumb, Breadcrumbs_default as default };