azure-devops-ui
Version:
React components for building web UI in Azure DevOps
130 lines (129 loc) • 7.66 kB
JavaScript
import "../../CommonImports";
import "../../Core/core.css";
import "./Breadcrumb.css";
import * as React from "react";
import { Icon } from '../../Icon';
import { Link } from '../../Link';
import { MenuCell } from '../../Menu';
import { OverflowButton, ResizeGroup } from '../../ResizeGroup';
import * as Resources from '../../Resources.Breadcrumb';
import { Tooltip } from '../../TooltipEx';
import { css, getSafeId } from '../../Util';
let breadcrumbCount = 0;
export class Breadcrumb extends React.Component {
constructor(props) {
super(props);
this.renderBreadcrumbItems = () => {
const { excludeFocusZone, excludeTabStop, extraContent, role } = this.props;
const { displayedItems, hiddenItemCount } = this.state;
const ariaLabel = this.props.ariaLabel || Resources.BreadcrumbItemAriaLabel;
// Find index of last rendered item so the divider icon
// knows not to render on that item
const lastItemIndex = displayedItems.length - 1;
const itemElements = displayedItems.map((item, index) => (React.createElement("div", { className: "bolt-breadcrumb-list-item", key: item.key || String(index) },
this.renderItem(item),
(index !== lastItemIndex || extraContent) && this.renderDivider(item.key))));
itemElements.splice(0, 0, React.createElement("div", { className: css("bolt-breadcrumb-overflow", hiddenItemCount > 0 && "bolt-breadcrumb-overflow-visible"), key: "overflowItem" },
React.createElement(OverflowButton, { excludeTabStop: excludeTabStop, excludeFocusZone: excludeFocusZone }),
hiddenItemCount > 0 && hiddenItemCount < displayedItems.length && this.renderDivider("overflow")));
if (extraContent) {
itemElements.push(React.createElement("div", { className: "bolt-breadcrumb-list-item", id: getSafeId("breadcrumb-extra-content"), key: "breadcrumb-extra-content" }, extraContent));
}
return (React.createElement("div", { className: css("bolt-breadcrumb flex-row flex-grow", itemElements.length > 0 && "bolt-breadcrumb-with-items"), role: role, "aria-label": ariaLabel }, ...itemElements));
};
this.renderDivider = (key) => {
return (React.createElement("div", { key: "BID$" + key, className: "bolt-breadcrumb-divider secondary-text cursor-default flex-noshrink" }, "/"));
};
this.renderItem = (item) => {
const internalItem = (React.createElement(React.Fragment, null,
item.iconProps && (React.createElement(Icon, Object.assign({}, item.iconProps, { className: css("bolt-breadcrumb-item-icon", !item.text && "icon-only", item.iconProps.className) }))),
item.text && (React.createElement(Tooltip, { overflowOnly: true },
React.createElement("span", { className: "bolt-breadcrumb-item-text text-ellipsis" }, item.text)))));
const ariaDescribedById = "bolt-breadcrumb" + this.breadcrumbId + "-item-described-by" + item.key;
if (item.onClick || item.href) {
return (React.createElement("div", { className: "bolt-breadcrumb-item" },
React.createElement(Link, { ariaLabel: item.ariaLabel, ariaDescribedBy: ariaDescribedById, key: "BreadcrumbItem$" + item.key, onClick: item.onClick, href: item.href, excludeFocusZone: this.props.excludeFocusZone, excludeTabStop: this.props.excludeTabStop, removeUnderline: true },
React.createElement("div", { className: "bolt-breadcrumb-item-text-container" }, internalItem),
React.createElement("div", { className: "bolt-breadcrumb-hidden-element", id: getSafeId(ariaDescribedById) }, item.ariaDescription))));
}
else {
return (React.createElement("div", { className: "bolt-breadcrumb-item", "aria-label": item.ariaLabel }, internalItem));
}
};
this.onLayoutChanged = (hiddenCount) => {
this.setState({ hiddenItemCount: hiddenCount });
};
this.onMenuItemActivate = (menuItem, event) => {
const breadcrumbItem = menuItem.data;
if (breadcrumbItem.onClick) {
breadcrumbItem.onClick(event, breadcrumbItem);
}
};
this.breadcrumbId = breadcrumbCount++;
this.state = Breadcrumb.getDerivedStateFromProps(props, this.state);
}
static getDerivedStateFromProps(props, state) {
const displayedItems = arrangeItems(props.items);
return Object.assign(Object.assign({}, state), { displayedItems, linkItems: getLinkItems(displayedItems) });
}
render() {
const items = this.state.displayedItems;
const overflowMenuItems = items.map((item, index) => (Object.assign(Object.assign({ data: item, iconProps: item.iconProps, text: item.text, id: item.key, href: item.href }, item.menuItemProps), { onActivate: this.onMenuItemActivate })));
if (this.props.extraContent) {
overflowMenuItems.push({
id: "overflow-item",
className: css(this.props.extraContentClassName || ""),
onActivate: () => {
return true;
},
renderMenuCell: (menuCell, menuItem, details) => {
if (menuCell === MenuCell.PrimaryText) {
return this.props.extraContent;
}
}
});
}
const responsiveChildren = overflowMenuItems.map((items, index) => index + 1);
const responsiveLayoutProps = { responsiveChildren, onLayoutChange: this.onLayoutChanged };
return (React.createElement("div", { className: css("bolt-breadcrumb-container", items.length > 0 && "bolt-breadcrumb-with-items", this.props.className), role: "navigation", "aria-label": Resources.BreadcrumbAriaLabel },
React.createElement(ResizeGroup, { responsiveLayoutProps: responsiveLayoutProps, overflowMenuItems: overflowMenuItems }, this.renderBreadcrumbItems())));
}
}
Breadcrumb.defaultProps = {
items: []
};
function getLinkItems(displayedItems) {
return displayedItems.filter(item => {
return item.href || item.onClick;
});
}
function arrangeItems(items) {
if (items === undefined) {
return [];
}
// Build map of what is currently in the list of breadcrumb items
const existingItemsMap = {};
// If exisiting items have the same id and different priorities, the higher priority item will be shown.
items.forEach(item => {
if (!existingItemsMap[item.key] || (existingItemsMap[item.key].priority || 0) <= (item.priority || 0)) {
existingItemsMap[item.key] = item;
}
});
const newItems = [];
Object.keys(existingItemsMap).forEach(key => {
if (!existingItemsMap[key].hidden) {
newItems.push(existingItemsMap[key]);
}
});
const shouldSortItems = newItems.some(x => x.rank >= 0);
if (shouldSortItems) {
// This is not a stable sort. If we pass items with the same rank, we may not always get them back in the
// same order. We have a stable sort in vssf webplatform, but we're not taking a dependency there.
newItems.sort((a, b) => {
const aRank = a.rank || Number.MAX_VALUE;
const bRank = b.rank || Number.MAX_VALUE;
return aRank - bRank;
});
}
return newItems;
}