diff --git a/package-lock.json b/package-lock.json index 24a3b3b07a..f31bf91e49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -245,6 +245,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -558,6 +559,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": ">=18" }, @@ -599,6 +601,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": ">=18" } @@ -1796,6 +1799,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.1.tgz", "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", @@ -2361,6 +2365,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2750,6 +2755,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3745,6 +3751,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7013,6 +7020,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -7853,6 +7861,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8019,6 +8028,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "dev": true, + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -8067,6 +8077,7 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", "dev": true, + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^1.2.0", diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index cedf889b4a..767267d8c4 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -1020,10 +1020,16 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { * @param ev The input event to be handled. */ protected _inputEvent(ev: InputEvent): boolean { - // Only support emoji IMEs when screen reader mode is disabled as the event must bubble up to - // support reading out character input which can doubling up input characters - // Based on these event traces: https://github.com/xtermjs/xterm.js/issues/3679 - if (ev.data && ev.inputType === 'insertText' && (!ev.composed || !this._keyDownSeen) && !this.optionsService.rawOptions.screenReaderMode) { + // Handle direct text input (not from composition). + // We skip input events when: + // - isComposing: Active composition in progress (e.g., emoji picker, CJK IME) + // - isSendingComposition: compositionend fired but setTimeout hasn't sent data yet + // CompositionHelper handles input in these cases to prevent duplicates. + // When NOT composing/sending, we accept input even if ev.composed=true, which fixes + // iOS Safari Chinese punctuation input (issue #3070, #4486). + // Screen reader mode needs the event to bubble for accessibility announcements. + const compositionHelper = this._compositionHelper!; + if (ev.data && ev.inputType === 'insertText' && !compositionHelper.isComposing && !compositionHelper.isSendingComposition && !this.optionsService.rawOptions.screenReaderMode) { if (this._keyPressHandled) { return false; } diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 485900e41a..ac6d129b47 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -344,6 +344,9 @@ export class MockCompositionHelper implements ICompositionHelper { public get isComposing(): boolean { return false; } + public get isSendingComposition(): boolean { + return false; + } public compositionstart(): void { throw new Error('Method not implemented.'); } diff --git a/src/browser/Types.ts b/src/browser/Types.ts index fa08de2e30..8badcb4403 100644 --- a/src/browser/Types.ts +++ b/src/browser/Types.ts @@ -38,6 +38,7 @@ export type LineData = CharData[]; export interface ICompositionHelper { readonly isComposing: boolean; + readonly isSendingComposition: boolean; compositionstart(): void; compositionupdate(ev: CompositionEvent): void; compositionend(): void; diff --git a/src/browser/input/CompositionHelper.ts b/src/browser/input/CompositionHelper.ts index d170eae3bc..a1aad4327a 100644 --- a/src/browser/input/CompositionHelper.ts +++ b/src/browser/input/CompositionHelper.ts @@ -32,9 +32,11 @@ export class CompositionHelper { /** * Whether a composition is in the process of being sent, setting this to false will cancel any - * in-progress composition. + * in-progress composition. This is true between compositionend and when the setTimeout callback + * fires to actually send the data. */ private _isSendingComposition: boolean; + public get isSendingComposition(): boolean { return this._isSendingComposition; } /** * Data already sent due to keydown event.