Skip to content

Commit 20838ab

Browse files
fix(ui5-multi-combobox): handle composition with validation (#12501)
JIRA: BGSOFUIRILA-4156 Prevent validation rollback from interrupting IME composition input stages by skipping validation until compositionend. Update test coverage for Korean/Japanese/Chinese composition scenarios.
1 parent 6b3e28e commit 20838ab

File tree

3 files changed

+133
-147
lines changed

3 files changed

+133
-147
lines changed

packages/main/cypress/specs/MultiComboBox.cy.tsx

Lines changed: 102 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -4153,180 +4153,141 @@ describe("Keyboard Handling", () => {
41534153
});
41544154

41554155
describe("MultiComboBox Composition", () => {
4156-
it("should handle Korean composition correctly", () => {
4156+
const mountMultiComboBox = (children: any, id: string, placeholder: string = "") => {
41574157
cy.mount(
4158-
<MultiComboBox
4159-
id="multicombobox-composition-korean"
4160-
placeholder="Type in Korean ..."
4161-
>
4162-
<MultiComboBoxItem text="안녕하세요" />
4163-
<MultiComboBoxItem text="고맙습니다" />
4164-
<MultiComboBoxItem text="사랑" />
4165-
<MultiComboBoxItem text="한국" />
4158+
<MultiComboBox id={id} placeholder={placeholder}>
4159+
{children}
41664160
</MultiComboBox>
41674161
);
4168-
4169-
cy.get("[ui5-multi-combobox]")
4170-
.as("multicombobox")
4171-
.realClick();
4172-
4173-
cy.get("@multicombobox")
4174-
.shadow()
4175-
.find("input")
4176-
.as("nativeInput")
4177-
.focus();
4178-
4179-
cy.get("@nativeInput").trigger("compositionstart", { data: "" });
4180-
4181-
cy.get("@multicombobox").should("have.prop", "_isComposing", true);
4182-
4183-
cy.get("@nativeInput").trigger("compositionupdate", { data: "사랑" });
4184-
4185-
cy.get("@multicombobox").should("have.prop", "_isComposing", true);
4186-
4187-
cy.get("@nativeInput").trigger("compositionend", { data: "사랑" });
4188-
4189-
cy.get("@nativeInput")
4190-
.invoke("val", "사랑")
4162+
cy.get("[ui5-multi-combobox]").as("mcb").realClick();
4163+
cy.get("@mcb").shadow().find("input").as("input").focus();
4164+
};
4165+
4166+
const simulateCompositionStages = (stages: string[], final: string) => {
4167+
cy.get("@input").trigger("compositionstart", { data: "" });
4168+
stages.forEach(stage => {
4169+
cy.get("@input")
4170+
.invoke("val", stage)
4171+
.trigger("input", { inputType: "insertCompositionText" })
4172+
.trigger("compositionupdate", { data: stage });
4173+
4174+
cy.get("@mcb").should("have.prop", "_isComposing", true);
4175+
cy.get("@input").should("have.value", stage);
4176+
cy.get("@mcb").should("have.attr", "value", stage);
4177+
cy.get("@mcb").should("have.attr", "value-state", "None");
4178+
});
4179+
cy.get("@input")
4180+
.trigger("compositionend", { data: final })
4181+
.invoke("val", final)
41914182
.trigger("input", { inputType: "insertCompositionText" });
4183+
cy.get("@mcb").should("have.prop", "_isComposing", false);
4184+
};
41924185

4193-
cy.get("@multicombobox").should("have.prop", "_isComposing", false);
4186+
it("IME Korean matching suggestion", () => {
4187+
mountMultiComboBox([
4188+
<MultiComboBoxItem key="1" text="사랑" />,
4189+
<MultiComboBoxItem key="2" text="사랑해요" />,
4190+
<MultiComboBoxItem key="3" text="한국" />,
4191+
], "mcb-korean", "Type in Korean ...");
41944192

4195-
cy.get("@multicombobox").should("have.attr", "value", "사랑");
4193+
simulateCompositionStages(["ㅅ", "사", "사랑"], "사랑");
41964194

4197-
cy.get("@multicombobox")
4195+
cy.get("@mcb")
41984196
.shadow()
4199-
.find<ResponsivePopover>("[ui5-responsive-popover]")
4200-
.as("popover")
4197+
.find<ResponsivePopover>("ui5-responsive-popover")
42014198
.ui5ResponsivePopoverOpened();
42024199

4203-
cy.get("@multicombobox")
4204-
.realPress("Enter");
4205-
4206-
cy.get("@multicombobox")
4200+
cy.get("@mcb").realPress("Enter");
4201+
cy.get("@mcb")
42074202
.shadow()
4208-
.find("[ui5-tokenizer]")
4209-
.find("[ui5-token]")
4203+
.find("[ui5-tokenizer] [ui5-token]")
42104204
.should("have.length", 1);
4211-
4212-
cy.get("@multicombobox").should("have.attr", "value", "");
4205+
cy.get("@mcb").should("have.attr", "value", "");
42134206
});
42144207

4215-
it("should handle Japanese composition correctly", () => {
4216-
cy.mount(
4217-
<MultiComboBox
4218-
id="multicombobox-composition-japanese"
4219-
placeholder="Type in Japanese ..."
4220-
>
4221-
<MultiComboBoxItem text="こんにちは" />
4222-
<MultiComboBoxItem text="ありがとう" />
4223-
<MultiComboBoxItem text="東京" />
4224-
<MultiComboBoxItem text="日本" />
4225-
</MultiComboBox>
4226-
);
4208+
it("IME Korean non-matching – error state after commit", () => {
4209+
mountMultiComboBox([
4210+
<MultiComboBoxItem key="1" text="사랑" />,
4211+
<MultiComboBoxItem key="2" text="한국" />,
4212+
], "mcb-ko-non", "Type in Korean ...");
42274213

4228-
cy.get("[ui5-multi-combobox]")
4229-
.as("multicombobox")
4230-
.realClick();
4214+
simulateCompositionStages(["ㄲ", "ㄲㅏ"], "까");
42314215

4232-
cy.get("@multicombobox")
4216+
cy.get("@mcb").should("have.attr", "value-state", "Negative");
4217+
cy.get("@input").should("have.value", "");
4218+
cy.get("@mcb")
42334219
.shadow()
4234-
.find("input")
4235-
.as("nativeInput")
4236-
.focus();
4237-
4238-
cy.get("@nativeInput").trigger("compositionstart", { data: "" });
4239-
4240-
cy.get("@multicombobox").should("have.prop", "_isComposing", true);
4241-
4242-
cy.get("@nativeInput").trigger("compositionupdate", { data: "ありがとう" });
4243-
4244-
cy.get("@multicombobox").should("have.prop", "_isComposing", true);
4245-
4246-
cy.get("@nativeInput").trigger("compositionend", { data: "ありがとう" });
4247-
4248-
cy.get("@nativeInput")
4249-
.invoke("val", "ありがとう")
4250-
.trigger("input", { inputType: "insertCompositionText" });
4251-
4252-
cy.get("@multicombobox").should("have.prop", "_isComposing", false);
4220+
.find("[ui5-tokenizer] [ui5-token]")
4221+
.should("have.length", 0);
4222+
});
42534223

4254-
cy.get("@multicombobox").should("have.attr", "value", "ありがとう");
4224+
it("IME Japanese matching – multi-stage selection", () => {
4225+
mountMultiComboBox([
4226+
<MultiComboBoxItem key="1" text="ありがとう" />,
4227+
<MultiComboBoxItem key="2" text="こんにちは" />,
4228+
<MultiComboBoxItem key="3" text="東京" />,
4229+
], "mcb-ja", "Type in Japanese ...");
42554230

4256-
cy.get("@multicombobox")
4231+
simulateCompositionStages(["あ", "あり", "ありが", "ありがとう"], "ありがとう");
4232+
cy.get("@mcb")
42574233
.shadow()
4258-
.find<ResponsivePopover>("[ui5-responsive-popover]")
4259-
.as("popover")
4234+
.find<ResponsivePopover>("ui5-responsive-popover")
42604235
.ui5ResponsivePopoverOpened();
4261-
4262-
cy.get("@multicombobox")
4263-
.realPress("Enter");
4264-
4265-
cy.get("@multicombobox")
4236+
cy.get("@mcb").realPress("Enter");
4237+
cy.get("@mcb")
42664238
.shadow()
4267-
.find("[ui5-tokenizer]")
4268-
.find("[ui5-token]")
4239+
.find("[ui5-tokenizer] [ui5-token]")
42694240
.should("have.length", 1);
4270-
4271-
cy.get("@multicombobox").should("have.attr", "value", "");
42724241
});
42734242

4274-
it("should handle Chinese composition correctly", () => {
4275-
cy.mount(
4276-
<MultiComboBox
4277-
id="multicombobox-composition-chinese"
4278-
placeholder="Type in Chinese ..."
4279-
>
4280-
<MultiComboBoxItem text="你好" />
4281-
<MultiComboBoxItem text="谢谢" />
4282-
<MultiComboBoxItem text="北京" />
4283-
<MultiComboBoxItem text="中国" />
4284-
</MultiComboBox>
4285-
);
4243+
it("IME Japanese non-matching – error state after commit", () => {
4244+
mountMultiComboBox([
4245+
<MultiComboBoxItem key="1" text="ありがとう" />,
4246+
<MultiComboBoxItem key="2" text="こんにちは" />,
4247+
], "mcb-ja-non", "Type in Japanese ...");
42864248

4287-
cy.get("[ui5-multi-combobox]")
4288-
.as("multicombobox")
4289-
.realClick();
4290-
4291-
cy.get("@multicombobox")
4249+
simulateCompositionStages(["ず", "ずx"], "ずx");
4250+
cy.get("@mcb").should("have.attr", "value-state", "Negative");
4251+
cy.get("@input").should("have.value", "");
4252+
cy.get("@mcb")
42924253
.shadow()
4293-
.find("input")
4294-
.as("nativeInput")
4295-
.focus();
4296-
4297-
cy.get("@nativeInput").trigger("compositionstart", { data: "" });
4298-
4299-
cy.get("@multicombobox").should("have.prop", "_isComposing", true);
4300-
4301-
cy.get("@nativeInput").trigger("compositionupdate", { data: "谢谢" });
4302-
4303-
cy.get("@multicombobox").should("have.prop", "_isComposing", true);
4304-
4305-
cy.get("@nativeInput").trigger("compositionend", { data: "谢谢" });
4306-
4307-
cy.get("@nativeInput")
4308-
.invoke("val", "谢谢")
4309-
.trigger("input", { inputType: "insertCompositionText" });
4310-
4311-
cy.get("@multicombobox").should("have.prop", "_isComposing", false);
4254+
.find("[ui5-tokenizer] [ui5-token]")
4255+
.should("have.length", 0);
4256+
});
43124257

4313-
cy.get("@multicombobox").should("have.attr", "value", "谢谢");
4258+
it("IME Chinese matching – preserves pinyin stages & selects", () => {
4259+
mountMultiComboBox([
4260+
<MultiComboBoxItem key="1" text="你好" />,
4261+
<MultiComboBoxItem key="2" text="谢谢" />,
4262+
<MultiComboBoxItem key="3" text="谢谢你" />,
4263+
<MultiComboBoxItem key="4" text="北京" />,
4264+
], "mcb-zh", "Type in Chinese ...");
43144265

4315-
cy.get("@multicombobox")
4266+
simulateCompositionStages(["x", "xi", "xie", "xiex", "xiexie"], "谢谢");
4267+
cy.get("@mcb")
43164268
.shadow()
4317-
.find<ResponsivePopover>("[ui5-responsive-popover]")
4318-
.as("popover")
4269+
.find<ResponsivePopover>("ui5-responsive-popover")
43194270
.ui5ResponsivePopoverOpened();
4320-
4321-
cy.get("@multicombobox")
4322-
.realPress("Enter");
4323-
4324-
cy.get("@multicombobox")
4271+
cy.get("@mcb").realPress("Enter");
4272+
cy.get("@mcb")
43254273
.shadow()
4326-
.find("[ui5-tokenizer]")
4327-
.find("[ui5-token]")
4274+
.find("[ui5-tokenizer] [ui5-token]")
43284275
.should("have.length", 1);
4276+
cy.get("@mcb").should("have.attr", "value", "");
4277+
});
4278+
4279+
it("IME Chinese non-matching – error state after commit", () => {
4280+
mountMultiComboBox([
4281+
<MultiComboBoxItem key="1" text="你好" />,
4282+
<MultiComboBoxItem key="2" text="谢谢" />,
4283+
], "mcb-zh-non", "Type in Chinese ...");
43294284

4330-
cy.get("@multicombobox").should("have.attr", "value", "");
4285+
simulateCompositionStages(["p", "pi", "pin"], "品味");
4286+
cy.get("@mcb").should("have.attr", "value-state", "Negative");
4287+
cy.get("@input").should("have.value", "");
4288+
cy.get("@mcb")
4289+
.shadow()
4290+
.find("[ui5-tokenizer] [ui5-token]")
4291+
.should("have.length", 0);
43314292
});
43324293
});

packages/main/src/MultiComboBox.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,7 @@ class MultiComboBox extends UI5Element implements IFormInputElement {
729729

730730
this._effectiveValueState = this.valueState;
731731

732-
if (!filteredItems.length && value && !this.noValidation) {
732+
if (!this._isComposing && !filteredItems.length && value && !this.noValidation) {
733733
const newValue = this.valueBeforeAutoComplete || this._inputLastValue;
734734

735735
input.value = newValue;
@@ -742,7 +742,10 @@ class MultiComboBox extends UI5Element implements IFormInputElement {
742742
return;
743743
}
744744

745-
this._inputLastValue = input.value;
745+
if (!this._isComposing) {
746+
this._inputLastValue = input.value;
747+
}
748+
746749
this.value = input.value;
747750
this._filteredItems = filteredItems;
748751

@@ -1749,8 +1752,6 @@ class MultiComboBox extends UI5Element implements IFormInputElement {
17491752

17501753
this._effectiveShowClearIcon = (this.showClearIcon && !!this.value && !this.readonly && !this.disabled);
17511754

1752-
this._inputLastValue = value;
1753-
17541755
if (input && !input.value) {
17551756
this.valueBeforeAutoComplete = "";
17561757
this._filteredItems = this._getItems();
@@ -1773,10 +1774,10 @@ class MultiComboBox extends UI5Element implements IFormInputElement {
17731774
if (this._shouldAutocomplete && !isAndroid()) {
17741775
const item = this._getFirstMatchingItem(value);
17751776

1776-
// Keep the original typed in text intact
1777-
this.valueBeforeAutoComplete = value;
17781777
// Prevent typeahead during composition to avoid interfering with the composition process
17791778
if (!this._isComposing && item) {
1779+
// Keep the original typed in text intact
1780+
this.valueBeforeAutoComplete = value;
17801781
this._handleTypeAhead(item, value);
17811782
}
17821783
}

packages/main/test/pages/MultiComboBox.html

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,18 @@ <h3>MultiComboBox Composition</h3>
325325
</ui5-multi-combobox>
326326
</div>
327327

328+
<div class="demo-section">
329+
<span>MultiComboBox Composition Korean with Validation</span>
330+
331+
<br>
332+
<ui5-multi-combobox placeholder="Type in Korean ..." id="mcb-composition-korean">
333+
<ui5-mcb-item text="안녕하세요"></ui5-mcb-item>
334+
<ui5-mcb-item text="고맙습니다"></ui5-mcb-item>
335+
<ui5-mcb-item text="사랑"></ui5-mcb-item>
336+
<ui5-mcb-item text="한국"></ui5-mcb-item>
337+
</ui5-multi-combobox>
338+
</div>
339+
328340
<div class="demo-section">
329341
<span>MultiComboBox Composition Japanese</span>
330342

@@ -349,6 +361,18 @@ <h3>MultiComboBox Composition</h3>
349361
</ui5-multi-combobox>
350362
</div>
351363

364+
<div class="demo-section">
365+
<span>MultiComboBox Composition Chinese with Validation</span>
366+
367+
<br>
368+
<ui5-multi-combobox placeholder="Type in Chinese ..." id="mcb-composition-chinese">
369+
<ui5-mcb-item text="你好"></ui5-mcb-item>
370+
<ui5-mcb-item text="謝謝"></ui5-mcb-item>
371+
<ui5-mcb-item text="北京"></ui5-mcb-item>
372+
<ui5-mcb-item text="上海"></ui5-mcb-item>
373+
</ui5-multi-combobox>
374+
</div>
375+
352376
</section>
353377

354378
<div class="demo-section">

0 commit comments

Comments
 (0)