@carbon/ibm-products
Version:
Carbon for IBM Products
724 lines (698 loc) • 24.9 kB
JavaScript
/**
* Copyright IBM Corp. 2020, 2025
*
* 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 { extends as _extends } from '../../../_virtual/_rollupPluginBabelHelpers.js';
import React__default, { forwardRef, useState, useRef, useEffect, useLayoutEffect, useMemo, useCallback } from 'react';
import PropTypes from '../../../_virtual/index.js';
import cx from 'classnames';
import { Grid, Column, DefinitionTooltip, unstable_Text, IconButton, BreadcrumbItem, usePrefix, Breadcrumb, Tag, Popover, OperationalTag, PopoverContent, MenuButton, MenuItem, AspectRatio } from '@carbon/react';
import { breakpoints } from '@carbon/layout';
import { blockClass } from '../PageHeaderUtils.js';
import { createOverflowHandler } from './overflowHandler.js';
import { createOverflowHandler as createOverflowHandler$1 } from '@carbon/utilities';
import { useOverflowItems } from '../../../global/js/hooks/useOverflowItems/useOverflowItems.js';
import { useId } from '../../../global/js/utils/useId.js';
import { ChevronUp } from '@carbon/react/icons';
import { PageHeaderContext, usePageHeader } from './context.js';
import { getHeaderOffset, scrollableAncestor } from './utils.js';
import { pkg } from '../../../settings.js';
import { useResizeObserver } from '../../../global/js/hooks/useResizeObserver.js';
/**
* ----------
* PageHeader
* ----------
*/
const PageHeader = /*#__PURE__*/React__default.forwardRef(function PageHeader(_ref, ref) {
let {
className,
children,
...other
} = _ref;
const [refs, setRefs] = useState({});
const tempRef = useRef(null);
const componentRef = ref ?? tempRef;
const classNames = cx({
[`${blockClass}`]: true,
[`${blockClass}__next`]: true
}, className);
// Used to set CSS custom property with PageHeaderContent height to be used
// for sticky positioning
useResizeObserver(componentRef, () => {
if (componentRef?.current && refs?.contentRef?.current) {
const pageHeaderContentHeight = refs?.contentRef?.current?.offsetHeight;
const totalHeaderOffset = getHeaderOffset(componentRef?.current);
componentRef?.current.style.setProperty(`--${pkg.prefix}-page-header-header-top`, `${(Math.round(pageHeaderContentHeight) - totalHeaderOffset) * -1}px`);
componentRef?.current.style.setProperty(`--${pkg.prefix}-page-header-breadcrumb-top`, `${totalHeaderOffset}px`);
}
});
const [fullyCollapsed, setFullyCollapsed] = useState(false);
const [titleClipped, setTitleClipped] = useState(false);
// Intersection Observer setup, tracks if the PageHeaderContent is visible on page.
// If it is not visible, we should set fully collapsed to true so that the
// scroller button will know if it is clicked to expand rather than
// collapse the header.
useEffect(() => {
if (!refs?.contentRef || !componentRef?.current) {
return;
}
const totalHeaderOffset = getHeaderOffset(componentRef?.current);
const predefinedContentPadding = 24;
const contentObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.target === refs?.contentRef.current) {
setFullyCollapsed(!entry.isIntersecting);
}
});
}, {
root: null,
rootMargin: `${(predefinedContentPadding + totalHeaderOffset + 40) * -1}px 0px 0px 0px`,
threshold: 0.1
});
if (!refs?.titleRef?.current) {
return;
}
const totalTitleHeight = refs?.titleRef.current.offsetHeight;
const titleObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.target === refs?.titleRef.current) {
setTitleClipped(!entry.isIntersecting);
}
});
}, {
root: null,
rootMargin: `${(predefinedContentPadding + totalTitleHeight + totalHeaderOffset + 24) * -1}px 0px 0px 0px`,
threshold: 0.1
});
if (refs?.contentRef.current) {
contentObserver.observe(refs?.contentRef.current);
}
if (refs?.titleRef.current) {
titleObserver.observe(refs?.titleRef.current);
}
return () => {
if (!refs?.contentRef?.current) {
return;
}
contentObserver.unobserve(refs?.contentRef.current);
if (!refs?.titleRef?.current) {
return;
}
contentObserver.unobserve(refs?.titleRef.current);
};
}, [refs, componentRef]);
return /*#__PURE__*/React__default.createElement(PageHeaderContext.Provider, {
value: {
refs,
setRefs,
fullyCollapsed,
titleClipped
}
}, /*#__PURE__*/React__default.createElement("div", _extends({
className: classNames,
ref: componentRef
}, other), children));
});
PageHeader.displayName = 'PageHeader';
/**
* -----------------------
* PageHeaderBreadcrumbBar
* -----------------------
*/
const PageHeaderBreadcrumbBar = /*#__PURE__*/React__default.forwardRef(function PageHeaderBreadcrumbBar(_ref2, ref) {
let {
border = true,
className,
children,
renderIcon: IconElement,
contentActions,
contentActionsFlush,
pageActions,
pageActionsFlush,
...other
} = _ref2;
const classNames = cx({
[`${blockClass}__breadcrumb-bar`]: true,
[`${blockClass}__breadcrumb-bar-border`]: border,
[`${blockClass}__breadcrumb__actions-flush`]: pageActionsFlush
}, className);
const contentActionsClasses = cx({
[`${blockClass}__breadcrumb__content-actions`]: !contentActionsFlush
});
return /*#__PURE__*/React__default.createElement("div", _extends({
className: classNames,
ref: ref
}, other), /*#__PURE__*/React__default.createElement(Grid, null, /*#__PURE__*/React__default.createElement(Column, {
lg: 16,
md: 8,
sm: 4
}, /*#__PURE__*/React__default.createElement("div", {
className: `${blockClass}__breadcrumb-container`
}, /*#__PURE__*/React__default.createElement("div", {
className: `${blockClass}__breadcrumb-wrapper`
}, IconElement && /*#__PURE__*/React__default.createElement("div", {
className: `${blockClass}__breadcrumb__icon`
}, /*#__PURE__*/React__default.createElement(IconElement, null)), children), /*#__PURE__*/React__default.createElement("div", {
className: `${blockClass}__breadcrumb__actions`
}, /*#__PURE__*/React__default.createElement("div", {
className: contentActionsClasses
}, contentActions), pageActions)))));
});
PageHeaderBreadcrumbBar.displayName = 'PageHeaderBreadcrumbBar';
/**
* -----------------
* PageHeaderContent
* -----------------
*/
const PageHeaderContent = /*#__PURE__*/React__default.forwardRef(function PageHeaderContent(_ref3, ref) {
let {
className,
children,
title,
renderIcon: IconElement,
contextualActions,
pageActions,
...other
} = _ref3;
const contentRef = useRef(null);
const componentRef = ref ?? contentRef;
const {
setRefs
} = usePageHeader();
const classNames = cx({
[`${blockClass}__content`]: true
}, className);
const titleRef = useRef(null);
useEffect(() => {
if (componentRef?.current) {
setRefs(prev => ({
...prev,
contentRef: componentRef,
titleRef
}));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [isEllipsisApplied, setIsEllipsisApplied] = useState(false);
const isEllipsisActive = element => {
setIsEllipsisApplied(element.offsetHeight < element.scrollHeight);
return element.offsetHeight < element.scrollHeight;
};
useLayoutEffect(() => {
titleRef.current && isEllipsisActive(titleRef.current);
}, [title]);
return /*#__PURE__*/React__default.createElement("div", _extends({
className: classNames,
ref: componentRef
}, other), /*#__PURE__*/React__default.createElement(Grid, null, /*#__PURE__*/React__default.createElement(Column, {
lg: 16,
md: 8,
sm: 4
}, /*#__PURE__*/React__default.createElement("div", {
className: `${blockClass}__content__title-wrapper`
}, /*#__PURE__*/React__default.createElement("div", {
className: `${blockClass}__content__start`
}, /*#__PURE__*/React__default.createElement("div", {
className: `${blockClass}__content__title-container`
}, IconElement && /*#__PURE__*/React__default.createElement("div", {
className: `${blockClass}__content__icon`
}, /*#__PURE__*/React__default.createElement(IconElement, null)), isEllipsisApplied ? /*#__PURE__*/React__default.createElement(DefinitionTooltip, {
definition: title
}, /*#__PURE__*/React__default.createElement(unstable_Text, {
ref: titleRef,
as: "h4",
className: `${blockClass}__content__title`
}, title)) : /*#__PURE__*/React__default.createElement(unstable_Text, {
ref: titleRef,
as: "h4",
className: `${blockClass}__content__title`
}, title)), contextualActions && /*#__PURE__*/React__default.createElement("div", {
className: `${blockClass}__content__contextual-actions`
}, contextualActions)), pageActions), children)));
});
PageHeaderContent.displayName = 'PageHeaderContent';
PageHeaderContent.propTypes = {
/**
* Provide child elements to be rendered inside PageHeaderContent.
*/
children: PropTypes.node,
/**
* Specify an optional className to be added to your PageHeaderContent
*/
className: PropTypes.string,
/**
* The PageHeaderContent's contextual actions
*/
contextualActions: PropTypes.node,
/**
* The PageHeaderContent's page actions
*/
pageActions: PropTypes.node,
/**
* Provide an optional icon to render in front of the PageHeaderContent's title.
*/
renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
/**
* The PageHeaderContent's subtitle
*/
subtitle: PropTypes.string,
/**
* The PageHeaderContent's title
*/
title: PropTypes.string.isRequired
};
/**
* ----------------
* PageHeaderContentPageActions
* ----------------
*/
const PageHeaderContentPageActions = _ref4 => {
let {
className,
children,
menuButtonLabel = 'Actions',
actions,
...other
} = _ref4;
const classNames = cx({
[`${blockClass}__content__page-actions`]: true
}, className);
const containerRef = useRef(null);
const offsetRef = useRef(null);
const [menuButtonVisibility, setMenuButtonVisibility] = useState(false);
const [hiddenItems, setHiddenItems] = useState([]);
// need to set the grid columns width based on the menu button's width
// to avoid overlapping when resizing
useLayoutEffect(() => {
if (menuButtonVisibility && offsetRef.current) {
const width = offsetRef.current.offsetWidth;
document.documentElement.style.setProperty('--page-header-title-grid-width', `${width}px`);
}
}, [menuButtonVisibility]);
useEffect(() => {
if (!containerRef.current || !Array.isArray(actions)) {
return;
}
createOverflowHandler$1({
container: containerRef.current,
// exclude the hidden menu button from children
maxVisibleItems: containerRef.current.children.length - 1,
onChange: (visible, hidden) => {
setHiddenItems(actions?.slice(visible.length));
if (hidden.length > 0) {
setMenuButtonVisibility(true);
}
}
});
}, [actions]);
return /*#__PURE__*/React__default.createElement("div", _extends({
className: classNames,
ref: containerRef
}, other), Array.isArray(actions) && /*#__PURE__*/React__default.createElement(React__default.Fragment, null, actions.map(action => /*#__PURE__*/React__default.createElement("div", {
key: action.id
}, /*#__PURE__*/React__default.cloneElement(action.body, {
...action.body.props,
onClick: action.onClick
}))), /*#__PURE__*/React__default.createElement("span", {
"data-offset": true,
"data-hidden": true,
ref: offsetRef
}, /*#__PURE__*/React__default.createElement(MenuButton, {
menuAlignment: "bottom-end",
label: menuButtonLabel,
size: "md"
}, [...hiddenItems].reverse().map(item => /*#__PURE__*/React__default.createElement(MenuItem, _extends({
key: item.id,
onClick: item.onClick
}, item.menuItem)))))));
};
PageHeaderContentPageActions.displayName = 'PageHeaderContentPageActions';
PageHeaderContentPageActions.propTypes = {
/**
* The PageHeaderContent's page actions
*/
actions: PropTypes.oneOfType([PropTypes.node, PropTypes.array]),
/**
* Provide child elements to be rendered inside PageHeaderContentPageActions.
*/
children: PropTypes.node,
/**
* Specify an optional className to be added to your PageHeaderContentPageActions
*/
className: PropTypes.string,
/**
* The PageHeaderContent's collapsible Menu button label
*/
menuButtonLabel: PropTypes.string
};
/**
* ----------------
* PageHeaderContentText
* ----------------
*/
const PageHeaderContentText = _ref5 => {
let {
className,
children,
subtitle,
...other
} = _ref5;
const classNames = cx({
[`${blockClass}__content__body`]: true
}, className);
return /*#__PURE__*/React__default.createElement("div", _extends({
className: classNames
}, other), subtitle && /*#__PURE__*/React__default.createElement(unstable_Text, {
as: "h3",
className: `${blockClass}__content__subtitle`
}, subtitle), children);
};
PageHeaderContentText.displayName = 'PageHeaderContentText';
PageHeaderContentText.propTypes = {
/**
* Provide child elements to be rendered inside PageHeaderContentText.
*/
children: PropTypes.node,
/**
* Specify an optional className to be added to your PageHeaderContentText
*/
className: PropTypes.string,
/**
* The PageHeaderContent's subtitle
*/
subtitle: PropTypes.string
};
/**
* ----------------
* PageHeaderHeroImage
* ----------------
*/
const PageHeaderHeroImage = _ref6 => {
let {
className,
children,
...other
} = _ref6;
const [lgBreakpoint, setLgBreakpoint] = useState(false);
const classNames = cx({
[`${blockClass}__hero-image`]: true
}, className);
const lgMediaQuery = `(min-width: ${breakpoints.lg.width})`;
useEffect(() => {
const listener = event => {
setLgBreakpoint(event.matches);
};
const mediaQueryList = window.matchMedia(lgMediaQuery);
mediaQueryList.addEventListener('change', listener);
setLgBreakpoint(mediaQueryList.matches);
return () => {
mediaQueryList.removeEventListener('change', listener);
};
}, [lgMediaQuery]);
return /*#__PURE__*/React__default.createElement(AspectRatio, _extends({
className: classNames
}, other, {
ratio: lgBreakpoint ? '2x1' : '3x2'
}), children);
};
PageHeaderHeroImage.displayName = 'PageHeaderHeroImage';
PageHeaderHeroImage.propTypes = {
/**
* Provide child elements to be rendered inside PageHeaderHeroImage.
*/
children: PropTypes.node,
/**
* Specify an optional className to be added to your PageHeaderHeroImage
*/
className: PropTypes.string
};
/**
* ----------------
* PageHeaderTabBar
* ----------------
*/
const PageHeaderTabBar = /*#__PURE__*/React__default.forwardRef(function PageHeaderTabBar(_ref7, ref) {
let {
className,
children,
tags = [],
scroller,
...other
} = _ref7;
const classNames = cx({
[`${blockClass}__tab-bar`]: true
}, className);
const renderScroller = () => scroller && /*#__PURE__*/React__default.createElement("div", {
className: `${pkg.prefix}--page-header--scroller-button-container`
}, scroller);
// Early return if no tags are provided
if (!tags.length) {
return /*#__PURE__*/React__default.createElement("div", _extends({
className: classNames,
ref: ref
}, other), /*#__PURE__*/React__default.createElement(Grid, null, /*#__PURE__*/React__default.createElement(Column, {
lg: 16,
md: 8,
sm: 4
}, children, renderScroller())));
}
const [openPopover, setOpenPopover] = useState(false);
const tagSize = tags[0]?.size || 'md';
const instanceId = useId('PageHeaderTabBar');
const tagsWithIds = useMemo(() => {
return tags.map((tag, index) => ({
...tag,
id: tag.id || `tag-${index}-${instanceId}`
}));
}, [instanceId, tags]);
const tagsContainerRef = useRef(null);
const offsetRef = useRef(null);
// To close popover when window resizes
useEffect(() => {
const handleResize = () => {
// Close the popover when window resizes to prevent unwanted opens
setOpenPopover(false);
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
// overflow items hook
const {
visibleItems = [],
hiddenItems = [],
itemRefHandler = () => {}
} = useOverflowItems(tagsWithIds, tagsContainerRef, offsetRef) || {
visibleItems: [],
hiddenItems: [],
itemRefHandler: () => {}
};
const handleOverflowClick = useCallback(event => {
event.stopPropagation();
setOpenPopover(prev => !prev);
}, []);
// Function to render tags
const renderTags = () => /*#__PURE__*/React__default.createElement("div", {
className: `${blockClass}__tags`,
ref: tagsContainerRef
}, visibleItems.map(tag => /*#__PURE__*/React__default.createElement(Tag, {
key: tag.id,
ref: node => itemRefHandler(tag.id, node),
type: tag.type,
size: tag.size,
className: `${blockClass}__tag-item`
}, tag.text)), hiddenItems.length > 0 && /*#__PURE__*/React__default.createElement(Popover, {
open: openPopover,
onRequestClose: () => setOpenPopover(false)
}, /*#__PURE__*/React__default.createElement(OperationalTag, {
onClick: handleOverflowClick,
"aria-expanded": openPopover,
text: `+${hiddenItems.length}`,
size: tagSize
}), /*#__PURE__*/React__default.createElement(PopoverContent, {
className: "tag-popover-content"
}, /*#__PURE__*/React__default.createElement("div", {
className: `${blockClass}__tags-popover-list`
}, hiddenItems.map(tag => /*#__PURE__*/React__default.createElement(Tag, {
key: tag.id,
type: tag.type,
size: tag.size
}, tag.text))))));
return /*#__PURE__*/React__default.createElement("div", _extends({
className: classNames,
ref: ref
}, other), /*#__PURE__*/React__default.createElement(Grid, null, /*#__PURE__*/React__default.createElement(Column, {
lg: 16,
md: 8,
sm: 4
}, /*#__PURE__*/React__default.createElement("div", {
className: cx(`${blockClass}__tab-bar--tablist`, {
[`${pkg.prefix}--page-header__tab-bar--with-scroller`]: !!scroller
})
}, children, tags.length > 0 && renderTags(), renderScroller()))));
});
PageHeaderTabBar.displayName = 'PageHeaderTabBar';
const PageHeaderScrollButton = /*#__PURE__*/React__default.forwardRef(function PageHeaderExpander(_ref8, ref) {
let {
className,
children,
label,
onClick,
collapseText = 'Collapse',
expandText = 'Expand',
...other
} = _ref8;
const {
refs,
fullyCollapsed
} = usePageHeader();
const handleScroller = isFullyCollapsed => {
if (!refs?.contentRef?.current) {
return;
}
const scrollableTarget = scrollableAncestor(refs?.contentRef.current);
// Page header content is not fully collapsed
if (!isFullyCollapsed) {
const pageHeaderContentHeight = refs?.contentRef.current.offsetHeight;
scrollableTarget?.scrollTo({
top: pageHeaderContentHeight,
// headerTopValue, check if breadcrumb bar is included
behavior: 'smooth'
});
} else {
// Page header content is fully collapsed
scrollableTarget?.scrollTo({
top: 0,
behavior: 'smooth'
});
}
};
return /*#__PURE__*/React__default.createElement(IconButton, _extends({
ref: ref,
label: fullyCollapsed ? expandText : collapseText,
size: "md",
kind: "ghost",
autoAlign: true
}, other, {
onClick: event => {
onClick?.(event);
handleScroller(!!fullyCollapsed);
},
className: cx(className, `${pkg.prefix}--page-header--scroller-button`)
}), /*#__PURE__*/React__default.createElement(ChevronUp, {
className: cx(`${pkg.prefix}--page-header--scroller-button-icon`, {
[`${pkg.prefix}--page-header--scroller-button-icon-collapsed`]: fullyCollapsed
})
}));
});
const PageHeaderTitleBreadcrumb = /*#__PURE__*/forwardRef((_ref9, ref) => {
let {
className,
children,
...other
} = _ref9;
const {
titleClipped,
refs
} = usePageHeader();
return /*#__PURE__*/React__default.createElement(BreadcrumbItem, _extends({
ref: ref,
isCurrentPage: true
}, other, {
className: cx(className, `${pkg.prefix}--page-header-title-breadcrumb`, {
[`${pkg.prefix}--page-header-title-breadcrumb-show`]: titleClipped && refs?.titleRef
})
}), children);
});
// This component is a wrapper for the Breadcrumb, and renders breadcrumb items as children
// including the overflow breadcrumb item. The overflowHandler determines what elements
// are visible and hidden and passes the hidden elements back to the render prop used
// to display the overflow breadcrumb
const PageHeaderBreadcrumbOverflow = /*#__PURE__*/forwardRef((_ref10, ref) => {
let {
renderOverflowBreadcrumb,
className,
children,
...other
} = _ref10;
const [hiddenBreadcrumbs, setHiddenBreadcrumbs] = React__default.useState([]);
const fallbackRef = useRef(null);
const componentRef = ref ?? fallbackRef;
// Initialize overflow resize handler
const carbonPrefix = usePrefix();
useEffect(() => {
if (!componentRef) {
return;
}
const breadcrumbList = componentRef?.current.querySelector(`.${carbonPrefix}--breadcrumb`);
createOverflowHandler({
container: breadcrumbList,
onChange: (_, hidden) => {
setHiddenBreadcrumbs(hidden);
}
});
// Don't want ref or carbon prefix in dependency array
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const renderChildren = () => {
// Only BreadcrumbItems and TitleBreadcrumbs are valid children
const filteredBreadcrumbs = React__default.Children.toArray(children).filter(child => {
if (/*#__PURE__*/React__default.isValidElement(child)) {
return child.type === BreadcrumbItem || PageHeaderTitleBreadcrumb;
}
});
// We need to clone the renderProp for the overflow breadcrumb item
// to place it before the title breadcrumb according to the design
if (filteredBreadcrumbs) {
const overflowBreadcrumb = renderOverflowBreadcrumb?.(hiddenBreadcrumbs);
// If no overflow breadcrumb provided, return here with the rest of the children
if (!overflowBreadcrumb) {
return children;
}
const clonedTitleBreadcrumb = /*#__PURE__*/React__default.cloneElement(overflowBreadcrumb, {
key: 'cloned overflow breadcrumb item',
'data-fixed': true,
className: cx(`${pkg.prefix}--page-header-breadcrumb-overflow-item`, {
[`${pkg.prefix}--page-header-overflow-breadcrumb-item-with-items`]: hiddenBreadcrumbs.length
})
});
const clonedChildren = [...filteredBreadcrumbs];
clonedChildren.splice(filteredBreadcrumbs.length - 1, 0, clonedTitleBreadcrumb); // second to last position
return clonedChildren;
}
return children;
};
return /*#__PURE__*/React__default.createElement(Breadcrumb, _extends({
className: cx(className, `${pkg.prefix}--page-header-breadcrumb-overflow`),
ref: componentRef
}, other), renderChildren());
});
/**
* -------
* Exports
* -------
*/
const Root = PageHeader;
Root.displayName = 'PageHeader.Root';
const BreadcrumbBar = PageHeaderBreadcrumbBar;
BreadcrumbBar.displayName = 'PageHeaderBreadcrumbBar';
const Content = PageHeaderContent;
Content.displayName = 'PageHeaderContent';
const ContentPageActions = PageHeaderContentPageActions;
ContentPageActions.displayName = 'PageHeaderContentPageActions';
const ContentText = PageHeaderContentText;
ContentText.displayName = 'PageHeaderContentText';
const HeroImage = PageHeaderHeroImage;
HeroImage.displayName = 'PageHeaderHeroImage';
const TabBar = PageHeaderTabBar;
TabBar.displayName = 'PageHeaderTabBar';
const ScrollButton = PageHeaderScrollButton;
ScrollButton.displayName = 'PageHeaderScrollButton';
const TitleBreadcrumb = PageHeaderTitleBreadcrumb;
TitleBreadcrumb.displayName = 'PageHeaderTitleBreadcrumb';
const BreadcrumbOverflow = PageHeaderBreadcrumbOverflow;
BreadcrumbOverflow.displayName = 'PageHeaderBreadcrumbOverflow';
export { BreadcrumbBar, BreadcrumbOverflow, Content, ContentPageActions, ContentText, HeroImage, PageHeader, PageHeaderBreadcrumbBar, PageHeaderBreadcrumbOverflow, PageHeaderContent, PageHeaderContentPageActions, PageHeaderContentText, PageHeaderHeroImage, PageHeaderScrollButton, PageHeaderTabBar, PageHeaderTitleBreadcrumb, Root, ScrollButton, TabBar, TitleBreadcrumb };