Content overlaps itself at a page break instead of moving to the next page
Describe the bug
When a block lands near the bottom of a page with just barely not enough room to fit, react-pdf force-fits it into the little space left instead of moving it to the next page. Its lines are then drawn on top of each other ("overlapping / scrunched at the page break"), and the rest of the page below is wasted.
It shows up most with two-column rows and wrap={false} blocks near a page boundary (headers, footers, itinerary-style entries, table rows).
The pagination logic lives in @react-pdf/layout, which @react-pdf/renderer depends on. The renderer version can look unrelated (e.g. renderer 4.5.1 → layout 4.6.1); the bug is in layout 4.6.1 and is present on the latest published version.
How to reproduce
Paste into the react-pdf REPL and look at the bottom of page 1. (A plain long <Text> is not enough to trigger it — it paginates cleanly. The trigger is a wrap={false} block that is the sole child of its container and lands at the bottom of an already-populated page.)
const Block = ({ label, value }) => (
<View style={{ marginTop: 5 }} wrap={false}>
<Text style={{ fontSize: 8, fontWeight: 700 }}>{label}</Text>
<Text style={{ fontSize: 8 }}>{value}</Text>
</View>
);
const Entry = ({ time, title, a, w, p }) => (
<View>
<View style={{ flexDirection: "row", borderBottomWidth: 1, paddingVertical: 8 }} wrap={false}>
<View style={{ width: 130, marginLeft: 20 }}>
<Text style={{ fontSize: 11, marginBottom: 5 }}>{time}</Text>
<Text style={{ fontSize: 7 }}>PENDING</Text>
</View>
<View style={{ flex: 1, marginRight: 20 }}>
<Text style={{ fontSize: 10 }}>{title}</Text>
<Text style={{ fontSize: 8, marginTop: 5 }}>2 Adults</Text>
<Block label="Address" value={a} />
<Block label="Website" value={w} />
<Block label="Phone" value={p} />
</View>
</View>
</View>
);
const Doc = () => (
<Document>
<Page size="A4" style={{ paddingTop: 40, paddingBottom: 90, paddingHorizontal: 40, flexDirection: "column" }}>
<View fixed>
<Text style={{ fontSize: 22 }}>Daily Itinerary</Text>
</View>
<View style={{ flex: 1 }}>
{/* spacer: pushes the 2nd entry to the bottom of page 1 */}
<View style={{ height: 500 }} />
<View>
<View minPresenceAhead={100}>
<View style={{ marginTop: 10, marginBottom: 8 }}>
<Text style={{ fontSize: 9, fontWeight: 700 }}>TUESDAY, APR 7</Text>
</View>
<Entry time="12:25 AM" title="Juvia"
a="1111 Lincoln Road, Miami Beach, Florida, 33139"
w="http://www.juviamiami.com/" p="+1 305-763-8272" />
</View>
<View>
<Entry time="01:05 AM" title="Miami Beach Golf Club"
a="2301 Alton Road, Miami Beach, Florida, 33140"
w="http://www.miamibeachgolfclub.com/" p="+1 305-532-3350" />
</View>
</View>
</View>
<View style={{ position: "absolute", bottom: 0, left: 40, right: 40, height: 50 }} fixed>
<Text style={{ fontSize: 8 }}>EXCLUSIVE RESORTS</Text>
</View>
</Page>
</Document>
);
ReactPDF.render(<Doc />);
The overlap only happens when the entry lands with just barely not enough room, and the exact height that does so depends on font metrics (they differ slightly between the browser REPL and a server render). The verified window is about height: 490–510; if 500 renders cleanly for you, try 490 or 510.
Expected behavior
The entry should move to the next page and render normally — never overlap its own lines / the footer.
Screenshots
Before (published 4.6.1) — page 1: the "01:05 AM / Miami Beach Golf Club" entry is squashed onto the bottom of page 1; Address sits on top of the street, Website on the URL, Phone on the number.
After (fix in #3450) — page 1 / page 2: page 1 ends cleanly after "Juvia"; the golf entry moves to page 2 and renders normally.
Root cause
In resolvePagination's splitNodes, when a node is split and all of its children move to the next page (leaving an empty shell), the parent is kept on the current page whenever currentChildren.length === 0. That check is meant to mean "the page is empty," but currentChildren is the local list for the node's own container — it is 0 simply because the node is the first child of its container, not because the page is empty. So a node that is the sole child of its container but lands on an already-populated page is force-fit into the space left, and Yoga compresses its lines on top of each other.
Environment
- @react-pdf/renderer: ^4.5.1 (uses @react-pdf/layout 4.6.1)
- Node.js server-side rendering (also reproduces in the in-browser REPL)
Proposed fix
#3450 — decide based on whether the page is actually empty above the node (threaded through the split recursion), so a node with content above it moves to the next page; it is only kept (and allowed to overflow) when the page is genuinely empty, which also avoids an infinite page loop. Includes regression tests.
Content overlaps itself at a page break instead of moving to the next page
Describe the bug
When a block lands near the bottom of a page with just barely not enough room to fit, react-pdf force-fits it into the little space left instead of moving it to the next page. Its lines are then drawn on top of each other ("overlapping / scrunched at the page break"), and the rest of the page below is wasted.
It shows up most with two-column rows and
wrap={false}blocks near a page boundary (headers, footers, itinerary-style entries, table rows).How to reproduce
Paste into the react-pdf REPL and look at the bottom of page 1. (A plain long
<Text>is not enough to trigger it — it paginates cleanly. The trigger is awrap={false}block that is the sole child of its container and lands at the bottom of an already-populated page.)The overlap only happens when the entry lands with just barely not enough room, and the exact height that does so depends on font metrics (they differ slightly between the browser REPL and a server render). The verified window is about height: 490–510; if 500 renders cleanly for you, try 490 or 510.
Expected behavior
The entry should move to the next page and render normally — never overlap its own lines / the footer.
Screenshots
Before (published 4.6.1) — page 1: the "01:05 AM / Miami Beach Golf Club" entry is squashed onto the bottom of page 1; Address sits on top of the street, Website on the URL, Phone on the number.
After (fix in #3450) — page 1 / page 2: page 1 ends cleanly after "Juvia"; the golf entry moves to page 2 and renders normally.
Root cause
In resolvePagination's splitNodes, when a node is split and all of its children move to the next page (leaving an empty shell), the parent is kept on the current page whenever currentChildren.length === 0. That check is meant to mean "the page is empty," but currentChildren is the local list for the node's own container — it is 0 simply because the node is the first child of its container, not because the page is empty. So a node that is the sole child of its container but lands on an already-populated page is force-fit into the space left, and Yoga compresses its lines on top of each other.
Environment
Proposed fix
#3450 — decide based on whether the page is actually empty above the node (threaded through the split recursion), so a node with content above it moves to the next page; it is only kept (and allowed to overflow) when the page is genuinely empty, which also avoids an infinite page loop. Includes regression tests.