diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 0a7662eb70..adea345ed0 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -417,6 +417,30 @@ let recording = false; let isComposing = false; + // Safari has a bug where compositionend is not triggered correctly #16615 + // when using the virtual keyboard on iOS. + let compositionEndedAt = -2e8; + const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); + function inOrNearComposition(event: Event) { + if (isComposing) { + return true; + } + // See https://www.stum.de/2016/06/24/handling-ime-events-in-javascript/. + // On Japanese input method editors (IMEs), the Enter key is used to confirm character + // selection. On Safari, when Enter is pressed, compositionend and keydown events are + // emitted. The keydown event triggers newline insertion, which we don't want. + // This method returns true if the keydown event should be ignored. + // We only ignore it once, as pressing Enter a second time *should* insert a newline. + // Furthermore, the keydown event timestamp must be close to the compositionEndedAt timestamp. + // This guards against the case where compositionend is triggered without the keyboard + // (e.g. character confirmation may be done with the mouse), and keydown is triggered + // afterwards- we wouldn't want to ignore the keydown event in this case. + if (isSafari && Math.abs(event.timeStamp - compositionEndedAt) < 500) { + compositionEndedAt = -2e8; + return true; + } + return false; + } let chatInputContainerElement; let chatInputElement; @@ -1169,19 +1193,9 @@ return res; }} oncompositionstart={() => (isComposing = true)} - oncompositionend={() => { - const isSafari = /^((?!chrome|android).)*safari/i.test( - navigator.userAgent - ); - - if (isSafari) { - // Safari has a bug where compositionend is not triggered correctly #16615 - // when using the virtual keyboard on iOS. - // We use a timeout to ensure that the composition is ended after a short delay. - setTimeout(() => (isComposing = false)); - } else { - isComposing = false; - } + oncompositionend={(e) => { + compositionEndedAt = e.timeStamp; + isComposing = false; }} on:keydown={async (e) => { e = e.detail.event; @@ -1290,7 +1304,7 @@ navigator.msMaxTouchPoints > 0 ) ) { - if (isComposing) { + if (inOrNearComposition(e)) { return; } @@ -1393,17 +1407,9 @@ command = getCommand(); }} on:compositionstart={() => (isComposing = true)} - on:compositionend={() => { - const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); - - if (isSafari) { - // Safari has a bug where compositionend is not triggered correctly #16615 - // when using the virtual keyboard on iOS. - // We use a timeout to ensure that the composition is ended after a short delay. - setTimeout(() => (isComposing = false)); - } else { - isComposing = false; - } + oncompositionend={(e) => { + compositionEndedAt = e.timeStamp; + isComposing = false; }} on:keydown={async (e) => { const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac @@ -1523,7 +1529,7 @@ navigator.msMaxTouchPoints > 0 ) ) { - if (isComposing) { + if (inOrNearComposition(e)) { return; }