diff --git a/.changeset/light-poems-trade.md b/.changeset/light-poems-trade.md
new file mode 100644
index 00000000000..25cf62fb663
--- /dev/null
+++ b/.changeset/light-poems-trade.md
@@ -0,0 +1,5 @@
+---
+'@itwin/itwinui-css': patch
+---
+
+`iui-breadcrumbs-list` now stretches to the width of `iui-breadcrumbs`.
diff --git a/packages/itwinui-css/src/breadcrumbs/breadcrumbs.scss b/packages/itwinui-css/src/breadcrumbs/breadcrumbs.scss
index 64004a580bc..bce71c12a7c 100644
--- a/packages/itwinui-css/src/breadcrumbs/breadcrumbs.scss
+++ b/packages/itwinui-css/src/breadcrumbs/breadcrumbs.scss
@@ -20,6 +20,7 @@
list-style-type: none;
user-select: none;
block-size: 100%;
+ inline-size: 100%;
}
.iui-breadcrumbs-item {
diff --git a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx
index 74f49e4501b..1c09a82d759 100644
--- a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx
+++ b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx
@@ -5,10 +5,9 @@
import * as React from 'react';
import cx from 'classnames';
import {
- useMergedRefs,
- useOverflow,
SvgChevronRight,
Box,
+ OverflowContainer,
useWarningLogger,
} from '../../utils/index.js';
import type { PolymorphicForwardRefComponent } from '../../utils/index.js';
@@ -114,70 +113,40 @@ type BreadcrumbsProps = {
*/
const BreadcrumbsComponent = React.forwardRef((props, ref) => {
const {
- children: items,
- currentIndex = items.length - 1,
+ children: childrenProp,
+ currentIndex = React.Children.count(childrenProp) - 1,
separator,
overflowButton,
className,
...rest
} = props;
- const [overflowRef, visibleCount] = useOverflow(items.length);
- const refs = useMergedRefs(overflowRef, ref);
+ const items = React.useMemo(
+ () => React.Children.toArray(childrenProp),
+ [childrenProp],
+ );
return (
-
- {visibleCount > 1 && (
- <>
-
-
- >
- )}
- {items.length - visibleCount > 0 && (
- <>
-
- {overflowButton ? (
- overflowButton(visibleCount)
- ) : (
-
- …
-
- )}
-
-
- >
- )}
- {items
- .slice(
- visibleCount > 1
- ? items.length - visibleCount + 1
- : items.length - 1,
- )
- .map((_, _index) => {
- const index =
- visibleCount > 1
- ? 1 + (items.length - visibleCount) + _index
- : items.length - 1;
- return (
-
-
- {index < items.length - 1 && (
-
- )}
-
- );
- })}
-
+
+
+ {items}
+
+
);
}) as PolymorphicForwardRefComponent<'nav', BreadcrumbsProps>;
@@ -187,6 +156,58 @@ if (process.env.NODE_ENV === 'development') {
// ----------------------------------------------------------------------------
+type BreadcrumbContentProps = Omit & {
+ currentIndex: NonNullable;
+};
+
+const BreadcrumbContent = (props: BreadcrumbContentProps) => {
+ const { children: items, currentIndex, overflowButton, separator } = props;
+ const { visibleCount } = OverflowContainer.useContext();
+
+ return (
+ <>
+ {visibleCount > 1 && (
+ <>
+
+
+ >
+ )}
+ {items.length - visibleCount > 0 && (
+ <>
+
+ {overflowButton ? (
+ overflowButton(visibleCount)
+ ) : (
+
+ …
+
+ )}
+
+
+ >
+ )}
+ {items
+ .slice(
+ visibleCount > 1 ? items.length - visibleCount + 1 : items.length - 1,
+ )
+ .map((_, _index) => {
+ const index =
+ visibleCount > 1
+ ? 1 + (items.length - visibleCount) + _index
+ : items.length - 1;
+ return (
+
+
+ {index < items.length - 1 && }
+
+ );
+ })}
+ >
+ );
+};
+
+// ----------------------------------------------------------------------------
+
const ListItem = ({
item,
isActive,
diff --git a/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx b/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx
index 1594117dad8..188653dcde2 100644
--- a/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx
+++ b/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as React from 'react';
import cx from 'classnames';
-import { useOverflow, useMergedRefs, Box } from '../../utils/index.js';
+import { Box, OverflowContainer } from '../../utils/index.js';
import type {
AnyString,
PolymorphicForwardRefComponent,
@@ -172,6 +172,12 @@ const BaseGroup = React.forwardRef((props, forwardedRef) => {
// ----------------------------------------------------------------------------
+type OverflowGroupProps = Pick<
+ ButtonGroupProps,
+ 'children' | 'orientation' | 'overflowPlacement'
+> &
+ Required>;
+
const OverflowGroup = React.forwardRef((props, forwardedRef) => {
const {
children: childrenProp,
@@ -186,14 +192,11 @@ const OverflowGroup = React.forwardRef((props, forwardedRef) => {
[childrenProp],
);
- const [overflowRef, visibleCount] = useOverflow(
- items.length,
- !overflowButton,
- orientation,
- );
-
return (
- {
},
props.className,
)}
- ref={useMergedRefs(forwardedRef, overflowRef)}
+ ref={forwardedRef}
>
- {(() => {
- if (!(visibleCount < items.length)) {
- return items;
- }
-
- const overflowStart =
- overflowPlacement === 'start'
- ? items.length - visibleCount
- : visibleCount - 1;
-
- return (
- <>
- {overflowButton &&
- overflowPlacement === 'start' &&
- overflowButton(overflowStart)}
-
- {overflowPlacement === 'start'
- ? items.slice(overflowStart + 1)
- : items.slice(0, Math.max(0, overflowStart))}
-
- {overflowButton &&
- overflowPlacement === 'end' &&
- overflowButton(overflowStart)}
- >
- );
- })()}
-
+
+
);
-}) as PolymorphicForwardRefComponent<
- 'div',
- Pick<
- ButtonGroupProps,
- 'children' | 'orientation' | 'overflowButton' | 'overflowPlacement'
- >
->;
+}) as PolymorphicForwardRefComponent<'div', OverflowGroupProps>;
+
+// ----------------------------------------------------------------------------
+
+type OverflowGroupContentProps = Pick<
+ OverflowGroupProps,
+ 'overflowButton' | 'overflowPlacement'
+> & {
+ items: ReturnType;
+};
+
+const OverflowGroupContent = (props: OverflowGroupContentProps) => {
+ const { overflowButton, overflowPlacement, items } = props;
+ const { visibleCount } = OverflowContainer.useContext();
+
+ const overflowStart =
+ overflowPlacement === 'start'
+ ? items.length - visibleCount
+ : visibleCount - 1;
+
+ if (!(visibleCount < items.length)) {
+ return items;
+ }
+
+ return (
+ <>
+ {overflowButton &&
+ overflowPlacement === 'start' &&
+ overflowButton(overflowStart)}
+
+ {overflowPlacement === 'start'
+ ? items.slice(overflowStart + 1)
+ : items.slice(0, Math.max(0, overflowStart))}
+
+ {overflowButton &&
+ overflowPlacement === 'end' &&
+ overflowButton(overflowStart)}
+ >
+ );
+};
diff --git a/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx b/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx
index 00336400ff4..8311ab26465 100644
--- a/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx
+++ b/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx
@@ -4,9 +4,9 @@
*--------------------------------------------------------------------------------------------*/
import * as React from 'react';
import cx from 'classnames';
-import { useOverflow, useMergedRefs, Box } from '../../utils/index.js';
import type { PolymorphicForwardRefComponent } from '../../utils/index.js';
import { SelectTag } from './SelectTag.js';
+import { OverflowContainer } from '../../utils/index.js';
type SelectTagContainerProps = {
/**
@@ -18,23 +18,42 @@ type SelectTagContainerProps = {
/**
*/
export const SelectTagContainer = React.forwardRef((props, ref) => {
- const { tags, className, ...rest } = props;
+ const { tags: tagsProp, className, ...rest } = props;
- const [containerRef, visibleCount] = useOverflow(tags.length);
- const refs = useMergedRefs(ref, containerRef);
+ const tags = React.useMemo(
+ () => React.Children.toArray(tagsProp),
+ [tagsProp],
+ );
return (
-
- <>
- {visibleCount < tags.length ? tags.slice(0, visibleCount - 1) : tags}
- {visibleCount < tags.length && (
-
- )}
- >
-
+
+
);
}) as PolymorphicForwardRefComponent<'div', SelectTagContainerProps>;
+
+// ----------------------------------------------------------------------------
+
+type SelectTagContainerContentProps = {
+ tags: ReturnType;
+};
+
+const SelectTagContainerContent = (props: SelectTagContainerContentProps) => {
+ const { tags } = props;
+ const { visibleCount } = OverflowContainer.useContext();
+
+ return (
+ <>
+ {visibleCount < tags.length ? tags.slice(0, visibleCount - 1) : tags}
+
+
+
+
+ >
+ );
+};
diff --git a/packages/itwinui-react/src/core/Table/TablePaginator.tsx b/packages/itwinui-react/src/core/Table/TablePaginator.tsx
index fa0ac1ad2e2..6f5a26e66ff 100644
--- a/packages/itwinui-react/src/core/Table/TablePaginator.tsx
+++ b/packages/itwinui-react/src/core/Table/TablePaginator.tsx
@@ -12,11 +12,11 @@ import { MenuItem } from '../Menu/MenuItem.js';
import {
getBoundedValue,
useGlobals,
- useOverflow,
useContainerWidth,
SvgChevronLeft,
SvgChevronRight,
Box,
+ OverflowContainer,
} from '../../utils/index.js';
import type { CommonProps } from '../../utils/index.js';
import type { TablePaginatorRendererProps } from './Table.js';
@@ -148,7 +148,7 @@ export const TablePaginator = (props: TablePaginatorProps) => {
const pageListRef = React.useRef(null);
const [focusedIndex, setFocusedIndex] = React.useState(currentPage);
- React.useEffect(() => {
+ React.useLayoutEffect(() => {
setFocusedIndex(currentPage);
}, [currentPage]);
@@ -170,37 +170,7 @@ export const TablePaginator = (props: TablePaginatorProps) => {
}, [focusedIndex]);
const buttonSize = size != 'default' ? 'small' : undefined;
-
- const pageButton = React.useCallback(
- (index: number, tabIndex = index === focusedIndex ? 0 : -1) => (
-
- ),
- [focusedIndex, currentPage, localization, buttonSize, onPageChange],
- );
-
const totalPagesCount = Math.ceil(totalRowsCount / pageSize);
- const pageList = React.useMemo(
- () =>
- new Array(totalPagesCount)
- .fill(null)
- .map((_, index) => pageButton(index)),
- [pageButton, totalPagesCount],
- );
- const [overflowRef, visibleCount] = useOverflow(pageList.length);
-
- const [paginatorResizeRef, paginatorWidth] = useContainerWidth();
const onKeyDown = (event: React.KeyboardEvent) => {
// alt + arrow keys are used by browser/assistive technologies
@@ -247,38 +217,13 @@ export const TablePaginator = (props: TablePaginatorProps) => {
}
};
- const halfVisibleCount = Math.floor(visibleCount / 2);
- let startPage = focusedIndex - halfVisibleCount;
- let endPage = focusedIndex + halfVisibleCount + 1;
- if (startPage < 0) {
- endPage = Math.min(totalPagesCount, endPage + Math.abs(startPage)); // If no room at the beginning, show extra pages at the end
- startPage = 0;
- }
- if (endPage > totalPagesCount) {
- startPage = Math.max(0, startPage - (endPage - totalPagesCount)); // If no room at the end, show extra pages at the beginning
- endPage = totalPagesCount;
- }
-
- // Show ellipsis only if there is a gap between the extremities and the middle pages
- const showStartEllipsis = startPage > 1;
- const showEndEllipsis = endPage < totalPagesCount - 1;
+ const [paginatorResizeRef, paginatorWidth] = useContainerWidth();
- const hasNoRows = totalPagesCount === 0;
const showPagesList = totalPagesCount > 1 || isLoading;
const showPageSizeList =
pageSizeList && !!onPageSizeChange && !!totalRowsCount;
- const ellipsis = (
-
- …
-
- );
-
+ const hasNoRows = totalPagesCount === 0;
const noRowsContent = (
<>
{isLoading ? (
@@ -307,7 +252,7 @@ export const TablePaginator = (props: TablePaginatorProps) => {
)}
{showPagesList && (
-
+
{
onKeyDown={onKeyDown}
ref={pageListRef}
>
- {(() => {
- if (hasNoRows) {
- return noRowsContent;
- }
- if (visibleCount === 1) {
- return pageButton(focusedIndex);
- }
-
- return (
- <>
- {startPage !== 0 && (
- <>
- {pageButton(0, 0)}
- {showStartEllipsis ? ellipsis : null}
- >
- )}
- {pageList.slice(startPage, endPage)}
- {endPage !== totalPagesCount && !isLoading && (
- <>
- {showEndEllipsis ? ellipsis : null}
- {pageButton(totalPagesCount - 1, 0)}
- >
- )}
- {isLoading && (
- <>
- {ellipsis}
-
- >
- )}
- >
- );
- })()}
+ {hasNoRows ? (
+ noRowsContent
+ ) : (
+
+ )}
{
>
-
+
)}
{showPageSizeList && (
@@ -407,3 +333,113 @@ export const TablePaginator = (props: TablePaginatorProps) => {
);
};
+
+// ----------------------------------------------------------------------------
+
+type TablePaginatorPageButtonsProps = Pick<
+ TablePaginatorProps,
+ 'onPageChange'
+> &
+ Required> & {
+ focusedIndex: number;
+ totalPagesCount: number;
+ currentPage: number;
+ };
+
+const TablePaginatorPageButtons = (props: TablePaginatorPageButtonsProps) => {
+ const {
+ focusedIndex,
+ totalPagesCount,
+ onPageChange,
+ currentPage,
+ localization,
+ isLoading,
+ size,
+ } = props;
+
+ const { visibleCount } = OverflowContainer.useContext();
+
+ const buttonSize = size != 'default' ? 'small' : undefined;
+
+ const pageButton = React.useCallback(
+ (index: number, tabIndex = index === focusedIndex ? 0 : -1) => (
+
+ ),
+ [focusedIndex, currentPage, localization, buttonSize, onPageChange],
+ );
+
+ const pageList = React.useMemo(
+ () =>
+ new Array(totalPagesCount)
+ .fill(null)
+ .map((_, index) => pageButton(index)),
+ [pageButton, totalPagesCount],
+ );
+
+ const halfVisibleCount = Math.floor(visibleCount / 2);
+ let startPage = focusedIndex - halfVisibleCount;
+ let endPage = focusedIndex + halfVisibleCount + 1;
+ if (startPage < 0) {
+ endPage = Math.min(totalPagesCount, endPage + Math.abs(startPage)); // If no room at the beginning, show extra pages at the end
+ startPage = 0;
+ }
+ if (endPage > totalPagesCount) {
+ startPage = Math.max(0, startPage - (endPage - totalPagesCount)); // If no room at the end, show extra pages at the beginning
+ endPage = totalPagesCount;
+ }
+
+ const ellipsis = (
+
+ …
+
+ );
+
+ if (visibleCount === 1) {
+ return pageButton(focusedIndex);
+ }
+
+ // Show ellipsis only if there is a gap between the extremities and the middle pages
+ const showStartEllipsis = startPage > 1;
+ const showEndEllipsis = endPage < totalPagesCount - 1;
+
+ return (
+ <>
+ {startPage !== 0 && (
+ <>
+ {pageButton(0, 0)}
+ {showStartEllipsis ? ellipsis : null}
+ >
+ )}
+ {pageList.slice(startPage, endPage)}
+ {endPage !== totalPagesCount && !isLoading && (
+ <>
+ {showEndEllipsis ? ellipsis : null}
+ {pageButton(totalPagesCount - 1, 0)}
+ >
+ )}
+ {isLoading && (
+ <>
+ {ellipsis}
+
+ >
+ )}
+ >
+ );
+};
diff --git a/packages/itwinui-react/src/utils/components/MiddleTextTruncation.tsx b/packages/itwinui-react/src/utils/components/MiddleTextTruncation.tsx
index ece5ac57d56..ec22e27733e 100644
--- a/packages/itwinui-react/src/utils/components/MiddleTextTruncation.tsx
+++ b/packages/itwinui-react/src/utils/components/MiddleTextTruncation.tsx
@@ -3,8 +3,8 @@
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import * as React from 'react';
-import { useOverflow } from '../hooks/useOverflow.js';
import type { CommonProps } from '../props.js';
+import { OverflowContainer } from './OverflowContainer.js';
const ELLIPSIS_CHAR = '…';
@@ -44,23 +44,11 @@ export type MiddleTextTruncationProps = {
* />
*/
export const MiddleTextTruncation = (props: MiddleTextTruncationProps) => {
- const { text, endCharsCount = 6, textRenderer, style, ...rest } = props;
-
- const [ref, visibleCount] = useOverflow(text.length);
-
- const truncatedText = React.useMemo(() => {
- if (visibleCount < text.length) {
- return `${text.substring(
- 0,
- visibleCount - endCharsCount - ELLIPSIS_CHAR.length,
- )}${ELLIPSIS_CHAR}${text.substring(text.length - endCharsCount)}`;
- } else {
- return text;
- }
- }, [endCharsCount, text, visibleCount]);
+ const { text, style, ...rest } = props;
return (
- {
whiteSpace: 'nowrap',
...style,
}}
- ref={ref}
+ itemsCount={text.length}
{...rest}
>
- {textRenderer?.(truncatedText, text) ?? truncatedText}
-
+
+
);
};
if (process.env.NODE_ENV === 'development') {
MiddleTextTruncation.displayName = 'MiddleTextTruncation';
}
+
+// ----------------------------------------------------------------------------
+
+const MiddleTextTruncationContent = (props: MiddleTextTruncationProps) => {
+ const { text, endCharsCount = 6, textRenderer } = props;
+ const { visibleCount } = OverflowContainer.useContext();
+
+ const truncatedText = React.useMemo(() => {
+ if (visibleCount < text.length) {
+ return `${text.substring(
+ 0,
+ visibleCount - endCharsCount - ELLIPSIS_CHAR.length,
+ )}${ELLIPSIS_CHAR}${text.substring(text.length - endCharsCount)}`;
+ } else {
+ return text;
+ }
+ }, [endCharsCount, text, visibleCount]);
+
+ return textRenderer?.(truncatedText, text) ?? truncatedText;
+};
diff --git a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx
new file mode 100644
index 00000000000..548b586195b
--- /dev/null
+++ b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx
@@ -0,0 +1,102 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
+ * See LICENSE.md in the project root for license terms and full copyright notice.
+ *--------------------------------------------------------------------------------------------*/
+import React from 'react';
+import { useMergedRefs } from '../hooks/useMergedRefs.js';
+import type { PolymorphicForwardRefComponent } from '../props.js';
+import { Box } from './Box.js';
+import { useOverflow } from '../hooks/useOverflow.js';
+import { useSafeContext } from '../hooks/useSafeContext.js';
+
+type OverflowContainerProps = {
+ /**
+ * The orientation of the overflow in container.
+ * @default 'horizontal'
+ */
+ overflowOrientation?: 'horizontal' | 'vertical';
+ /**
+ * Count of the *original* items (i.e. when sufficient space is available).
+ */
+ itemsCount: number;
+};
+
+const OverflowContainerComponent = React.forwardRef((props, ref) => {
+ const { itemsCount, children, overflowOrientation, ...rest } = props;
+
+ const [containerRef, visibleCount] = useOverflow(
+ itemsCount,
+ false,
+ overflowOrientation,
+ );
+
+ const overflowContainerContextValue = React.useMemo(
+ () => ({ visibleCount, itemsCount }),
+ [itemsCount, visibleCount],
+ );
+
+ return (
+
+
+ {children}
+
+
+ );
+}) as PolymorphicForwardRefComponent<'div', OverflowContainerProps>;
+
+// ----------------------------------------------------------------------------
+
+type OverflowContainerOverflowNodeProps = {
+ children: React.ReactNode;
+};
+
+/**
+ * Shows the content only when the container is overflowing.
+ */
+const OverflowContainerOverflowNode = (
+ props: OverflowContainerOverflowNodeProps,
+) => {
+ const { children } = props;
+
+ const { visibleCount, itemsCount } = useOverflowContainerContext();
+ const isOverflowing = visibleCount < itemsCount;
+
+ return isOverflowing ? children : null;
+};
+
+// ----------------------------------------------------------------------------
+
+/**
+ * Wrapper over `useOverflow`.
+ *
+ * - Use `OverflowContainer.useContext()` to get overflow related properties.
+ * - Wrap overflow content in `OverflowContainer.OverflowNode` to conditionally render it when overflowing.
+ */
+export const OverflowContainer = Object.assign(OverflowContainerComponent, {
+ /**
+ * Wrap overflow content in this component to conditionally render it when overflowing.
+ */
+ OverflowNode: OverflowContainerOverflowNode,
+ /**
+ * Get overflow related properties of the nearest `OverflowContainer` ancestor.
+ */
+ useContext: useOverflowContainerContext,
+});
+
+// ----------------------------------------------------------------------------
+
+const OverflowContainerContext = React.createContext<
+ | {
+ visibleCount: number;
+ itemsCount: number;
+ }
+ | undefined
+>(undefined);
+if (process.env.NODE_ENV === 'development') {
+ OverflowContainerContext.displayName = 'OverflowContainerContext';
+}
+
+function useOverflowContainerContext() {
+ const overflowContainerContext = useSafeContext(OverflowContainerContext);
+ return overflowContainerContext;
+}
diff --git a/packages/itwinui-react/src/utils/components/index.ts b/packages/itwinui-react/src/utils/components/index.ts
index 07a621438b0..d537eaaf939 100644
--- a/packages/itwinui-react/src/utils/components/index.ts
+++ b/packages/itwinui-react/src/utils/components/index.ts
@@ -16,3 +16,4 @@ export * from './Portal.js';
export * from './ShadowRoot.js';
export * from './LineClamp.js';
export * from './FieldsetBase.js';
+export * from './OverflowContainer.js';