Skip to content

Commit

Permalink
Fix: duplicated newlines when using dictation on iOS 18+.
Browse files Browse the repository at this point in the history
This fixes a bug that started to happen with iOS 18, where you would get duplicated newlines
after completing the dictation.

The bug happens because, upon dictation completion, iOS sends the sequence of `beforeinput` events
very quickly. With the `insertParagraph` ones, it can happen that the internal document range fails
to update, resulting in duplicated newlines and missed content.

This workaround is necessary due to the inability to distinguish text entered in dictation mode, as
iOS WebKit doesn’t trigger composition events during dictation (https://bugs.webkit.org/show_bug.cgi?id=261764).
  • Loading branch information
jorgemanrubia committed Oct 28, 2024
1 parent e597bc4 commit cd64200
Show file tree
Hide file tree
Showing 4 changed files with 35 additions and 11 deletions.
13 changes: 10 additions & 3 deletions src/test/system/level_2_input_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,18 @@ const performInputTypeUsingExecCommand = async (command, { inputType, data }) =>

await nextFrame()

const isInsertParagraph = inputType === "insertParagraph"

triggerInputEvent(document.activeElement, "beforeinput", { inputType, data })
document.execCommand(command, false, data)
assert.equal(inputEvents.length, 2)

// See `shouldRenderInmmediatelyToDealWithiOSDictation` to deal with iOS 18+ dictation bug.
if (!isInsertParagraph) {
document.execCommand(command, false, data)
assert.equal(inputEvents[1].type, "input")
}

assert.equal(inputEvents.length, isInsertParagraph ? 1 : 2)
assert.equal(inputEvents[0].type, "beforeinput")
assert.equal(inputEvents[1].type, "input")
assert.equal(inputEvents[0].inputType, inputType)
assert.equal(inputEvents[0].data, data)

Expand Down
3 changes: 1 addition & 2 deletions src/test/test_helpers/input_helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -276,9 +276,8 @@ const simulateKeypress = async (keyName) => {
await deleteInDirection("right")
break
case "return":
await nextFrame()
triggerInputEvent(document.activeElement, "beforeinput", { inputType: "insertParagraph" })
await insertNode(document.createElement("br"))
break
}
}

Expand Down
16 changes: 10 additions & 6 deletions src/trix/controllers/level_2_input_controller.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getAllAttributeNames, squishBreakableWhitespace } from "trix/core/helpers"
import { getAllAttributeNames, shouldRenderInmmediatelyToDealWithiOSDictation, squishBreakableWhitespace } from "trix/core/helpers"
import InputController from "trix/controllers/input_controller"
import * as config from "trix/config"

Expand Down Expand Up @@ -73,14 +73,18 @@ export default class Level2InputController extends InputController {
beforeinput(event) {
const handler = this.constructor.inputTypes[event.inputType]

// Handles bug with Siri dictation on iOS 18+.
if (!event.inputType) {
this.render()
}
const inmmediateRender = shouldRenderInmmediatelyToDealWithiOSDictation(event)

if (handler) {
this.withEvent(event, handler)
this.scheduleRender()

if (!inmmediateRender) {
this.scheduleRender()
}
}

if (inmmediateRender) {
this.render()
}
},

Expand Down
14 changes: 14 additions & 0 deletions src/trix/core/helpers/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,17 @@ export const keyEventIsKeyboardCommand = (function() {
return (event) => event.ctrlKey
}
})()

export function shouldRenderInmmediatelyToDealWithiOSDictation(inputEvent) {
if (/iPhone|iPad/.test(navigator.userAgent)) {
// Handle garbled content and duplicated newlines when using dictation on iOS 18+. Upon dictation completion, iOS sends
// the list of insertText / insertParagraph events in a quick sequence. If we don't render
// the editor synchronously, the internal range fails to update and results in garbled content or duplicated newlines.
//
// This workaround is necessary because iOS doesn't send composing events as expected while dictating:
// https://bugs.webkit.org/show_bug.cgi?id=261764
return !inputEvent.inputType || inputEvent.inputType === "insertParagraph"
} else {
return false
}
}

0 comments on commit cd64200

Please sign in to comment.