From d77b66f0368d545ad1556987c434097f5b3b7d1b Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Tue, 26 May 2026 19:33:13 -0700 Subject: [PATCH 1/3] fix: autoFit table width overflow from cell preferences --- .../measuring/dom/src/autofit-columns.test.ts | 139 ++++++++++++++++++ .../measuring/dom/src/autofit-columns.ts | 6 +- .../dom/src/autofit-normalize.test.ts | 83 +++++++++++ .../measuring/dom/src/autofit-normalize.ts | 53 ++++++- .../measuring/dom/src/index.test.ts | 54 +++++++ 5 files changed, 329 insertions(+), 6 deletions(-) diff --git a/packages/layout-engine/measuring/dom/src/autofit-columns.test.ts b/packages/layout-engine/measuring/dom/src/autofit-columns.test.ts index 78d06f6edf..038c104458 100644 --- a/packages/layout-engine/measuring/dom/src/autofit-columns.test.ts +++ b/packages/layout-engine/measuring/dom/src/autofit-columns.test.ts @@ -180,6 +180,124 @@ describe('computeAutoFitColumnWidths', () => { expect(result.columnWidths[3]).toBeLessThan(156); }); + it('keeps a fitting uniform tblW auto grid within its width budget when tcW preferences overflow it', () => { + const result = computeAutoFitColumnWidths( + buildExplicitInput({ + workingInput: withAutoGridWidthBudget( + buildWorkingInput({ + preferredTableWidth: undefined, + maxTableWidth: 576, + preferredColumnWidths: [144, 144, 144, 144], + gridColumnCount: 4, + rows: [ + buildAutoGridRow([240, 76.8, 144, 288]), + buildAutoGridRow([240, 76.8, 144, 288]), + buildAutoGridRow([144, 144, 144, 144]), + ], + }), + 576, + ), + fixedLayout: { + columnWidths: [240, 144, 144, 288], + totalWidth: 816, + gridColumnCount: 4, + preferredTableWidth: undefined, + }, + contentMetrics: buildContentMetrics([ + [ + { min: 40, max: 160, preferredWidth: 240 }, + { min: 40, max: 80, preferredWidth: 76.8 }, + { min: 40, max: 120, preferredWidth: 144 }, + { min: 40, max: 220, preferredWidth: 288 }, + ], + [ + { min: 40, max: 160, preferredWidth: 240 }, + { min: 40, max: 80, preferredWidth: 76.8 }, + { min: 40, max: 120, preferredWidth: 144 }, + { min: 40, max: 220, preferredWidth: 288 }, + ], + [ + { min: 40, max: 120, preferredWidth: 144 }, + { min: 40, max: 120, preferredWidth: 144 }, + { min: 40, max: 120, preferredWidth: 144 }, + { min: 40, max: 120, preferredWidth: 144 }, + ], + ]), + }), + ); + + expect(result.totalWidth).toBeLessThanOrEqual(576); + expect(result.columnWidths[0]).toBeGreaterThan(result.columnWidths[1]); + expect(result.columnWidths[3]).toBeGreaterThan(result.columnWidths[2]); + }); + + it('keeps a fitting non-uniform tblW auto grid within its width budget when tcW preferences overflow it', () => { + const result = computeAutoFitColumnWidths( + buildExplicitInput({ + workingInput: withAutoGridWidthBudget( + buildWorkingInput({ + preferredTableWidth: undefined, + preserveAutoGrid: true, + maxTableWidth: 576, + preferredColumnWidths: [180, 120, 276], + gridColumnCount: 3, + rows: [buildAutoGridRow([240, 144, 360])], + }), + 576, + ), + fixedLayout: { + columnWidths: [240, 144, 360], + totalWidth: 744, + gridColumnCount: 3, + preferredTableWidth: undefined, + }, + contentMetrics: buildContentMetrics([ + [ + { min: 60, max: 180, preferredWidth: 240 }, + { min: 60, max: 120, preferredWidth: 144 }, + { min: 60, max: 240, preferredWidth: 360 }, + ], + ]), + }), + ); + + expect(result.totalWidth).toBeLessThanOrEqual(576); + }); + + it('allows a tblW auto grid budget to grow only when content minimums require it', () => { + const result = computeAutoFitColumnWidths( + buildExplicitInput({ + workingInput: withAutoGridWidthBudget( + buildWorkingInput({ + preferredTableWidth: undefined, + maxTableWidth: 500, + preferredColumnWidths: [100, 100, 100], + gridColumnCount: 3, + rows: [buildAutoGridRow([240, 100, 100])], + }), + 300, + ), + fixedLayout: { + columnWidths: [240, 100, 100], + totalWidth: 440, + gridColumnCount: 3, + preferredTableWidth: undefined, + }, + contentMetrics: buildContentMetrics([ + [ + { min: 260, max: 320, preferredWidth: 240 }, + { min: 80, max: 100, preferredWidth: 100 }, + { min: 80, max: 100, preferredWidth: 100 }, + ], + ]), + }), + ); + + expect(result.totalWidth).toBeGreaterThan(300); + expect(result.totalWidth).toBeLessThanOrEqual(500); + expect(result.columnWidths[0]).toBeGreaterThanOrEqual(260); + }); + it('preserves explicit tblW AutoFit authored grid when content already fits', () => { const authoredWidths = [95.867, 472.533, 84.467]; const tableWidth = authoredWidths.reduce((sum, width) => sum + width, 0); @@ -854,6 +972,27 @@ function buildWorkingInput(overrides: Partial = {}): Work }; } +function withAutoGridWidthBudget(workingInput: WorkingTableGridInput, budget: number): WorkingTableGridInput { + return { + ...workingInput, + autoGridWidthBudget: budget, + } as WorkingTableGridInput; +} + +function buildAutoGridRow(preferredWidths: number[]): WorkingTableGridInput['rows'][number] { + return { + skippedBefore: [], + skippedAfter: [], + skippedColumns: [], + logicalColumnCount: preferredWidths.length, + cells: preferredWidths.map((preferredWidth, startColumn) => ({ + startColumn, + span: 1, + preferredWidth, + })), + }; +} + function buildContentMetrics( rows: Array>, ): AutoFitContentMetricsInput { diff --git a/packages/layout-engine/measuring/dom/src/autofit-columns.ts b/packages/layout-engine/measuring/dom/src/autofit-columns.ts index 3b870a2b6a..f85f7b021c 100644 --- a/packages/layout-engine/measuring/dom/src/autofit-columns.ts +++ b/packages/layout-engine/measuring/dom/src/autofit-columns.ts @@ -234,8 +234,10 @@ export function computeAutoFitColumnWidths(input: AutoFitInput): AutoFitResult { triggerCells.length > 0 ? collectNonProtectedColumns(triggerCells, gridColumnCount) : undefined; let resolvedWidths = currentWidths.slice(); const preferredTableWidth = sanitizeOptionalWidth(workingInput.preferredTableWidth); - let targetTableWidth = preferredTableWidth ?? fixedLayout.totalWidth; - const canOverflowAvailableWidth = preferredTableWidth != null || hasCompleteAuthoredGrid(workingInput); + const autoGridWidthBudget = sanitizeOptionalWidth(workingInput.autoGridWidthBudget); + let targetTableWidth = preferredTableWidth ?? autoGridWidthBudget ?? fixedLayout.totalWidth; + const canOverflowAvailableWidth = + preferredTableWidth != null || (autoGridWidthBudget == null && hasCompleteAuthoredGrid(workingInput)); const maxResolvedTableWidth = canOverflowAvailableWidth ? Math.max(workingInput.maxTableWidth, targetTableWidth) : workingInput.maxTableWidth; diff --git a/packages/layout-engine/measuring/dom/src/autofit-normalize.test.ts b/packages/layout-engine/measuring/dom/src/autofit-normalize.test.ts index d4769ac600..e922cf5384 100644 --- a/packages/layout-engine/measuring/dom/src/autofit-normalize.test.ts +++ b/packages/layout-engine/measuring/dom/src/autofit-normalize.test.ts @@ -193,6 +193,89 @@ describe('buildAutoFitWorkingGridInput', () => { expect(result.gridColumnCount).toBe(4); }); + it('records a fitting uniform tblW auto grid as the AutoFit width budget even when tcW preferences overflow it', () => { + const block = createTableBlock({ + attrs: { + tableWidth: { value: 0, type: 'auto' }, + }, + columnWidths: [144, 144, 144, 144], + rows: [ + { + id: 'row-1', + cells: [ + { id: 'cell-1', attrs: { tableCellProperties: { cellWidth: { value: 3600, type: 'dxa' } } } }, + { id: 'cell-2', attrs: { tableCellProperties: { cellWidth: { value: 1152, type: 'dxa' } } } }, + { id: 'cell-3', attrs: { tableCellProperties: { cellWidth: { value: 2160, type: 'dxa' } } } }, + { id: 'cell-4', attrs: { tableCellProperties: { cellWidth: { value: 4320, type: 'dxa' } } } }, + ], + }, + { + id: 'row-2', + cells: [ + { id: 'cell-5', attrs: { tableCellProperties: { cellWidth: { value: 2160, type: 'dxa' } } } }, + { id: 'cell-6', attrs: { tableCellProperties: { cellWidth: { value: 2160, type: 'dxa' } } } }, + { id: 'cell-7', attrs: { tableCellProperties: { cellWidth: { value: 2160, type: 'dxa' } } } }, + { id: 'cell-8', attrs: { tableCellProperties: { cellWidth: { value: 2160, type: 'dxa' } } } }, + ], + }, + ], + }); + + const result = buildAutoFitWorkingGridInput(block, { maxWidth: 576 }); + + expect(result.preferredTableWidth).toBeUndefined(); + expect(result.preferredColumnWidths).toEqual([144, 144, 144, 144]); + expect(result.preserveAutoGrid).toBeUndefined(); + expect(result).toMatchObject({ autoGridWidthBudget: 576 }); + }); + + it('records a fitting non-uniform tblW auto grid as the AutoFit width budget when tcW preferences overflow it', () => { + const block = createTableBlock({ + attrs: { + tableWidth: { value: 0, type: 'auto' }, + }, + columnWidths: [180, 120, 276], + rows: [ + { + id: 'row-1', + cells: [ + { id: 'cell-1', attrs: { tableCellProperties: { cellWidth: { value: 3600, type: 'dxa' } } } }, + { id: 'cell-2', attrs: { tableCellProperties: { cellWidth: { value: 2160, type: 'dxa' } } } }, + { id: 'cell-3', attrs: { tableCellProperties: { cellWidth: { value: 5400, type: 'dxa' } } } }, + ], + }, + ], + }); + + const result = buildAutoFitWorkingGridInput(block, { maxWidth: 576 }); + + expect(result.preserveAutoGrid).toBe(true); + expect(result.preferredColumnWidths).toEqual([180, 120, 276]); + expect(result).toMatchObject({ autoGridWidthBudget: 576 }); + }); + + it('records omitted table width as AutoFit width-budget semantics when a complete grid fits', () => { + const block = createTableBlock({ + attrs: {}, + columnWidths: [100, 100], + rows: [ + { + id: 'row-1', + cells: [ + { id: 'cell-1', attrs: { tableCellProperties: { cellWidth: { value: 3000, type: 'dxa' } } } }, + { id: 'cell-2', attrs: { tableCellProperties: { cellWidth: { value: 3000, type: 'dxa' } } } }, + ], + }, + ], + }); + + const result = buildAutoFitWorkingGridInput(block, { maxWidth: 200 }); + + expect(result.preferredTableWidth).toBeUndefined(); + expect(result.preferredColumnWidths).toEqual([100, 100]); + expect(result).toMatchObject({ autoGridWidthBudget: 200 }); + }); + it('does not mark incomplete tblW auto grids as preferred AutoFit geometry', () => { const block = createTableBlock({ attrs: { diff --git a/packages/layout-engine/measuring/dom/src/autofit-normalize.ts b/packages/layout-engine/measuring/dom/src/autofit-normalize.ts index bf7ca20ca2..895e643382 100644 --- a/packages/layout-engine/measuring/dom/src/autofit-normalize.ts +++ b/packages/layout-engine/measuring/dom/src/autofit-normalize.ts @@ -75,6 +75,13 @@ export type WorkingTableGridInput = { * force growth. */ preserveExplicitAutoGrid?: boolean; + /** + * AutoFit tables with auto-width semantics and a complete authored grid that + * fits the available width should use the grid sum as their outer width + * budget. Cell `tcW` preferences may still reshape columns inside this budget, + * but should not by themselves expand the table beyond it. + */ + autoGridWidthBudget?: number; /** Preferred table width target, in pixels, if resolvable. */ preferredTableWidth?: number; /** Preferred/authored grid widths, in pixels, in logical-column order. */ @@ -144,10 +151,8 @@ export function buildAutoFitWorkingGridInput( ): WorkingTableGridInput { const maxTableWidth = sanitizePositiveNumber(constraints.maxWidth); const layoutMode = resolveLayoutMode(block.attrs?.tableLayout); - const preferredTableWidth = resolvePreferredTableWidth( - block.attrs?.tableWidth as TableWidthAttr | undefined, - maxTableWidth, - ); + const tableWidth = block.attrs?.tableWidth as TableWidthAttr | undefined; + const preferredTableWidth = resolvePreferredTableWidth(tableWidth, maxTableWidth); const rawPreferredColumnWidths = normalizePreferredColumnWidths(block.columnWidths); const logicalColumnLimit = resolveTrailingPlaceholderColumnLimit(rawPreferredColumnWidths); let activeRowSpans: number[] = []; @@ -181,6 +186,14 @@ export function buildAutoFitWorkingGridInput( gridColumnCount, rows, }); + const autoGridWidthBudget = resolveAutoGridWidthBudget({ + layoutMode, + tableWidth, + preferredColumnWidths, + preferredTableWidth, + gridColumnCount, + maxTableWidth, + }); return { layoutMode, @@ -188,6 +201,7 @@ export function buildAutoFitWorkingGridInput( ...(preserveAuthoredGrid ? { preserveAuthoredGrid } : {}), ...(preserveAutoGrid ? { preserveAutoGrid } : {}), ...(preserveExplicitAutoGrid ? { preserveExplicitAutoGrid } : {}), + ...(autoGridWidthBudget != null ? { autoGridWidthBudget } : {}), preferredTableWidth, preferredColumnWidths, gridColumnCount, @@ -250,6 +264,37 @@ function shouldPreserveExplicitAutoGrid(args: { return approximatelyEqual(sumWidths(preferredColumnWidths), preferredTableWidth); } +function resolveAutoGridWidthBudget(args: { + layoutMode: AutoFitLayoutMode; + tableWidth: TableWidthAttr | undefined; + preferredColumnWidths: number[]; + preferredTableWidth: number | undefined; + gridColumnCount: number; + maxTableWidth: number; +}): number | undefined { + const { layoutMode, tableWidth, preferredColumnWidths, preferredTableWidth, gridColumnCount, maxTableWidth } = args; + if (layoutMode !== 'autofit') return undefined; + if (!hasAutoTableWidthSemantics(tableWidth)) return undefined; + if (preferredTableWidth != null) return undefined; + if (preferredColumnWidths.length === 0 || preferredColumnWidths.length !== gridColumnCount) return undefined; + + const gridWidth = sumWidths(preferredColumnWidths); + if (gridWidth <= 0) return undefined; + if (gridWidth > maxTableWidth + 0.5) return undefined; + + return gridWidth; +} + +function hasAutoTableWidthSemantics(tableWidth: TableWidthAttr | undefined): boolean { + if (tableWidth == null) return true; + if (typeof tableWidth !== 'object') return false; + const type = typeof tableWidth.type === 'string' ? tableWidth.type.toLowerCase() : undefined; + if (type !== 'auto') return false; + + const rawWidth = tableWidth.width ?? tableWidth.value; + return rawWidth == null || (typeof rawWidth === 'number' && Number.isFinite(rawWidth) && rawWidth <= 0); +} + function hasNonUniformGrid(widths: number[]): boolean { if (widths.length <= 1) return true; const firstWidth = widths[0]; diff --git a/packages/layout-engine/measuring/dom/src/index.test.ts b/packages/layout-engine/measuring/dom/src/index.test.ts index a2a0ee80f6..cae829804a 100644 --- a/packages/layout-engine/measuring/dom/src/index.test.ts +++ b/packages/layout-engine/measuring/dom/src/index.test.ts @@ -3640,6 +3640,60 @@ describe('measureBlock', () => { expect(measure.totalWidth).toBe(450); }); + it('keeps tblW auto tables with a fitting authored grid within that grid budget despite wider tcW preferences', async () => { + const paragraph = (id: string, text: string): FlowBlock => ({ + kind: 'paragraph', + id, + runs: [{ text, fontFamily: 'Arial', fontSize: 12 }], + }); + const cell = (id: string, text: string, width: number) => ({ + id, + blocks: [paragraph(`${id}-paragraph`, text)], + attrs: { + tableCellProperties: { + cellWidth: { value: width, type: 'dxa' }, + }, + }, + }); + const block: FlowBlock = { + kind: 'table', + id: 'table-autofit-grid-budget', + attrs: { + tableWidth: { value: 0, type: 'auto' }, + }, + rows: [ + { + id: 'row-0', + cells: [ + cell('cell-0-0', 'Title', 3600), + cell('cell-0-1', 'Year', 1152), + cell('cell-0-2', 'Genre', 2160), + cell('cell-0-3', 'Notes', 4320), + ], + }, + { + id: 'row-1', + cells: [ + cell('cell-1-0', 'A', 2160), + cell('cell-1-1', 'B', 2160), + cell('cell-1-2', 'C', 2160), + cell('cell-1-3', 'D', 2160), + ], + }, + ], + columnWidths: [144, 144, 144, 144], + }; + + const measure = await measureBlock(block, { maxWidth: 576 }); + + expect(measure.kind).toBe('table'); + if (measure.kind !== 'table') throw new Error('expected table measure'); + expect(measure.columnWidths).toHaveLength(4); + expect(measure.totalWidth).toBeLessThanOrEqual(576); + expect(measure.columnWidths[0]).toBeGreaterThan(measure.columnWidths[1]); + expect(measure.columnWidths[3]).toBeGreaterThan(measure.columnWidths[2]); + }); + it('scales column widths proportionally when exceeding available width', async () => { const block: FlowBlock = { kind: 'table', From f4ce579747eb3221676d4fdc2ca4ddd0d99e8f00 Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Tue, 26 May 2026 20:21:14 -0700 Subject: [PATCH 2/3] fix: autofit table width budget for missing-grid tables --- .../measuring/dom/src/autofit-columns.test.ts | 33 ++++++++++++++ .../measuring/dom/src/autofit-columns.ts | 4 +- .../dom/src/autofit-normalize.test.ts | 14 +++--- .../measuring/dom/src/autofit-normalize.ts | 2 +- .../measuring/dom/src/index.test.ts | 43 +++++++++++++++++++ .../dom/src/table-autofit-metrics.ts | 1 + 6 files changed, 88 insertions(+), 9 deletions(-) diff --git a/packages/layout-engine/measuring/dom/src/autofit-columns.test.ts b/packages/layout-engine/measuring/dom/src/autofit-columns.test.ts index 038c104458..d87aa7a389 100644 --- a/packages/layout-engine/measuring/dom/src/autofit-columns.test.ts +++ b/packages/layout-engine/measuring/dom/src/autofit-columns.test.ts @@ -264,6 +264,39 @@ describe('computeAutoFitColumnWidths', () => { expect(result.totalWidth).toBeLessThanOrEqual(576); }); + it('does not expand a narrower AutoFit table up to the grid budget', () => { + const result = computeAutoFitColumnWidths( + buildExplicitInput({ + workingInput: withAutoGridWidthBudget( + buildWorkingInput({ + preferredTableWidth: undefined, + maxTableWidth: 624, + preferredColumnWidths: [312, 312], + gridColumnCount: 2, + rows: [buildAutoGridRow([259, 260])], + }), + 624, + ), + fixedLayout: { + columnWidths: [259, 260], + totalWidth: 519, + gridColumnCount: 2, + preferredTableWidth: undefined, + }, + contentMetrics: buildContentMetrics([ + [ + { min: 40, max: 120, preferredWidth: 259 }, + { min: 40, max: 160, preferredWidth: 260 }, + ], + ]), + }), + ); + + expect(result.totalWidth).toBeCloseTo(519, 3); + expect(result.columnWidths[0]).toBeLessThan(312); + expect(result.columnWidths[1]).toBeLessThan(312); + }); + it('allows a tblW auto grid budget to grow only when content minimums require it', () => { const result = computeAutoFitColumnWidths( buildExplicitInput({ diff --git a/packages/layout-engine/measuring/dom/src/autofit-columns.ts b/packages/layout-engine/measuring/dom/src/autofit-columns.ts index f85f7b021c..d2f1e59e46 100644 --- a/packages/layout-engine/measuring/dom/src/autofit-columns.ts +++ b/packages/layout-engine/measuring/dom/src/autofit-columns.ts @@ -235,7 +235,9 @@ export function computeAutoFitColumnWidths(input: AutoFitInput): AutoFitResult { let resolvedWidths = currentWidths.slice(); const preferredTableWidth = sanitizeOptionalWidth(workingInput.preferredTableWidth); const autoGridWidthBudget = sanitizeOptionalWidth(workingInput.autoGridWidthBudget); - let targetTableWidth = preferredTableWidth ?? autoGridWidthBudget ?? fixedLayout.totalWidth; + let targetTableWidth = + preferredTableWidth ?? + (autoGridWidthBudget != null ? Math.min(fixedLayout.totalWidth, autoGridWidthBudget) : fixedLayout.totalWidth); const canOverflowAvailableWidth = preferredTableWidth != null || (autoGridWidthBudget == null && hasCompleteAuthoredGrid(workingInput)); const maxResolvedTableWidth = canOverflowAvailableWidth diff --git a/packages/layout-engine/measuring/dom/src/autofit-normalize.test.ts b/packages/layout-engine/measuring/dom/src/autofit-normalize.test.ts index e922cf5384..821baf9b52 100644 --- a/packages/layout-engine/measuring/dom/src/autofit-normalize.test.ts +++ b/packages/layout-engine/measuring/dom/src/autofit-normalize.test.ts @@ -254,26 +254,26 @@ describe('buildAutoFitWorkingGridInput', () => { expect(result).toMatchObject({ autoGridWidthBudget: 576 }); }); - it('records omitted table width as AutoFit width-budget semantics when a complete grid fits', () => { + it('does not create an AutoFit grid budget from fallback widths when tblW and tblGrid are omitted', () => { const block = createTableBlock({ attrs: {}, - columnWidths: [100, 100], + columnWidths: [312, 312], rows: [ { id: 'row-1', cells: [ - { id: 'cell-1', attrs: { tableCellProperties: { cellWidth: { value: 3000, type: 'dxa' } } } }, - { id: 'cell-2', attrs: { tableCellProperties: { cellWidth: { value: 3000, type: 'dxa' } } } }, + { id: 'cell-1', attrs: { tableCellProperties: { cellWidth: { value: 3885, type: 'dxa' } } } }, + { id: 'cell-2', attrs: { tableCellProperties: { cellWidth: { value: 3900, type: 'dxa' } } } }, ], }, ], }); - const result = buildAutoFitWorkingGridInput(block, { maxWidth: 200 }); + const result = buildAutoFitWorkingGridInput(block, { maxWidth: 624 }); expect(result.preferredTableWidth).toBeUndefined(); - expect(result.preferredColumnWidths).toEqual([100, 100]); - expect(result).toMatchObject({ autoGridWidthBudget: 200 }); + expect(result.preferredColumnWidths).toEqual([312, 312]); + expect(result.autoGridWidthBudget).toBeUndefined(); }); it('does not mark incomplete tblW auto grids as preferred AutoFit geometry', () => { diff --git a/packages/layout-engine/measuring/dom/src/autofit-normalize.ts b/packages/layout-engine/measuring/dom/src/autofit-normalize.ts index 895e643382..8b2bcb4c12 100644 --- a/packages/layout-engine/measuring/dom/src/autofit-normalize.ts +++ b/packages/layout-engine/measuring/dom/src/autofit-normalize.ts @@ -286,7 +286,7 @@ function resolveAutoGridWidthBudget(args: { } function hasAutoTableWidthSemantics(tableWidth: TableWidthAttr | undefined): boolean { - if (tableWidth == null) return true; + if (tableWidth == null) return false; if (typeof tableWidth !== 'object') return false; const type = typeof tableWidth.type === 'string' ? tableWidth.type.toLowerCase() : undefined; if (type !== 'auto') return false; diff --git a/packages/layout-engine/measuring/dom/src/index.test.ts b/packages/layout-engine/measuring/dom/src/index.test.ts index cae829804a..78f6e8ab23 100644 --- a/packages/layout-engine/measuring/dom/src/index.test.ts +++ b/packages/layout-engine/measuring/dom/src/index.test.ts @@ -3694,6 +3694,49 @@ describe('measureBlock', () => { expect(measure.columnWidths[3]).toBeGreaterThan(measure.columnWidths[2]); }); + it('does not stretch missing-grid AutoFit tables to fallback content width', async () => { + const paragraph = (id: string, text: string): FlowBlock => ({ + kind: 'paragraph', + id, + runs: [{ text, fontFamily: 'Times New Roman', fontSize: 12 }], + }); + const cell = (id: string, text: string, width: number) => ({ + id, + blocks: [paragraph(`${id}-paragraph`, text)], + attrs: { + tableCellProperties: { + cellWidth: { value: width, type: 'dxa' }, + }, + }, + }); + const block: FlowBlock = { + kind: 'table', + id: 'table-autofit-missing-grid', + attrs: {}, + rows: [ + { + id: 'row-0', + cells: [ + cell('cell-0-0', '', 3885), + cell('cell-0-1', 'Lets see how Word Reacts seems liek we are...', 3900), + ], + }, + { + id: 'row-1', + cells: [cell('cell-1-0', 'Lets go ..', 3885), cell('cell-1-1', '', 3900)], + }, + ], + columnWidths: [312, 312], + }; + + const measure = await measureBlock(block, { maxWidth: 624 }); + + expect(measure.kind).toBe('table'); + if (measure.kind !== 'table') throw new Error('expected table measure'); + expect(measure.columnWidths).toHaveLength(2); + expect(measure.totalWidth).toBeCloseTo(519, 3); + }); + it('scales column widths proportionally when exceeding available width', async () => { const block: FlowBlock = { kind: 'table', diff --git a/packages/layout-engine/measuring/dom/src/table-autofit-metrics.ts b/packages/layout-engine/measuring/dom/src/table-autofit-metrics.ts index b9a580f500..b0e3ab7f31 100644 --- a/packages/layout-engine/measuring/dom/src/table-autofit-metrics.ts +++ b/packages/layout-engine/measuring/dom/src/table-autofit-metrics.ts @@ -286,6 +286,7 @@ export function buildAutoFitTableResultCacheKey(table: TableBlock, options: Auto preserveAuthoredGrid: options.workingInput.preserveAuthoredGrid === true, preserveAutoGrid: options.workingInput.preserveAutoGrid === true, preserveExplicitAutoGrid: options.workingInput.preserveExplicitAutoGrid === true, + autoGridWidthBudget: options.workingInput.autoGridWidthBudget ?? null, preferredTableWidth: options.workingInput.preferredTableWidth ?? null, preferredColumnWidths: options.workingInput.preferredColumnWidths, rows: options.workingInput.rows.map((row) => ({ From d158e650beed6959fcdf5165480c928225ba0e36 Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Tue, 26 May 2026 20:33:39 -0700 Subject: [PATCH 3/3] chore: generated fixes