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 `insertParagraph` events
and shifts the range offset to the next line, resulting in duplicated newlines. This patch
ignores these events unless triggered by an Enter keypress. 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 b3390da
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 6 deletions.
4 changes: 4 additions & 0 deletions src/test/system/level_2_input_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,11 @@ testGroup("Level 2 Input", testOptions, () => {
test("insertParagraph", async () => {
await clickToolbarButton({ attribute: "quote" })
insertString("abc")

// Insert Enter keydown events to deal with workaround for iOS 18 dictation issues. See `shouldInsertParagraph`.
triggerEvent(document.activeElement, "keydown", { key: "Enter", code: "Enter" })
await performInputTypeUsingExecCommand("insertParagraph", { inputType: "insertParagraph" })
triggerEvent(document.activeElement, "keydown", { key: "Enter", code: "Enter" })
await performInputTypeUsingExecCommand("insertParagraph", { inputType: "insertParagraph" })

assert.blockAttributes([ 0, 4 ], [ "quote" ])
Expand Down
18 changes: 15 additions & 3 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, shouldInsertParagraph, squishBreakableWhitespace } from "trix/core/helpers"
import InputController from "trix/controllers/input_controller"
import * as config from "trix/config"

Expand All @@ -14,6 +14,8 @@ export default class Level2InputController extends InputController {

static events = {
keydown(event) {
this.saveLastEvent("lastKeydownEvent", event)

if (keyEventIsKeyboardCommand(event)) {
const command = keyboardCommandFromKeyEvent(event)
if (this.delegate?.inputControllerDidReceiveKeyboardCommand(command)) {
Expand Down Expand Up @@ -71,6 +73,8 @@ export default class Level2InputController extends InputController {
},

beforeinput(event) {
this.saveLastEvent("lastBeforeInputEvent", event)

const handler = this.constructor.inputTypes[event.inputType]

// Handles bug with Siri dictation on iOS 18+.
Expand Down Expand Up @@ -441,8 +445,10 @@ export default class Level2InputController extends InputController {

insertParagraph() {
this.delegate?.inputControllerWillPerformTyping()
return this.withTargetDOMRange(function() {
return this.responder?.insertLineBreak()
return this.withTargetDOMRange(() => {
if (shouldInsertParagraph(this.lastKeydownEvent, this.lastBeforeInputEvent)) {
return this.responder?.insertLineBreak()
}
})
},

Expand All @@ -468,6 +474,12 @@ export default class Level2InputController extends InputController {
},
}

saveLastEvent(name, event) {
// Native timestamp is not reliable as some browsers do not update it on keydown.
event._timeStamp = Date.now()
this[name] = event
}

elementDidMutate() {
if (this.scheduledRender) {
if (this.composing) {
Expand Down
22 changes: 19 additions & 3 deletions src/trix/core/helpers/events.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const testTransferData = { "application/x-trix-feature-detection": "test" }

export const dataTransferIsPlainText = function(dataTransfer) {
export const dataTransferIsPlainText = function (dataTransfer) {
const text = dataTransfer.getData("text/plain")
const html = dataTransfer.getData("text/html")

Expand All @@ -20,7 +20,7 @@ export const dataTransferIsMsOfficePaste = ({ dataTransfer }) => {
dataTransfer.getData("text/html").includes("urn:schemas-microsoft-com:office:office")
}

export const dataTransferIsWritable = function(dataTransfer) {
export const dataTransferIsWritable = function (dataTransfer) {
if (!dataTransfer?.setData) return false

for (const key in testTransferData) {
Expand All @@ -36,10 +36,26 @@ export const dataTransferIsWritable = function(dataTransfer) {
return true
}

export const keyEventIsKeyboardCommand = (function() {
export const keyEventIsKeyboardCommand = (function () {
if (/Mac|^iP/.test(navigator.platform)) {
return (event) => event.metaKey
} else {
return (event) => event.ctrlKey
}
})()

export function shouldInsertParagraph(keydownEvent, beforeInputEvent) {
if (/iPhone|iPad/i.test(navigator.userAgent)) {
// Handle duplicated newlines when using dictation on iOS 18+. Upon dictation completion, iOS sends `insertParagraph` events
// and shifts the range offset to the next line, resulting in duplicated newlines. This patch ignores these events unless triggered
// by an Enter keypress. 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).
if (keydownEvent && beforeInputEvent && Math.abs(keydownEvent._timeStamp - beforeInputEvent._timeStamp) < 1000) {
return keydownEvent.key === "Enter" || keydownEvent.key === "Return"
} else {
return false
}
} else {
return true
}
}

0 comments on commit b3390da

Please sign in to comment.