diff --git a/changes/52810-cap-truncated-text-list-tooltip b/changes/52810-cap-truncated-text-list-tooltip new file mode 100644 index 00000000000..4b93dbe1ef7 --- /dev/null +++ b/changes/52810-cap-truncated-text-list-tooltip @@ -0,0 +1 @@ +- Fixed the software details modal so that hovering the vulnerabilities list shows the first 10 CVEs and a count of the rest, instead of a tooltip that runs off the screen when software has many CVEs. diff --git a/frontend/components/TruncatedTextList/TruncatedTextList.stories.tsx b/frontend/components/TruncatedTextList/TruncatedTextList.stories.tsx index 31dc16b0e20..e38ab745a39 100644 --- a/frontend/components/TruncatedTextList/TruncatedTextList.stories.tsx +++ b/frontend/components/TruncatedTextList/TruncatedTextList.stories.tsx @@ -37,3 +37,12 @@ export const AllFit: Story = { args: { items: ["Mac", "Linux"] }, decorators: [withFrame(360)], }; + +/** Hover "+N more" — the tooltip lists 10 items and counts the rest, rather + * than growing past the height of the window. */ +export const ManyItems: Story = { + args: { + items: Array.from({ length: 57 }, (_, i) => `CVE-2026-${1000 + i}`), + }, + decorators: [withFrame(360)], +}; diff --git a/frontend/components/TruncatedTextList/TruncatedTextList.tests.tsx b/frontend/components/TruncatedTextList/TruncatedTextList.tests.tsx index d1c87510d98..8cf817c1f3a 100644 --- a/frontend/components/TruncatedTextList/TruncatedTextList.tests.tsx +++ b/frontend/components/TruncatedTextList/TruncatedTextList.tests.tsx @@ -65,4 +65,19 @@ describe("TruncatedTextList — truncatedFirstContent edge cases", () => { expect.objectContaining({ tipContent: longName }) ); }); + + it("caps the hidden items listed in the tooltip and counts the rest", () => { + const items = Array.from({ length: 57 }, (_, i) => `CVE-2026-${i}`); + render(); + + // The first item is short enough to skip its own tooltip, so the only + // wrapper here is the "+N more" pill's. + const { tipContent } = mockedTooltipWrapper.mock.calls[0][0]; + const { container } = render(
{tipContent}
); + + // 56 items are hidden: 10 listed, the remaining 46 rolled into a count. + expect(container).toHaveTextContent("CVE-2026-10"); + expect(container).not.toHaveTextContent("CVE-2026-11"); + expect(container).toHaveTextContent("+46 more"); + }); }); diff --git a/frontend/components/TruncatedTextList/TruncatedTextList.tsx b/frontend/components/TruncatedTextList/TruncatedTextList.tsx index c8319e0174e..160d7c3efaa 100644 --- a/frontend/components/TruncatedTextList/TruncatedTextList.tsx +++ b/frontend/components/TruncatedTextList/TruncatedTextList.tsx @@ -25,16 +25,26 @@ interface ITruncatedTextListProps { const truncateString = (s: string, max: number) => s.length > max ? `${s.slice(0, max).trimEnd()}...` : s; -const renderItemsList = (list: string[]) => ( - <> - {list.map((name, i) => ( - - {name} - {i < list.length - 1 &&
} -
- ))} - -); +/** A hover tooltip can't scroll, so an uncapped list runs off the screen with + * no way to reach the rest of it. Same cap idea as `VulnerabilitiesCell`. */ +const MAX_ITEMS_IN_TOOLTIP = 10; + +const renderItemsList = (list: string[]) => { + const shown = list.slice(0, MAX_ITEMS_IN_TOOLTIP); + const remaining = list.length - shown.length; + + return ( + <> + {shown.map((name, i) => ( + + {name} + {(i < shown.length - 1 || remaining > 0) &&
} +
+ ))} + {remaining > 0 && `+${remaining} more`} + + ); +}; interface IRenderVisibleRowParams { visibleCount: number;