You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Adds two optional, default-off, non-breaking props to OverflowList (and the underlying useOverflow hook):
maxVisibleItems?: number — a hard ceiling on how many items render, even when they all fit. The symmetric partner to the existing minVisibleItems floor; extra items collapse into the overflow indicator. Defaults to undefined (no cap).
maxRows?: number — bounded multi-row wrapping. Items wrap onto up to N rows, then the remainder collapses into the overflow indicator. A number, not a boolean (unbounded wrapping is a plain flex-wrap layout, not overflow collapse). Defaults to undefined (single line); maxRows={1} is equivalent to today.
Capped lists ("show at most N, then +count") and bounded multi-row tag clouds ("wrap onto two rows, then +count") are the two most common collapsing-row patterns the current single-line, floor-only API can't express. A numeric cap matches the industry-standard shape (max, maxCount, maxTagCount, AvatarGroup max); maxVisibleItems mirrors the existing minVisibleItems vocabulary so the pair is discoverable.
Both props are optional and undefined by default — no existing call site changes behavior.
Behavior
Single source of truth for the count:visibleCount = clamp(fitCount, minVisibleItems, maxVisibleItems ?? itemCount).
max < min precedence: the floor wins (it's the stronger guarantee), with a dev-only console.warn.
Measurement (Strategy A): multi-row packing is simulated from the already-measured per-item widths — walk items, accumulate row width, wrap when the next item would exceed the available width, stop admitting once a new row would exceed maxRows, reserving indicator space on the final row. This reuses the existing "measure widths once, compute in JS" model — one measure pass, no extra reflow. It assumes uniform row height (true for typical tag/pill/avatar rows); the visible container's max-height is derived from the measured max item height.
collapseFrom × multi-row:end → the indicator ends the last visible row; start → the indicator begins the first row (packing runs in the same reversed order the single-line path already uses).
The single-line path is untouched when maxRows is undefined; the visible container only switches to flexWrap: wrap with a bounded max-height when maxRows is set.
Independent unit tests for the overflow math
Given the history of overflow-calculation bugs, the core math is extracted into a pure, DOM-free helper — packages/core/src/hooks/computeOverflow.ts — that takes plain inputs (item widths, gap, available width, indicator width, min/max, maxRows, collapseFrom) and returns {visibleCount, rows}. useOverflow calls it after measuring the DOM.
computeOverflow.test.ts exercises it in isolation (34 cases): cap-only, floor-only, cap+floor, max<min (min wins + warn), exact-fit and off-by-one boundaries, indicator-space reservation, multi-row packing (1 row, N rows, overflow into indicator), cap-vs-rows precedence both directions, collapseFrom start vs end, resize shrink/grow re-clamping never exceeding the cap, and degenerate values (0, cap>itemCount, maxRows taller than content, empty list). All assert exact numbers.
Docs & stories
Docsite examples: two new live example blocks — Multi-row Tags (maxRows={2}) and Capped Toolbar (maxVisibleItems={3}) — under packages/cli/templates/blocks/components/OverflowList/, following the existing exampleFor block convention.
Storybook:CappedItems, MultiRow, and CappedMultiRow stories.
.doc.mjs: new prop entries (en/zh/dense) and the "vertical layouts" best-practice reworded to distinguish supported multi-row horizontal wrap from unsupported vertical stacks.
Testing
computeOverflow.test.ts — 34 pure-math cases, all pass.
Self-review — Quick Self-Review checklist (updates to an existing component)
Icons from registry — n/a; no icons added.
themeProps present — no new interactive root elements; the visible container keeps its existing themeProps('overflow-list').
No wrapper divs — no new wrapping DOM; multi-row reuses the existing visible container by switching its style branch (whiteSpace: nowrap → flexWrap: wrap with a bounded max-height) only when maxRows is set.
No CSS shorthands — the multi-row style uses explicit flexWrap/alignContent; the dynamic max-height is a calc() string via a StyleX dynamic function (a supported capability), not a raw inline style.
ARIA still correct — no primitive/role changes; the measurement container stays aria-hidden/inert.
Sandbox/stories updated — three new Storybook stories (cap, multi-row, cap+multi-row) and two new docsite example blocks; no props removed.
Popover background — n/a.
Extra reviewer requirements
Independent pure-function math tests. The fit/clamp logic and the multi-row row-packing simulation were extracted out of the hook into a pure, DOM-free helper — packages/core/src/hooks/computeOverflow.ts — that takes plain inputs and returns {visibleCount, rows}. useOverflow measures the DOM, then delegates to it. computeOverflow.test.ts covers the math in isolation with 34 cases asserting exact numbers: cap-only, floor-only, cap+floor, max<min (min wins + warn), exact-fit + off-by-one boundaries, indicator-space reservation, multi-row packing (1/N rows, overflow into indicator), cap-vs-rows precedence both ways, collapseFrom start vs end, resize shrink/grow re-clamp (never exceeds the cap), and degenerate values (0, cap>itemCount, maxRows taller than content, empty list). Single-line parity with the previous inline loop is preserved (existing hook tests unchanged and green).
Docsite multi-row example. Added live docsite example blocks following the existing exampleFor convention under packages/cli/templates/blocks/components/OverflowList/: OverflowList — Multi-row Tags (maxRows={2}) and OverflowList — Capped Toolbar (maxVisibleItems={3}). The generated example registry picks both up (verified via the docsite data-extraction tests).
Spec compliance / decisions
Built to the approved contract: prop names maxVisibleItems/maxRows (D2/D3), min-wins-with-dev-warn (D1), Strategy A simulated row-packing with documented uniform-row-height assumption (D4), collapseFrom end→last-row / start→first-row (D5), row height from measured max item height (D6), and the .doc.mjs best-practice reworded to distinguish multi-row horizontal wrap from vertical stacks (D7). No deviations.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
CLA SignedThis label is managed by the Meta Open Source bot.needs:design-reviewAffects visuals — Design should review
1 participant
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds two optional, default-off, non-breaking props to
OverflowList(and the underlyinguseOverflowhook):maxVisibleItems?: number— a hard ceiling on how many items render, even when they all fit. The symmetric partner to the existingminVisibleItemsfloor; extra items collapse into the overflow indicator. Defaults toundefined(no cap).maxRows?: number— bounded multi-row wrapping. Items wrap onto up to N rows, then the remainder collapses into the overflow indicator. A number, not a boolean (unbounded wrapping is a plain flex-wrap layout, not overflow collapse). Defaults toundefined(single line);maxRows={1}is equivalent to today.Closes #4176.
Why
Capped lists ("show at most N, then +count") and bounded multi-row tag clouds ("wrap onto two rows, then +count") are the two most common collapsing-row patterns the current single-line, floor-only API can't express. A numeric cap matches the industry-standard shape (
max,maxCount,maxTagCount,AvatarGroup max);maxVisibleItemsmirrors the existingminVisibleItemsvocabulary so the pair is discoverable.Both props are optional and
undefinedby default — no existing call site changes behavior.Behavior
visibleCount = clamp(fitCount, minVisibleItems, maxVisibleItems ?? itemCount).max < minprecedence: the floor wins (it's the stronger guarantee), with a dev-onlyconsole.warn.maxRows, reserving indicator space on the final row. This reuses the existing "measure widths once, compute in JS" model — one measure pass, no extra reflow. It assumes uniform row height (true for typical tag/pill/avatar rows); the visible container's max-height is derived from the measured max item height.collapseFrom× multi-row:end→ the indicator ends the last visible row;start→ the indicator begins the first row (packing runs in the same reversed order the single-line path already uses).maxRowsis undefined; the visible container only switches toflexWrap: wrapwith a bounded max-height whenmaxRowsis set.Independent unit tests for the overflow math
Given the history of overflow-calculation bugs, the core math is extracted into a pure, DOM-free helper —
packages/core/src/hooks/computeOverflow.ts— that takes plain inputs (item widths, gap, available width, indicator width, min/max, maxRows, collapseFrom) and returns{visibleCount, rows}.useOverflowcalls it after measuring the DOM.computeOverflow.test.tsexercises it in isolation (34 cases): cap-only, floor-only, cap+floor,max<min(min wins + warn), exact-fit and off-by-one boundaries, indicator-space reservation, multi-row packing (1 row, N rows, overflow into indicator), cap-vs-rows precedence both directions,collapseFromstart vs end, resize shrink/grow re-clamping never exceeding the cap, and degenerate values (0, cap>itemCount, maxRows taller than content, empty list). All assert exact numbers.Docs & stories
maxRows={2}) and Capped Toolbar (maxVisibleItems={3}) — underpackages/cli/templates/blocks/components/OverflowList/, following the existingexampleForblock convention.CappedItems,MultiRow, andCappedMultiRowstories..doc.mjs: new prop entries (en/zh/dense) and the "vertical layouts" best-practice reworded to distinguish supported multi-row horizontal wrap from unsupported vertical stacks.Testing
computeOverflow.test.ts— 34 pure-math cases, all pass.useOverflow.test.ts— existing single-line parity preserved; new cap/multi-row hook cases (26 total).OverflowList.test.tsx— existing behavior preserved; new cap and multi-row component cases (19 total).data-extractionregistry tests pass with the new example blocks.eslintclean on changed files;@astryxdesign/corebuilds cleanly (StyleX CSS generated for the dynamic multi-row max-height).