diff --git a/.changeset/orange-moons-chew.md b/.changeset/orange-moons-chew.md
new file mode 100644
index 000000000..9f1f49826
--- /dev/null
+++ b/.changeset/orange-moons-chew.md
@@ -0,0 +1,6 @@
+---
+'@react-pdf/layout': minor
+---
+
+- Nested page break support for views.
+- Fallback behavior for unwrappable content to wrap if it is a wrappable component (View / Text) when it doesn't fit within a page. Content will start on next page.
diff --git a/packages/examples/src/index.js b/packages/examples/src/index.js
index b7a1b97d1..1ef0df28d 100644
--- a/packages/examples/src/index.js
+++ b/packages/examples/src/index.js
@@ -27,8 +27,6 @@ const EXAMPLES = {
const Viewer = () => {
const [example, setExample] = useState('pageWrap');
- console.log(example);
-
const handleExampleChange = e => {
setExample(e.target.dataset.name);
};
diff --git a/packages/layout/src/node/getWrap.js b/packages/layout/src/node/getWrap.js
index 5cd18bb7a..c2a64ae20 100644
--- a/packages/layout/src/node/getWrap.js
+++ b/packages/layout/src/node/getWrap.js
@@ -1,7 +1,7 @@
import * as P from '@react-pdf/primitives';
import { isNil } from '@react-pdf/fns';
-const NON_WRAP_TYPES = [P.Svg, P.Note, P.Image, P.Canvas];
+export const NON_WRAP_TYPES = [P.Svg, P.Note, P.Image, P.Canvas];
const getWrap = node => {
if (NON_WRAP_TYPES.includes(node.type)) return false;
diff --git a/packages/layout/src/steps/resolveDimensions.js b/packages/layout/src/steps/resolveDimensions.js
index 7d2102a4b..df9beb637 100644
--- a/packages/layout/src/steps/resolveDimensions.js
+++ b/packages/layout/src/steps/resolveDimensions.js
@@ -266,13 +266,15 @@ const freeYogaNodes = node => {
export const resolvePageDimensions = (page, fontStore) => {
if (isNil(page)) return null;
- return compose(
+ const result = compose(
destroyYogaNodes,
freeYogaNodes,
persistDimensions,
calculateLayout,
createYogaNodes(page, fontStore),
)(page);
+
+ return result;
};
/**
diff --git a/packages/layout/src/steps/resolvePagination.js b/packages/layout/src/steps/resolvePagination.js
index c4a84903e..5a4773482 100644
--- a/packages/layout/src/steps/resolvePagination.js
+++ b/packages/layout/src/steps/resolvePagination.js
@@ -7,7 +7,7 @@ import { isNil, omit, compose } from '@react-pdf/fns';
import isFixed from '../node/isFixed';
import splitText from '../text/splitText';
import splitNode from '../node/splitNode';
-import canNodeWrap from '../node/getWrap';
+import canNodeWrap, { NON_WRAP_TYPES } from '../node/getWrap';
import getWrapArea from '../page/getWrapArea';
import getContentArea from '../page/getContentArea';
import createInstances from '../node/createInstances';
@@ -42,6 +42,34 @@ const warnUnavailableSpace = node => {
);
};
+const warnFallbackSpace = node => {
+ console.warn(
+ `Node of type ${node.type} can't wrap between pages and it's bigger than available page height, falling back to wrap, and putting it on the next page`,
+ );
+};
+const breakableViewChild = (children, height, path = '') => {
+ for (let i = 0; i < children.length; i += 1) {
+ if (children[i].type !== 'VIEW') continue;
+
+ if (shouldNodeBreak(children[i], children.slice(i + 1, height))) {
+ return {
+ child: children[i],
+ path: `${path}/${i}`,
+ };
+ }
+
+ if (children[i].children && children[i].children.length > 0) {
+ const breakable = breakableViewChild(
+ children[i].children,
+ height,
+ `${path}/${i}`,
+ );
+ if (breakable) return breakable;
+ }
+ }
+ return null;
+};
+
const splitNodes = (height, contentArea, nodes) => {
const currentChildren = [];
const nextChildren = [];
@@ -55,7 +83,14 @@ const splitNodes = (height, contentArea, nodes) => {
const nodeHeight = child.box.height;
const isOutside = height <= nodeTop;
const shouldBreak = shouldNodeBreak(child, futureNodes, height);
+
+ const firstBreakableViewChild =
+ child.children &&
+ child.children.length > 0 &&
+ breakableViewChild(child.children, height, '');
+
const shouldSplit = height + SAFTY_THRESHOLD < nodeTop + nodeHeight;
+
const canWrap = canNodeWrap(child);
const fitsInsidePage = nodeHeight <= contentArea;
@@ -73,10 +108,29 @@ const splitNodes = (height, contentArea, nodes) => {
}
if (!fitsInsidePage && !canWrap) {
- currentChildren.push(child);
- nextChildren.push(...futureNodes);
- warnUnavailableSpace(child);
- break;
+ if (NON_WRAP_TYPES.includes(child.type)) {
+ // We don't want to break non wrapable nodes, so we just let them be.
+ // They will be cropped, user will need to fix their ~image usage?
+ currentChildren.push(child);
+ nextChildren.push(...futureNodes);
+ warnUnavailableSpace(child);
+ break;
+ } else {
+ // We don't want to break non wrapable nodes, so we just let them be.
+ warnFallbackSpace(child);
+ const box = Object.assign({}, child.box, {
+ top: child.box.top - height,
+ });
+ const props = Object.assign({}, child.props, {
+ wrap: true,
+ break: false,
+ });
+ const next = Object.assign({}, child, { box, props });
+
+ currentChildren.push(...futureFixedNodes);
+ nextChildren.push(next, ...futureNodes);
+ break;
+ }
}
if (shouldBreak) {
@@ -92,15 +146,16 @@ const splitNodes = (height, contentArea, nodes) => {
break;
}
- if (shouldSplit) {
+ if (shouldSplit || firstBreakableViewChild) {
const [currentChild, nextChild] = split(child, height, contentArea);
// All children are moved to the next page, it doesn't make sense to show the parent on the current page
+ // This was causing an infinite loop parent will now be discarded when it has no content
if (child.children.length > 0 && currentChild.children.length === 0) {
- const box = Object.assign({}, child.box, {
- top: child.box.top - height,
+ const box = Object.assign({}, nextChild.box, {
+ top: nextChild.box.top - height,
});
- const next = Object.assign({}, child, { box });
+ const next = Object.assign({}, nextChild, { box });
currentChildren.push(...futureFixedNodes);
nextChildren.push(next, ...futureNodes);
@@ -121,16 +176,19 @@ const splitNodes = (height, contentArea, nodes) => {
const splitChildren = (height, contentArea, node) => {
const children = node.children || [];
+
const availableHeight = height - getTop(node);
+
return splitNodes(availableHeight, contentArea, children);
};
-const splitView = (node, height, contentArea) => {
+const splitView = (node, height, contentArea, foundBreakableViewChild) => {
const [currentNode, nextNode] = splitNode(node, height);
const [currentChilds, nextChildren] = splitChildren(
height,
contentArea,
node,
+ foundBreakableViewChild,
);
return [
@@ -139,8 +197,10 @@ const splitView = (node, height, contentArea) => {
];
};
-const split = (node, height, contentArea) =>
- isText(node) ? splitText(node, height) : splitView(node, height, contentArea);
+const split = (node, height, contentArea, foundBreakableViewChild) =>
+ isText(node)
+ ? splitText(node, height)
+ : splitView(node, height, contentArea, foundBreakableViewChild);
const shouldResolveDynamicNodes = node => {
const children = node.children || [];
diff --git a/packages/renderer/tests/pageBreak.test.js b/packages/renderer/tests/pageBreak.test.js
new file mode 100644
index 000000000..bfa5ca5f2
--- /dev/null
+++ b/packages/renderer/tests/pageBreak.test.js
@@ -0,0 +1,177 @@
+/* eslint-disable react/no-array-index-key */
+import renderToImage from './renderComponent';
+import { Document, Font, Page, Text, View, StyleSheet } from '..';
+
+const styles = StyleSheet.create({
+ body: {
+ paddingTop: 35,
+ paddingBottom: 65,
+ paddingHorizontal: 35,
+ },
+ title: {
+ fontSize: 24,
+ textAlign: 'center',
+ fontFamily: 'Oswald',
+ },
+ author: {
+ fontSize: 12,
+ textAlign: 'center',
+ marginBottom: 40,
+ },
+ subtitle: {
+ fontSize: 18,
+ margin: 12,
+ fontFamily: 'Oswald',
+ },
+ text: {
+ fontFamily: 'Open Sans',
+ margin: 12,
+ fontSize: 14,
+ textAlign: 'justify',
+ },
+ image: {
+ marginVertical: 15,
+ marginHorizontal: 100,
+ },
+ header: {
+ fontFamily: 'Open Sans',
+ fontSize: 12,
+ marginBottom: 20,
+ textAlign: 'center',
+ color: 'grey',
+ },
+ footer: {
+ fontFamily: 'Open Sans',
+ fontSize: 12,
+ marginBottom: 20,
+ textAlign: 'center',
+ color: 'grey',
+ },
+ pageNumber: {
+ fontFamily: 'Open Sans',
+ position: 'absolute',
+ fontSize: 12,
+ bottom: 30,
+ left: 0,
+ right: 0,
+ textAlign: 'center',
+ color: 'grey',
+ },
+});
+
+Font.register({
+ family: 'Oswald',
+ src: 'https://fonts.gstatic.com/s/oswald/v13/Y_TKV6o8WovbUd3m_X9aAA.ttf',
+});
+
+Font.register({
+ family: 'Open Sans',
+ src: 'https://fonts.gstatic.com/s/opensans/v17/mem8YaGs126MiZpBA-UFVZ0e.ttf',
+});
+
+const PageWrap = () => (
+
+
+
+ ~ Created with react-pdf ~
+
+ New Page 1
+
+
+
+ {'New Page 1 Content'.repeat(800)}
+ End of Page 1 content
+
+
+
+ New Page 2
+
+ New Page 2 Content
+ <>
+
+
+ New Page 3
+
+
+ {'New Page 3 Content'.repeat(1000)}
+ End of Page 3 content
+
+ <>
+
+ New Page 4
+
+ {'New Page 4 Content'.repeat(1000)}
+ End of Page 4 content
+
+
+ New Page 5
+
+ New Page 5 Content
+
+ >
+
+ >
+
+ New Page 6
+ New Page 6 Content
+
+
+ New Page 7
+ New Page 7 Content
+
+ New Page 8
+
+ New Page 8 Content
+
+
+ {`New Page ${9}`}
+ New Page 9 Content
+
+
+
+
+ New Page 10
+ New Page 10 Content
+
+
+
+ New Page 11
+ New Page 11 Content
+
+
+ New Page 12
+ New Page 12 Content
+
+ New Page 13
+ New Page 13 Content
+
+ New Page 14
+ New Page 14 Content
+
+
+
+
+
+ New Page 15
+ New Page 15 Content
+
+
+
+
+
+ `${pageNumber} / ${totalPages}`}
+ fixed
+ />
+
+
+);
+
+describe('pageBreak', () => {
+ test('should put every break instance on a new page', async () => {
+ const image = await renderToImage();
+
+ expect(image).toMatchImageSnapshot();
+ }, 30_000);
+});
diff --git a/packages/renderer/tests/pageWrapFallback.test.js b/packages/renderer/tests/pageWrapFallback.test.js
new file mode 100644
index 000000000..b05a43fa4
--- /dev/null
+++ b/packages/renderer/tests/pageWrapFallback.test.js
@@ -0,0 +1,92 @@
+/* eslint-disable react/no-array-index-key */
+import renderToImage from './renderComponent';
+import { Document, Font, Page, Text, View, StyleSheet, Image } from '..';
+
+const styles = StyleSheet.create({
+ body: {
+ paddingTop: 35,
+ paddingBottom: 65,
+ paddingHorizontal: 35,
+ },
+ title: {
+ fontSize: 24,
+ textAlign: 'center',
+ fontFamily: 'Oswald',
+ },
+ author: {
+ fontSize: 12,
+ textAlign: 'center',
+ marginBottom: 40,
+ },
+ subtitle: {
+ fontSize: 18,
+ margin: 12,
+ fontFamily: 'Oswald',
+ },
+ text: {
+ fontFamily: 'Open Sans',
+ margin: 12,
+ fontSize: 14,
+ textAlign: 'justify',
+ },
+ image: {
+ marginVertical: 15,
+ marginHorizontal: 100,
+ },
+ header: {
+ fontFamily: 'Open Sans',
+ fontSize: 12,
+ marginBottom: 20,
+ textAlign: 'center',
+ color: 'grey',
+ },
+ pageNumber: {
+ fontFamily: 'Open Sans',
+ position: 'absolute',
+ fontSize: 12,
+ bottom: 30,
+ left: 0,
+ right: 0,
+ textAlign: 'center',
+ color: 'grey',
+ },
+});
+
+Font.register({
+ family: 'Oswald',
+ src: 'https://fonts.gstatic.com/s/oswald/v13/Y_TKV6o8WovbUd3m_X9aAA.ttf',
+});
+
+Font.register({
+ family: 'Open Sans',
+ src: 'https://fonts.gstatic.com/s/opensans/v17/mem8YaGs126MiZpBA-UFVZ0e.ttf',
+});
+
+const PageWrap = () => (
+
+
+
+ {'yadayada '.repeat(1000)}
+
+
+
+ {'yada '.repeat(1000)}
+
+
+
+
+
+
+);
+
+describe('pageBreak', () => {
+ test('When content does not fit in one page, it should wrap if possible', async () => {
+ const image = await renderToImage();
+
+ expect(image).toMatchImageSnapshot();
+ }, 30_000);
+});
diff --git a/packages/renderer/tests/snapshots/page-break-test-js-page-break-should-put-every-break-instance-on-a-new-page-1-snap.png b/packages/renderer/tests/snapshots/page-break-test-js-page-break-should-put-every-break-instance-on-a-new-page-1-snap.png
new file mode 100644
index 000000000..e6cae749f
Binary files /dev/null and b/packages/renderer/tests/snapshots/page-break-test-js-page-break-should-put-every-break-instance-on-a-new-page-1-snap.png differ
diff --git a/packages/renderer/tests/snapshots/page-wrap-fallback-test-js-page-break-when-content-does-not-fit-in-one-page-it-should-wrap-if-possible-1-snap.png b/packages/renderer/tests/snapshots/page-wrap-fallback-test-js-page-break-when-content-does-not-fit-in-one-page-it-should-wrap-if-possible-1-snap.png
new file mode 100644
index 000000000..d82c55fd6
Binary files /dev/null and b/packages/renderer/tests/snapshots/page-wrap-fallback-test-js-page-break-when-content-does-not-fit-in-one-page-it-should-wrap-if-possible-1-snap.png differ