@primer/react
Version:
An implementation of GitHub's Primer Design System using React
495 lines (485 loc) • 15.9 kB
JavaScript
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 };