Skip to content

Review 5567

Cindy Zhang edited this page Aug 26, 2026 · 3 revisions

#5567 — feat(Collapsible): animate the content reveal, and rewrite the block examples

ernestt · open · view on GitHub

Verdict: request changes · re-review of 0720144f9b57eccb2ebeca10f6ebe950b7b715f9

PR

#5567 feat(Collapsible): animate the content reveal, and rewrite the block examples by ernestt (bucket: internal — .github/DESIGNOWNERS, write permission)

HEAD REVIEWED

0720144f9b57eccb2ebeca10f6ebe950b7b715f9

VERSIONS

LOOP VERSION: 1.5.0 AUDIT RUBRIC: 1.12

LANE

LANE: full WHY: three runtime decisions, intended visual and default-behavior changes, two new Effects, imperative listeners/timing, and a released theme target changing element.

PROBLEM

WHY 1: A Collapsible panel snaps open and closed, a closed FAQ answer is hard to discover with browser find, and an undensified trigger can be only 17px tall. WHY 2: The abrupt change gives weak spatial orientation, search can miss the answer a reader wants, and a small target is harder to operate. WHY 3: A disclosure exists to hide optional detail without making it hard to find or control. USER-FACING PROBLEM: Someone scanning or searching a disclosure-heavy page should be able to find, reveal, and operate the right section without a jarring jump or tiny target. PROBLEM SEVERITY: harmful friction — motion alone is polish; findability and target size affect discovery and operation.

VERDICT: clear

SOLUTION

The panel stays mounted on a clipped height track and changes height and opacity when its open state changes. The component separately coordinates browser hiding, focus exclusion, animation frames, transition completion, and browser-find events so a closed panel can supposedly remain searchable. The trigger gains a minimum pointer size, and the examples teach row-versus-heading typography.

SOLUTION (3 runtime decisions · ~142 added non-comment lines of 824 additions)

  1. animate disclosure height and opacity around the existing open state — stated motion problem
  2. replace display:none with an inert / hidden="until-found" lifecycle and beforematch reopening — findability is described as a benefit, not established as a starting problem
  3. raise every trigger to 24px on fine pointers and 44px on coarse pointers — not stated in the opening problem

The eight block rewrites are one holistic example-design pass by a design owner, not eight independent runtime decisions.

BURDEN: high — extra wrapper, one state value, two Effects, two animation frames, a per-instance listener, a retained transition listener, a 1.5s timer, shared reset change, and a moved released theme target. BURDEN MATCH: disproportionate — most machinery serves polish, and the findability half does not work while the same subtree is inert.

VERDICT: BLOCKS — the high-burden timing protocol misses its findability outcome and changes a released theming seam.

ARCHITECTURE

OWNER: Collapsible owns disclosure state, accessibility, and motion; CollapsibleGroup owns coordination and density. TIER 1: native hidden="until-found" / beforematch, inert, CSS transitions, and the existing useCollapsible state owner. TIER 2: target size via the shared spacing scale; no icon/status subsystem change. SEAMS: standalone and grouped state, controlled and uncontrolled state, arbitrary focusable children, Card/Section composition, and the released collapsible-content theme target. BEHAVIOR UNIT: inline — two Effects coordinate a ref, React state, rAF, a timer, transition events, and native attributes; the real browser path has no focused test.

seam driven result
browser find on a rested closed panel fails by platform contract: inert subtrees are excluded from find-in-page, so beforematch cannot rescue this state
released collapsible-content target with padding-block:16px main: closed panels 0px; head: each closed panel leaves a 32px box
focusable last child in an open panel main overflow visible; head overflow hidden and clips the 5px-outset focus ring
standalone/grouped rapid toggles settles to matching aria-expanded, inert, hidden, and 0px height

The timing protocol crosses the mechanical complex-behavior threshold and remains embedded in the component with only jsdom tests. The shipped CSS-grid disclosure precedent keeps motion declarative; this change has a second job—browser find—but that does not make the combined protocol testable inline.

VERDICT: BLOCKS — complex behavior has no named testable unit, and three documented seams fail.

IMPACT

