diff --git a/packages/main/cypress/specs/TextArea.cy.tsx b/packages/main/cypress/specs/TextArea.cy.tsx
index 0211883fa1a2..56b1c411c4bf 100644
--- a/packages/main/cypress/specs/TextArea.cy.tsx
+++ b/packages/main/cypress/specs/TextArea.cy.tsx
@@ -965,3 +965,192 @@ describe("Validation inside a form", () => {
.should("have.been.calledOnce");
});
});
+
+describe("TextArea Composition", () => {
+ it("should handle Korean composition correctly", () => {
+ cy.mount(
+
+ );
+
+ cy.get("[ui5-textarea]")
+ .as("textarea")
+ .realClick();
+
+ cy.get("@textarea")
+ .shadow()
+ .find("textarea")
+ .as("nativeTextarea")
+ .focus();
+
+ cy.get("@nativeTextarea").trigger("compositionstart", { data: "" });
+
+ cy.get("@textarea").should("have.prop", "_isComposing", true);
+
+ cy.get("@nativeTextarea").trigger("compositionupdate", { data: "사랑" });
+
+ cy.get("@textarea").should("have.prop", "_isComposing", true);
+
+ cy.get("@nativeTextarea").trigger("compositionend", { data: "사랑" });
+
+ cy.get("@nativeTextarea")
+ .invoke("val", "사랑")
+ .trigger("input", { inputType: "insertCompositionText" });
+
+ cy.get("@textarea").should("have.prop", "_isComposing", false);
+
+ cy.get("@textarea").should("have.attr", "value", "사랑");
+ });
+
+ it("should handle Japanese composition correctly", () => {
+ cy.mount(
+
+ );
+
+ cy.get("[ui5-textarea]")
+ .as("textarea")
+ .realClick();
+
+ cy.get("@textarea")
+ .shadow()
+ .find("textarea")
+ .as("nativeTextarea")
+ .focus();
+
+ cy.get("@nativeTextarea").trigger("compositionstart", { data: "" });
+
+ cy.get("@textarea").should("have.prop", "_isComposing", true);
+
+ cy.get("@nativeTextarea").trigger("compositionupdate", { data: "ありがとう" });
+
+ cy.get("@textarea").should("have.prop", "_isComposing", true);
+
+ cy.get("@nativeTextarea").trigger("compositionend", { data: "ありがとう" });
+
+ cy.get("@nativeTextarea")
+ .invoke("val", "ありがとう")
+ .trigger("input", { inputType: "insertCompositionText" });
+
+ cy.get("@textarea").should("have.prop", "_isComposing", false);
+
+ cy.get("@textarea").should("have.attr", "value", "ありがとう");
+ });
+
+ it("should handle Chinese composition correctly", () => {
+ cy.mount(
+
+ );
+
+ cy.get("[ui5-textarea]")
+ .as("textarea")
+ .realClick();
+
+ cy.get("@textarea")
+ .shadow()
+ .find("textarea")
+ .as("nativeTextarea")
+ .focus();
+
+ cy.get("@nativeTextarea").trigger("compositionstart", { data: "" });
+
+ cy.get("@textarea").should("have.prop", "_isComposing", true);
+
+ cy.get("@nativeTextarea").trigger("compositionupdate", { data: "谢谢" });
+
+ cy.get("@textarea").should("have.prop", "_isComposing", true);
+
+ cy.get("@nativeTextarea").trigger("compositionend", { data: "谢谢" });
+
+ cy.get("@nativeTextarea")
+ .invoke("val", "谢谢")
+ .trigger("input", { inputType: "insertCompositionText" });
+
+ cy.get("@textarea").should("have.prop", "_isComposing", false);
+
+ cy.get("@textarea").should("have.attr", "value", "谢谢");
+ });
+
+ it("should not revert value on Escape during composition", () => {
+ cy.mount(
+
+ );
+
+ cy.get("[ui5-textarea]")
+ .as("textarea")
+ .realClick();
+
+ cy.get("@textarea")
+ .shadow()
+ .find("textarea")
+ .as("nativeTextarea")
+ .focus();
+
+ cy.get("@nativeTextarea").trigger("compositionstart", { data: "" });
+
+ cy.get("@textarea").should("have.prop", "_isComposing", true);
+
+ cy.get("@nativeTextarea").trigger("compositionupdate", { data: "테스트" });
+
+ cy.get("@nativeTextarea")
+ .invoke("val", "initial테스트")
+ .trigger("input", { inputType: "insertCompositionText" });
+
+ cy.get("@nativeTextarea").trigger("keydown", { key: "Escape", keyCode: 27 });
+
+ cy.get("@textarea").should("have.attr", "value", "initial테스트");
+
+ cy.get("@nativeTextarea").trigger("compositionend", { data: "테스트" });
+
+ cy.get("@textarea").should("have.prop", "_isComposing", false);
+
+ cy.get("@textarea").should("have.attr", "value", "initial테스트");
+ });
+
+ it("should revert value on Escape after composition ends", () => {
+ cy.mount(
+
+ );
+
+ cy.get("[ui5-textarea]")
+ .as("textarea")
+ .realClick();
+
+ cy.get("@textarea")
+ .shadow()
+ .find("textarea")
+ .as("nativeTextarea")
+ .focus();
+
+ cy.get("@nativeTextarea").trigger("compositionstart", { data: "" });
+
+ cy.get("@nativeTextarea").trigger("compositionupdate", { data: "완료" });
+
+ cy.get("@nativeTextarea")
+ .invoke("val", "initial완료")
+ .trigger("input", { inputType: "insertCompositionText" });
+
+ cy.get("@nativeTextarea").trigger("compositionend", { data: "완료" });
+
+ cy.get("@textarea").should("have.prop", "_isComposing", false);
+
+ cy.get("@textarea").should("have.attr", "value", "initial완료");
+
+ cy.get("@nativeTextarea").realPress("Escape");
+
+ cy.get("@textarea").should("have.attr", "value", "initial");
+ });
+});
diff --git a/packages/main/src/TextArea.ts b/packages/main/src/TextArea.ts
index 31502fa2842c..d42797eeba13 100644
--- a/packages/main/src/TextArea.ts
+++ b/packages/main/src/TextArea.ts
@@ -18,6 +18,7 @@ import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
import { isEscape } from "@ui5/webcomponents-base/dist/Keys.js";
import type { IFormInputElement } from "@ui5/webcomponents-base/dist/features/InputElementsFormSupport.js";
import type Popover from "./Popover.js";
+import type InputComposition from "./features/InputComposition.js";
import TextAreaTemplate from "./TextAreaTemplate.js";
@@ -326,6 +327,14 @@ class TextArea extends UI5Element implements IFormInputElement {
@property({ type: Number })
_width?: number;
+ /**
+ * Indicates whether IME composition is currently active
+ * @default false
+ * @private
+ */
+ @property({ type: Boolean, noAttribute: true })
+ _isComposing = false;
+
/**
* Defines the value state message that will be displayed as pop up under the component.
* The value state message slot should contain only one root element.
@@ -347,9 +356,11 @@ class TextArea extends UI5Element implements IFormInputElement {
_keyDown?: boolean;
previousValue: string;
valueStatePopover?: Popover;
+ _composition?: InputComposition;
@i18n("@ui5/webcomponents")
static i18nBundle: I18nBundle;
+ static composition: typeof InputComposition;
get formValidityMessage() {
if (this.formValidity.valueMissing) {
@@ -387,10 +398,12 @@ class TextArea extends UI5Element implements IFormInputElement {
onEnterDOM() {
ResizeHandler.register(this, this._fnOnResize);
+ this._enableComposition();
}
onExitDOM() {
ResizeHandler.deregister(this, this._fnOnResize);
+ this._composition?.removeEventListeners();
}
onBeforeRendering() {
@@ -426,6 +439,10 @@ class TextArea extends UI5Element implements IFormInputElement {
_onkeydown(e: KeyboardEvent) {
this._keyDown = true;
+ if (this._isComposing) {
+ return;
+ }
+
if (isEscape(e)) {
const nativeTextArea = this.getInputDomRef();
@@ -575,6 +592,31 @@ class TextArea extends UI5Element implements IFormInputElement {
};
}
+ _enableComposition() {
+ if (this._composition) {
+ return;
+ }
+
+ const setup = (FeatureClass: typeof InputComposition) => {
+ this._composition = new FeatureClass({
+ getInputEl: () => this.getInputDomRef(),
+ updateCompositionState: (isComposing: boolean) => {
+ this._isComposing = isComposing;
+ },
+ });
+ this._composition.addEventListeners();
+ };
+
+ if (TextArea.composition) {
+ setup(TextArea.composition);
+ } else {
+ import("./features/InputComposition.js").then(CompositionModule => {
+ TextArea.composition = CompositionModule.default;
+ setup(CompositionModule.default);
+ });
+ }
+ }
+
get classes() {
return {
root: {
diff --git a/packages/main/src/features/InputComposition.ts b/packages/main/src/features/InputComposition.ts
index 17a883145174..a879fdaad76e 100644
--- a/packages/main/src/features/InputComposition.ts
+++ b/packages/main/src/features/InputComposition.ts
@@ -1,5 +1,5 @@
export interface CompositionComponent {
- getInputEl: () => HTMLInputElement | null;
+ getInputEl: () => HTMLInputElement | HTMLTextAreaElement | null;
updateCompositionState: (isComposing: boolean) => void;
}