diff --git a/src/components/Tag/TagList.tsx b/src/components/Tag/TagList.tsx index 6aed6706..d8f02b7b 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;