[lexical] Bug Fix: Prevent deleteLine from removing adjacent block decorators#8744
[lexical] Bug Fix: Prevent deleteLine from removing adjacent block decorators#8744mayrang wants to merge 2 commits into
Conversation
…corators
When deleteLine calls modify('extend', …, 'lineboundary') from an empty
block, the lineboundary extension can overreach past a block-level
decorator (e.g. HorizontalRuleNode, PageBreakNode). The subsequent
removeText() then removes the decorator along with the empty block.
After the lineboundary extend, check whether the resulting selection
includes any non-inline decorator. If so, collapse back to the anchor
and delegate to deleteCharacter, which already handles decorator
boundaries correctly — it removes the empty anchor block and selects
the decorator via NodeSelection.
Fixes facebook#8722
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
potatowagon
left a comment
There was a problem hiding this comment.
Reviewed by Navi (Tater Thoughts Bobblehead) on behalf of @potatowagon.
Assessment: LGTM ✅ — Ready to approve
This is a well-crafted bug fix for #8722. The approach is sound and minimal.
What I checked:
-
Logic correctness: The fix correctly identifies when
deleteLineoverreaches past block-level decorators after the lineboundary extend. By collapsing the selection back to the anchor and delegating todeleteCharacter, it leverages the existing decorator-boundary logic rather than reimplementing it. This is the right abstraction layer to handle this. -
Edge cases covered by tests:
- Backward delete from empty ListItem with preceding decorator ✅
- Backward delete from empty paragraph with preceding decorator ✅
- Forward delete from empty ListItem with following decorator ✅
- Backward delete should not remove preceding empty paragraph ✅
- Backward delete from list item preceded by non-empty paragraph (normal merge behavior preserved) ✅
-
No regressions: The condition (
this.getNodes().some(node => $isDecoratorNode(node) && !node.isInline())) is narrow — it only triggers when the extended selection actually contains a block-level decorator. Normal text deletion, inline decorator handling, and paragraph merging are unaffected. -
Placement in control flow: The new branch sits between the "anchor === focus after extend" early return (which delegates to deleteCharacter) and the
removeText()fallback. This is the correct insertion point — it catches the case where the extend DID move focus but overshot into a decorator. -
www compat: No API changes, no export modifications, no changed function signatures. Purely behavioral fix in internal selection logic. Safe for internal consumers (MLCComposer, XDSRichText, EPS).
-
CI: All checks green — core-tests (unit Node 22/24, browser, integrity), e2e canary, CLA, Vercel deploys all pass.
One minor observation: The .some() call iterates all selected nodes on every deleteLine. For typical selections this is negligible, but in theory a very large selection could be slightly slower. Not a concern for real-world usage since deleteLine operates on single lines.
Ship it. 🚀
Description
deleteLine(Cmd+Backspace / Cmd+Delete) from an empty block removes an adjacent block-level decorator (HorizontalRule, PageBreak) when it should only delete the empty block. Themodify('extend', isBackward, 'lineboundary')call extends the selection past block boundaries into the adjacent decorator, and the subsequentremoveText()deletes the decorator along with the empty block.After the lineboundary extend, check whether the resulting selection includes any non-inline
DecoratorNode. If so, collapse back to the anchor and delegate todeleteCharacter(), which already handles decorator boundaries correctly — it removes the empty anchor block and selects the decorator viaNodeSelection.Closes #8722
Test plan
pnpm vitest run --project unit -t "DELETE_LINE_COMMAND"— 6 tests pass:deleteCharacterbackward from empty ListItem preserves preceding decoratordeleteLinebackward/forward from empty ListItem and empty paragraph preserves adjacent decorator (3 cases viatest.for)deleteLinebackward from empty ListItem preserves preceding empty paragraph (regression guard)deleteLinebackward with preceding non-empty paragraph merges normallypnpm vitest run --project unit— full suite passes (3337 tests).pnpm tsc --noEmit, prettier / eslint clean.