diff --git a/src/components/Tag/TagList.tsx b/src/components/Tag/TagList.tsx index 6aed6706..4004924c 100644 --- a/src/components/Tag/TagList.tsx +++ b/src/components/Tag/TagList.tsx @@ -1,21 +1,190 @@ import React from "react"; import { CLASSPREFIX as eccgui } from "../../configuration/constants"; +import Tooltip from "../Tooltip/Tooltip"; + +import Tag from "./Tag"; 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.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; + } + + 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 + rafId = requestAnimationFrame(() => { + checkOverflow(); + }); + + // Watch for size changes + const resizeObserver = new ResizeObserver(() => { + if (rafId !== null) { + cancelAnimationFrame(rafId); + } + rafId = requestAnimationFrame(checkOverflow); + }); + if (containerRef.current) { + resizeObserver.observe(containerRef.current); + } + + return () => { + if (rafId !== null) { + cancelAnimationFrame(rafId); + } + resizeObserver.disconnect(); + }; + }, [childArray]); + + 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 +192,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 27221c36..c9c5f95b 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.', + }, + }, +};