From 460369cf2e0aa9660ef1892e3ee09972f03d5fd1 Mon Sep 17 00:00:00 2001 From: Artem Nistuley Date: Mon, 11 May 2026 12:59:25 +0300 Subject: [PATCH 1/2] feat: rtl for tables --- packages/layout-engine/contracts/src/index.ts | 6 + .../dom/src/table/renderTableFragment.ts | 1 + .../src/editors/v1/components/SuperEditor.vue | 20 +++- .../v1/components/TableResizeOverlay.vue | 41 +++++-- .../table-bidivisual-roundtrip.test.js | 51 ++++++++ .../tests/tables/fixtures/ltr-table.docx | Bin 0 -> 13777 bytes .../tests/tables/fixtures/rtl-table-1.docx | Bin 0 -> 19066 bytes .../tests/tables/fixtures/rtl-table-2.docx | Bin 0 -> 18295 bytes tests/behavior/tests/tables/resize.spec.ts | 54 ++++++++- .../tables/rtl-table-click-fallback.spec.ts | 80 +++++++++++++ .../tables/rtl-table-tab-navigation.spec.ts | 110 ++++++++++++++++++ 11 files changed, 348 insertions(+), 15 deletions(-) create mode 100644 packages/super-editor/src/editors/v1/tests/import-export/table-bidivisual-roundtrip.test.js create mode 100644 tests/behavior/tests/tables/fixtures/ltr-table.docx create mode 100644 tests/behavior/tests/tables/fixtures/rtl-table-1.docx create mode 100644 tests/behavior/tests/tables/fixtures/rtl-table-2.docx create mode 100644 tests/behavior/tests/tables/rtl-table-click-fallback.spec.ts create mode 100644 tests/behavior/tests/tables/rtl-table-tab-navigation.spec.ts diff --git a/packages/layout-engine/contracts/src/index.ts b/packages/layout-engine/contracts/src/index.ts index f22d14296e..689e7f16d9 100644 --- a/packages/layout-engine/contracts/src/index.ts +++ b/packages/layout-engine/contracts/src/index.ts @@ -606,10 +606,16 @@ export type TableCellAttrs = { tableCellProperties?: Record; }; +export type TablePropertiesAttrs = { + rightToLeft?: boolean; + [key: string]: unknown; +}; + export type TableAttrs = { borders?: TableBorders; borderCollapse?: 'collapse' | 'separate'; cellSpacing?: CellSpacing; + tableProperties?: TablePropertiesAttrs; sdt?: SdtMetadata; containerSdt?: SdtMetadata; [key: string]: unknown; diff --git a/packages/layout-engine/painters/dom/src/table/renderTableFragment.ts b/packages/layout-engine/painters/dom/src/table/renderTableFragment.ts index 5057b2677b..4b0d9cec2f 100644 --- a/packages/layout-engine/painters/dom/src/table/renderTableFragment.ts +++ b/packages/layout-engine/painters/dom/src/table/renderTableFragment.ts @@ -306,6 +306,7 @@ export const renderTableFragment = (deps: TableRenderDependencies): HTMLElement min: boundary.minWidth, r: boundary.resizable ? 1 : 0, })), + rtl: isRtl, // Add segments for each column boundary (segments where resize handle should appear) segments: boundarySegments.map((segs, colIndex) => segs.map((seg) => ({ diff --git a/packages/super-editor/src/editors/v1/components/SuperEditor.vue b/packages/super-editor/src/editors/v1/components/SuperEditor.vue index 08fcc9bd0b..f274818449 100644 --- a/packages/super-editor/src/editors/v1/components/SuperEditor.vue +++ b/packages/super-editor/src/editors/v1/components/SuperEditor.vue @@ -457,6 +457,14 @@ const isNearColumnBoundary = (event, tableElement) => { try { const metadata = JSON.parse(boundariesAttr); if (!metadata.columns || !Array.isArray(metadata.columns)) return false; + const isRtl = metadata.rtl === true; + const firstColumn = metadata.columns[0]; + const lastColumn = metadata.columns[metadata.columns.length - 1]; + const tableContentLeft = firstColumn && typeof firstColumn.x === 'number' ? firstColumn.x : 0; + const tableContentWidth = + lastColumn && typeof lastColumn.x === 'number' && typeof lastColumn.w === 'number' + ? lastColumn.x + lastColumn.w + : 0; // Get zoom factor to properly compare screen coordinates with layout coordinates const zoom = getEditorZoom(); @@ -486,7 +494,9 @@ const isNearColumnBoundary = (event, tableElement) => { // The boundary x position is at (col.x + col.w) - the right edge of the column // This is in layout coordinates, so multiply by zoom to convert to screen space - const boundaryXScreen = (col.x + col.w) * zoom; + const logicalBoundaryX = col.x + col.w; + const visualBoundaryX = isRtl ? tableContentLeft + tableContentWidth - logicalBoundaryX : logicalBoundaryX; + const boundaryXScreen = visualBoundaryX * zoom; // Check if mouse is horizontally near this boundary (both in screen space now) if (Math.abs(mouseXScreen - boundaryXScreen) <= TABLE_RESIZE_HOVER_THRESHOLD) { @@ -514,8 +524,12 @@ const isNearColumnBoundary = (event, tableElement) => { } } - // Also check left edge of table (x = 0) - if (Math.abs(mouseXScreen) <= TABLE_RESIZE_HOVER_THRESHOLD) { + // Also check table outer edges. + const tableWidthScreen = tableRect.width; + if ( + Math.abs(mouseXScreen) <= TABLE_RESIZE_HOVER_THRESHOLD || + Math.abs(mouseXScreen - tableWidthScreen) <= TABLE_RESIZE_HOVER_THRESHOLD + ) { return true; } diff --git a/packages/super-editor/src/editors/v1/components/TableResizeOverlay.vue b/packages/super-editor/src/editors/v1/components/TableResizeOverlay.vue index b757cf2906..a9fa6b29d7 100644 --- a/packages/super-editor/src/editors/v1/components/TableResizeOverlay.vue +++ b/packages/super-editor/src/editors/v1/components/TableResizeOverlay.vue @@ -381,6 +381,21 @@ function updateOverlayRect() { * - Inner boundaries (between columns) * - Right edge (resize last column) */ +const isRtlTable = computed(() => tableMetadata.value?.rtl === true); + +const tableContentWidth = computed(() => { + const columns = tableMetadata.value?.columns; + if (!columns || columns.length === 0) return 0; + const last = columns[columns.length - 1]; + return last.x + last.w; +}); + +const tableContentLeft = computed(() => { + const columns = tableMetadata.value?.columns; + if (!columns || columns.length === 0) return 0; + return columns[0].x; +}); + const resizableBoundaries = computed(() => { if (!tableMetadata.value?.columns) { return []; @@ -394,20 +409,24 @@ const resizableBoundaries = computed(() => { const col = columns[i]; const nextCol = columns[i + 1]; + const logicalX = nextCol.x; + const visualX = isRtlTable.value ? tableContentLeft.value + tableContentWidth.value - logicalX : logicalX; + boundaries.push({ ...col, index: i, - x: nextCol.x, + x: visualX, type: 'inner', }); } // Add handle for right edge of table (resize last column) const lastCol = columns[columns.length - 1]; + const rtlRightEdgeX = tableContentWidth.value; boundaries.push({ ...lastCol, - index: columns.length - 1, - x: lastCol.x + lastCol.w, + index: isRtlTable.value ? 0 : columns.length - 1, + x: isRtlTable.value ? rtlRightEdgeX : lastCol.x + lastCol.w, type: 'right-edge', }); @@ -727,7 +746,7 @@ function parseTableMetadata() { ) : undefined; - tableMetadata.value = { columns: validatedColumns, segments, rows }; + tableMetadata.value = { columns: validatedColumns, segments, rows, rtl: parsed.rtl === true }; } catch (error) { tableMetadata.value = null; emit('resize-error', { @@ -853,7 +872,8 @@ const mouseMoveThrottle = throttle((event) => { // Calculate raw delta in screen pixels, then convert to layout space // This ensures constraints (which are in layout space) can be compared correctly const screenDelta = event.clientX - dragState.value.initialX; - const delta = screenDelta / zoom; + const visualDelta = screenDelta / zoom; + const delta = isRtlTable.value && !dragState.value.isRightEdge ? -visualDelta : visualDelta; // Calculate constraints based on layout-computed minWidth (already in layout space) const minDelta = -(dragState.value.leftColumn.width - dragState.value.leftColumn.minWidth); @@ -886,13 +906,15 @@ const mouseMoveThrottle = throttle((event) => { // Constrain delta const constrainedDelta = Math.max(minDelta, Math.min(maxDelta, delta)); + const constrainedVisualDelta = + isRtlTable.value && !dragState.value.isRightEdge ? -constrainedDelta : constrainedDelta; // Update visual guideline only (no PM transaction yet) - dragState.value.constrainedDelta = constrainedDelta; + dragState.value.constrainedDelta = constrainedVisualDelta; emit('resize-move', { columnIndex: dragState.value.columnIndex, - delta: constrainedDelta, + delta: constrainedVisualDelta, }); }, THROTTLE_INTERVAL_MS); @@ -906,7 +928,8 @@ const onDocumentMouseMove = mouseMoveThrottle.throttled; function onDocumentMouseUp(event) { if (!dragState.value) return; - const finalDelta = dragState.value.constrainedDelta; + const visualFinalDelta = dragState.value.constrainedDelta; + const finalDelta = isRtlTable.value && !dragState.value.isRightEdge ? -visualFinalDelta : visualFinalDelta; const columnIndex = dragState.value.columnIndex; const initialWidths = dragState.value.initialWidths; const isRightEdge = dragState.value.isRightEdge; @@ -939,7 +962,7 @@ function onDocumentMouseUp(event) { emit('resize-end', { columnIndex, finalWidths: newWidths, - delta: finalDelta, + delta: visualFinalDelta, }); dragState.value = null; diff --git a/packages/super-editor/src/editors/v1/tests/import-export/table-bidivisual-roundtrip.test.js b/packages/super-editor/src/editors/v1/tests/import-export/table-bidivisual-roundtrip.test.js new file mode 100644 index 0000000000..ac156afb4e --- /dev/null +++ b/packages/super-editor/src/editors/v1/tests/import-export/table-bidivisual-roundtrip.test.js @@ -0,0 +1,51 @@ +import { describe, it, expect } from 'vitest'; +import JSZip from 'jszip'; +import { Editor } from '@core/Editor.js'; +import DocxZipper from '@core/DocxZipper.js'; +import { initTestEditor, getTestDataAsFileBuffer } from '../helpers/helpers.js'; + +const TEST_DOC = 'table-width-issue.docx'; + +async function buildDocxWithBidiVisualTable() { + const baseBuffer = await getTestDataAsFileBuffer(TEST_DOC); + const zip = await JSZip.loadAsync(baseBuffer); + const documentEntry = zip.file('word/document.xml'); + if (!documentEntry) throw new Error('word/document.xml not found in fixture.'); + + const documentXml = await documentEntry.async('string'); + const patchedDocumentXml = documentXml.replace(//, ''); + + if (patchedDocumentXml === documentXml) { + throw new Error('Could not inject into first table.'); + } + + zip.file('word/document.xml', patchedDocumentXml); + return zip.generateAsync({ type: 'nodebuffer' }); +} + +describe('table bidiVisual import/export roundtrip', () => { + it('preserves w:bidiVisual in word/document.xml on export', async () => { + const patchedBuffer = await buildDocxWithBidiVisualTable(); + const inputFiles = await new DocxZipper().getDocxData(patchedBuffer, true); + const inputDocument = inputFiles.find((entry) => entry.name === 'word/document.xml')?.content; + expect(inputDocument).toContain(' entry.name === 'word/document.xml')?.content; + + expect(exportedDocument).toBeTruthy(); + expect(exportedDocument).toContain('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 literal 0 HcmV?d00001 diff --git a/tests/behavior/tests/tables/fixtures/rtl-table-1.docx b/tests/behavior/tests/tables/fixtures/rtl-table-1.docx new file mode 100644 index 0000000000000000000000000000000000000000..45e3f76f3f9136bb6dbacfe9080f745442d20c42 GIT binary patch literal 19066 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 literal 0 HcmV?d00001 diff --git a/tests/behavior/tests/tables/resize.spec.ts b/tests/behavior/tests/tables/resize.spec.ts index 60478ad769..82102ed351 100644 --- a/tests/behavior/tests/tables/resize.spec.ts +++ b/tests/behavior/tests/tables/resize.spec.ts @@ -1,7 +1,12 @@ import { test, expect } from '../../fixtures/superdoc.js'; import type { Page, Locator } from '@playwright/test'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; test.use({ config: { toolbar: 'full', showSelection: true } }); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const RTL_DOC = path.resolve(__dirname, 'fixtures/rtl-table-1.docx'); +const LTR_DOC = path.resolve(__dirname, 'fixtures/ltr-table.docx'); /** * Hover near a column boundary on the table fragment to trigger the resize overlay. @@ -11,13 +16,18 @@ async function hoverColumnBoundary(page: Page, target: number | 'right-edge') { const pos = await page.evaluate((t) => { const frag = document.querySelector('.superdoc-table-fragment[data-table-boundaries]'); if (!frag) throw new Error('No table fragment with boundaries found'); - const { columns } = JSON.parse(frag.getAttribute('data-table-boundaries')!); + const meta = JSON.parse(frag.getAttribute('data-table-boundaries')!); + const columns = meta.columns as Array<{ x: number; w: number }>; + const isRtl = meta.rtl === true; const col = t === 'right-edge' ? columns[columns.length - 1] : columns[t]; if (!col) throw new Error(`Column ${t} not found`); + const tableContentWidth = columns[columns.length - 1].x + columns[columns.length - 1].w; + const logicalBoundaryX = col.x + col.w; + const visualBoundaryX = isRtl ? tableContentWidth - logicalBoundaryX : logicalBoundaryX; const rect = frag.getBoundingClientRect(); // Hover 2px inside the right edge so the cursor stays within the table element - const offset = t === 'right-edge' ? -2 : 0; - return { x: rect.left + col.x + col.w + offset, y: rect.top + rect.height / 2 }; + const offset = t === 'right-edge' ? (isRtl ? 2 : -2) : 0; + return { x: rect.left + visualBoundaryX + offset, y: rect.top + rect.height / 2 }; }, target); await page.mouse.move(pos.x, pos.y); @@ -158,3 +168,41 @@ test('row handles are hidden during column resize drag (SD-2094)', async ({ supe await superdoc.page.mouse.up(); await superdoc.waitForStable(); }); + +test('rtl table shows resize indicator again after drag on same boundary', async ({ superdoc }) => { + await superdoc.loadDocument(RTL_DOC); + await superdoc.waitForStable(); + + await hoverColumnBoundary(superdoc.page, 0); + await superdoc.waitForStable(); + const handle = superdoc.page.locator('.resize-handle[data-boundary-type="inner"]').first(); + await expect(handle).toBeAttached({ timeout: 5000 }); + + await dragHandle(superdoc.page, handle, 40); + await superdoc.waitForStable(); + + await hoverColumnBoundary(superdoc.page, 0); + await superdoc.waitForStable(); + await expect(superdoc.page.locator('.resize-handle[data-boundary-type="inner"]').first()).toBeAttached({ + timeout: 5000, + }); +}); + +test('ltr table still shows resize indicator again after drag (guard)', async ({ superdoc }) => { + await superdoc.loadDocument(LTR_DOC); + await superdoc.waitForStable(); + + await hoverColumnBoundary(superdoc.page, 0); + await superdoc.waitForStable(); + const handle = superdoc.page.locator('.resize-handle[data-boundary-type="inner"]').first(); + await expect(handle).toBeAttached({ timeout: 5000 }); + + await dragHandle(superdoc.page, handle, 40); + await superdoc.waitForStable(); + + await hoverColumnBoundary(superdoc.page, 0); + await superdoc.waitForStable(); + await expect(superdoc.page.locator('.resize-handle[data-boundary-type="inner"]').first()).toBeAttached({ + timeout: 5000, + }); +}); diff --git a/tests/behavior/tests/tables/rtl-table-click-fallback.spec.ts b/tests/behavior/tests/tables/rtl-table-click-fallback.spec.ts new file mode 100644 index 0000000000..9c904897b8 --- /dev/null +++ b/tests/behavior/tests/tables/rtl-table-click-fallback.spec.ts @@ -0,0 +1,80 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { test, expect } from '../../fixtures/superdoc.js'; + +test.use({ config: { toolbar: 'full', showCaret: true, showSelection: true } }); + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const DOCS = [ + path.resolve(__dirname, 'fixtures/rtl-table-1.docx'), + path.resolve(__dirname, 'fixtures/rtl-table-2.docx'), +] as const; + +for (const docPath of DOCS) { + test(`rtl table click mapping stays in cell for text and empty-area clicks (${path.basename(docPath)})`, async ({ + superdoc, + }) => { + await superdoc.loadDocument(docPath); + await superdoc.waitForStable(); + + const data = await superdoc.page.evaluate(() => { + const line = document.querySelector('.superdoc-table-fragment .superdoc-line') as HTMLElement | null; + if (!line) { + return null; + } + + const lineRect = line.getBoundingClientRect(); + const linePmStart = Number(line.dataset.pmStart ?? 'NaN'); + const linePmEnd = Number(line.dataset.pmEnd ?? 'NaN'); + + // Find cell container by style signature used by DomPainter table cells. + let cell: HTMLElement | null = line.parentElement as HTMLElement | null; + while (cell) { + const style = getComputedStyle(cell); + if (style.position === 'absolute' && style.overflow === 'hidden') { + break; + } + cell = cell.parentElement as HTMLElement | null; + } + + if (!cell) { + return null; + } + + const cellRect = cell.getBoundingClientRect(); + + const textPoint = { + x: lineRect.x + Math.min(Math.max(8, lineRect.width * 0.5), Math.max(8, lineRect.width - 8)), + y: lineRect.y + lineRect.height / 2, + }; + + // Empty area in same cell: lower part of the cell, away from the text line. + const emptyPoint = { + x: Math.min(Math.max(cellRect.x + 8, textPoint.x), cellRect.right - 8), + y: Math.max(lineRect.bottom + 6, cellRect.y + cellRect.height * 0.8), + }; + + return { + linePmStart, + linePmEnd, + textPoint, + emptyPoint, + }; + }); + + expect(data).not.toBeNull(); + if (!data) return; + + await superdoc.page.mouse.click(data.textPoint.x, data.textPoint.y); + await superdoc.waitForStable(); + const afterTextClick = await superdoc.getSelection(); + expect(afterTextClick.from).toBeGreaterThanOrEqual(data.linePmStart); + expect(afterTextClick.from).toBeLessThanOrEqual(data.linePmEnd); + + await superdoc.page.mouse.click(data.emptyPoint.x, data.emptyPoint.y); + await superdoc.waitForStable(); + const afterEmptyClick = await superdoc.getSelection(); + expect(afterEmptyClick.from).toBeGreaterThanOrEqual(data.linePmStart); + expect(afterEmptyClick.from).toBeLessThanOrEqual(data.linePmEnd); + }); +} diff --git a/tests/behavior/tests/tables/rtl-table-tab-navigation.spec.ts b/tests/behavior/tests/tables/rtl-table-tab-navigation.spec.ts new file mode 100644 index 0000000000..2b96f9aad5 --- /dev/null +++ b/tests/behavior/tests/tables/rtl-table-tab-navigation.spec.ts @@ -0,0 +1,110 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { test, expect } from '../../fixtures/superdoc.js'; + +test.use({ config: { toolbar: 'full', showCaret: true, showSelection: true } }); + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const DOCS = [ + path.resolve(__dirname, 'fixtures/rtl-table-1.docx'), + path.resolve(__dirname, 'fixtures/rtl-table-2.docx'), +] as const; + +async function getSelectionLineY(page: import('@playwright/test').Page): Promise { + return page.evaluate(() => { + const editor = (window as any).editor; + const pos = editor?.state?.selection?.from; + if (typeof pos !== 'number') return null; + + const lines = Array.from(document.querySelectorAll('.superdoc-line')); + let nearest: { y: number; distance: number } | null = null; + for (const line of lines) { + const start = Number(line.dataset.pmStart ?? 'NaN'); + const end = Number(line.dataset.pmEnd ?? 'NaN'); + if (!Number.isFinite(start) || !Number.isFinite(end)) continue; + if (pos >= start && pos <= end) { + return line.getBoundingClientRect().y; + } + + const distance = pos < start ? start - pos : pos > end ? pos - end : 0; + const y = line.getBoundingClientRect().y; + if (!nearest || distance < nearest.distance) { + nearest = { y, distance }; + } + } + + return nearest?.y ?? null; + }); +} + +for (const docPath of DOCS) { + test(`rtl table Tab from visual top-left moves out of the first visual row (${path.basename(docPath)})`, async ({ + superdoc, + }) => { + await superdoc.loadDocument(docPath); + await superdoc.waitForStable(); + + const clickPoint = await superdoc.page.evaluate(() => { + const frag = document.querySelector('.superdoc-table-fragment') as HTMLElement | null; + if (!frag) return null; + const r = frag.getBoundingClientRect(); + return { x: r.x + 8, y: r.y + 8 }; + }); + + expect(clickPoint).not.toBeNull(); + if (!clickPoint) return; + + await superdoc.page.mouse.click(clickPoint.x, clickPoint.y); + await superdoc.waitForStable(); + + const before = await getSelectionLineY(superdoc.page); + expect(before).not.toBeNull(); + if (!before) return; + + await superdoc.press('Tab'); + await superdoc.waitForStable(); + + const after = await getSelectionLineY(superdoc.page); + expect(after).not.toBeNull(); + if (!after) return; + + // Word-like RTL-table behavior reported by customers: from visual top-left, + // first Tab leaves the current visual row (typically to next row's visual right cell). + // We assert this by requiring a visible downward move of the painted caret. + expect(after).toBeGreaterThan(before + 1); + }); + + test(`rtl table Shift+Tab from second visual row returns to first visual row (${path.basename(docPath)})`, async ({ + superdoc, + }) => { + await superdoc.loadDocument(docPath); + await superdoc.waitForStable(); + + const clickPoint = await superdoc.page.evaluate(() => { + const frag = document.querySelector('.superdoc-table-fragment') as HTMLElement | null; + if (!frag) return null; + const r = frag.getBoundingClientRect(); + return { x: r.x + 8, y: r.y + 8 }; + }); + + expect(clickPoint).not.toBeNull(); + if (!clickPoint) return; + + await superdoc.page.mouse.click(clickPoint.x, clickPoint.y); + await superdoc.waitForStable(); + + await superdoc.press('Tab'); + await superdoc.waitForStable(); + const afterTab = await getSelectionLineY(superdoc.page); + expect(afterTab).not.toBeNull(); + if (!afterTab) return; + + await superdoc.page.keyboard.press('Shift+Tab'); + await superdoc.waitForStable(); + const afterShiftTab = await getSelectionLineY(superdoc.page); + expect(afterShiftTab).not.toBeNull(); + if (!afterShiftTab) return; + + expect(afterShiftTab).toBeLessThan(afterTab - 1); + }); +} From 9900ff09143719bc797862a20e57e7d74167e75d Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Tue, 12 May 2026 21:24:30 -0300 Subject: [PATCH 2/2] fix(rtl-tables): emit logical resize delta and document right-edge index convention Two review-fix changes on the table-RTL stability work: 1. resize-move / resize-end events now emit the LOGICAL delta (the value applied to newWidths), restoring the pre-PR contract for external listeners (logging, analytics, undo metadata). The visual delta still lives on dragState.constrainedDelta for the in-flight preview guideline at line 591. LTR consumers see no behavior change; RTL inner-boundary consumers no longer see a sign-flipped payload. 2. Added a load-bearing comment at the right-edge boundary push explaining why the reported columnIndex is 0 in RTL and columns.length - 1 in LTR. Downstream consumers that need 'is this the outer edge?' should key off type === 'right-edge', not on the numeric column index. --- .../v1/components/TableResizeOverlay.vue | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/super-editor/src/editors/v1/components/TableResizeOverlay.vue b/packages/super-editor/src/editors/v1/components/TableResizeOverlay.vue index a9fa6b29d7..e4c0a0e731 100644 --- a/packages/super-editor/src/editors/v1/components/TableResizeOverlay.vue +++ b/packages/super-editor/src/editors/v1/components/TableResizeOverlay.vue @@ -420,7 +420,15 @@ const resizableBoundaries = computed(() => { }); } - // Add handle for right edge of table (resize last column) + // Add handle for right edge of table (resize last column). + // SD-2810 RTL note: the right-edge handle ALWAYS sits on the visual right + // side of the table. In an LTR table that means the trailing edge of the + // last logical column (`columns.length - 1`); in a `bidiVisual` RTL table + // the visually-rightmost column is the FIRST logical column (`0`) because + // cells are stored logically and rendered right-to-left. Downstream + // consumers that need "is this the table's outer edge?" should key off + // `type === 'right-edge'`, NOT `columnIndex === columns.length - 1`, + // because that index equality is LTR-only. const lastCol = columns[columns.length - 1]; const rtlRightEdgeX = tableContentWidth.value; boundaries.push({ @@ -909,12 +917,17 @@ const mouseMoveThrottle = throttle((event) => { const constrainedVisualDelta = isRtlTable.value && !dragState.value.isRightEdge ? -constrainedDelta : constrainedDelta; - // Update visual guideline only (no PM transaction yet) + // Update visual guideline only (no PM transaction yet). + // `constrainedDelta` on dragState stays in VISUAL coordinates so the preview + // guideline at line 591 (`initialBoundary.x + dragState.value.constrainedDelta`) + // tracks the cursor. The emitted `delta` below is LOGICAL: it matches the + // value applied to `newWidths` and preserves the pre-PR contract for + // external listeners (logging, analytics, undo metadata). dragState.value.constrainedDelta = constrainedVisualDelta; emit('resize-move', { columnIndex: dragState.value.columnIndex, - delta: constrainedVisualDelta, + delta: constrainedDelta, }); }, THROTTLE_INTERVAL_MS); @@ -958,11 +971,14 @@ function onDocumentMouseUp(event) { dispatchResizeTransaction(columnIndex, newWidths); } - // Always emit resize-end so the parent can clear its dragging flag + // Always emit resize-end so the parent can clear its dragging flag. + // `delta` is the LOGICAL change applied to `newWidths`, matching the + // pre-PR contract. In LTR this equals `visualFinalDelta`; in RTL inner + // boundaries it is the sign-flipped value. emit('resize-end', { columnIndex, finalWidths: newWidths, - delta: visualFinalDelta, + delta: finalDelta, }); dragState.value = null;