From 1697f3bb6a156648ad3d28101c53523a9fb96e02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-David=20St=C3=BCtz?= <39377488+J-x-D@users.noreply.github.com> Date: Thu, 12 Feb 2026 13:23:38 +0100 Subject: [PATCH 1/5] Implement overflow handling for TagList component --- src/components/Tag/TagList.tsx | 184 +++++++++++++++++- .../Tag/stories/TagList.stories.tsx | 16 ++ 2 files changed, 190 insertions(+), 10 deletions(-) diff --git a/src/components/Tag/TagList.tsx b/src/components/Tag/TagList.tsx index 6aed6706c..d8f02b7bb 100644 --- a/src/components/Tag/TagList.tsx +++ b/src/components/Tag/TagList.tsx @@ -1,21 +1,177 @@ import React from "react"; import { CLASSPREFIX as eccgui } from "../../configuration/constants"; +import Tag from "./Tag"; +import Tooltip from "../Tooltip/Tooltip"; export interface TagListProps extends React.HTMLAttributes { label?: string; } function TagList({ children, className = "", label = "", ...otherProps }: TagListProps) { + const containerRef = React.useRef(null); + const measurementRef = React.useRef(null); + const moreTagRef = React.useRef(null); + const [visibleCount, setVisibleCount] = React.useState(null); + + const childArray = React.Children.toArray(children).filter(Boolean); + + React.useEffect(() => { + const checkOverflow = () => { + if (!containerRef.current || !measurementRef.current || !moreTagRef.current || childArray.length === 0) { + return; + } + + const container = containerRef.current; + const measurement = measurementRef.current; + const containerWidth = container.clientWidth; + + // If no size constraints, show all tags + if (containerWidth === 0) { + setVisibleCount(null); + return; + } + + const items = Array.from(measurement.children).filter( + (child) => !(child as HTMLElement).dataset.moreTag + ) as HTMLLIElement[]; + + if (items.length === 0) { + setVisibleCount(null); + return; + } + + // Get the actual width of the "+X more" tag + const moreTagWidth = moreTagRef.current.offsetWidth; + + let totalWidth = 0; + let count = 0; + + // Calculate how many items fit in one row + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const itemWidth = item.offsetWidth; + + if (totalWidth + itemWidth <= containerWidth) { + totalWidth += itemWidth; + count++; + } else { + // This item doesn't fit + break; + } + } + + // If not all items fit, adjust count to leave room for "+X more" tag + if (count < childArray.length) { + let adjustedWidth = 0; + let adjustedCount = 0; + + for (let i = 0; i < count; i++) { + const item = items[i]; + const itemWidth = item.offsetWidth; + + if (adjustedWidth + itemWidth + moreTagWidth <= containerWidth) { + adjustedWidth += itemWidth; + adjustedCount++; + } else { + break; + } + } + + // Ensure at least one tag is visible before the "+X more" tag + // Only show overflow if we have at least 1 visible tag + if (adjustedCount > 0) { + setVisibleCount(adjustedCount); + } else { + // If no tags fit with the "+X more" tag, show all tags instead + setVisibleCount(null); + } + } else { + // All items fit + setVisibleCount(null); + } + }; + + // Use RAF to ensure DOM is ready + requestAnimationFrame(() => { + checkOverflow(); + }); + + // Watch for size changes + const resizeObserver = new ResizeObserver(() => { + requestAnimationFrame(checkOverflow); + }); + if (containerRef.current) { + resizeObserver.observe(containerRef.current); + } + + return () => { + resizeObserver.disconnect(); + }; + }, [childArray.length]); + + const showOverflowTag = visibleCount !== null && visibleCount < childArray.length; + const visibleChildren = showOverflowTag ? childArray.slice(0, visibleCount) : childArray; + const hiddenCount = childArray.length - (visibleCount ?? childArray.length); + const tagList = ( -
    - {React.Children.map(children, (child, i) => { - return child ? ( -
  • - {child} -
  • - ) : null; - })} +
      + {visibleChildren.map((child, i) => ( +
    • + {child} +
    • + ))} + {showOverflowTag && ( +
    • + + {childArray.map((child, i) => ( + {child} + ))} + + } + size="large" + > + +{hiddenCount} more + +
    • + )} +
    + ); + + // Hidden measurement list - always rendered for measurements + const measurementList = ( + ); @@ -23,12 +179,20 @@ function TagList({ children, className = "", label = "", ...otherProps }: TagLis return (
    {label} - {tagList} + + {tagList} + {measurementList} +
    ); } - return tagList; + return ( +
    + {tagList} + {measurementList} +
    + ); } export default TagList; diff --git a/src/components/Tag/stories/TagList.stories.tsx b/src/components/Tag/stories/TagList.stories.tsx index 27221c360..a4bddda92 100644 --- a/src/components/Tag/stories/TagList.stories.tsx +++ b/src/components/Tag/stories/TagList.stories.tsx @@ -20,3 +20,19 @@ List.args = { label: "Tag list", children: [Short, List, Of, Tags], }; + +export const ListWithOverflow = Template.bind({}); +ListWithOverflow.args = { + label: "Tag list with overflow", + style: { width: '300px' }, + children: [ + First Tag, + Second Tag, + Third Tag, + Fourth Tag, + Fifth Tag, + Sixth Tag, + Seventh Tag, + Eighth Tag, + ], +}; From eb48d86cc27ab1eabfd69a81e1ff23d7d9340607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-David=20St=C3=BCtz?= <39377488+J-x-D@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:19:57 +0100 Subject: [PATCH 2/5] Remove ListWithOverflow story from TagList Removed the ListWithOverflow story from TagList stories. --- src/components/Tag/stories/TagList.stories.tsx | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/components/Tag/stories/TagList.stories.tsx b/src/components/Tag/stories/TagList.stories.tsx index a4bddda92..27221c360 100644 --- a/src/components/Tag/stories/TagList.stories.tsx +++ b/src/components/Tag/stories/TagList.stories.tsx @@ -20,19 +20,3 @@ List.args = { label: "Tag list", children: [Short, List, Of, Tags], }; - -export const ListWithOverflow = Template.bind({}); -ListWithOverflow.args = { - label: "Tag list with overflow", - style: { width: '300px' }, - children: [ - First Tag, - Second Tag, - Third Tag, - Fourth Tag, - Fifth Tag, - Sixth Tag, - Seventh Tag, - Eighth Tag, - ], -}; From 40dd128503b3092b35a17bf7d8e7410f72471284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-David=20St=C3=BCtz?= <39377488+J-x-D@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:16:02 +0100 Subject: [PATCH 3/5] optimize overflow handling and improve accessibility; add storybook example --- src/components/Tag/TagList.tsx | 61 +++++++++++-------- .../Tag/stories/TagList.stories.tsx | 22 +++++++ 2 files changed, 59 insertions(+), 24 deletions(-) diff --git a/src/components/Tag/TagList.tsx b/src/components/Tag/TagList.tsx index d8f02b7bb..4004924c2 100644 --- a/src/components/Tag/TagList.tsx +++ b/src/components/Tag/TagList.tsx @@ -1,9 +1,10 @@ import React from "react"; import { CLASSPREFIX as eccgui } from "../../configuration/constants"; -import Tag from "./Tag"; import Tooltip from "../Tooltip/Tooltip"; +import Tag from "./Tag"; + export interface TagListProps extends React.HTMLAttributes { label?: string; } @@ -14,9 +15,11 @@ function TagList({ children, className = "", label = "", ...otherProps }: TagLis const moreTagRef = React.useRef(null); const [visibleCount, setVisibleCount] = React.useState(null); - const childArray = React.Children.toArray(children).filter(Boolean); + const childArray = React.useMemo(() => React.Children.toArray(children).filter(Boolean), [children]); React.useEffect(() => { + let rafId: number | null = null; + const checkOverflow = () => { if (!containerRef.current || !measurementRef.current || !moreTagRef.current || childArray.length === 0) { return; @@ -93,22 +96,28 @@ function TagList({ children, className = "", label = "", ...otherProps }: TagLis }; // Use RAF to ensure DOM is ready - requestAnimationFrame(() => { + rafId = requestAnimationFrame(() => { checkOverflow(); }); // Watch for size changes const resizeObserver = new ResizeObserver(() => { - requestAnimationFrame(checkOverflow); + if (rafId !== null) { + cancelAnimationFrame(rafId); + } + rafId = requestAnimationFrame(checkOverflow); }); if (containerRef.current) { resizeObserver.observe(containerRef.current); } return () => { + if (rafId !== null) { + cancelAnimationFrame(rafId); + } resizeObserver.disconnect(); }; - }, [childArray.length]); + }, [childArray]); const showOverflowTag = visibleCount !== null && visibleCount < childArray.length; const visibleChildren = showOverflowTag ? childArray.slice(0, visibleCount) : childArray; @@ -118,19 +127,21 @@ function TagList({ children, className = "", label = "", ...otherProps }: TagLis
      {visibleChildren.map((child, i) => ( -
    • +
    • {child}
    • ))} {showOverflowTag && ( -
    • +
    • +
      {childArray.map((child, i) => ( {child} ))} @@ -138,7 +149,14 @@ function TagList({ children, className = "", label = "", ...otherProps }: TagLis } size="large" > - +{hiddenCount} more + + +{hiddenCount} more +
    • )} @@ -149,12 +167,12 @@ function TagList({ children, className = "", label = "", ...otherProps }: TagLis const measurementList = ( @@ -179,7 +192,7 @@ function TagList({ children, className = "", label = "", ...otherProps }: TagLis return (
      {label} - + {tagList} {measurementList} @@ -188,7 +201,7 @@ function TagList({ children, className = "", label = "", ...otherProps }: TagLis } return ( -
      +
      {tagList} {measurementList}
      diff --git a/src/components/Tag/stories/TagList.stories.tsx b/src/components/Tag/stories/TagList.stories.tsx index 27221c360..c9c5f95bb 100644 --- a/src/components/Tag/stories/TagList.stories.tsx +++ b/src/components/Tag/stories/TagList.stories.tsx @@ -20,3 +20,25 @@ List.args = { label: "Tag list", children: [Short, List, Of, Tags], }; + +export const ListWithOverflow: StoryFn = () => ( +
      + + JavaScript + TypeScript + Python + Java + C++ + Ruby + Go + Rust + +
      +); +ListWithOverflow.parameters = { + docs: { + description: { + story: 'When tags exceed the container width, a "+X more" button appears. Hover over it to see all tags in a tooltip.', + }, + }, +}; From 6df74c15b93f6877eb1e4607cf013237097c5dd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-David=20St=C3=BCtz?= <39377488+J-x-D@users.noreply.github.com> Date: Thu, 19 Feb 2026 08:52:14 +0100 Subject: [PATCH 4/5] Enhance TagList component with improved overflow handling and styling; update storybook example --- src/components/Tag/TagList.tsx | 38 ++++++++------- .../Tag/stories/TagList.stories.tsx | 2 +- src/components/Tag/tag.scss | 46 +++++++++++++++++++ src/components/Tooltip/tooltip.scss | 4 ++ 4 files changed, 72 insertions(+), 18 deletions(-) diff --git a/src/components/Tag/TagList.tsx b/src/components/Tag/TagList.tsx index 4004924c2..ddb72b32f 100644 --- a/src/components/Tag/TagList.tsx +++ b/src/components/Tag/TagList.tsx @@ -82,12 +82,13 @@ function TagList({ children, className = "", label = "", ...otherProps }: TagLis } // Ensure at least one tag is visible before the "+X more" tag - // Only show overflow if we have at least 1 visible tag if (adjustedCount > 0) { setVisibleCount(adjustedCount); } else { - // If no tags fit with the "+X more" tag, show all tags instead - setVisibleCount(null); + // No tags fit alongside the "+X more" tag (e.g. first tag is very wide). + // Force 1 visible tag so the overflow indicator is still shown; + // CSS (min-width: 0 + overflow: hidden on the li) handles clipping. + setVisibleCount(1); } } else { // All items fit @@ -129,19 +130,28 @@ function TagList({ children, className = "", label = "", ...otherProps }: TagLis {...otherProps} role="list" aria-label={label || "Tag list"} - style={{ ...otherProps.style, display: "flex", flexWrap: "nowrap", overflow: "hidden" }} ref={containerRef} > {visibleChildren.map((child, i) => ( -
    • +
    • {child}
    • ))} {showOverflowTag && ( -
    • +
    • +
      {childArray.map((child, i) => ( {child} ))} @@ -166,14 +176,8 @@ function TagList({ children, className = "", label = "", ...otherProps }: TagLis // Hidden measurement list - always rendered for measurements const measurementList = (