Every app taking the next release gets animated disclosure and taller triggers without opting in. A reader using browser find still misses text inside every closed panel; a keyboard user can lose the bottom of a focused child’s outline; and a theme author’s existing content-padding override now leaves visible gaps where rows are closed.

VERDICT: BLOCKS — the change introduces reachable search, focus, and theme regressions.

API

No prop, export, accepted value, or default changes.

OSSIFICATION: no new public TypeScript surface. Four new --_collapsible-* variables are private and undocumented, although the description and changeset call them theme-tunable; supported theme compilation rejects direct private-var configuration.

VERDICT: note — no public API change, but the advertised tuning path is not part of the supported theme API.

THEMING

The released astryx-collapsible-content target existed in v0.5.0 on the padded painting element. This head keeps the name but moves it to the zero-height overflow:hidden animation track, while the new padded inner element has no target. A normal unlayered override (the same cascade level consumers may use) and the sensored !important attribution arm both produce the same result: 0px closed panels on main become 32px on head.

The duration/easing defaults use tokens, but the four new private variables are absent from Collapsible.doc.mjs and cannot be configured directly through defineTheme.

VERDICT: BLOCKS — a released target now points at layout machinery and changes existing override behavior.

BREAKING

BEHAVIOR: yes — open/close becomes asynchronous, browser-find handling is added, and closed descendants become inert. API: no — signatures, values, and defaults are unchanged. VISUAL: yes, intentional — reveal motion and trigger height change; the 640px Single Mode frame measures triggers at 17px on main and 24px on head. THEME: yes, unintended — the released content target moved from the padded content box to the clipping track; padding overrides leave 32px closed gaps.

VERDICT: BLOCKS — existing theme overrides can change meaning after upgrade.

PERFORMANCE & RESOURCES

EFFECTS: two added.

Effect + deps external system why render/handler cannot do it measured render cost lifetime + cleanup focused test
useLayoutEffect [canInterpolateSize,isOpen] Collapsible.tsx:452 DOM attributes, CSS transition completion controlled changes and native reveal are not always initiated by this component’s click handler N=3 group toggle: 1→2 React commits; Layout 1→30; UpdateLayoutTree 8→33; Paint 8→43 rAF and 1.5s timer clean up; transitionend remains attached while the item stays closed none
useEffect [isOpen,isDisabled,toggle] Collapsible.tsx:520 native beforematch event no React event prop/type exists for this browser event no state update until event; listeners scale exactly 2/4/6 at N=2/4/6 one listener per mounted Collapsible; cleanup present jsdom dispatch only; no native-find test

RENDER: delayed visual state adds one commit per N=3 group toggle in Chromium (1→2). LISTENERS/OBSERVERS: one permanent beforematch listener per instance; one transitionend listener remains on each item after it closes until that Effect reruns or unmounts; no observers or global listeners. LAYOUT: the intended disclosure animation adds 29 Layout, 25 UpdateLayoutTree, and 35 Paint events for the measured N=3 toggle versus main. BUNDLE: no dependency; exact-head CI reports 1.2KB gzipped but no before delta.

The layout work is the feature rather than an independent regression. The avoidable issue is an extra React commit and permanent/per-state listeners inside a behavior unit with no focused browser test.

VERDICT: note — the measured cost is the intended animation; the focused-test failure is carried by ARCHITECTURE.

VISUAL EVIDENCE

VISUAL CHECK: manual frames required WHY: pixels and default geometry intentionally change, and the two blocking visual claims are absent from the exact-head visual job’s stories/states.

Released theme target changes collapsed geometry

Sensor Expected Observed Pass?
Build main f4aab4c2532b3cbf2c80ca7f46eb3a36c3b82663; head 0720144f9b57eccb2ebeca10f6ebe950b7b715f9 exact SHAs from both worktrees at capture yes
Story core-collapsible--single-mode same in both yes
Theme default global; rendered neutral {}; neutral yes
Color mode default; computed light light yes
Direction default; computed ltr ltr yes
Viewport/media 640×520 @1; forced colors/reduced motion/coarse false, hover true same in both yes
Rendered state 3 triggers; [open, closed, closed]; padding-block:16px arm exact match yes
Subject geometry 3 targets; first visible/non-zero/in viewport main 536×72; head 536×76 yes
Settled render fonts loaded; 0 animations; no page/Storybook errors exact match yes
Image PNG, 640×520, non-blank hash 18,316 bytes / 5c235df45da23ebfec82b458d66df5c063d1af71ff4038237d4a870326e7dd64; 18,335 bytes / ec2dce7007228345b29e4db8907a8643ab2175c4911955454c88364027938cca yes

