From 16e49878af5525e2a32b0d39aec5838e04636944 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Thu, 28 Nov 2024 14:39:51 -0800 Subject: [PATCH] [lexical-table] Bug Fix: Fix table tab navigation (#6880) --- .../__tests__/e2e/Tables.spec.mjs | 298 ++++++++++++++++++ .../lexical-table/src/LexicalTableNode.ts | 22 +- .../lexical-table/src/LexicalTableObserver.ts | 19 +- .../src/LexicalTableSelectionHelpers.ts | 70 +++- 4 files changed, 375 insertions(+), 34 deletions(-) diff --git a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs index d85bb4c4ccd..4fd7ca25b30 100644 --- a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs @@ -2437,6 +2437,304 @@ test.describe.parallel('Tables', () => { ); }); + test('Merged cell tab navigation forward', async ({ + page, + isPlainText, + isCollab, + }) => { + await initialize({isCollab, page}); + test.skip(isPlainText); + test.skip(isCollab); + + await focusEditor(page); + + await insertTable(page, 3, 3); + + await click(page, '.PlaygroundEditorTheme__tableCell'); + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 0, y: 1}, + true, + true, + ); + await mergeTableCells(page); + await selectCellsFromTableCords( + page, + {x: 1, y: 0}, + {x: 2, y: 0}, + true, + true, + ); + await mergeTableCells(page); + await assertHTML( + page, + html` +


+ + + + + + + + + + + + + + + + + + + +
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+ `, + ); + await click(page, '.PlaygroundEditorTheme__tableCell'); + for (const i of Array.from({length: 9 - 2}, (_v, idx) => idx)) { + await page.keyboard.type(String(i)); + await page.keyboard.press('Tab'); + } + await page.keyboard.type('Done!'); + await assertHTML( + page, + html` +


+ + + + + + + + + + + + + + + + + + + +
+

+ 0 +

+
+

+ 1 +

+
+

+ 2 +

+
+

+ 3 +

+
+

+ 4 +

+
+

+ 5 +

+
+

+ 6 +

+
+

+ Done! +

+ `, + ); + }); + + test('Merged cell tab navigation reverse', async ({ + page, + isPlainText, + isCollab, + }) => { + await initialize({isCollab, page}); + test.skip(isPlainText); + test.skip(isCollab); + + await focusEditor(page); + + await insertTable(page, 3, 3); + + await click(page, '.PlaygroundEditorTheme__tableCell'); + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 0, y: 1}, + true, + true, + ); + await mergeTableCells(page); + await selectCellsFromTableCords( + page, + {x: 1, y: 0}, + {x: 2, y: 0}, + true, + true, + ); + await mergeTableCells(page); + await assertHTML( + page, + html` +


+ + + + + + + + + + + + + + + + + + + +
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+ `, + ); + await click(page, ':nth-match(.PlaygroundEditorTheme__tableCell, 7)'); + for (const i of Array.from({length: 9 - 2}, (_v, idx) => idx)) { + await page.keyboard.type(String(i)); + await page.keyboard.down('Shift'); + await page.keyboard.press('Tab'); + await page.keyboard.up('Shift'); + } + await page.keyboard.type('Done!'); + await assertHTML( + page, + html` +

+ Done! +

+ + + + + + + + + + + + + + + + + + + +
+

+ 6 +

+
+

+ 5 +

+
+

+ 4 +

+
+

+ 3 +

+
+

+ 2 +

+
+

+ 1 +

+
+

+ 0 +

+
+


+ `, + ); + }); + test('Merge with content', async ({page, isPlainText, isCollab}) => { await initialize({isCollab, page}); test.skip(isPlainText); diff --git a/packages/lexical-table/src/LexicalTableNode.ts b/packages/lexical-table/src/LexicalTableNode.ts index 838758e2e83..1de74231576 100644 --- a/packages/lexical-table/src/LexicalTableNode.ts +++ b/packages/lexical-table/src/LexicalTableNode.ts @@ -34,7 +34,10 @@ import {PIXEL_VALUE_REG_EXP} from './constants'; import {$isTableCellNode, TableCellNode} from './LexicalTableCellNode'; import {TableDOMCell, TableDOMTable} from './LexicalTableObserver'; import {TableRowNode} from './LexicalTableRowNode'; -import {getTable} from './LexicalTableSelectionHelpers'; +import { + $getNearestTableCellInTableFromDOMNode, + getTable, +} from './LexicalTableSelectionHelpers'; export type SerializedTableNode = Spread< { @@ -270,17 +273,16 @@ export class TableNode extends ElementNode { continue; } - const x = row.findIndex((cell) => { - if (!cell) { - return; + for (let x = 0; x < row.length; x++) { + const cell = row[x]; + if (cell == null) { + continue; } const {elem} = cell; - const cellNode = $getNearestNodeFromDOMNode(elem); - return cellNode === tableCellNode; - }); - - if (x !== -1) { - return {x, y}; + const cellNode = $getNearestTableCellInTableFromDOMNode(this, elem); + if (cellNode !== null && tableCellNode.is(cellNode)) { + return {x, y}; + } } } diff --git a/packages/lexical-table/src/LexicalTableObserver.ts b/packages/lexical-table/src/LexicalTableObserver.ts index 0d148257121..059c471b96d 100644 --- a/packages/lexical-table/src/LexicalTableObserver.ts +++ b/packages/lexical-table/src/LexicalTableObserver.ts @@ -17,7 +17,6 @@ import { $createRangeSelection, $createTextNode, $getEditor, - $getNearestNodeFromDOMNode, $getNodeByKey, $getRoot, $getSelection, @@ -38,7 +37,7 @@ import { type TableSelection, } from './LexicalTableSelection'; import { - $findTableNode, + $getNearestTableCellInTableFromDOMNode, $updateDOMForSelection, getTable, getTableElement, @@ -351,13 +350,15 @@ export class TableObserver { this.focusY = cellY; if (this.isHighlightingCells) { - const focusTableCellNode = $getNearestNodeFromDOMNode(cell.elem); + const focusTableCellNode = $getNearestTableCellInTableFromDOMNode( + tableNode, + cell.elem, + ); if ( this.tableSelection != null && this.anchorCellNodeKey != null && - $isTableCellNode(focusTableCellNode) && - tableNode.is($findTableNode(focusTableCellNode)) + focusTableCellNode !== null ) { this.focusCellNodeKey = focusTableCellNode.getKey(); this.tableSelection = $createTableSelectionFrom( @@ -407,9 +408,13 @@ export class TableObserver { this.anchorX = cell.x; this.anchorY = cell.y; - const anchorTableCellNode = $getNearestNodeFromDOMNode(cell.elem); + const {tableNode} = this.$lookup(); + const anchorTableCellNode = $getNearestTableCellInTableFromDOMNode( + tableNode, + cell.elem, + ); - if ($isTableCellNode(anchorTableCellNode)) { + if (anchorTableCellNode !== null) { const anchorNodeKey = anchorTableCellNode.getKey(); this.tableSelection = this.tableSelection != null diff --git a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts index 08c6f1f4fd4..7959f1cfd75 100644 --- a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts +++ b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts @@ -15,6 +15,7 @@ import type { } from './LexicalTableSelection'; import type { BaseSelection, + EditorState, ElementFormatType, ElementNode, LexicalCommand, @@ -661,23 +662,17 @@ export function applyTableHandlers( } const tableCellNode = $findCellNode(selection.anchor.getNode()); - if (tableCellNode === null) { + if ( + tableCellNode === null || + !tableNode.is($findTableNode(tableCellNode)) + ) { return false; } stopEvent(event); - - const currentCords = tableNode.getCordsFromCellNode( + $selectAdjacentCell( tableCellNode, - tableObserver.table, - ); - - selectTableNodeInDirection( - tableObserver, - tableNode, - currentCords.x, - currentCords.y, - !event.shiftKey ? 'forward' : 'backward', + event.shiftKey ? 'previous' : 'next', ); return true; @@ -975,13 +970,13 @@ export function applyTableHandlers( domSelection.focusNode, ); const isFocusOutside = - focusNode && !tableNode.is($findTableNode(focusNode)); + focusNode && !tableNode.isParentOf(focusNode); const anchorNode = $getNearestNodeFromDOMNode( domSelection.anchorNode, ); const isAnchorInside = - anchorNode && tableNode.is($findTableNode(anchorNode)); + anchorNode && tableNode.isParentOf(anchorNode); if ( isFocusOutside && @@ -1302,6 +1297,36 @@ export function $removeHighlightStyleToTable( }); } +function $selectAdjacentCell( + tableCellNode: TableCellNode, + direction: 'next' | 'previous', +) { + const siblingMethod = + direction === 'next' ? 'getNextSibling' : 'getPreviousSibling'; + const childMethod = direction === 'next' ? 'getFirstChild' : 'getLastChild'; + const sibling = tableCellNode[siblingMethod](); + if ($isElementNode(sibling)) { + return sibling.selectEnd(); + } + const parentRow = $findMatchingParent(tableCellNode, $isTableRowNode); + invariant(parentRow !== null, 'selectAdjacentCell: Cell not in table row'); + for ( + let nextRow = parentRow[siblingMethod](); + $isTableRowNode(nextRow); + nextRow = nextRow[siblingMethod]() + ) { + const child = nextRow[childMethod](); + if ($isElementNode(child)) { + return child.selectEnd(); + } + } + const parentTable = $findMatchingParent(parentRow, $isTableNode); + invariant(parentTable !== null, 'selectAdjacentCell: Row not in table'); + return direction === 'next' + ? parentTable.selectNext() + : parentTable.selectPrevious(); +} + type Direction = 'backward' | 'forward' | 'up' | 'down'; const selectTableNodeInDirection = ( @@ -1722,11 +1747,11 @@ function $handleArrowKey( ? selection.getNodes()[selection.getNodes().length - 1] : selection.getNodes()[0]; if (selectedNode) { - const tableCellNode = $findMatchingParent( + const tableCellNode = $findParentTableCellNodeInTable( + tableNode, selectedNode, - $isTableCellNode, ); - if (tableCellNode && tableNode.isParentOf(tableCellNode)) { + if (tableCellNode !== null) { const firstDescendant = tableNode.getFirstDescendant(); const lastDescendant = tableNode.getLastDescendant(); if (!firstDescendant || !lastDescendant) { @@ -2239,3 +2264,14 @@ export function $getObserverCellFromCellNodeOrThrow( tableObserver.table, ); } + +export function $getNearestTableCellInTableFromDOMNode( + tableNode: TableNode, + startingDOM: Node, + editorState?: EditorState, +) { + return $findParentTableCellNodeInTable( + tableNode, + $getNearestNodeFromDOMNode(startingDOM, editorState), + ); +}