diff --git a/packages/layout/src/steps/resolvePagination.ts b/packages/layout/src/steps/resolvePagination.ts index f029d7619..745d140ae 100644 --- a/packages/layout/src/steps/resolvePagination.ts +++ b/packages/layout/src/steps/resolvePagination.ts @@ -271,12 +271,40 @@ const resolvePageIndices = (fontStore, yoga, page, pageNumber, pages) => { return resolveDynamicPage(props, page, fontStore, yoga); }; -const assocSubPageData = (subpages) => { - return subpages.map((page, i) => ({ - ...page, - subPageNumber: i, - subPageTotalPages: subpages.length, - })); +const assocSubPageData = ( + subpages: SafePageNode[], + originalPage?: SafePageNode, + startPageNumber?: number, +) => { + return subpages.map((page, i) => { + const basePage = { + ...page, + subPageNumber: i, + subPageTotalPages: subpages.length, + }; + + // Apply wrapStyles if provided + // Always apply wrapStyles (even for single pages) to ensure correct styling + // based on absolute page number in the document + if (originalPage?.props?.wrapStyles) { + // Calculate absolute page number for this subpage + const absolutePageNumber = + startPageNumber !== undefined ? startPageNumber + i : i + 1; + const wrapStyles = originalPage.props.wrapStyles( + i, + subpages.length, + absolutePageNumber, + ); + if (wrapStyles) { + // Merge wrapStyles with existing style + basePage.style = Array.isArray(basePage.style) + ? ([...basePage.style, wrapStyles] as any) + : ([basePage.style || {}, wrapStyles] as any); + } + } + + return basePage; + }); }; const dissocSubPageData = (page) => { @@ -332,7 +360,7 @@ const resolvePagination = ( const page = root.children[i]; let subpages = paginate(page, pageNumber, fontStore, root.yoga); - subpages = assocSubPageData(subpages); + subpages = assocSubPageData(subpages, page, pageNumber); pageNumber += subpages.length; pages = pages.concat(subpages); } diff --git a/packages/layout/src/types/page.ts b/packages/layout/src/types/page.ts index 25e8548d4..a4d66a51c 100644 --- a/packages/layout/src/types/page.ts +++ b/packages/layout/src/types/page.ts @@ -84,6 +84,15 @@ interface PageProps extends NodeProps { * @see https://react-pdf.org/components#page-wrapping */ wrap?: boolean; + /** + * Styles to apply to each page when wrapping occurs. + * Function receives page index (0-based), total pages count, and absolute page number. + */ + wrapStyles?: ( + pageIndex: number, + totalPages: number, + absolutePageNumber: number, + ) => Style | Style[]; size?: PageSize; orientation?: Orientation; dpi?: number; diff --git a/packages/layout/tests/steps/resolvePagination.test.ts b/packages/layout/tests/steps/resolvePagination.test.ts index f8ca257b3..ccd09de31 100644 --- a/packages/layout/tests/steps/resolvePagination.test.ts +++ b/packages/layout/tests/steps/resolvePagination.test.ts @@ -339,4 +339,138 @@ describe('pagination step', () => { // If calcLayout returns then we did not hit an infinite loop expect(true).toBe(true); }); + + test('should apply wrapStyles to each wrapped page', async () => { + const yoga = await loadYoga(); + + const layout = calcLayout({ + type: 'DOCUMENT', + yoga, + props: {}, + style: {}, + children: [ + { + type: 'PAGE', + style: { + width: 5, + height: 60, + }, + props: { + wrap: true, + wrapStyles: (pageIndex: number) => ({ + backgroundColor: pageIndex === 0 ? 'red' : 'blue', + padding: pageIndex * 10, + }), + }, + children: [ + { + type: 'VIEW', + style: { height: 130 }, + props: {}, + children: [], + }, + ], + }, + ], + }); + + expect(layout.children.length).toBe(3); + + // First page should have red background and no padding + const page1 = layout.children[0]; + expect(page1.style).toEqual([ + { width: 5, height: 60 }, + { backgroundColor: 'red', padding: 0 }, + ]); + + // Second page should have blue background and padding of 10 + const page2 = layout.children[1]; + expect(page2.style).toEqual([ + { width: 5, height: 60 }, + { backgroundColor: 'blue', padding: 10 }, + ]); + + // Third page should have blue background and padding of 20 + const page3 = layout.children[2]; + expect(page3.style).toEqual([ + { width: 5, height: 60 }, + { backgroundColor: 'blue', padding: 20 }, + ]); + }); + + test('should not apply wrapStyles when wrap is false', async () => { + const yoga = await loadYoga(); + + const layout = calcLayout({ + type: 'DOCUMENT', + yoga, + props: {}, + style: {}, + children: [ + { + type: 'PAGE', + style: { + width: 5, + height: 60, + }, + props: { + wrap: false, + wrapStyles: () => ({ + backgroundColor: 'red', + }), + }, + children: [ + { + type: 'VIEW', + style: { height: 130 }, + props: {}, + children: [], + }, + ], + }, + ], + }); + + expect(layout.children.length).toBe(1); + const page = layout.children[0]; + expect(page.style).toEqual({ width: 5, height: 60 }); + }); + + test('should not apply wrapStyles when only one page is generated', async () => { + const yoga = await loadYoga(); + + const layout = calcLayout({ + type: 'DOCUMENT', + yoga, + props: {}, + style: {}, + children: [ + { + type: 'PAGE', + style: { + width: 5, + height: 60, + }, + props: { + wrap: true, + wrapStyles: () => ({ + backgroundColor: 'red', + }), + }, + children: [ + { + type: 'VIEW', + style: { height: 30 }, + props: {}, + children: [], + }, + ], + }, + ], + }); + + expect(layout.children.length).toBe(1); + const page = layout.children[0]; + expect(page.style).toEqual({ width: 5, height: 60 }); + }); }); diff --git a/packages/renderer/index.d.ts b/packages/renderer/index.d.ts index 39cb5445b..25e483bf8 100644 --- a/packages/renderer/index.d.ts +++ b/packages/renderer/index.d.ts @@ -88,6 +88,15 @@ declare namespace ReactPDF { * @see https://react-pdf.org/advanced#debugging */ debug?: boolean; + /** + * Styles to apply to each page when wrapping occurs. + * Function receives page index (0-based), total pages count, and absolute page number. + */ + wrapStyles?: ( + pageIndex: number, + totalPages: number, + absolutePageNumber: number, + ) => Style | Style[]; size?: PageSize; orientation?: Orientation; dpi?: number; diff --git a/packages/types/node.d.ts b/packages/types/node.d.ts index d0ab9b85a..b64c24517 100644 --- a/packages/types/node.d.ts +++ b/packages/types/node.d.ts @@ -33,6 +33,15 @@ interface ViewProps extends BaseProps { interface PageProps extends BaseProps { wrap?: boolean; + /** + * Styles to apply to each page when wrapping occurs. + * Function receives page index (0-based), total pages count, and absolute page number. + */ + wrapStyles?: ( + pageIndex: number, + totalPages: number, + absolutePageNumber: number, + ) => Style | Style[]; size?: PageSize; orientation?: Orientation; dpi?: number;