Skip to content

Commit fdea3d0

Browse files
committed
chore: additional fixes
1 parent 98af5b6 commit fdea3d0

3 files changed

Lines changed: 74 additions & 11 deletions

File tree

packages/layout-engine/layout-engine/src/layout-table.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,67 @@ describe('layoutTableBlock', () => {
454454
expect(rowBoundaries![0].resizable).toBe(false);
455455
expect(rowBoundaries![1].resizable).toBe(true);
456456
});
457+
458+
it('marks row boundaries as non-resizable when a rowspan from a prior fragment crosses them', () => {
459+
// 5 rows, 2 columns. First cell in row 0 has rowSpan=4, covering rows 0-3.
460+
// When the table splits so a continuation fragment renders rows 2-4,
461+
// the boundary after row 2 must be blocked because the span from row 0
462+
// still extends through it (the span covers rows 0,1,2,3).
463+
// The boundary after row 3 (end of span) and row 4 should be resizable.
464+
const block = createMockTableBlock(5);
465+
const measure = createMockTableMeasure([100, 100], [30, 30, 30, 30, 30]);
466+
467+
// Inject rowSpan=4 on the first cell of row 0
468+
(measure.rows[0].cells[0] as any).rowSpan = 4;
469+
470+
const fragments: TableFragment[] = [];
471+
let cursorY = 0;
472+
let contentBottom = 65; // Fits rows 0-1 (30+30=60 < 65), forces split before row 2
473+
474+
layoutTableBlock({
475+
block,
476+
measure,
477+
columnWidth: 200,
478+
ensurePage: () => ({
479+
page: { fragments },
480+
columnIndex: 0,
481+
cursorY,
482+
contentBottom,
483+
}),
484+
advanceColumn: () => {
485+
cursorY = 0;
486+
contentBottom = 200; // continuation page has room for remaining rows
487+
return {
488+
page: { fragments },
489+
columnIndex: 0,
490+
cursorY,
491+
contentBottom,
492+
};
493+
},
494+
columnX: () => 0,
495+
});
496+
497+
// Collect all row boundaries across continuation fragments (fromRow >= 2)
498+
const continuationFragments = fragments.filter((f) => f.fromRow >= 2);
499+
expect(continuationFragments.length).toBeGreaterThan(0);
500+
501+
const allRowBoundaries = continuationFragments.flatMap((f) => f.metadata?.rowBoundaries ?? []);
502+
503+
// Row 2 boundary should be blocked (rowSpan from row 0 extends through row 3)
504+
const row2 = allRowBoundaries.find((rb) => rb.index === 2);
505+
expect(row2).toBeDefined();
506+
expect(row2!.resizable).toBe(false);
507+
508+
// Row 3 is the last row of the span — its bottom boundary is NOT blocked
509+
const row3 = allRowBoundaries.find((rb) => rb.index === 3);
510+
expect(row3).toBeDefined();
511+
expect(row3!.resizable).toBe(true);
512+
513+
// Row 4 is entirely outside the span (may be in a later fragment)
514+
const row4 = allRowBoundaries.find((rb) => rb.index === 4);
515+
expect(row4).toBeDefined();
516+
expect(row4!.resizable).toBe(true);
517+
});
457518
});
458519

459520
describe('cellSpacing', () => {

packages/layout-engine/layout-engine/src/layout-table.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -303,23 +303,25 @@ function generateRowBoundaries(
303303
renderedRows.push({ rowIndex: r, isRepeatedHeader: false });
304304
}
305305

306-
// Build a set of ABSOLUTE row boundaries blocked by rowspan cells.
307-
// A boundary after absolute row N is blocked if any cell starting at row N
308-
// has a rowSpan that extends beyond row N.
306+
// Build a set of ABSOLUTE row indices whose bottom boundary is blocked by rowspan cells.
307+
// A boundary after absolute row N is blocked if any cell's rowSpan crosses it.
308+
//
309+
// We must scan ALL table rows, not just renderedRows, because a rowspan that
310+
// starts before fromRow can extend into this fragment's rendered range.
311+
// Example: row 1 has rowSpan=4, fragment renders rows 3-5. The boundary after
312+
// row 3 is blocked because the span from row 1 crosses it.
309313
const blockedBoundaries = new Set<number>();
310-
for (let ri = 0; ri < renderedRows.length; ri++) {
311-
const { rowIndex } = renderedRows[ri];
312-
const rowMeasure = measure.rows[rowIndex];
314+
for (let r = 0; r < measure.rows.length; r++) {
315+
const rowMeasure = measure.rows[r];
313316
if (!rowMeasure) continue;
314317

315318
for (const cellMeasure of rowMeasure.cells) {
316319
const rowSpan = cellMeasure.rowSpan ?? 1;
317320
if (rowSpan <= 1) continue;
318321

319-
// This cell spans from rowIndex to rowIndex + rowSpan - 1.
320-
// Block absolute boundaries between the start row and end row.
321-
// Example: rowIndex=2, rowSpan=3 blocks boundaries after rows 2 and 3.
322-
for (let boundaryRow = rowIndex; boundaryRow < rowIndex + rowSpan - 1; boundaryRow++) {
322+
// This cell spans from row r to r + rowSpan - 1.
323+
// Block boundaries after rows r through r + rowSpan - 2.
324+
for (let boundaryRow = r; boundaryRow < r + rowSpan - 1; boundaryRow++) {
323325
blockedBoundaries.add(boundaryRow);
324326
}
325327
}

packages/super-editor/src/components/TableResizeOverlay.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
<!-- Resize handles for each row boundary -->
3434
<div
3535
v-for="(rowBoundary, rowBoundaryIndex) in resizableRowBoundaries"
36-
:key="`row-handle-${rowBoundary.index}`"
36+
:key="`row-handle-${rowBoundary.i}`"
3737
class="resize-handle resize-handle--row"
3838
:class="{
3939
'resize-handle--active': rowDragState && rowDragState.rowBoundaryIndex === rowBoundaryIndex,

0 commit comments

Comments
 (0)