ARM: content-target padding BASE: each exact build above DELTA: one injected declaration, .astryx-collapsible-content { padding-block:16px !important; }; a second run without !important measured the same 0→32px result UNRELATED DELTA: none

Before After
Main: closed rows remain 0px Head: closed rows become 32px

Difference: intentional target override; unintentional target behavior change. Main closed heights are 0,0; head is 32,32.

Open track clips a focused last child

Sensor Expected Observed Pass?
Build same main/head SHAs exact SHAs from both worktrees at capture yes
Story core-collapsible--single-mode same in both yes
Theme / mode / direction neutral / light / ltr same in both yes
Viewport/media 640×520 @1; default media same in both yes
Rendered state first panel open; final child focused; 2px outline + 3px offset exact match yes
Subject geometry 3 targets; first visible/non-zero/in viewport main 536×44; head 536×64 yes
Settled render fonts loaded; 0 animations; no page/Storybook errors exact match yes
Image PNG, 640×520, non-blank hash 19,688 bytes / c3ea0d8e7fa5e681b3a7d8447797ce3c573da64c484a8adfc1fa5ec4403c0826; 20,221 bytes / 20bb775c48d4539fc60b6081822174f5058317ea72308626d84e21e687fd744c yes

ARM: focused final child BASE: each exact build above DELTA: append one final focusable anchor to the open content and apply the system’s 2px outline + 3px offset UNRELATED DELTA: none

Before After
Main: overflow visible Head: overflow hidden clips the outline

Difference: unintentional. The ring extends 5px below the content box in both arms; main paints it with overflow:visible, while head clips it with overflow:hidden.

Containment assumptions: the target is the full-width content box; the child ends at its block-end edge; the parent Card is unconstrained. Narrow/wide, RTL, zoom, forced-colors, and icon-only do not alter either causal boundary, so they were omitted after the decisive pairs. Exact-head pr-visual is green and reports 2/20 changed shots (Single Mode neutral light/dark), but it covers neither an existing target override nor a focused child.

VERDICT: BLOCKS — the target move creates collapsed gaps, and the permanent clip cuts descendant focus paint.

A11Y & I18N

Auto-covered: exact-head pr-a11y and pr-rtl are green; .github/a11y-baseline.json is unchanged. Browser-checked: trigger height is 24px on fine pointers and 44px on coarse; rapid toggles settle; reduced motion resolves to 1ms; the open panel clips a focused child’s 5px-outset outline.

The remaining prior-review failure is structural: inert={!isOpen} at Collapsible.tsx:592 keeps every rested closed subtree out of browser find. MDN says inert content “are not searchable via browser find-in-page features”; the HTML standard says the user agent should ignore inert nodes for find-in-page. The unit test at Collapsible.test.tsx:183 dispatches beforematch directly, so it cannot catch that the browser will not produce the event in this state.

No new translatable runtime string or physical-direction property is introduced. Template copy is demo content and exact-head pr-rtl is green.

VERDICT: BLOCKS — closed content is still not findable, and open descendant focus paint is clipped.

JUDGEMENT

slot verdict
PROBLEM clear
SOLUTION BLOCKS — burden does not deliver findability and changes a released seam
ARCHITECTURE BLOCKS — complex inline timing state machine; failed seams
IMPACT BLOCKS — search, focus, and theme regressions
API note — advertised private tuning is unsupported
THEMING BLOCKS — released target moved onto layout machinery
BREAKING BLOCKS — existing theme overrides change meaning
PERFORMANCE note — intended animation cost measured; no leak found
VISUAL BLOCKS — collapsed gaps and clipped focus paint
A11Y & I18N BLOCKS — inert defeats find and overflow clips focus

