Print preview

Google-Sheets-style full-screen print preview dialog with paginated previews, paper-size + margin + orientation settings, headers/footers, scope, and a paginated browser print

PrintPreviewDialog is a self-contained, full-screen modal that paginates the active sheet into printable page-sized blocks, renders the same blocks into a hidden iframe with the appropriate @page rules, and triggers the browser print dialog. Modelled on Google Sheets' Print pane — same sidebar controls, same paginated preview area, same paper sizes.

Is this a breaking change?

No. Print preview is purely additive:

  • New named export from @rowsncolumns/spreadsheet: PrintPreviewDialog and PrintSettings / PaperSizeId / related types.

  • ButtonPrint's API is unchanged — it's still a thin ToolbarIconButton. The only thing that updates is your onClick handler (you used to call window.print(), now you open the dialog).

  • No props on useSpreadsheetState, CanvasGrid, or any existing component change. If you don't render PrintPreviewDialog, nothing happens.

Upgrade path: drop the new dialog into your app and rewire ButtonPrint. That's it.

Why a dialog instead of window.print()?

window.print() prints the entire host page — your toolbar, formula bar, sidebar, panels, ads, whatever else lives on the route. That's almost never what a user wants when they hit ⌘P on a spreadsheet.

The dialog instead:

  1. Paginates the sheet data into page-sized blocks at the chosen paper size + orientation + scale.

  2. Renders each block as a self-contained HTML table styled from getEffectiveFormat.

  3. On Print, snapshots only those blocks into a hidden iframe alongside the matching @page { size: ... } CSS, then calls iframe.contentWindow.print(). The browser print dialog shows the actual paginated spreadsheet, nothing else from your app.

Integration

The dialog reads everything it needs from values you already have on useSpreadsheetState. No adapter object; the props are flat.

That's the whole integration. The dialog manages its own settings state internally — the host doesn't need to persist or store anything. Settings carry over across open/close cycles within the same mount, so a user who picks A4 + Landscape and reopens still sees A4 + Landscape. If you want every open to start fresh, pass a changing key (e.g. key={isPrintOpen ? "open" : "closed"}) — React will remount the component and reset state to defaults.

PrintPreviewDialog props

Prop
Type
Required
Purpose

isOpen

boolean

Controlled visibility.

onClose

() => void

Fires on Cancel, the close icon, or after Print. (Esc binding lives in user space — see Integration above.)

sheets

Sheet[]

Full sheet list — same array on useSpreadsheetState.sheets. Drives both the active-sheet lookup and the "Workbook" scope.

activeSheetId

number

Which sheet to preview. The dialog looks it up in sheets.

selections

SelectionArea[]?

The first selection's range powers the "Selection" scope. Pass useSpreadsheetState.selections straight through; omit / pass [] to hide the option.

title

string?

Workbook display name. Shown in the header strip when the "Workbook title" header field is enabled.

getFormattedValue

(sheetId, row, col) => string

From useSpreadsheetState.

getEffectiveFormat

(sheetId, row, col) => CellFormat

From useSpreadsheetState.

getDataRowCount

(sheetId) => number?

When present, restricts the print extent to the last non-empty row (matches Google Sheets). Falls back to sheet.rowCount.

getDataColumnCount

(sheetId) => number?

Same as above for columns.

getCellData

(sheetId, row, col) => CellData?

Surfaces the "Show notes" red corner marker. Without it the toggle still renders but produces no markers.

The dialog pulls everything else off the Sheet it resolves — title (display name), merges, frozenRowCount / frozenColumnCount, and per-row / per-column sizes from rowMetadata / columnMetadata.

The dialog manages its own settings state — defaults match Google Sheets where they line up.

Setting
Options

Print scope

Current sheet · Workbook (when sheets.length > 1) · Selection (when selections[0] is set)

Paper size

Letter · Tabloid · Legal · Statement · Executive · Folio · A3 · A4 · A5 · B4 · B5

Orientation

Portrait · Landscape

Scale

Normal (100 %) · Fit to width · Fit to page · Custom (10 – 400 %)

Margins

Normal · Narrow · Wide · Custom (per-edge px)

Show gridlines

toggle

Show notes

toggle — paints a red corner triangle on noted cells (needs getCellData)

Page order

Over, then down (default) · Down, then over

Horizontal alignment

Left · Center · Right

Vertical alignment

Top · Middle · Bottom

Headers & footers

Page numbers · Workbook title · Sheet name · Current date · Current time — auto-placed across the 3-zone strip

Repeat frozen rows

toggle, disabled when Sheet.frozenRowCount is 0 / unset

Repeat frozen columns

toggle, disabled when Sheet.frozenColumnCount is 0 / unset

Pagination

The pagination engine (computePageLayout) walks the cell range with the active scale, splitting rows and columns into bands whose summed scaled dimensions fit the page content area (paper minus margins minus optional header/footer strips, minus a 1 px reserve for the closing cell border).

  • Over, then down: outer = row bands, inner = column bands — page order goes left-to-right across the first row band, then advances. (Google Sheets' default.)

  • Down, then over: outer = column bands, inner = row bands — page order goes top-to-bottom within the first column band, then advances. (Excel's pageOrder="downThenOver" default.)

Frozen rows / columns, when their toggle is on, are subtracted from the content area on every page and prepended as a fixed prefix to each band — every page renders with the same frozen header rows / leftmost columns.

What's NOT in this release

Deferred so we could ship the core flow. None block the integration above.

  • Set custom page breaks — drag-edit page breaks UI. The underlying <rowBreaks> / <colBreaks> XLSX metadata still round-trips on import/export.

  • Edit custom fields — the Google-Sheets 3-zone (&L / &C / &R) header / footer composer. v1 auto-places enabled fields.

  • Show notes — note text — the toggle ships and marks noted cells with a red corner triangle, but the note text isn't emitted in a trailing endnote section (Google Sheets does this; deferred).

Files

  • apps/spreadsheet/components/print-preview/settings.tsPrintSettings model + paper-size / margin constants.

  • apps/spreadsheet/components/print-preview/use-page-layout.ts — pure pagination math; band slicing, scale resolution.

  • apps/spreadsheet/components/print-preview/cell-styles.ts — maps CellFormat to inline CSS for the printed cells.

  • apps/spreadsheet/components/print-preview/page-block.tsx — renders one page as a styled HTML table.

  • apps/spreadsheet/components/print-preview/print-preview-dialog.tsx — the modal, sidebar, and pagination orchestration.

  • apps/spreadsheet/components/print-preview/run-print.ts — hidden-iframe window.print() driver with @page rules.

Last updated