Skip to content

Commit 23a708f

Browse files
fix(ui5-multi-combobox): handle composition with validation
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 185ea04 commit 23a708f

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
@@ -4116,180 +4116,141 @@ describe("Keyboard Handling", () => {
41164116
});
41174117

41184118
describe("MultiComboBox Composition", () => {
4119-
it("should handle Korean composition correctly", () => {
4119+
const mountMultiComboBox = (children: any, id: string, placeholder: string = "") => {
41204120
cy.mount(
4121-
<MultiComboBox
4122-
id="multicombobox-composition-korean"
4123-
placeholder="Type in Korean ..."
4124-
>
4125-
<MultiComboBoxItem text="안녕하세요" />
4126-
<MultiComboBoxItem text="고맙습니다" />
4127-
<MultiComboBoxItem text="사랑" />
4128-
<MultiComboBoxItem text="한국" />
4121+
<MultiComboBox id={id} placeholder={placeholder}>
4122+
{children}
41294123
</MultiComboBox>
41304124
);
4131-
4132-
cy.get("[ui5-multi-combobox]")
4133-
.as("multicombobox")
4134-
.realClick();
4135-
4136-
cy.get("@multicombobox")
4137-
.shadow()
4138-
.find("input")
4139-
.as("nativeInput")
4140-
.focus();
4141-
4142-
cy.get("@nativeInput").trigger("compositionstart", { data: "" });
4143-
4144-
cy.get("@multicombobox").should("have.prop", "_isComposing", true);
4145-
4146-
cy.get("@nativeInput").trigger("compositionupdate", { data: "사랑" });
4147-
4148-
cy.get("@multicombobox").should("have.prop", "_isComposing", true);
4149-
4150-
cy.get("@nativeInput").trigger("compositionend", { data: "사랑" });
4151-
4152-
cy.get("@nativeInput")
4153-
.invoke("val", "사랑")
4125+
cy.get("[ui5-multi-combobox]").as("mcb").realClick();
4126+
cy.get("@mcb").shadow().find("input").as("input").focus();
4127+
};
4128+
4129+
const simulateCompositionStages = (stages: string[], final: string) => {
4130+
cy.get("@input").trigger("compositionstart", { data: "" });
4131+
stages.forEach(stage => {
4132+
cy.get("@input")
4133+
.invoke("val", stage)
4134+
.trigger("input", { inputType: "insertCompositionText" })
4135+
.trigger("compositionupdate", { data: stage });
4136+
4137+
cy.get("@mcb").should("have.prop", "_isComposing", true);
4138+
cy.get("@input").should("have.value", stage);
4139+
cy.get("@mcb").should("have.attr", "value", stage);
4140+
cy.get("@mcb").should("have.attr", "value-state", "None");
4141+
});
4142+
cy.get("@input")
4143+
.trigger("compositionend", { data: final })
4144+
.invoke("val", final)
41544145
.trigger("input", { inputType: "insertCompositionText" });
4146+
cy.get("@mcb").should("have.prop", "_isComposing", false);
4147+
};
41554148

4156-
cy.get("@multicombobox").should("have.prop", "_isComposing", false);
4149+
it("IME Korean matching suggestion", () => {
4150+
mountMultiComboBox([
4151+
<MultiComboBoxItem key="1" text="사랑" />,
4152+
<MultiComboBoxItem key="2" text="사랑해요" />,
4153+
<MultiComboBoxItem key="3" text="한국" />,
4154+
], "mcb-korean", "Type in Korean ...");
41574155

4158-
cy.get("@multicombobox").should("have.attr", "value", "사랑");
4156+
simulateCompositionStages(["ㅅ", "사", "사랑"], "사랑");
41594157

4160-
cy.get("@multicombobox")
4158+
cy.get("@mcb")
41614159
.shadow()
4162-
.find<ResponsivePopover>("[ui5-responsive-popover]")
4163-
.as("popover")
4160+
.find<ResponsivePopover>("ui5-responsive-popover")
41644161
.ui5ResponsivePopoverOpened();
41654162

4166-
cy.get("@multicombobox")
4167-
.realPress("Enter");
4168-
4169-
cy.get("@multicombobox")
4163+
cy.get("@mcb").realPress("Enter");
4164+
cy.get("@mcb")
41704165
.shadow()
4171-
.find("[ui5-tokenizer]")
4172-
.find("[ui5-token]")
4166+
.find("[ui5-tokenizer] [ui5-token]")
41734167
.should("have.length", 1);
4174-
4175-
cy.get("@multicombobox").should("have.attr", "value", "");
4168+
cy.get("@mcb").should("have.attr", "value", "");
41764169
});
41774170

4178-
it("should handle Japanese composition correctly", () => {
4179-
cy.mount(
4180-
<MultiComboBox
4181-
id="multicombobox-composition-japanese"
4182-
placeholder="Type in Japanese ..."
4183-
>
4184-
<MultiComboBoxItem text="こんにちは" />
4185-
<MultiComboBoxItem text="ありがとう" />
4186-
<MultiComboBoxItem text="東京" />
4187-
<MultiComboBoxItem text="日本" />
4188-
</MultiComboBox>
4189-
);
4171+
it("IME Korean non-matching – error state after commit", () => {
4172+
mountMultiComboBox([
4173+
<MultiComboBoxItem key="1" text="사랑" />,
4174+
<MultiComboBoxItem key="2" text="한국" />,
4175+
], "mcb-ko-non", "Type in Korean ...");
41904176

4191-
cy.get("[ui5-multi-combobox]")
4192-
.as("multicombobox")
4193-
.realClick();
4177+
simulateCompositionStages(["ㄲ", "ㄲㅏ"], "까");
41944178

4195-
cy.get("@multicombobox")
4179+
cy.get("@mcb").should("have.attr", "value-state", "Negative");
4180+
cy.get("@input").should("have.value", "");
4181+
cy.get("@mcb")
41964182
.shadow()
4197-
.find("input")
4198-
.as("nativeInput")
4199-
.focus();
4200-
4201-
cy.get("@nativeInput").trigger("compositionstart", { data: "" });
4202-
4203-
cy.get("@multicombobox").should("have.prop", "_isComposing", true);
4204-
4205-
cy.get("@nativeInput").trigger("compositionupdate", { data: "ありがとう" });
4206-
4207-
cy.get("@multicombobox").should("have.prop", "_isComposing", true);
4208-
4209-
cy.get("@nativeInput").trigger("compositionend", { data: "ありがとう" });
4210-
4211-
cy.get("@nativeInput")
4212-
.invoke("val", "ありがとう")
4213-
.trigger("input", { inputType: "insertCompositionText" });
4214-
4215-
cy.get("@multicombobox").should("have.prop", "_isComposing", false);
4183+
.find("[ui5-tokenizer] [ui5-token]")
4184+
.should("have.length", 0);
4185+
});
42164186

4217-
cy.get("@multicombobox").should("have.attr", "value", "ありがとう");
4187+
it("IME Japanese matching – multi-stage selection", () => {
4188+
mountMultiComboBox([
4189+
<MultiComboBoxItem key="1" text="ありがとう" />,
4190+
<MultiComboBoxItem key="2" text="こんにちは" />,
4191+
<MultiComboBoxItem key="3" text="東京" />,
4192+
], "mcb-ja", "Type in Japanese ...");
42184193

4219-
cy.get("@multicombobox")
4194+
simulateCompositionStages(["あ", "あり", "ありが", "ありがとう"], "ありがとう");
4195+
cy.get("@mcb")
42204196
.shadow()
4221-
.find<ResponsivePopover>("[ui5-responsive-popover]")
4222-
.as("popover")
4197+
.find<ResponsivePopover>("ui5-responsive-popover")
42234198
.ui5ResponsivePopoverOpened();
4224-
4225-
cy.get("@multicombobox")
4226-
.realPress("Enter");
4227-
4228-
cy.get("@multicombobox")
4199+
cy.get("@mcb").realPress("Enter");
4200+
cy.get("@mcb")
42294201
.shadow()
4230-
.find("[ui5-tokenizer]")
4231-
.find("[ui5-token]")
4202+
.find("[ui5-tokenizer] [ui5-token]")
42324203
.should("have.length", 1);
4233-
4234-
cy.get("@multicombobox").should("have.attr", "value", "");
42354204
});
42364205

4237-
it("should handle Chinese composition correctly", () => {
4238-
cy.mount(
4239-
<MultiComboBox
4240-
id="multicombobox-composition-chinese"
4241-
placeholder="Type in Chinese ..."
4242-
>
4243-
<MultiComboBoxItem text="你好" />
4244-
<MultiComboBoxItem text="谢谢" />
4245-
<MultiComboBoxItem text="北京" />
4246-
<MultiComboBoxItem text="中国" />
4247-
</MultiComboBox>
4248-
);
4206+
it("IME Japanese non-matching – error state after commit", () => {
4207+
mountMultiComboBox([
4208+
<MultiComboBoxItem key="1" text="ありがとう" />,
4209+
<MultiComboBoxItem key="2" text="こんにちは" />,
4210+
], "mcb-ja-non", "Type in Japanese ...");
42494211

4250-
cy.get("[ui5-multi-combobox]")
4251-
.as("multicombobox")
4252-
.realClick();
4253-
4254-
cy.get("@multicombobox")
4212+
simulateCompositionStages(["ず", "ずx"], "ずx");
4213+
cy.get("@mcb").should("have.attr", "value-state", "Negative");
4214+
cy.get("@input").should("have.value", "");
4215+
cy.get("@mcb")
42554216
.shadow()
4256-
.find("input")
4257-
.as("nativeInput")
4258-
.focus();
4259-
4260-
cy.get("@nativeInput").trigger("compositionstart", { data: "" });
4261-
4262-
cy.get("@multicombobox").should("have.prop", "_isComposing", true);
4263-
4264-
cy.get("@nativeInput").trigger("compositionupdate", { data: "谢谢" });
4265-
4266-
cy.get("@multicombobox").should("have.prop", "_isComposing", true);
4267-
4268-
cy.get("@nativeInput").trigger("compositionend", { data: "谢谢" });
4269-
4270-
cy.get("@nativeInput")
4271-
.invoke("val", "谢谢")
4272-
.trigger("input", { inputType: "insertCompositionText" });
4273-
4274-
cy.get("@multicombobox").should("have.prop", "_isComposing", false);
4217+
.find("[ui5-tokenizer] [ui5-token]")
4218+
.should("have.length", 0);
4219+
});
42754220

4276-
cy.get("@multicombobox").should("have.attr", "value", "谢谢");
4221+
it("IME Chinese matching – preserves pinyin stages & selects", () => {
4222+
mountMultiComboBox([
4223+
<MultiComboBoxItem key="1" text="你好" />,
4224+
<MultiComboBoxItem key="2" text="谢谢" />,
4225+
<MultiComboBoxItem key="3" text="谢谢你" />,
4226+
<MultiComboBoxItem key="4" text="北京" />,
4227+
], "mcb-zh", "Type in Chinese ...");
42774228

4278-
cy.get("@multicombobox")
4229+
simulateCompositionStages(["x", "xi", "xie", "xiex", "xiexie"], "谢谢");
4230+
cy.get("@mcb")
42794231
.shadow()
4280-
.find<ResponsivePopover>("[ui5-responsive-popover]")
4281-
.as("popover")
4232+
.find<ResponsivePopover>("ui5-responsive-popover")
42824233
.ui5ResponsivePopoverOpened();
4283-
4284-
cy.get("@multicombobox")
4285-
.realPress("Enter");
4286-
4287-
cy.get("@multicombobox")
4234+
cy.get("@mcb").realPress("Enter");
4235+
cy.get("@mcb")
42884236
.shadow()
4289-
.find("[ui5-tokenizer]")
4290-
.find("[ui5-token]")
4237+
.find("[ui5-tokenizer] [ui5-token]")
42914238
.should("have.length", 1);
4239+
cy.get("@mcb").should("have.attr", "value", "");
4240+
});
4241+
4242+
it("IME Chinese non-matching – error state after commit", () => {
4243+
mountMultiComboBox([
4244+
<MultiComboBoxItem key="1" text="你好" />,
4245+
<MultiComboBoxItem key="2" text="谢谢" />,
4246+
], "mcb-zh-non", "Type in Chinese ...");
42924247

4293-
cy.get("@multicombobox").should("have.attr", "value", "");
4248+
simulateCompositionStages(["p", "pi", "pin"], "品味");
4249+
cy.get("@mcb").should("have.attr", "value-state", "Negative");
4250+
cy.get("@input").should("have.value", "");
4251+
cy.get("@mcb")
4252+
.shadow()
4253+
.find("[ui5-tokenizer] [ui5-token]")
4254+
.should("have.length", 0);
42944255
});
42954256
});

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
@@ -324,6 +324,18 @@ <h3>MultiComboBox Composition</h3>
324324
</ui5-multi-combobox>
325325
</div>
326326

327+
<div class="demo-section">
328+
<span>MultiComboBox Composition Korean with Validation</span>
329+
330+
<br>
331+
<ui5-multi-combobox placeholder="Type in Korean ..." id="mcb-composition-korean">
332+
<ui5-mcb-item text="안녕하세요"></ui5-mcb-item>
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-multi-combobox>
337+
</div>
338+
327339
<div class="demo-section">
328340
<span>MultiComboBox Composition Japanese</span>
329341

@@ -348,6 +360,18 @@ <h3>MultiComboBox Composition</h3>
348360
</ui5-multi-combobox>
349361
</div>
350362

363+
<div class="demo-section">
364+
<span>MultiComboBox Composition Chinese with Validation</span>
365+
366+
<br>
367+
<ui5-multi-combobox placeholder="Type in Chinese ..." id="mcb-composition-chinese">
368+
<ui5-mcb-item text="你好"></ui5-mcb-item>
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-multi-combobox>
373+
</div>
374+
351375
</section>
352376

353377
<div class="demo-section">

0 commit comments

Comments
 (0)