diff --git a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs
index 105ed967e1e..af4f7c94832 100644
--- a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs
+++ b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs
@@ -41,6 +41,7 @@ import {
selectCellsFromTableCords,
selectFromAdditionalStylesDropdown,
selectFromAlignDropdown,
+ selectorBoundingBox,
setBackgroundColor,
test,
toggleColumnHeader,
@@ -1521,6 +1522,243 @@ test.describe.parallel('Tables', () => {
);
});
+ test('Resize merged cells width (1)', async ({
+ page,
+ isPlainText,
+ isCollab,
+ }) => {
+ await initialize({isCollab, page});
+ test.skip(isPlainText);
+ if (IS_COLLAB) {
+ // The contextual menu positioning needs fixing (it's hardcoded to show on the right side)
+ page.setViewportSize({height: 1000, width: 3000});
+ }
+
+ await focusEditor(page);
+
+ await insertTable(page, 3, 3);
+ await click(page, '.PlaygroundEditorTheme__tableCell');
+ await selectCellsFromTableCords(
+ page,
+ {x: 0, y: 0},
+ {x: 1, y: 1},
+ true,
+ false,
+ );
+ await mergeTableCells(page);
+ await click(page, 'td:nth-child(3) > .PlaygroundEditorTheme__paragraph');
+ const resizerBoundingBox = await selectorBoundingBox(
+ page,
+ '.TableCellResizer__resizer:first-child',
+ );
+ const x = resizerBoundingBox.x + resizerBoundingBox.width / 2;
+ const y = resizerBoundingBox.y + resizerBoundingBox.height / 2;
+ await page.mouse.move(x, y);
+ await page.mouse.down();
+ await page.mouse.move(x + 50, y);
+ await page.mouse.up();
+
+ await assertHTML(
+ page,
+ html`
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+ |
+
+
+
+ `,
+ );
+ });
+
+ test('Resize merged cells width (2)', async ({
+ page,
+ isPlainText,
+ isCollab,
+ }) => {
+ await initialize({isCollab, page});
+ test.skip(isPlainText);
+ if (IS_COLLAB) {
+ // The contextual menu positioning needs fixing (it's hardcoded to show on the right side)
+ page.setViewportSize({height: 1000, width: 3000});
+ }
+
+ await focusEditor(page);
+
+ await insertTable(page, 3, 3);
+ await click(page, '.PlaygroundEditorTheme__tableCell');
+ await selectCellsFromTableCords(
+ page,
+ {x: 0, y: 0},
+ {x: 1, y: 1},
+ true,
+ false,
+ );
+ await mergeTableCells(page);
+ await click(page, 'th');
+ const resizerBoundingBox = await selectorBoundingBox(
+ page,
+ '.TableCellResizer__resizer:first-child',
+ );
+ const x = resizerBoundingBox.x + resizerBoundingBox.width / 2;
+ const y = resizerBoundingBox.y + resizerBoundingBox.height / 2;
+ await page.mouse.move(x, y);
+ await page.mouse.down();
+ await page.mouse.move(x + 50, y);
+ await page.mouse.up();
+
+ await assertHTML(
+ page,
+ html`
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+ |
+
+
+
+ `,
+ );
+ });
+
+ test('Resize merged cells height', async ({page, isPlainText, isCollab}) => {
+ await initialize({isCollab, page});
+ test.skip(isPlainText);
+ if (IS_COLLAB) {
+ // The contextual menu positioning needs fixing (it's hardcoded to show on the right side)
+ page.setViewportSize({height: 1000, width: 3000});
+ }
+
+ await focusEditor(page);
+
+ await insertTable(page, 3, 3);
+ await click(page, '.PlaygroundEditorTheme__tableCell');
+ await selectCellsFromTableCords(
+ page,
+ {x: 0, y: 0},
+ {x: 1, y: 1},
+ true,
+ false,
+ );
+ await mergeTableCells(page);
+ await click(page, 'th');
+ const resizerBoundingBox = await selectorBoundingBox(
+ page,
+ '.TableCellResizer__resizer:nth-child(2)',
+ );
+ const x = resizerBoundingBox.x + resizerBoundingBox.width / 2;
+ const y = resizerBoundingBox.y + resizerBoundingBox.height / 2;
+ await page.mouse.move(x, y);
+ await page.mouse.down();
+ await page.mouse.move(x, y + 50);
+ await page.mouse.up();
+
+ await assertHTML(
+ page,
+ html`
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+ |
+
+
+
+ `,
+ undefined,
+ {
+ ignoreClasses: false,
+ ignoreInlineStyles: false,
+ },
+ (actualHtml) =>
+ // flaky fix: +- 1px for the height assertion
+ actualHtml.replace(
+ '',
+ '
',
+ ),
+ );
+ });
+
test('Merge/unmerge cells (1)', async ({page, isPlainText, isCollab}) => {
await initialize({isCollab, page});
test.skip(isPlainText);
diff --git a/packages/lexical-playground/__tests__/utils/index.mjs b/packages/lexical-playground/__tests__/utils/index.mjs
index c9097052429..990e1e99799 100644
--- a/packages/lexical-playground/__tests__/utils/index.mjs
+++ b/packages/lexical-playground/__tests__/utils/index.mjs
@@ -241,6 +241,7 @@ export async function assertHTML(
ignoreClasses,
ignoreInlineStyles,
'page',
+ actualHtmlModificationsCallback,
);
}
}
diff --git a/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx b/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx
index bc96ffe81e0..43290761f08 100644
--- a/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx
+++ b/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx
@@ -5,7 +5,12 @@
* LICENSE file in the root directory of this source tree.
*
*/
-import type {TableDOMCell} from '@lexical/table';
+import type {
+ TableCellNode,
+ TableDOMCell,
+ TableMapType,
+ TableMapValueType,
+} from '@lexical/table';
import type {LexicalEditor} from 'lexical';
import './index.css';
@@ -13,13 +18,12 @@ import './index.css';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {useLexicalEditable} from '@lexical/react/useLexicalEditable';
import {
- $getTableColumnIndexFromTableCellNode,
+ $computeTableMapSkipCellCheck,
$getTableNodeFromLexicalNodeOrThrow,
$getTableRowIndexFromTableCellNode,
$isTableCellNode,
$isTableRowNode,
getDOMCellFromTarget,
- TableCellNode,
} from '@lexical/table';
import {calculateZoomLevel} from '@lexical/utils';
import {$getNearestNodeFromDOMNode} from 'lexical';
@@ -149,7 +153,7 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element {
};
const updateRowHeight = useCallback(
- (newHeight: number) => {
+ (heightChange: number) => {
if (!activeCell) {
throw new Error('TableCellResizer: Expected active cell.');
}
@@ -178,6 +182,17 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element {
throw new Error('Expected table row');
}
+ let height = tableRow.getHeight();
+ if (height === undefined) {
+ const rowCells = tableRow.getChildren();
+ height = Math.min(
+ ...rowCells.map(
+ (cell) => getCellNodeHeight(cell, editor) ?? Infinity,
+ ),
+ );
+ }
+
+ const newHeight = Math.max(height + heightChange, MIN_ROW_HEIGHT);
tableRow.setHeight(newHeight);
},
{tag: 'skip-scroll-into-view'},
@@ -186,8 +201,50 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element {
[activeCell, editor],
);
+ const getCellNodeWidth = (
+ cell: TableCellNode,
+ activeEditor: LexicalEditor,
+ ): number | undefined => {
+ const width = cell.getWidth();
+ if (width !== undefined) {
+ return width;
+ }
+
+ const domCellNode = activeEditor.getElementByKey(cell.getKey());
+ if (domCellNode == null) {
+ return undefined;
+ }
+ const computedStyle = getComputedStyle(domCellNode);
+ return (
+ domCellNode.clientWidth -
+ parseFloat(computedStyle.paddingLeft) -
+ parseFloat(computedStyle.paddingRight)
+ );
+ };
+
+ const getCellNodeHeight = (
+ cell: TableCellNode,
+ activeEditor: LexicalEditor,
+ ): number | undefined => {
+ const domCellNode = activeEditor.getElementByKey(cell.getKey());
+ return domCellNode?.clientHeight;
+ };
+
+ const getCellColumnIndex = (
+ tableCellNode: TableCellNode,
+ tableMap: TableMapType,
+ ) => {
+ for (let row = 0; row < tableMap.length; row++) {
+ for (let column = 0; column < tableMap[row].length; column++) {
+ if (tableMap[row][column].cell === tableCellNode) {
+ return column;
+ }
+ }
+ }
+ };
+
const updateColumnWidth = useCallback(
- (newWidth: number) => {
+ (widthChange: number) => {
if (!activeCell) {
throw new Error('TableCellResizer: Expected active cell.');
}
@@ -199,48 +256,31 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element {
}
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
+ const [tableMap] = $computeTableMapSkipCellCheck(
+ tableNode,
+ null,
+ null,
+ );
+ const columnIndex = getCellColumnIndex(tableCellNode, tableMap);
+ if (columnIndex === undefined) {
+ throw new Error('TableCellResizer: Table column not found.');
+ }
- const tableColumnIndex =
- $getTableColumnIndexFromTableCellNode(tableCellNode);
-
- const tableRows = tableNode.getChildren();
-
- for (let r = 0; r < tableRows.length; r++) {
- const tableRow = tableRows[r];
-
- if (!$isTableRowNode(tableRow)) {
- throw new Error('Expected table row');
- }
-
- const rowCells = tableRow.getChildren();
- const rowCellsSpan = rowCells.map((cell) => cell.getColSpan());
-
- const aggregatedRowSpans = rowCellsSpan.reduce(
- (rowSpans: number[], cellSpan) => {
- const previousCell = rowSpans[rowSpans.length - 1] ?? 0;
- rowSpans.push(previousCell + cellSpan);
- return rowSpans;
- },
- [],
- );
- const rowColumnIndexWithSpan = aggregatedRowSpans.findIndex(
- (cellSpan: number) => cellSpan > tableColumnIndex,
- );
-
+ for (let row = 0; row < tableMap.length; row++) {
+ const cell: TableMapValueType = tableMap[row][columnIndex];
if (
- rowColumnIndexWithSpan >= rowCells.length ||
- rowColumnIndexWithSpan < 0
+ cell.startRow === row &&
+ (columnIndex === tableMap[row].length - 1 ||
+ tableMap[row][columnIndex].cell !==
+ tableMap[row][columnIndex + 1].cell)
) {
- throw new Error('Expected table cell to be inside of table row.');
- }
-
- const tableCell = rowCells[rowColumnIndexWithSpan];
-
- if (!$isTableCellNode(tableCell)) {
- throw new Error('Expected table cell');
+ const width = getCellNodeWidth(cell.cell, editor);
+ if (width === undefined) {
+ continue;
+ }
+ const newWidth = Math.max(width + widthChange, MIN_COLUMN_WIDTH);
+ cell.cell.setWidth(newWidth);
}
-
- tableCell.setWidth(newWidth);
}
},
{tag: 'skip-scroll-into-view'},
@@ -268,33 +308,11 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element {
const zoom = calculateZoomLevel(event.target as Element);
if (isHeightChanging(direction)) {
- const height = activeCell.elem.getBoundingClientRect().height;
- const heightChange = Math.abs(event.clientY - y) / zoom;
-
- const isShrinking = direction === 'bottom' && y > event.clientY;
-
- updateRowHeight(
- Math.max(
- isShrinking ? height - heightChange : heightChange + height,
- MIN_ROW_HEIGHT,
- ),
- );
+ const heightChange = (event.clientY - y) / zoom;
+ updateRowHeight(heightChange);
} else {
- const computedStyle = getComputedStyle(activeCell.elem);
- let width = activeCell.elem.clientWidth; // width with padding
- width -=
- parseFloat(computedStyle.paddingLeft) +
- parseFloat(computedStyle.paddingRight);
- const widthChange = Math.abs(event.clientX - x) / zoom;
-
- const isShrinking = direction === 'right' && x > event.clientX;
-
- updateColumnWidth(
- Math.max(
- isShrinking ? width - widthChange : widthChange + width,
- MIN_COLUMN_WIDTH,
- ),
- );
+ const widthChange = (event.clientX - x) / zoom;
+ updateColumnWidth(widthChange);
}
resetState();
diff --git a/packages/lexical-table/src/index.ts b/packages/lexical-table/src/index.ts
index c9f3964cf07..41462c0237e 100644
--- a/packages/lexical-table/src/index.ts
+++ b/packages/lexical-table/src/index.ts
@@ -34,6 +34,8 @@ export {
TableRowNode,
} from './LexicalTableRowNode';
export type {
+ TableMapType,
+ TableMapValueType,
TableSelection,
TableSelectionShape,
} from './LexicalTableSelection';