diff --git a/packages/lexical-playground/__tests__/e2e/List.spec.mjs b/packages/lexical-playground/__tests__/e2e/List.spec.mjs index d4c1bc250e8..b9a67ac932e 100644 --- a/packages/lexical-playground/__tests__/e2e/List.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/List.spec.mjs @@ -14,7 +14,9 @@ import { moveRight, moveToEditorBeginning, moveToEditorEnd, + moveToLineBeginning, moveToParagraphEnd, + pressBackspace, redo, selectAll, selectCharacters, @@ -1946,4 +1948,95 @@ test.describe.parallel('Nested List', () => { `, ); }); + test('collapseAtStart for trivial bullet list', async ({page}) => { + await focusEditor(page); + await toggleBulletList(page); + await assertHTML( + page, + html` + + `, + ); + await pressBackspace(page); + await assertHTML( + page, + html` +


+ `, + ); + }); + test('collapseAtStart for bullet list with text', async ({page}) => { + await focusEditor(page); + await toggleBulletList(page); + await page.keyboard.type('Hello World'); + await moveToLineBeginning(page); + await assertHTML( + page, + html` + + `, + ); + await pressBackspace(page); + await assertHTML( + page, + html` +

+ Hello World +

+ `, + ); + }); + test('collapseAtStart for bullet list with text inside autolink', async ({ + page, + }) => { + await focusEditor(page); + await toggleBulletList(page); + await page.keyboard.type('www.example.com'); + await moveToLineBeginning(page); + await assertHTML( + page, + html` + + `, + ); + await pressBackspace(page); + await assertHTML( + page, + html` +

+ + www.example.com + +

+ `, + ); + }); }); diff --git a/packages/lexical/src/LexicalSelection.ts b/packages/lexical/src/LexicalSelection.ts index 08ed6571418..d3c7c5665d6 100644 --- a/packages/lexical/src/LexicalSelection.ts +++ b/packages/lexical/src/LexicalSelection.ts @@ -1842,11 +1842,7 @@ export class RangeSelection implements BaseSelection { $updateCaretSelectionForUnicodeCharacter(this, isBackward); } else if (isBackward && anchor.offset === 0) { // Special handling around rich text nodes - const element = - anchor.type === 'element' - ? anchor.getNode() - : anchor.getNode().getParentOrThrow(); - if (element.collapseAtStart(this)) { + if ($collapseAtStart(this, anchor.getNode())) { return; } } @@ -1863,9 +1859,9 @@ export class RangeSelection implements BaseSelection { if ( anchorNode.isEmpty() && $isRootNode(anchorNode.getParent()) && - anchorNode.getIndexWithinParent() === 0 + anchorNode.getPreviousSibling() === null ) { - anchorNode.collapseAtStart(this); + $collapseAtStart(this, anchorNode); } } } @@ -1972,6 +1968,30 @@ export function $getCharacterOffsets( return [getCharacterOffset(anchor), getCharacterOffset(focus)]; } +function $collapseAtStart( + selection: RangeSelection, + startNode: LexicalNode, +): boolean { + for ( + let node: null | LexicalNode = startNode; + node; + node = node.getParent() + ) { + if ($isElementNode(node)) { + if (node.collapseAtStart(selection)) { + return true; + } + if (!node.isInline()) { + break; + } + } + if (node.getPreviousSibling()) { + break; + } + } + return false; +} + function $swapPoints(selection: RangeSelection): void { const focus = selection.focus; const anchor = selection.anchor; diff --git a/packages/lexical/src/nodes/LexicalElementNode.ts b/packages/lexical/src/nodes/LexicalElementNode.ts index f53f04560cf..11872243f5e 100644 --- a/packages/lexical/src/nodes/LexicalElementNode.ts +++ b/packages/lexical/src/nodes/LexicalElementNode.ts @@ -880,9 +880,15 @@ export class ElementNode extends LexicalNode { return true; } /* - * This method controls the behavior of a the node during backwards + * This method controls the behavior of the node during backwards * deletion (i.e., backspace) when selection is at the beginning of - * the node (offset 0) + * the node (offset 0). You may use this to have the node replace + * itself, change its state, or do nothing. When you do make such + * a change, you should return true. + * + * When true is returned, the collapse phase will stop. + * When false is returned, and isInline() is true, and getPreviousSibling() is null, + * then this function will be called on its parent. */ collapseAtStart(selection: RangeSelection): boolean { return false;