portabletext / portabletext/editor

Behaviors that delete text during `insert.break` crash on Android

Open
#2,275 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
277
Forks
21
Avg merge
14h 23m
Merged PRs (30d)
133

Description

Description

Custom behaviors that intercept insert.break and perform text-level deletions (e.g., raise({ type: 'delete.text' })) crash on Android with:

Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.

Steps to reproduce

  1. Create a behavior that intercepts insert.break and deletes text:
const dissolveOnBreak = defineBehavior({
  on: 'insert.break',
  guard: ({ snapshot }) => {
    const focusTextBlock = getFocusTextBlock(snapshot)
    if (!focusTextBlock) return false
    const text = focusTextBlock.node.children.map(c => c.text).join('')
    if (text === '- ') return { focusTextBlock, prefixLength: 2 }
    return false
  },
  actions: [
    (_, { focusTextBlock, prefixLength }) => [
      raise({
        type: 'delete.text',
        at: {
          anchor: { path: focusTextBlock.path, offset: 0 },
          focus: { path: focusTextBlock.path, offset: prefixLength },
        },
      }),
    ],
  ],
})
  1. Open the editor on an Android device
  2. Type - (dash space)
  3. Press Enter
  4. Crash

Expected behavior

The behavior deletes the prefix text and the block is dissolved — same as on desktop.

Actual behavior

removeChild DOM error. The editor crashes.

Analysis

On Android, beforeinput for insertParagraph is not cancelable — the browser modifies the DOM (splits the block) before PTE's behavior pipeline fires insert.break. restoreDOM reverts the structural split, but when the behavior then raises delete.text, Slate tries to reconcile the text deletion against a DOM that was already mutated by the native Enter handling.

PTE's own list dissolution behaviors avoid this because they use metadata operations (block.unset for listItem/level) which don't touch DOM text nodes. But any behavior that modifies text content during insert.break hits this race condition.

Workaround

Use block-level operations instead of text deletion:

actions: [
  (_, { focusTextBlock }) => [
    raise({
      type: 'insert.block',
      block: { /* empty block */ },
      placement: 'after',
      select: 'start',
    }),
    raise({
      type: 'delete.block',
      at: focusTextBlock.path,
    }),
  ],
]

Environment

  • Android Chrome (tested on multiple devices)
  • Works fine on desktop browsers and iOS Safari
  • PTE 6.0.0

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reproducing the behavior on Android Chrome with the provided dissolveOnBreak example, then trace the insert.break behavior pipeline around restoreDOM and the subsequent delete.text operation. Done means pressing Enter after typing '- ' deletes the prefix and dissolves the block without a removeChild crash, while preserving the existing desktop and iOS behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, typescript
Domain
frontend, mobile-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.