Skip to content

fix(styles): keep the height of wrappers whose children are all out of flow (#467) - #469

Merged
tinchox5 merged 1 commit into
zumerlab:mainfrom
dylan1951:fix/abspos-wrapper-height-collapse
Jul 24, 2026
Merged

fix(styles): keep the height of wrappers whose children are all out of flow (#467)#469
tinchox5 merged 1 commit into
zumerlab:mainfrom
dylan1951:fix/abspos-wrapper-height-collapse

Conversation

@dylan1951

Copy link
Copy Markdown

Closes #467.

Problem

stripHeightForWrappers drops height/block-size from the style snapshot of a "transparent flow wrapper", relying on hasFlowFast to prove the element has in-flow content that will re-establish the box in the clone. Inside the foreignObject the authored stylesheet is gone, so when that assumption is wrong nothing restores the height and the wrapper collapses to 0 — every following section shifts up and paints over it.

Both of hasFlowFast's signals answer a different question than the one being asked:

  • el.textContent also sees text inside absolutely positioned descendants, so a wrapper holding only an abspos overlay with a caption reads as having in-flow text.
  • scrollHeight > paddingTop + paddingBottom is unreliable because scrollHeight is floored at clientHeight. A non-scrolling block always reports at least its own used height, whatever its children are. The old comment said abspos children don't contribute — true of the content height, but the probe never drops to the padding it's compared against.

For a height:300px hero whose children are all position:absolute, textContent is non-blank and scrollHeight is 300. Both signals say "has flow content", so the height is stripped.

Fix

hasFlowFast now asks the real question — does this element have in-flow content?

  • direct non-blank text nodes (not textContent, which reaches into out-of-flow descendants)
  • an immediate <br> (unchanged)
  • an element child that is itself in flow — skipping display:none, absolute and fixed

The children walk runs only after the cheap text/<br> paths miss, and getStyle memoizes per node (cache.computedStyle), so children whose styles are needed later aren't recomputed. cs is no longer used and is dropped from the signature; the single caller is updated.

The direct-text-node loop uses the same nodeType === 3 && /\S/.test(...) idiom already present in this file at styles.js:222.

Behaviour change, scoped

The new check returns false in exactly the intended case (all children out of flow or display:none). It returns true in one case the old probe returned false: an in-flow child when scrollHeight === 0. That can't cause a regression — scrollHeight is floored at clientHeight, so scrollHeight === 0 only when the element's own used height is already 0, making the strip a no-op. Guard 2b (|usedH - scrollHeight| > TOL) covers every other path.

Tests

__tests__/module.styles.abspos-wrapper.test.js, 4 cases:

  1. abspos-only wrapper keeps its height end to end (asserted on the size rule in the exported SVG)
  2. same, asserted via the clone style key through inlineAllStyles
  3. negative control — a genuinely transparent wrapper still gets its height stripped, so the pass keeps doing its job
  4. a display:none-only wrapper is treated as having no flow content

3 of the 4 fail on main; the negative control passes both ways by design.

Note for reviewers: the wrapper height must come from a stylesheet. An inline style="height:…" is already respected by guard 1 and masks the bug.

Full suite: 693 passed / 1 skipped, against 689 / 1 on main — no regressions. npm run lint and npm run test:types clean. Verified on Chromium; I don't have the Firefox/WebKit Playwright binaries locally.

…f flow

stripHeightForWrappers dropped height/block-size from any "transparent flow
wrapper", relying on hasFlowFast to prove the element had in-flow content that
would re-establish its box in the clone. Both of hasFlowFast's signals answer a
different question than the one being asked:

  - `textContent` also sees text inside absolutely positioned descendants, so a
    wrapper holding only an abspos overlay with a caption reads as having text.
  - `scrollHeight` is floored by clientHeight, so a non-scrolling block reports
    its own used height no matter what its children are. The old comment claimed
    abspos children do not contribute; in practice the probe never drops to the
    padding it was compared against.

A hero banner sized `height:100vh` whose children are all `position:absolute`
therefore lost its height. Inside the foreignObject the authored stylesheet is
gone, nothing restores it and the wrapper collapses to 0 — every later section
shifts up by the banner's height and paints over it.

hasFlowFast now asks the real question: direct non-blank text nodes, an
immediate <br>, or an element child that is itself in flow. The children walk
runs only after the cheap text/<br> paths miss, and skips display:none.
`cs` is no longer needed, so it is dropped from the signature.

Regression test covers the abspos-only wrapper end to end (size survives into
the exported SVG) and via the clone style key, plus a display:none-only wrapper.
A negative control keeps a genuinely transparent wrapper's height stripped, so
the pass still does its job. Without the fix 3 of the 4 fail.

Closes zumerlab#467

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tinchox5
tinchox5 merged commit c1194ae into zumerlab:main Jul 24, 2026
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.

Wrappers whose children are all absolutely positioned lose their height and collapse to 0

2 participants