Skip to content

fix: finish the CollapsibleText children-mode review fixes (#4170) - #4272

Merged
atomantic merged 3 commits into
mainfrom
claim/issue-4170-review-fixes
Aug 15, 2026
Merged

fix: finish the CollapsibleText children-mode review fixes (#4170)#4272
atomantic merged 3 commits into
mainfrom
claim/issue-4170-review-fixes

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

Follow-up to #4265, which shipped the children-based max-height variant for CollapsibleText and migrated AgentCard's TaskDescription onto it. That PR was merged while its review round was still running, so two of the three review findings didn't make it in. They ship here.

(The third — the effect deps missing the mode flag, which left the observer bound to the detached element after a <p> → capped <div> swap — landed on main in 6605399.)

1. hasChildren counted an empty list as children

children != null && children !== false is true for [] and ''. A caller writing <CollapsibleText text={fallback}>{items.map(…)}</CollapsibleText> over an empty list got an empty capped box with its text fallback silently dropped — the exact opposite of what the "children wins over text" rule exists to protect against.

2. Focus inside the clipped region had no way back out

The collapsed container is overflow-hidden: a scroll container with no visible scrollbar. Children in this mode are precisely the content that holds focusable things — MarkdownOutput emits <a target="_blank">, <img>, and <pre className="overflow-x-auto">. Tabbing to a link below the cap made the browser scroll the capped box to reveal it, and from there the top of the description was gone with no scrollbar, no wheel target, and no code path resetting scrollTop. Meanwhile the toggle asserted aria-expanded="false" over content that was fully present in the accessibility tree and the tab order.

Focus inside the capped region now expands it, which puts the target on screen and makes the aria-expanded claim honest. It's gated on real overflow: content that fits is never clipped, so expanding it would only mint a no-op "Show less" into the tab order.

Also

Adds the children-mode regression test for the deps fix — 6605399 shipped that fix without one, so nothing currently guards it.

The text / line-clamp path is untouched; TaskItem.jsx (4 call sites) and Review.jsx (2) pass no children and are unaffected.

Test plan

  • client/src/components/ui/CollapsibleText.test.jsx — 3 new cases: children arriving after mount get measured (guards 6605399), an empty child list falls back to text and its line clamp, focus inside a clipped preview expands it, and focus inside a fitting preview does not.
  • cd client && npx vitest run src/components/ui/CollapsibleText.test.jsx src/components/cos/tabs/AgentCard.test.jsx — 34 passed.
  • Each new assertion was verified to fail against the pre-fix component before the fix was applied.
  • cd client && npm test — 647 files, 7892 tests, all green on the branch this was cut from.
  • cd client && npm run lint — clean (biome, --error-on-warnings).

Refs #4170, #4265

Two findings from the review round on #4265 landed after that PR was
merged, so they ship separately here. (The third — the effect deps
missing the mode flag — went in with 6605399.)

- `hasChildren` counted `[]` and `''` as children, so a caller writing
  `<CollapsibleText text={fallback}>{items.map(…)}</CollapsibleText>`
  over an empty list got an empty capped box with its `text` fallback
  silently dropped — the opposite of what "children wins over text" is
  meant to protect against.
- The collapsed container is a scroll container with no visible
  scrollbar, and children may hold links, buttons, or a horizontally
  scrollable <pre>. Tabbing to one below the cap scrolled the preview to
  reveal it with no scrollbar, no wheel target, and no code path to
  scroll back — while `aria-expanded="false"` claimed content that was
  fully in the tab order was hidden. Focus inside now expands, gated on
  real overflow so content that fits doesn't mint a no-op "Show less".

Also adds the children-mode regression test for the deps fix, which
6605399 shipped without one.
@atomantic

Copy link
Copy Markdown
Owner Author

Review loop closed (codex, 3 rounds).

Fixed in this PR:

  • [null] / [false, undefined, ' '] counted as children by array length, so items.map(i => i.show ? <Row/> : null) over an all-hidden list rendered an empty capped box and silently dropped the text fallback. Now counted via Children.toArray + a whitespace-string filter (587c0ae).
  • onFocus gated on the isOverflowing state flag, which a passive effect populates after commit — a descendant with autoFocus takes focus during commit, reads a stale false, and stays stranded inside the clipped scroll region. Now measured live off the ref; the duplicated scrollHeight > clientHeight + 1 predicate is extracted to one overflows() helper (c5e2bb8).

Both regressions have a test verified to fail against the pre-fix component.

Declined — empty React fragments (<></>) count as a child. Correct as reported, but a fragment is a legitimate child, and deciding whether an arbitrary subtree renders nothing means recursing fragments and evaluating components — a component returning null is undecidable without rendering it. No caller passes a bare empty fragment; the array case above was the one that actually occurs.

@atomantic
atomantic merged commit 06e7c9d into main Aug 15, 2026
7 checks passed
@atomantic
atomantic deleted the claim/issue-4170-review-fixes branch August 15, 2026 05:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant