From 78c44a4c18030eab8a4aaab0be95e48b2699c0c4 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Wed, 26 Jun 2024 20:04:19 -0400 Subject: [PATCH 01/65] Trying different approach in useOverflow --- .../src/utils/hooks/useOverflow.tsx | 98 ++++++++++--------- .../src/utils/hooks/usePrevious.ts | 21 ++++ playgrounds/vite/src/App.tsx | 32 ++++-- playgrounds/vite/src/main.tsx | 6 +- 4 files changed, 101 insertions(+), 56 deletions(-) create mode 100644 packages/itwinui-react/src/utils/hooks/usePrevious.ts diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx index 157571f75c2..1633261426a 100644 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx +++ b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx @@ -6,8 +6,11 @@ import * as React from 'react'; import { useMergedRefs } from './useMergedRefs.js'; import { useResizeObserver } from './useResizeObserver.js'; import { useLayoutEffect } from './useIsomorphicLayoutEffect.js'; +import usePrevious from './usePrevious.js'; +import { useLatestRef } from './useLatestRef.js'; -const STARTING_MAX_ITEMS_COUNT = 20; +/** First guess of the number of items that overflows. We refine this guess with subsequent renders */ +const STARTING_MAX_ITEMS_COUNT = 32; /** * Hook that observes the size of an element and returns the number of items @@ -37,73 +40,74 @@ export const useOverflow = ( orientation: 'horizontal' | 'vertical' = 'horizontal', ) => { const containerRef = React.useRef(null); - + const initialVisibleCount = Math.min(items.length, STARTING_MAX_ITEMS_COUNT); const [visibleCount, setVisibleCount] = React.useState(() => - disabled ? items.length : Math.min(items.length, STARTING_MAX_ITEMS_COUNT), + disabled ? items.length : initialVisibleCount, ); - const needsFullRerender = React.useRef(true); - const [containerSize, setContainerSize] = React.useState(0); - const previousContainerSize = React.useRef(0); + const previousContainerSize = usePrevious(containerSize); + previousContainerSize; + const updateContainerSize = React.useCallback( ({ width, height }: DOMRectReadOnly) => setContainerSize(orientation === 'horizontal' ? width : height), [orientation], ); - const [resizeRef, observer] = useResizeObserver(updateContainerSize); - const resizeObserverRef = React.useRef(observer); - useLayoutEffect(() => { - if (disabled) { - setVisibleCount(items.length); - } else { - setVisibleCount(Math.min(items.length, STARTING_MAX_ITEMS_COUNT)); - needsFullRerender.current = true; - } - }, [containerSize, disabled, items]); + const [resizeRef, observer] = useResizeObserver(updateContainerSize); + const resizeObserverRef = useLatestRef(observer); + resizeObserverRef; - const mergedRefs = useMergedRefs(containerRef, resizeRef); + const [visibleCountGuessRange, setVisibleCountGuessRange] = React.useState< + [number, number] | null + >([0, initialVisibleCount]); + // TODO: Replace eslint-disable with proper listening to containerRef resize + // eslint-disable-next-line react-hooks/exhaustive-deps useLayoutEffect(() => { - if (!containerRef.current || disabled) { - resizeObserverRef.current?.disconnect(); + // TODO: Handle the case where there is a resize after we've stabilized on a visibleCount + // if (searchIndexes == null) { + // setSearchIndexes + // } + + // Already stabilized + if (visibleCountGuessRange == null) { return; } + const dimension = orientation === 'horizontal' ? 'Width' : 'Height'; + const availableSize = containerRef.current?.[`offset${dimension}`] ?? 0; + const requiredSize = containerRef.current?.[`scroll${dimension}`] ?? 0; - const availableSize = containerRef.current[`offset${dimension}`]; - const requiredSize = containerRef.current[`scroll${dimension}`]; + const isOverflowing = availableSize < requiredSize; - if (availableSize < requiredSize) { - const avgItemSize = requiredSize / visibleCount; - const visibleItems = Math.floor(availableSize / avgItemSize); - /* When first item is larger than the container - visibleItems count is 0, - We can assume that at least some part of the first item is visible and return 1. */ - setVisibleCount(visibleItems > 0 ? visibleItems : 1); - } else if (needsFullRerender.current) { - const childrenSize = Array.from(containerRef.current.children).reduce( - (sum: number, child: HTMLElement) => sum + child[`offset${dimension}`], - 0, - ); - // Previous `useEffect` might have updated visible count, but we still have old one - // If it is 0, lets try to update it with items length. - const currentVisibleCount = - visibleCount || Math.min(items.length, STARTING_MAX_ITEMS_COUNT); - const avgItemSize = childrenSize / currentVisibleCount; - const visibleItems = Math.floor(availableSize / avgItemSize); + console.log('RUNNING', { + visibleCountGuessRange: visibleCountGuessRange.toString(), + isOverflowing, + visibleCount, + }); - if (!isNaN(visibleItems)) { - // Doubling the visible items to overflow the container. Just to be safe. - setVisibleCount(Math.min(items.length, visibleItems * 2)); - } + const newGuess = Math.floor( + (visibleCountGuessRange[0] + visibleCountGuessRange[1]) / 2, + ); + setVisibleCount(newGuess); + + // We have found the correct visibleCount + if (visibleCountGuessRange[1] - visibleCountGuessRange[0] === 1) { + setVisibleCountGuessRange(null); + } + // overflowing = we guessed too high. So, new max guess = half the current guess + else if (isOverflowing) { + setVisibleCountGuessRange([visibleCountGuessRange[0], newGuess]); } - needsFullRerender.current = false; - }, [containerSize, visibleCount, disabled, items.length, orientation]); + // not overflowing = maybe we guessed too low. So, new min guess = half of current guess + else { + setVisibleCountGuessRange([newGuess, visibleCountGuessRange[1]]); + } + }); - useLayoutEffect(() => { - previousContainerSize.current = containerSize; - }, [containerSize]); + const mergedRefs = useMergedRefs(containerRef, resizeRef); return [mergedRefs, visibleCount] as const; }; diff --git a/packages/itwinui-react/src/utils/hooks/usePrevious.ts b/packages/itwinui-react/src/utils/hooks/usePrevious.ts new file mode 100644 index 00000000000..12f65e363aa --- /dev/null +++ b/packages/itwinui-react/src/utils/hooks/usePrevious.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ +/** + * Based on react-use's usePrevious + * The original code is licensed under "Unlimited License" - https://unpkg.com/browse/react-use@17.5.0/LICENSE + */ + +import { useRef } from 'react'; +import { useLayoutEffect } from './useIsomorphicLayoutEffect.js'; + +export default function usePrevious(state: T): T | undefined { + const ref = useRef(); + + useLayoutEffect(() => { + ref.current = state; + }); + + return ref.current; +} diff --git a/playgrounds/vite/src/App.tsx b/playgrounds/vite/src/App.tsx index 308ffd82cc0..0d7c5c67711 100644 --- a/playgrounds/vite/src/App.tsx +++ b/playgrounds/vite/src/App.tsx @@ -1,11 +1,31 @@ -import { Button } from '@itwin/itwinui-react'; +import { ComboBox } from '@itwin/itwinui-react'; + +export default function App() { + const data: { label: string; value: number }[] = []; + for (let i = 0; i < 15; i++) { + data.push({ label: `option ${i}`, value: i }); + } + const widths = []; + for (let i = 0; i < 20; i++) { + widths.push(790 + i * 3); + } -const App = () => { return ( <> - + {widths.slice(5, 6).map((width) => ( + x.value)} + onChange={() => {}} + inputProps={{ + placeholder: 'Placeholder', + }} + /> + ))} ); -}; - -export default App; +} diff --git a/playgrounds/vite/src/main.tsx b/playgrounds/vite/src/main.tsx index 9cfdf284a4e..d1192c70584 100644 --- a/playgrounds/vite/src/main.tsx +++ b/playgrounds/vite/src/main.tsx @@ -32,7 +32,7 @@ const Shell = () => { }; createRoot(document.getElementById('root')!).render( - - - , + // + , + // , ); From 8c7dd698188e95589798a175b2a4597623fb9dc1 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:04:28 -0400 Subject: [PATCH 02/65] Fixed bug causing lower than correct visibleCount. i.e. Updating range based on next visibleCount/isOverflowing instead of current --- .../src/utils/hooks/useOverflow.tsx | 53 +++++++++++++------ playgrounds/vite/src/App.tsx | 2 +- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx index 1633261426a..7230a861dcc 100644 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx +++ b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx @@ -9,6 +9,8 @@ import { useLayoutEffect } from './useIsomorphicLayoutEffect.js'; import usePrevious from './usePrevious.js'; import { useLatestRef } from './useLatestRef.js'; +type GuessRange = [number, number] | null; + /** First guess of the number of items that overflows. We refine this guess with subsequent renders */ const STARTING_MAX_ITEMS_COUNT = 32; @@ -59,9 +61,8 @@ export const useOverflow = ( const resizeObserverRef = useLatestRef(observer); resizeObserverRef; - const [visibleCountGuessRange, setVisibleCountGuessRange] = React.useState< - [number, number] | null - >([0, initialVisibleCount]); + const [visibleCountGuessRange, setVisibleCountGuessRange] = + React.useState([0, initialVisibleCount]); // TODO: Replace eslint-disable with proper listening to containerRef resize // eslint-disable-next-line react-hooks/exhaustive-deps @@ -70,12 +71,18 @@ export const useOverflow = ( // if (searchIndexes == null) { // setSearchIndexes // } - + // (() => { // Already stabilized if (visibleCountGuessRange == null) { return; } + // We have already found the correct visibleCount + if (visibleCountGuessRange[1] - visibleCountGuessRange[0] === 1) { + setVisibleCountGuessRange(null); + return; + } + const dimension = orientation === 'horizontal' ? 'Width' : 'Height'; const availableSize = containerRef.current?.[`offset${dimension}`] ?? 0; const requiredSize = containerRef.current?.[`scroll${dimension}`] ?? 0; @@ -88,23 +95,39 @@ export const useOverflow = ( visibleCount, }); - const newGuess = Math.floor( - (visibleCountGuessRange[0] + visibleCountGuessRange[1]) / 2, - ); - setVisibleCount(newGuess); + let newVisibleCountGuessRange = visibleCountGuessRange; - // We have found the correct visibleCount - if (visibleCountGuessRange[1] - visibleCountGuessRange[0] === 1) { - setVisibleCountGuessRange(null); - } // overflowing = we guessed too high. So, new max guess = half the current guess - else if (isOverflowing) { - setVisibleCountGuessRange([visibleCountGuessRange[0], newGuess]); + if (isOverflowing) { + newVisibleCountGuessRange = [visibleCountGuessRange[0], visibleCount]; } // not overflowing = maybe we guessed too low. So, new min guess = half of current guess else { - setVisibleCountGuessRange([newGuess, visibleCountGuessRange[1]]); + newVisibleCountGuessRange = [visibleCount, visibleCountGuessRange[1]]; } + + setVisibleCountGuessRange(newVisibleCountGuessRange); + + // Always guess that the correct visibleCount is in the middle of the range + setVisibleCount( + Math.floor( + (newVisibleCountGuessRange[0] + newVisibleCountGuessRange[1]) / 2, + ), + ); + + // // We have found the correct visibleCount + // if (visibleCountGuessRange[1] - visibleCountGuessRange[0] === 1) { + // setVisibleCountGuessRange(null); + // } + // // overflowing = we guessed too high. So, new max guess = half the current guess + // else if (isOverflowing) { + // setVisibleCountGuessRange([visibleCountGuessRange[0], newGuess]); + // } + // // not overflowing = maybe we guessed too low. So, new min guess = half of current guess + // else { + // setVisibleCountGuessRange([newGuess, visibleCountGuessRange[1]]); + // } + // })(); }); const mergedRefs = useMergedRefs(containerRef, resizeRef); diff --git a/playgrounds/vite/src/App.tsx b/playgrounds/vite/src/App.tsx index 0d7c5c67711..9d47847a5be 100644 --- a/playgrounds/vite/src/App.tsx +++ b/playgrounds/vite/src/App.tsx @@ -12,7 +12,7 @@ export default function App() { return ( <> - {widths.slice(5, 6).map((width) => ( + {widths.slice(0, 1).map((width) => ( Date: Fri, 28 Jun 2024 12:38:40 -0400 Subject: [PATCH 03/65] Bug fixes. Handle resize. Can be optimized a bit more. --- .../src/utils/hooks/useOverflow.tsx | 92 +++++++++++++------ playgrounds/next/pages/index.tsx | 28 +++++- 2 files changed, 89 insertions(+), 31 deletions(-) diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx index 7230a861dcc..75088a7cd63 100644 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx +++ b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx @@ -7,7 +7,6 @@ import { useMergedRefs } from './useMergedRefs.js'; import { useResizeObserver } from './useResizeObserver.js'; import { useLayoutEffect } from './useIsomorphicLayoutEffect.js'; import usePrevious from './usePrevious.js'; -import { useLatestRef } from './useLatestRef.js'; type GuessRange = [number, number] | null; @@ -47,7 +46,7 @@ export const useOverflow = ( disabled ? items.length : initialVisibleCount, ); - const [containerSize, setContainerSize] = React.useState(0); + const [containerSize, setContainerSize] = React.useState(-1); const previousContainerSize = usePrevious(containerSize); previousContainerSize; @@ -58,20 +57,12 @@ export const useOverflow = ( ); const [resizeRef, observer] = useResizeObserver(updateContainerSize); - const resizeObserverRef = useLatestRef(observer); - resizeObserverRef; + const resizeObserverRef = React.useRef(observer); const [visibleCountGuessRange, setVisibleCountGuessRange] = React.useState([0, initialVisibleCount]); - // TODO: Replace eslint-disable with proper listening to containerRef resize - // eslint-disable-next-line react-hooks/exhaustive-deps - useLayoutEffect(() => { - // TODO: Handle the case where there is a resize after we've stabilized on a visibleCount - // if (searchIndexes == null) { - // setSearchIndexes - // } - // (() => { + const guessVisibleCount = React.useCallback(() => { // Already stabilized if (visibleCountGuessRange == null) { return; @@ -79,6 +70,7 @@ export const useOverflow = ( // We have already found the correct visibleCount if (visibleCountGuessRange[1] - visibleCountGuessRange[0] === 1) { + console.log('STABILIZED'); setVisibleCountGuessRange(null); return; } @@ -95,14 +87,23 @@ export const useOverflow = ( visibleCount, }); + // // Firstly, the highest guess MUST be above the correct visibleCount value. If not, double the highest guess + if (visibleCountGuessRange[1] === visibleCount && !isOverflowing) { + setVisibleCountGuessRange([ + visibleCountGuessRange[0], + visibleCountGuessRange[1] * 2, + ]); + setVisibleCount(visibleCountGuessRange[1] * 2); + return; + } + let newVisibleCountGuessRange = visibleCountGuessRange; // overflowing = we guessed too high. So, new max guess = half the current guess if (isOverflowing) { newVisibleCountGuessRange = [visibleCountGuessRange[0], visibleCount]; - } - // not overflowing = maybe we guessed too low. So, new min guess = half of current guess - else { + } else { + // not overflowing = maybe we guessed too low. So, new min guess = half of current guess newVisibleCountGuessRange = [visibleCount, visibleCountGuessRange[1]]; } @@ -114,22 +115,57 @@ export const useOverflow = ( (newVisibleCountGuessRange[0] + newVisibleCountGuessRange[1]) / 2, ), ); + }, [orientation, visibleCount, visibleCountGuessRange]); - // // We have found the correct visibleCount - // if (visibleCountGuessRange[1] - visibleCountGuessRange[0] === 1) { - // setVisibleCountGuessRange(null); - // } - // // overflowing = we guessed too high. So, new max guess = half the current guess - // else if (isOverflowing) { - // setVisibleCountGuessRange([visibleCountGuessRange[0], newGuess]); - // } - // // not overflowing = maybe we guessed too low. So, new min guess = half of current guess - // else { - // setVisibleCountGuessRange([newGuess, visibleCountGuessRange[1]]); - // } - // })(); + // TODO: Replace eslint-disable with proper listening to containerRef resize + // eslint-disable-next-line react-hooks/exhaustive-deps + useLayoutEffect(() => { + console.log('IN FIRST LOOP'); + + if (!containerRef.current || disabled) { + resizeObserverRef.current?.disconnect(); + return; + } + + guessVisibleCount(); }); + useLayoutEffect(() => { + if (visibleCountGuessRange != null) { + // No need to listen to resizes since we're already in the process of finding the correct visibleCount + return; + } + + // Only start re-guessing if containerSize changes *after* the containerSize is first set. + // This prevents unnecessary renders + if ( + containerSize === previousContainerSize || + previousContainerSize === -1 + ) { + return; + } + + console.log('containerSize changed', { + containerSize, + previousContainerSize, + }); + + // Set the visibleCountGuessRange to again find the correct visibleCount; + setVisibleCountGuessRange([0, visibleCount]); + // const growing = containerSize > (previousContainerSize ?? 0); + // if (growing) { + // } else { + // setVisibleCountGuessRange([0, visibleCount]); + // } + // guessVisibleCount(); + }, [ + containerSize, + guessVisibleCount, + previousContainerSize, + visibleCount, + visibleCountGuessRange, + ]); + const mergedRefs = useMergedRefs(containerRef, resizeRef); return [mergedRefs, visibleCount] as const; diff --git a/playgrounds/next/pages/index.tsx b/playgrounds/next/pages/index.tsx index 43668d8af0b..9d47847a5be 100644 --- a/playgrounds/next/pages/index.tsx +++ b/playgrounds/next/pages/index.tsx @@ -1,9 +1,31 @@ -import { Button } from '@itwin/itwinui-react'; +import { ComboBox } from '@itwin/itwinui-react'; + +export default function App() { + const data: { label: string; value: number }[] = []; + for (let i = 0; i < 15; i++) { + data.push({ label: `option ${i}`, value: i }); + } + const widths = []; + for (let i = 0; i < 20; i++) { + widths.push(790 + i * 3); + } -export default function Home() { return ( <> - + {widths.slice(0, 1).map((width) => ( + x.value)} + onChange={() => {}} + inputProps={{ + placeholder: 'Placeholder', + }} + /> + ))} ); } From b938917e03cfbd61ad4146a4b91d85527b2a9ed5 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Fri, 28 Jun 2024 13:04:52 -0400 Subject: [PATCH 04/65] Slight code cleanup --- .../src/utils/hooks/useOverflow.tsx | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx index 75088a7cd63..38b8c7a6da8 100644 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx +++ b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx @@ -8,9 +8,10 @@ import { useResizeObserver } from './useResizeObserver.js'; import { useLayoutEffect } from './useIsomorphicLayoutEffect.js'; import usePrevious from './usePrevious.js'; +/** `[number, number]` means that we're still guessing. `null` means that we got the correct `visibleCount`. */ type GuessRange = [number, number] | null; -/** First guess of the number of items that overflows. We refine this guess with subsequent renders */ +/** First guess of the number of items that overflows. We refine this guess with subsequent renders. */ const STARTING_MAX_ITEMS_COUNT = 32; /** @@ -62,6 +63,10 @@ export const useOverflow = ( const [visibleCountGuessRange, setVisibleCountGuessRange] = React.useState([0, initialVisibleCount]); + /** + * Call this function to guess the new `visibleCount`. + * The `visibleCount` is not changed if the correct `visibleCount` has already been found. + */ const guessVisibleCount = React.useCallback(() => { // Already stabilized if (visibleCountGuessRange == null) { @@ -87,7 +92,8 @@ export const useOverflow = ( visibleCount, }); - // // Firstly, the highest guess MUST be above the correct visibleCount value. If not, double the highest guess + // Firstly, the highest guess MUST be above the correct visibleCount value. If not, double the highest guess. + // i.e. the container should overflow when visibleCount = max guess. if (visibleCountGuessRange[1] === visibleCount && !isOverflowing) { setVisibleCountGuessRange([ visibleCountGuessRange[0], @@ -109,7 +115,7 @@ export const useOverflow = ( setVisibleCountGuessRange(newVisibleCountGuessRange); - // Always guess that the correct visibleCount is in the middle of the range + // Always guess that the correct visibleCount is in the middle of the new range setVisibleCount( Math.floor( (newVisibleCountGuessRange[0] + newVisibleCountGuessRange[1]) / 2, @@ -120,8 +126,6 @@ export const useOverflow = ( // TODO: Replace eslint-disable with proper listening to containerRef resize // eslint-disable-next-line react-hooks/exhaustive-deps useLayoutEffect(() => { - console.log('IN FIRST LOOP'); - if (!containerRef.current || disabled) { resizeObserverRef.current?.disconnect(); return; @@ -130,32 +134,28 @@ export const useOverflow = ( guessVisibleCount(); }); + // TODO: Better way to listen to containerSize changes instead of having containerSize in dep array. useLayoutEffect(() => { - if (visibleCountGuessRange != null) { - // No need to listen to resizes since we're already in the process of finding the correct visibleCount - return; - } - - // Only start re-guessing if containerSize changes *after* the containerSize is first set. - // This prevents unnecessary renders if ( + // No need to listen to resizes since we're already in the process of finding the correct visibleCount + visibleCountGuessRange != null || + // Only start re-guessing if containerSize changes *after* the containerSize is first set. + // This prevents unnecessary renders containerSize === previousContainerSize || previousContainerSize === -1 ) { return; } - console.log('containerSize changed', { - containerSize, - previousContainerSize, - }); - // Set the visibleCountGuessRange to again find the correct visibleCount; setVisibleCountGuessRange([0, visibleCount]); + + // TODO: Have better optimizations on resizing. // const growing = containerSize > (previousContainerSize ?? 0); // if (growing) { + // setVisibleCountGuessRange([visibleCount, visibleCount + 1]); // } else { - // setVisibleCountGuessRange([0, visibleCount]); + // setVisibleCountGuessRange([visibleCount - 2, visibleCount]); // } // guessVisibleCount(); }, [ From 4d426683add606351b33cea9495138fa6cc13d4e Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Tue, 2 Jul 2024 17:54:37 -0400 Subject: [PATCH 05/65] Tried to remove items prop. Better disabled handling. --- .../src/core/Breadcrumbs/Breadcrumbs.tsx | 2 +- .../src/core/ButtonGroup/ButtonGroup.tsx | 2 +- .../src/core/Select/SelectTagContainer.tsx | 2 +- .../src/core/Table/TablePaginator.tsx | 2 +- .../utils/components/MiddleTextTruncation.tsx | 5 +++-- .../src/utils/hooks/useOverflow.tsx | 20 ++++++++++--------- 6 files changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx index b5096ec1a77..fc5bd0a94b0 100644 --- a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx @@ -124,7 +124,7 @@ const BreadcrumbsComponent = React.forwardRef((props, ref) => { ...rest } = props; - const [overflowRef, visibleCount] = useOverflow(items); + const [overflowRef, visibleCount] = useOverflow(items.length); const refs = useMergedRefs(overflowRef, ref); return ( diff --git a/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx b/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx index 9d6eee4a7df..357b19e5d63 100644 --- a/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx +++ b/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx @@ -182,7 +182,7 @@ const OverflowGroup = React.forwardRef((props, forwardedRef) => { ); const [overflowRef, visibleCount] = useOverflow( - items, + items.length, !overflowButton, orientation, ); diff --git a/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx b/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx index 28ec34d33e5..00336400ff4 100644 --- a/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx +++ b/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx @@ -20,7 +20,7 @@ type SelectTagContainerProps = { export const SelectTagContainer = React.forwardRef((props, ref) => { const { tags, className, ...rest } = props; - const [containerRef, visibleCount] = useOverflow(tags); + const [containerRef, visibleCount] = useOverflow(tags.length); const refs = useMergedRefs(ref, containerRef); return ( diff --git a/packages/itwinui-react/src/core/Table/TablePaginator.tsx b/packages/itwinui-react/src/core/Table/TablePaginator.tsx index 8ef30a080a4..7196c350b75 100644 --- a/packages/itwinui-react/src/core/Table/TablePaginator.tsx +++ b/packages/itwinui-react/src/core/Table/TablePaginator.tsx @@ -197,7 +197,7 @@ export const TablePaginator = (props: TablePaginatorProps) => { .map((_, index) => pageButton(index)), [pageButton, totalPagesCount], ); - const [overflowRef, visibleCount] = useOverflow(pageList); + const [overflowRef, visibleCount] = useOverflow(pageList.length); const [paginatorResizeRef, paginatorWidth] = useContainerWidth(); diff --git a/packages/itwinui-react/src/utils/components/MiddleTextTruncation.tsx b/packages/itwinui-react/src/utils/components/MiddleTextTruncation.tsx index 41b18524fb4..9a6b689ae87 100644 --- a/packages/itwinui-react/src/utils/components/MiddleTextTruncation.tsx +++ b/packages/itwinui-react/src/utils/components/MiddleTextTruncation.tsx @@ -5,6 +5,7 @@ import * as React from 'react'; import { useOverflow } from '../hooks/useOverflow.js'; import type { CommonProps } from '../props.js'; +import { mergeRefs } from '../hooks/useMergedRefs.js'; const ELLIPSIS_CHAR = '…'; @@ -46,7 +47,7 @@ export type MiddleTextTruncationProps = { export const MiddleTextTruncation = (props: MiddleTextTruncationProps) => { const { text, endCharsCount = 6, textRenderer, style, ...rest } = props; - const [ref, visibleCount] = useOverflow(text); + const [ref, visibleCount] = useOverflow(text.length); const truncatedText = React.useMemo(() => { if (visibleCount < text.length) { @@ -68,7 +69,7 @@ export const MiddleTextTruncation = (props: MiddleTextTruncationProps) => { whiteSpace: 'nowrap', ...style, }} - ref={ref} + ref={mergeRefs(ref)} {...rest} > {textRenderer?.(truncatedText, text) ?? truncatedText} diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx index 38b8c7a6da8..b2c7aa249c4 100644 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx +++ b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx @@ -37,14 +37,16 @@ const STARTING_MAX_ITEMS_COUNT = 32; * ); */ export const useOverflow = ( - items: React.ReactNode[] | string, + // TODO: Try more to remove this prop, if possible. + itemsLength: number, disabled = false, orientation: 'horizontal' | 'vertical' = 'horizontal', ) => { const containerRef = React.useRef(null); - const initialVisibleCount = Math.min(items.length, STARTING_MAX_ITEMS_COUNT); - const [visibleCount, setVisibleCount] = React.useState(() => - disabled ? items.length : initialVisibleCount, + const initialVisibleCount = Math.min(itemsLength, STARTING_MAX_ITEMS_COUNT); + + const [visibleCount, setVisibleCount] = React.useState(() => + disabled ? itemsLength : initialVisibleCount, ); const [containerSize, setContainerSize] = React.useState(-1); @@ -61,15 +63,15 @@ export const useOverflow = ( const resizeObserverRef = React.useRef(observer); const [visibleCountGuessRange, setVisibleCountGuessRange] = - React.useState([0, initialVisibleCount]); + React.useState(disabled ? null : [0, initialVisibleCount]); /** * Call this function to guess the new `visibleCount`. * The `visibleCount` is not changed if the correct `visibleCount` has already been found. */ const guessVisibleCount = React.useCallback(() => { - // Already stabilized - if (visibleCountGuessRange == null) { + // If disabled or already stabilized + if (disabled || visibleCountGuessRange == null) { return; } @@ -121,12 +123,12 @@ export const useOverflow = ( (newVisibleCountGuessRange[0] + newVisibleCountGuessRange[1]) / 2, ), ); - }, [orientation, visibleCount, visibleCountGuessRange]); + }, [disabled, orientation, visibleCount, visibleCountGuessRange]); // TODO: Replace eslint-disable with proper listening to containerRef resize // eslint-disable-next-line react-hooks/exhaustive-deps useLayoutEffect(() => { - if (!containerRef.current || disabled) { + if (disabled || !containerRef.current) { resizeObserverRef.current?.disconnect(); return; } From d09c68d35e2715bb82f8b3e155f39969a9a9221a Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Tue, 2 Jul 2024 18:18:09 -0400 Subject: [PATCH 06/65] Fix infinite loop when no overflow --- .../src/utils/hooks/useOverflow.tsx | 54 ++++++++++++++----- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx index b2c7aa249c4..65ff9f1491c 100644 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx +++ b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx @@ -45,9 +45,24 @@ export const useOverflow = ( const containerRef = React.useRef(null); const initialVisibleCount = Math.min(itemsLength, STARTING_MAX_ITEMS_COUNT); - const [visibleCount, setVisibleCount] = React.useState(() => + const [visibleCount, _setVisibleCount] = React.useState(() => disabled ? itemsLength : initialVisibleCount, ); + const setVisibleCount = React.useCallback( + (newVisibleCount: React.SetStateAction) => { + _setVisibleCount((prev) => { + const safeVisibleCount = Math.min( + typeof newVisibleCount === 'function' + ? newVisibleCount(prev) + : newVisibleCount, + itemsLength, + ); + + return safeVisibleCount; + }); + }, + [itemsLength], + ); const [containerSize, setContainerSize] = React.useState(-1); const previousContainerSize = usePrevious(containerSize); @@ -75,13 +90,6 @@ export const useOverflow = ( return; } - // We have already found the correct visibleCount - if (visibleCountGuessRange[1] - visibleCountGuessRange[0] === 1) { - console.log('STABILIZED'); - setVisibleCountGuessRange(null); - return; - } - const dimension = orientation === 'horizontal' ? 'Width' : 'Height'; const availableSize = containerRef.current?.[`offset${dimension}`] ?? 0; const requiredSize = containerRef.current?.[`scroll${dimension}`] ?? 0; @@ -92,16 +100,27 @@ export const useOverflow = ( visibleCountGuessRange: visibleCountGuessRange.toString(), isOverflowing, visibleCount, + availableSize, + requiredSize, }); + // We have already found the correct visibleCount + if ( + (visibleCount === itemsLength && !isOverflowing) || + visibleCountGuessRange[1] - visibleCountGuessRange[0] === 1 + ) { + console.log('STABILIZED'); + setVisibleCountGuessRange(null); + return; + } + // Firstly, the highest guess MUST be above the correct visibleCount value. If not, double the highest guess. // i.e. the container should overflow when visibleCount = max guess. if (visibleCountGuessRange[1] === visibleCount && !isOverflowing) { - setVisibleCountGuessRange([ - visibleCountGuessRange[0], - visibleCountGuessRange[1] * 2, - ]); - setVisibleCount(visibleCountGuessRange[1] * 2); + const doubleOfMaxGuess = visibleCountGuessRange[1] * 2; + + setVisibleCountGuessRange([visibleCountGuessRange[0], doubleOfMaxGuess]); + setVisibleCount(doubleOfMaxGuess); return; } @@ -123,7 +142,14 @@ export const useOverflow = ( (newVisibleCountGuessRange[0] + newVisibleCountGuessRange[1]) / 2, ), ); - }, [disabled, orientation, visibleCount, visibleCountGuessRange]); + }, [ + disabled, + itemsLength, + orientation, + setVisibleCount, + visibleCount, + visibleCountGuessRange, + ]); // TODO: Replace eslint-disable with proper listening to containerRef resize // eslint-disable-next-line react-hooks/exhaustive-deps From c2f6b140a22f5760ba639b29587c530c5de9ac04 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Wed, 3 Jul 2024 12:06:46 -0400 Subject: [PATCH 07/65] =?UTF-8?q?Update=20min=20guess=20when=20doubling.?= =?UTF-8?q?=20=E2=80=A6to=20avoid=20guessing=20what=20we=20already=20know?= =?UTF-8?q?=20underflows.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/utils/hooks/useOverflow.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx index 65ff9f1491c..1924f4e3a8e 100644 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx +++ b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx @@ -12,7 +12,7 @@ import usePrevious from './usePrevious.js'; type GuessRange = [number, number] | null; /** First guess of the number of items that overflows. We refine this guess with subsequent renders. */ -const STARTING_MAX_ITEMS_COUNT = 32; +const STARTING_MAX_ITEMS_COUNT = 2; /** * Hook that observes the size of an element and returns the number of items @@ -114,29 +114,31 @@ export const useOverflow = ( return; } - // Firstly, the highest guess MUST be above the correct visibleCount value. If not, double the highest guess. - // i.e. the container should overflow when visibleCount = max guess. + // Before the main logic, the max guess MUST be above the correct visibleCount for the algorithm to work. + // If not: + // - double the max guess and visibleCount: since we need to overflow. + // - set min guess to current visibleCount: since underflow means correct visibleCount >= current visibleCount. if (visibleCountGuessRange[1] === visibleCount && !isOverflowing) { const doubleOfMaxGuess = visibleCountGuessRange[1] * 2; - setVisibleCountGuessRange([visibleCountGuessRange[0], doubleOfMaxGuess]); + setVisibleCountGuessRange([visibleCount, doubleOfMaxGuess]); setVisibleCount(doubleOfMaxGuess); return; } let newVisibleCountGuessRange = visibleCountGuessRange; - // overflowing = we guessed too high. So, new max guess = half the current guess if (isOverflowing) { + // overflowing = we guessed too high. So, new max guess = half the current guess newVisibleCountGuessRange = [visibleCountGuessRange[0], visibleCount]; } else { - // not overflowing = maybe we guessed too low. So, new min guess = half of current guess + // not overflowing = maybe we guessed too low? So, new min guess = half of current guess newVisibleCountGuessRange = [visibleCount, visibleCountGuessRange[1]]; } setVisibleCountGuessRange(newVisibleCountGuessRange); - // Always guess that the correct visibleCount is in the middle of the new range + // Next guess is always the middle of the new guess range setVisibleCount( Math.floor( (newVisibleCountGuessRange[0] + newVisibleCountGuessRange[1]) / 2, From 8418ab0e9da0e2213b1b45a53936a93374ba3977 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Wed, 3 Jul 2024 12:09:07 -0400 Subject: [PATCH 08/65] Remove optimization for resize --- .../src/utils/hooks/useOverflow.tsx | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx index 1924f4e3a8e..e6faa60b87c 100644 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx +++ b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx @@ -167,27 +167,18 @@ export const useOverflow = ( // TODO: Better way to listen to containerSize changes instead of having containerSize in dep array. useLayoutEffect(() => { if ( - // No need to listen to resizes since we're already in the process of finding the correct visibleCount + // No need to listen to resizes if we're already in the process of finding the correct visibleCount visibleCountGuessRange != null || - // Only start re-guessing if containerSize changes *after* the containerSize is first set. - // This prevents unnecessary renders + // Only start re-guessing if containerSize changes AFTER the containerSize is first set. + // This prevents unnecessary renders. containerSize === previousContainerSize || previousContainerSize === -1 ) { return; } - // Set the visibleCountGuessRange to again find the correct visibleCount; + // Reset the guess range to again start finding the correct visibleCount; setVisibleCountGuessRange([0, visibleCount]); - - // TODO: Have better optimizations on resizing. - // const growing = containerSize > (previousContainerSize ?? 0); - // if (growing) { - // setVisibleCountGuessRange([visibleCount, visibleCount + 1]); - // } else { - // setVisibleCountGuessRange([visibleCount - 2, visibleCount]); - // } - // guessVisibleCount(); }, [ containerSize, guessVisibleCount, From 8467fac72f79ff359e15033f7d3e19c6170a5667 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Wed, 3 Jul 2024 12:14:24 -0400 Subject: [PATCH 09/65] Fix incorrect reset --- packages/itwinui-react/src/utils/hooks/useOverflow.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx index e6faa60b87c..9923a1e2204 100644 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx +++ b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx @@ -178,10 +178,11 @@ export const useOverflow = ( } // Reset the guess range to again start finding the correct visibleCount; - setVisibleCountGuessRange([0, visibleCount]); + setVisibleCountGuessRange([0, initialVisibleCount]); }, [ containerSize, guessVisibleCount, + initialVisibleCount, previousContainerSize, visibleCount, visibleCountGuessRange, From 24f5957b8eed267faa2647264c71914992c34524 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Wed, 3 Jul 2024 16:44:33 -0400 Subject: [PATCH 10/65] Left a TODO from the debugging with edge cases --- packages/itwinui-react/src/utils/hooks/useOverflow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx index 9923a1e2204..9ad44cfb055 100644 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx +++ b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx @@ -107,7 +107,7 @@ export const useOverflow = ( // We have already found the correct visibleCount if ( (visibleCount === itemsLength && !isOverflowing) || - visibleCountGuessRange[1] - visibleCountGuessRange[0] === 1 + visibleCountGuessRange[1] - visibleCountGuessRange[0] === 1 // TODO: I think this causes issue when item count is 1 and so the initial range is [0, 1] ) { console.log('STABILIZED'); setVisibleCountGuessRange(null); From 1ac77c5d8e3ecfe5ddc44237681ea29848f4c499 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Wed, 3 Jul 2024 17:00:28 -0400 Subject: [PATCH 11/65] =?UTF-8?q?Avoid=20necessary=20reguessing=20on=20res?= =?UTF-8?q?ize=20=E2=80=A6when=20showing=20all=20items=20and=20still=20no?= =?UTF-8?q?=20overflowing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/utils/hooks/useOverflow.tsx | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx index 9ad44cfb055..d46a5a68b4b 100644 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx +++ b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx @@ -80,6 +80,14 @@ export const useOverflow = ( const [visibleCountGuessRange, setVisibleCountGuessRange] = React.useState(disabled ? null : [0, initialVisibleCount]); + const getIsOverflowing = React.useCallback(() => { + const dimension = orientation === 'horizontal' ? 'Width' : 'Height'; + const availableSize = containerRef.current?.[`offset${dimension}`] ?? 0; + const requiredSize = containerRef.current?.[`scroll${dimension}`] ?? 0; + + return availableSize < requiredSize; + }, [orientation]); + /** * Call this function to guess the new `visibleCount`. * The `visibleCount` is not changed if the correct `visibleCount` has already been found. @@ -90,12 +98,12 @@ export const useOverflow = ( return; } + const isOverflowing = getIsOverflowing(); + const dimension = orientation === 'horizontal' ? 'Width' : 'Height'; const availableSize = containerRef.current?.[`offset${dimension}`] ?? 0; const requiredSize = containerRef.current?.[`scroll${dimension}`] ?? 0; - const isOverflowing = availableSize < requiredSize; - console.log('RUNNING', { visibleCountGuessRange: visibleCountGuessRange.toString(), isOverflowing, @@ -146,6 +154,7 @@ export const useOverflow = ( ); }, [ disabled, + getIsOverflowing, itemsLength, orientation, setVisibleCount, @@ -167,6 +176,7 @@ export const useOverflow = ( // TODO: Better way to listen to containerSize changes instead of having containerSize in dep array. useLayoutEffect(() => { if ( + disabled || // No need to listen to resizes if we're already in the process of finding the correct visibleCount visibleCountGuessRange != null || // Only start re-guessing if containerSize changes AFTER the containerSize is first set. @@ -177,13 +187,26 @@ export const useOverflow = ( return; } + const isOverflowing = getIsOverflowing(); + + // If we're showing all the items in the list and still not overflowing, no need to do anything + if (visibleCount === itemsLength && !isOverflowing) { + return; + } + // Reset the guess range to again start finding the correct visibleCount; setVisibleCountGuessRange([0, initialVisibleCount]); + setVisibleCount(initialVisibleCount); }, [ containerSize, + disabled, + getIsOverflowing, guessVisibleCount, initialVisibleCount, + itemsLength, + orientation, previousContainerSize, + setVisibleCount, visibleCount, visibleCountGuessRange, ]); From 2b70f31a88ed25f2c7408733d209ac0440da6f47 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Fri, 5 Jul 2024 11:54:36 -0400 Subject: [PATCH 12/65] Trying the component approach --- apps/react-workshop/src/ComboBox.stories.tsx | 6 ++- .../src/core/Select/SelectTagContainer.tsx | 27 +++++++---- .../src/utils/hooks/useOverflow.tsx | 47 +++++++++++++++++++ playgrounds/vite/src/App.tsx | 2 +- 4 files changed, 71 insertions(+), 11 deletions(-) diff --git a/apps/react-workshop/src/ComboBox.stories.tsx b/apps/react-workshop/src/ComboBox.stories.tsx index ca78217a116..17bfeb02fa0 100644 --- a/apps/react-workshop/src/ComboBox.stories.tsx +++ b/apps/react-workshop/src/ComboBox.stories.tsx @@ -28,7 +28,11 @@ export default { } satisfies StoryDefault; const countriesList = [ - { label: 'Afghanistan', value: 'AF' }, + { + label: + 'Afghanistan awhdbvjabowndb hanwidb habnwidb awbnidb ajwbdhinb awdbnihba bwdbnhinab wbdbnianbw dnbaiwndbk hbnqbnw dbn awndj', + value: 'AF', + }, { label: 'Åland Islands', value: 'AX' }, { label: 'Albania', value: 'AL' }, { label: 'Algeria', value: 'DZ' }, diff --git a/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx b/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx index 00336400ff4..398e0481084 100644 --- a/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx +++ b/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx @@ -4,7 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as React from 'react'; import cx from 'classnames'; -import { useOverflow, useMergedRefs, Box } from '../../utils/index.js'; +import { + // useOverflow, + useMergedRefs, + // Box, + OverflowContainer, +} from '../../utils/index.js'; import type { PolymorphicForwardRefComponent } from '../../utils/index.js'; import { SelectTag } from './SelectTag.js'; @@ -20,21 +25,25 @@ type SelectTagContainerProps = { export const SelectTagContainer = React.forwardRef((props, ref) => { const { tags, className, ...rest } = props; - const [containerRef, visibleCount] = useOverflow(tags.length); - const refs = useMergedRefs(ref, containerRef); + // const [containerRef, visibleCount] = useOverflow(tags.length); + const refs = useMergedRefs(ref); return ( - ( + + )} className={cx('iui-select-tag-container', className)} ref={refs} {...rest} > - <> - {visibleCount < tags.length ? tags.slice(0, visibleCount - 1) : tags} + {/* <> */} + {tags} + {/* {visibleCount < tags.length ? tags.slice(0, visibleCount - 1) : tags} {visibleCount < tags.length && ( - )} - - + )} */} + {/* */} + ); }) as PolymorphicForwardRefComponent<'div', SelectTagContainerProps>; diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx index d46a5a68b4b..e47006ff091 100644 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx +++ b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx @@ -7,6 +7,8 @@ import { useMergedRefs } from './useMergedRefs.js'; import { useResizeObserver } from './useResizeObserver.js'; import { useLayoutEffect } from './useIsomorphicLayoutEffect.js'; import usePrevious from './usePrevious.js'; +import { Box } from '../components/Box.js'; +import type { PolymorphicForwardRefComponent } from '../props.js'; /** `[number, number]` means that we're still guessing. `null` means that we got the correct `visibleCount`. */ type GuessRange = [number, number] | null; @@ -215,3 +217,48 @@ export const useOverflow = ( return [mergedRefs, visibleCount] as const; }; + +// ---------------------------------------------------------------------------- + +type OverflowContainerProps = { + overflowTag: (visibleCount: number) => React.ReactNode; + /** + * Where the overflowTag is placed. Values: + * - end: At the end + * - center: After the first item + * @default 'end' + */ + overflowTagLocation?: 'center' | 'end'; + children: React.ReactNode[]; +}; + +export const OverflowContainer = React.forwardRef((props, ref) => { + const { overflowTag, overflowTagLocation = 'end', children, ...rest } = props; + + const [containerRef, visibleCount] = useOverflow(children.length); + + console.log('children', children.length, visibleCount); + + const itemsToRender = React.useMemo(() => { + if (overflowTagLocation === 'center') { + return [overflowTag(visibleCount), children.slice(visibleCount - 1)]; + } + return visibleCount < children.length + ? [children.slice(0, visibleCount - 1), overflowTag(visibleCount)] + : [children, []]; + }, [children, overflowTag, overflowTagLocation, visibleCount]); + + return ( + + {itemsToRender[0]} + {itemsToRender[1]} + {/* {visibleCount < children.length + ? children.slice(0, visibleCount - 1) + : children} + {visibleCount < children.length && + overflowTagLocation === 'end' && + // + overflowTag(visibleCount)} */} + + ); +}) as PolymorphicForwardRefComponent<'div', OverflowContainerProps>; diff --git a/playgrounds/vite/src/App.tsx b/playgrounds/vite/src/App.tsx index 9d47847a5be..e0d4c640c74 100644 --- a/playgrounds/vite/src/App.tsx +++ b/playgrounds/vite/src/App.tsx @@ -12,7 +12,7 @@ export default function App() { return ( <> - {widths.slice(0, 1).map((width) => ( + {widths.map((width) => ( Date: Fri, 5 Jul 2024 16:46:32 -0400 Subject: [PATCH 13/65] Working resize and tag location center --- .../src/Breadcrumbs.stories.tsx | 6 +- .../src/core/Breadcrumbs/Breadcrumbs.tsx | 60 +++++++-------- .../src/utils/hooks/useOverflow.tsx | 74 ++++++++++++++----- playgrounds/vite/src/App.tsx | 7 +- 4 files changed, 91 insertions(+), 56 deletions(-) diff --git a/apps/react-workshop/src/Breadcrumbs.stories.tsx b/apps/react-workshop/src/Breadcrumbs.stories.tsx index 2f0dab197eb..ad507b081c2 100644 --- a/apps/react-workshop/src/Breadcrumbs.stories.tsx +++ b/apps/react-workshop/src/Breadcrumbs.stories.tsx @@ -167,8 +167,10 @@ export const CustomOverflowDropdown = () => {
{ ...rest } = props; - const [overflowRef, visibleCount] = useOverflow(items.length); - const refs = useMergedRefs(overflowRef, ref); + // const [overflowRef, visibleCount] = useOverflow(items.length); + const [containerSize, setContainerSize] = React.useState(-1); + const [resizeRef] = useResizeObserver((size) => setContainerSize(size.width)); + + const overflowContainerRef = React.useRef(null); + const refs = useMergedRefs(ref, overflowContainerRef, resizeRef); return ( { aria-label='Breadcrumb' {...rest} > - - {visibleCount > 1 && ( - <> - - - - )} - {items.length - visibleCount > 0 && ( + ( <> {overflowButton ? ( @@ -156,30 +160,16 @@ const BreadcrumbsComponent = React.forwardRef((props, ref) => { )} - {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.map((_, index) => { + return ( + + + {index < items.length - 1 && } + + ); + })} + ); }) as PolymorphicForwardRefComponent<'nav', BreadcrumbsProps>; diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx index e47006ff091..5bb7beac55d 100644 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx +++ b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx @@ -43,8 +43,11 @@ export const useOverflow = ( itemsLength: number, disabled = false, orientation: 'horizontal' | 'vertical' = 'horizontal', + containerRefProp?: React.RefObject, ) => { - const containerRef = React.useRef(null); + const fallbackContainerRef = React.useRef(null); + const containerRef = containerRefProp ?? fallbackContainerRef; + const initialVisibleCount = Math.min(itemsLength, STARTING_MAX_ITEMS_COUNT); const [visibleCount, _setVisibleCount] = React.useState(() => @@ -88,7 +91,7 @@ export const useOverflow = ( const requiredSize = containerRef.current?.[`scroll${dimension}`] ?? 0; return availableSize < requiredSize; - }, [orientation]); + }, [containerRef, orientation]); /** * Call this function to guess the new `visibleCount`. @@ -155,6 +158,7 @@ export const useOverflow = ( ), ); }, [ + containerRef, disabled, getIsOverflowing, itemsLength, @@ -230,35 +234,71 @@ type OverflowContainerProps = { */ overflowTagLocation?: 'center' | 'end'; children: React.ReactNode[]; + /** + * Use this optional prop when the `OverflowContainer` is not the overflowing container. + */ + containerRef?: React.RefObject; }; export const OverflowContainer = React.forwardRef((props, ref) => { - const { overflowTag, overflowTagLocation = 'end', children, ...rest } = props; - - const [containerRef, visibleCount] = useOverflow(children.length); + const { + overflowTag, + overflowTagLocation = 'end', + children, + containerRef, + ...rest + } = props; + + // TODO: Should this be children.length + 1? + // Because if there are 10 items and visibleCount is 10, + // how do we know whether to display 10 items vs 9 items and 1 overflow tag? + const [overflowContainerRef, visibleCount] = useOverflow( + children.length, + undefined, + undefined, + containerRef, + ); console.log('children', children.length, visibleCount); const itemsToRender = React.useMemo(() => { + if (visibleCount >= children.length) { + return [children, [], []]; + } + + // TODO: Fix some off by one errors. It is visible when visibleCount = children.length - 1 if (overflowTagLocation === 'center') { - return [overflowTag(visibleCount), children.slice(visibleCount - 1)]; + return visibleCount >= 3 + ? [ + children[0], + overflowTag(visibleCount), + children.slice( + visibleCount > 1 + ? children.length - visibleCount + 1 + : children.length - 1, + ), + ] + : [ + [], + overflowTag(visibleCount - 1), + children.slice( + visibleCount > 1 + ? children.length - visibleCount + 1 + : children.length - 1, + ), + ]; } - return visibleCount < children.length - ? [children.slice(0, visibleCount - 1), overflowTag(visibleCount)] - : [children, []]; + return [children.slice(0, visibleCount - 1), [], overflowTag(visibleCount)]; }, [children, overflowTag, overflowTagLocation, visibleCount]); return ( - + {itemsToRender[0]} {itemsToRender[1]} - {/* {visibleCount < children.length - ? children.slice(0, visibleCount - 1) - : children} - {visibleCount < children.length && - overflowTagLocation === 'end' && - // - overflowTag(visibleCount)} */} + {itemsToRender[2]} ); }) as PolymorphicForwardRefComponent<'div', OverflowContainerProps>; diff --git a/playgrounds/vite/src/App.tsx b/playgrounds/vite/src/App.tsx index e0d4c640c74..b2c97fc2f23 100644 --- a/playgrounds/vite/src/App.tsx +++ b/playgrounds/vite/src/App.tsx @@ -3,7 +3,10 @@ import { ComboBox } from '@itwin/itwinui-react'; export default function App() { const data: { label: string; value: number }[] = []; for (let i = 0; i < 15; i++) { - data.push({ label: `option ${i}`, value: i }); + data.push({ + label: `option ${i} wdpokanwda jwdoman jwdoaniw jdoamnjw kdkokpainwjk dapiwnjk dkoanwjk dakomnwjk damnwj kdnamnjw kdnpmanjwk dawnjdk`, + value: i, + }); } const widths = []; for (let i = 0; i < 20; i++) { @@ -19,7 +22,7 @@ export default function App() { enableVirtualization={false} multiple={true} options={data} - value={data.map((x) => x.value)} + defaultValue={data.map((x) => x.value)} onChange={() => {}} inputProps={{ placeholder: 'Placeholder', From 7aafdc3a6273df129c9ff74b0f1c98a259d8b0f3 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Fri, 5 Jul 2024 17:03:10 -0400 Subject: [PATCH 14/65] Add resize to leftover overflow examples --- .../src/Breadcrumbs.stories.tsx | 160 ++++++++++-------- 1 file changed, 90 insertions(+), 70 deletions(-) diff --git a/apps/react-workshop/src/Breadcrumbs.stories.tsx b/apps/react-workshop/src/Breadcrumbs.stories.tsx index 2f0dab197eb..0f5847e4d8f 100644 --- a/apps/react-workshop/src/Breadcrumbs.stories.tsx +++ b/apps/react-workshop/src/Breadcrumbs.stories.tsx @@ -117,39 +117,49 @@ export const CustomOverflowBackButton = () => { )); return ( -
{ + const previousBreadcrumb = + visibleCount > 1 ? items.length - visibleCount : items.length - 2; + return ( + + { + console.log(`Visit breadcrumb ${previousBreadcrumb}`); + }} + styleType='borderless' + > + + + + ); }} > - { - const previousBreadcrumb = - visibleCount > 1 ? items.length - visibleCount : items.length - 2; - return ( - - { - console.log(`Visit breadcrumb ${previousBreadcrumb}`); - }} - styleType='borderless' - > - - - - ); - }} - > - {items} - -
+ {items} + ); }; +CustomOverflowBackButton.decorators = [ + (Story: () => React.ReactNode) => ( + <> + + Resize the container to see overflow behavior. + +
+ +
+ + ), +]; export const CustomOverflowDropdown = () => { const items = Array(10) @@ -164,50 +174,60 @@ export const CustomOverflowDropdown = () => { )); return ( -
- ( - - Array(items.length - visibleCount) - .fill(null) - .map((_, _index) => { - const index = visibleCount > 1 ? _index + 1 : _index; - const onClick = () => { - console.log(`Visit breadcrumb ${index}`); - close(); - }; - return ( - - Item {index} - - ); - }) - } + ( + + Array(items.length - visibleCount) + .fill(null) + .map((_, _index) => { + const index = visibleCount > 1 ? _index + 1 : _index; + const onClick = () => { + console.log(`Visit breadcrumb ${index}`); + close(); + }; + return ( + + Item {index} + + ); + }) + } + > + console.log('Clicked on overflow icon')} + styleType='borderless' > - console.log('Clicked on overflow icon')} - styleType='borderless' - > - - - - )} - > - {items} - -
+ + + + )} + > + {items} + ); }; +CustomOverflowDropdown.decorators = [ + (Story: () => React.ReactNode) => ( + <> + + Resize the container to see overflow behavior. + +
+ +
+ + ), +]; export const FolderNavigation = () => { const items = useMemo( From ffcb17e02eeebab0d74ebc278a1270917d8fdb13 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Fri, 5 Jul 2024 17:27:07 -0400 Subject: [PATCH 15/65] Bring back image tests --- ...umbs.test.ts-Custom Overflow Back Button.png | Bin 0 -> 13962 bytes ...dcrumbs.test.ts-Custom Overflow Dropdown.png | Bin 0 -> 16050 bytes apps/react-workshop/src/Breadcrumbs.test.ts | 10 ++++++++-- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100755 apps/react-workshop/cypress-visual-screenshots/baseline/Breadcrumbs.test.ts-Custom Overflow Back Button.png create mode 100755 apps/react-workshop/cypress-visual-screenshots/baseline/Breadcrumbs.test.ts-Custom Overflow Dropdown.png diff --git a/apps/react-workshop/cypress-visual-screenshots/baseline/Breadcrumbs.test.ts-Custom Overflow Back Button.png b/apps/react-workshop/cypress-visual-screenshots/baseline/Breadcrumbs.test.ts-Custom Overflow Back Button.png new file mode 100755 index 0000000000000000000000000000000000000000..28b645e59f2c2fe8d80c2b490a49159a557a10eb GIT binary patch literal 13962 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 OiMXMulzrXE@BaX-RBvwp literal 0 HcmV?d00001 diff --git a/apps/react-workshop/cypress-visual-screenshots/baseline/Breadcrumbs.test.ts-Custom Overflow Dropdown.png b/apps/react-workshop/cypress-visual-screenshots/baseline/Breadcrumbs.test.ts-Custom Overflow Dropdown.png new file mode 100755 index 0000000000000000000000000000000000000000..7c6268af2604b1fb76ce38f962bf32d79cf1fc8f GIT binary patch literal 16050 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 { 'Folder Navigation', 'Links', 'Overflow', - // 'Custom Overflow Dropdown', // excluding because these fellas keep failing in CI - // 'Custom Overflow Back Button', + 'Custom Overflow Dropdown', + 'Custom Overflow Back Button', ]; tests.forEach((testName) => { @@ -23,6 +23,12 @@ describe('Breadcrumbs', () => { cy.get('small').hide(); } + if (testName === 'Custom Overflow Dropdown') { + cy.get('button').eq(1).click(); + } else if (testName === 'Custom Overflow Back Button') { + cy.get('button').eq(1).trigger('mouseenter'); + } + cy.compareSnapshot(testName); }); }); From c5789cc6bd328a8e87e2927867f98c340afcabc9 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Mon, 8 Jul 2024 07:29:46 -0400 Subject: [PATCH 16/65] =?UTF-8?q?Working,=20but=E2=80=A6,=20useOverflow=20?= =?UTF-8?q?doesn't=20listen=20to=20resize.=20=E2=80=A6will=20need=20to=20r?= =?UTF-8?q?emove=20containerRef=20as=20that's=20causing=20the=20effect=20t?= =?UTF-8?q?o=20run=20twice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Breadcrumbs.stories.tsx | 8 +- apps/react-workshop/src/ComboBox.stories.tsx | 28 +- .../src/core/Breadcrumbs/Breadcrumbs.tsx | 22 +- .../src/utils/hooks/useDebounce.ts | 64 +++++ .../src/utils/hooks/useOverflow.tsx | 271 +++++++++--------- .../src/utils/hooks/useResizeObserver.tsx | 3 + playgrounds/vite/src/App.tsx | 2 +- 7 files changed, 247 insertions(+), 151 deletions(-) create mode 100644 packages/itwinui-react/src/utils/hooks/useDebounce.ts diff --git a/apps/react-workshop/src/Breadcrumbs.stories.tsx b/apps/react-workshop/src/Breadcrumbs.stories.tsx index 0f5847e4d8f..7a749714334 100644 --- a/apps/react-workshop/src/Breadcrumbs.stories.tsx +++ b/apps/react-workshop/src/Breadcrumbs.stories.tsx @@ -149,8 +149,8 @@ CustomOverflowBackButton.decorators = [
{ export const MultipleSelect = () => { const options = React.useMemo(() => countriesList, []); const [selectedOptions, setSelectedOptions] = React.useState([ - 'CA', 'AX', + 'AL', + 'DZ', + 'AS', + 'AD', + 'AO', + 'AI', ]); return ( @@ -536,3 +542,23 @@ export const MultipleSelect = () => { /> ); }; +MultipleSelect.decorators = [ + (Story: () => React.ReactNode) => ( + <> + + Resize the container to see overflow behavior. + +
+ +
+ + ), +]; diff --git a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx index c9cc786f8f1..b4d770f5d5c 100644 --- a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx @@ -15,6 +15,7 @@ import { import type { PolymorphicForwardRefComponent } from '../../utils/index.js'; import { Button } from '../Buttons/Button.js'; import { Anchor } from '../Typography/Anchor.js'; +import { useDebounce } from '../../utils/hooks/useDebounce.js'; const logWarning = createWarningLogger(); @@ -126,8 +127,25 @@ const BreadcrumbsComponent = React.forwardRef((props, ref) => { } = props; // const [overflowRef, visibleCount] = useOverflow(items.length); - const [containerSize, setContainerSize] = React.useState(-1); - const [resizeRef] = useResizeObserver((size) => setContainerSize(size.width)); + const [_containerSize, setContainerSize] = React.useState(-1); + const [containerSize, _setContainerSize] = React.useState(_containerSize); + + useDebounce( + () => { + console.log('debounce', { old: containerSize, new: _containerSize }); + _setContainerSize(_containerSize); + }, + 2000, + [_containerSize], + ); + + const [resizeRef] = useResizeObserver((size) => { + // setTimeout(() => { + // console.log('KEY RESET'); + console.log('resize'); + setContainerSize(size.width); + // }, 1000); + }); const overflowContainerRef = React.useRef(null); const refs = useMergedRefs(ref, overflowContainerRef, resizeRef); diff --git a/packages/itwinui-react/src/utils/hooks/useDebounce.ts b/packages/itwinui-react/src/utils/hooks/useDebounce.ts new file mode 100644 index 00000000000..f7eedd56731 --- /dev/null +++ b/packages/itwinui-react/src/utils/hooks/useDebounce.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ +/* eslint-disable react-hooks/exhaustive-deps */ +/* eslint-disable @typescript-eslint/ban-types */ +import { useEffect, useCallback, useRef } from 'react'; +import type { DependencyList } from 'react'; + +export type UseDebounceReturn = [() => boolean | null, () => void]; + +// TODO: Delete this temp testing func from react-use +export function useDebounce( + fn: Function, + ms = 0, + deps: DependencyList = [], +): UseDebounceReturn { + const [isReady, cancel, reset] = useTimeoutFn(fn, ms); + + useEffect(reset, deps); + + return [isReady, cancel]; +} + +// ---------------------------------------------------------------------------- + +export type UseTimeoutFnReturn = [() => boolean | null, () => void, () => void]; + +export function useTimeoutFn(fn: Function, ms = 0): UseTimeoutFnReturn { + const ready = useRef(false); + const timeout = useRef>(); + const callback = useRef(fn); + + const isReady = useCallback(() => ready.current, []); + + const set = useCallback(() => { + ready.current = false; + timeout.current && clearTimeout(timeout.current); + + timeout.current = setTimeout(() => { + ready.current = true; + callback.current(); + }, ms); + }, [ms]); + + const clear = useCallback(() => { + ready.current = null; + timeout.current && clearTimeout(timeout.current); + }, []); + + // update ref when function changes + useEffect(() => { + callback.current = fn; + }, [fn]); + + // set on mount, clear on unmount + useEffect(() => { + set(); + + return clear; + }, [ms]); + + return [isReady, clear, set]; +} diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx index 5bb7beac55d..95b7a9a57d7 100644 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx +++ b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx @@ -4,11 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as React from 'react'; import { useMergedRefs } from './useMergedRefs.js'; -import { useResizeObserver } from './useResizeObserver.js'; import { useLayoutEffect } from './useIsomorphicLayoutEffect.js'; -import usePrevious from './usePrevious.js'; import { Box } from '../components/Box.js'; import type { PolymorphicForwardRefComponent } from '../props.js'; +// import usePrevious from './usePrevious.js'; /** `[number, number]` means that we're still guessing. `null` means that we got the correct `visibleCount`. */ type GuessRange = [number, number] | null; @@ -17,11 +16,12 @@ type GuessRange = [number, number] | null; const STARTING_MAX_ITEMS_COUNT = 2; /** - * Hook that observes the size of an element and returns the number of items - * that should be visible based on the size of the container element. + * Hook that returns the number of items that should be visible based on the size of the container element. * * The returned number should be used to render the element with fewer items. * + * This hook does not observe the size of an element. To listen to container size changes, consider `OverflowContainer`. + * * @private * @param items Items that this element contains. * @param disabled Set to true to disconnect the observer. @@ -44,6 +44,7 @@ export const useOverflow = ( disabled = false, orientation: 'horizontal' | 'vertical' = 'horizontal', containerRefProp?: React.RefObject, + minVisibleCount?: number, ) => { const fallbackContainerRef = React.useRef(null); const containerRef = containerRefProp ?? fallbackContainerRef; @@ -53,114 +54,121 @@ export const useOverflow = ( const [visibleCount, _setVisibleCount] = React.useState(() => disabled ? itemsLength : initialVisibleCount, ); + + /** + * Ensures that `minVisibleCount <= visibleCount <= itemsLength` + */ const setVisibleCount = React.useCallback( - (newVisibleCount: React.SetStateAction) => { + (setStateAction: React.SetStateAction) => { _setVisibleCount((prev) => { - const safeVisibleCount = Math.min( - typeof newVisibleCount === 'function' - ? newVisibleCount(prev) - : newVisibleCount, - itemsLength, - ); + const newVisibleCount = + typeof setStateAction === 'function' + ? setStateAction(prev) + : setStateAction; + + let safeVisibleCount = Math.min(newVisibleCount, itemsLength); + + if (minVisibleCount != null) { + safeVisibleCount = Math.max(safeVisibleCount, minVisibleCount); + } return safeVisibleCount; }); }, - [itemsLength], + [itemsLength, minVisibleCount], ); - const [containerSize, setContainerSize] = React.useState(-1); - const previousContainerSize = usePrevious(containerSize); - previousContainerSize; - - const updateContainerSize = React.useCallback( - ({ width, height }: DOMRectReadOnly) => - setContainerSize(orientation === 'horizontal' ? width : height), - [orientation], - ); - - const [resizeRef, observer] = useResizeObserver(updateContainerSize); - const resizeObserverRef = React.useRef(observer); - const [visibleCountGuessRange, setVisibleCountGuessRange] = React.useState(disabled ? null : [0, initialVisibleCount]); - const getIsOverflowing = React.useCallback(() => { - const dimension = orientation === 'horizontal' ? 'Width' : 'Height'; - const availableSize = containerRef.current?.[`offset${dimension}`] ?? 0; - const requiredSize = containerRef.current?.[`scroll${dimension}`] ?? 0; - - return availableSize < requiredSize; - }, [containerRef, orientation]); - /** * Call this function to guess the new `visibleCount`. * The `visibleCount` is not changed if the correct `visibleCount` has already been found. */ + const isGuessing = React.useRef(false); const guessVisibleCount = React.useCallback(() => { // If disabled or already stabilized - if (disabled || visibleCountGuessRange == null) { + if (disabled || visibleCountGuessRange == null || isGuessing.current) { return; } - const isOverflowing = getIsOverflowing(); - - const dimension = orientation === 'horizontal' ? 'Width' : 'Height'; - const availableSize = containerRef.current?.[`offset${dimension}`] ?? 0; - const requiredSize = containerRef.current?.[`scroll${dimension}`] ?? 0; - - console.log('RUNNING', { - visibleCountGuessRange: visibleCountGuessRange.toString(), - isOverflowing, - visibleCount, - availableSize, - requiredSize, - }); - - // We have already found the correct visibleCount - if ( - (visibleCount === itemsLength && !isOverflowing) || - visibleCountGuessRange[1] - visibleCountGuessRange[0] === 1 // TODO: I think this causes issue when item count is 1 and so the initial range is [0, 1] - ) { - console.log('STABILIZED'); - setVisibleCountGuessRange(null); - return; - } + isGuessing.current = true; - // Before the main logic, the max guess MUST be above the correct visibleCount for the algorithm to work. - // If not: - // - double the max guess and visibleCount: since we need to overflow. - // - set min guess to current visibleCount: since underflow means correct visibleCount >= current visibleCount. - if (visibleCountGuessRange[1] === visibleCount && !isOverflowing) { - const doubleOfMaxGuess = visibleCountGuessRange[1] * 2; + try { + const dimension = orientation === 'horizontal' ? 'Width' : 'Height'; + const availableSize = containerRef.current?.[`offset${dimension}`] ?? 0; + const requiredSize = containerRef.current?.[`scroll${dimension}`] ?? 0; - setVisibleCountGuessRange([visibleCount, doubleOfMaxGuess]); - setVisibleCount(doubleOfMaxGuess); - return; - } + const isOverflowing = availableSize < requiredSize; - let newVisibleCountGuessRange = visibleCountGuessRange; + console.log('RUNNING', { + visibleCountGuessRange: visibleCountGuessRange.toString(), + isOverflowing, + visibleCount, + availableSize, + requiredSize, + }); - if (isOverflowing) { - // overflowing = we guessed too high. So, new max guess = half the current guess - newVisibleCountGuessRange = [visibleCountGuessRange[0], visibleCount]; - } else { - // not overflowing = maybe we guessed too low? So, new min guess = half of current guess - newVisibleCountGuessRange = [visibleCount, visibleCountGuessRange[1]]; + // We have already found the correct visibleCount + if ( + (visibleCount === itemsLength && !isOverflowing) || + visibleCountGuessRange[1] - visibleCountGuessRange[0] === 1 // TODO: I think this causes issue when item count is 1 and so the initial range is [0, 1] + ) { + console.log('STABILIZED'); + setVisibleCountGuessRange(null); + return; + } + + // Before the main logic, the max guess MUST be >= the correct visibleCount for the algorithm to work. + // If not: + // - double the max guess and visibleCount: since we need to overflow. + // - set min guess to current visibleCount: since underflow means correct visibleCount >= current visibleCount. + if (visibleCountGuessRange[1] === visibleCount && !isOverflowing) { + const doubleOfMaxGuess = visibleCountGuessRange[1] * 2; + + setVisibleCountGuessRange([visibleCount, doubleOfMaxGuess]); + setVisibleCount(doubleOfMaxGuess); + + // console.log('DOUBLING', { + // visibleCountGuessRange: visibleCountGuessRange.toString(), + // isOverflowing, + // visibleCount, + // availableSize, + // requiredSize, + // newRange: [visibleCount, doubleOfMaxGuess], + // newVisibleCount: doubleOfMaxGuess, + // }); + return; + } + + let newVisibleCountGuessRange = visibleCountGuessRange; + + if (isOverflowing) { + // overflowing = we guessed too high. So, new max guess = half the current guess + newVisibleCountGuessRange = [visibleCountGuessRange[0], visibleCount]; + } else { + // not overflowing = maybe we guessed too low? So, new min guess = half of current guess + newVisibleCountGuessRange = [visibleCount, visibleCountGuessRange[1]]; + } + + setVisibleCountGuessRange(newVisibleCountGuessRange); + + // Next guess is always the middle of the new guess range + setVisibleCount( + Math.floor( + (newVisibleCountGuessRange[0] + newVisibleCountGuessRange[1]) / 2, + ), + ); + } finally { + isGuessing.current = false; } - setVisibleCountGuessRange(newVisibleCountGuessRange); - - // Next guess is always the middle of the new guess range - setVisibleCount( - Math.floor( - (newVisibleCountGuessRange[0] + newVisibleCountGuessRange[1]) / 2, - ), - ); + // queueMicrotask(() => { + // guessVisibleCount(); + // }) }, [ containerRef, disabled, - getIsOverflowing, itemsLength, orientation, setVisibleCount, @@ -168,56 +176,24 @@ export const useOverflow = ( visibleCountGuessRange, ]); + // const guessVisibleCountCalled = React.useRef(false); + // TODO: Replace eslint-disable with proper listening to containerRef resize // eslint-disable-next-line react-hooks/exhaustive-deps useLayoutEffect(() => { - if (disabled || !containerRef.current) { - resizeObserverRef.current?.disconnect(); + // if (disabled || guessVisibleCountCalled.current) { + // return; + // } + // guessVisibleCountCalled.current = true; + if (disabled) { return; } guessVisibleCount(); + // }, [disabled, guessVisibleCount]); }); - // TODO: Better way to listen to containerSize changes instead of having containerSize in dep array. - useLayoutEffect(() => { - if ( - disabled || - // No need to listen to resizes if we're already in the process of finding the correct visibleCount - visibleCountGuessRange != null || - // Only start re-guessing if containerSize changes AFTER the containerSize is first set. - // This prevents unnecessary renders. - containerSize === previousContainerSize || - previousContainerSize === -1 - ) { - return; - } - - const isOverflowing = getIsOverflowing(); - - // If we're showing all the items in the list and still not overflowing, no need to do anything - if (visibleCount === itemsLength && !isOverflowing) { - return; - } - - // Reset the guess range to again start finding the correct visibleCount; - setVisibleCountGuessRange([0, initialVisibleCount]); - setVisibleCount(initialVisibleCount); - }, [ - containerSize, - disabled, - getIsOverflowing, - guessVisibleCount, - initialVisibleCount, - itemsLength, - orientation, - previousContainerSize, - setVisibleCount, - visibleCount, - visibleCountGuessRange, - ]); - - const mergedRefs = useMergedRefs(containerRef, resizeRef); + const mergedRefs = useMergedRefs(containerRef); return [mergedRefs, visibleCount] as const; }; @@ -238,6 +214,10 @@ type OverflowContainerProps = { * Use this optional prop when the `OverflowContainer` is not the overflowing container. */ containerRef?: React.RefObject; + /** + * The returned `visibleCount` will be >= `minVisibleCount` + */ + minVisibleCount?: number; }; export const OverflowContainer = React.forwardRef((props, ref) => { @@ -252,17 +232,22 @@ export const OverflowContainer = React.forwardRef((props, ref) => { // TODO: Should this be children.length + 1? // Because if there are 10 items and visibleCount is 10, // how do we know whether to display 10 items vs 9 items and 1 overflow tag? - const [overflowContainerRef, visibleCount] = useOverflow( - children.length, + const [overflowContainerRef, _visibleCount] = useOverflow( + children.length + 1, undefined, undefined, containerRef, ); + const visibleCount = _visibleCount; - console.log('children', children.length, visibleCount); + // console.log('children', children.length, visibleCount); + /** + * - `visibleCount === children.length + 1` means that we show all children and no overflow tag. + * - `visibleCount <= children.length` means that we show visibleCount - 1 children and 1 overflow tag. + */ const itemsToRender = React.useMemo(() => { - if (visibleCount >= children.length) { + if (visibleCount > children.length) { return [children, [], []]; } @@ -271,22 +256,22 @@ export const OverflowContainer = React.forwardRef((props, ref) => { return visibleCount >= 3 ? [ children[0], - overflowTag(visibleCount), - children.slice( - visibleCount > 1 - ? children.length - visibleCount + 1 - : children.length - 1, - ), - ] - : [ - [], overflowTag(visibleCount - 1), - children.slice( - visibleCount > 1 - ? children.length - visibleCount + 1 - : children.length - 1, - ), - ]; + children.slice(children.length - (visibleCount - 2)), + ] + : visibleCount === 2 + ? [ + [ + [], + overflowTag(2 - 1), + children.slice(children.length - (visibleCount - 1)), + ], + ] + : [ + [], + overflowTag(1 - 1), + children.slice(children.length - (visibleCount - 1)), + ]; } return [children.slice(0, visibleCount - 1), [], overflowTag(visibleCount)]; }, [children, overflowTag, overflowTagLocation, visibleCount]); diff --git a/packages/itwinui-react/src/utils/hooks/useResizeObserver.tsx b/packages/itwinui-react/src/utils/hooks/useResizeObserver.tsx index b08c7c9e4c9..6c66402f437 100644 --- a/packages/itwinui-react/src/utils/hooks/useResizeObserver.tsx +++ b/packages/itwinui-react/src/utils/hooks/useResizeObserver.tsx @@ -40,7 +40,10 @@ export const useResizeObserver = ( } const [{ contentRect }] = entries; + + // setTimeout(() => { return onResize(contentRect); + // }, 1000); }); }); resizeObserver.current?.observe?.(element); diff --git a/playgrounds/vite/src/App.tsx b/playgrounds/vite/src/App.tsx index b2c97fc2f23..401278043f6 100644 --- a/playgrounds/vite/src/App.tsx +++ b/playgrounds/vite/src/App.tsx @@ -4,7 +4,7 @@ export default function App() { const data: { label: string; value: number }[] = []; for (let i = 0; i < 15; i++) { data.push({ - label: `option ${i} wdpokanwda jwdoman jwdoaniw jdoamnjw kdkokpainwjk dapiwnjk dkoanwjk dakomnwjk damnwj kdnamnjw kdnpmanjwk dawnjdk`, + label: `option ${i}`, value: i, }); } From 7835190694b5a93e63d7f27416bb3fb66a9ef406 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Mon, 8 Jul 2024 15:17:30 -0400 Subject: [PATCH 17/65] =?UTF-8?q?Fixed=20the=20bug=20=E2=80=A6but=20could?= =?UTF-8?q?=20investigate=20why=20two=20guesses=20happen=20with=20no=20cha?= =?UTF-8?q?nge.=20i.e.=20double=20when=20called=20the=20first=20time=20doe?= =?UTF-8?q?sn't=20double=20the=20guess.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/core/Breadcrumbs/Breadcrumbs.tsx | 29 ++++--- .../src/core/ButtonGroup/ButtonGroup.tsx | 1 + .../src/core/Table/TablePaginator.tsx | 7 +- .../utils/components/MiddleTextTruncation.tsx | 7 +- .../src/utils/hooks/useOverflow.tsx | 86 +++++++++++++++++-- 5 files changed, 108 insertions(+), 22 deletions(-) diff --git a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx index b4d770f5d5c..2a09ccc6409 100644 --- a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx @@ -11,11 +11,12 @@ import { createWarningLogger, OverflowContainer, useResizeObserver, + // OverflowContainerContext, } from '../../utils/index.js'; import type { PolymorphicForwardRefComponent } from '../../utils/index.js'; import { Button } from '../Buttons/Button.js'; import { Anchor } from '../Typography/Anchor.js'; -import { useDebounce } from '../../utils/hooks/useDebounce.js'; +// import { useDebounce } from '../../utils/hooks/useDebounce.js'; const logWarning = createWarningLogger(); @@ -127,17 +128,17 @@ const BreadcrumbsComponent = React.forwardRef((props, ref) => { } = props; // const [overflowRef, visibleCount] = useOverflow(items.length); - const [_containerSize, setContainerSize] = React.useState(-1); - const [containerSize, _setContainerSize] = React.useState(_containerSize); + const [containerSize, setContainerSize] = React.useState(-1); + // const [containerSize, _setContainerSize] = React.useState(_containerSize); - useDebounce( - () => { - console.log('debounce', { old: containerSize, new: _containerSize }); - _setContainerSize(_containerSize); - }, - 2000, - [_containerSize], - ); + // useDebounce( + // () => { + // console.log('debounce', { old: containerSize, new: _containerSize }); + // _setContainerSize(_containerSize); + // }, + // 2000, + // [_containerSize], + // ); const [resizeRef] = useResizeObserver((size) => { // setTimeout(() => { @@ -158,6 +159,11 @@ const BreadcrumbsComponent = React.forwardRef((props, ref) => { aria-label='Breadcrumb' {...rest} > + {/* */} { ); })} + {/* */} ); }) as PolymorphicForwardRefComponent<'nav', BreadcrumbsProps>; diff --git a/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx b/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx index 357b19e5d63..c1538bca761 100644 --- a/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx +++ b/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx @@ -185,6 +185,7 @@ const OverflowGroup = React.forwardRef((props, forwardedRef) => { items.length, !overflowButton, orientation, + undefined, ); return ( diff --git a/packages/itwinui-react/src/core/Table/TablePaginator.tsx b/packages/itwinui-react/src/core/Table/TablePaginator.tsx index 7196c350b75..f2f1b1e0dc5 100644 --- a/packages/itwinui-react/src/core/Table/TablePaginator.tsx +++ b/packages/itwinui-react/src/core/Table/TablePaginator.tsx @@ -197,7 +197,12 @@ export const TablePaginator = (props: TablePaginatorProps) => { .map((_, index) => pageButton(index)), [pageButton, totalPagesCount], ); - const [overflowRef, visibleCount] = useOverflow(pageList.length); + const [overflowRef, visibleCount] = useOverflow( + pageList.length, + undefined, + undefined, + undefined, + ); const [paginatorResizeRef, paginatorWidth] = useContainerWidth(); diff --git a/packages/itwinui-react/src/utils/components/MiddleTextTruncation.tsx b/packages/itwinui-react/src/utils/components/MiddleTextTruncation.tsx index 9a6b689ae87..49efda2f8ba 100644 --- a/packages/itwinui-react/src/utils/components/MiddleTextTruncation.tsx +++ b/packages/itwinui-react/src/utils/components/MiddleTextTruncation.tsx @@ -47,7 +47,12 @@ export type MiddleTextTruncationProps = { export const MiddleTextTruncation = (props: MiddleTextTruncationProps) => { const { text, endCharsCount = 6, textRenderer, style, ...rest } = props; - const [ref, visibleCount] = useOverflow(text.length); + const [ref, visibleCount] = useOverflow( + text.length, + undefined, + undefined, + undefined, + ); const truncatedText = React.useMemo(() => { if (visibleCount < text.length) { diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx index 95b7a9a57d7..b8e459af193 100644 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx +++ b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx @@ -8,6 +8,8 @@ import { useLayoutEffect } from './useIsomorphicLayoutEffect.js'; import { Box } from '../components/Box.js'; import type { PolymorphicForwardRefComponent } from '../props.js'; // import usePrevious from './usePrevious.js'; +import { useLatestRef } from './useLatestRef.js'; +// import usePrevious from './usePrevious.js'; /** `[number, number]` means that we're still guessing. `null` means that we got the correct `visibleCount`. */ type GuessRange = [number, number] | null; @@ -38,16 +40,17 @@ const STARTING_MAX_ITEMS_COUNT = 2; *
* ); */ -export const useOverflow = ( +export const useOverflow = ( // TODO: Try more to remove this prop, if possible. itemsLength: number, disabled = false, orientation: 'horizontal' | 'vertical' = 'horizontal', - containerRefProp?: React.RefObject, + containerRefProp: React.RefObject | undefined, minVisibleCount?: number, ) => { - const fallbackContainerRef = React.useRef(null); - const containerRef = containerRefProp ?? fallbackContainerRef; + // const fallbackContainerRef = React.useRef(null); + const _containerRef = containerRefProp; + const containerRef = useLatestRef(_containerRef?.current); const initialVisibleCount = Math.min(itemsLength, STARTING_MAX_ITEMS_COUNT); @@ -94,16 +97,22 @@ export const useOverflow = ( isGuessing.current = true; + // We need to wait for the ref to be attached so that we can measure available and required sizes. + if (containerRef?.current == null) { + return; + } + try { const dimension = orientation === 'horizontal' ? 'Width' : 'Height'; - const availableSize = containerRef.current?.[`offset${dimension}`] ?? 0; - const requiredSize = containerRef.current?.[`scroll${dimension}`] ?? 0; + const availableSize = containerRef.current[`offset${dimension}`]; + const requiredSize = containerRef.current[`scroll${dimension}`]; const isOverflowing = availableSize < requiredSize; console.log('RUNNING', { visibleCountGuessRange: visibleCountGuessRange.toString(), - isOverflowing, + myRef: containerRef.current, + // isOverflowing, visibleCount, availableSize, requiredSize, @@ -138,6 +147,11 @@ export const useOverflow = ( // newRange: [visibleCount, doubleOfMaxGuess], // newVisibleCount: doubleOfMaxGuess, // }); + console.log( + 'doubling', + [visibleCount, doubleOfMaxGuess], + doubleOfMaxGuess, + ); return; } @@ -176,6 +190,48 @@ export const useOverflow = ( visibleCountGuessRange, ]); + // const previousVisibleCount = usePrevious(visibleCount); + // const previousVisibleCountGuessRange = usePrevious(visibleCountGuessRange); + // const previousContainerRef = usePrevious(containerRef); + + // useLayoutEffect(() => { + // if (disabled) { + // return; + // } + + // console.log( + // 'CHECKING', + // visibleCount !== previousVisibleCount, + // containerRef !== previousContainerRef, + // visibleCount, + // previousVisibleCount, + // containerRef, + // previousContainerRef, + // ); + + // if ( + // visibleCount !== previousVisibleCount || + // containerRef !== previousContainerRef + // // || + // // !!visibleCountGuessRange != !!previousVisibleCountGuessRange || + // // (visibleCountGuessRange != null && + // // previousVisibleCountGuessRange != null && + // // (visibleCountGuessRange[0] !== previousVisibleCountGuessRange[0] || + // // visibleCountGuessRange[1] !== previousVisibleCountGuessRange[1])) + // ) { + // guessVisibleCount(); + // } + // }, [ + // containerRef, + // disabled, + // guessVisibleCount, + // previousContainerRef, + // previousVisibleCount, + // previousVisibleCountGuessRange, + // visibleCount, + // visibleCountGuessRange, + // ]); + // const guessVisibleCountCalled = React.useRef(false); // TODO: Replace eslint-disable with proper listening to containerRef resize @@ -225,10 +281,13 @@ export const OverflowContainer = React.forwardRef((props, ref) => { overflowTag, overflowTagLocation = 'end', children, - containerRef, + containerRef: containerRefProp, ...rest } = props; + // const containerRef = React.useContext(OverflowContainerContext)?.containerRef; + const containerRef = containerRefProp; + // TODO: Should this be children.length + 1? // Because if there are 10 items and visibleCount is 10, // how do we know whether to display 10 items vs 9 items and 1 overflow tag? @@ -278,7 +337,10 @@ export const OverflowContainer = React.forwardRef((props, ref) => { return ( {itemsToRender[0]} @@ -287,3 +349,9 @@ export const OverflowContainer = React.forwardRef((props, ref) => { ); }) as PolymorphicForwardRefComponent<'div', OverflowContainerProps>; + +// ---------------------------------------------------------------------------- + +// export const OverflowContainerContext = React.createContext<{ +// containerRef: React.RefObject; +// } | null>(null); From f52b103bcf729496c598f14a3f59d4c11772976d Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Mon, 8 Jul 2024 16:10:44 -0400 Subject: [PATCH 18/65] Added `minVisibleCount` to `OverflowContainer` for `Breadcrumbs` --- .../src/core/Breadcrumbs/Breadcrumbs.tsx | 1 + .../src/utils/hooks/useOverflow.tsx | 19 +++++++------------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx index 2a09ccc6409..9d8c49ec9da 100644 --- a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx @@ -170,6 +170,7 @@ const BreadcrumbsComponent = React.forwardRef((props, ref) => { overflowTagLocation='center' containerRef={overflowContainerRef} className='iui-breadcrumbs-list' + minVisibleCount={2} overflowTag={(visibleCount) => ( <> diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx index b8e459af193..e3709ad73b0 100644 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx +++ b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx @@ -46,7 +46,6 @@ export const useOverflow = ( disabled = false, orientation: 'horizontal' | 'vertical' = 'horizontal', containerRefProp: React.RefObject | undefined, - minVisibleCount?: number, ) => { // const fallbackContainerRef = React.useRef(null); const _containerRef = containerRefProp; @@ -59,7 +58,7 @@ export const useOverflow = ( ); /** - * Ensures that `minVisibleCount <= visibleCount <= itemsLength` + * Ensures that `visibleCount <= itemsLength` */ const setVisibleCount = React.useCallback( (setStateAction: React.SetStateAction) => { @@ -69,16 +68,10 @@ export const useOverflow = ( ? setStateAction(prev) : setStateAction; - let safeVisibleCount = Math.min(newVisibleCount, itemsLength); - - if (minVisibleCount != null) { - safeVisibleCount = Math.max(safeVisibleCount, minVisibleCount); - } - - return safeVisibleCount; + return Math.min(newVisibleCount, itemsLength); }); }, - [itemsLength, minVisibleCount], + [itemsLength], ); const [visibleCountGuessRange, setVisibleCountGuessRange] = @@ -271,7 +264,8 @@ type OverflowContainerProps = { */ containerRef?: React.RefObject; /** - * The returned `visibleCount` will be >= `minVisibleCount` + * The number of items will always be >= `minVisibleCount` + * @default 1 */ minVisibleCount?: number; }; @@ -282,6 +276,7 @@ export const OverflowContainer = React.forwardRef((props, ref) => { overflowTagLocation = 'end', children, containerRef: containerRefProp, + minVisibleCount = 1, ...rest } = props; @@ -297,7 +292,7 @@ export const OverflowContainer = React.forwardRef((props, ref) => { undefined, containerRef, ); - const visibleCount = _visibleCount; + const visibleCount = Math.max(_visibleCount, minVisibleCount); // console.log('children', children.length, visibleCount); From 6f1acf1b2b8880afa6f229e58e13a3180635f722 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Mon, 8 Jul 2024 16:12:07 -0400 Subject: [PATCH 19/65] Generalized calculation --- .../src/utils/hooks/useOverflow.tsx | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx index e3709ad73b0..3a8755c8e3e 100644 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx +++ b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx @@ -306,6 +306,7 @@ export const OverflowContainer = React.forwardRef((props, ref) => { } // TODO: Fix some off by one errors. It is visible when visibleCount = children.length - 1 + // I think they are fixed. if (overflowTagLocation === 'center') { return visibleCount >= 3 ? [ @@ -313,19 +314,11 @@ export const OverflowContainer = React.forwardRef((props, ref) => { overflowTag(visibleCount - 1), children.slice(children.length - (visibleCount - 2)), ] - : visibleCount === 2 - ? [ - [ - [], - overflowTag(2 - 1), - children.slice(children.length - (visibleCount - 1)), - ], - ] - : [ - [], - overflowTag(1 - 1), - children.slice(children.length - (visibleCount - 1)), - ]; + : [ + [], + overflowTag(visibleCount - 1), + children.slice(children.length - (visibleCount - 1)), + ]; } return [children.slice(0, visibleCount - 1), [], overflowTag(visibleCount)]; }, [children, overflowTag, overflowTagLocation, visibleCount]); From 9be9adb405a5ba7c33936163f3338bb4c8edd57a Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Tue, 9 Jul 2024 09:14:52 -0400 Subject: [PATCH 20/65] Move OverflowContainer to a separate file --- .../utils/components/OverflowContainer.tsx | 94 ++++++++++++++++ .../src/utils/hooks/useOverflow.tsx | 100 ------------------ 2 files changed, 94 insertions(+), 100 deletions(-) create mode 100644 packages/itwinui-react/src/utils/components/OverflowContainer.tsx 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..9e13c50322c --- /dev/null +++ b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { useMergedRefs } from '../hooks/useMergedRefs.js'; +import { useOverflow } from '../hooks/useOverflow.js'; +import type { PolymorphicForwardRefComponent } from '../props.js'; +import { Box } from './Box.js'; + +type OverflowContainerProps = { + overflowTag: (visibleCount: number) => React.ReactNode; + /** + * Where the overflowTag is placed. Values: + * - end: At the end + * - center: After the first item + * @default 'end' + */ + overflowTagLocation?: 'center' | 'end'; + children: React.ReactNode[]; + /** + * Use this optional prop when the `OverflowContainer` is not the overflowing container. + */ + containerRef?: React.RefObject; + /** + * The number of items will always be >= `minVisibleCount` + * @default 1 + */ + minVisibleCount?: number; +}; + +export const OverflowContainer = React.forwardRef((props, ref) => { + const { + overflowTag, + overflowTagLocation = 'end', + children, + containerRef: containerRefProp, + minVisibleCount = 1, + ...rest + } = props; + + // const containerRef = React.useContext(OverflowContainerContext)?.containerRef; + const containerRef = containerRefProp; + + // TODO: Should this be children.length + 1? + // Because if there are 10 items and visibleCount is 10, + // how do we know whether to display 10 items vs 9 items and 1 overflow tag? + const [overflowContainerRef, _visibleCount] = useOverflow( + children.length + 1, + undefined, + undefined, + containerRef, + ); + const visibleCount = Math.max(_visibleCount, minVisibleCount); + + // console.log('children', children.length, visibleCount); + + /** + * - `visibleCount === children.length + 1` means that we show all children and no overflow tag. + * - `visibleCount <= children.length` means that we show visibleCount - 1 children and 1 overflow tag. + */ + const itemsToRender = React.useMemo(() => { + if (visibleCount > children.length) { + return [children, [], []]; + } + + // TODO: Fix some off by one errors. It is visible when visibleCount = children.length - 1 + // I think they are fixed. + if (overflowTagLocation === 'center') { + return visibleCount >= 3 + ? [ + children[0], + overflowTag(visibleCount - 1), + children.slice(children.length - (visibleCount - 2)), + ] + : [ + [], + overflowTag(visibleCount - 1), + children.slice(children.length - (visibleCount - 1)), + ]; + } + return [children.slice(0, visibleCount - 1), [], overflowTag(visibleCount)]; + }, [children, overflowTag, overflowTagLocation, visibleCount]); + + return ( + + {itemsToRender[0]} + {itemsToRender[1]} + {itemsToRender[2]} + + ); +}) as PolymorphicForwardRefComponent<'div', OverflowContainerProps>; diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx index 3a8755c8e3e..f23fba66974 100644 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx +++ b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx @@ -5,9 +5,6 @@ import * as React from 'react'; import { useMergedRefs } from './useMergedRefs.js'; import { useLayoutEffect } from './useIsomorphicLayoutEffect.js'; -import { Box } from '../components/Box.js'; -import type { PolymorphicForwardRefComponent } from '../props.js'; -// import usePrevious from './usePrevious.js'; import { useLatestRef } from './useLatestRef.js'; // import usePrevious from './usePrevious.js'; @@ -246,100 +243,3 @@ export const useOverflow = ( return [mergedRefs, visibleCount] as const; }; - -// ---------------------------------------------------------------------------- - -type OverflowContainerProps = { - overflowTag: (visibleCount: number) => React.ReactNode; - /** - * Where the overflowTag is placed. Values: - * - end: At the end - * - center: After the first item - * @default 'end' - */ - overflowTagLocation?: 'center' | 'end'; - children: React.ReactNode[]; - /** - * Use this optional prop when the `OverflowContainer` is not the overflowing container. - */ - containerRef?: React.RefObject; - /** - * The number of items will always be >= `minVisibleCount` - * @default 1 - */ - minVisibleCount?: number; -}; - -export const OverflowContainer = React.forwardRef((props, ref) => { - const { - overflowTag, - overflowTagLocation = 'end', - children, - containerRef: containerRefProp, - minVisibleCount = 1, - ...rest - } = props; - - // const containerRef = React.useContext(OverflowContainerContext)?.containerRef; - const containerRef = containerRefProp; - - // TODO: Should this be children.length + 1? - // Because if there are 10 items and visibleCount is 10, - // how do we know whether to display 10 items vs 9 items and 1 overflow tag? - const [overflowContainerRef, _visibleCount] = useOverflow( - children.length + 1, - undefined, - undefined, - containerRef, - ); - const visibleCount = Math.max(_visibleCount, minVisibleCount); - - // console.log('children', children.length, visibleCount); - - /** - * - `visibleCount === children.length + 1` means that we show all children and no overflow tag. - * - `visibleCount <= children.length` means that we show visibleCount - 1 children and 1 overflow tag. - */ - const itemsToRender = React.useMemo(() => { - if (visibleCount > children.length) { - return [children, [], []]; - } - - // TODO: Fix some off by one errors. It is visible when visibleCount = children.length - 1 - // I think they are fixed. - if (overflowTagLocation === 'center') { - return visibleCount >= 3 - ? [ - children[0], - overflowTag(visibleCount - 1), - children.slice(children.length - (visibleCount - 2)), - ] - : [ - [], - overflowTag(visibleCount - 1), - children.slice(children.length - (visibleCount - 1)), - ]; - } - return [children.slice(0, visibleCount - 1), [], overflowTag(visibleCount)]; - }, [children, overflowTag, overflowTagLocation, visibleCount]); - - return ( - - {itemsToRender[0]} - {itemsToRender[1]} - {itemsToRender[2]} - - ); -}) as PolymorphicForwardRefComponent<'div', OverflowContainerProps>; - -// ---------------------------------------------------------------------------- - -// export const OverflowContainerContext = React.createContext<{ -// containerRef: React.RefObject; -// } | null>(null); From 017d4c33fc1833ca6f9529cba4edf5d3c4497027 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Wed, 10 Jul 2024 11:29:16 -0400 Subject: [PATCH 21/65] Fix some errors --- packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx | 2 +- packages/itwinui-react/src/core/Select/SelectTagContainer.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx index 9d8c49ec9da..022abd8f40b 100644 --- a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx @@ -9,13 +9,13 @@ import { SvgChevronRight, Box, createWarningLogger, - OverflowContainer, useResizeObserver, // OverflowContainerContext, } from '../../utils/index.js'; import type { PolymorphicForwardRefComponent } from '../../utils/index.js'; import { Button } from '../Buttons/Button.js'; import { Anchor } from '../Typography/Anchor.js'; +import { OverflowContainer } from 'src/utils/components/OverflowContainer.js'; // import { useDebounce } from '../../utils/hooks/useDebounce.js'; const logWarning = createWarningLogger(); diff --git a/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx b/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx index 398e0481084..2249ca1951a 100644 --- a/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx +++ b/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx @@ -8,10 +8,10 @@ import { // useOverflow, useMergedRefs, // Box, - OverflowContainer, } from '../../utils/index.js'; import type { PolymorphicForwardRefComponent } from '../../utils/index.js'; import { SelectTag } from './SelectTag.js'; +import { OverflowContainer } from 'src/utils/components/OverflowContainer.js'; type SelectTagContainerProps = { /** From 88b20f9f6a30c6af2d8a76cc7d1b2ae8c7404a07 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Wed, 10 Jul 2024 11:58:39 -0400 Subject: [PATCH 22/65] Fix imports --- packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx | 2 +- packages/itwinui-react/src/core/Select/SelectTagContainer.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx index 022abd8f40b..f702b72abbd 100644 --- a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx @@ -15,7 +15,7 @@ import { import type { PolymorphicForwardRefComponent } from '../../utils/index.js'; import { Button } from '../Buttons/Button.js'; import { Anchor } from '../Typography/Anchor.js'; -import { OverflowContainer } from 'src/utils/components/OverflowContainer.js'; +import { OverflowContainer } from '../../utils/components/OverflowContainer.js'; // import { useDebounce } from '../../utils/hooks/useDebounce.js'; const logWarning = createWarningLogger(); diff --git a/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx b/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx index 2249ca1951a..1ebac13f253 100644 --- a/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx +++ b/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx @@ -11,7 +11,7 @@ import { } from '../../utils/index.js'; import type { PolymorphicForwardRefComponent } from '../../utils/index.js'; import { SelectTag } from './SelectTag.js'; -import { OverflowContainer } from 'src/utils/components/OverflowContainer.js'; +import { OverflowContainer } from '../../utils/components/OverflowContainer.js'; type SelectTagContainerProps = { /** From 864137701fb13965b7b986f9328fc94b6e5e2ceb Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Wed, 10 Jul 2024 16:01:06 -0400 Subject: [PATCH 23/65] Using elements instead of refs --- .../src/core/Select/SelectTagContainer.tsx | 4 +- .../utils/components/OverflowContainer.tsx | 12 +- .../src/utils/hooks/useOverflow.tsx | 138 +++++++----------- playgrounds/vite/src/App.tsx | 2 +- 4 files changed, 61 insertions(+), 95 deletions(-) diff --git a/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx b/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx index 1ebac13f253..893bc1de4ec 100644 --- a/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx +++ b/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx @@ -26,10 +26,12 @@ export const SelectTagContainer = React.forwardRef((props, ref) => { const { tags, className, ...rest } = props; // const [containerRef, visibleCount] = useOverflow(tags.length); - const refs = useMergedRefs(ref); + const [container, setContainer] = React.useState(); + const refs = useMergedRefs(ref, setContainer); return ( ( )} diff --git a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx index 9e13c50322c..fb4810df22f 100644 --- a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx +++ b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx @@ -17,7 +17,7 @@ type OverflowContainerProps = { /** * Use this optional prop when the `OverflowContainer` is not the overflowing container. */ - containerRef?: React.RefObject; + container: HTMLElement | undefined; /** * The number of items will always be >= `minVisibleCount` * @default 1 @@ -30,13 +30,13 @@ export const OverflowContainer = React.forwardRef((props, ref) => { overflowTag, overflowTagLocation = 'end', children, - containerRef: containerRefProp, + container: containerProp, minVisibleCount = 1, ...rest } = props; // const containerRef = React.useContext(OverflowContainerContext)?.containerRef; - const containerRef = containerRefProp; + const container = containerProp; // TODO: Should this be children.length + 1? // Because if there are 10 items and visibleCount is 10, @@ -45,8 +45,10 @@ export const OverflowContainer = React.forwardRef((props, ref) => { children.length + 1, undefined, undefined, - containerRef, + container, ); + overflowContainerRef; + const visibleCount = Math.max(_visibleCount, minVisibleCount); // console.log('children', children.length, visibleCount); @@ -82,7 +84,7 @@ export const OverflowContainer = React.forwardRef((props, ref) => { diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx index f23fba66974..7e686f9a1e4 100644 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx +++ b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx @@ -3,10 +3,8 @@ * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import * as React from 'react'; -import { useMergedRefs } from './useMergedRefs.js'; import { useLayoutEffect } from './useIsomorphicLayoutEffect.js'; -import { useLatestRef } from './useLatestRef.js'; -// import usePrevious from './usePrevious.js'; +import usePrevious from './usePrevious.js'; /** `[number, number]` means that we're still guessing. `null` means that we got the correct `visibleCount`. */ type GuessRange = [number, number] | null; @@ -19,8 +17,6 @@ const STARTING_MAX_ITEMS_COUNT = 2; * * The returned number should be used to render the element with fewer items. * - * This hook does not observe the size of an element. To listen to container size changes, consider `OverflowContainer`. - * * @private * @param items Items that this element contains. * @param disabled Set to true to disconnect the observer. @@ -42,12 +38,8 @@ export const useOverflow = ( itemsLength: number, disabled = false, orientation: 'horizontal' | 'vertical' = 'horizontal', - containerRefProp: React.RefObject | undefined, + container: HTMLElement | undefined, ) => { - // const fallbackContainerRef = React.useRef(null); - const _containerRef = containerRefProp; - const containerRef = useLatestRef(_containerRef?.current); - const initialVisibleCount = Math.min(itemsLength, STARTING_MAX_ITEMS_COUNT); const [visibleCount, _setVisibleCount] = React.useState(() => @@ -73,6 +65,7 @@ export const useOverflow = ( const [visibleCountGuessRange, setVisibleCountGuessRange] = React.useState(disabled ? null : [0, initialVisibleCount]); + const isStabilized = visibleCountGuessRange == null; /** * Call this function to guess the new `visibleCount`. @@ -81,27 +74,27 @@ export const useOverflow = ( const isGuessing = React.useRef(false); const guessVisibleCount = React.useCallback(() => { // If disabled or already stabilized - if (disabled || visibleCountGuessRange == null || isGuessing.current) { + if (disabled || isStabilized || isGuessing.current) { return; } - isGuessing.current = true; - // We need to wait for the ref to be attached so that we can measure available and required sizes. - if (containerRef?.current == null) { + if (container == null) { return; } try { + isGuessing.current = true; + const dimension = orientation === 'horizontal' ? 'Width' : 'Height'; - const availableSize = containerRef.current[`offset${dimension}`]; - const requiredSize = containerRef.current[`scroll${dimension}`]; + const availableSize = container[`offset${dimension}`]; + const requiredSize = container[`scroll${dimension}`]; const isOverflowing = availableSize < requiredSize; console.log('RUNNING', { visibleCountGuessRange: visibleCountGuessRange.toString(), - myRef: containerRef.current, + myRef: container, // isOverflowing, visibleCount, availableSize, @@ -128,20 +121,6 @@ export const useOverflow = ( setVisibleCountGuessRange([visibleCount, doubleOfMaxGuess]); setVisibleCount(doubleOfMaxGuess); - // console.log('DOUBLING', { - // visibleCountGuessRange: visibleCountGuessRange.toString(), - // isOverflowing, - // visibleCount, - // availableSize, - // requiredSize, - // newRange: [visibleCount, doubleOfMaxGuess], - // newVisibleCount: doubleOfMaxGuess, - // }); - console.log( - 'doubling', - [visibleCount, doubleOfMaxGuess], - doubleOfMaxGuess, - ); return; } @@ -166,13 +145,10 @@ export const useOverflow = ( } finally { isGuessing.current = false; } - - // queueMicrotask(() => { - // guessVisibleCount(); - // }) }, [ - containerRef, + container, disabled, + isStabilized, itemsLength, orientation, setVisibleCount, @@ -180,66 +156,52 @@ export const useOverflow = ( visibleCountGuessRange, ]); - // const previousVisibleCount = usePrevious(visibleCount); - // const previousVisibleCountGuessRange = usePrevious(visibleCountGuessRange); - // const previousContainerRef = usePrevious(containerRef); + const previousVisibleCount = usePrevious(visibleCount); + const previousVisibleCountGuessRange = usePrevious(visibleCountGuessRange); + const previousContainerRef = usePrevious(container); - // useLayoutEffect(() => { - // if (disabled) { - // return; - // } + useLayoutEffect(() => { + if (disabled) { + return; + } - // console.log( - // 'CHECKING', - // visibleCount !== previousVisibleCount, - // containerRef !== previousContainerRef, - // visibleCount, - // previousVisibleCount, - // containerRef, - // previousContainerRef, - // ); - - // if ( - // visibleCount !== previousVisibleCount || - // containerRef !== previousContainerRef - // // || - // // !!visibleCountGuessRange != !!previousVisibleCountGuessRange || - // // (visibleCountGuessRange != null && - // // previousVisibleCountGuessRange != null && - // // (visibleCountGuessRange[0] !== previousVisibleCountGuessRange[0] || - // // visibleCountGuessRange[1] !== previousVisibleCountGuessRange[1])) - // ) { - // guessVisibleCount(); - // } - // }, [ - // containerRef, - // disabled, - // guessVisibleCount, - // previousContainerRef, - // previousVisibleCount, - // previousVisibleCountGuessRange, - // visibleCount, - // visibleCountGuessRange, - // ]); + if ( + visibleCount !== previousVisibleCount || + container !== previousContainerRef || + isStabilized + ) { + guessVisibleCount(); + } + }, [ + container, + disabled, + guessVisibleCount, + isStabilized, + previousContainerRef, + previousVisibleCount, + previousVisibleCountGuessRange, + visibleCount, + visibleCountGuessRange, + ]); // const guessVisibleCountCalled = React.useRef(false); // TODO: Replace eslint-disable with proper listening to containerRef resize // eslint-disable-next-line react-hooks/exhaustive-deps - useLayoutEffect(() => { - // if (disabled || guessVisibleCountCalled.current) { - // return; - // } - // guessVisibleCountCalled.current = true; - if (disabled) { - return; - } + // useLayoutEffect(() => { + // // if (disabled || guessVisibleCountCalled.current) { + // // return; + // // } + // // guessVisibleCountCalled.current = true; + // if (disabled) { + // return; + // } - guessVisibleCount(); - // }, [disabled, guessVisibleCount]); - }); + // guessVisibleCount(); + // // }, [disabled, guessVisibleCount]); + // }); - const mergedRefs = useMergedRefs(containerRef); + // const mergedRefs = useMergedRefs(containerRef); - return [mergedRefs, visibleCount] as const; + return [React.useRef(container), visibleCount] as const; }; diff --git a/playgrounds/vite/src/App.tsx b/playgrounds/vite/src/App.tsx index 401278043f6..b4bc50954ed 100644 --- a/playgrounds/vite/src/App.tsx +++ b/playgrounds/vite/src/App.tsx @@ -15,7 +15,7 @@ export default function App() { return ( <> - {widths.map((width) => ( + {widths.slice(0, 1).map((width) => ( Date: Wed, 10 Jul 2024 17:36:14 -0400 Subject: [PATCH 24/65] useEffect doesn't run continuously --- .../utils/components/OverflowContainer.tsx | 2 +- .../src/utils/hooks/useOverflow.tsx | 78 ++++++++++++++----- 2 files changed, 58 insertions(+), 22 deletions(-) diff --git a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx index fb4810df22f..76b4a3a4675 100644 --- a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx +++ b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx @@ -47,7 +47,6 @@ export const OverflowContainer = React.forwardRef((props, ref) => { undefined, container, ); - overflowContainerRef; const visibleCount = Math.max(_visibleCount, minVisibleCount); @@ -84,6 +83,7 @@ export const OverflowContainer = React.forwardRef((props, ref) => { { - const initialVisibleCount = Math.min(itemsLength, STARTING_MAX_ITEMS_COUNT); + const initialVisibleCount = React.useMemo( + () => Math.min(itemsLength, STARTING_MAX_ITEMS_COUNT), + [itemsLength], + ); + const initialVisibleCountGuessRange = React.useMemo( + () => [0, initialVisibleCount] satisfies GuessRange, + [initialVisibleCount], + ); const [visibleCount, _setVisibleCount] = React.useState(() => disabled ? itemsLength : initialVisibleCount, @@ -63,8 +70,19 @@ export const useOverflow = ( [itemsLength], ); + const [resizeRef] = useResizeObserver( + React.useCallback( + (size) => { + console.log('RESET', size.width); + setVisibleCount(initialVisibleCount); + setVisibleCountGuessRange(initialVisibleCountGuessRange); + }, + [initialVisibleCount, initialVisibleCountGuessRange, setVisibleCount], + ), + ); + const [visibleCountGuessRange, setVisibleCountGuessRange] = - React.useState(disabled ? null : [0, initialVisibleCount]); + React.useState(disabled ? null : initialVisibleCountGuessRange); const isStabilized = visibleCountGuessRange == null; /** @@ -73,6 +91,15 @@ export const useOverflow = ( */ const isGuessing = React.useRef(false); const guessVisibleCount = React.useCallback(() => { + console.log('RUNNING', { + visibleCountGuessRange: visibleCountGuessRange?.toString(), + myRef: container, + // isOverflowing, + visibleCount, + // availableSize, + // requiredSize, + }); + // If disabled or already stabilized if (disabled || isStabilized || isGuessing.current) { return; @@ -92,15 +119,6 @@ export const useOverflow = ( const isOverflowing = availableSize < requiredSize; - console.log('RUNNING', { - visibleCountGuessRange: visibleCountGuessRange.toString(), - myRef: container, - // isOverflowing, - visibleCount, - availableSize, - requiredSize, - }); - // We have already found the correct visibleCount if ( (visibleCount === itemsLength && !isOverflowing) || @@ -156,20 +174,26 @@ export const useOverflow = ( visibleCountGuessRange, ]); - const previousVisibleCount = usePrevious(visibleCount); - const previousVisibleCountGuessRange = usePrevious(visibleCountGuessRange); - const previousContainerRef = usePrevious(container); + const previousVisibleCount = React.useRef(visibleCount); + const previousVisibleCountGuessRange = React.useRef(visibleCountGuessRange); + const previousContainer = React.useRef(container); useLayoutEffect(() => { - if (disabled) { + if (disabled || isStabilized) { return; } if ( - visibleCount !== previousVisibleCount || - container !== previousContainerRef || - isStabilized + visibleCount !== previousVisibleCount.current || + // TODO: Better list value comparison + visibleCountGuessRange.toString() !== + previousVisibleCountGuessRange.current?.toString() || + container !== previousContainer.current ) { + previousVisibleCount.current = visibleCount; + previousVisibleCountGuessRange.current = visibleCountGuessRange; + previousContainer.current = container; + guessVisibleCount(); } }, [ @@ -177,13 +201,25 @@ export const useOverflow = ( disabled, guessVisibleCount, isStabilized, - previousContainerRef, previousVisibleCount, previousVisibleCountGuessRange, visibleCount, visibleCountGuessRange, ]); + // React.useEffect(() => { + // console.log('size', containerSize, previousContainerSize); + + // if (containerSize != previousContainerSize) { + // setVisibleCountGuessRange(initialVisibleCountGuessRange); + // } + // }, [ + // containerSize, + // initialVisibleCount, + // initialVisibleCountGuessRange, + // previousContainerSize, + // ]); + // const guessVisibleCountCalled = React.useRef(false); // TODO: Replace eslint-disable with proper listening to containerRef resize @@ -203,5 +239,5 @@ export const useOverflow = ( // const mergedRefs = useMergedRefs(containerRef); - return [React.useRef(container), visibleCount] as const; + return [resizeRef, visibleCount] as const; }; From 6a77e1ca8a6805f0fd1512847287413501c7ca0d Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Wed, 10 Jul 2024 19:38:16 -0400 Subject: [PATCH 25/65] Working Breadcrumbs --- .../src/core/Breadcrumbs/Breadcrumbs.tsx | 34 +++++--------- .../utils/components/OverflowContainer.tsx | 20 +++++--- .../src/utils/hooks/useOverflow.tsx | 47 +++++++++++++------ 3 files changed, 56 insertions(+), 45 deletions(-) diff --git a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx index f702b72abbd..2949e2132cd 100644 --- a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx @@ -9,7 +9,6 @@ import { SvgChevronRight, Box, createWarningLogger, - useResizeObserver, // OverflowContainerContext, } from '../../utils/index.js'; import type { PolymorphicForwardRefComponent } from '../../utils/index.js'; @@ -127,29 +126,14 @@ const BreadcrumbsComponent = React.forwardRef((props, ref) => { ...rest } = props; - // const [overflowRef, visibleCount] = useOverflow(items.length); - const [containerSize, setContainerSize] = React.useState(-1); - // const [containerSize, _setContainerSize] = React.useState(_containerSize); + const [overflowContainer, setOverflowContainer] = + React.useState>(); - // useDebounce( - // () => { - // console.log('debounce', { old: containerSize, new: _containerSize }); - // _setContainerSize(_containerSize); - // }, - // 2000, - // [_containerSize], - // ); + const [myNum, setMyNum] = React.useState(0); - const [resizeRef] = useResizeObserver((size) => { - // setTimeout(() => { - // console.log('KEY RESET'); - console.log('resize'); - setContainerSize(size.width); - // }, 1000); - }); + console.log('overflowContainer', myNum, overflowContainer); - const overflowContainerRef = React.useRef(null); - const refs = useMergedRefs(ref, overflowContainerRef, resizeRef); + const refs = useMergedRefs(ref, overflowContainer); return ( { }} > */} { + console.log('called', ref); + setOverflowContainer(() => ref); + setMyNum((prev) => prev + 1); + }, [])} + // setContainerRef={setOverflowContainer} className='iui-breadcrumbs-list' minVisibleCount={2} overflowTag={(visibleCount) => ( diff --git a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx index 76b4a3a4675..e229ccf9a82 100644 --- a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx +++ b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx @@ -14,10 +14,11 @@ type OverflowContainerProps = { */ overflowTagLocation?: 'center' | 'end'; children: React.ReactNode[]; - /** - * Use this optional prop when the `OverflowContainer` is not the overflowing container. - */ - container: HTMLElement | undefined; + // /** + // * Use this optional prop when the `OverflowContainer` is not the overflowing container. + // */ + // container: HTMLElement | undefined; + setContainerRef: (containerRef: ReturnType[0]) => void; /** * The number of items will always be >= `minVisibleCount` * @default 1 @@ -30,13 +31,14 @@ export const OverflowContainer = React.forwardRef((props, ref) => { overflowTag, overflowTagLocation = 'end', children, - container: containerProp, + // container: containerProp, + setContainerRef, minVisibleCount = 1, ...rest } = props; // const containerRef = React.useContext(OverflowContainerContext)?.containerRef; - const container = containerProp; + // const container = containerProp; // TODO: Should this be children.length + 1? // Because if there are 10 items and visibleCount is 10, @@ -45,9 +47,13 @@ export const OverflowContainer = React.forwardRef((props, ref) => { children.length + 1, undefined, undefined, - container, ); + React.useEffect(() => { + console.log('useEffect'); + setContainerRef(overflowContainerRef); + }, [overflowContainerRef, setContainerRef]); + const visibleCount = Math.max(_visibleCount, minVisibleCount); // console.log('children', children.length, visibleCount); diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx index 56cb06be12c..139e2d17c60 100644 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx +++ b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx @@ -5,6 +5,8 @@ import * as React from 'react'; import { useLayoutEffect } from './useIsomorphicLayoutEffect.js'; import { useResizeObserver } from './useResizeObserver.js'; +import { useLatestRef } from './useLatestRef.js'; +import { useMergedRefs } from './useMergedRefs.js'; /** `[number, number]` means that we're still guessing. `null` means that we got the correct `visibleCount`. */ type GuessRange = [number, number] | null; @@ -33,13 +35,15 @@ const STARTING_MAX_ITEMS_COUNT = 2; *
* ); */ -export const useOverflow = ( +export const useOverflow = ( // TODO: Try more to remove this prop, if possible. itemsLength: number, disabled = false, orientation: 'horizontal' | 'vertical' = 'horizontal', - container: HTMLElement | undefined, + // container: HTMLElement | undefined, ) => { + const containerRef = React.useRef(null); + const initialVisibleCount = React.useMemo( () => Math.min(itemsLength, STARTING_MAX_ITEMS_COUNT), [itemsLength], @@ -93,7 +97,7 @@ export const useOverflow = ( const guessVisibleCount = React.useCallback(() => { console.log('RUNNING', { visibleCountGuessRange: visibleCountGuessRange?.toString(), - myRef: container, + myRef: containerRef, // isOverflowing, visibleCount, // availableSize, @@ -106,7 +110,7 @@ export const useOverflow = ( } // We need to wait for the ref to be attached so that we can measure available and required sizes. - if (container == null) { + if (containerRef.current == null) { return; } @@ -114,8 +118,8 @@ export const useOverflow = ( isGuessing.current = true; const dimension = orientation === 'horizontal' ? 'Width' : 'Height'; - const availableSize = container[`offset${dimension}`]; - const requiredSize = container[`scroll${dimension}`]; + const availableSize = containerRef.current[`offset${dimension}`]; + const requiredSize = containerRef.current[`scroll${dimension}`]; const isOverflowing = availableSize < requiredSize; @@ -164,7 +168,7 @@ export const useOverflow = ( isGuessing.current = false; } }, [ - container, + containerRef, disabled, isStabilized, itemsLength, @@ -174,33 +178,46 @@ export const useOverflow = ( visibleCountGuessRange, ]); - const previousVisibleCount = React.useRef(visibleCount); - const previousVisibleCountGuessRange = React.useRef(visibleCountGuessRange); - const previousContainer = React.useRef(container); + const isGuessCalledAtLeastOnce = React.useRef(false); + + const previousVisibleCount = useLatestRef(visibleCount); + const previousVisibleCountGuessRange = useLatestRef(visibleCountGuessRange); + const previousContainer = useLatestRef(containerRef.current); useLayoutEffect(() => { if (disabled || isStabilized) { return; } + console.log( + 'TRYING', + containerRef, + // visibleCount !== previousVisibleCount.current, + // // TODO: Better list value comparison + // visibleCountGuessRange.toString() !== + // previousVisibleCountGuessRange.current?.toString(), + // containerRef.current !== previousContainer.current, + ); if ( + !isGuessCalledAtLeastOnce.current || visibleCount !== previousVisibleCount.current || // TODO: Better list value comparison visibleCountGuessRange.toString() !== previousVisibleCountGuessRange.current?.toString() || - container !== previousContainer.current + containerRef.current !== previousContainer.current ) { + isGuessCalledAtLeastOnce.current = true; previousVisibleCount.current = visibleCount; previousVisibleCountGuessRange.current = visibleCountGuessRange; - previousContainer.current = container; + previousContainer.current = containerRef.current; guessVisibleCount(); } }, [ - container, disabled, guessVisibleCount, isStabilized, + previousContainer, previousVisibleCount, previousVisibleCountGuessRange, visibleCount, @@ -237,7 +254,7 @@ export const useOverflow = ( // // }, [disabled, guessVisibleCount]); // }); - // const mergedRefs = useMergedRefs(containerRef); + const mergedRefs = useMergedRefs(containerRef, resizeRef); - return [resizeRef, visibleCount] as const; + return [mergedRefs, visibleCount] as const; }; From f7383397ba1cc1150dfd765710a41e9409f200a4 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Wed, 10 Jul 2024 19:51:11 -0400 Subject: [PATCH 26/65] Cleanup --- .../src/core/Breadcrumbs/Breadcrumbs.tsx | 6 -- .../utils/components/OverflowContainer.tsx | 1 - .../src/utils/hooks/useOverflow.tsx | 56 ++----------------- 3 files changed, 4 insertions(+), 59 deletions(-) diff --git a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx index 2949e2132cd..55de63ba199 100644 --- a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx @@ -129,10 +129,6 @@ const BreadcrumbsComponent = React.forwardRef((props, ref) => { const [overflowContainer, setOverflowContainer] = React.useState>(); - const [myNum, setMyNum] = React.useState(0); - - console.log('overflowContainer', myNum, overflowContainer); - const refs = useMergedRefs(ref, overflowContainer); return ( @@ -152,9 +148,7 @@ const BreadcrumbsComponent = React.forwardRef((props, ref) => { as='ol' overflowTagLocation='center' setContainerRef={React.useCallback((ref) => { - console.log('called', ref); setOverflowContainer(() => ref); - setMyNum((prev) => prev + 1); }, [])} // setContainerRef={setOverflowContainer} className='iui-breadcrumbs-list' diff --git a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx index e229ccf9a82..82fc6b03713 100644 --- a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx +++ b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx @@ -50,7 +50,6 @@ export const OverflowContainer = React.forwardRef((props, ref) => { ); React.useEffect(() => { - console.log('useEffect'); setContainerRef(overflowContainerRef); }, [overflowContainerRef, setContainerRef]); diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx index 139e2d17c60..930dca3e98a 100644 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx +++ b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx @@ -75,14 +75,10 @@ export const useOverflow = ( ); const [resizeRef] = useResizeObserver( - React.useCallback( - (size) => { - console.log('RESET', size.width); - setVisibleCount(initialVisibleCount); - setVisibleCountGuessRange(initialVisibleCountGuessRange); - }, - [initialVisibleCount, initialVisibleCountGuessRange, setVisibleCount], - ), + React.useCallback(() => { + setVisibleCount(initialVisibleCount); + setVisibleCountGuessRange(initialVisibleCountGuessRange); + }, [initialVisibleCount, initialVisibleCountGuessRange, setVisibleCount]), ); const [visibleCountGuessRange, setVisibleCountGuessRange] = @@ -178,8 +174,6 @@ export const useOverflow = ( visibleCountGuessRange, ]); - const isGuessCalledAtLeastOnce = React.useRef(false); - const previousVisibleCount = useLatestRef(visibleCount); const previousVisibleCountGuessRange = useLatestRef(visibleCountGuessRange); const previousContainer = useLatestRef(containerRef.current); @@ -189,24 +183,13 @@ export const useOverflow = ( return; } - console.log( - 'TRYING', - containerRef, - // visibleCount !== previousVisibleCount.current, - // // TODO: Better list value comparison - // visibleCountGuessRange.toString() !== - // previousVisibleCountGuessRange.current?.toString(), - // containerRef.current !== previousContainer.current, - ); if ( - !isGuessCalledAtLeastOnce.current || visibleCount !== previousVisibleCount.current || // TODO: Better list value comparison visibleCountGuessRange.toString() !== previousVisibleCountGuessRange.current?.toString() || containerRef.current !== previousContainer.current ) { - isGuessCalledAtLeastOnce.current = true; previousVisibleCount.current = visibleCount; previousVisibleCountGuessRange.current = visibleCountGuessRange; previousContainer.current = containerRef.current; @@ -224,37 +207,6 @@ export const useOverflow = ( visibleCountGuessRange, ]); - // React.useEffect(() => { - // console.log('size', containerSize, previousContainerSize); - - // if (containerSize != previousContainerSize) { - // setVisibleCountGuessRange(initialVisibleCountGuessRange); - // } - // }, [ - // containerSize, - // initialVisibleCount, - // initialVisibleCountGuessRange, - // previousContainerSize, - // ]); - - // const guessVisibleCountCalled = React.useRef(false); - - // TODO: Replace eslint-disable with proper listening to containerRef resize - // eslint-disable-next-line react-hooks/exhaustive-deps - // useLayoutEffect(() => { - // // if (disabled || guessVisibleCountCalled.current) { - // // return; - // // } - // // guessVisibleCountCalled.current = true; - // if (disabled) { - // return; - // } - - // guessVisibleCount(); - // // }, [disabled, guessVisibleCount]); - // }); - const mergedRefs = useMergedRefs(containerRef, resizeRef); - return [mergedRefs, visibleCount] as const; }; From cce04e9e06f47fb80f1d6e08f2f54f0bd93e210b Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Wed, 10 Jul 2024 19:51:26 -0400 Subject: [PATCH 27/65] Fix jumping UI in the beginning --- .../itwinui-react/src/utils/components/OverflowContainer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx index 82fc6b03713..14aabf8ed4b 100644 --- a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx +++ b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx @@ -49,7 +49,7 @@ export const OverflowContainer = React.forwardRef((props, ref) => { undefined, ); - React.useEffect(() => { + React.useLayoutEffect(() => { setContainerRef(overflowContainerRef); }, [overflowContainerRef, setContainerRef]); From 15810397360d8dfa24396efd8f5598bb7d573787 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Thu, 11 Jul 2024 12:14:09 -0400 Subject: [PATCH 28/65] Easier solution than setting container from outside --- .../src/breadcrumbs/breadcrumbs.scss | 1 + .../src/core/Breadcrumbs/Breadcrumbs.tsx | 12 +--------- .../src/core/Select/SelectTagContainer.tsx | 11 ++-------- .../utils/components/OverflowContainer.tsx | 22 ++----------------- .../src/utils/hooks/useOverflow.tsx | 20 +++++++++-------- 5 files changed, 17 insertions(+), 49 deletions(-) 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 55de63ba199..fcd30957637 100644 --- a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx @@ -5,7 +5,6 @@ import * as React from 'react'; import cx from 'classnames'; import { - useMergedRefs, SvgChevronRight, Box, createWarningLogger, @@ -126,16 +125,11 @@ const BreadcrumbsComponent = React.forwardRef((props, ref) => { ...rest } = props; - const [overflowContainer, setOverflowContainer] = - React.useState>(); - - const refs = useMergedRefs(ref, overflowContainer); - return ( @@ -147,10 +141,6 @@ const BreadcrumbsComponent = React.forwardRef((props, ref) => { { - setOverflowContainer(() => ref); - }, [])} - // setContainerRef={setOverflowContainer} className='iui-breadcrumbs-list' minVisibleCount={2} overflowTag={(visibleCount) => ( diff --git a/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx b/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx index 893bc1de4ec..f04f282fd67 100644 --- a/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx +++ b/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx @@ -4,11 +4,6 @@ *--------------------------------------------------------------------------------------------*/ 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/components/OverflowContainer.js'; @@ -26,17 +21,15 @@ export const SelectTagContainer = React.forwardRef((props, ref) => { const { tags, className, ...rest } = props; // const [containerRef, visibleCount] = useOverflow(tags.length); - const [container, setContainer] = React.useState(); - const refs = useMergedRefs(ref, setContainer); return ( ( )} className={cx('iui-select-tag-container', className)} - ref={refs} + ref={ref} {...rest} > {/* <> */} diff --git a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx index 14aabf8ed4b..9b59895de7c 100644 --- a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx +++ b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx @@ -14,11 +14,6 @@ type OverflowContainerProps = { */ overflowTagLocation?: 'center' | 'end'; children: React.ReactNode[]; - // /** - // * Use this optional prop when the `OverflowContainer` is not the overflowing container. - // */ - // container: HTMLElement | undefined; - setContainerRef: (containerRef: ReturnType[0]) => void; /** * The number of items will always be >= `minVisibleCount` * @default 1 @@ -31,8 +26,6 @@ export const OverflowContainer = React.forwardRef((props, ref) => { overflowTag, overflowTagLocation = 'end', children, - // container: containerProp, - setContainerRef, minVisibleCount = 1, ...rest } = props; @@ -43,16 +36,12 @@ export const OverflowContainer = React.forwardRef((props, ref) => { // TODO: Should this be children.length + 1? // Because if there are 10 items and visibleCount is 10, // how do we know whether to display 10 items vs 9 items and 1 overflow tag? - const [overflowContainerRef, _visibleCount] = useOverflow( + const [containerRef, _visibleCount] = useOverflow( children.length + 1, undefined, undefined, ); - React.useLayoutEffect(() => { - setContainerRef(overflowContainerRef); - }, [overflowContainerRef, setContainerRef]); - const visibleCount = Math.max(_visibleCount, minVisibleCount); // console.log('children', children.length, visibleCount); @@ -85,14 +74,7 @@ export const OverflowContainer = React.forwardRef((props, ref) => { }, [children, overflowTag, overflowTagLocation, visibleCount]); return ( - + {itemsToRender[0]} {itemsToRender[1]} {itemsToRender[2]} diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx index 930dca3e98a..e22c56a5ea6 100644 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx +++ b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx @@ -91,15 +91,6 @@ export const useOverflow = ( */ const isGuessing = React.useRef(false); const guessVisibleCount = React.useCallback(() => { - console.log('RUNNING', { - visibleCountGuessRange: visibleCountGuessRange?.toString(), - myRef: containerRef, - // isOverflowing, - visibleCount, - // availableSize, - // requiredSize, - }); - // If disabled or already stabilized if (disabled || isStabilized || isGuessing.current) { return; @@ -119,6 +110,15 @@ export const useOverflow = ( const isOverflowing = availableSize < requiredSize; + console.log('RUNNING', { + visibleCountGuessRange: visibleCountGuessRange?.toString(), + containerRefNotNull: containerRef.current != null, + // isOverflowing, + visibleCount, + availableSize, + requiredSize, + }); + // We have already found the correct visibleCount if ( (visibleCount === itemsLength && !isOverflowing) || @@ -208,5 +208,7 @@ export const useOverflow = ( ]); const mergedRefs = useMergedRefs(containerRef, resizeRef); + + // return [containerRef, resizeRef, visibleCount] as const; return [mergedRefs, visibleCount] as const; }; From 5afb58e4f09fce136f3560c8f1f8fbde27b07467 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Thu, 11 Jul 2024 12:50:08 -0400 Subject: [PATCH 29/65] Working MiddleTextTruncation --- apps/react-workshop/src/Select.stories.tsx | 4 + .../utils/components/MiddleTextTruncation.tsx | 50 ++++++----- .../utils/components/OverflowContainer.tsx | 83 +++++++++++++------ 3 files changed, 90 insertions(+), 47 deletions(-) diff --git a/apps/react-workshop/src/Select.stories.tsx b/apps/react-workshop/src/Select.stories.tsx index 7b4e855a4e9..fef29eec6ae 100644 --- a/apps/react-workshop/src/Select.stories.tsx +++ b/apps/react-workshop/src/Select.stories.tsx @@ -199,6 +199,10 @@ export const TruncateMiddleText = () => { value={selectedValue} onChange={setSelectedValue} placeholder='Placeholder text' + style={{ + resize: 'inline', + overflow: 'hidden', + }} itemRenderer={(option) => ( */ +// TODO: Add React.forwardRef export const MiddleTextTruncation = (props: MiddleTextTruncationProps) => { const { text, endCharsCount = 6, textRenderer, style, ...rest } = props; - const [ref, visibleCount] = useOverflow( - text.length, - undefined, - undefined, - undefined, - ); + // const [ref, visibleCount] = useOverflow(text.length, undefined, undefined); + // console.log('visibleCount', visibleCount); + + console.log('RENDER'); - 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 truncatedText = React.useCallback( + (visibleCount: number) => { + 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], + ); return ( - { whiteSpace: 'nowrap', ...style, }} - ref={mergeRefs(ref)} + // ref={mergeRefs(ref)} + itemsLength={text.length} {...rest} > - {textRenderer?.(truncatedText, text) ?? truncatedText} - + {(visibleCount) => + textRenderer?.(truncatedText(visibleCount), text) ?? + truncatedText(visibleCount) + } + ); }; diff --git a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx index 9b59895de7c..cb0fed7c80c 100644 --- a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx +++ b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx @@ -5,7 +5,7 @@ import type { PolymorphicForwardRefComponent } from '../props.js'; import { Box } from './Box.js'; type OverflowContainerProps = { - overflowTag: (visibleCount: number) => React.ReactNode; + overflowTag?: (visibleCount: number) => React.ReactNode; /** * Where the overflowTag is placed. Values: * - end: At the end @@ -13,19 +13,28 @@ type OverflowContainerProps = { * @default 'end' */ overflowTagLocation?: 'center' | 'end'; - children: React.ReactNode[]; /** * The number of items will always be >= `minVisibleCount` * @default 1 */ minVisibleCount?: number; -}; +} & ( + | { + children: React.ReactNode[]; + itemsLength: undefined; + } + | { + children: (visibleCount: number) => React.ReactNode; + itemsLength: number; + } +); export const OverflowContainer = React.forwardRef((props, ref) => { const { overflowTag, overflowTagLocation = 'end', children, + itemsLength, minVisibleCount = 1, ...rest } = props; @@ -37,7 +46,9 @@ export const OverflowContainer = React.forwardRef((props, ref) => { // Because if there are 10 items and visibleCount is 10, // how do we know whether to display 10 items vs 9 items and 1 overflow tag? const [containerRef, _visibleCount] = useOverflow( - children.length + 1, + // TODO: Remove eslint-disable + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + typeof children === 'function' ? itemsLength! : children.length + 1, undefined, undefined, ); @@ -51,33 +62,55 @@ export const OverflowContainer = React.forwardRef((props, ref) => { * - `visibleCount <= children.length` means that we show visibleCount - 1 children and 1 overflow tag. */ const itemsToRender = React.useMemo(() => { - if (visibleCount > children.length) { - return [children, [], []]; - } + // let returnValue: React.ReactNode[] | null = null; + + // TODO: Is this try-catch needed? + try { + if (typeof children === 'function') { + throw null; + } + + if (visibleCount > children.length) { + throw [children, [], []]; + } - // TODO: Fix some off by one errors. It is visible when visibleCount = children.length - 1 - // I think they are fixed. - if (overflowTagLocation === 'center') { - return visibleCount >= 3 - ? [ - children[0], - overflowTag(visibleCount - 1), - children.slice(children.length - (visibleCount - 2)), - ] - : [ - [], - overflowTag(visibleCount - 1), - children.slice(children.length - (visibleCount - 1)), - ]; + // TODO: Fix some off by one errors. It is visible when visibleCount = children.length - 1 + // I think they are fixed. + if (overflowTagLocation === 'center') { + throw visibleCount >= 3 + ? [ + children[0], + overflowTag?.(visibleCount - 1), + children.slice(children.length - (visibleCount - 2)), + ] + : [ + [], + overflowTag?.(visibleCount - 1), + children.slice(children.length - (visibleCount - 1)), + ]; + } + throw [ + children.slice(0, visibleCount - 1), + [], + overflowTag?.(visibleCount), + ]; + } catch (returnValue) { + console.log('returnValue', returnValue); + return returnValue?.filter(Boolean); } - return [children.slice(0, visibleCount - 1), [], overflowTag(visibleCount)]; }, [children, overflowTag, overflowTagLocation, visibleCount]); return ( - {itemsToRender[0]} - {itemsToRender[1]} - {itemsToRender[2]} + {typeof children === 'function' ? ( + children(visibleCount) + ) : ( + <> + {itemsToRender?.[0]} + {itemsToRender?.[1]} + {itemsToRender?.[2]} + + )} ); }) as PolymorphicForwardRefComponent<'div', OverflowContainerProps>; From 345d91332e996c08f6b9ed6c8c81a42f7c76b58c Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Thu, 11 Jul 2024 17:35:49 -0400 Subject: [PATCH 30/65] Working for horizontal ButtonGroup --- .../src/ButtonGroup.stories.tsx | 3 ++ .../src/core/Breadcrumbs/Breadcrumbs.tsx | 2 +- .../src/core/ButtonGroup/ButtonGroup.tsx | 41 +++++++++++++------ .../utils/components/OverflowContainer.tsx | 20 ++++++--- 4 files changed, 47 insertions(+), 19 deletions(-) diff --git a/apps/react-workshop/src/ButtonGroup.stories.tsx b/apps/react-workshop/src/ButtonGroup.stories.tsx index 60e3eae2a90..c0bff9d9141 100644 --- a/apps/react-workshop/src/ButtonGroup.stories.tsx +++ b/apps/react-workshop/src/ButtonGroup.stories.tsx @@ -61,7 +61,10 @@ export const Overflow = () => { return ( { + console.log('overflowStart', overflowStart); + return ( { diff --git a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx index fcd30957637..27aa328764d 100644 --- a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx @@ -140,7 +140,7 @@ const BreadcrumbsComponent = React.forwardRef((props, ref) => { > */} ( diff --git a/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx b/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx index c1538bca761..43c5a965a6f 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 { useMergedRefs, Box } from '../../utils/index.js'; import type { AnyString, PolymorphicForwardRefComponent, @@ -14,6 +14,7 @@ import { CompositeItem, FloatingDelayGroup, } from '@floating-ui/react'; +import { OverflowContainer } from '../../utils/components/OverflowContainer.js'; // ---------------------------------------------------------------------------- @@ -181,15 +182,16 @@ const OverflowGroup = React.forwardRef((props, forwardedRef) => { [childrenProp], ); - const [overflowRef, visibleCount] = useOverflow( - items.length, - !overflowButton, - orientation, - undefined, - ); + // TODO: Add disabled (and maybe orientation) to OverflowContainer + // const [overflowRef, visibleCount] = useOverflow( + // items.length, + // !overflowButton, + // orientation, + // ); return ( - { }, props.className, )} - ref={useMergedRefs(forwardedRef, overflowRef)} + ref={useMergedRefs(forwardedRef)} + overflowTag={ + overflowButton != null + ? (visibleCount) => { + const firstOverflowingIndex = + overflowPlacement === 'start' + ? items.length - visibleCount - 2 + : visibleCount - 1; + + return overflowButton(firstOverflowingIndex); + } + : undefined + } + overflowPlacement={'start'} + itemsLength={undefined} // TODO: Why's it's forcing to add itemsLength? > - {(() => { + {items as React.ReactNode[]} + {/* {(() => { if (!(visibleCount < items.length)) { return items; } @@ -226,8 +243,8 @@ const OverflowGroup = React.forwardRef((props, forwardedRef) => { overflowButton(overflowStart)} ); - })()} - + })()} */} + ); }) as PolymorphicForwardRefComponent< 'div', diff --git a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx index cb0fed7c80c..654b3a775f5 100644 --- a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx +++ b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx @@ -5,6 +5,7 @@ import type { PolymorphicForwardRefComponent } from '../props.js'; import { Box } from './Box.js'; type OverflowContainerProps = { + // TODO: Confirm what happens when overflowTag === undefined. Maybe some off by 1 errors? overflowTag?: (visibleCount: number) => React.ReactNode; /** * Where the overflowTag is placed. Values: @@ -12,7 +13,7 @@ type OverflowContainerProps = { * - center: After the first item * @default 'end' */ - overflowTagLocation?: 'center' | 'end'; + overflowPlacement?: 'start' | 'center' | 'end'; /** * The number of items will always be >= `minVisibleCount` * @default 1 @@ -21,7 +22,7 @@ type OverflowContainerProps = { } & ( | { children: React.ReactNode[]; - itemsLength: undefined; + itemsLength?: undefined; } | { children: (visibleCount: number) => React.ReactNode; @@ -32,7 +33,7 @@ type OverflowContainerProps = { export const OverflowContainer = React.forwardRef((props, ref) => { const { overflowTag, - overflowTagLocation = 'end', + overflowPlacement = 'end', children, itemsLength, minVisibleCount = 1, @@ -76,7 +77,7 @@ export const OverflowContainer = React.forwardRef((props, ref) => { // TODO: Fix some off by one errors. It is visible when visibleCount = children.length - 1 // I think they are fixed. - if (overflowTagLocation === 'center') { + if (overflowPlacement === 'center') { throw visibleCount >= 3 ? [ children[0], @@ -89,16 +90,23 @@ export const OverflowContainer = React.forwardRef((props, ref) => { children.slice(children.length - (visibleCount - 1)), ]; } + + if (overflowPlacement === 'start') { + throw [ + overflowTag?.(visibleCount - 2), + children.slice(children.length - visibleCount + 1), + ]; + } + throw [ children.slice(0, visibleCount - 1), [], overflowTag?.(visibleCount), ]; } catch (returnValue) { - console.log('returnValue', returnValue); return returnValue?.filter(Boolean); } - }, [children, overflowTag, overflowTagLocation, visibleCount]); + }, [children, overflowTag, overflowPlacement, visibleCount]); return ( From e692a0f06b6a45ef2d470108ce316cb59b99b7ec Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Thu, 11 Jul 2024 18:15:10 -0400 Subject: [PATCH 31/65] Added disabled & orientation to OverflowContainer --- .../src/ButtonGroup.stories.tsx | 12 +++++- .../src/core/ButtonGroup/ButtonGroup.tsx | 37 ++----------------- .../utils/components/OverflowContainer.tsx | 16 +++++++- 3 files changed, 27 insertions(+), 38 deletions(-) diff --git a/apps/react-workshop/src/ButtonGroup.stories.tsx b/apps/react-workshop/src/ButtonGroup.stories.tsx index c0bff9d9141..7e55c5444e3 100644 --- a/apps/react-workshop/src/ButtonGroup.stories.tsx +++ b/apps/react-workshop/src/ButtonGroup.stories.tsx @@ -85,7 +85,10 @@ export const Overflow = () => { }); }} > - + console.log('Clicked on overflow icon')} + > @@ -173,6 +176,7 @@ export const VerticalOverflow = () => { .map((_, index) => ( console.log(`Clicked on button ${index + 1}`)} > @@ -185,6 +189,7 @@ export const VerticalOverflow = () => { style={{ height: 'clamp(100px, 40vmax, 80vh)' }} overflowButton={(overflowStart) => ( Array(buttons.length - overflowStart + 1) .fill(null) @@ -206,7 +211,10 @@ export const VerticalOverflow = () => { }) } > - console.log('Clicked on overflow icon')}> + console.log('Clicked on overflow icon')} + > diff --git a/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx b/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx index 43c5a965a6f..53b91a58904 100644 --- a/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx +++ b/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx @@ -182,16 +182,11 @@ const OverflowGroup = React.forwardRef((props, forwardedRef) => { [childrenProp], ); - // TODO: Add disabled (and maybe orientation) to OverflowContainer - // const [overflowRef, visibleCount] = useOverflow( - // items.length, - // !overflowButton, - // orientation, - // ); - return ( { overflowPlacement={'start'} itemsLength={undefined} // TODO: Why's it's forcing to add itemsLength? > - {items as React.ReactNode[]} - {/* {(() => { - 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)} - - ); - })()} */} + {items} ); }) as PolymorphicForwardRefComponent< diff --git a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx index 654b3a775f5..b42625898dc 100644 --- a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx +++ b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx @@ -19,6 +19,16 @@ type OverflowContainerProps = { * @default 1 */ minVisibleCount?: number; + /** + * If the overflow detection is disabled, the children will be returned as-is. + * @default true + */ + overflowDisabled?: boolean; + /** + * The orientation of the overflow in container. + * @default 'horizontal' + */ + overflowOrientation?: 'horizontal' | 'vertical'; } & ( | { children: React.ReactNode[]; @@ -36,6 +46,8 @@ export const OverflowContainer = React.forwardRef((props, ref) => { overflowPlacement = 'end', children, itemsLength, + overflowDisabled, + overflowOrientation, minVisibleCount = 1, ...rest } = props; @@ -50,8 +62,8 @@ export const OverflowContainer = React.forwardRef((props, ref) => { // TODO: Remove eslint-disable // eslint-disable-next-line @typescript-eslint/no-non-null-assertion typeof children === 'function' ? itemsLength! : children.length + 1, - undefined, - undefined, + overflowDisabled, + overflowOrientation, ); const visibleCount = Math.max(_visibleCount, minVisibleCount); From e48dce72e94b20ede7f6286d53fe68e2d7c3ef74 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Fri, 12 Jul 2024 07:02:52 -0400 Subject: [PATCH 32/65] =?UTF-8?q?Fixed=20incorrect=20ButtonGroup=20firstOv?= =?UTF-8?q?erflowIndex.=20=E2=80=A6=20when=20overflowLocation=3Dstart=20an?= =?UTF-8?q?d=20orientation=3Dvertical?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/ButtonGroup.stories.tsx | 67 ++++++++++--------- .../src/core/Breadcrumbs/Breadcrumbs.tsx | 2 +- .../src/core/ButtonGroup/ButtonGroup.tsx | 10 ++- .../src/core/Table/TablePaginator.tsx | 1 - .../utils/components/OverflowContainer.tsx | 12 ++-- 5 files changed, 53 insertions(+), 39 deletions(-) diff --git a/apps/react-workshop/src/ButtonGroup.stories.tsx b/apps/react-workshop/src/ButtonGroup.stories.tsx index 7e55c5444e3..c8e1d90790b 100644 --- a/apps/react-workshop/src/ButtonGroup.stories.tsx +++ b/apps/react-workshop/src/ButtonGroup.stories.tsx @@ -186,39 +186,44 @@ export const VerticalOverflow = () => { return ( ( - - Array(buttons.length - overflowStart + 1) - .fill(null) - .map((_, _index) => { - const index = overflowStart + _index; - const onClick = () => { - console.log(`Clicked button ${index} (overflow)`); - close(); - }; - return ( - } - > - Button #{index} - - ); - }) - } - > - console.log('Clicked on overflow icon')} + overflowButton={(overflowStart) => { + console.log('overflowStart', overflowStart); + + return ( + + Array(buttons.length - overflowStart + 1) + .fill(null) + .map((_, _index) => { + const index = overflowStart + _index; + const onClick = () => { + console.log(`Clicked button ${index} (overflow)`); + close(); + }; + return ( + } + > + Button #{index} + + ); + }) + } > - - - - )} + console.log('Clicked on overflow icon')} + > + + + + ); + }} > {buttons} diff --git a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx index 27aa328764d..8883c539a85 100644 --- a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx @@ -140,7 +140,7 @@ const BreadcrumbsComponent = React.forwardRef((props, ref) => { > */} ( diff --git a/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx b/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx index 53b91a58904..bee191c144b 100644 --- a/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx +++ b/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx @@ -187,6 +187,7 @@ const OverflowGroup = React.forwardRef((props, forwardedRef) => { as={BaseGroup} overflowDisabled={!overflowButton} overflowOrientation={orientation} + overflowLocation={overflowPlacement} orientation={orientation} {...rest} className={cx( @@ -205,11 +206,18 @@ const OverflowGroup = React.forwardRef((props, forwardedRef) => { ? items.length - visibleCount - 2 : visibleCount - 1; + console.log( + 'firstOverflowingIndex', + overflowPlacement === 'start', + firstOverflowingIndex, + items.length, + visibleCount, + ); + return overflowButton(firstOverflowingIndex); } : undefined } - overflowPlacement={'start'} itemsLength={undefined} // TODO: Why's it's forcing to add itemsLength? > {items} diff --git a/packages/itwinui-react/src/core/Table/TablePaginator.tsx b/packages/itwinui-react/src/core/Table/TablePaginator.tsx index f2f1b1e0dc5..b5f14866cf0 100644 --- a/packages/itwinui-react/src/core/Table/TablePaginator.tsx +++ b/packages/itwinui-react/src/core/Table/TablePaginator.tsx @@ -201,7 +201,6 @@ export const TablePaginator = (props: TablePaginatorProps) => { pageList.length, undefined, undefined, - undefined, ); const [paginatorResizeRef, paginatorWidth] = useContainerWidth(); diff --git a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx index b42625898dc..110fc6f304f 100644 --- a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx +++ b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx @@ -13,7 +13,7 @@ type OverflowContainerProps = { * - center: After the first item * @default 'end' */ - overflowPlacement?: 'start' | 'center' | 'end'; + overflowLocation?: 'start' | 'center' | 'end'; /** * The number of items will always be >= `minVisibleCount` * @default 1 @@ -43,7 +43,7 @@ type OverflowContainerProps = { export const OverflowContainer = React.forwardRef((props, ref) => { const { overflowTag, - overflowPlacement = 'end', + overflowLocation = 'end', children, itemsLength, overflowDisabled, @@ -83,13 +83,15 @@ export const OverflowContainer = React.forwardRef((props, ref) => { throw null; } + console.log('visibleCount', visibleCount); + if (visibleCount > children.length) { throw [children, [], []]; } // TODO: Fix some off by one errors. It is visible when visibleCount = children.length - 1 // I think they are fixed. - if (overflowPlacement === 'center') { + if (overflowLocation === 'center') { throw visibleCount >= 3 ? [ children[0], @@ -103,7 +105,7 @@ export const OverflowContainer = React.forwardRef((props, ref) => { ]; } - if (overflowPlacement === 'start') { + if (overflowLocation === 'start') { throw [ overflowTag?.(visibleCount - 2), children.slice(children.length - visibleCount + 1), @@ -118,7 +120,7 @@ export const OverflowContainer = React.forwardRef((props, ref) => { } catch (returnValue) { return returnValue?.filter(Boolean); } - }, [children, overflowTag, overflowPlacement, visibleCount]); + }, [children, overflowTag, overflowLocation, visibleCount]); return ( From b417c5e4e11d2d1cfc76fed4a342de1e0b59f882 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Fri, 12 Jul 2024 08:00:40 -0400 Subject: [PATCH 33/65] Working for TablePaginator --- apps/react-workshop/src/Table.stories.tsx | 3 +- .../src/core/Table/TablePaginator.tsx | 156 +++++++++--------- 2 files changed, 82 insertions(+), 77 deletions(-) diff --git a/apps/react-workshop/src/Table.stories.tsx b/apps/react-workshop/src/Table.stories.tsx index ec4f8cec2b7..2abd5602a92 100644 --- a/apps/react-workshop/src/Table.stories.tsx +++ b/apps/react-workshop/src/Table.stories.tsx @@ -1887,11 +1887,12 @@ export const WithPaginator = () => { emptyTableContent='No data.' isSelectable isSortable + // isLoading columns={columns} data={data} pageSize={50} paginatorRenderer={paginator} - style={{ height: '100%' }} + style={{ height: '100%', resize: 'inline', overflow: 'hidden' }} /> ); diff --git a/packages/itwinui-react/src/core/Table/TablePaginator.tsx b/packages/itwinui-react/src/core/Table/TablePaginator.tsx index b5f14866cf0..d8c26cfc38c 100644 --- a/packages/itwinui-react/src/core/Table/TablePaginator.tsx +++ b/packages/itwinui-react/src/core/Table/TablePaginator.tsx @@ -12,7 +12,6 @@ import { MenuItem } from '../Menu/MenuItem.js'; import { getBoundedValue, useGlobals, - useOverflow, useContainerWidth, SvgChevronLeft, SvgChevronRight, @@ -20,6 +19,7 @@ import { } from '../../utils/index.js'; import type { CommonProps } from '../../utils/index.js'; import type { TablePaginatorRendererProps } from './Table.js'; +import { OverflowContainer } from '../../utils/components/OverflowContainer.js'; const defaultLocalization = { pageSizeLabel: (size: number) => `${size} per page`, @@ -197,11 +197,6 @@ export const TablePaginator = (props: TablePaginatorProps) => { .map((_, index) => pageButton(index)), [pageButton, totalPagesCount], ); - const [overflowRef, visibleCount] = useOverflow( - pageList.length, - undefined, - undefined, - ); const [paginatorResizeRef, paginatorWidth] = useContainerWidth(); @@ -250,18 +245,6 @@ 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; - } - const hasNoRows = totalPagesCount === 0; const showPagesList = totalPagesCount > 1 || isLoading; const showPageSizeList = @@ -306,64 +289,85 @@ export const TablePaginator = (props: TablePaginatorProps) => { )} {showPagesList && ( - - onPageChange(currentPage - 1)} - size={buttonSize} - aria-label={localization.previousPage} - > - - - - {(() => { - if (hasNoRows) { - return noRowsContent; - } - if (visibleCount === 1) { - return pageButton(focusedIndex); - } - return ( - <> - {startPage !== 0 && ( - <> - {pageButton(0, 0)} - {ellipsis} - - )} - {pageList.slice(startPage, endPage)} - {endPage !== totalPagesCount && !isLoading && ( - <> - {ellipsis} - {pageButton(totalPagesCount - 1, 0)} - - )} - {isLoading && ( - <> - {ellipsis} - - - )} - - ); - })()} - - onPageChange(currentPage + 1)} - size={buttonSize} - aria-label={localization.nextPage} - > - - - + + {(visibleCount) => { + 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; + } + + return ( + <> + onPageChange(currentPage - 1)} + size={buttonSize} + aria-label={localization.previousPage} + > + + + + {(() => { + if (hasNoRows) { + return noRowsContent; + } + if (visibleCount === 1) { + return pageButton(focusedIndex); + } + return ( + <> + {startPage !== 0 && ( + <> + {pageButton(0, 0)} + {ellipsis} + + )} + {pageList.slice(startPage, endPage)} + {endPage !== totalPagesCount && !isLoading && ( + <> + {ellipsis} + {pageButton(totalPagesCount - 1, 0)} + + )} + {isLoading && ( + <> + {ellipsis} + + + )} + + ); + })()} + + onPageChange(currentPage + 1)} + size={buttonSize} + aria-label={localization.nextPage} + > + + + + ); + }} + )} {showPageSizeList && ( From cbf924c5301747eaee1df9f9c95e99425b2de06f Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Fri, 12 Jul 2024 08:03:55 -0400 Subject: [PATCH 34/65] Cleanup --- .../itwinui-react/src/core/Select/SelectTagContainer.tsx | 9 --------- .../src/utils/components/MiddleTextTruncation.tsx | 8 +------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx b/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx index f04f282fd67..c9bacea2f2b 100644 --- a/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx +++ b/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx @@ -20,11 +20,8 @@ type SelectTagContainerProps = { export const SelectTagContainer = React.forwardRef((props, ref) => { const { tags, className, ...rest } = props; - // const [containerRef, visibleCount] = useOverflow(tags.length); - return ( ( )} @@ -32,13 +29,7 @@ export const SelectTagContainer = React.forwardRef((props, ref) => { ref={ref} {...rest} > - {/* <> */} {tags} - {/* {visibleCount < tags.length ? tags.slice(0, visibleCount - 1) : tags} - {visibleCount < tags.length && ( - - )} */} - {/* */} ); }) as PolymorphicForwardRefComponent<'div', SelectTagContainerProps>; diff --git a/packages/itwinui-react/src/utils/components/MiddleTextTruncation.tsx b/packages/itwinui-react/src/utils/components/MiddleTextTruncation.tsx index 283701d88c8..30c1630d6b7 100644 --- a/packages/itwinui-react/src/utils/components/MiddleTextTruncation.tsx +++ b/packages/itwinui-react/src/utils/components/MiddleTextTruncation.tsx @@ -43,15 +43,10 @@ export type MiddleTextTruncationProps = { * )} * /> */ -// TODO: Add React.forwardRef +// TODO: Add React.forwardRef? export const MiddleTextTruncation = (props: MiddleTextTruncationProps) => { const { text, endCharsCount = 6, textRenderer, style, ...rest } = props; - // const [ref, visibleCount] = useOverflow(text.length, undefined, undefined); - // console.log('visibleCount', visibleCount); - - console.log('RENDER'); - const truncatedText = React.useCallback( (visibleCount: number) => { if (visibleCount < text.length) { @@ -76,7 +71,6 @@ export const MiddleTextTruncation = (props: MiddleTextTruncationProps) => { whiteSpace: 'nowrap', ...style, }} - // ref={mergeRefs(ref)} itemsLength={text.length} {...rest} > From 8e689b9c1674875f0af3e99e47410fa3e04d9cfb Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Fri, 12 Jul 2024 08:37:26 -0400 Subject: [PATCH 35/65] `overflowTag` is required if children is an array --- .../src/core/ButtonGroup/ButtonGroup.tsx | 48 +++++++++---------- .../utils/components/OverflowContainer.tsx | 22 +++++---- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx b/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx index bee191c144b..3925fb1a106 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 { useMergedRefs, Box } from '../../utils/index.js'; +import { Box } from '../../utils/index.js'; import type { AnyString, PolymorphicForwardRefComponent, @@ -168,6 +168,7 @@ const BaseGroup = React.forwardRef((props, forwardedRef) => { // ---------------------------------------------------------------------------- +/** Note: If `overflowButton == null`, it behaves like a `BaseGroup`. */ const OverflowGroup = React.forwardRef((props, forwardedRef) => { const { children: childrenProp, @@ -182,7 +183,7 @@ const OverflowGroup = React.forwardRef((props, forwardedRef) => { [childrenProp], ); - return ( + return overflowButton != null ? ( { }, props.className, )} - ref={useMergedRefs(forwardedRef)} - overflowTag={ - overflowButton != null - ? (visibleCount) => { - const firstOverflowingIndex = - overflowPlacement === 'start' - ? items.length - visibleCount - 2 - : visibleCount - 1; - - console.log( - 'firstOverflowingIndex', - overflowPlacement === 'start', - firstOverflowingIndex, - items.length, - visibleCount, - ); - - return overflowButton(firstOverflowingIndex); - } - : undefined - } - itemsLength={undefined} // TODO: Why's it's forcing to add itemsLength? + ref={forwardedRef} + overflowTag={(visibleCount) => { + const firstOverflowingIndex = + overflowPlacement === 'start' + ? items.length - visibleCount - 2 + : visibleCount - 1; + + console.log( + 'firstOverflowingIndex', + overflowPlacement === 'start', + firstOverflowingIndex, + items.length, + visibleCount, + ); + + return overflowButton(firstOverflowingIndex); + }} > {items} + ) : ( + + {childrenProp} + ); }) as PolymorphicForwardRefComponent< 'div', diff --git a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx index 110fc6f304f..155a6d8265f 100644 --- a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx +++ b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx @@ -5,17 +5,8 @@ import type { PolymorphicForwardRefComponent } from '../props.js'; import { Box } from './Box.js'; type OverflowContainerProps = { - // TODO: Confirm what happens when overflowTag === undefined. Maybe some off by 1 errors? - overflowTag?: (visibleCount: number) => React.ReactNode; /** - * Where the overflowTag is placed. Values: - * - end: At the end - * - center: After the first item - * @default 'end' - */ - overflowLocation?: 'start' | 'center' | 'end'; - /** - * The number of items will always be >= `minVisibleCount` + * The number of items (including the `overflowTag`, if passed) will always be `>= minVisibleCount`. * @default 1 */ minVisibleCount?: number; @@ -33,10 +24,21 @@ type OverflowContainerProps = { | { children: React.ReactNode[]; itemsLength?: undefined; + // TODO: Confirm what happens when overflowTag === undefined. Maybe some off by 1 errors? + overflowTag: (visibleCount: number) => React.ReactNode; + /** + * Where the overflowTag is placed. Values: + * - end: At the end + * - center: After the first item + * @default 'end' + */ + overflowLocation?: 'start' | 'center' | 'end'; } | { children: (visibleCount: number) => React.ReactNode; itemsLength: number; + overflowTag?: undefined; + overflowLocation?: undefined; } ); From 56612df9ad897fab94a026bcd6802753d92cc0df Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Fri, 12 Jul 2024 09:11:21 -0400 Subject: [PATCH 36/65] JSDocs --- .../src/core/Select/SelectTagContainer.tsx | 2 +- .../utils/components/OverflowContainer.tsx | 60 +++++++++++++++++-- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx b/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx index c9bacea2f2b..e0c0778ffb9 100644 --- a/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx +++ b/packages/itwinui-react/src/core/Select/SelectTagContainer.tsx @@ -23,7 +23,7 @@ export const SelectTagContainer = React.forwardRef((props, ref) => { return ( ( - + // -1 to account for the overflowTag )} className={cx('iui-select-tag-container', className)} ref={ref} diff --git a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx index 155a6d8265f..002a665936d 100644 --- a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx +++ b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx @@ -11,8 +11,9 @@ type OverflowContainerProps = { */ minVisibleCount?: number; /** - * If the overflow detection is disabled, the children will be returned as-is. - * @default true + * // TODO: What happens with overflowDisabled=true and children=function? + * If the overflow detection is disabled, visibleCount stays. + * @default false */ overflowDisabled?: boolean; /** @@ -23,13 +24,24 @@ type OverflowContainerProps = { } & ( | { children: React.ReactNode[]; + /** + * Number of items to display. Since overflow detection considers *all* children, `itemsLength` may need to + * account for the `overflowTag` depending on your implementation to prevent off-by-one errors. + * + * Required if `children: React.ReactNode[]`. + */ itemsLength?: undefined; - // TODO: Confirm what happens when overflowTag === undefined. Maybe some off by 1 errors? + /** + * What is rendered at `overflowLocation` when `OverflowContainer` starts overflowing. + * + * Required if `children: React.ReactNode[]`. + */ overflowTag: (visibleCount: number) => React.ReactNode; /** * Where the overflowTag is placed. Values: + * - start: At the start * - end: At the end - * - center: After the first item + * - center: After the first item and before all other items // TODO: Maybe remove this Breadcrumbs specific loc? * @default 'end' */ overflowLocation?: 'start' | 'center' | 'end'; @@ -42,13 +54,51 @@ type OverflowContainerProps = { } ); +/** + * Renders fewer children + an `overflowTag` when it starts overflowing. When not overflowing, it renders all children. + * This component listens to resize events and updates the rendered content accordingly. + * + * Two forms of usage: + * 1. `children: React.ReactNode[]`: Pass all the children and an `overflowTag` and this component handles when to show + * what, depending on whether the component is overflowing. + * 2. `children: (visibleCount: number) => React.ReactNode`: For more customization, pass a function to get the + * `visibleCount` and then render custom content based on that. + * + * @example + * ( + * +${tags.length - (visibleCount - 1)} item(s) // -1 to account for the overflowTag + * )} + * overflowLocation='start' + * > + * {items} + * + * + * @example + * + * {(visibleCount) => { + * // Custom content dependent on visibleCount + * return ( + * <> + * itemsLeft(visibleCount) + * overflowButton(visibleCount) + * itemsRight(visibleCount) + * + * ); + * } + * + */ export const OverflowContainer = React.forwardRef((props, ref) => { const { overflowTag, overflowLocation = 'end', children, itemsLength, - overflowDisabled, + overflowDisabled = false, overflowOrientation, minVisibleCount = 1, ...rest From d1e1a560f5f8dcf4fc49b166c7fa2e253651ad1b Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Fri, 12 Jul 2024 10:15:28 -0400 Subject: [PATCH 37/65] Remove unnecessary try-catch --- .../utils/components/OverflowContainer.tsx | 94 +++++++------------ 1 file changed, 36 insertions(+), 58 deletions(-) diff --git a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx index 002a665936d..dcf6d9b4ff0 100644 --- a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx +++ b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx @@ -104,12 +104,6 @@ export const OverflowContainer = React.forwardRef((props, ref) => { ...rest } = props; - // const containerRef = React.useContext(OverflowContainerContext)?.containerRef; - // const container = containerProp; - - // TODO: Should this be children.length + 1? - // Because if there are 10 items and visibleCount is 10, - // how do we know whether to display 10 items vs 9 items and 1 overflow tag? const [containerRef, _visibleCount] = useOverflow( // TODO: Remove eslint-disable // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -120,71 +114,55 @@ export const OverflowContainer = React.forwardRef((props, ref) => { const visibleCount = Math.max(_visibleCount, minVisibleCount); - // console.log('children', children.length, visibleCount); - /** * - `visibleCount === children.length + 1` means that we show all children and no overflow tag. * - `visibleCount <= children.length` means that we show visibleCount - 1 children and 1 overflow tag. */ const itemsToRender = React.useMemo(() => { - // let returnValue: React.ReactNode[] | null = null; - - // TODO: Is this try-catch needed? - try { - if (typeof children === 'function') { - throw null; - } - - console.log('visibleCount', visibleCount); - - if (visibleCount > children.length) { - throw [children, [], []]; - } + if (typeof children === 'function') { + return null; + } - // TODO: Fix some off by one errors. It is visible when visibleCount = children.length - 1 - // I think they are fixed. - if (overflowLocation === 'center') { - throw visibleCount >= 3 - ? [ - children[0], - overflowTag?.(visibleCount - 1), - children.slice(children.length - (visibleCount - 2)), - ] - : [ - [], - overflowTag?.(visibleCount - 1), - children.slice(children.length - (visibleCount - 1)), - ]; - } + if (visibleCount > children.length) { + return children; + } - if (overflowLocation === 'start') { - throw [ - overflowTag?.(visibleCount - 2), - children.slice(children.length - visibleCount + 1), - ]; - } + // TODO: Fix some off by one errors. It is visible when visibleCount = children.length - 1 + // I think they are fixed. + if (overflowLocation === 'center') { + return visibleCount >= 3 ? ( + <> + {children[0]} + {overflowTag?.(visibleCount - 1)} + {children.slice(children.length - (visibleCount - 2))} + + ) : ( + <> + {overflowTag?.(visibleCount - 1)} + {children.slice(children.length - (visibleCount - 1))} + + ); + } - throw [ - children.slice(0, visibleCount - 1), - [], - overflowTag?.(visibleCount), - ]; - } catch (returnValue) { - return returnValue?.filter(Boolean); + if (overflowLocation === 'start') { + return ( + <> + {overflowTag?.(visibleCount - 2)} + {children.slice(children.length - visibleCount + 1)} + + ); } + + throw [ + children.slice(0, visibleCount - 1), + [], + overflowTag?.(visibleCount), + ]; }, [children, overflowTag, overflowLocation, visibleCount]); return ( - {typeof children === 'function' ? ( - children(visibleCount) - ) : ( - <> - {itemsToRender?.[0]} - {itemsToRender?.[1]} - {itemsToRender?.[2]} - - )} + {typeof children === 'function' ? children(visibleCount) : itemsToRender} ); }) as PolymorphicForwardRefComponent<'div', OverflowContainerProps>; From fc2c7fd7249fb93272eed1bf9fe18502b0c8a15c Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Fri, 12 Jul 2024 10:50:11 -0400 Subject: [PATCH 38/65] Removed unnecessary props from OverflowContainer --- .../src/core/Breadcrumbs/Breadcrumbs.tsx | 78 +++++++++++-------- .../utils/components/OverflowContainer.tsx | 52 ++++--------- 2 files changed, 60 insertions(+), 70 deletions(-) diff --git a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx index 8883c539a85..9db54945cc1 100644 --- a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx @@ -8,13 +8,11 @@ import { SvgChevronRight, Box, createWarningLogger, - // OverflowContainerContext, } from '../../utils/index.js'; import type { PolymorphicForwardRefComponent } from '../../utils/index.js'; import { Button } from '../Buttons/Button.js'; import { Anchor } from '../Typography/Anchor.js'; import { OverflowContainer } from '../../utils/components/OverflowContainer.js'; -// import { useDebounce } from '../../utils/hooks/useDebounce.js'; const logWarning = createWarningLogger(); @@ -133,41 +131,59 @@ const BreadcrumbsComponent = React.forwardRef((props, ref) => { aria-label='Breadcrumb' {...rest} > - {/* */} ( - <> - - {overflowButton ? ( - overflowButton(visibleCount) - ) : ( - - … + itemsLength={items.length} + > + {(visibleCount: number) => ( + + {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.map((_, index) => { - return ( - - - {index < items.length - 1 && } - - ); - })} - {/* */} ); }) as PolymorphicForwardRefComponent<'nav', BreadcrumbsProps>; diff --git a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx index dcf6d9b4ff0..d0734cfb45c 100644 --- a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx +++ b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx @@ -5,11 +5,6 @@ import type { PolymorphicForwardRefComponent } from '../props.js'; import { Box } from './Box.js'; type OverflowContainerProps = { - /** - * The number of items (including the `overflowTag`, if passed) will always be `>= minVisibleCount`. - * @default 1 - */ - minVisibleCount?: number; /** * // TODO: What happens with overflowDisabled=true and children=function? * If the overflow detection is disabled, visibleCount stays. @@ -41,10 +36,9 @@ type OverflowContainerProps = { * Where the overflowTag is placed. Values: * - start: At the start * - end: At the end - * - center: After the first item and before all other items // TODO: Maybe remove this Breadcrumbs specific loc? * @default 'end' */ - overflowLocation?: 'start' | 'center' | 'end'; + overflowLocation?: 'start' | 'end'; } | { children: (visibleCount: number) => React.ReactNode; @@ -100,26 +94,22 @@ export const OverflowContainer = React.forwardRef((props, ref) => { itemsLength, overflowDisabled = false, overflowOrientation, - minVisibleCount = 1, ...rest } = props; - const [containerRef, _visibleCount] = useOverflow( - // TODO: Remove eslint-disable - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - typeof children === 'function' ? itemsLength! : children.length + 1, + const [containerRef, visibleCount] = useOverflow( + typeof children === 'function' ? itemsLength ?? 0 : children.length + 1, overflowDisabled, overflowOrientation, ); - const visibleCount = Math.max(_visibleCount, minVisibleCount); - /** * - `visibleCount === children.length + 1` means that we show all children and no overflow tag. * - `visibleCount <= children.length` means that we show visibleCount - 1 children and 1 overflow tag. */ const itemsToRender = React.useMemo(() => { - if (typeof children === 'function') { + // User wants complete control over what items are rendered. + if (typeof children === 'function' || overflowTag == null) { return null; } @@ -127,37 +117,21 @@ export const OverflowContainer = React.forwardRef((props, ref) => { return children; } - // TODO: Fix some off by one errors. It is visible when visibleCount = children.length - 1 - // I think they are fixed. - if (overflowLocation === 'center') { - return visibleCount >= 3 ? ( - <> - {children[0]} - {overflowTag?.(visibleCount - 1)} - {children.slice(children.length - (visibleCount - 2))} - - ) : ( - <> - {overflowTag?.(visibleCount - 1)} - {children.slice(children.length - (visibleCount - 1))} - - ); - } - if (overflowLocation === 'start') { return ( <> - {overflowTag?.(visibleCount - 2)} - {children.slice(children.length - visibleCount + 1)} + {overflowTag(visibleCount - 2)} + {children.slice(children.length - (visibleCount - 1))} ); } - throw [ - children.slice(0, visibleCount - 1), - [], - overflowTag?.(visibleCount), - ]; + return ( + <> + {children.slice(0, visibleCount - 1)} + {overflowTag(visibleCount)} + + ); }, [children, overflowTag, overflowLocation, visibleCount]); return ( From 0dee6a3a28e8b49b7ee01169b140dcfbf00ab216 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Fri, 12 Jul 2024 12:34:57 -0400 Subject: [PATCH 39/65] Better disabled support --- .../src/ButtonGroup.stories.tsx | 134 +++++++++--------- .../src/core/Breadcrumbs/Breadcrumbs.tsx | 97 +++++++------ .../src/utils/hooks/useOverflow.tsx | 23 ++- 3 files changed, 129 insertions(+), 125 deletions(-) diff --git a/apps/react-workshop/src/ButtonGroup.stories.tsx b/apps/react-workshop/src/ButtonGroup.stories.tsx index c8e1d90790b..57575754f77 100644 --- a/apps/react-workshop/src/ButtonGroup.stories.tsx +++ b/apps/react-workshop/src/ButtonGroup.stories.tsx @@ -5,10 +5,10 @@ import { Button, ButtonGroup, - DropdownMenu, + // DropdownMenu, IconButton, Input, - MenuItem, + // MenuItem, Text, } from '@itwin/itwinui-react'; import { @@ -17,7 +17,7 @@ import { SvgEdit, SvgUndo, SvgPlaceholder, - SvgMore, + // SvgMore, } from '@itwin/itwinui-icons-react'; export default { @@ -62,38 +62,38 @@ export const Overflow = () => { { - console.log('overflowStart', overflowStart); + // overflowButton={(overflowStart) => { + // console.log('overflowStart', overflowStart); - return ( - { - const length = items.length - overflowStart; + // return ( + // { + // const length = items.length - overflowStart; - return Array.from({ length }, (_, _index) => { - const index = overflowStart + _index; + // return Array.from({ length }, (_, _index) => { + // const index = overflowStart + _index; - return ( - } - > - Item #{index} - - ); - }); - }} - > - console.log('Clicked on overflow icon')} - > - - - - ); - }} + // return ( + // } + // > + // Item #{index} + // + // ); + // }); + // }} + // > + // console.log('Clicked on overflow icon')} + // > + // + // + // + // ); + // }} > {items} @@ -188,42 +188,42 @@ export const VerticalOverflow = () => { orientation='vertical' overflowPlacement='start' style={{ height: 'clamp(100px, 40vmax, 80vh)' }} - overflowButton={(overflowStart) => { - console.log('overflowStart', overflowStart); + // overflowButton={(overflowStart) => { + // console.log('overflowStart', overflowStart); - return ( - - Array(buttons.length - overflowStart + 1) - .fill(null) - .map((_, _index) => { - const index = overflowStart + _index; - const onClick = () => { - console.log(`Clicked button ${index} (overflow)`); - close(); - }; - return ( - } - > - Button #{index} - - ); - }) - } - > - console.log('Clicked on overflow icon')} - > - - - - ); - }} + // return ( + // + // Array(buttons.length - overflowStart + 1) + // .fill(null) + // .map((_, _index) => { + // const index = overflowStart + _index; + // const onClick = () => { + // console.log(`Clicked button ${index} (overflow)`); + // close(); + // }; + // return ( + // } + // > + // Button #{index} + // + // ); + // }) + // } + // > + // console.log('Clicked on overflow icon')} + // > + // + // + // + // ); + // }} > {buttons} diff --git a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx index 9db54945cc1..75eb7e25bd4 100644 --- a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx @@ -135,54 +135,59 @@ const BreadcrumbsComponent = React.forwardRef((props, ref) => { as='ol' className='iui-breadcrumbs-list' itemsLength={items.length} + // overflowDisabled={true} > - {(visibleCount: number) => ( - - {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 && ( - + {(visibleCount: number) => { + console.log('visibleCount', visibleCount); + + 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 && ( + + )} + + ); + })} + + ); + }} ); diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx index e22c56a5ea6..e738721294e 100644 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx +++ b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx @@ -40,22 +40,25 @@ export const useOverflow = ( itemsLength: number, disabled = false, orientation: 'horizontal' | 'vertical' = 'horizontal', - // container: HTMLElement | undefined, ) => { const containerRef = React.useRef(null); const initialVisibleCount = React.useMemo( - () => Math.min(itemsLength, STARTING_MAX_ITEMS_COUNT), - [itemsLength], + () => + disabled ? itemsLength : Math.min(itemsLength, STARTING_MAX_ITEMS_COUNT), + [disabled, itemsLength], ); const initialVisibleCountGuessRange = React.useMemo( - () => [0, initialVisibleCount] satisfies GuessRange, - [initialVisibleCount], + () => (disabled ? null : ([0, initialVisibleCount] satisfies GuessRange)), + [disabled, initialVisibleCount], ); - const [visibleCount, _setVisibleCount] = React.useState(() => - disabled ? itemsLength : initialVisibleCount, - ); + const [visibleCount, _setVisibleCount] = + React.useState(initialVisibleCount); + const [visibleCountGuessRange, setVisibleCountGuessRange] = + React.useState(initialVisibleCountGuessRange); + + const isStabilized = visibleCountGuessRange == null; /** * Ensures that `visibleCount <= itemsLength` @@ -81,10 +84,6 @@ export const useOverflow = ( }, [initialVisibleCount, initialVisibleCountGuessRange, setVisibleCount]), ); - const [visibleCountGuessRange, setVisibleCountGuessRange] = - React.useState(disabled ? null : initialVisibleCountGuessRange); - const isStabilized = visibleCountGuessRange == null; - /** * Call this function to guess the new `visibleCount`. * The `visibleCount` is not changed if the correct `visibleCount` has already been found. From ccdd5a8ab362e513977cfc698ba75cb65eacd042 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Fri, 12 Jul 2024 15:20:09 -0400 Subject: [PATCH 40/65] Revert overflowButton playground changes --- .../src/ButtonGroup.stories.tsx | 76 +++++++-------- .../src/core/Breadcrumbs/Breadcrumbs.tsx | 97 +++++++++---------- .../utils/components/OverflowContainer.tsx | 3 +- 3 files changed, 85 insertions(+), 91 deletions(-) diff --git a/apps/react-workshop/src/ButtonGroup.stories.tsx b/apps/react-workshop/src/ButtonGroup.stories.tsx index 57575754f77..49e7c872b80 100644 --- a/apps/react-workshop/src/ButtonGroup.stories.tsx +++ b/apps/react-workshop/src/ButtonGroup.stories.tsx @@ -5,10 +5,10 @@ import { Button, ButtonGroup, - // DropdownMenu, + DropdownMenu, IconButton, Input, - // MenuItem, + MenuItem, Text, } from '@itwin/itwinui-react'; import { @@ -17,7 +17,7 @@ import { SvgEdit, SvgUndo, SvgPlaceholder, - // SvgMore, + SvgMore, } from '@itwin/itwinui-icons-react'; export default { @@ -188,42 +188,42 @@ export const VerticalOverflow = () => { orientation='vertical' overflowPlacement='start' style={{ height: 'clamp(100px, 40vmax, 80vh)' }} - // overflowButton={(overflowStart) => { - // console.log('overflowStart', overflowStart); + overflowButton={(overflowStart) => { + console.log('overflowStart', overflowStart); - // return ( - // - // Array(buttons.length - overflowStart + 1) - // .fill(null) - // .map((_, _index) => { - // const index = overflowStart + _index; - // const onClick = () => { - // console.log(`Clicked button ${index} (overflow)`); - // close(); - // }; - // return ( - // } - // > - // Button #{index} - // - // ); - // }) - // } - // > - // console.log('Clicked on overflow icon')} - // > - // - // - // - // ); - // }} + return ( + + Array(buttons.length - overflowStart + 1) + .fill(null) + .map((_, _index) => { + const index = overflowStart + _index; + const onClick = () => { + console.log(`Clicked button ${index} (overflow)`); + close(); + }; + return ( + } + > + Button #{index} + + ); + }) + } + > + console.log('Clicked on overflow icon')} + > + + + + ); + }} > {buttons} diff --git a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx index 75eb7e25bd4..9db54945cc1 100644 --- a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.tsx @@ -135,59 +135,54 @@ const BreadcrumbsComponent = React.forwardRef((props, ref) => { as='ol' className='iui-breadcrumbs-list' itemsLength={items.length} - // overflowDisabled={true} > - {(visibleCount: number) => { - console.log('visibleCount', visibleCount); - - return ( - - {visibleCount > 1 && ( - <> - - - - )} - {items.length - visibleCount > 0 && ( - <> - - {overflowButton ? ( - overflowButton(visibleCount) - ) : ( - - … - - )} - - - - )} - {items - .slice( + {(visibleCount: number) => ( + + {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 - ? 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 && ( - - )} - - ); - })} - - ); - }} + ? 1 + (items.length - visibleCount) + _index + : items.length - 1; + return ( + + + {index < items.length - 1 && ( + + )} + + ); + })} + + )} ); diff --git a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx index d0734cfb45c..fcd19ce88ad 100644 --- a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx +++ b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx @@ -6,8 +6,7 @@ import { Box } from './Box.js'; type OverflowContainerProps = { /** - * // TODO: What happens with overflowDisabled=true and children=function? - * If the overflow detection is disabled, visibleCount stays. + * If the overflow detection is disabled, all items will be displayed. * @default false */ overflowDisabled?: boolean; From f815339329bba75cfd76af3250e588fb65fdb908 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Mon, 15 Jul 2024 12:22:09 -0400 Subject: [PATCH 41/65] WIP e2e tests --- .../src/Breadcrumbs.stories.tsx | 58 ++-- apps/react-workshop/src/Select.stories.tsx | 4 +- .../components/OverflowContainer.test.tsx | 19 ++ .../utils/components/OverflowContainer.tsx | 5 +- .../src/utils/hooks/useOverflow.test.tsx | 25 +- playgrounds/vite/src/App.tsx | 30 +- testing/e2e/app/routes/Breadcrumbs/route.tsx | 25 ++ testing/e2e/app/routes/Breadcrumbs/spec.ts | 99 ++++++ testing/e2e/app/routes/ButtonGroup/route.tsx | 69 +++- testing/e2e/app/routes/ButtonGroup/spec.ts | 296 ++++++++++++++---- .../app/routes/MiddleTextTruncation/route.tsx | 13 + .../app/routes/MiddleTextTruncation/spec.ts | 82 +++++ 12 files changed, 614 insertions(+), 111 deletions(-) create mode 100644 packages/itwinui-react/src/utils/components/OverflowContainer.test.tsx create mode 100644 testing/e2e/app/routes/Breadcrumbs/route.tsx create mode 100644 testing/e2e/app/routes/Breadcrumbs/spec.ts create mode 100644 testing/e2e/app/routes/MiddleTextTruncation/route.tsx create mode 100644 testing/e2e/app/routes/MiddleTextTruncation/spec.ts diff --git a/apps/react-workshop/src/Breadcrumbs.stories.tsx b/apps/react-workshop/src/Breadcrumbs.stories.tsx index 84ad21ce479..b768421d01a 100644 --- a/apps/react-workshop/src/Breadcrumbs.stories.tsx +++ b/apps/react-workshop/src/Breadcrumbs.stories.tsx @@ -176,34 +176,38 @@ export const CustomOverflowDropdown = () => { return ( ( - - Array(items.length - visibleCount) - .fill(null) - .map((_, _index) => { - const index = visibleCount > 1 ? _index + 1 : _index; - const onClick = () => { - console.log(`Visit breadcrumb ${index}`); - close(); - }; - return ( - - Item {index} - - ); - }) - } - > - console.log('Clicked on overflow icon')} - styleType='borderless' + overflowButton={(visibleCount: number) => { + console.log(visibleCount); + + return ( + + Array(items.length - visibleCount) + .fill(null) + .map((_, _index) => { + const index = visibleCount > 1 ? _index + 1 : _index; + const onClick = () => { + console.log(`Visit breadcrumb ${index}`); + close(); + }; + return ( + + Item {index} + + ); + }) + } > - - - - )} + console.log('Clicked on overflow icon')} + styleType='borderless' + > + + + + ); + }} > {items} diff --git a/apps/react-workshop/src/Select.stories.tsx b/apps/react-workshop/src/Select.stories.tsx index fef29eec6ae..40f46dc21fa 100644 --- a/apps/react-workshop/src/Select.stories.tsx +++ b/apps/react-workshop/src/Select.stories.tsx @@ -2,7 +2,7 @@ * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ -import React, { useCallback } from 'react'; +import React from 'react'; import { MenuItem, Select, MiddleTextTruncation } from '@itwin/itwinui-react'; import { SvgSmileyHappy, @@ -184,7 +184,7 @@ export const TruncateMiddleText = () => { options[0].value, ); - const textRenderer = useCallback( + const textRenderer = React.useCallback( (truncatedText: string, originalText: string) => ( {truncatedText} diff --git a/packages/itwinui-react/src/utils/components/OverflowContainer.test.tsx b/packages/itwinui-react/src/utils/components/OverflowContainer.test.tsx new file mode 100644 index 00000000000..eaf8be81a3d --- /dev/null +++ b/packages/itwinui-react/src/utils/components/OverflowContainer.test.tsx @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ +import { render } from '@testing-library/react'; +import { MiddleTextTruncation } from './MiddleTextTruncation.js'; +import * as UseOverflow from '../hooks/useOverflow.js'; + +it('should render all items when no overflow', () => { + vi.spyOn(UseOverflow, 'useOverflow').mockReturnValue([vi.fn(), 20]); + const { container } = render( +
+ +
, + ); + + const containerSpan = container.querySelector('span') as HTMLSpanElement; + expect(containerSpan.textContent).toBe('This is some …lipsis'); +}); diff --git a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx index fcd19ce88ad..6fffcb68e86 100644 --- a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx +++ b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx @@ -96,12 +96,15 @@ export const OverflowContainer = React.forwardRef((props, ref) => { ...rest } = props; - const [containerRef, visibleCount] = useOverflow( + const [containerRef, _visibleCount] = useOverflow( typeof children === 'function' ? itemsLength ?? 0 : children.length + 1, overflowDisabled, overflowOrientation, ); + // Minimum visibleCount of 1 since we always at least show the `overflowTag` in small sizes. + const visibleCount = Math.max(_visibleCount, 1); + /** * - `visibleCount === children.length + 1` means that we show all children and no overflow tag. * - `visibleCount <= children.length` means that we show visibleCount - 1 children and 1 overflow tag. diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.test.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.test.tsx index 8187717394c..8cfb8bd3722 100644 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.test.tsx +++ b/packages/itwinui-react/src/utils/hooks/useOverflow.test.tsx @@ -17,7 +17,7 @@ const MockComponent = ({ orientation?: 'horizontal' | 'vertical'; }) => { const [overflowRef, visibleCount] = useOverflow( - children, + children.length, disableOverflow, orientation, ); @@ -60,22 +60,27 @@ it.each(['horizontal', 'vertical'] as const)( }, ); -it('should overflow when there is not enough space (string)', async () => { - vi.spyOn(HTMLDivElement.prototype, 'scrollWidth', 'get') - .mockReturnValueOnce(50) - .mockReturnValue(28); - vi.spyOn(HTMLDivElement.prototype, 'offsetWidth', 'get').mockReturnValue(28); +it.only('should overflow when there is not enough space (string)', async () => { + const fullText = 'This is a very long text.'; + const truncatedText = 'This is a v'; + + vi.spyOn(HTMLDivElement.prototype, 'scrollWidth', 'get').mockReturnValue( + 2.5 * truncatedText.length, + ); + // .mockReturnValueOnce(50) + // .mockReturnValue(28); + vi.spyOn(HTMLDivElement.prototype, 'offsetWidth', 'get').mockReturnValue( + 2.5 * fullText.length, + ); // 20 symbols (default value taken), 50 width // avg 2.5px per symbol - const { container } = render( - This is a very long text., - ); + const { container } = render({fullText}); // have 28px of a place // 11 symbols can fit await waitFor(() => { - expect(container.textContent).toBe('This is a v'); + expect(container.textContent).toBe(truncatedText); }); }); diff --git a/playgrounds/vite/src/App.tsx b/playgrounds/vite/src/App.tsx index b4bc50954ed..ac0c70c2278 100644 --- a/playgrounds/vite/src/App.tsx +++ b/playgrounds/vite/src/App.tsx @@ -1,4 +1,5 @@ -import { ComboBox } from '@itwin/itwinui-react'; +import { SvgPlaceholder } from '@itwin/itwinui-icons-react'; +import { ButtonGroup, ComboBox, IconButton } from '@itwin/itwinui-react'; export default function App() { const data: { label: string; value: number }[] = []; @@ -15,7 +16,7 @@ export default function App() { return ( <> - {widths.slice(0, 1).map((width) => ( + {/* {widths.slice(0, 1).map((width) => ( - ))} + ))} */} +
+ { + return {firstOverflowingIndex}; + }} + > + + + + + + + + + + +
); } diff --git a/testing/e2e/app/routes/Breadcrumbs/route.tsx b/testing/e2e/app/routes/Breadcrumbs/route.tsx new file mode 100644 index 00000000000..4d75c35af3d --- /dev/null +++ b/testing/e2e/app/routes/Breadcrumbs/route.tsx @@ -0,0 +1,25 @@ +import { Breadcrumbs, Button } from '@itwin/itwinui-react'; + +export default function BreadcrumbsTest() { + const items = Array(5) + .fill(null) + .map((_, index) => ( + + Item {index} + + )); + + return ( + <> +
+ { + return ; + }} + > + {items} + +
+ + ); +} diff --git a/testing/e2e/app/routes/Breadcrumbs/spec.ts b/testing/e2e/app/routes/Breadcrumbs/spec.ts new file mode 100644 index 00000000000..28db1adaea2 --- /dev/null +++ b/testing/e2e/app/routes/Breadcrumbs/spec.ts @@ -0,0 +1,99 @@ +import { test, expect, Page } from '@playwright/test'; + +test.describe('Breadcrumbs', () => { + test(`should overflow whenever there is not enough space`, async ({ + page, + }) => { + await page.goto(`/Breadcrumbs`); + + const setContainerSize = getSetContainerSize(page); + const expectOverflowState = getExpectOverflowState(page); + + await expectOverflowState({ + expectedItemLength: 5, + expectedOverflowButtonVisibleCount: undefined, + }); + + await setContainerSize('200px'); + await page.waitForTimeout(100); + + await expectOverflowState({ + expectedItemLength: 2, + expectedOverflowButtonVisibleCount: 2, + }); + + // should restore hidden items when space is available again + await setContainerSize(undefined); + await page.waitForTimeout(100); + + await expectOverflowState({ + expectedItemLength: 5, + expectedOverflowButtonVisibleCount: undefined, + }); + }); + + test(`should at minimum always show one overflow tag and one item`, async ({ + page, + }) => { + await page.goto(`/Breadcrumbs`); + + const setContainerSize = getSetContainerSize(page); + const expectOverflowState = getExpectOverflowState(page); + + await page.locator('#container').evaluate((element) => { + element.style.overflow = 'hidden'; + }); + await page.waitForTimeout(100); + + await expectOverflowState({ + expectedItemLength: 5, + expectedOverflowButtonVisibleCount: undefined, + }); + + await setContainerSize('10px'); + await page.waitForTimeout(100); + + await expectOverflowState({ + expectedItemLength: 1, + expectedOverflowButtonVisibleCount: 1, + }); + }); +}); + +// ---------------------------------------------------------------------------- + +const getSetContainerSize = (page: Page) => { + return async (dimension: string | undefined) => { + await page.locator('#container').evaluate( + (element, args) => { + element.style.width = args.dimension ? args.dimension : `999px`; + }, + { + dimension, + }, + ); + }; +}; + +const getExpectOverflowState = (page: Page) => { + return async ({ + expectedItemLength, + expectedOverflowButtonVisibleCount, + }: { + expectedItemLength: number; + expectedOverflowButtonVisibleCount: number | undefined; + }) => { + const items = page.getByTestId('item'); + expect(items).toHaveCount(expectedItemLength); + + const overflowButton = page.locator('button'); + + if (expectedOverflowButtonVisibleCount != null) { + expect(await overflowButton.textContent()).toBe( + `${expectedOverflowButtonVisibleCount}`, + ); + } else { + expect(overflowButton).toHaveCount(0); + } + }; +}; diff --git a/testing/e2e/app/routes/ButtonGroup/route.tsx b/testing/e2e/app/routes/ButtonGroup/route.tsx index 1399e71b436..a39e71da39a 100644 --- a/testing/e2e/app/routes/ButtonGroup/route.tsx +++ b/testing/e2e/app/routes/ButtonGroup/route.tsx @@ -1,23 +1,66 @@ -import { ButtonGroup, IconButton } from '@itwin/itwinui-react'; +import { Button, ButtonGroup, Flex, IconButton } from '@itwin/itwinui-react'; import { SvgPlaceholder } from '@itwin/itwinui-icons-react'; import { useSearchParams } from '@remix-run/react'; +import React from 'react'; export default function ButtonGroupTest() { const [searchParams] = useSearchParams(); - const orientation = searchParams.get('orientation') || 'horizontal'; + const initialProvideOverflowButton = + (searchParams.get('provideOverflowButton') ?? 'true') !== 'false'; + const orientation = + (searchParams.get('orientation') as 'horizontal' | 'vertical') || + 'horizontal'; + const overflowPlacement = + (searchParams.get('overflowPlacement') as 'start' | 'end') || undefined; + + const [provideOverflowButton, setProvideOverflowButton] = React.useState( + initialProvideOverflowButton, + ); return ( - - - - - - - - - - - + + + +
+ { + return ( + + {firstOverflowingIndex} + + ); + } + : undefined + } + > + + + + + + + + + + +
+
); } diff --git a/testing/e2e/app/routes/ButtonGroup/spec.ts b/testing/e2e/app/routes/ButtonGroup/spec.ts index 0ad18d62823..48a31471eec 100644 --- a/testing/e2e/app/routes/ButtonGroup/spec.ts +++ b/testing/e2e/app/routes/ButtonGroup/spec.ts @@ -1,63 +1,249 @@ -import { test, expect } from '@playwright/test'; +import { test, expect, Page } from '@playwright/test'; test.describe('ButtonGroup', () => { - test("should support keyboard navigation when role='toolbar'", async ({ - page, - }) => { - await page.goto('/ButtonGroup'); - - await page.keyboard.press('Tab'); - await expect(page.getByRole('button', { name: 'Button 1' })).toBeFocused(); - - await page.keyboard.press('ArrowRight'); - await expect(page.getByRole('button', { name: 'Button 2' })).toBeFocused(); - - await page.keyboard.press('ArrowRight'); - await expect(page.getByRole('button', { name: 'Button 3' })).toBeFocused(); - - await page.keyboard.press('ArrowRight'); - await expect(page.getByRole('button', { name: 'Button 1' })).toBeFocused(); - - await page.keyboard.press('ArrowLeft'); - await expect(page.getByRole('button', { name: 'Button 3' })).toBeFocused(); - - await page.keyboard.press('ArrowLeft'); - await expect(page.getByRole('button', { name: 'Button 2' })).toBeFocused(); - - await page.keyboard.press('ArrowLeft'); - await expect(page.getByRole('button', { name: 'Button 1' })).toBeFocused(); - - await page.keyboard.press('ArrowLeft'); - await expect(page.getByRole('button', { name: 'Button 3' })).toBeFocused(); + // test("should support keyboard navigation when role='toolbar'", async ({ + // page, + // }) => { + // await page.goto('/ButtonGroup'); + + // await page.keyboard.press('Tab'); + // await expect(page.getByRole('button', { name: 'Button 1' })).toBeFocused(); + + // await page.keyboard.press('ArrowRight'); + // await expect(page.getByRole('button', { name: 'Button 2' })).toBeFocused(); + + // await page.keyboard.press('ArrowRight'); + // await expect(page.getByRole('button', { name: 'Button 3' })).toBeFocused(); + + // await page.keyboard.press('ArrowRight'); + // await expect(page.getByRole('button', { name: 'Button 1' })).toBeFocused(); + + // await page.keyboard.press('ArrowLeft'); + // await expect(page.getByRole('button', { name: 'Button 3' })).toBeFocused(); + + // await page.keyboard.press('ArrowLeft'); + // await expect(page.getByRole('button', { name: 'Button 2' })).toBeFocused(); + + // await page.keyboard.press('ArrowLeft'); + // await expect(page.getByRole('button', { name: 'Button 1' })).toBeFocused(); + + // await page.keyboard.press('ArrowLeft'); + // await expect(page.getByRole('button', { name: 'Button 3' })).toBeFocused(); + // }); + + // test("should support keyboard navigation when role='toolbar' and orientation='vertical'", async ({ + // page, + // }) => { + // await page.goto('/ButtonGroup?orientation=vertical'); + + // await page.keyboard.press('Tab'); + // await expect(page.getByRole('button', { name: 'Button 1' })).toBeFocused(); + + // await page.keyboard.press('ArrowDown'); + // await expect(page.getByRole('button', { name: 'Button 2' })).toBeFocused(); + + // await page.keyboard.press('ArrowDown'); + // await expect(page.getByRole('button', { name: 'Button 3' })).toBeFocused(); + + // await page.keyboard.press('ArrowDown'); + // await expect(page.getByRole('button', { name: 'Button 1' })).toBeFocused(); + + // await page.keyboard.press('ArrowUp'); + // await expect(page.getByRole('button', { name: 'Button 3' })).toBeFocused(); + + // await page.keyboard.press('ArrowUp'); + // await expect(page.getByRole('button', { name: 'Button 2' })).toBeFocused(); + + // await page.keyboard.press('ArrowUp'); + // await expect(page.getByRole('button', { name: 'Button 1' })).toBeFocused(); + + // await page.keyboard.press('ArrowUp'); + // await expect(page.getByRole('button', { name: 'Button 3' })).toBeFocused(); + // }); + + ( + [ + { + orientation: 'horizontal', + overflowPlacement: 'end', + }, + { + orientation: 'horizontal', + overflowPlacement: 'start', + }, + { + orientation: 'vertical', + overflowPlacement: 'end', + }, + { + orientation: 'vertical', + overflowPlacement: 'start', + }, + ] as const + ).forEach(({ orientation, overflowPlacement }) => { + test(`should overflow whenever there is not enough space (orientation=${orientation}, overflowPlacement=${overflowPlacement})`, async ({ + page, + }) => { + await page.goto( + `/ButtonGroup?orientation=${orientation}&overflowPlacement=${overflowPlacement}`, + ); + + const setContainerSize = getSetContainerSize(page, orientation); + const expectOverflowState = getExpectOverflowState( + page, + overflowPlacement, + ); + + await expectOverflowState({ + expectedButtonLength: 3, + expectedOverflowTagFirstOverflowingIndex: undefined, + }); + + await setContainerSize(2.5); + await page.waitForTimeout(100); + + await expectOverflowState({ + expectedButtonLength: 2, + expectedOverflowTagFirstOverflowingIndex: 1, + }); + + await setContainerSize(1.5); + await page.waitForTimeout(100); + + await expectOverflowState({ + expectedButtonLength: 1, + expectedOverflowTagFirstOverflowingIndex: + overflowPlacement === 'end' ? 0 : 2, + }); + + await setContainerSize(0.5); + await page.waitForTimeout(100); + + // should return 1 overflowTag when item is bigger than the container + await expectOverflowState({ + expectedButtonLength: 1, + expectedOverflowTagFirstOverflowingIndex: + overflowPlacement === 'end' ? 0 : 2, + }); + + await page.waitForTimeout(100); + + // should restore hidden items when space is available again + await setContainerSize(1.5); + await page.waitForTimeout(100); + + await expectOverflowState({ + expectedButtonLength: 1, + expectedOverflowTagFirstOverflowingIndex: + overflowPlacement === 'end' ? 0 : 2, + }); + + await setContainerSize(2.5); + await page.waitForTimeout(100); + + await expectOverflowState({ + expectedButtonLength: 2, + expectedOverflowTagFirstOverflowingIndex: 1, + }); + + await setContainerSize(undefined); + await page.waitForTimeout(100); + + await expectOverflowState({ + expectedButtonLength: 3, + expectedOverflowTagFirstOverflowingIndex: undefined, + }); + + await page.waitForTimeout(100); + }); }); - test("should support keyboard navigation when role='toolbar' and orientation='vertical'", async ({ + test(`should handle overflow only whenever overflowButton is passed`, async ({ page, }) => { - await page.goto('/ButtonGroup?orientation=vertical'); - - await page.keyboard.press('Tab'); - await expect(page.getByRole('button', { name: 'Button 1' })).toBeFocused(); - - await page.keyboard.press('ArrowDown'); - await expect(page.getByRole('button', { name: 'Button 2' })).toBeFocused(); - - await page.keyboard.press('ArrowDown'); - await expect(page.getByRole('button', { name: 'Button 3' })).toBeFocused(); - - await page.keyboard.press('ArrowDown'); - await expect(page.getByRole('button', { name: 'Button 1' })).toBeFocused(); - - await page.keyboard.press('ArrowUp'); - await expect(page.getByRole('button', { name: 'Button 3' })).toBeFocused(); - - await page.keyboard.press('ArrowUp'); - await expect(page.getByRole('button', { name: 'Button 2' })).toBeFocused(); - - await page.keyboard.press('ArrowUp'); - await expect(page.getByRole('button', { name: 'Button 1' })).toBeFocused(); - - await page.keyboard.press('ArrowUp'); - await expect(page.getByRole('button', { name: 'Button 3' })).toBeFocused(); + await page.goto(`/ButtonGroup?provideOverflowButton=false`); + + const setContainerSize = getSetContainerSize(page, 'horizontal'); + const expectOverflowState = getExpectOverflowState(page, 'end'); + + await expectOverflowState({ + expectedButtonLength: 3, + expectedOverflowTagFirstOverflowingIndex: undefined, + }); + + await setContainerSize(2.5); + await page.waitForTimeout(100); + + await expectOverflowState({ + expectedButtonLength: 3, + expectedOverflowTagFirstOverflowingIndex: undefined, + }); + + const toggleProviderOverflowContainerButton = page.getByTestId( + 'toggle-provide-overflow-container', + ); + + await toggleProviderOverflowContainerButton.click(); + await expectOverflowState({ + expectedButtonLength: 2, + expectedOverflowTagFirstOverflowingIndex: 1, + }); + + await toggleProviderOverflowContainerButton.click(); + await expectOverflowState({ + expectedButtonLength: 3, + expectedOverflowTagFirstOverflowingIndex: undefined, + }); }); }); + +// ---------------------------------------------------------------------------- + +const getSetContainerSize = ( + page: Page, + orientation: 'horizontal' | 'vertical', +) => { + return async (multiplier: number | undefined) => { + await page.locator('#container').evaluate( + (element, args) => { + if (args.orientation === 'horizontal') { + element.style.width = + args.multiplier != null ? `${50 * args.multiplier}px` : `999px`; + } else { + element.style.height = + args.multiplier != null ? `${36 * args.multiplier}px` : `999px`; + } + }, + { + orientation, + multiplier, + }, + ); + }; +}; + +const getExpectOverflowState = ( + page: Page, + overflowPlacement: 'start' | 'end', +) => { + return async ({ + expectedButtonLength, + expectedOverflowTagFirstOverflowingIndex, + }: { + expectedButtonLength: number; + expectedOverflowTagFirstOverflowingIndex: number | undefined; + }) => { + const buttons = await page.locator('#container button').all(); + expect(buttons.length).toBe(expectedButtonLength); + + if (expectedOverflowTagFirstOverflowingIndex != null) { + expect( + await buttons[ + overflowPlacement === 'end' ? buttons.length - 1 : 0 + ].textContent(), + ).toBe(`${expectedOverflowTagFirstOverflowingIndex}`); + } else { + expect(page.getByTestId('overflow-button')).toHaveCount(0); + } + }; +}; diff --git a/testing/e2e/app/routes/MiddleTextTruncation/route.tsx b/testing/e2e/app/routes/MiddleTextTruncation/route.tsx new file mode 100644 index 00000000000..c50e072ebb1 --- /dev/null +++ b/testing/e2e/app/routes/MiddleTextTruncation/route.tsx @@ -0,0 +1,13 @@ +import { MiddleTextTruncation } from '@itwin/itwinui-react'; +import React from 'react'; + +const longText = + 'MyFileWithAReallyLongNameThatWillBeTruncatedBecauseItIsReallyThatLongSoHardToBelieve_FinalVersion_V2.html'; + +export default function MiddleTextTruncationTest() { + return ( +
+ +
+ ); +} diff --git a/testing/e2e/app/routes/MiddleTextTruncation/spec.ts b/testing/e2e/app/routes/MiddleTextTruncation/spec.ts new file mode 100644 index 00000000000..3e8c684ad9a --- /dev/null +++ b/testing/e2e/app/routes/MiddleTextTruncation/spec.ts @@ -0,0 +1,82 @@ +import { test, expect, Page } from '@playwright/test'; + +test.describe('MiddleTextTruncation', () => { + const longItem = + 'MyFileWithAReallyLongNameThatWillBeTruncatedBecauseItIsReallyThatLongSoHardToBelieve_FinalVersion_V2.html'; + + test(`should overflow whenever there is not enough space`, async ({ + page, + }) => { + await page.goto(`/MiddleTextTruncation`); + + const setContainerSize = getSetContainerSize(page); + + await page.waitForTimeout(100); + + const middleTextTruncation = page.getByTestId('root'); + expect(await middleTextTruncation.first().textContent()).toHaveLength( + longItem.length, + ); + + await setContainerSize('200px'); + await page.waitForTimeout(100); + + expect(await middleTextTruncation.first().textContent()).toHaveLength( + 'MyFileWithAReallyLon…2.html'.length, + ); + + await setContainerSize(undefined); + await page.waitForTimeout(100); + + // should restore hidden items when space is available again + expect(await middleTextTruncation.first().textContent()).toHaveLength( + longItem.length, + ); + }); + + test(`should at minimum always show ellipses and endCharsCount number of characters`, async ({ + page, + }) => { + await page.goto(`/MiddleTextTruncation`); + + const endCharsCount = 6; + const setContainerSize = getSetContainerSize(page); + + await page.waitForTimeout(100); + + const middleTextTruncation = page.getByTestId('root'); + expect(await middleTextTruncation.first().textContent()).toHaveLength( + longItem.length, + ); + + await setContainerSize('20px'); + await page.waitForTimeout(100); + + expect(await middleTextTruncation.first().textContent()).toHaveLength( + endCharsCount + 1, // +1 for the ellipses + ); + + await setContainerSize(undefined); + await page.waitForTimeout(100); + + // should restore hidden items when space is available again + expect(await middleTextTruncation.first().textContent()).toHaveLength( + longItem.length, + ); + }); +}); + +// ---------------------------------------------------------------------------- + +const getSetContainerSize = (page: Page) => { + return async (dimension: string | undefined) => { + await page.getByTestId('root').evaluate( + (element, args) => { + element.style.width = args.dimension != null ? args.dimension : `999px`; + }, + { + dimension, + }, + ); + }; +}; From 815d876a776dcd382f5b1d98ba690b64c315f2e7 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Tue, 16 Jul 2024 07:10:15 -0400 Subject: [PATCH 42/65] Revert storybook --- .../src/Breadcrumbs.stories.tsx | 66 +++++----- .../src/ButtonGroup.stories.tsx | 122 ++++++++---------- apps/react-workshop/src/ComboBox.stories.tsx | 15 +-- apps/react-workshop/src/Select.stories.tsx | 4 - apps/react-workshop/src/Table.stories.tsx | 3 +- 5 files changed, 87 insertions(+), 123 deletions(-) diff --git a/apps/react-workshop/src/Breadcrumbs.stories.tsx b/apps/react-workshop/src/Breadcrumbs.stories.tsx index b768421d01a..579092d7200 100644 --- a/apps/react-workshop/src/Breadcrumbs.stories.tsx +++ b/apps/react-workshop/src/Breadcrumbs.stories.tsx @@ -150,8 +150,8 @@ CustomOverflowBackButton.decorators = [
{ return ( { - console.log(visibleCount); - - return ( - - Array(items.length - visibleCount) - .fill(null) - .map((_, _index) => { - const index = visibleCount > 1 ? _index + 1 : _index; - const onClick = () => { - console.log(`Visit breadcrumb ${index}`); - close(); - }; - return ( - - Item {index} - - ); - }) - } + overflowButton={(visibleCount: number) => ( + + Array(items.length - visibleCount) + .fill(null) + .map((_, _index) => { + const index = visibleCount > 1 ? _index + 1 : _index; + const onClick = () => { + console.log(`Visit breadcrumb ${index}`); + close(); + }; + return ( + + Item {index} + + ); + }) + } + > + console.log('Clicked on overflow icon')} + styleType='borderless' > - console.log('Clicked on overflow icon')} - styleType='borderless' - > - - - - ); - }} + + + + )} > {items} @@ -222,8 +218,8 @@ CustomOverflowDropdown.decorators = [
{ return ( { - // console.log('overflowStart', overflowStart); - - // return ( - // { - // const length = items.length - overflowStart; + overflowButton={(overflowStart) => { + return ( + { + const length = items.length - overflowStart; - // return Array.from({ length }, (_, _index) => { - // const index = overflowStart + _index; + return Array.from({ length }, (_, _index) => { + const index = overflowStart + _index; - // return ( - // } - // > - // Item #{index} - // - // ); - // }); - // }} - // > - // console.log('Clicked on overflow icon')} - // > - // - // - // - // ); - // }} + return ( + } + > + Item #{index} + + ); + }); + }} + > + + + + + ); + }} > {items} @@ -176,7 +170,6 @@ export const VerticalOverflow = () => { .map((_, index) => ( console.log(`Clicked on button ${index + 1}`)} > @@ -186,44 +179,35 @@ export const VerticalOverflow = () => { return ( { - console.log('overflowStart', overflowStart); - - return ( - - Array(buttons.length - overflowStart + 1) - .fill(null) - .map((_, _index) => { - const index = overflowStart + _index; - const onClick = () => { - console.log(`Clicked button ${index} (overflow)`); - close(); - }; - return ( - } - > - Button #{index} - - ); - }) - } - > - console.log('Clicked on overflow icon')} - > - - - - ); - }} + overflowButton={(overflowStart) => ( + + Array(buttons.length - overflowStart + 1) + .fill(null) + .map((_, _index) => { + const index = overflowStart + _index; + const onClick = () => { + console.log(`Clicked button ${index} (overflow)`); + close(); + }; + return ( + } + > + Button #{index} + + ); + }) + } + > + console.log('Clicked on overflow icon')}> + + + + )} > {buttons} diff --git a/apps/react-workshop/src/ComboBox.stories.tsx b/apps/react-workshop/src/ComboBox.stories.tsx index 0745b585de5..53c8628dd3b 100644 --- a/apps/react-workshop/src/ComboBox.stories.tsx +++ b/apps/react-workshop/src/ComboBox.stories.tsx @@ -29,11 +29,7 @@ export default { } satisfies StoryDefault; const countriesList = [ - { - label: - 'Afghanistan awhdbvjabowndb hanwidb habnwidb awbnidb ajwbdhinb awdbnihba bwdbnhinab wbdbnianbw dnbaiwndbk hbnqbnw dbn awndj', - value: 'AF', - }, + { label: 'Afghanistan', value: 'AF' }, { label: 'Åland Islands', value: 'AX' }, { label: 'Albania', value: 'AL' }, { label: 'Algeria', value: 'DZ' }, @@ -520,13 +516,8 @@ export const Virtualized = () => { export const MultipleSelect = () => { const options = React.useMemo(() => countriesList, []); const [selectedOptions, setSelectedOptions] = React.useState([ + 'CA', 'AX', - 'AL', - 'DZ', - 'AS', - 'AD', - 'AO', - 'AI', ]); return ( @@ -551,8 +542,6 @@ MultipleSelect.decorators = [
{ value={selectedValue} onChange={setSelectedValue} placeholder='Placeholder text' - style={{ - resize: 'inline', - overflow: 'hidden', - }} itemRenderer={(option) => ( { emptyTableContent='No data.' isSelectable isSortable - // isLoading columns={columns} data={data} pageSize={50} paginatorRenderer={paginator} - style={{ height: '100%', resize: 'inline', overflow: 'hidden' }} + style={{ height: '100%' }} /> ); From 2be10a178f619e70580e4248060d6102ebae8e14 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Tue, 16 Jul 2024 07:10:39 -0400 Subject: [PATCH 43/65] Remove testing hooks --- .../src/utils/hooks/useDebounce.ts | 64 ------------------- .../src/utils/hooks/usePrevious.ts | 21 ------ 2 files changed, 85 deletions(-) delete mode 100644 packages/itwinui-react/src/utils/hooks/useDebounce.ts delete mode 100644 packages/itwinui-react/src/utils/hooks/usePrevious.ts diff --git a/packages/itwinui-react/src/utils/hooks/useDebounce.ts b/packages/itwinui-react/src/utils/hooks/useDebounce.ts deleted file mode 100644 index f7eedd56731..00000000000 --- a/packages/itwinui-react/src/utils/hooks/useDebounce.ts +++ /dev/null @@ -1,64 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Bentley Systems, Incorporated. All rights reserved. - * See LICENSE.md in the project root for license terms and full copyright notice. - *--------------------------------------------------------------------------------------------*/ -/* eslint-disable react-hooks/exhaustive-deps */ -/* eslint-disable @typescript-eslint/ban-types */ -import { useEffect, useCallback, useRef } from 'react'; -import type { DependencyList } from 'react'; - -export type UseDebounceReturn = [() => boolean | null, () => void]; - -// TODO: Delete this temp testing func from react-use -export function useDebounce( - fn: Function, - ms = 0, - deps: DependencyList = [], -): UseDebounceReturn { - const [isReady, cancel, reset] = useTimeoutFn(fn, ms); - - useEffect(reset, deps); - - return [isReady, cancel]; -} - -// ---------------------------------------------------------------------------- - -export type UseTimeoutFnReturn = [() => boolean | null, () => void, () => void]; - -export function useTimeoutFn(fn: Function, ms = 0): UseTimeoutFnReturn { - const ready = useRef(false); - const timeout = useRef>(); - const callback = useRef(fn); - - const isReady = useCallback(() => ready.current, []); - - const set = useCallback(() => { - ready.current = false; - timeout.current && clearTimeout(timeout.current); - - timeout.current = setTimeout(() => { - ready.current = true; - callback.current(); - }, ms); - }, [ms]); - - const clear = useCallback(() => { - ready.current = null; - timeout.current && clearTimeout(timeout.current); - }, []); - - // update ref when function changes - useEffect(() => { - callback.current = fn; - }, [fn]); - - // set on mount, clear on unmount - useEffect(() => { - set(); - - return clear; - }, [ms]); - - return [isReady, clear, set]; -} diff --git a/packages/itwinui-react/src/utils/hooks/usePrevious.ts b/packages/itwinui-react/src/utils/hooks/usePrevious.ts deleted file mode 100644 index 12f65e363aa..00000000000 --- a/packages/itwinui-react/src/utils/hooks/usePrevious.ts +++ /dev/null @@ -1,21 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Bentley Systems, Incorporated. All rights reserved. - * See LICENSE.md in the project root for license terms and full copyright notice. - *--------------------------------------------------------------------------------------------*/ -/** - * Based on react-use's usePrevious - * The original code is licensed under "Unlimited License" - https://unpkg.com/browse/react-use@17.5.0/LICENSE - */ - -import { useRef } from 'react'; -import { useLayoutEffect } from './useIsomorphicLayoutEffect.js'; - -export default function usePrevious(state: T): T | undefined { - const ref = useRef(); - - useLayoutEffect(() => { - ref.current = state; - }); - - return ref.current; -} From 64f461effabfdbb4635e3f8b18fe4b3dd4d7283a Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Tue, 16 Jul 2024 07:11:33 -0400 Subject: [PATCH 44/65] Move useOverflow to OverflowContainer --- .../components/OverflowContainer.test.tsx | 19 -- .../utils/components/OverflowContainer.tsx | 210 ++++++++++++++++- .../src/utils/hooks/useOverflow.test.tsx | 221 ------------------ .../src/utils/hooks/useOverflow.tsx | 213 ----------------- .../src/utils/hooks/useResizeObserver.tsx | 3 - 5 files changed, 209 insertions(+), 457 deletions(-) delete mode 100644 packages/itwinui-react/src/utils/components/OverflowContainer.test.tsx delete mode 100644 packages/itwinui-react/src/utils/hooks/useOverflow.test.tsx delete mode 100644 packages/itwinui-react/src/utils/hooks/useOverflow.tsx diff --git a/packages/itwinui-react/src/utils/components/OverflowContainer.test.tsx b/packages/itwinui-react/src/utils/components/OverflowContainer.test.tsx deleted file mode 100644 index eaf8be81a3d..00000000000 --- a/packages/itwinui-react/src/utils/components/OverflowContainer.test.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Bentley Systems, Incorporated. All rights reserved. - * See LICENSE.md in the project root for license terms and full copyright notice. - *--------------------------------------------------------------------------------------------*/ -import { render } from '@testing-library/react'; -import { MiddleTextTruncation } from './MiddleTextTruncation.js'; -import * as UseOverflow from '../hooks/useOverflow.js'; - -it('should render all items when no overflow', () => { - vi.spyOn(UseOverflow, 'useOverflow').mockReturnValue([vi.fn(), 20]); - const { container } = render( -
- -
, - ); - - const containerSpan = container.querySelector('span') as HTMLSpanElement; - expect(containerSpan.textContent).toBe('This is some …lipsis'); -}); diff --git a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx index 6fffcb68e86..38182a97a2d 100644 --- a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx +++ b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx @@ -1,8 +1,10 @@ import React from 'react'; import { useMergedRefs } from '../hooks/useMergedRefs.js'; -import { useOverflow } from '../hooks/useOverflow.js'; import type { PolymorphicForwardRefComponent } from '../props.js'; import { Box } from './Box.js'; +import { useResizeObserver } from '../hooks/useResizeObserver.js'; +import { useLatestRef } from '../hooks/useLatestRef.js'; +import { useLayoutEffect } from '../hooks/useIsomorphicLayoutEffect.js'; type OverflowContainerProps = { /** @@ -142,3 +144,209 @@ export const OverflowContainer = React.forwardRef((props, ref) => { ); }) as PolymorphicForwardRefComponent<'div', OverflowContainerProps>; + +// ---------------------------------------------------------------------------- + +/** `[number, number]` means that we're still guessing. `null` means that we got the correct `visibleCount`. */ +type GuessRange = [number, number] | null; + +/** First guess of the number of items that overflows. We refine this guess with subsequent renders. */ +const STARTING_MAX_ITEMS_COUNT = 2; + +/** + * Hook that returns the number of items that should be visible based on the size of the container element. + * + * The returned number should be used to render the element with fewer items. + * + * @private + * @param items Items that this element contains. + * @param disabled Set to true to disconnect the observer. + * @param dimension 'horizontal' (default) or 'vertical' + * @returns [callback ref to set on container, stateful count of visible items] + * + * @example + * const items = Array(10).fill().map((_, i) => Item {i}); + * const [ref, visibleCount] = useOverflow(items); + * ... + * return ( + *
+ * {items.slice(0, visibleCount)} + *
+ * ); + */ +const useOverflow = ( + // TODO: Try more to remove this prop, if possible. + itemsLength: number, + disabled = false, + orientation: 'horizontal' | 'vertical' = 'horizontal', +) => { + const containerRef = React.useRef(null); + + const initialVisibleCount = React.useMemo( + () => + disabled ? itemsLength : Math.min(itemsLength, STARTING_MAX_ITEMS_COUNT), + [disabled, itemsLength], + ); + const initialVisibleCountGuessRange = React.useMemo( + () => (disabled ? null : ([0, initialVisibleCount] satisfies GuessRange)), + [disabled, initialVisibleCount], + ); + + const [visibleCount, _setVisibleCount] = + React.useState(initialVisibleCount); + const [visibleCountGuessRange, setVisibleCountGuessRange] = + React.useState(initialVisibleCountGuessRange); + + const isStabilized = visibleCountGuessRange == null; + + /** + * Ensures that `visibleCount <= itemsLength` + */ + const setVisibleCount = React.useCallback( + (setStateAction: React.SetStateAction) => { + _setVisibleCount((prev) => { + const newVisibleCount = + typeof setStateAction === 'function' + ? setStateAction(prev) + : setStateAction; + + return Math.min(newVisibleCount, itemsLength); + }); + }, + [itemsLength], + ); + + const [resizeRef] = useResizeObserver( + React.useCallback(() => { + setVisibleCount(initialVisibleCount); + setVisibleCountGuessRange(initialVisibleCountGuessRange); + }, [initialVisibleCount, initialVisibleCountGuessRange, setVisibleCount]), + ); + + /** + * Call this function to guess the new `visibleCount`. + * The `visibleCount` is not changed if the correct `visibleCount` has already been found. + */ + const isGuessing = React.useRef(false); + const guessVisibleCount = React.useCallback(() => { + // If disabled or already stabilized + if (disabled || isStabilized || isGuessing.current) { + return; + } + + // We need to wait for the ref to be attached so that we can measure available and required sizes. + if (containerRef.current == null) { + return; + } + + try { + isGuessing.current = true; + + const dimension = orientation === 'horizontal' ? 'Width' : 'Height'; + const availableSize = containerRef.current[`offset${dimension}`]; + const requiredSize = containerRef.current[`scroll${dimension}`]; + + const isOverflowing = availableSize < requiredSize; + + console.log('RUNNING', { + visibleCountGuessRange: visibleCountGuessRange?.toString(), + containerRefNotNull: containerRef.current != null, + // isOverflowing, + visibleCount, + availableSize, + requiredSize, + }); + + // We have already found the correct visibleCount + if ( + (visibleCount === itemsLength && !isOverflowing) || + visibleCountGuessRange[1] - visibleCountGuessRange[0] === 1 // TODO: I think this causes issue when item count is 1 and so the initial range is [0, 1] + ) { + console.log('STABILIZED'); + setVisibleCountGuessRange(null); + return; + } + + // Before the main logic, the max guess MUST be >= the correct visibleCount for the algorithm to work. + // If not: + // - double the max guess and visibleCount: since we need to overflow. + // - set min guess to current visibleCount: since underflow means correct visibleCount >= current visibleCount. + if (visibleCountGuessRange[1] === visibleCount && !isOverflowing) { + const doubleOfMaxGuess = visibleCountGuessRange[1] * 2; + + setVisibleCountGuessRange([visibleCount, doubleOfMaxGuess]); + setVisibleCount(doubleOfMaxGuess); + + return; + } + + let newVisibleCountGuessRange = visibleCountGuessRange; + + if (isOverflowing) { + // overflowing = we guessed too high. So, new max guess = half the current guess + newVisibleCountGuessRange = [visibleCountGuessRange[0], visibleCount]; + } else { + // not overflowing = maybe we guessed too low? So, new min guess = half of current guess + newVisibleCountGuessRange = [visibleCount, visibleCountGuessRange[1]]; + } + + setVisibleCountGuessRange(newVisibleCountGuessRange); + + // Next guess is always the middle of the new guess range + setVisibleCount( + Math.floor( + (newVisibleCountGuessRange[0] + newVisibleCountGuessRange[1]) / 2, + ), + ); + } finally { + isGuessing.current = false; + } + }, [ + containerRef, + disabled, + isStabilized, + itemsLength, + orientation, + setVisibleCount, + visibleCount, + visibleCountGuessRange, + ]); + + const previousVisibleCount = useLatestRef(visibleCount); + const previousVisibleCountGuessRange = useLatestRef(visibleCountGuessRange); + const previousContainer = useLatestRef(containerRef.current); + + useLayoutEffect(() => { + if (disabled || isStabilized) { + return; + } + + if ( + visibleCount !== previousVisibleCount.current || + // TODO: Better list value comparison + visibleCountGuessRange.toString() !== + previousVisibleCountGuessRange.current?.toString() || + containerRef.current !== previousContainer.current + ) { + previousVisibleCount.current = visibleCount; + previousVisibleCountGuessRange.current = visibleCountGuessRange; + previousContainer.current = containerRef.current; + + guessVisibleCount(); + } + }, [ + disabled, + guessVisibleCount, + isStabilized, + previousContainer, + previousVisibleCount, + previousVisibleCountGuessRange, + visibleCount, + visibleCountGuessRange, + ]); + + const mergedRefs = useMergedRefs(containerRef, resizeRef); + + // return [containerRef, resizeRef, visibleCount] as const; + return [mergedRefs, visibleCount] as const; +}; diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.test.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.test.tsx deleted file mode 100644 index 8cfb8bd3722..00000000000 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.test.tsx +++ /dev/null @@ -1,221 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Bentley Systems, Incorporated. All rights reserved. - * See LICENSE.md in the project root for license terms and full copyright notice. - *--------------------------------------------------------------------------------------------*/ -import { act, render, waitFor } from '@testing-library/react'; -import * as React from 'react'; -import { useOverflow } from './useOverflow.js'; -import * as UseResizeObserver from './useResizeObserver.js'; - -const MockComponent = ({ - children, - disableOverflow = false, - orientation = 'horizontal', -}: { - children: React.ReactNode[] | string; - disableOverflow?: boolean; - orientation?: 'horizontal' | 'vertical'; -}) => { - const [overflowRef, visibleCount] = useOverflow( - children.length, - disableOverflow, - orientation, - ); - return
{children.slice(0, visibleCount)}
; -}; - -afterEach(() => { - vi.restoreAllMocks(); -}); - -it.each(['horizontal', 'vertical'] as const)( - 'should overflow when there is not enough space (%s)', - async (orientation) => { - const dimension = orientation === 'horizontal' ? 'Width' : 'Height'; - vi.spyOn(HTMLDivElement.prototype, `scroll${dimension}`, 'get') - .mockReturnValueOnce(120) - .mockReturnValue(100); - vi.spyOn( - HTMLDivElement.prototype, - `offset${dimension}`, - 'get', - ).mockReturnValue(100); - vi.spyOn( - HTMLSpanElement.prototype, - `offset${dimension}`, - 'get', - ).mockReturnValue(25); - - const { container } = render( - - {[...Array(5)].map((_, i) => ( - Test {i} - ))} - , - ); - - await waitFor(() => { - expect(container.querySelectorAll('span')).toHaveLength(4); - }); - }, -); - -it.only('should overflow when there is not enough space (string)', async () => { - const fullText = 'This is a very long text.'; - const truncatedText = 'This is a v'; - - vi.spyOn(HTMLDivElement.prototype, 'scrollWidth', 'get').mockReturnValue( - 2.5 * truncatedText.length, - ); - // .mockReturnValueOnce(50) - // .mockReturnValue(28); - vi.spyOn(HTMLDivElement.prototype, 'offsetWidth', 'get').mockReturnValue( - 2.5 * fullText.length, - ); - - // 20 symbols (default value taken), 50 width - // avg 2.5px per symbol - const { container } = render({fullText}); - - // have 28px of a place - // 11 symbols can fit - await waitFor(() => { - expect(container.textContent).toBe(truncatedText); - }); -}); - -it('should overflow when there is not enough space but container fits 30 items', async () => { - vi.spyOn(HTMLDivElement.prototype, 'scrollWidth', 'get') - .mockReturnValueOnce(300) - .mockReturnValueOnce(600) - .mockReturnValue(300); - vi.spyOn(HTMLDivElement.prototype, 'offsetWidth', 'get').mockReturnValue(300); - vi.spyOn(HTMLSpanElement.prototype, 'offsetWidth', 'get').mockReturnValue(10); - - const { container } = render( - - {[...Array(100)].map((_, i) => ( - Test {i} - ))} - , - ); - - await waitFor(() => { - expect(container.querySelectorAll('span')).toHaveLength(30); - }); -}); - -it('should restore hidden items when space is available again', async () => { - let onResizeFn: (size: DOMRectReadOnly) => void = vi.fn(); - vi.spyOn(UseResizeObserver, 'useResizeObserver').mockImplementation( - (onResize) => { - onResizeFn = onResize; - return [vi.fn(), { disconnect: vi.fn() } as unknown as ResizeObserver]; - }, - ); - const scrollWidthSpy = vi - .spyOn(HTMLDivElement.prototype, 'scrollWidth', 'get') - .mockReturnValueOnce(120) - .mockReturnValue(100); - const offsetWidthSpy = vi - .spyOn(HTMLDivElement.prototype, 'offsetWidth', 'get') - .mockReturnValue(100); - vi.spyOn(HTMLSpanElement.prototype, 'offsetWidth', 'get').mockReturnValue(25); - - const { container, rerender } = render( - - {[...Array(5)].map((_, i) => ( - Test {i} - ))} - , - ); - - await waitFor(() => { - expect(container.querySelectorAll('span')).toHaveLength(4); - }); - - scrollWidthSpy.mockReturnValue(125); - offsetWidthSpy.mockReturnValue(125); - rerender( - - {[...Array(5)].map((_, i) => ( - Test {i} - ))} - , - ); - - act(() => onResizeFn({ width: 125 } as DOMRectReadOnly)); - - await waitFor(() => { - expect(container.querySelectorAll('span')).toHaveLength(5); - }); -}); - -it('should not overflow when disabled', () => { - vi.spyOn(HTMLElement.prototype, 'scrollWidth', 'get') - .mockReturnValueOnce(120) - .mockReturnValue(100); - vi.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(100); - - const { container } = render( - - {[...Array(50)].map((_, i) => ( - Test {i} - ))} - , - ); - - expect(container.querySelectorAll('span')).toHaveLength(50); -}); - -it('should hide items and then show them all when overflow is disabled', async () => { - vi.spyOn(HTMLDivElement.prototype, 'scrollWidth', 'get') - .mockReturnValueOnce(300) - .mockReturnValueOnce(600) - .mockReturnValue(300); - vi.spyOn(HTMLDivElement.prototype, 'offsetWidth', 'get').mockReturnValue(300); - vi.spyOn(HTMLSpanElement.prototype, 'offsetWidth', 'get').mockReturnValue(10); - - const { container, rerender } = render( - - {[...Array(100)].map((_, i) => ( - Test {i} - ))} - , - ); - - await waitFor(() => { - expect(container.querySelectorAll('span')).toHaveLength(30); - }); - - rerender( - - {[...Array(100)].map((_, i) => ( - Test {i} - ))} - , - ); - - await waitFor(() => { - expect(container.querySelectorAll('span')).toHaveLength(100); - }); -}); - -it('should return 1 when item is bigger than the container', () => { - vi.spyOn(HTMLDivElement.prototype, 'scrollWidth', 'get') - .mockReturnValueOnce(50) - .mockReturnValueOnce(100) - .mockReturnValue(50); - vi.spyOn(HTMLDivElement.prototype, 'offsetWidth', 'get').mockReturnValue(50); - vi.spyOn(HTMLSpanElement.prototype, 'offsetWidth', 'get').mockReturnValue(60); - - const { container } = render( - - {[...Array(5)].map((_, i) => ( - Test {i} - ))} - , - ); - - expect(container.querySelectorAll('span')).toHaveLength(1); -}); diff --git a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx b/packages/itwinui-react/src/utils/hooks/useOverflow.tsx deleted file mode 100644 index e738721294e..00000000000 --- a/packages/itwinui-react/src/utils/hooks/useOverflow.tsx +++ /dev/null @@ -1,213 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Bentley Systems, Incorporated. All rights reserved. - * See LICENSE.md in the project root for license terms and full copyright notice. - *--------------------------------------------------------------------------------------------*/ -import * as React from 'react'; -import { useLayoutEffect } from './useIsomorphicLayoutEffect.js'; -import { useResizeObserver } from './useResizeObserver.js'; -import { useLatestRef } from './useLatestRef.js'; -import { useMergedRefs } from './useMergedRefs.js'; - -/** `[number, number]` means that we're still guessing. `null` means that we got the correct `visibleCount`. */ -type GuessRange = [number, number] | null; - -/** First guess of the number of items that overflows. We refine this guess with subsequent renders. */ -const STARTING_MAX_ITEMS_COUNT = 2; - -/** - * Hook that returns the number of items that should be visible based on the size of the container element. - * - * The returned number should be used to render the element with fewer items. - * - * @private - * @param items Items that this element contains. - * @param disabled Set to true to disconnect the observer. - * @param dimension 'horizontal' (default) or 'vertical' - * @returns [callback ref to set on container, stateful count of visible items] - * - * @example - * const items = Array(10).fill().map((_, i) => Item {i}); - * const [ref, visibleCount] = useOverflow(items); - * ... - * return ( - *
- * {items.slice(0, visibleCount)} - *
- * ); - */ -export const useOverflow = ( - // TODO: Try more to remove this prop, if possible. - itemsLength: number, - disabled = false, - orientation: 'horizontal' | 'vertical' = 'horizontal', -) => { - const containerRef = React.useRef(null); - - const initialVisibleCount = React.useMemo( - () => - disabled ? itemsLength : Math.min(itemsLength, STARTING_MAX_ITEMS_COUNT), - [disabled, itemsLength], - ); - const initialVisibleCountGuessRange = React.useMemo( - () => (disabled ? null : ([0, initialVisibleCount] satisfies GuessRange)), - [disabled, initialVisibleCount], - ); - - const [visibleCount, _setVisibleCount] = - React.useState(initialVisibleCount); - const [visibleCountGuessRange, setVisibleCountGuessRange] = - React.useState(initialVisibleCountGuessRange); - - const isStabilized = visibleCountGuessRange == null; - - /** - * Ensures that `visibleCount <= itemsLength` - */ - const setVisibleCount = React.useCallback( - (setStateAction: React.SetStateAction) => { - _setVisibleCount((prev) => { - const newVisibleCount = - typeof setStateAction === 'function' - ? setStateAction(prev) - : setStateAction; - - return Math.min(newVisibleCount, itemsLength); - }); - }, - [itemsLength], - ); - - const [resizeRef] = useResizeObserver( - React.useCallback(() => { - setVisibleCount(initialVisibleCount); - setVisibleCountGuessRange(initialVisibleCountGuessRange); - }, [initialVisibleCount, initialVisibleCountGuessRange, setVisibleCount]), - ); - - /** - * Call this function to guess the new `visibleCount`. - * The `visibleCount` is not changed if the correct `visibleCount` has already been found. - */ - const isGuessing = React.useRef(false); - const guessVisibleCount = React.useCallback(() => { - // If disabled or already stabilized - if (disabled || isStabilized || isGuessing.current) { - return; - } - - // We need to wait for the ref to be attached so that we can measure available and required sizes. - if (containerRef.current == null) { - return; - } - - try { - isGuessing.current = true; - - const dimension = orientation === 'horizontal' ? 'Width' : 'Height'; - const availableSize = containerRef.current[`offset${dimension}`]; - const requiredSize = containerRef.current[`scroll${dimension}`]; - - const isOverflowing = availableSize < requiredSize; - - console.log('RUNNING', { - visibleCountGuessRange: visibleCountGuessRange?.toString(), - containerRefNotNull: containerRef.current != null, - // isOverflowing, - visibleCount, - availableSize, - requiredSize, - }); - - // We have already found the correct visibleCount - if ( - (visibleCount === itemsLength && !isOverflowing) || - visibleCountGuessRange[1] - visibleCountGuessRange[0] === 1 // TODO: I think this causes issue when item count is 1 and so the initial range is [0, 1] - ) { - console.log('STABILIZED'); - setVisibleCountGuessRange(null); - return; - } - - // Before the main logic, the max guess MUST be >= the correct visibleCount for the algorithm to work. - // If not: - // - double the max guess and visibleCount: since we need to overflow. - // - set min guess to current visibleCount: since underflow means correct visibleCount >= current visibleCount. - if (visibleCountGuessRange[1] === visibleCount && !isOverflowing) { - const doubleOfMaxGuess = visibleCountGuessRange[1] * 2; - - setVisibleCountGuessRange([visibleCount, doubleOfMaxGuess]); - setVisibleCount(doubleOfMaxGuess); - - return; - } - - let newVisibleCountGuessRange = visibleCountGuessRange; - - if (isOverflowing) { - // overflowing = we guessed too high. So, new max guess = half the current guess - newVisibleCountGuessRange = [visibleCountGuessRange[0], visibleCount]; - } else { - // not overflowing = maybe we guessed too low? So, new min guess = half of current guess - newVisibleCountGuessRange = [visibleCount, visibleCountGuessRange[1]]; - } - - setVisibleCountGuessRange(newVisibleCountGuessRange); - - // Next guess is always the middle of the new guess range - setVisibleCount( - Math.floor( - (newVisibleCountGuessRange[0] + newVisibleCountGuessRange[1]) / 2, - ), - ); - } finally { - isGuessing.current = false; - } - }, [ - containerRef, - disabled, - isStabilized, - itemsLength, - orientation, - setVisibleCount, - visibleCount, - visibleCountGuessRange, - ]); - - const previousVisibleCount = useLatestRef(visibleCount); - const previousVisibleCountGuessRange = useLatestRef(visibleCountGuessRange); - const previousContainer = useLatestRef(containerRef.current); - - useLayoutEffect(() => { - if (disabled || isStabilized) { - return; - } - - if ( - visibleCount !== previousVisibleCount.current || - // TODO: Better list value comparison - visibleCountGuessRange.toString() !== - previousVisibleCountGuessRange.current?.toString() || - containerRef.current !== previousContainer.current - ) { - previousVisibleCount.current = visibleCount; - previousVisibleCountGuessRange.current = visibleCountGuessRange; - previousContainer.current = containerRef.current; - - guessVisibleCount(); - } - }, [ - disabled, - guessVisibleCount, - isStabilized, - previousContainer, - previousVisibleCount, - previousVisibleCountGuessRange, - visibleCount, - visibleCountGuessRange, - ]); - - const mergedRefs = useMergedRefs(containerRef, resizeRef); - - // return [containerRef, resizeRef, visibleCount] as const; - return [mergedRefs, visibleCount] as const; -}; diff --git a/packages/itwinui-react/src/utils/hooks/useResizeObserver.tsx b/packages/itwinui-react/src/utils/hooks/useResizeObserver.tsx index 6c66402f437..b08c7c9e4c9 100644 --- a/packages/itwinui-react/src/utils/hooks/useResizeObserver.tsx +++ b/packages/itwinui-react/src/utils/hooks/useResizeObserver.tsx @@ -40,10 +40,7 @@ export const useResizeObserver = ( } const [{ contentRect }] = entries; - - // setTimeout(() => { return onResize(contentRect); - // }, 1000); }); }); resizeObserver.current?.observe?.(element); From a96ee4613ac94cf25f6c59936ce3c6f7bd7b4921 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Tue, 16 Jul 2024 07:11:54 -0400 Subject: [PATCH 45/65] Remaining e2e tests --- testing/e2e/app/routes/Breadcrumbs/spec.ts | 15 +- testing/e2e/app/routes/ButtonGroup/spec.ts | 21 +- testing/e2e/app/routes/ComboBox/route.tsx | 41 +++ testing/e2e/app/routes/ComboBox/spec.ts | 111 +++++++ .../app/routes/MiddleTextTruncation/spec.ts | 10 +- testing/e2e/app/routes/Select/route.tsx | 41 +++ testing/e2e/app/routes/Select/spec.ts | 113 +++++++ testing/e2e/app/routes/Table/route.tsx | 311 +++++++++++------- testing/e2e/app/routes/Table/spec.ts | 132 +++++++- 9 files changed, 658 insertions(+), 137 deletions(-) create mode 100644 testing/e2e/app/routes/ComboBox/route.tsx create mode 100644 testing/e2e/app/routes/ComboBox/spec.ts create mode 100644 testing/e2e/app/routes/Select/route.tsx create mode 100644 testing/e2e/app/routes/Select/spec.ts diff --git a/testing/e2e/app/routes/Breadcrumbs/spec.ts b/testing/e2e/app/routes/Breadcrumbs/spec.ts index 28db1adaea2..2ab05c096eb 100644 --- a/testing/e2e/app/routes/Breadcrumbs/spec.ts +++ b/testing/e2e/app/routes/Breadcrumbs/spec.ts @@ -40,11 +40,6 @@ test.describe('Breadcrumbs', () => { const setContainerSize = getSetContainerSize(page); const expectOverflowState = getExpectOverflowState(page); - await page.locator('#container').evaluate((element) => { - element.style.overflow = 'hidden'; - }); - await page.waitForTimeout(100); - await expectOverflowState({ expectedItemLength: 5, expectedOverflowButtonVisibleCount: undefined, @@ -66,11 +61,13 @@ const getSetContainerSize = (page: Page) => { return async (dimension: string | undefined) => { await page.locator('#container').evaluate( (element, args) => { - element.style.width = args.dimension ? args.dimension : `999px`; - }, - { - dimension, + if (args.dimension != null) { + element.style.setProperty('width', args.dimension); + } else { + element.style.removeProperty('width'); + } }, + { dimension }, ); }; }; diff --git a/testing/e2e/app/routes/ButtonGroup/spec.ts b/testing/e2e/app/routes/ButtonGroup/spec.ts index 48a31471eec..71c9ddc22d6 100644 --- a/testing/e2e/app/routes/ButtonGroup/spec.ts +++ b/testing/e2e/app/routes/ButtonGroup/spec.ts @@ -100,7 +100,7 @@ test.describe('ButtonGroup', () => { }); await setContainerSize(2.5); - await page.waitForTimeout(100); + await page.waitForTimeout(100); // TODO: Try removing all timeouts await expectOverflowState({ expectedButtonLength: 2, @@ -207,17 +207,20 @@ const getSetContainerSize = ( await page.locator('#container').evaluate( (element, args) => { if (args.orientation === 'horizontal') { - element.style.width = - args.multiplier != null ? `${50 * args.multiplier}px` : `999px`; + if (args.multiplier != null) { + element.style.setProperty('width', `${50 * args.multiplier}px`); + } else { + element.style.removeProperty('width'); + } } else { - element.style.height = - args.multiplier != null ? `${36 * args.multiplier}px` : `999px`; + if (args.multiplier != null) { + element.style.setProperty('height', `${36 * args.multiplier}px`); + } else { + element.style.removeProperty('height'); + } } }, - { - orientation, - multiplier, - }, + { orientation, multiplier }, ); }; }; diff --git a/testing/e2e/app/routes/ComboBox/route.tsx b/testing/e2e/app/routes/ComboBox/route.tsx new file mode 100644 index 00000000000..013cf5e61ad --- /dev/null +++ b/testing/e2e/app/routes/ComboBox/route.tsx @@ -0,0 +1,41 @@ +import { ComboBox } from '@itwin/itwinui-react'; +import { useSearchParams } from '@remix-run/react'; +import React from 'react'; + +export default function BreadcrumbsTest() { + const [searchParams] = useSearchParams(); + + const options = [ + ...Array(9) + .fill(null) + .map((_, index) => { + return { + label: `option ${index}`, + value: index, + }; + }), + { + label: 'Very long option', + value: 9, + }, + ]; + + const valueSearchParam = searchParams.get('value'); + const value = + valueSearchParam != null + ? JSON.parse(valueSearchParam) + : options.map((option) => option.value); + + return ( + <> +
+ option.value)} + multiple + /> +
+ + ); +} diff --git a/testing/e2e/app/routes/ComboBox/spec.ts b/testing/e2e/app/routes/ComboBox/spec.ts new file mode 100644 index 00000000000..cdb37383aad --- /dev/null +++ b/testing/e2e/app/routes/ComboBox/spec.ts @@ -0,0 +1,111 @@ +import { test, expect, Page } from '@playwright/test'; + +test.describe('ComboBox', () => { + test(`should overflow whenever there is not enough space`, async ({ + page, + }) => { + await page.goto(`/ComboBox`); + + const setContainerSize = getSetContainerSize(page); + const expectOverflowState = getExpectOverflowState(page); + + await page.waitForTimeout(100); + + await expectOverflowState({ + expectedItemLength: 10, + expectedLastTagTextContent: 'Very long option', + }); + + await setContainerSize('600px'); + await page.waitForTimeout(100); + + await expectOverflowState({ + expectedItemLength: 5, + expectedLastTagTextContent: '+6 item(s)', + }); + }); + + test(`should at minimum always show one overflow tag`, async ({ page }) => { + await page.goto(`/ComboBox`); + + const setContainerSize = getSetContainerSize(page); + const expectOverflowState = getExpectOverflowState(page); + + await page.waitForTimeout(100); + + await expectOverflowState({ + expectedItemLength: 10, + expectedLastTagTextContent: 'Very long option', + }); + + await setContainerSize('10px'); + await page.waitForTimeout(100); + + await expectOverflowState({ + expectedItemLength: 1, + expectedLastTagTextContent: '+10 item(s)', + }); + }); + + test(`should always show the selected tag and no overflow tag when only one item is selected`, async ({ + page, + }) => { + await page.goto(`/ComboBox?value=[9]`); + + const setContainerSize = getSetContainerSize(page); + const expectOverflowState = getExpectOverflowState(page); + + await page.waitForTimeout(100); + + await expectOverflowState({ + expectedItemLength: 1, + expectedLastTagTextContent: 'Very long option', + }); + + await setContainerSize('80px'); + await page.waitForTimeout(100); + + await expectOverflowState({ + expectedItemLength: 1, + expectedLastTagTextContent: 'Very long option', + }); + }); +}); + +// ---------------------------------------------------------------------------- + +const getSetContainerSize = (page: Page) => { + return async (dimension: string | undefined) => { + await page.locator('#container').evaluate( + (element, args) => { + if (args.dimension != null) { + element.style.setProperty('width', args.dimension); + } else { + element.style.removeProperty('width'); + } + }, + { dimension }, + ); + }; +}; + +const getExpectOverflowState = (page: Page) => { + return async ({ + expectedItemLength, + expectedLastTagTextContent, + }: { + expectedItemLength: number; + expectedLastTagTextContent: string | undefined; + }) => { + const tags = await page.locator('div[id$="-selected-live"] > span').all(); + expect(tags).toHaveLength(expectedItemLength); + + const lastTag = tags[tags.length - 1]; + + if (expectedLastTagTextContent != null) { + expect(await lastTag.textContent()).toBe(expectedLastTagTextContent); + } else { + expect(tags).toHaveLength(0); + } + }; +}; diff --git a/testing/e2e/app/routes/MiddleTextTruncation/spec.ts b/testing/e2e/app/routes/MiddleTextTruncation/spec.ts index 3e8c684ad9a..716f5bd4e3e 100644 --- a/testing/e2e/app/routes/MiddleTextTruncation/spec.ts +++ b/testing/e2e/app/routes/MiddleTextTruncation/spec.ts @@ -72,11 +72,13 @@ const getSetContainerSize = (page: Page) => { return async (dimension: string | undefined) => { await page.getByTestId('root').evaluate( (element, args) => { - element.style.width = args.dimension != null ? args.dimension : `999px`; - }, - { - dimension, + if (args.dimension != null) { + element.style.setProperty('width', args.dimension); + } else { + element.style.removeProperty('width'); + } }, + { dimension }, ); }; }; diff --git a/testing/e2e/app/routes/Select/route.tsx b/testing/e2e/app/routes/Select/route.tsx new file mode 100644 index 00000000000..03f92192160 --- /dev/null +++ b/testing/e2e/app/routes/Select/route.tsx @@ -0,0 +1,41 @@ +import { Select } from '@itwin/itwinui-react'; +import { useSearchParams } from '@remix-run/react'; +import React from 'react'; + +export default function BreadcrumbsTest() { + const [searchParams] = useSearchParams(); + + const options = [ + ...Array(9) + .fill(null) + .map((_, index) => { + return { + label: `option ${index}`, + value: `index`, + }; + }), + { + label: 'Very long option', + value: 9, + }, + ]; + + const valueSearchParam = searchParams.get('value'); + const value = + valueSearchParam != null + ? JSON.parse(valueSearchParam) + : options.map((option) => option.value); + + return ( + <> +
+ { }); await setContainerSize('600px'); - await page.waitForTimeout(30); await expectOverflowState({ expectedItemLength: 6, @@ -39,7 +38,6 @@ test.describe('Select', () => { }); await setContainerSize('10px'); - await page.waitForTimeout(30); await expectOverflowState({ expectedItemLength: 1, @@ -63,7 +61,6 @@ test.describe('Select', () => { }); await setContainerSize('160px'); - await page.waitForTimeout(30); await expectOverflowState({ expectedItemLength: 1, @@ -86,6 +83,7 @@ const getSetContainerSize = (page: Page) => { }, { dimension }, ); + await page.waitForTimeout(30); }; }; diff --git a/testing/e2e/app/routes/Table/route.tsx b/testing/e2e/app/routes/Table/route.tsx index 85634eb585a..5a406b8f95d 100644 --- a/testing/e2e/app/routes/Table/route.tsx +++ b/testing/e2e/app/routes/Table/route.tsx @@ -121,53 +121,51 @@ export default function Resizing() { return ( <> -
- - rows.findIndex((row) => row.original === data[scrollRow]) - : undefined - } - /> - +
+ rows.findIndex((row) => row.original === data[scrollRow]) + : undefined + } + /> ); }; @@ -192,35 +190,15 @@ export default function Resizing() { [], ); - type TableDataType = { - name: string; - description: string; - subRows: TableDataType[]; - }; - - const generateItem = React.useCallback( - (index: number, parentRow = '', depth = 0): TableDataType => { - const keyValue = parentRow ? `${parentRow}.${index}` : `${index}`; - return { - name: `Name ${keyValue}`, - description: `Description ${keyValue}`, - subRows: - depth < 2 - ? Array(Math.round(index % 5)) - .fill(null) - .map((_, index) => generateItem(index, keyValue, depth + 1)) - : [], - }; - }, - [], - ); - const data = React.useMemo( () => Array(505) .fill(null) - .map((_, index) => generateItem(index)), - [generateItem], + .map((_, index) => ({ + name: `Name ${index}`, + description: `Description ${index}`, + })), + [], ); const paginator = React.useCallback( @@ -232,17 +210,15 @@ export default function Resizing() { return ( <> -
+
+
+ ); }; diff --git a/testing/e2e/app/routes/Table/spec.ts b/testing/e2e/app/routes/Table/spec.ts index 4b219c9785d..edd30b67213 100644 --- a/testing/e2e/app/routes/Table/spec.ts +++ b/testing/e2e/app/routes/Table/spec.ts @@ -379,48 +379,6 @@ test.describe('Table row selection', () => { }); test.describe('Table Paginator', () => { - const getSetContainerSize = (page: Page) => { - return async (dimension: string | undefined) => { - await page.locator('[role="table"]').evaluate( - (element, args) => { - if (args.dimension != null) { - element.style.setProperty( - 'width', - args.dimension ? args.dimension : `999px`, - ); - } else { - element.style.removeProperty('width'); - } - }, - { - dimension, - }, - ); - }; - }; - - const getExpectOverflowState = (page: Page) => { - return async ({ - expectedItemLength, - expectedOverflowingEllipsisVisibleCount, - }: { - expectedItemLength: number; - expectedOverflowingEllipsisVisibleCount: number; - }) => { - const allItems = await page.locator('#paginator button').all(); - const items = - allItems.length >= 2 - ? allItems.slice(1, allItems.length - 1) // since the first and last button and to toggle pages - : []; - expect(items).toHaveLength(expectedItemLength); - - const overflowingEllipsis = page.getByText('…'); - expect(overflowingEllipsis).toHaveCount( - expectedOverflowingEllipsisVisibleCount, - ); - }; - }; - test(`should overflow whenever there is not enough space`, async ({ page, }) => { @@ -429,15 +387,12 @@ test.describe('Table Paginator', () => { const setContainerSize = getSetContainerSize(page); const expectOverflowState = getExpectOverflowState(page); - await page.waitForTimeout(30); - await expectOverflowState({ expectedItemLength: 11, expectedOverflowingEllipsisVisibleCount: 0, }); await setContainerSize('750px'); - await page.waitForTimeout(30); await expectOverflowState({ expectedItemLength: 6, @@ -446,7 +401,6 @@ test.describe('Table Paginator', () => { // should restore hidden items when space is available again await setContainerSize(undefined); - await page.waitForTimeout(30); await expectOverflowState({ expectedItemLength: 11, @@ -460,15 +414,12 @@ test.describe('Table Paginator', () => { const setContainerSize = getSetContainerSize(page); const expectOverflowState = getExpectOverflowState(page); - await page.waitForTimeout(30); - await expectOverflowState({ expectedItemLength: 11, expectedOverflowingEllipsisVisibleCount: 0, }); await setContainerSize('10px'); - await page.waitForTimeout(30); await expectOverflowState({ expectedItemLength: 1, @@ -484,21 +435,63 @@ test.describe('Table Paginator', () => { const setContainerSize = getSetContainerSize(page); const expectOverflowState = getExpectOverflowState(page); - await page.waitForTimeout(30); - await expectOverflowState({ expectedItemLength: 11, expectedOverflowingEllipsisVisibleCount: 0, }); await setContainerSize('10px'); - await page.waitForTimeout(30); await expectOverflowState({ expectedItemLength: 1, expectedOverflowingEllipsisVisibleCount: 0, }); }); + + //#region Helpers for table paginator tests + const getSetContainerSize = (page: Page) => { + return async (dimension: string | undefined) => { + await page.locator('#container').evaluate( + (element, args) => { + if (args.dimension != null) { + element.style.setProperty( + 'width', + args.dimension ? args.dimension : `999px`, + ); + } else { + element.style.removeProperty('width'); + } + }, + { + dimension, + }, + ); + await page.waitForTimeout(30); + }; + }; + + const getExpectOverflowState = (page: Page) => { + return async ({ + expectedItemLength, + expectedOverflowingEllipsisVisibleCount, + }: { + expectedItemLength: number; + expectedOverflowingEllipsisVisibleCount: number; + }) => { + const allItems = await page.locator('#paginator button').all(); + const items = + allItems.length >= 2 + ? allItems.slice(1, allItems.length - 1) // since the first and last button and to toggle pages + : []; + expect(items).toHaveLength(expectedItemLength); + + const overflowingEllipsis = page.getByText('…'); + expect(overflowingEllipsis).toHaveCount( + expectedOverflowingEllipsisVisibleCount, + ); + }; + }; + //#endregion }); test.describe('Virtual Scroll Tests', () => { From eb97cbf41bde052d2cce74575d3bbd51a4d915fc Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Tue, 16 Jul 2024 17:05:09 -0400 Subject: [PATCH 53/65] Set STARTING_MAX_ITEMS_COUNT = 32 --- .../src/utils/components/OverflowContainer.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx index 8614133ff41..ff149014aeb 100644 --- a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx +++ b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx @@ -147,12 +147,6 @@ export const OverflowContainer = React.forwardRef((props, ref) => { // ---------------------------------------------------------------------------- -/** `[number, number]` means that we're still guessing. `null` means that we got the correct `visibleCount`. */ -type GuessRange = [number, number] | null; - -/** First guess of the number of items that overflows. We refine this guess with subsequent renders. */ -const STARTING_MAX_ITEMS_COUNT = 2; - /** * Hook that returns the number of items that should be visible based on the size of the container element. * @@ -179,6 +173,12 @@ const useOverflow = ( disabled = false, orientation: 'horizontal' | 'vertical' = 'horizontal', ) => { + /** `[number, number]` means that we're still guessing. `null` means that we got the correct `visibleCount`. */ + type GuessRange = [number, number] | null; + + /** First guess of the number of items that overflows. We refine this guess with subsequent renders. */ + const STARTING_MAX_ITEMS_COUNT = 32; + const containerRef = React.useRef(null); const initialVisibleCount = React.useMemo( From 5926d74d71a6ed7333f0e1a8b9176fa1177acead Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Tue, 16 Jul 2024 17:07:46 -0400 Subject: [PATCH 54/65] Cleanup --- .../src/utils/components/OverflowContainer.tsx | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx index ff149014aeb..7b51ce76033 100644 --- a/packages/itwinui-react/src/utils/components/OverflowContainer.tsx +++ b/packages/itwinui-react/src/utils/components/OverflowContainer.tsx @@ -222,11 +222,12 @@ const useOverflow = ( }, [initialVisibleCount, initialVisibleCountGuessRange, setVisibleCount]), ); + const isGuessing = React.useRef(false); + /** * Call this function to guess the new `visibleCount`. * The `visibleCount` is not changed if the correct `visibleCount` has already been found. */ - const isGuessing = React.useRef(false); const guessVisibleCount = React.useCallback(() => { // If disabled or already stabilized if (disabled || isStabilized || isGuessing.current) { @@ -247,15 +248,6 @@ const useOverflow = ( const isOverflowing = availableSize < requiredSize; - console.log('RUNNING', { - visibleCountGuessRange: visibleCountGuessRange?.toString(), - containerRefNotNull: containerRef.current != null, - // isOverflowing, - visibleCount, - availableSize, - requiredSize, - }); - // We have already found the correct visibleCount if ( (visibleCount === itemsLength && !isOverflowing) || @@ -263,7 +255,6 @@ const useOverflow = ( (visibleCountGuessRange[1] - visibleCountGuessRange[0] === 1 && visibleCount === visibleCountGuessRange[0]) ) { - console.log('STABILIZED'); setVisibleCountGuessRange(null); return; } From c3e75ffb3fe628d9f61c31fc3ab1025ef68dbcba Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Tue, 16 Jul 2024 17:20:41 -0400 Subject: [PATCH 55/65] Remove unnecessary changes --- .changeset/cool-beds-hunt.md | 5 ----- .changeset/fifty-seahorses-dance.md | 5 ----- .changeset/rare-buckets-mate.md | 14 -------------- .changeset/silver-ways-enjoy.md | 5 ----- .changeset/violet-rats-breathe.md | 5 ----- .changeset/wet-trainers-heal.md | 5 ----- .../src/core/ButtonGroup/ButtonGroup.tsx | 8 -------- 7 files changed, 47 deletions(-) delete mode 100644 .changeset/cool-beds-hunt.md delete mode 100644 .changeset/fifty-seahorses-dance.md delete mode 100644 .changeset/rare-buckets-mate.md delete mode 100644 .changeset/silver-ways-enjoy.md delete mode 100644 .changeset/violet-rats-breathe.md delete mode 100644 .changeset/wet-trainers-heal.md diff --git a/.changeset/cool-beds-hunt.md b/.changeset/cool-beds-hunt.md deleted file mode 100644 index 7aad2f273be..00000000000 --- a/.changeset/cool-beds-hunt.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@itwin/itwinui-react': patch ---- - -Development-only warnings will now be displayed when multiple versions of iTwinUI are detected. diff --git a/.changeset/fifty-seahorses-dance.md b/.changeset/fifty-seahorses-dance.md deleted file mode 100644 index 48ad796da2a..00000000000 --- a/.changeset/fifty-seahorses-dance.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@itwin/itwinui-react': minor ---- - -Updates `ComboBox` virtualization to support dynamic sizing. This change fixes an issue where having options both with and without `subLabel` values would cause `ComboBox` components with virtualization enabled to be sized incorrectly. diff --git a/.changeset/rare-buckets-mate.md b/.changeset/rare-buckets-mate.md deleted file mode 100644 index a773b095b11..00000000000 --- a/.changeset/rare-buckets-mate.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@itwin/itwinui-react': minor ---- - -Replaced old virtualization implementation with `@tanstack/react-virtual` for the `Tree` component. Also adds `overflow: 'auto'` to the style of the outer `Tree` div when the `Tree` is virtualized, removing the need for a wrapping scrollable element. - -```diff --
- --
-``` diff --git a/.changeset/silver-ways-enjoy.md b/.changeset/silver-ways-enjoy.md deleted file mode 100644 index 231c2d97e97..00000000000 --- a/.changeset/silver-ways-enjoy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@itwin/itwinui-react': patch ---- - -Added `overflow: auto` to `Tree` component to provide more consistent styling across components. diff --git a/.changeset/violet-rats-breathe.md b/.changeset/violet-rats-breathe.md deleted file mode 100644 index 721417b2856..00000000000 --- a/.changeset/violet-rats-breathe.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@itwin/itwinui-react': minor ---- - -Replaced old virtualization implementation with `@tanstack/react-virtual` for the `Table` component. This change also fixed some issues with `Table` virtualization, including the issue where scrolling would jump when rows are scrolled past in some cases. diff --git a/.changeset/wet-trainers-heal.md b/.changeset/wet-trainers-heal.md deleted file mode 100644 index 9819db97be9..00000000000 --- a/.changeset/wet-trainers-heal.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@itwin/itwinui-react': minor ---- - -Added dependency on `@tanstack/react-virtual`. diff --git a/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx b/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx index 2f24fcab2b6..a8a5b5448cf 100644 --- a/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx +++ b/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx @@ -210,14 +210,6 @@ const OverflowGroup = React.forwardRef((props, forwardedRef) => { ? items.length - visibleCount - 2 : visibleCount - 1; - console.log( - 'firstOverflowingIndex', - overflowPlacement === 'start', - firstOverflowingIndex, - items.length, - visibleCount, - ); - return overflowButton(firstOverflowingIndex); }} > From 1bca8043ed8aef00c5badee2173ca15405b418df Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Wed, 17 Jul 2024 07:03:31 -0400 Subject: [PATCH 56/65] Moved failing unit tests to e2e --- .../src/core/Breadcrumbs/Breadcrumbs.test.tsx | 64 +-- .../src/core/ButtonGroup/ButtonGroup.test.tsx | 141 +----- .../src/core/ButtonGroup/ButtonGroup.tsx | 1 - .../src/core/Table/Table.test.tsx | 36 -- testing/e2e/app/routes/ButtonGroup/route.tsx | 107 +++-- testing/e2e/app/routes/ButtonGroup/spec.ts | 441 ++++++++++++------ testing/e2e/app/routes/Select/route.tsx | 2 +- testing/e2e/app/routes/Table/route.tsx | 5 +- testing/e2e/app/routes/Table/spec.ts | 17 + 9 files changed, 387 insertions(+), 427 deletions(-) diff --git a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.test.tsx b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.test.tsx index 15eea9a0711..c322feaa9f2 100644 --- a/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.test.tsx +++ b/packages/itwinui-react/src/core/Breadcrumbs/Breadcrumbs.test.tsx @@ -3,12 +3,10 @@ * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import * as React from 'react'; -import { fireEvent, render, screen } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import { Breadcrumbs } from './Breadcrumbs.js'; -import { SvgChevronRight, SvgMore } from '../../utils/index.js'; -import * as UseOverflow from '../../utils/hooks/useOverflow.js'; -import { IconButton } from '../Buttons/IconButton.js'; +import { SvgChevronRight } from '../../utils/index.js'; import { Button } from '../Buttons/Button.js'; import { userEvent } from '@testing-library/user-event'; @@ -56,13 +54,6 @@ const assertBaseElement = ( }); }; -const useOverflowMock = vi - .spyOn(UseOverflow, 'useOverflow') - .mockImplementation((items) => [vi.fn(), items.length]); -beforeEach(() => { - useOverflowMock.mockImplementation((items) => [vi.fn(), items.length]); -}); - it('should render all elements in default state', () => { const { container } = renderComponent(); assertBaseElement(container); @@ -181,57 +172,6 @@ it('should accept currentIndex prop', () => { assertBaseElement(container, { currentIndex: 1 }); }); -it('should overflow when there is not enough space', () => { - useOverflowMock.mockReturnValue([vi.fn(), 2]); - const { container } = renderComponent(); - - expect(container.querySelector('.iui-breadcrumbs')).toBeTruthy(); - expect(container.querySelector('.iui-breadcrumbs-list')).toBeTruthy(); - - const breadcrumbs = container.querySelectorAll('.iui-breadcrumbs-item'); - expect(breadcrumbs.length).toEqual(3); - expect(breadcrumbs[0].textContent).toEqual('Item 0'); - expect(breadcrumbs[1].textContent).toEqual('…'); - expect(breadcrumbs[1].firstElementChild?.textContent).toContain('…'); - expect(breadcrumbs[2].textContent).toEqual('Item 2'); -}); - -it('should handle overflow when overflowButton is specified', () => { - const onClick = vi.fn(); - useOverflowMock.mockReturnValue([vi.fn(), 2]); - const { container } = renderComponent({ - overflowButton: (visibleCount) => ( - - - - ), - }); - - expect(container.querySelector('.iui-breadcrumbs')).toBeTruthy(); - expect(container.querySelector('.iui-breadcrumbs-list')).toBeTruthy(); - - const breadcrumbs = container.querySelectorAll('.iui-breadcrumbs-item'); - expect(breadcrumbs.length).toEqual(3); - fireEvent.click(breadcrumbs[1]); - expect(onClick).toHaveBeenCalledTimes(1); - expect(onClick).toHaveBeenCalledWith(2); -}); - -it('should show the last item when only one can be visible', () => { - useOverflowMock.mockReturnValue([vi.fn(), 1]); - - const { container } = renderComponent(); - - expect(container.querySelector('.iui-breadcrumbs')).toBeTruthy(); - expect(container.querySelector('.iui-breadcrumbs-list')).toBeTruthy(); - - const breadcrumbs = container.querySelectorAll('.iui-breadcrumbs-item'); - expect(breadcrumbs.length).toEqual(2); - expect(breadcrumbs[0].textContent).toEqual('…'); - expect(breadcrumbs[0].firstElementChild?.textContent).toContain('…'); - expect(breadcrumbs[1].textContent).toEqual('Item 2'); -}); - it('should support legacy api', async () => { const onClick = vi.fn(); diff --git a/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.test.tsx b/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.test.tsx index 44022720fb8..5f56f929178 100644 --- a/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.test.tsx +++ b/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.test.tsx @@ -2,13 +2,10 @@ * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ -import { SvgMore, SvgClose as SvgPlaceholder } from '../../utils/index.js'; -import { fireEvent, render } from '@testing-library/react'; -import * as UseOverflow from '../../utils/hooks/useOverflow.js'; +import { render } from '@testing-library/react'; import { ButtonGroup } from './ButtonGroup.js'; import { Button } from '../Buttons/Button.js'; -import { IconButton } from '../Buttons/IconButton.js'; it('should render with two buttons', () => { const { container } = render( @@ -34,101 +31,6 @@ it('should render with four buttons', () => { expect(container.querySelectorAll('button').length).toBe(4); }); -it.each(['start', 'end'] as const)( - 'should handle overflow when overflowButton is specified (%s)', - (overflowPlacement) => { - const scrollWidthSpy = vi - .spyOn(HTMLElement.prototype, 'scrollWidth', 'get') - .mockReturnValueOnce(250) - .mockReturnValue(200); - const offsetWidthSpy = vi - .spyOn(HTMLElement.prototype, 'offsetWidth', 'get') - .mockReturnValue(200); - - const mockOnClick = vi.fn(); - - const OverflowGroup = () => { - const buttons = [...Array(3)].map((_, index) => ( - - - - )); - return ( - ( - mockOnClick(overflowStart)}> - - - )} - overflowPlacement={overflowPlacement} - > - {buttons} - - ); - }; - const { container } = render(); - - expect(container.querySelector('.iui-button-group')).toBeTruthy(); - - const buttons = container.querySelectorAll('.iui-button'); - expect(buttons).toHaveLength(2); - fireEvent.click(overflowPlacement === 'end' ? buttons[1] : buttons[0]); - expect(mockOnClick).toHaveBeenCalledWith(1); - - scrollWidthSpy.mockRestore(); - offsetWidthSpy.mockRestore(); - }, -); - -it.each(['start', 'end'] as const)( - 'should handle overflow when available space is smaller than one element (%s)', - (overflowPlacement) => { - const scrollWidthSpy = vi - .spyOn(HTMLElement.prototype, 'scrollWidth', 'get') - .mockReturnValue(200); - const offsetWidthSpy = vi - .spyOn(HTMLElement.prototype, 'offsetWidth', 'get') - .mockReturnValue(50); - - const OverflowGroup = () => { - const buttons = [...Array(3)].map((_, index) => ( - - - - )); - return ( - ( - - - - )} - overflowPlacement={overflowPlacement} - > - {buttons} - - ); - }; - const { container } = render(); - const { - container: { firstChild: moreIconButton }, - } = render( - - - , - ); - - expect(container.querySelector('.iui-button-group')).toBeTruthy(); - - const buttons = container.querySelectorAll('.iui-button'); - expect(buttons).toHaveLength(1); - expect(buttons[0]).toEqual(moreIconButton); - - scrollWidthSpy.mockRestore(); - offsetWidthSpy.mockRestore(); - }, -); - it('should work in vertical orientation', () => { const { container } = render( @@ -142,44 +44,3 @@ it('should work in vertical orientation', () => { expect(group).toBeTruthy(); expect(group.children).toHaveLength(2); }); - -it.each` - visibleCount | overflowStart | length | overflowPlacement - ${9} | ${1} | ${10} | ${'start'} - ${8} | ${2} | ${10} | ${'start'} - ${4} | ${6} | ${10} | ${'start'} - ${3} | ${7} | ${10} | ${'start'} - ${1} | ${9} | ${10} | ${'start'} - ${9} | ${8} | ${10} | ${'end'} - ${8} | ${7} | ${10} | ${'end'} - ${4} | ${3} | ${10} | ${'end'} - ${3} | ${2} | ${10} | ${'end'} - ${1} | ${0} | ${10} | ${'end'} -`( - 'should calculate correct values when overflowPlacement=$overflowPlacement and visibleCount=$visibleCount', - ({ visibleCount, overflowStart, length, overflowPlacement }) => { - const useOverflowMock = vi - .spyOn(UseOverflow, 'useOverflow') - .mockReturnValue([vi.fn(), visibleCount]); - - const buttons = [...Array(length)].map((_, index) => ( - - )); - - const { container } = render( - {startIndex}} - overflowPlacement={overflowPlacement} - > - {buttons} - , - ); - - expect(container.querySelectorAll('button')).toHaveLength(visibleCount - 1); - expect(container.querySelector('span')).toHaveTextContent( - `${overflowStart}`, - ); - - useOverflowMock.mockRestore(); - }, -); diff --git a/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx b/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx index a8a5b5448cf..7f253185417 100644 --- a/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx +++ b/packages/itwinui-react/src/core/ButtonGroup/ButtonGroup.tsx @@ -191,7 +191,6 @@ const OverflowGroup = React.forwardRef((props, forwardedRef) => { return overflowButton != null ? ( { expect(onSelect).not.toHaveBeenCalled(); }); -it('should render data in pages', async () => { - vi.spyOn(UseOverflow, 'useOverflow').mockImplementation((items) => [ - vi.fn(), - items.length, - ]); - const { container } = renderComponent({ - data: mockedData(100), - pageSize: 10, - paginatorRenderer: (props) => , - }); - - let rows = container.querySelectorAll('.iui-table-body .iui-table-row'); - expect(rows).toHaveLength(10); - expect(rows[0].querySelector('.iui-table-cell')?.textContent).toEqual( - 'Name1', - ); - expect(rows[9].querySelector('.iui-table-cell')?.textContent).toEqual( - 'Name10', - ); - - const pages = container.querySelectorAll( - '.iui-table-paginator .iui-table-paginator-page-button', - ); - expect(pages).toHaveLength(10); - await userEvent.click(pages[3]); - rows = container.querySelectorAll('.iui-table-body .iui-table-row'); - expect(rows).toHaveLength(10); - expect(rows[0].querySelector('.iui-table-cell')?.textContent).toEqual( - 'Name31', - ); - expect(rows[9].querySelector('.iui-table-cell')?.textContent).toEqual( - 'Name40', - ); -}); - it('should change page size', async () => { const { container } = renderComponent({ data: mockedData(100), diff --git a/testing/e2e/app/routes/ButtonGroup/route.tsx b/testing/e2e/app/routes/ButtonGroup/route.tsx index b4e5577c4c6..862e3f12b79 100644 --- a/testing/e2e/app/routes/ButtonGroup/route.tsx +++ b/testing/e2e/app/routes/ButtonGroup/route.tsx @@ -6,6 +6,9 @@ import React from 'react'; export default function ButtonGroupTest() { const [searchParams] = useSearchParams(); + const exampleType = (searchParams.get('exampleType') ?? 'default') as + | 'default' + | 'overflow'; const initialProvideOverflowButton = searchParams.get('provideOverflowButton') !== 'false'; const orientation = @@ -14,52 +17,74 @@ export default function ButtonGroupTest() { const overflowPlacement = (searchParams.get('overflowPlacement') as 'start' | 'end') || undefined; - const [provideOverflowButton, setProvideOverflowButton] = React.useState( - initialProvideOverflowButton, - ); + const Default = () => { + const [provideOverflowButton, setProvideOverflowButton] = React.useState( + initialProvideOverflowButton, + ); - return ( - - + return ( + + + +
+ { + return ( + + {firstOverflowingIndex} + + ); + } + : undefined + } + > + + + + + + + + + + +
+
+ ); + }; -
+ const Overflow = () => { + const buttons = [...Array(10)].map((_, index) => ( + + + + )); + + return ( +
{startIndex}} overflowPlacement={overflowPlacement} - overflowButton={ - provideOverflowButton - ? (firstOverflowingIndex) => { - return ( - - {firstOverflowingIndex} - - ); - } - : undefined - } > - - - - - - - - - + {buttons}
- - ); + ); + }; + + return exampleType === 'default' ? : ; } diff --git a/testing/e2e/app/routes/ButtonGroup/spec.ts b/testing/e2e/app/routes/ButtonGroup/spec.ts index bce259fcf48..9beaa2c11a5 100644 --- a/testing/e2e/app/routes/ButtonGroup/spec.ts +++ b/testing/e2e/app/routes/ButtonGroup/spec.ts @@ -1,98 +1,199 @@ import { test, expect, Page } from '@playwright/test'; test.describe('ButtonGroup', () => { - // test("should support keyboard navigation when role='toolbar'", async ({ - // page, - // }) => { - // await page.goto('/ButtonGroup'); + test.describe('Toolbar', () => { + test("should support keyboard navigation when role='toolbar'", async ({ + page, + }) => { + await page.goto('/ButtonGroup'); - // await page.keyboard.press('Tab'); - // await expect(page.getByRole('button', { name: 'Button 1' })).toBeFocused(); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); - // await page.keyboard.press('ArrowRight'); - // await expect(page.getByRole('button', { name: 'Button 2' })).toBeFocused(); + await expect( + page.getByRole('button', { name: 'Button 1' }), + ).toBeFocused(); + + await page.keyboard.press('ArrowRight'); + await expect( + page.getByRole('button', { name: 'Button 2' }), + ).toBeFocused(); + + await page.keyboard.press('ArrowRight'); + await expect( + page.getByRole('button', { name: 'Button 3' }), + ).toBeFocused(); + + await page.keyboard.press('ArrowRight'); + await expect( + page.getByRole('button', { name: 'Button 1' }), + ).toBeFocused(); - // await page.keyboard.press('ArrowRight'); - // await expect(page.getByRole('button', { name: 'Button 3' })).toBeFocused(); + await page.keyboard.press('ArrowLeft'); + await expect( + page.getByRole('button', { name: 'Button 3' }), + ).toBeFocused(); - // await page.keyboard.press('ArrowRight'); - // await expect(page.getByRole('button', { name: 'Button 1' })).toBeFocused(); + await page.keyboard.press('ArrowLeft'); + await expect( + page.getByRole('button', { name: 'Button 2' }), + ).toBeFocused(); - // await page.keyboard.press('ArrowLeft'); - // await expect(page.getByRole('button', { name: 'Button 3' })).toBeFocused(); + await page.keyboard.press('ArrowLeft'); + await expect( + page.getByRole('button', { name: 'Button 1' }), + ).toBeFocused(); - // await page.keyboard.press('ArrowLeft'); - // await expect(page.getByRole('button', { name: 'Button 2' })).toBeFocused(); + await page.keyboard.press('ArrowLeft'); + await expect( + page.getByRole('button', { name: 'Button 3' }), + ).toBeFocused(); + }); - // await page.keyboard.press('ArrowLeft'); - // await expect(page.getByRole('button', { name: 'Button 1' })).toBeFocused(); + test("should support keyboard navigation when role='toolbar' and orientation='vertical'", async ({ + page, + }) => { + await page.goto('/ButtonGroup?orientation=vertical'); - // await page.keyboard.press('ArrowLeft'); - // await expect(page.getByRole('button', { name: 'Button 3' })).toBeFocused(); - // }); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); - // test("should support keyboard navigation when role='toolbar' and orientation='vertical'", async ({ - // page, - // }) => { - // await page.goto('/ButtonGroup?orientation=vertical'); + await expect( + page.getByRole('button', { name: 'Button 1' }), + ).toBeFocused(); - // await page.keyboard.press('Tab'); - // await expect(page.getByRole('button', { name: 'Button 1' })).toBeFocused(); + await page.keyboard.press('ArrowDown'); + await expect( + page.getByRole('button', { name: 'Button 2' }), + ).toBeFocused(); - // await page.keyboard.press('ArrowDown'); - // await expect(page.getByRole('button', { name: 'Button 2' })).toBeFocused(); + await page.keyboard.press('ArrowDown'); + await expect( + page.getByRole('button', { name: 'Button 3' }), + ).toBeFocused(); - // await page.keyboard.press('ArrowDown'); - // await expect(page.getByRole('button', { name: 'Button 3' })).toBeFocused(); + await page.keyboard.press('ArrowDown'); + await expect( + page.getByRole('button', { name: 'Button 1' }), + ).toBeFocused(); - // await page.keyboard.press('ArrowDown'); - // await expect(page.getByRole('button', { name: 'Button 1' })).toBeFocused(); + await page.keyboard.press('ArrowUp'); + await expect( + page.getByRole('button', { name: 'Button 3' }), + ).toBeFocused(); - // await page.keyboard.press('ArrowUp'); - // await expect(page.getByRole('button', { name: 'Button 3' })).toBeFocused(); + await page.keyboard.press('ArrowUp'); + await expect( + page.getByRole('button', { name: 'Button 2' }), + ).toBeFocused(); - // await page.keyboard.press('ArrowUp'); - // await expect(page.getByRole('button', { name: 'Button 2' })).toBeFocused(); + await page.keyboard.press('ArrowUp'); + await expect( + page.getByRole('button', { name: 'Button 1' }), + ).toBeFocused(); - // await page.keyboard.press('ArrowUp'); - // await expect(page.getByRole('button', { name: 'Button 1' })).toBeFocused(); + await page.keyboard.press('ArrowUp'); + await expect( + page.getByRole('button', { name: 'Button 3' }), + ).toBeFocused(); + }); + }); - // await page.keyboard.press('ArrowUp'); - // await expect(page.getByRole('button', { name: 'Button 3' })).toBeFocused(); - // }); + test.describe('Overflow', () => { + ( + [ + { + orientation: 'horizontal', + overflowPlacement: 'end', + }, + { + orientation: 'horizontal', + overflowPlacement: 'start', + }, + { + orientation: 'vertical', + overflowPlacement: 'end', + }, + { + orientation: 'vertical', + overflowPlacement: 'start', + }, + ] as const + ).forEach(({ orientation, overflowPlacement }) => { + test(`should overflow whenever there is not enough space (orientation=${orientation}, overflowPlacement=${overflowPlacement})`, async ({ + page, + }) => { + await page.goto( + `/ButtonGroup?orientation=${orientation}&overflowPlacement=${overflowPlacement}`, + ); + + const setContainerSize = getSetContainerSize(page, orientation); + const expectOverflowState = getExpectOverflowState( + page, + overflowPlacement, + ); + + await expectOverflowState({ + expectedButtonLength: 3, + expectedOverflowTagFirstOverflowingIndex: undefined, + }); + + await setContainerSize(2.5); + + await expectOverflowState({ + expectedButtonLength: 2, + expectedOverflowTagFirstOverflowingIndex: 1, + }); + + await setContainerSize(1.5); + + await expectOverflowState({ + expectedButtonLength: 1, + expectedOverflowTagFirstOverflowingIndex: + overflowPlacement === 'end' ? 0 : 2, + }); + + await setContainerSize(0.5); + + // should return 1 overflowTag when item is bigger than the container + await expectOverflowState({ + expectedButtonLength: 1, + expectedOverflowTagFirstOverflowingIndex: + overflowPlacement === 'end' ? 0 : 2, + }); + + // should restore hidden items when space is available again + await setContainerSize(1.5); + + await expectOverflowState({ + expectedButtonLength: 1, + expectedOverflowTagFirstOverflowingIndex: + overflowPlacement === 'end' ? 0 : 2, + }); + + await setContainerSize(2.5); + + await expectOverflowState({ + expectedButtonLength: 2, + expectedOverflowTagFirstOverflowingIndex: 1, + }); + + await setContainerSize(undefined); + + await expectOverflowState({ + expectedButtonLength: 3, + expectedOverflowTagFirstOverflowingIndex: undefined, + }); + }); + }); - ( - [ - { - orientation: 'horizontal', - overflowPlacement: 'end', - }, - { - orientation: 'horizontal', - overflowPlacement: 'start', - }, - { - orientation: 'vertical', - overflowPlacement: 'end', - }, - { - orientation: 'vertical', - overflowPlacement: 'start', - }, - ] as const - ).forEach(({ orientation, overflowPlacement }) => { - test(`should overflow whenever there is not enough space (orientation=${orientation}, overflowPlacement=${overflowPlacement})`, async ({ + test(`should handle overflow only whenever overflowButton is passed`, async ({ page, }) => { - await page.goto( - `/ButtonGroup?orientation=${orientation}&overflowPlacement=${overflowPlacement}`, - ); + await page.goto(`/ButtonGroup?provideOverflowButton=false`); - const setContainerSize = getSetContainerSize(page, orientation); - const expectOverflowState = getExpectOverflowState( - page, - overflowPlacement, - ); + const setContainerSize = getSetContainerSize(page, 'horizontal'); + const expectOverflowState = getExpectOverflowState(page, 'end'); await expectOverflowState({ expectedButtonLength: 3, @@ -102,87 +203,131 @@ test.describe('ButtonGroup', () => { await setContainerSize(2.5); await expectOverflowState({ - expectedButtonLength: 2, - expectedOverflowTagFirstOverflowingIndex: 1, - }); - - await setContainerSize(1.5); - - await expectOverflowState({ - expectedButtonLength: 1, - expectedOverflowTagFirstOverflowingIndex: - overflowPlacement === 'end' ? 0 : 2, - }); - - await setContainerSize(0.5); - - // should return 1 overflowTag when item is bigger than the container - await expectOverflowState({ - expectedButtonLength: 1, - expectedOverflowTagFirstOverflowingIndex: - overflowPlacement === 'end' ? 0 : 2, - }); - - // should restore hidden items when space is available again - await setContainerSize(1.5); - - await expectOverflowState({ - expectedButtonLength: 1, - expectedOverflowTagFirstOverflowingIndex: - overflowPlacement === 'end' ? 0 : 2, + expectedButtonLength: 3, + expectedOverflowTagFirstOverflowingIndex: undefined, }); - await setContainerSize(2.5); + const toggleProviderOverflowContainerButton = page.getByTestId( + 'toggle-provide-overflow-container', + ); + await toggleProviderOverflowContainerButton.click(); await expectOverflowState({ expectedButtonLength: 2, expectedOverflowTagFirstOverflowingIndex: 1, }); - await setContainerSize(undefined); - + await toggleProviderOverflowContainerButton.click(); await expectOverflowState({ expectedButtonLength: 3, expectedOverflowTagFirstOverflowingIndex: undefined, }); }); - }); - - test(`should handle overflow only whenever overflowButton is passed`, async ({ - page, - }) => { - await page.goto(`/ButtonGroup?provideOverflowButton=false`); - - const setContainerSize = getSetContainerSize(page, 'horizontal'); - const expectOverflowState = getExpectOverflowState(page, 'end'); - await expectOverflowState({ - expectedButtonLength: 3, - expectedOverflowTagFirstOverflowingIndex: undefined, - }); - - await setContainerSize(2.5); - - await expectOverflowState({ - expectedButtonLength: 3, - expectedOverflowTagFirstOverflowingIndex: undefined, - }); - - const toggleProviderOverflowContainerButton = page.getByTestId( - 'toggle-provide-overflow-container', + ( + [ + { + visibleCount: 10, + containerSize: '488px', + overflowStart: 0, + overflowPlacement: 'start', + }, + { + visibleCount: 9, + containerSize: '481px', + overflowStart: 1, + overflowPlacement: 'start', + }, + { + visibleCount: 8, + containerSize: '429px', + overflowStart: 2, + overflowPlacement: 'start', + }, + { + visibleCount: 4, + containerSize: '220px', + overflowStart: 6, + overflowPlacement: 'start', + }, + { + visibleCount: 3, + containerSize: '183px', + overflowStart: 7, + overflowPlacement: 'start', + }, + { + visibleCount: 1, + containerSize: '77px', + overflowStart: 9, + overflowPlacement: 'start', + }, + { + visibleCount: 10, + containerSize: '487px', + overflowStart: 9, + overflowPlacement: 'end', + }, + { + visibleCount: 9, + containerSize: '475px', + overflowStart: 8, + overflowPlacement: 'end', + }, + { + visibleCount: 8, + containerSize: '429px', + overflowStart: 7, + overflowPlacement: 'end', + }, + { + visibleCount: 4, + containerSize: '221px', + overflowStart: 3, + overflowPlacement: 'end', + }, + { + visibleCount: 3, + containerSize: '183px', + overflowStart: 2, + overflowPlacement: 'end', + }, + { + visibleCount: 1, + containerSize: '78px', + overflowStart: 0, + overflowPlacement: 'end', + }, + ] as const + ).forEach( + ({ visibleCount, containerSize, overflowStart, overflowPlacement }) => { + test(`should calculate correct values when overflowPlacement=${overflowPlacement} and visibleCount=${visibleCount}`, async ({ + page, + }) => { + await page.goto( + `/ButtonGroup?exampleType=overflow&containerSize${containerSize}&overflowPlacement=${overflowPlacement}`, + ); + + await page.waitForTimeout(60); + + const setContainerSize = getSetContainerSize(page, 'horizontal'); + await setContainerSize( + visibleCount === 10 ? visibleCount : visibleCount + 0.5, + ); + + const allItems = await page.locator('button').all(); + const overflowButton = + allItems[overflowPlacement === 'end' ? allItems.length - 1 : 0]; + const buttonGroupButtons = allItems.slice( + overflowPlacement === 'end' ? 0 : 1, + overflowPlacement === 'end' ? -1 : undefined, + ); + + expect(overflowButton).toHaveText(`${overflowStart}`); + expect(buttonGroupButtons).toHaveLength(visibleCount - 1); + }); + }, ); - - await toggleProviderOverflowContainerButton.click(); - await expectOverflowState({ - expectedButtonLength: 2, - expectedOverflowTagFirstOverflowingIndex: 1, - }); - - await toggleProviderOverflowContainerButton.click(); - await expectOverflowState({ - expectedButtonLength: 3, - expectedOverflowTagFirstOverflowingIndex: undefined, - }); }); }); @@ -195,15 +340,23 @@ const getSetContainerSize = ( return async (multiplier: number | undefined) => { await page.locator('#container').evaluate( (element, args) => { - if (args.orientation === 'horizontal') { - if (args.multiplier != null) { - element.style.setProperty('width', `${50 * args.multiplier}px`); + if (args.multiplier != null) { + const overlappingBorderOvercount = args.multiplier - 1; + + if (args.orientation === 'horizontal') { + element.style.setProperty( + 'width', + `${50 * args.multiplier - overlappingBorderOvercount - 1}px`, // - 1 to force the overflow + ); } else { - element.style.removeProperty('width'); + element.style.setProperty( + 'height', + `${36 * args.multiplier - overlappingBorderOvercount - 1}px`, + ); } } else { - if (args.multiplier != null) { - element.style.setProperty('height', `${36 * args.multiplier}px`); + if (args.orientation === 'horizontal') { + element.style.removeProperty('width'); } else { element.style.removeProperty('height'); } @@ -211,7 +364,7 @@ const getSetContainerSize = ( }, { orientation, multiplier }, ); - await page.waitForTimeout(30); + await page.waitForTimeout(100); }; }; diff --git a/testing/e2e/app/routes/Select/route.tsx b/testing/e2e/app/routes/Select/route.tsx index 64afee33945..d271e84044e 100644 --- a/testing/e2e/app/routes/Select/route.tsx +++ b/testing/e2e/app/routes/Select/route.tsx @@ -1,7 +1,7 @@ import { Select } from '@itwin/itwinui-react'; import { useSearchParams } from '@remix-run/react'; -export default function BreadcrumbsTest() { +export default function SelectTest() { const [searchParams] = useSearchParams(); const options = [ diff --git a/testing/e2e/app/routes/Table/route.tsx b/testing/e2e/app/routes/Table/route.tsx index 5a406b8f95d..26cb2fa8228 100644 --- a/testing/e2e/app/routes/Table/route.tsx +++ b/testing/e2e/app/routes/Table/route.tsx @@ -7,7 +7,7 @@ import { import { useSearchParams } from '@remix-run/react'; import React from 'react'; -export default function Resizing() { +export default function TableTest() { const [searchParams] = useSearchParams(); const exampleType = searchParams.get('exampleType') || 'default'; @@ -210,8 +210,9 @@ export default function Resizing() { return ( <> -
+
{ }); test.describe('Table Paginator', () => { + test(`should render data in pages`, async ({ page }) => { + await page.goto(`/Table?exampleType=withTablePaginator`); + + expect(page.locator(`[role="cell"]`).first()).toHaveText('Name 0'); + expect(page.locator(`[role="cell"]`).last()).toHaveText('Description 49'); + + await page + .locator('button', { + hasText: '6', + }) + .first() + .click(); + + expect(page.locator(`[role="cell"]`).first()).toHaveText('Name 250'); + expect(page.locator(`[role="cell"]`).last()).toHaveText('Description 299'); + }); + test(`should overflow whenever there is not enough space`, async ({ page, }) => { From fde3af2a39b62b875e9613b0c5a9bf0a378cd502 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Wed, 17 Jul 2024 08:08:16 -0400 Subject: [PATCH 57/65] Leftover failing unit tests to e2e --- .../src/core/Table/TablePaginator.test.tsx | 67 --------------- .../components/MiddleTextTruncation.test.tsx | 83 ------------------- testing/e2e/app/routes/ButtonGroup/spec.ts | 2 +- .../app/routes/MiddleTextTruncation/route.tsx | 15 ++++ .../app/routes/MiddleTextTruncation/spec.ts | 15 +++- testing/e2e/app/routes/Table/route.tsx | 7 ++ testing/e2e/app/routes/Table/spec.ts | 68 ++++++++++----- 7 files changed, 84 insertions(+), 173 deletions(-) delete mode 100644 packages/itwinui-react/src/utils/components/MiddleTextTruncation.test.tsx diff --git a/packages/itwinui-react/src/core/Table/TablePaginator.test.tsx b/packages/itwinui-react/src/core/Table/TablePaginator.test.tsx index ccb8544f9a9..7eb5f2f96e3 100644 --- a/packages/itwinui-react/src/core/Table/TablePaginator.test.tsx +++ b/packages/itwinui-react/src/core/Table/TablePaginator.test.tsx @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { fireEvent, render, screen } from '@testing-library/react'; import { TablePaginator, type TablePaginatorProps } from './TablePaginator.js'; -import * as UseOverflow from '../../utils/hooks/useOverflow.js'; import * as UseContainerWidth from '../../utils/hooks/useContainerWidth.js'; import { userEvent } from '@testing-library/user-event'; @@ -21,13 +20,6 @@ const renderComponent = (props?: Partial) => { ); }; -beforeEach(() => { - vi.spyOn(UseOverflow, 'useOverflow').mockImplementation((items) => [ - vi.fn(), - items.length, - ]); -}); - afterEach(() => { vi.restoreAllMocks(); }); @@ -190,38 +182,6 @@ it('should handle clicks', async () => { expect(onPageChange).toHaveBeenCalledWith(6); }); -it('should render truncated pages list', () => { - vi.spyOn(UseOverflow, 'useOverflow').mockReturnValue([vi.fn(), 5]); - const { container } = renderComponent({ currentPage: 10 }); - - const pages = container.querySelectorAll('.iui-table-paginator-page-button'); - expect(pages).toHaveLength(7); - expect(pages[0].textContent).toEqual('1'); - expect(pages[1].textContent).toEqual('9'); - expect(pages[2].textContent).toEqual('10'); - expect(pages[3].textContent).toEqual('11'); - expect(pages[3]).toHaveAttribute('data-iui-active', 'true'); - expect(pages[4].textContent).toEqual('12'); - expect(pages[5].textContent).toEqual('13'); - expect(pages[6].textContent).toEqual('20'); - - const ellipsis = container.querySelectorAll('.iui-table-paginator-ellipsis'); - expect(ellipsis).toHaveLength(2); -}); - -it('should render only the current page when screen is very small', () => { - vi.spyOn(UseOverflow, 'useOverflow').mockReturnValue([vi.fn(), 1]); - const { container } = renderComponent({ currentPage: 10 }); - - const pages = container.querySelectorAll('.iui-table-paginator-page-button'); - expect(pages).toHaveLength(1); - expect(pages[0].textContent).toEqual('11'); - expect(pages[0]).toHaveAttribute('data-iui-active', 'true'); - - const ellipsis = container.querySelectorAll('.iui-table-paginator-ellipsis'); - expect(ellipsis).toHaveLength(0); -}); - it('should handle keyboard navigation when focusActivationMode is auto', () => { const onPageChange = vi.fn(); const { container } = renderComponent({ @@ -272,33 +232,6 @@ it('should handle keyboard navigation when focusActivationMode is manual', () => expect(onPageChange).toHaveBeenCalledWith(0); }); -it('should render elements in small size', () => { - vi.spyOn(UseOverflow, 'useOverflow').mockReturnValue([vi.fn(), 5]); - const { container } = renderComponent({ - size: 'small', - pageSizeList: [10, 25, 50], - currentPage: 10, - onPageSizeChange: vi.fn(), - }); - - const pageSwitchers = container.querySelectorAll('.iui-button'); - expect( - Array.from(pageSwitchers).every( - (p) => p.getAttribute('data-iui-size') === 'small', - ), - ).toBe(true); - - const pages = container.querySelectorAll( - '.iui-table-paginator-page-button[data-iui-size="small"]', - ); - expect(pages).toHaveLength(7); - - const ellipsis = container.querySelectorAll( - '.iui-table-paginator-ellipsis-small', - ); - expect(ellipsis).toHaveLength(2); -}); - it('should render with custom localization', async () => { vi.spyOn(UseContainerWidth, 'useContainerWidth').mockImplementation(() => [ vi.fn(), diff --git a/packages/itwinui-react/src/utils/components/MiddleTextTruncation.test.tsx b/packages/itwinui-react/src/utils/components/MiddleTextTruncation.test.tsx deleted file mode 100644 index 4393d666e8a..00000000000 --- a/packages/itwinui-react/src/utils/components/MiddleTextTruncation.test.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Bentley Systems, Incorporated. All rights reserved. - * See LICENSE.md in the project root for license terms and full copyright notice. - *--------------------------------------------------------------------------------------------*/ -import { render } from '@testing-library/react'; -import { MiddleTextTruncation } from './MiddleTextTruncation.js'; -import * as UseOverflow from '../hooks/useOverflow.js'; - -it('should truncate the text', () => { - vi.spyOn(UseOverflow, 'useOverflow').mockReturnValue([vi.fn(), 20]); - const { container } = render( -
- -
, - ); - - const containerSpan = container.querySelector('span') as HTMLSpanElement; - expect(containerSpan.textContent).toBe('This is some …lipsis'); -}); - -it('should truncate the text and leave 20 symbols at the end', () => { - vi.spyOn(UseOverflow, 'useOverflow').mockReturnValue([vi.fn(), 50]); - const { container } = render( -
- -
, - ); - - const containerSpan = container.querySelector('span') as HTMLSpanElement; - expect(containerSpan.textContent).toBe( - 'This is some very long text t…f the truncated text', - ); -}); - -it('should leave original text (same length)', () => { - vi.spyOn(UseOverflow, 'useOverflow').mockReturnValue([vi.fn(), 20]); - const { container } = render( -
- -
, - ); - - const containerSpan = container.querySelector('span') as HTMLSpanElement; - expect(containerSpan.textContent).toBe('This is a short text'); -}); - -it('should leave original text', () => { - vi.spyOn(UseOverflow, 'useOverflow').mockReturnValue([vi.fn(), 20]); - const { container } = render( -
- -
, - ); - - const containerSpan = container.querySelector('span') as HTMLSpanElement; - expect(containerSpan.textContent).toBe('No trunc'); -}); - -it('should render custom text', () => { - vi.spyOn(UseOverflow, 'useOverflow').mockReturnValue([vi.fn(), 20]); - const text = 'This is some very long text to truncate and expect ellipsis'; - const { container } = render( - ( - - {truncatedText} - some additional text - - )} - />, - ); - - const containerSpan = container.querySelector('span') as HTMLSpanElement; - expect(containerSpan.textContent).toBe( - 'This is some …lipsis - some additional text', - ); - expect( - containerSpan.querySelector('[data-testid="custom-text"]'), - ).toBeTruthy(); -}); diff --git a/testing/e2e/app/routes/ButtonGroup/spec.ts b/testing/e2e/app/routes/ButtonGroup/spec.ts index 9beaa2c11a5..9b6dc059249 100644 --- a/testing/e2e/app/routes/ButtonGroup/spec.ts +++ b/testing/e2e/app/routes/ButtonGroup/spec.ts @@ -364,7 +364,7 @@ const getSetContainerSize = ( }, { orientation, multiplier }, ); - await page.waitForTimeout(100); + await page.waitForTimeout(30); }; }; diff --git a/testing/e2e/app/routes/MiddleTextTruncation/route.tsx b/testing/e2e/app/routes/MiddleTextTruncation/route.tsx index 2e3f3f47768..4ff48dc1247 100644 --- a/testing/e2e/app/routes/MiddleTextTruncation/route.tsx +++ b/testing/e2e/app/routes/MiddleTextTruncation/route.tsx @@ -1,15 +1,30 @@ import { MiddleTextTruncation } from '@itwin/itwinui-react'; +import { useSearchParams } from '@remix-run/react'; const longText = 'MyFileWithAReallyLongNameThatWillBeTruncatedBecauseItIsReallyThatLongSoHardToBelieve_FinalVersion_V2.html'; export default function MiddleTextTruncationTest() { + const [searchParams] = useSearchParams(); + + const shouldUseCustomRenderer = + searchParams.get('shouldUseCustomRenderer') === 'true'; + return (
( + + {truncatedText} - some additional text + + ) + : undefined + } />
); diff --git a/testing/e2e/app/routes/MiddleTextTruncation/spec.ts b/testing/e2e/app/routes/MiddleTextTruncation/spec.ts index 9cc1c123aba..f3a7832178f 100644 --- a/testing/e2e/app/routes/MiddleTextTruncation/spec.ts +++ b/testing/e2e/app/routes/MiddleTextTruncation/spec.ts @@ -56,6 +56,19 @@ test.describe('MiddleTextTruncation', () => { longItem.length, ); }); + + test('should render custom text', async ({ page }) => { + await page.goto(`/MiddleTextTruncation?shouldUseCustomRenderer=true`); + + const setContainerSize = getSetContainerSize(page); + await setContainerSize('500px'); + + await expect(page.getByTestId('custom-text')).toHaveText( + 'MyFileWithAReallyLongNameThatWillBeTruncat…2.html - some additional text', + ); + + await page.waitForTimeout(100); + }); }); // ---------------------------------------------------------------------------- @@ -72,6 +85,6 @@ const getSetContainerSize = (page: Page) => { }, { dimension }, ); - await page.waitForTimeout(100); + await page.waitForTimeout(30); }; }; diff --git a/testing/e2e/app/routes/Table/route.tsx b/testing/e2e/app/routes/Table/route.tsx index 26cb2fa8228..e73d79cdb72 100644 --- a/testing/e2e/app/routes/Table/route.tsx +++ b/testing/e2e/app/routes/Table/route.tsx @@ -15,6 +15,11 @@ export default function TableTest() { const columnResizeMode = searchParams.get('columnResizeMode') || 'fit'; const maxWidths = searchParams.getAll('maxWidth'); const minWidths = searchParams.getAll('minWidth'); + const density = (searchParams.get('density') || undefined) as + | 'default' + | 'condensed' + | 'extra-condensed' + | undefined; const isSelectable = searchParams.get('isSelectable') === 'true'; const subRows = searchParams.get('subRows') === 'true'; const filter = searchParams.get('filter') === 'true'; @@ -155,6 +160,7 @@ export default function TableTest() { isRowDisabled={isRowDisabled} isSelectable={isSelectable} isSortable + density={density} columnResizeMode={columnResizeMode as 'fit' | 'expand' | undefined} selectSubRows={selectSubRows} enableVirtualization={enableVirtualization} @@ -217,6 +223,7 @@ export default function TableTest() { columns={columns} data={data} pageSize={50} + density={density} paginatorRenderer={paginator} /> diff --git a/testing/e2e/app/routes/Table/spec.ts b/testing/e2e/app/routes/Table/spec.ts index 90af5f85902..a8020054b14 100644 --- a/testing/e2e/app/routes/Table/spec.ts +++ b/testing/e2e/app/routes/Table/spec.ts @@ -385,17 +385,42 @@ test.describe('Table Paginator', () => { expect(page.locator(`[role="cell"]`).first()).toHaveText('Name 0'); expect(page.locator(`[role="cell"]`).last()).toHaveText('Description 49'); - await page - .locator('button', { - hasText: '6', - }) - .first() - .click(); + // Go to the 6th page + await page.locator('button').last().click({ clickCount: 5 }); expect(page.locator(`[role="cell"]`).first()).toHaveText('Name 250'); expect(page.locator(`[role="cell"]`).last()).toHaveText('Description 299'); }); + test('should render truncated pages list', async ({ page }) => { + await page.goto(`/Table?exampleType=withTablePaginator`); + + const setContainerSize = getSetContainerSize(page); + setContainerSize('800px'); + + // Go to the 6th page + await page.locator('button').last().click({ clickCount: 5 }); + + const paginatorButtons = page.locator('#paginator button', { + hasText: /[0-9]+/, + }); + await expect(paginatorButtons).toHaveText([ + '1', + '4', + '5', + '6', + '7', + '8', + '11', + ]); + await expect(paginatorButtons.nth(3)).toHaveAttribute( + 'data-iui-active', + 'true', + ); + + await expect(page.getByText('…')).toHaveCount(2); + }); + test(`should overflow whenever there is not enough space`, async ({ page, }) => { @@ -436,33 +461,34 @@ test.describe('Table Paginator', () => { expectedOverflowingEllipsisVisibleCount: 0, }); - await setContainerSize('10px'); + await setContainerSize('100px'); await expectOverflowState({ expectedItemLength: 1, expectedOverflowingEllipsisVisibleCount: 0, }); + + expect(page.locator('#paginator button', { hasText: /1/ })).toHaveAttribute( + 'data-iui-active', + 'true', + ); }); - test(`should show first and last page when on a middle page`, async ({ - page, - }) => { - await page.goto(`/Table?exampleType=withTablePaginator`); + test(`should render elements in small size`, async ({ page }) => { + await page.goto(`/Table?exampleType=withTablePaginator&density=condensed`); const setContainerSize = getSetContainerSize(page); - const expectOverflowState = getExpectOverflowState(page); + await setContainerSize('500px'); - await expectOverflowState({ - expectedItemLength: 11, - expectedOverflowingEllipsisVisibleCount: 0, + (await page.locator('#paginator button').all()).forEach(async (button) => { + await expect(button).toHaveAttribute('data-iui-size', 'small'); }); - await setContainerSize('10px'); + await expect(page.getByText('…')).toHaveClass( + /_iui[0-9]+-table-paginator-ellipsis-small/, + ); - await expectOverflowState({ - expectedItemLength: 1, - expectedOverflowingEllipsisVisibleCount: 0, - }); + await page.waitForTimeout(300); }); //#region Helpers for table paginator tests @@ -483,7 +509,7 @@ test.describe('Table Paginator', () => { dimension, }, ); - await page.waitForTimeout(30); + await page.waitForTimeout(60); }; }; From f612b2ff339baa3528427b675104dee09d898097 Mon Sep 17 00:00:00 2001 From: Rohan <45748283+rohan-kadkol@users.noreply.github.com> Date: Wed, 17 Jul 2024 11:28:37 -0400 Subject: [PATCH 58/65] More failing unit tests to e2e --- .../src/core/ComboBox/ComboBox.test.tsx | 85 ------------- .../src/core/Select/Select.test.tsx | 41 ------ testing/e2e/app/routes/ComboBox/route.tsx | 22 +++- testing/e2e/app/routes/ComboBox/spec.ts | 98 ++++++++++++++- testing/e2e/app/routes/Select/route.tsx | 29 +++-- testing/e2e/app/routes/Select/spec.ts | 118 +++++++++++------- 6 files changed, 201 insertions(+), 192 deletions(-) diff --git a/packages/itwinui-react/src/core/ComboBox/ComboBox.test.tsx b/packages/itwinui-react/src/core/ComboBox/ComboBox.test.tsx index 478494784e2..ed33de74e19 100644 --- a/packages/itwinui-react/src/core/ComboBox/ComboBox.test.tsx +++ b/packages/itwinui-react/src/core/ComboBox/ComboBox.test.tsx @@ -642,91 +642,6 @@ it('should update options (does not have selected option in new options list)', expect(input).toHaveValue(''); }); -it('should select multiple options', async () => { - const mockOnChange = vi.fn(); - const options = [0, 1, 2, 3].map((value) => ({ - value, - label: `Item ${value}`, - })); - - const { container } = render( - , - ); - - const inputContainer = container.querySelector( - '.iui-input-grid', - ) as HTMLDivElement; - await userEvent.tab(); - await userEvent.click(screen.getByText('Item 1')); - await userEvent.click(screen.getByText('Item 2')); - await userEvent.click(screen.getByText('Item 3')); - - const tags = inputContainer.querySelectorAll('.iui-select-tag'); - expect(tags.length).toBe(3); - expect(tags[0].querySelector('.iui-select-tag-label')).toHaveTextContent( - 'Item 1', - ); - expect(tags[1].querySelector('.iui-select-tag-label')).toHaveTextContent( - 'Item 2', - ); - expect(tags[2].querySelector('.iui-select-tag-label')).toHaveTextContent( - 'Item 3', - ); -}); - -it('should override multiple selected options', async () => { - const mockOnChange = vi.fn(); - const options = [0, 1, 2, 3].map((value) => ({ - value, - label: `Item ${value}`, - })); - const values = [0, 1]; - - const { container, rerender } = render( - , - ); - - const inputContainer = container.querySelector( - '.iui-input-grid', - ) as HTMLDivElement; - - const tags = inputContainer.querySelectorAll('.iui-select-tag'); - expect(tags.length).toBe(2); - expect(tags[0].querySelector('.iui-select-tag-label')).toHaveTextContent( - 'Item 0', - ); - expect(tags[1].querySelector('.iui-select-tag-label')).toHaveTextContent( - 'Item 1', - ); - - const values2 = [1, 2, 3]; - - rerender( - , - ); - const tags2 = inputContainer.querySelectorAll('.iui-select-tag'); - expect(tags2.length).toBe(3); - expect(tags2[0].querySelector('.iui-select-tag-label')).toHaveTextContent( - 'Item 1', - ); - expect(tags2[1].querySelector('.iui-select-tag-label')).toHaveTextContent( - 'Item 2', - ); - expect(tags2[2].querySelector('.iui-select-tag-label')).toHaveTextContent( - 'Item 3', - ); -}); - it('should handle keyboard navigation when multiple is enabled', async () => { const id = 'test-component'; const mockOnChange = vi.fn(); diff --git a/packages/itwinui-react/src/core/Select/Select.test.tsx b/packages/itwinui-react/src/core/Select/Select.test.tsx index 6d63de0614a..fd67d3167a0 100644 --- a/packages/itwinui-react/src/core/Select/Select.test.tsx +++ b/packages/itwinui-react/src/core/Select/Select.test.tsx @@ -579,47 +579,6 @@ it('should update live region when selection changes', async () => { expect(liveRegion).toHaveTextContent('Item 1, Item 2'); }); -it.each([true, false] as const)( - 'should work in uncontrolled mode (multiple=%s)', - async (multiple) => { - const { container } = render( - `${option.value}`)} - multiple - /> +