Repository navigation
Description
Steps
- Insert a table, then a blockquote right after it.
- Leave the blockquote empty and put the caret in it.
- Press Backspace.
Expected
The empty blockquote is removed, the same way an empty paragraph after a table is removed, or an empty blockquote after a paragraph.
Actual
The caret jumps into the last table cell and the empty blockquote stays. Every further Backspace deletes text in the table, so there is no way to remove the blockquote with the keyboard.
Reproduced on main (f0b3563, Lexical 0.53):
| Content before Backspace | After Backspace |
|---|---|
<p>a</p><blockquote><p></p></blockquote> |
<p>a</p>: quote removed ✓ |
<table>…</table><blockquote><p></p></blockquote> |
quote still there, caret in tablecell > paragraph > text ✗ |
<table>…</table><blockquote></blockquote> |
same ✗ |
Cause
With the caret at the start of the blockquote's only paragraph, RangeSelection.deleteCharacter takes the shadow-root branch: the previous sibling of the quote is a shadow root (the table), so Lexical moves the selection into the table and removes only anchor.origin, the empty paragraph. The quote is now empty, and Lexxy's $ensureQuoteHasParagraphChild transform (src/extensions/format_escape_extension.js) appends a new paragraph, so the quote survives.
A possible fix is to handle Backspace in an empty blockquote whose previous sibling is a table before Lexical's default, removing the whole quote (or turning it into a paragraph, as Backspace at the start of a quote does elsewhere).
Test
import { afterEach, expect, test } from "vitest"
import { $getRoot, $getSelection, KEY_BACKSPACE_COMMAND } from "lexical"
import { $isQuoteNode } from "@lexical/rich-text"
import { createTestEditor, destroyTestEditor, setContent, tick } from "../helpers/editor_helper"
let editorElement
afterEach(async () => { await destroyTestEditor(editorElement) })
test.each([
[ "after paragraph", "<p>a</p><blockquote><p></p></blockquote>" ],
[ "after table", "<table><tr><td>a</td></tr></table><blockquote><p></p></blockquote>" ]
])("Backspace removes an empty blockquote %s", async (_name, html) => {
editorElement = await createTestEditor()
await setContent(editorElement, html)
editorElement.editor.update(() => {
$getRoot().getChildren().find($isQuoteNode).getFirstChild().select(0, 0)
}, { discrete: true })
editorElement.editor.dispatchCommand(KEY_BACKSPACE_COMMAND,
new KeyboardEvent("keydown", { key: "Backspace", cancelable: true }))
await tick()
editorElement.editor.getEditorState().read(() => {
expect($getRoot().getChildren().some($isQuoteNode)).toBe(false)
expect($getSelection().anchor.getNode().getParents().map(n => n.getType())).not.toContain("tablecell")
})
})
Activity