diff --git a/src/test/system/level_2_input_test.js b/src/test/system/level_2_input_test.js index 150419b74..aae390263 100644 --- a/src/test/system/level_2_input_test.js +++ b/src/test/system/level_2_input_test.js @@ -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) diff --git a/src/test/test_helpers/input_helpers.js b/src/test/test_helpers/input_helpers.js index 8990adeaf..35c782bfb 100644 --- a/src/test/test_helpers/input_helpers.js +++ b/src/test/test_helpers/input_helpers.js @@ -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 } } diff --git a/src/trix/controllers/level_2_input_controller.js b/src/trix/controllers/level_2_input_controller.js index 65b302157..121461d51 100644 --- a/src/trix/controllers/level_2_input_controller.js +++ b/src/trix/controllers/level_2_input_controller.js @@ -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" @@ -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() } }, diff --git a/src/trix/core/helpers/events.js b/src/trix/core/helpers/events.js index 4248830d3..f8411089e 100644 --- a/src/trix/core/helpers/events.js +++ b/src/trix/core/helpers/events.js @@ -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 + } +}