PRIOR REVIEW: cixzhang asked for the hidden="until-found" reset conflict and an actual browser-path regression. Commit 0720144 fixes the reset selector; the test still dispatches beforematch directly, and inert keeps the native search path blocked. This review extends that unresolved ask and adds two shape-level regressions; it does not silently reverse the prior conclusion. GOAL: partly met — the reset now leaves hidden="until-found" at display:block, motion runs, and targets grow; inert still excludes the closed subtree from find-in-page. DISPOSITION: inert/find conflict → blocks now; released target relocation → blocks now; complex inline timing protocol without focused browser coverage → blocks now; focused-child clipping → blocks now; stale description/private-var claim → omit from public review until the shape is corrected. ADVICE: bounded direction — preserve three outcomes: native find opens any closed panel, the released target still styles the padded content without affecting collapsed geometry, and a focused descendant’s full ring remains visible; put the timing protocol behind a named unit with a browser test covering open, close, interruption, reduced motion, and native find. Reproduction: pr-5567-r2-probe.cjs and pr-5567-r2-visual.cjs, with results and receipts beside them. AUTHOR CAN PROCEED: yes — the required outcomes and reproductions are explicit; the implementation choice remains theirs. WORST OUTCOME: “A reader using browser find still misses text inside every closed panel.” → request changes.

JUDGEMENT NEEDED: none — these are regressions against existing browser, accessibility, and theming contracts.

request changes

  1. The collapsed content remains inert. → browser find ignores every closed answer, so beforematch never gets a chance to open it · packages/core/src/Collapsible/Collapsible.tsx:592

  2. The released content target moved onto the zero-height clip track. → a theme’s existing 16px content padding leaves each closed row 32px tall · packages/core/src/Collapsible/Collapsible.tsx:594

  3. The animation track keeps clipping while open. → a keyboard user focusing the last child loses the bottom 5px of its outline · packages/core/src/Collapsible/Collapsible.tsx:164

REVIEW

Thanks — the reset exemption fixes the selector from my last review, but the browser-find path is still blocked. The closed panel remains inert, and inert subtrees are excluded from find-in-page, so the browser never reaches beforematch; the test still dispatches that event directly.

The released collapsible-content target also moved from the padded content to the zero-height clipping track. An existing 16px padding override now leaves every closed row 32px tall, and the permanent clip cuts the bottom 5px from a focused last child. This timing now spans two Effects, rAF, a timer, events, and native attributes without a focused browser test.

Could we preserve those content/focus contracts and move the timing protocol into a focused unit with real find-in-page coverage?

[Reviewed by Robohands]

INLINE (0–3, only when genuinely line-anchored)

  • packages/core/src/Collapsible/Collapsible.tsx:592inert prevents browser find from reaching closed content, so beforematch cannot run.
  • packages/core/src/Collapsible/Collapsible.tsx:594 — This moved the released target onto layout machinery; padding now keeps closed rows 32px tall.
  • packages/core/src/Collapsible/Collapsible.tsx:164 — This stays clipped while open and cuts a focused child’s bottom outline.

EVIDENCE I DID NOT SPEND

  • The description still names visibility:hidden and a staggered fade, while this head uses inert / hidden="until-found" and a parallel ramp.
  • The four new private motion variables are absent from Collapsible.doc.mjs, despite the changeset calling them theme-tunable.
  • The prior review accepted the target-size and example updates; the response commit changes only reset.css.

TIME

TIME total 28m setup 5m dedicated worktree + clone/reconcile install + one build + Storybook dev on port 6317 (warm main reused: yes, port 6100) reading 9m full brief/critic/harness, prior review/gates, body, complete diff, source, tests, rubric, and conventions measuring 8m 72 targeted tests + 13 browser arms/traces + 4 sensor-receipted frames; 2 re-measures to isolate theme/focus behavior writing 6m three presentations + three critic gates waste 3m missing $KIT lookup, one malformed CLI invocation, a mis-scoped docsite test, and one baseline recapture invalidated when the shared main checkout moved

WHAT I COULD NOT VERIFY

  • Browser chrome’s find UI cannot be driven reliably through Playwright on this Mac; the finding uses the HTML/MDN inert contract plus the exact-head DOM state. The requested native-path regression test is still absent.
  • Safari and Firefox cannot launch under this Mac’s managed-device policy, so their fallback path was not driven.

Clone this wiki locally