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;