Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Breaking Change][lexical][lexical-link] Bug Fix: Collapse through inline elements in deleteCharacter #7180

Merged
merged 2 commits into from
Feb 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions packages/lexical-playground/__tests__/e2e/List.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import {
moveRight,
moveToEditorBeginning,
moveToEditorEnd,
moveToLineBeginning,
moveToParagraphEnd,
pressBackspace,
redo,
selectAll,
selectCharacters,
Expand Down Expand Up @@ -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`
<ul class="PlaygroundEditorTheme__ul">
<li class="PlaygroundEditorTheme__listItem" value="1">
<br />
</li>
</ul>
`,
);
await pressBackspace(page);
await assertHTML(
page,
html`
<p class="PlaygroundEditorTheme__paragraph"><br /></p>
`,
);
});
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`
<ul class="PlaygroundEditorTheme__ul">
<li
class="PlaygroundEditorTheme__listItem PlaygroundEditorTheme__ltr"
dir="ltr"
value="1">
<span data-lexical-text="true">Hello World</span>
</li>
</ul>
`,
);
await pressBackspace(page);
await assertHTML(
page,
html`
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true">Hello World</span>
</p>
`,
);
});
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`
<ul class="PlaygroundEditorTheme__ul">
<li
class="PlaygroundEditorTheme__listItem PlaygroundEditorTheme__ltr"
dir="ltr"
value="1">
<a
class="PlaygroundEditorTheme__link PlaygroundEditorTheme__ltr"
dir="ltr"
href="https://www.example.com">
<span data-lexical-text="true">www.example.com</span>
</a>
</li>
</ul>
`,
);
await pressBackspace(page);
await assertHTML(
page,
html`
<p class="PlaygroundEditorTheme__paragraph">
<a
class="PlaygroundEditorTheme__link PlaygroundEditorTheme__ltr"
dir="ltr"
href="https://www.example.com">
<span data-lexical-text="true">www.example.com</span>
</a>
</p>
`,
);
});
});
34 changes: 27 additions & 7 deletions packages/lexical/src/LexicalSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand All @@ -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);
}
}
}
Expand Down Expand Up @@ -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;
Expand Down
10 changes: 8 additions & 2 deletions packages/lexical/src/nodes/LexicalElementNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down