diff --git a/package-lock.json b/package-lock.json index 4c218b7097..91271d5988 100644 --- a/package-lock.json +++ b/package-lock.json @@ -244,6 +244,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", @@ -557,6 +558,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": ">=18" }, @@ -598,6 +600,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": ">=18" } @@ -1899,6 +1902,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", @@ -2347,6 +2351,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" }, @@ -2736,6 +2741,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3737,6 +3743,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", @@ -7092,6 +7099,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", @@ -7959,6 +7967,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8142,6 +8151,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", @@ -8190,6 +8200,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 c26e670515..aa18f0de63 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -1241,10 +1241,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 5777d14761..97891d1e25 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 77f6a61ce1..7c7588a30e 100644 --- a/src/browser/Types.ts +++ b/src/browser/Types.ts @@ -40,6 +40,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 b4b03a8424..bd0db3eca9 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.