Skip to content

[lexical-playground] Bug Fix: EquationNode click → NodeSelection + empty-input Backspace removes#8534

Merged
etrepum merged 6 commits into
facebook:mainfrom
mayrang:fix/equation-decorator-selection
May 22, 2026
Merged

[lexical-playground] Bug Fix: EquationNode click → NodeSelection + empty-input Backspace removes#8534
etrepum merged 6 commits into
facebook:mainfrom
mayrang:fix/equation-decorator-selection

Conversation

@mayrang

@mayrang mayrang commented May 21, 2026

Copy link
Copy Markdown
Contributor

Description

EquationNode (playground's KaTeX wrapper, a DecoratorNode) had no path that turned a single click into a NodeSelection, so downstream commands (Backspace, copy, Enter, arrow keys) had nothing to act on. A block equation that ends up as the sole root child becomes uninteractive (no caret, no selection, no way to delete), and the inline LaTeX editor's empty-input Backspace was a dead end.

Fix

Mirror the ImageComponent / PollComponent pattern:

  • Register CLICK_COMMAND on EquationComponent (COMMAND_PRIORITY_LOW). A click inside the keyed editor-equation DOM sets a NodeSelection; shift+click toggles. The command-level hook fires after lexical's own selection normalization, so the NodeSelection survives the surrounding selectionchange flow.
  • Mirror isSelected onto the keyed wrapper as a focused class so the existing .editor-equation.focused outline renders.
  • Register KEY_ENTER_COMMAND: NodeSelection on a block equation inserts an empty paragraph after the equation and selects it. Inline equation inserts the paragraph after the wrapper (insertAfter on the inline equation itself would nest paragraph-in-paragraph).
  • Drop tabIndex={-1} and role="button" from KatexRenderer. With tabIndex, focus stayed inside the decorator subtree on click and KEY_BACKSPACE_COMMAND short-circuited via $isTargetWithinDecorator(event.target). Removing them matches ImageComponent — focus stays on the contenteditable root, Backspace routes through DELETE_CHARACTER_COMMANDNodeSelection.deleteNodes().
  • Wire EquationEditor's onKeyDown to an onDeleteEmpty callback: when the LaTeX input is already empty and Backspace fires, remove the host node. Block path selects the previous sibling's end (or replaces with an empty paragraph if none); inline path clears the selection and node.remove(true) to preserve the wrapper paragraph.
  • Drop the previous "auto-open EquationEditor on lone NodeSelection" effect — it would now fire on every single click. Double-click remains the only entry into edit mode.

Known limitation

Deleting an inline equation (from any path, including main on this branch's base) emits a console error: Lexical node does not exist in active editor state. Avoid using the same node references between nested closures from editorState.read/editor.update.

Rough debugging: the throw lands in $commitPendingUpdatestriggerListeners('update', ...) (LexicalUpdates.ts:714), i.e. after the equation has already been removed and the editor state is committing. One of the registered update listeners reads a node reference that is no longer in the active NodeMap. The trigger reproduces on plain main too, not just on this branch — the fix here doesn't add the bug, it surfaces it more often because the new flows reach this path.

Splitting it off as a follow-up because identifying which listener holds the stale reference means stepping through lexical's internal update / mutation listener chain, and reshaping that chain is well outside the playground change in this PR. Better as a focused follow-up against lexical core once the trigger is isolated.

Closes #8533.

Test plan

  • pnpm tsc --noEmit -p tsconfig.json clean.
  • Manual on Chrome and Safari:
    • Block equation as sole root child: click → NodeSelection, outline appears. Backspace removes the equation and replaces it with an empty paragraph (caret in the paragraph).
    • Block equation between paragraphs: Enter on the NodeSelection inserts an empty paragraph after the equation, caret inside ready for typing.
    • Inline equation: click → NodeSelection, outline. Backspace removes the equation; caret stays in the surrounding paragraph.
    • Inline equation double-click → input → delete all LaTeX → Backspace removes the host node (wrapper paragraph survives).
    • Double-click remains the only entry into edit mode.

mayrang added 3 commits May 21, 2026 10:56
…lection and remove on empty Backspace

EquationComponent had no path that turned a click or an empty-input
Backspace into a lexical-level selection / deletion. A single click on
the rendered KaTeX would leave `$getSelection()` null (lexical's
generic click handler does not promote DecoratorNode clicks to a
NodeSelection), and Backspace inside an already-empty inline editor
would sit on the host node forever.

Two changes, mirroring the ImageComponent pattern:

- Register `CLICK_COMMAND` so a click whose target is inside the keyed
  `<div|span class="editor-equation">` sets / clears a NodeSelection
  on this node (shift+click toggles). The command-level hook fires
  after lexical's own selection normalization, so the NodeSelection
  survives the surrounding `selectionchange` flow. An effect mirrors
  `isSelected` onto the keyed DOM as a `focused` class so the existing
  `.editor-equation.focused` outline rule applies. Dropped the previous
  auto-open-on-NodeSelection branch — it would now fire on every
  single click; the double-click gesture remains the only path into
  the inline `EquationEditor`.

- Wire an `onDeleteEmpty` callback through `EquationEditor`'s
  `onKeyDown`: when the input value is empty and the user presses
  Backspace, remove the host EquationNode and put the caret at the
  end of the previous sibling (if any).
…NodeSelection Enter

After the click-to-NodeSelection fix in the previous commit, pressing
Enter while an EquationNode is the lone NodeSelection routed through
RichTextExtension's generic block-decorator branch and landed on a
root-level RangeSelection element point (e.g. `{key: root, offset: 1,
type: element}` for an equation that is root's only child). The
default Enter / typing path from that element point does not surface
a new paragraph, so the editor appeared stuck — Enter and typing did
nothing visible.

Handle the case directly: when this equation is the only selected
node and the user presses Enter, insert a fresh empty paragraph right
after the equation and select it. The caret lands in a text-bearing
paragraph and subsequent typing flows normally.
… + empty-input delete

Two follow-ups on the click → NodeSelection fix:

- Drop the `tabIndex={-1}` and `role="button"` attributes from
  KatexRenderer's wrapper span. With tabIndex, focus settled inside
  the equation's decorator subtree on click and rich-text's
  `KEY_BACKSPACE_COMMAND` short-circuited via
  `$isTargetWithinDecorator(event.target)`. Removing the attributes
  matches the ImageComponent pattern — focus stays on the
  contenteditable root, so Backspace fires through
  `DELETE_CHARACTER_COMMAND` → `NodeSelection.deleteNodes()`.

- Branch `onDeleteEmpty` on `node.isInline()`. Inline path clears
  the selection and preserves the wrapper paragraph
  (`node.remove(true)`). Block path keeps the existing prevSibling /
  paragraph-replace logic. The inline path now removes the host
  EquationNode on empty-input Backspace, though commit still emits
  the same "Lexical node does not exist in active editor state"
  error that main already produces for inline-equation deletion —
  the stale ref originates in lexical's updateListener chain and is
  left as a follow-up.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label May 21, 2026
@vercel

vercel Bot commented May 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lexical Ready Ready Preview, Comment May 21, 2026 5:23am
lexical-playground Ready Ready Preview, Comment May 21, 2026 5:23am

Request Review

…or KatexRenderer attribute removal

Match the spec snapshot to the actual KatexRenderer output now that
`role="button"` and `tabIndex={-1}` are gone from the wrapper span.
…rer attribute removal

Same follow-up as the previous EquationNode spec fix — replace the
expected `<span role="button" tabindex="-1">` markup with `<span>` to
match the new KatexRenderer output.
…KatexRenderer attribute removal

Last spec with the stale `<span role="button" tabindex="-1">` markup.
@etrepum etrepum added this pull request to the merge queue May 22, 2026
Merged via the queue into facebook:main with commit 4a61ff4 May 22, 2026
45 checks passed
@etrepum etrepum mentioned this pull request May 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. extended-tests Run extended e2e tests on a PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: EquationNode click yields no selection, and empty-input Backspace leaves the node behind

2 participants