Skip to content

fix(html): record what an Android keyboard types into a document - #905

Merged
andiwand merged 3 commits into
mainfrom
fix/composition-records-its-edit
Sep 19, 2026
Merged

andiwand merged 3 commits into
mainfrom
fix/composition-records-its-edit

Conversation

@andiwand

@andiwand andiwand commented Sep 18, 2026

Copy link
Copy Markdown
Member

An Android keyboard (Gboard) holds a composition open on the word under the caret for as long as the caret stays there. The document editor (document.js, the odt, docx and pptx view) lost that input in two ways:

  1. On compositionend it called setRunText(target, target.textContent). setRunText reads before from the same run, so the two texts were always equal, and the step was dropped as a no-op.
  2. While composing !== null, it let every beforeinput through without a record, including insertText, insertParagraph and the deletes, which it can cancel. The browser applied them natively. This is also why Enter split a paragraph under scope paragraph.

The page showed the typed text, but getOperations() stayed empty, and a save wrote the document as it was. Found while integrating 7.0.0 in OpenDocument.droid (opendocument-app/OpenDocument.droid#662).

The fix

  • The editor notes a run's text before the browser writes into it (on compositionstart, and on each beforeinput that cannot be cancelled). After the input, it records the change as one step. record() puts the step on the log without applying it, because the page shows it already. Rewriting the text node would break the composition.
  • Only an event that cannot be cancelled goes through to the browser. A cancelable event goes through the editor even while a composition is open, so the scope gate holds there too.
  • Before the editor acts, and before an undo, a redo, format or toggle, it records what the browser wrote. This keeps the log in order.
  • compositionend still reads the watched runs back, for a browser that writes a composition without an input for it.

Checked

  • test/browser/text/tests.html: a new group, "a composition", has 21 checks. All 190 checks pass in Chrome. Against main, the group's first check fails, and the next one throws.
  • On an Android 12 emulator with Gboard, in the droid app's WebView, with the fixed script swapped into the page:
    • A real composition (non-cancelable insertCompositionText from tapping the on-screen keys) is recorded key by key.
    • The space that commits the word is recorded, and undo takes it back.
    • With adb input text, the cancelable insertText per character is taken by the editor.
    • Backspace is recorded.
    • Enter splits the paragraph under scope document, and is refused as outOfScope under scope paragraph.

Seen, not fixed here

  • Enter after a trailing space splits one character early. Chrome reports the target range of insertParagraph at offset 6 in "TextHi " while the selection is at 7, because it does not render the trailing space. So the space moves into the new paragraph. This has nothing to do with compositions, and a desktop Chrome probably does the same.
  • The trailing space of a committed word is U+00A0. The browser writes it that way inside a run, and the recorded text carries it as-is. The old code read the run back the same way.

The editor's inline copy in the reference outputs changes, so the pins probably need a follow-up advance, as after #897.

An Android keyboard holds a composition open on the word under the caret
for as long as the caret stays there. The document editor lost that input
in two ways:

- On `compositionend` it compared a run with itself, so the step was
  always empty and was dropped.
- While a composition was open, it let every `beforeinput` through
  without a record, including the keys, Enter and Backspace that it can
  cancel. So scope `paragraph` did not hold there either.

The page showed the text, but the log was empty, and a save wrote the
document as it was.

Now the editor notes a run's text before the browser writes into it, and
records the change after the `input`. Only an event that cannot be
cancelled goes through to the browser. Before the editor acts, and before
an undo or a redo, it records what the browser wrote.

Checked with the browser checks and on an Android 12 emulator with Gboard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F6AQY2k86AaPq12nxBfN7A
andiwand and others added 2 commits September 19, 2026 08:41
# Conflicts:
#	CHANGELOG.md
#	src/odr/internal/html/frontend/document.js
The `beforeinput` handler started a gesture and then recorded what the
browser wrote without an `input`, so that text and the key after it had
one gesture, and one undo took back both. The handler now records the
text first.

WebKit can fire a composition `beforeinput` that can be cancelled. The
editor refused it, so the browser typed nothing. The editor now lets
every composition type through.

`committed()` no longer drops text that the browser wrote but the log
does not hold yet, because the save did not contain that text.

The comments, the design notes and the changelog entry are shorter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C725vurM2ZqBY6ddV2FMZ7
@andiwand
andiwand merged commit 525681a into main Sep 19, 2026
36 checks passed
@andiwand
andiwand deleted the fix/composition-records-its-edit branch September 19, 2026 07:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant