diff --git a/.changeset/fuzzy-pans-happen.md b/.changeset/fuzzy-pans-happen.md new file mode 100644 index 000000000..bfa8a7850 --- /dev/null +++ b/.changeset/fuzzy-pans-happen.md @@ -0,0 +1,7 @@ +--- +"@react-pdf/layout": minor +"@react-pdf/renderer": minor +"@react-pdf/types": minor +--- + +feat: add `breakWhenNeeded` for wrap-able nodes that should move to the next page before their first split diff --git a/packages/layout/src/node/shouldBreak.ts b/packages/layout/src/node/shouldBreak.ts index eaffe48f4..9bbe00033 100644 --- a/packages/layout/src/node/shouldBreak.ts +++ b/packages/layout/src/node/shouldBreak.ts @@ -5,6 +5,9 @@ import isFixed from './isFixed'; const getBreak = (node: SafeNode) => 'break' in node.props ? node.props.break : false; +const getBreakWhenNeeded = (node: SafeNode) => + 'breakWhenNeeded' in node.props ? node.props.breakWhenNeeded : false; + const getMinPresenceAhead = (node: SafeNode) => 'minPresenceAhead' in node.props ? node.props.minPresenceAhead : 0; @@ -53,10 +56,16 @@ const shouldBreak = ( // (as long as react-pdf does not support breaking into differently sized containers) const breakingImprovesPresence = previousElements.filter((node: SafeNode) => !isFixed(node)).length > 0; + const shouldBreakWhenNeeded = + shouldSplit && + canWrap && + getBreakWhenNeeded(child) && + breakingImprovesPresence; return ( getBreak(child) || (shouldSplit && !canWrap) || + shouldBreakWhenNeeded || (!shouldSplit && endOfPresence > height && breakingImprovesPresence) ); }; diff --git a/packages/layout/src/types/base.ts b/packages/layout/src/types/base.ts index 200be278b..7104bf4e5 100644 --- a/packages/layout/src/types/base.ts +++ b/packages/layout/src/types/base.ts @@ -74,6 +74,12 @@ export type NodeProps = { * @see https://react-pdf.org/advanced#page-breaks */ break?: boolean; + /** + * Move the element to the next page when it would otherwise start splitting + * in the remaining space, while still allowing it to continue across later + * pages. + */ + breakWhenNeeded?: boolean; /** * Hint that no page wrapping should occur between all sibling elements following the element within n points * @see https://react-pdf.org/advanced#orphan-&-widow-protection diff --git a/packages/layout/tests/node/shouldBreak.test.ts b/packages/layout/tests/node/shouldBreak.test.ts index 665881515..877c4ee36 100644 --- a/packages/layout/tests/node/shouldBreak.test.ts +++ b/packages/layout/tests/node/shouldBreak.test.ts @@ -110,6 +110,69 @@ describe('node shouldBreak', () => { expect(result).toEqual(true); }); + test('should break when breakWhenNeeded is enabled and moving improves presence', () => { + const result = shouldBreak( + { + type: 'VIEW', + props: { wrap: true, breakWhenNeeded: true }, + style: {}, + children: [], + box: { + top: 700, + right: 0, + bottom: 0, + left: 0, + height: 400, + width: 200, + }, + }, + [], + 1000, + [ + { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { + top: 0, + right: 0, + bottom: 0, + left: 0, + height: 700, + width: 200, + }, + }, + ], + ); + + expect(result).toEqual(true); + }); + + test('should not break when breakWhenNeeded is enabled but the node is already first on the page', () => { + const result = shouldBreak( + { + type: 'VIEW', + props: { wrap: true, breakWhenNeeded: true }, + style: {}, + children: [], + box: { + top: 700, + right: 0, + bottom: 0, + left: 0, + height: 400, + width: 200, + }, + }, + [], + 1000, + [], + ); + + expect(result).toEqual(false); + }); + test('should break when minPresenceAhead is large enough and there are overflowing siblings after the child', () => { const result = shouldBreak( { diff --git a/packages/layout/tests/steps/resolvePagination.test.ts b/packages/layout/tests/steps/resolvePagination.test.ts index 2466aaf4b..830dc8456 100644 --- a/packages/layout/tests/steps/resolvePagination.test.ts +++ b/packages/layout/tests/steps/resolvePagination.test.ts @@ -238,6 +238,83 @@ describe('pagination step', () => { expect(page2.children![0].box!.height).toBe(40); }); + test('should move breakWhenNeeded containers to the next page before splitting them', async () => { + const yoga = await loadYoga(); + + const layout = calcLayout({ + type: 'DOCUMENT', + yoga, + props: {}, + children: [ + { + type: 'PAGE', + props: {}, + style: { + width: 5, + height: 60, + }, + children: [ + { + type: 'VIEW', + style: { + width: 5, + height: 20, + }, + props: {}, + children: [], + }, + { + type: 'VIEW', + style: { + width: 5, + }, + props: { + breakWhenNeeded: true, + }, + children: [ + { + type: 'VIEW', + style: { + height: 30, + }, + props: {}, + children: [], + }, + { + type: 'VIEW', + style: { + height: 30, + }, + props: {}, + children: [], + }, + { + type: 'VIEW', + style: { + height: 30, + }, + props: {}, + children: [], + }, + ], + }, + ], + }, + ], + }); + + const page1 = layout.children[0]; + const page2 = layout.children[1]; + const page3 = layout.children[2]; + + expect(layout.children).toHaveLength(3); + expect(page1.children).toHaveLength(1); + expect(page2.children).toHaveLength(1); + expect(page3.children).toHaveLength(1); + expect(page2.children![0].children).toHaveLength(2); + expect(page3.children![0].children).toHaveLength(1); + }); + test('should not infinitely loop when splitting pages', async () => { const yoga = await loadYoga(); diff --git a/packages/renderer/index.d.ts b/packages/renderer/index.d.ts index 1aa2dc804..f4acc8a87 100644 --- a/packages/renderer/index.d.ts +++ b/packages/renderer/index.d.ts @@ -86,6 +86,12 @@ declare namespace ReactPDF { * @see https://react-pdf.org/advanced#page-breaks */ break?: boolean; + /** + * Move the element to the next page when it would otherwise start + * splitting in the remaining space, while still allowing it to continue + * across later pages. + */ + breakWhenNeeded?: boolean; /** * Hint that no page wrapping should occur between all sibling elements following the element within n points * @see https://react-pdf.org/advanced#orphan-&-widow-protection diff --git a/packages/types/node.d.ts b/packages/types/node.d.ts index 96ecbf866..6b09e0f9a 100644 --- a/packages/types/node.d.ts +++ b/packages/types/node.d.ts @@ -8,6 +8,7 @@ interface BaseProps { id?: string; fixed?: boolean; break?: boolean; + breakWhenNeeded?: boolean; debug?: boolean; bookmark?: Bookmark; minPresenceAhead?: number;