diff --git a/packages/main/cypress/specs/MultiComboBox.cy.tsx b/packages/main/cypress/specs/MultiComboBox.cy.tsx index 04c109fa59af..f038ee6497ae 100644 --- a/packages/main/cypress/specs/MultiComboBox.cy.tsx +++ b/packages/main/cypress/specs/MultiComboBox.cy.tsx @@ -4153,180 +4153,141 @@ describe("Keyboard Handling", () => { }); describe("MultiComboBox Composition", () => { - it("should handle Korean composition correctly", () => { + const mountMultiComboBox = (children: any, id: string, placeholder: string = "") => { cy.mount( - - - - - + + {children} ); - - cy.get("[ui5-multi-combobox]") - .as("multicombobox") - .realClick(); - - cy.get("@multicombobox") - .shadow() - .find("input") - .as("nativeInput") - .focus(); - - cy.get("@nativeInput").trigger("compositionstart", { data: "" }); - - cy.get("@multicombobox").should("have.prop", "_isComposing", true); - - cy.get("@nativeInput").trigger("compositionupdate", { data: "사랑" }); - - cy.get("@multicombobox").should("have.prop", "_isComposing", true); - - cy.get("@nativeInput").trigger("compositionend", { data: "사랑" }); - - cy.get("@nativeInput") - .invoke("val", "사랑") + cy.get("[ui5-multi-combobox]").as("mcb").realClick(); + cy.get("@mcb").shadow().find("input").as("input").focus(); + }; + + const simulateCompositionStages = (stages: string[], final: string) => { + cy.get("@input").trigger("compositionstart", { data: "" }); + stages.forEach(stage => { + cy.get("@input") + .invoke("val", stage) + .trigger("input", { inputType: "insertCompositionText" }) + .trigger("compositionupdate", { data: stage }); + + cy.get("@mcb").should("have.prop", "_isComposing", true); + cy.get("@input").should("have.value", stage); + cy.get("@mcb").should("have.attr", "value", stage); + cy.get("@mcb").should("have.attr", "value-state", "None"); + }); + cy.get("@input") + .trigger("compositionend", { data: final }) + .invoke("val", final) .trigger("input", { inputType: "insertCompositionText" }); + cy.get("@mcb").should("have.prop", "_isComposing", false); + }; - cy.get("@multicombobox").should("have.prop", "_isComposing", false); + it("IME Korean matching suggestion", () => { + mountMultiComboBox([ + , + , + , + ], "mcb-korean", "Type in Korean ..."); - cy.get("@multicombobox").should("have.attr", "value", "사랑"); + simulateCompositionStages(["ㅅ", "사", "사랑"], "사랑"); - cy.get("@multicombobox") + cy.get("@mcb") .shadow() - .find("[ui5-responsive-popover]") - .as("popover") + .find("ui5-responsive-popover") .ui5ResponsivePopoverOpened(); - cy.get("@multicombobox") - .realPress("Enter"); - - cy.get("@multicombobox") + cy.get("@mcb").realPress("Enter"); + cy.get("@mcb") .shadow() - .find("[ui5-tokenizer]") - .find("[ui5-token]") + .find("[ui5-tokenizer] [ui5-token]") .should("have.length", 1); - - cy.get("@multicombobox").should("have.attr", "value", ""); + cy.get("@mcb").should("have.attr", "value", ""); }); - it("should handle Japanese composition correctly", () => { - cy.mount( - - - - - - - ); + it("IME Korean non-matching – error state after commit", () => { + mountMultiComboBox([ + , + , + ], "mcb-ko-non", "Type in Korean ..."); - cy.get("[ui5-multi-combobox]") - .as("multicombobox") - .realClick(); + simulateCompositionStages(["ㄲ", "ㄲㅏ"], "까"); - cy.get("@multicombobox") + cy.get("@mcb").should("have.attr", "value-state", "Negative"); + cy.get("@input").should("have.value", ""); + cy.get("@mcb") .shadow() - .find("input") - .as("nativeInput") - .focus(); - - cy.get("@nativeInput").trigger("compositionstart", { data: "" }); - - cy.get("@multicombobox").should("have.prop", "_isComposing", true); - - cy.get("@nativeInput").trigger("compositionupdate", { data: "ありがとう" }); - - cy.get("@multicombobox").should("have.prop", "_isComposing", true); - - cy.get("@nativeInput").trigger("compositionend", { data: "ありがとう" }); - - cy.get("@nativeInput") - .invoke("val", "ありがとう") - .trigger("input", { inputType: "insertCompositionText" }); - - cy.get("@multicombobox").should("have.prop", "_isComposing", false); + .find("[ui5-tokenizer] [ui5-token]") + .should("have.length", 0); + }); - cy.get("@multicombobox").should("have.attr", "value", "ありがとう"); + it("IME Japanese matching – multi-stage selection", () => { + mountMultiComboBox([ + , + , + , + ], "mcb-ja", "Type in Japanese ..."); - cy.get("@multicombobox") + simulateCompositionStages(["あ", "あり", "ありが", "ありがとう"], "ありがとう"); + cy.get("@mcb") .shadow() - .find("[ui5-responsive-popover]") - .as("popover") + .find("ui5-responsive-popover") .ui5ResponsivePopoverOpened(); - - cy.get("@multicombobox") - .realPress("Enter"); - - cy.get("@multicombobox") + cy.get("@mcb").realPress("Enter"); + cy.get("@mcb") .shadow() - .find("[ui5-tokenizer]") - .find("[ui5-token]") + .find("[ui5-tokenizer] [ui5-token]") .should("have.length", 1); - - cy.get("@multicombobox").should("have.attr", "value", ""); }); - it("should handle Chinese composition correctly", () => { - cy.mount( - - - - - - - ); + it("IME Japanese non-matching – error state after commit", () => { + mountMultiComboBox([ + , + , + ], "mcb-ja-non", "Type in Japanese ..."); - cy.get("[ui5-multi-combobox]") - .as("multicombobox") - .realClick(); - - cy.get("@multicombobox") + simulateCompositionStages(["ず", "ずx"], "ずx"); + cy.get("@mcb").should("have.attr", "value-state", "Negative"); + cy.get("@input").should("have.value", ""); + cy.get("@mcb") .shadow() - .find("input") - .as("nativeInput") - .focus(); - - cy.get("@nativeInput").trigger("compositionstart", { data: "" }); - - cy.get("@multicombobox").should("have.prop", "_isComposing", true); - - cy.get("@nativeInput").trigger("compositionupdate", { data: "谢谢" }); - - cy.get("@multicombobox").should("have.prop", "_isComposing", true); - - cy.get("@nativeInput").trigger("compositionend", { data: "谢谢" }); - - cy.get("@nativeInput") - .invoke("val", "谢谢") - .trigger("input", { inputType: "insertCompositionText" }); - - cy.get("@multicombobox").should("have.prop", "_isComposing", false); + .find("[ui5-tokenizer] [ui5-token]") + .should("have.length", 0); + }); - cy.get("@multicombobox").should("have.attr", "value", "谢谢"); + it("IME Chinese matching – preserves pinyin stages & selects", () => { + mountMultiComboBox([ + , + , + , + , + ], "mcb-zh", "Type in Chinese ..."); - cy.get("@multicombobox") + simulateCompositionStages(["x", "xi", "xie", "xiex", "xiexie"], "谢谢"); + cy.get("@mcb") .shadow() - .find("[ui5-responsive-popover]") - .as("popover") + .find("ui5-responsive-popover") .ui5ResponsivePopoverOpened(); - - cy.get("@multicombobox") - .realPress("Enter"); - - cy.get("@multicombobox") + cy.get("@mcb").realPress("Enter"); + cy.get("@mcb") .shadow() - .find("[ui5-tokenizer]") - .find("[ui5-token]") + .find("[ui5-tokenizer] [ui5-token]") .should("have.length", 1); + cy.get("@mcb").should("have.attr", "value", ""); + }); + + it("IME Chinese non-matching – error state after commit", () => { + mountMultiComboBox([ + , + , + ], "mcb-zh-non", "Type in Chinese ..."); - cy.get("@multicombobox").should("have.attr", "value", ""); + simulateCompositionStages(["p", "pi", "pin"], "品味"); + cy.get("@mcb").should("have.attr", "value-state", "Negative"); + cy.get("@input").should("have.value", ""); + cy.get("@mcb") + .shadow() + .find("[ui5-tokenizer] [ui5-token]") + .should("have.length", 0); }); }); diff --git a/packages/main/src/MultiComboBox.ts b/packages/main/src/MultiComboBox.ts index 3fd6d49e6656..15fb35bf27a7 100644 --- a/packages/main/src/MultiComboBox.ts +++ b/packages/main/src/MultiComboBox.ts @@ -729,7 +729,7 @@ class MultiComboBox extends UI5Element implements IFormInputElement { this._effectiveValueState = this.valueState; - if (!filteredItems.length && value && !this.noValidation) { + if (!this._isComposing && !filteredItems.length && value && !this.noValidation) { const newValue = this.valueBeforeAutoComplete || this._inputLastValue; input.value = newValue; @@ -742,7 +742,10 @@ class MultiComboBox extends UI5Element implements IFormInputElement { return; } - this._inputLastValue = input.value; + if (!this._isComposing) { + this._inputLastValue = input.value; + } + this.value = input.value; this._filteredItems = filteredItems; @@ -1749,8 +1752,6 @@ class MultiComboBox extends UI5Element implements IFormInputElement { this._effectiveShowClearIcon = (this.showClearIcon && !!this.value && !this.readonly && !this.disabled); - this._inputLastValue = value; - if (input && !input.value) { this.valueBeforeAutoComplete = ""; this._filteredItems = this._getItems(); @@ -1773,10 +1774,10 @@ class MultiComboBox extends UI5Element implements IFormInputElement { if (this._shouldAutocomplete && !isAndroid()) { const item = this._getFirstMatchingItem(value); - // Keep the original typed in text intact - this.valueBeforeAutoComplete = value; // Prevent typeahead during composition to avoid interfering with the composition process if (!this._isComposing && item) { + // Keep the original typed in text intact + this.valueBeforeAutoComplete = value; this._handleTypeAhead(item, value); } } diff --git a/packages/main/test/pages/MultiComboBox.html b/packages/main/test/pages/MultiComboBox.html index 69c7593a7598..2e3c384eefe6 100644 --- a/packages/main/test/pages/MultiComboBox.html +++ b/packages/main/test/pages/MultiComboBox.html @@ -325,6 +325,18 @@

MultiComboBox Composition

+
+ MultiComboBox Composition Korean with Validation + +
+ + + + + + +
+
MultiComboBox Composition Japanese @@ -349,6 +361,18 @@

MultiComboBox Composition

+
+ MultiComboBox Composition Chinese with Validation + +
+ + + + + + +
+