Skip to content

Backspace in an empty blockquote after a table moves the caret into the table and keeps the quote #1380

Open

Description

@christopher-valiido

Steps

  1. Insert a table, then a blockquote right after it.
  2. Leave the blockquote empty and put the caret in it.
  3. 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions