From c325bba87f99b1a29d7425910d6f2d57aab5ca9a Mon Sep 17 00:00:00 2001 From: David Menendez Date: Fri, 27 Dec 2024 13:08:33 -0600 Subject: [PATCH 1/7] refactor: decouple overflow logic to create independent overflow utility --- .../TagOverflow/TagOverflow.stories.jsx | 1 + .../components/TagOverflow/TagOverflow.tsx | 147 +++--------------- .../src/global/js/hooks/useOverflowItems.ts | 93 +++++++++++ 3 files changed, 116 insertions(+), 125 deletions(-) create mode 100644 packages/ibm-products/src/global/js/hooks/useOverflowItems.ts diff --git a/packages/ibm-products/src/components/TagOverflow/TagOverflow.stories.jsx b/packages/ibm-products/src/components/TagOverflow/TagOverflow.stories.jsx index e7fd34845e..e10ac46038 100644 --- a/packages/ibm-products/src/components/TagOverflow/TagOverflow.stories.jsx +++ b/packages/ibm-products/src/components/TagOverflow/TagOverflow.stories.jsx @@ -75,6 +75,7 @@ export const TagsWithOverflowCount = Template.bind({}); TagsWithOverflowCount.args = { containerWidth: 250, items: fiveTags, + onOverflowTagChange: (items) => console.log(items), }; export const TagsWithTruncation = Template.bind({}); diff --git a/packages/ibm-products/src/components/TagOverflow/TagOverflow.tsx b/packages/ibm-products/src/components/TagOverflow/TagOverflow.tsx index ee592090a9..afaf53d5ed 100644 --- a/packages/ibm-products/src/components/TagOverflow/TagOverflow.tsx +++ b/packages/ibm-products/src/components/TagOverflow/TagOverflow.tsx @@ -7,12 +7,10 @@ import React, { ReactNode, - Ref, RefObject, createElement, forwardRef, useCallback, - useEffect, useRef, useState, } from 'react'; @@ -26,7 +24,7 @@ import cx from 'classnames'; import { getDevtoolsProps } from '../../global/js/utils/devtools'; import { isRequiredIf } from '../../global/js/utils/props-helper'; import { pkg } from '../../settings'; -import { useResizeObserver } from '../../global/js/hooks/useResizeObserver'; +import { useOverflowItems } from '../../global/js/hooks/useOverflowItems'; export interface TagOverflowItem { className?: string; /** @@ -61,10 +59,19 @@ export interface TagOverflowProps { allTagsModalTitle?: string; autoAlign?: boolean; className?: string; - containingElementRef?: RefObject; + /** + * @deprecated The `containingElementRef` prop is no longer going to be used in favor of the forwarded ref. + */ + containingElementRef?: RefObject; items: TagOverflowItem[]; maxVisible?: number; + /** + * @deprecated The `measurementOffset` prop is no longer going to be used. This value will now be calculated automatically. + */ measurementOffset?: number; + /** + * @deprecated The `multiline` prop is no longer going to be used. This component should only be used when you need to hide overflowing items. + */ multiline?: boolean; overflowAlign?: | 'top' @@ -90,8 +97,8 @@ const blockClass = `${pkg.prefix}--tag-overflow`; const componentName = 'TagOverflow'; const allTagsModalSearchThreshold = 10; -export let TagOverflow = forwardRef( - (props: TagOverflowProps, ref: Ref) => { +export let TagOverflow = forwardRef( + (props, ref) => { const { align = 'start', allTagsModalAriaLabel, @@ -101,11 +108,8 @@ export let TagOverflow = forwardRef( allTagsModalTitle, autoAlign, className, - containingElementRef, items, maxVisible, - measurementOffset = 0, - multiline, overflowAlign = 'bottom', overflowClassName, overflowType = 'default', @@ -115,25 +119,16 @@ export let TagOverflow = forwardRef( ...rest } = props; - const localContainerRef = useRef(null); - const containerRef = ref || localContainerRef; - const itemRefs = useRef | null>(null); const overflowRef = useRef(null); - // itemOffset is the value of margin applied on each items - // This value is required for calculating how many items will fit within the container - const itemOffset = 4; - const overflowIndicatorWidth = 40; - - const [containerWidth, setContainerWidth] = useState(0); - const [visibleItems, setVisibleItems] = useState([]); - const [overflowItems, setOverflowItems] = useState([]); const [showAllModalOpen, setShowAllModalOpen] = useState(false); const [popoverOpen, setPopoverOpen] = useState(false); - const resizeElm = - containingElementRef && containingElementRef.current - ? containingElementRef - : containerRef; + const { + visibleItems, + hiddenItems: overflowItems, + containerRef, + itemRefHandler, + } = useOverflowItems(items, ref, maxVisible, onOverflowTagChange); const handleShowAllClick = () => { setShowAllModalOpen(true); @@ -143,80 +138,6 @@ export let TagOverflow = forwardRef( setShowAllModalOpen(false); }; - const handleResize = () => { - if (typeof resizeElm !== 'function' && resizeElm.current) { - setContainerWidth(resizeElm.current.offsetWidth); - } - }; - - useResizeObserver(resizeElm, handleResize); - - const getMap = () => { - if (!itemRefs.current) { - itemRefs.current = new Map(); - } - return itemRefs.current; - }; - - const itemRefHandler = (id, node) => { - const map = getMap(); - if (id && node && map.get(id) !== node.offsetWidth) { - map.set(id, node.offsetWidth); - } - }; - - const getVisibleItems = useCallback(() => { - if (!itemRefs.current) { - return items; - } - - if (multiline) { - const visibleItems = maxVisible ? items?.slice(0, maxVisible) : items; - return visibleItems; - } - - const map = getMap(); - const optionalContainingElement = containingElementRef?.current; - const measurementOffsetValue = - typeof measurementOffset === 'number' ? measurementOffset : 0; - const spaceAvailable = optionalContainingElement - ? optionalContainingElement.offsetWidth - measurementOffsetValue - : containerWidth; - - const overflowContainerWidth = - overflowRef && - overflowRef.current && - overflowRef.current.offsetWidth > overflowIndicatorWidth - ? overflowRef.current.offsetWidth - : overflowIndicatorWidth; - const maxWidth = spaceAvailable - overflowContainerWidth; - - let childrenWidth = 0; - let maxReached = false; - - return items.reduce((prev: TagOverflowItem[], cur: TagOverflowItem) => { - if (!maxReached) { - const itemWidth = (map ? Number(map.get(cur.id)) : 0) + itemOffset; - const fits = itemWidth + childrenWidth < maxWidth; - - if (fits) { - childrenWidth += itemWidth; - prev.push(cur); - } else { - maxReached = true; - } - } - return prev; - }, []); - }, [ - containerWidth, - containingElementRef, - items, - maxVisible, - measurementOffset, - multiline, - ]); - const getCustomComponent = ( item: TagOverflowItem, tagComponent: string @@ -226,27 +147,10 @@ export let TagOverflow = forwardRef( ...other, key: id, className: cx(`${blockClass}__item`, className), - ref: (node) => itemRefHandler(id, node), + ref: (node) => itemRefHandler(id, node as HTMLDivElement), }); }; - useEffect(() => { - let visibleItemsArr = getVisibleItems(); - - if (maxVisible && maxVisible < visibleItemsArr.length) { - visibleItemsArr = visibleItemsArr?.slice(0, maxVisible); - } - - const hiddenItems = items?.slice(visibleItemsArr.length); - const overflowItemsArr = hiddenItems?.map(({ tagType, ...other }) => { - return { type: tagType, ...other }; - }); - - setVisibleItems(visibleItemsArr); - setOverflowItems(overflowItemsArr); - onOverflowTagChange?.(overflowItemsArr); - }, [getVisibleItems, items, maxVisible, onOverflowTagChange]); - const handleTagOnClose = useCallback( (onClose, index) => { onClose?.(); @@ -259,24 +163,17 @@ export let TagOverflow = forwardRef( return (
{visibleItems?.length > 0 && visibleItems.map((item, index) => { - // Render custom components if (tagComponent) { return getCustomComponent(item, tagComponent); } else { const { id, label, tagType, onClose, filter, ...other } = item; - // If there is no template prop, then render items as Tags return (
itemRefHandler(id, node)} key={id}> {typeof onClose === 'function' || filter ? ( diff --git a/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts b/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts new file mode 100644 index 0000000000..b24b7b2621 --- /dev/null +++ b/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts @@ -0,0 +1,93 @@ +import { useState, useRef, ForwardedRef } from 'react'; +import { useResizeObserver } from './useResizeObserver'; + +type Item = { + id: string; +}; + +export function useOverflowItems( + items: T[] = [], + ref?: ForwardedRef, + maxVisible?: number, + onChange?: (hiddenItems: T[]) => void +) { + const localRef = useRef(null); + const itemRefs = useRef | null>(null); + const [containerWidth, setContainerWidth] = useState(0); + const containerRef = ref || localRef; + const visibleItemCount = useRef(0); + + const handleResize = () => { + if (typeof containerRef !== 'function' && containerRef.current) { + setContainerWidth(containerRef.current.offsetWidth); + } + }; + + const getMap = (): Map => { + if (!itemRefs.current) { + itemRefs.current = new Map(); + } + return itemRefs.current; + }; + + const itemRefHandler = ( + id: string | number, + node: HTMLElement | null + ): void => { + const map = getMap(); + if (node && !map.get(id)) { + const style = getComputedStyle?.(node); + const totalWidth = style + ? parseInt(style.marginLeft) + + parseInt(style.marginRight) + + node.offsetWidth + : node.offsetWidth; + map.set(id, totalWidth); + } + }; + + const getItems = (): T[] => { + const map = getMap(); + if (!map) { + return items; + } + const maxWidth = containerWidth; + let maxReached = false; + let totalWidth = 0; + + return items.reduce((prev, cur) => { + if (maxVisible && prev.length >= maxVisible) { + maxReached = true; + } + if (!maxReached) { + const itemWidth = map.get(cur.id) || 0; + const willFit = itemWidth + totalWidth <= maxWidth; + if (willFit) { + totalWidth += itemWidth; + prev.push(cur); + } else { + maxReached = true; + } + } + return prev; + }, [] as T[]); + }; + + useResizeObserver(containerRef, handleResize); + + const visibleItems = getItems(); + const visibleItemsNum = visibleItems.length; + const hiddenItems = items.slice(visibleItemsNum); + // only call the change handler when the number of visible items has changed + if (visibleItemsNum !== visibleItemCount.current) { + visibleItemCount.current = visibleItemsNum; + onChange?.(hiddenItems); + } + + return { + visibleItems, + hiddenItems, + containerRef, + itemRefHandler, + }; +} From c639cf2e2aa7fa95d4f36854c3a5c53fe713f1f9 Mon Sep 17 00:00:00 2001 From: David Menendez Date: Fri, 27 Dec 2024 13:18:38 -0600 Subject: [PATCH 2/7] chore: copyright --- .../ibm-products/src/global/js/hooks/useOverflowItems.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts b/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts index b24b7b2621..0323a02191 100644 --- a/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts +++ b/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts @@ -1,3 +1,10 @@ +/** + * Copyright IBM Corp. 2024, 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + import { useState, useRef, ForwardedRef } from 'react'; import { useResizeObserver } from './useResizeObserver'; From c3572f0460180d9e36abc5e7ccbc23ecc2c3ba29 Mon Sep 17 00:00:00 2001 From: David Menendez Date: Mon, 6 Jan 2025 16:09:30 -0600 Subject: [PATCH 3/7] fix: include additional offset measure --- .../src/components/TagOverflow/TagOverflow.tsx | 2 +- .../src/global/js/hooks/useOverflowItems.ts | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/ibm-products/src/components/TagOverflow/TagOverflow.tsx b/packages/ibm-products/src/components/TagOverflow/TagOverflow.tsx index afaf53d5ed..dfc7772792 100644 --- a/packages/ibm-products/src/components/TagOverflow/TagOverflow.tsx +++ b/packages/ibm-products/src/components/TagOverflow/TagOverflow.tsx @@ -128,7 +128,7 @@ export let TagOverflow = forwardRef( hiddenItems: overflowItems, containerRef, itemRefHandler, - } = useOverflowItems(items, ref, maxVisible, onOverflowTagChange); + } = useOverflowItems(items, ref, maxVisible, onOverflowTagChange, 40); const handleShowAllClick = () => { setShowAllModalOpen(true); diff --git a/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts b/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts index 0323a02191..ff6367ae91 100644 --- a/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts +++ b/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts @@ -16,7 +16,8 @@ export function useOverflowItems( items: T[] = [], ref?: ForwardedRef, maxVisible?: number, - onChange?: (hiddenItems: T[]) => void + onChange?: (hiddenItems: T[]) => void, + additionalOffset = 0 ) { const localRef = useRef(null); const itemRefs = useRef | null>(null); @@ -55,10 +56,10 @@ export function useOverflowItems( const getItems = (): T[] => { const map = getMap(); - if (!map) { + if (!containerWidth) { return items; } - const maxWidth = containerWidth; + const spaceAvailable = containerWidth - additionalOffset; let maxReached = false; let totalWidth = 0; @@ -66,9 +67,9 @@ export function useOverflowItems( if (maxVisible && prev.length >= maxVisible) { maxReached = true; } - if (!maxReached) { + if (maxReached === false) { const itemWidth = map.get(cur.id) || 0; - const willFit = itemWidth + totalWidth <= maxWidth; + const willFit = itemWidth + totalWidth <= spaceAvailable; if (willFit) { totalWidth += itemWidth; prev.push(cur); From d44bb1fa48bf25a2494dd7efd75b76a0666642c1 Mon Sep 17 00:00:00 2001 From: David Menendez Date: Tue, 7 Jan 2025 10:59:54 -0600 Subject: [PATCH 4/7] fix: test fix --- .../src/global/js/hooks/useOverflowItems.ts | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts b/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts index ff6367ae91..c8a50e9dc7 100644 --- a/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts +++ b/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts @@ -17,7 +17,7 @@ export function useOverflowItems( ref?: ForwardedRef, maxVisible?: number, onChange?: (hiddenItems: T[]) => void, - additionalOffset = 0 + additionalOffset: number = 0 ) { const localRef = useRef(null); const itemRefs = useRef | null>(null); @@ -42,26 +42,25 @@ export function useOverflowItems( id: string | number, node: HTMLElement | null ): void => { - const map = getMap(); - if (node && !map.get(id)) { + if (id && node) { + const map = getMap(); const style = getComputedStyle?.(node); const totalWidth = style ? parseInt(style.marginLeft) + parseInt(style.marginRight) + node.offsetWidth : node.offsetWidth; - map.set(id, totalWidth); + if (map.get(id) !== totalWidth) { + map.set(id, totalWidth); + } } }; const getItems = (): T[] => { const map = getMap(); - if (!containerWidth) { - return items; - } - const spaceAvailable = containerWidth - additionalOffset; + const maxWidth = containerWidth ? containerWidth - additionalOffset : 0; let maxReached = false; - let totalWidth = 0; + let currentWidth = 0; return items.reduce((prev, cur) => { if (maxVisible && prev.length >= maxVisible) { @@ -69,9 +68,9 @@ export function useOverflowItems( } if (maxReached === false) { const itemWidth = map.get(cur.id) || 0; - const willFit = itemWidth + totalWidth <= spaceAvailable; + const willFit = itemWidth + currentWidth <= maxWidth; if (willFit) { - totalWidth += itemWidth; + currentWidth += itemWidth; prev.push(cur); } else { maxReached = true; From 0ae2d759a8ebdf6c36bd26c7900498eb8cba2013 Mon Sep 17 00:00:00 2001 From: David Menendez Date: Thu, 9 Jan 2025 08:42:03 -0600 Subject: [PATCH 5/7] fix: testing update --- .../components/TagOverflow/_tag-overflow.scss | 7 +- .../TagOverflow/TagOverflow.test.js | 11 +-- .../components/TagOverflow/TagOverflow.tsx | 82 ++++++++++--------- .../src/global/js/hooks/useOverflowItems.ts | 19 +++-- 4 files changed, 58 insertions(+), 61 deletions(-) diff --git a/packages/ibm-products-styles/src/components/TagOverflow/_tag-overflow.scss b/packages/ibm-products-styles/src/components/TagOverflow/_tag-overflow.scss index 207d86585d..897310cc29 100644 --- a/packages/ibm-products-styles/src/components/TagOverflow/_tag-overflow.scss +++ b/packages/ibm-products-styles/src/components/TagOverflow/_tag-overflow.scss @@ -26,13 +26,8 @@ $block-class: #{c4p-settings.$pkg-prefix}--tag-overflow; $block-class-overflow: #{$block-class}-popover; $block-class-modal: #{$block-class}-modal; -.#{$block-class} { +.#{$block-class}__visible-tags { display: flex; - width: 100%; - min-width: $spacing-12; - align-items: center; - justify-content: flex-start; - white-space: nowrap; } .#{$block-class}--align-end { diff --git a/packages/ibm-products/src/components/TagOverflow/TagOverflow.test.js b/packages/ibm-products/src/components/TagOverflow/TagOverflow.test.js index 263e41f434..85727438fa 100644 --- a/packages/ibm-products/src/components/TagOverflow/TagOverflow.test.js +++ b/packages/ibm-products/src/components/TagOverflow/TagOverflow.test.js @@ -6,7 +6,7 @@ */ import React from 'react'; -import { fireEvent, render, screen } from '@testing-library/react'; // https://testing-library.com/docs/react-testing-library/intro +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; // https://testing-library.com/docs/react-testing-library/intro import { pkg } from '../../settings'; import uuidv4 from '../../global/js/utils/uuidv4'; @@ -104,12 +104,9 @@ describe(componentName, () => { it('Obeys max visible', async () => { render(); - - expect( - screen.getAllByText(/Tag [0-9]+/, { - selector: `.${blockClass}__item--tag span`, - }).length - ).toEqual(3); + await waitFor(() => { + expect(screen.getByText('+2')); + }); }); // The below test case is failing due to ResizeObserver mock diff --git a/packages/ibm-products/src/components/TagOverflow/TagOverflow.tsx b/packages/ibm-products/src/components/TagOverflow/TagOverflow.tsx index dfc7772792..7698540135 100644 --- a/packages/ibm-products/src/components/TagOverflow/TagOverflow.tsx +++ b/packages/ibm-products/src/components/TagOverflow/TagOverflow.tsx @@ -142,12 +142,10 @@ export let TagOverflow = forwardRef( item: TagOverflowItem, tagComponent: string ) => { - const { className, id, ...other } = item; + const { className, ...other } = item; return createElement(tagComponent, { ...other, - key: id, className: cx(`${blockClass}__item`, className), - ref: (node) => itemRefHandler(id, node as HTMLDivElement), }); }; @@ -164,43 +162,49 @@ export let TagOverflow = forwardRef( return (
- {visibleItems?.length > 0 && - visibleItems.map((item, index) => { - if (tagComponent) { - return getCustomComponent(item, tagComponent); - } else { - const { id, label, tagType, onClose, filter, ...other } = item; - return ( -
itemRefHandler(id, node)} key={id}> - {typeof onClose === 'function' || filter ? ( - handleTagOnClose(onClose, index)} - text={label} - /> - ) : ( - - {label} - - )} -
- ); - } +
+ {visibleItems.map((item, index) => { + const { id, label, tagType, onClose, filter, ...other } = item; + return ( +
itemRefHandler(id, node)} + key={id} + > + {tagComponent ? ( + getCustomComponent(item, tagComponent) + ) : typeof onClose === 'function' || filter ? ( + handleTagOnClose(onClose, index)} + text={label} + /> + ) : ( + + {label} + + )} +
+ ); })} - - - {overflowItems?.length > 0 && ( - <> + {overflowItems.length > 0 && ( +
( searchPlaceholder={allTagsModalSearchPlaceholderText} portalTarget={allTagsModalTarget} /> - +
)} -
+
); } diff --git a/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts b/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts index c8a50e9dc7..8d241e0889 100644 --- a/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts +++ b/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts @@ -58,22 +58,23 @@ export function useOverflowItems( const getItems = (): T[] => { const map = getMap(); - const maxWidth = containerWidth ? containerWidth - additionalOffset : 0; + const visibleItems = maxVisible ? items.slice(0, maxVisible) : items; + if (containerWidth === 0) { + return visibleItems; + } + const maxWidth = containerWidth - additionalOffset; let maxReached = false; let currentWidth = 0; - return items.reduce((prev, cur) => { - if (maxVisible && prev.length >= maxVisible) { - maxReached = true; - } + return visibleItems.reduce((prev, cur) => { if (maxReached === false) { const itemWidth = map.get(cur.id) || 0; - const willFit = itemWidth + currentWidth <= maxWidth; - if (willFit) { + if (itemWidth + currentWidth > maxWidth) { + maxReached = true; + } + if (maxReached === false) { currentWidth += itemWidth; prev.push(cur); - } else { - maxReached = true; } } return prev; From 0158df1a4dd959f3d68ae4899987ef341a114b88 Mon Sep 17 00:00:00 2001 From: David Menendez Date: Mon, 13 Jan 2025 14:33:40 -0600 Subject: [PATCH 6/7] fix: improved offset detection --- .../components/TagOverflow/TagOverflow.tsx | 21 ++- .../src/global/js/hooks/useOverflowItems.ts | 121 ++++++++---------- 2 files changed, 70 insertions(+), 72 deletions(-) diff --git a/packages/ibm-products/src/components/TagOverflow/TagOverflow.tsx b/packages/ibm-products/src/components/TagOverflow/TagOverflow.tsx index 7698540135..582b0833a2 100644 --- a/packages/ibm-products/src/components/TagOverflow/TagOverflow.tsx +++ b/packages/ibm-products/src/components/TagOverflow/TagOverflow.tsx @@ -118,17 +118,22 @@ export let TagOverflow = forwardRef( tagComponent, ...rest } = props; - - const overflowRef = useRef(null); + const containerRef = useRef(null); + const offsetRef = useRef(null); const [showAllModalOpen, setShowAllModalOpen] = useState(false); const [popoverOpen, setPopoverOpen] = useState(false); const { visibleItems, hiddenItems: overflowItems, - containerRef, itemRefHandler, - } = useOverflowItems(items, ref, maxVisible, onOverflowTagChange, 40); + } = useOverflowItems( + items, + containerRef, + offsetRef, + maxVisible, + onOverflowTagChange + ); const handleShowAllClick = () => { setShowAllModalOpen(true); @@ -178,7 +183,9 @@ export let TagOverflow = forwardRef( return (
itemRefHandler(id, node)} + ref={(node) => { + itemRefHandler(id, node); + }} key={id} > {tagComponent ? ( @@ -204,7 +211,7 @@ export let TagOverflow = forwardRef( ); })} {overflowItems.length > 0 && ( -
+
( overflowType={overflowType} showAllTagsLabel={showAllTagsLabel} key="tag-overflow-popover" - ref={overflowRef} + ref={offsetRef} popoverOpen={popoverOpen} setPopoverOpen={setPopoverOpen} autoAlign={autoAlign} diff --git a/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts b/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts index 8d241e0889..3c6e0c300b 100644 --- a/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts +++ b/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts @@ -1,101 +1,92 @@ -/** - * Copyright IBM Corp. 2024, 2025 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { useState, useRef, ForwardedRef } from 'react'; +import { RefObject, useRef, useState } from 'react'; import { useResizeObserver } from './useResizeObserver'; type Item = { id: string; }; -export function useOverflowItems( +export const useOverflowItems = ( items: T[] = [], - ref?: ForwardedRef, - maxVisible?: number, - onChange?: (hiddenItems: T[]) => void, - additionalOffset: number = 0 -) { - const localRef = useRef(null); - const itemRefs = useRef | null>(null); - const [containerWidth, setContainerWidth] = useState(0); - const containerRef = ref || localRef; + containerRef: RefObject, + offsetRef?: RefObject, + maxItems?: number, + onChange?: (hiddenItems: T[]) => void +) => { + const itemsRef = useRef | null>(null); + const [maxWidth, setMaxWidth] = useState(0); const visibleItemCount = useRef(0); const handleResize = () => { - if (typeof containerRef !== 'function' && containerRef.current) { - setContainerWidth(containerRef.current.offsetWidth); + if (containerRef.current) { + const offset = offsetRef?.current?.offsetWidth || 0; + const newMax = containerRef.current.offsetWidth - offset; + setMaxWidth(newMax); } }; - const getMap = (): Map => { - if (!itemRefs.current) { - itemRefs.current = new Map(); + useResizeObserver(containerRef, handleResize); + + const getMap = () => { + if (!itemsRef.current) { + itemsRef.current = new Map(); } - return itemRefs.current; + return itemsRef.current; }; - const itemRefHandler = ( - id: string | number, - node: HTMLElement | null - ): void => { - if (id && node) { - const map = getMap(); + const itemRefHandler = (id: string, node: HTMLDivElement | null) => { + const map = getMap(); + if (node) { const style = getComputedStyle?.(node); - const totalWidth = style - ? parseInt(style.marginLeft) + - parseInt(style.marginRight) + - node.offsetWidth - : node.offsetWidth; - if (map.get(id) !== totalWidth) { - map.set(id, totalWidth); - } + const totalWidth = + node.offsetWidth + + parseInt(style.marginLeft) + + parseInt(style.marginRight); + map.set(id, totalWidth); } + + return () => { + map.delete(id); + }; }; - const getItems = (): T[] => { - const map = getMap(); - const visibleItems = maxVisible ? items.slice(0, maxVisible) : items; - if (containerWidth === 0) { - return visibleItems; + const getVisibleItems = () => { + if (!containerRef) { + return items; } - const maxWidth = containerWidth - additionalOffset; + + const map = getMap(); let maxReached = false; - let currentWidth = 0; + let accumulatedWidth = 0; + + const visibleItems = items.slice(0, maxItems).reduce((prev, cur) => { + if (maxReached) { + return prev; + } - return visibleItems.reduce((prev, cur) => { - if (maxReached === false) { - const itemWidth = map.get(cur.id) || 0; - if (itemWidth + currentWidth > maxWidth) { - maxReached = true; - } - if (maxReached === false) { - currentWidth += itemWidth; - prev.push(cur); - } + const itemWidth = map.get(cur.id) || 0; + const willFit = accumulatedWidth + itemWidth <= maxWidth; + if (willFit) { + accumulatedWidth += itemWidth; + prev.push(cur); + } else { + maxReached = true; } return prev; }, [] as T[]); + return visibleItems; }; - useResizeObserver(containerRef, handleResize); - - const visibleItems = getItems(); - const visibleItemsNum = visibleItems.length; - const hiddenItems = items.slice(visibleItemsNum); + const visibleItems = getVisibleItems(); + const hiddenItems = items.slice(visibleItems.length); // only call the change handler when the number of visible items has changed - if (visibleItemsNum !== visibleItemCount.current) { - visibleItemCount.current = visibleItemsNum; + if (visibleItems.length !== visibleItemCount.current) { + visibleItemCount.current = visibleItems.length; onChange?.(hiddenItems); } return { visibleItems, - hiddenItems, - containerRef, itemRefHandler, + hiddenItems, }; -} +}; From 9bb01f19ae37b0c96f7ca085cb1a4bda4286f15f Mon Sep 17 00:00:00 2001 From: David Menendez Date: Mon, 13 Jan 2025 14:37:21 -0600 Subject: [PATCH 7/7] fix: license --- .../ibm-products/src/global/js/hooks/useOverflowItems.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts b/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts index 3c6e0c300b..f18955a9e3 100644 --- a/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts +++ b/packages/ibm-products/src/global/js/hooks/useOverflowItems.ts @@ -1,3 +1,10 @@ +/** + * Copyright IBM Corp. 2025, 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + import { RefObject, useRef, useState } from 'react'; import { useResizeObserver } from './useResizeObserver';