Skip to content

Commit

Permalink
Merge pull request #1196 from basecamp/fix-ios-dictation-newlines
Browse files Browse the repository at this point in the history
Fix: duplicated newlines when using dictation on iOS 18+.
  • Loading branch information
jorgemanrubia authored Oct 30, 2024
2 parents e597bc4 + c681258 commit 3a32d87
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 immmediateRender = shouldRenderInmmediatelyToDealWithIOSDictation(event)

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

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

if (immmediateRender) {
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 3a32d87

Please sign in to comment.