diff --git a/packages/pdf/src/templates/shared/list-pagination.test.tsx b/packages/pdf/src/templates/shared/list-pagination.test.tsx
index c37f92f57..2359fcbdc 100644
--- a/packages/pdf/src/templates/shared/list-pagination.test.tsx
+++ b/packages/pdf/src/templates/shared/list-pagination.test.tsx
@@ -42,16 +42,23 @@ async function listPages(
try {
const doc = await task.promise;
const pages: string[][] = [];
+ const positions: { str: string; x: number; y: number }[][] = [];
for (let n = 1; n <= doc.numPages; n++) {
const page = await doc.getPage(n);
const text = await page.getTextContent();
pages.push(text.items.flatMap((item) => ("str" in item && item.str ? [item.str] : [])));
+ positions.push(
+ text.items.flatMap((item) =>
+ "str" in item && item.str ? [{ str: item.str, x: item.transform[4], y: item.transform[5] }] : [],
+ ),
+ );
}
return {
marker: pages.findIndex((p) => p.some((s) => s.includes("•") || s.startsWith("1."))),
first: pages.findIndex((p) => p.some((s) => s.includes("TARGET"))),
last: pages.findIndex((p) => p.some((s) => s.includes("END"))),
pages,
+ positions,
};
} finally {
await task.destroy();
@@ -256,4 +263,40 @@ describe("list marker pagination (#3344)", () => {
.match(/\bSome\b/g),
).toHaveLength(30);
});
+ it.each(["ul", "ol"])("keeps %s continuation lines at the content indentation", async (tag) => {
+ const result = await listPages(0, 60, "", {
+ html: `<${tag}>
TARGET ${"Some words to fill several lines and force wrapping. ".repeat(60)} END
${tag}>`,
+ });
+ expect(result.marker).toBe(result.first);
+ expect(result.last).toBeGreaterThan(result.first);
+ // The widest x on the item's first page is its content column; the marker
+ // and the section heading sit further left. Continuation pages must keep it.
+ const contentX = Math.max(...(result.positions[result.first] ?? []).map((item) => item.x));
+ for (const page of result.positions.slice(result.first + 1)) {
+ for (const item of page) expect(item.x).toBeCloseTo(contentX, 0);
+ }
+ });
+ it("keeps a nested list's inner indentation on continuation pages", async () => {
+ const result = await listPages(0, 1, "", {
+ html: ``,
+ });
+ expect(result.last).toBeGreaterThan(result.first);
+ const contentX = Math.max(...(result.positions[result.first] ?? []).map((item) => item.x));
+ for (const page of result.positions.slice(result.first + 1)) {
+ for (const item of page) expect(item.x).toBeCloseTo(contentX, 0);
+ }
+ });
+ it("preserves a widened ordered-marker column on continuation pages", async () => {
+ const result = await listPages(0, 40, "list-marker { font-size: 24pt; }", {
+ html: `TARGET ${"Some words to fill several lines and force wrapping. ".repeat(40)} END
`,
+ });
+ expect(result.marker).toBe(result.first);
+ expect(result.last).toBeGreaterThan(result.first);
+ const contentX = Math.max(...(result.positions[result.first] ?? []).map((item) => item.x));
+ // The 24pt marker pushes the content column well past the default gutter.
+ expect(contentX).toBeGreaterThan(30);
+ for (const page of result.positions.slice(result.first + 1)) {
+ for (const item of page) expect(item.x).toBeCloseTo(contentX, 0);
+ }
+ });
});
diff --git a/patches/@react-pdf__layout@5.2.0.patch b/patches/@react-pdf__layout@5.2.0.patch
index b752e32a9..092dd3dae 100644
--- a/patches/@react-pdf__layout@5.2.0.patch
+++ b/patches/@react-pdf__layout@5.2.0.patch
@@ -1,5 +1,5 @@
diff --git a/lib/index.js b/lib/index.js
-index 18c2618f4630b3c1296c8c3cac317b8d7666bbe8..8e6117947e87e40dd39f1bf33a1435194cc3f334 100644
+index 18c2618f4630b3c1296c8c3cac317b8d7666bbe8..716ca874d496d40e0709236d4d7fe828a77088a7 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -1887,7 +1887,7 @@ const isTextInstance$2 = (node) => node.type === P.TextInstance;
@@ -49,7 +49,7 @@ index 18c2618f4630b3c1296c8c3cac317b8d7666bbe8..8e6117947e87e40dd39f1bf33a143519
// But if the current page is empty then we can just include the parent on the current page
if (currentChildren.length === 0) {
currentChildren.push(child, ...futureFixedNodes);
-@@ -3196,7 +3202,52 @@ const splitChildren = (height, contentArea, node) => {
+@@ -3196,7 +3202,74 @@ const splitChildren = (height, contentArea, node) => {
};
const splitView = (node, height, contentArea) => {
const [currentNode, nextNode] = splitNode(node, height);
@@ -99,6 +99,28 @@ index 18c2618f4630b3c1296c8c3cac317b8d7666bbe8..8e6117947e87e40dd39f1bf33a143519
+ if (markerIndex < contentIndex) nextChildren.unshift(nextMarker);
+ else nextChildren.push(nextMarker);
+ }
++ if (markerIndex !== -1 && !hasMarker(nextChildren) && hasText(content(nextChildren))) {
++ // The marker was fully consumed on an earlier fragment while the
++ // content continues. Page relayout runs Yoga on the remaining
++ // children only, so a missing marker collapses the flex gutter and
++ // continuation lines shift to the row's left edge. Re-seat an
++ // empty marker column pinned to the width Yoga already measured,
++ // preserving the authored indentation without drawing a marker.
++ const marker = node.children[markerIndex];
++ const ghostMarker = {
++ ...marker,
++ children: [],
++ lines: [],
++ box: { ...marker.box, top: 0, height: 0 },
++ props: { ...marker.props, break: false, minPresenceAhead: 0 },
++ style: {
++ ...marker.style,
++ ...(typeof marker.box?.width === 'number' ? { width: marker.box.width } : {}),
++ },
++ };
++ if (markerIndex < contentIndex) nextChildren.unshift(ghostMarker);
++ else nextChildren.push(ghostMarker);
++ }
+ }
return [
assingChildren(currentChilds, currentNode),
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8a3041080..d68e1ba96 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -113,7 +113,7 @@ overrides:
uuid@<11.1.1: ^11.1.1
patchedDependencies:
- '@react-pdf/layout@5.2.0': 0b6c66a14ff45a9b594fcd6c207fcb72dc9608384451803359d36bc8ec5c1245
+ '@react-pdf/layout@5.2.0': 0409efd0fcb399ac0b7e962b4a7f0ab763c82317001a6637b71216cf2fc480b0
'@react-pdf/textkit': e1e963cedc6a2f322b4309c782bba317d2b49bad73da787321151a89491e5d62
fontkit@2.0.4: 90f4c51c676a88b91dcc397d60a677c77b5e6dc8e15ffb3538310965ef5c05a4
react-pdf-html@2.1.5: e8456fc885241f35e3b642ad6122015b88f855d717663793f5b3293c19afac9b
@@ -11360,7 +11360,7 @@ snapshots:
jay-peg: 1.1.1
png-js: 2.0.0
- '@react-pdf/layout@5.2.0(patch_hash=0b6c66a14ff45a9b594fcd6c207fcb72dc9608384451803359d36bc8ec5c1245)':
+ '@react-pdf/layout@5.2.0(patch_hash=0409efd0fcb399ac0b7e962b4a7f0ab763c82317001a6637b71216cf2fc480b0)':
dependencies:
'@react-pdf/fns': 3.1.3
'@react-pdf/image': 3.1.2
@@ -11401,7 +11401,7 @@ snapshots:
'@babel/runtime': 7.29.7
'@react-pdf/fns': 3.1.3
'@react-pdf/font': 4.1.2
- '@react-pdf/layout': 5.2.0(patch_hash=0b6c66a14ff45a9b594fcd6c207fcb72dc9608384451803359d36bc8ec5c1245)
+ '@react-pdf/layout': 5.2.0(patch_hash=0409efd0fcb399ac0b7e962b4a7f0ab763c82317001a6637b71216cf2fc480b0)
'@react-pdf/primitives': 4.4.0
'@react-pdf/reconciler': 2.0.0(react@19.2.8)
'@react-pdf/render': 4.7.0