Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions packages/layout-engine/measuring/dom/src/autofit-columns.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,157 @@ 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('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({
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);
Expand Down Expand Up @@ -854,6 +1005,27 @@ function buildWorkingInput(overrides: Partial<WorkingTableGridInput> = {}): 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<Array<{ span?: number; min: number; max: number; preferredWidth?: number }>>,
): AutoFitContentMetricsInput {
Expand Down
8 changes: 6 additions & 2 deletions packages/layout-engine/measuring/dom/src/autofit-columns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,12 @@ 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 != null ? Math.min(fixedLayout.totalWidth, autoGridWidthBudget) : fixedLayout.totalWidth);
const canOverflowAvailableWidth =
preferredTableWidth != null || (autoGridWidthBudget == null && hasCompleteAuthoredGrid(workingInput));
const maxResolvedTableWidth = canOverflowAvailableWidth
? Math.max(workingInput.maxTableWidth, targetTableWidth)
: workingInput.maxTableWidth;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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('does not create an AutoFit grid budget from fallback widths when tblW and tblGrid are omitted', () => {
const block = createTableBlock({
attrs: {},
columnWidths: [312, 312],
rows: [
{
id: 'row-1',
cells: [
{ 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: 624 });

expect(result.preferredTableWidth).toBeUndefined();
expect(result.preferredColumnWidths).toEqual([312, 312]);
expect(result.autoGridWidthBudget).toBeUndefined();
});

it('does not mark incomplete tblW auto grids as preferred AutoFit geometry', () => {
const block = createTableBlock({
attrs: {
Expand Down
53 changes: 49 additions & 4 deletions packages/layout-engine/measuring/dom/src/autofit-normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -181,13 +186,22 @@ export function buildAutoFitWorkingGridInput(
gridColumnCount,
rows,
});
const autoGridWidthBudget = resolveAutoGridWidthBudget({
layoutMode,
tableWidth,
preferredColumnWidths,
preferredTableWidth,
gridColumnCount,
maxTableWidth,
});

return {
layoutMode,
maxTableWidth,
...(preserveAuthoredGrid ? { preserveAuthoredGrid } : {}),
...(preserveAutoGrid ? { preserveAutoGrid } : {}),
...(preserveExplicitAutoGrid ? { preserveExplicitAutoGrid } : {}),
...(autoGridWidthBudget != null ? { autoGridWidthBudget } : {}),
Comment thread
harbournick marked this conversation as resolved.
preferredTableWidth,
preferredColumnWidths,
gridColumnCount,
Expand Down Expand Up @@ -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 false;
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];
Expand Down
Loading
Loading