Merge pull request #16885 from ShirasawaSama/patch-5

fix: fix Safari IME composition bug (#16615)
This commit is contained in:
Tim Jaeryang Baek 2025-08-26 14:26:12 +04:00 committed by GitHub
commit e3207f35d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -417,6 +417,30 @@
let recording = false; let recording = false;
let isComposing = 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 chatInputContainerElement;
let chatInputElement; let chatInputElement;
@ -1169,19 +1193,9 @@
return res; return res;
}} }}
oncompositionstart={() => (isComposing = true)} oncompositionstart={() => (isComposing = true)}
oncompositionend={() => { oncompositionend={(e) => {
const isSafari = /^((?!chrome|android).)*safari/i.test( compositionEndedAt = e.timeStamp;
navigator.userAgent isComposing = false;
);
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;
}
}} }}
on:keydown={async (e) => { on:keydown={async (e) => {
e = e.detail.event; e = e.detail.event;
@ -1290,7 +1304,7 @@
navigator.msMaxTouchPoints > 0 navigator.msMaxTouchPoints > 0
) )
) { ) {
if (isComposing) { if (inOrNearComposition(e)) {
return; return;
} }
@ -1393,17 +1407,9 @@
command = getCommand(); command = getCommand();
}} }}
on:compositionstart={() => (isComposing = true)} on:compositionstart={() => (isComposing = true)}
on:compositionend={() => { oncompositionend={(e) => {
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); compositionEndedAt = e.timeStamp;
isComposing = false;
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;
}
}} }}
on:keydown={async (e) => { on:keydown={async (e) => {
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
@ -1523,7 +1529,7 @@
navigator.msMaxTouchPoints > 0 navigator.msMaxTouchPoints > 0
) )
) { ) {
if (isComposing) { if (inOrNearComposition(e)) {
return; return;
} }