From 22b478afd5ea7d3f3efbfffed8869c5cb3cfe229 Mon Sep 17 00:00:00 2001 From: aleksandar-terziev Date: Tue, 19 Aug 2025 11:15:15 +0300 Subject: [PATCH] feat(ui5-input): poc composition handling and typeahead --- packages/main/src/Input.ts | 47 ++++++++++++++++++++- packages/main/src/InputTemplate.tsx | 3 ++ packages/main/src/MultiComboBox.ts | 44 ++++++++++++++++++- packages/main/test/pages/ComboBox.html | 42 ++++++++++++++++++ packages/main/test/pages/Input.html | 36 ++++++++++++++++ packages/main/test/pages/MultiComboBox.html | 6 +++ packages/main/test/pages/MultiInput.html | 4 ++ 7 files changed, 180 insertions(+), 2 deletions(-) diff --git a/packages/main/src/Input.ts b/packages/main/src/Input.ts index 2e7283b07b6c..ede9f9daceef 100644 --- a/packages/main/src/Input.ts +++ b/packages/main/src/Input.ts @@ -135,6 +135,8 @@ type InputAccInfo = { ariaInvalid?: boolean, } +type CompositionEventHandler = (e?: CompositionEvent) => void; + // all sementic events enum INPUT_EVENTS { CHANGE = "change", @@ -566,6 +568,14 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement @property({ type: Array }) _linksListenersArray: Array<(args: any) => void> = []; + /** + * Indicates whether IME composition is currently active + * @default false + * @private + */ + @property({ type: Boolean, noAttribute: true }) + _isComposing = false; + /** * Defines the suggestion items. * @@ -628,6 +638,8 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement _isLatestValueFromSuggestions: boolean; _isChangeTriggeredBySuggestion: boolean; _valueStateLinks: Array; + _onCompositionStartBound: CompositionEventHandler; + _onCompositionEndBound: CompositionEventHandler; @i18n("@ui5/webcomponents") static i18nBundle: I18nBundle; @@ -702,17 +714,22 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement this._keepInnerValue = false; this._focusedAfterClear = false; this._valueStateLinks = []; + + this._onCompositionStartBound = this._onCompositionStart.bind(this); + this._onCompositionEndBound = this._onCompositionEnd.bind(this); } onEnterDOM() { ResizeHandler.register(this, this._handleResizeBound); registerUI5Element(this, this._updateAssociatedLabelsTexts.bind(this)); + this._addCompositionEventListeners(); } onExitDOM() { ResizeHandler.deregister(this, this._handleResizeBound); deregisterUI5Element(this); this._removeLinksEventListeners(); + this._removeCompositionEventListeners(); } _highlightSuggestionItem(item: SuggestionItem) { @@ -776,7 +793,9 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement if (this._shouldAutocomplete && !isAndroid() && !autoCompletedChars && !this._isKeyNavigation) { const item = this._getFirstMatchingItem(value); if (item) { - this._handleTypeAhead(item); + if (!this._isComposing) { + this._handleTypeAhead(item); + } this._selectMatchingItem(item); } } @@ -1119,6 +1138,32 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement } } + _addCompositionEventListeners() { + const input = this.getInputDOMRefSync(); + if (!input) { return; } + + this._removeCompositionEventListeners(); + + input.addEventListener("compositionstart", this._onCompositionStartBound); + input.addEventListener("compositionend", this._onCompositionEndBound); + } + + _removeCompositionEventListeners() { + const input = this.getInputDOMRefSync(); + if (!input) { return; } + + input.removeEventListener("compositionstart", this._onCompositionStartBound); + input.removeEventListener("compositionend", this._onCompositionEndBound); + } + + _onCompositionStart() { + this._isComposing = true; + } + + _onCompositionEnd() { + this._isComposing = false; + } + _clearPopoverFocusAndSelection() { if (!this.showSuggestions || !this.Suggestions) { return; diff --git a/packages/main/src/InputTemplate.tsx b/packages/main/src/InputTemplate.tsx index a5e3c75f98b2..0bb831fe9dc6 100644 --- a/packages/main/src/InputTemplate.tsx +++ b/packages/main/src/InputTemplate.tsx @@ -58,6 +58,9 @@ export default function InputTemplate(this: Input, hooks?: { preContent: Templat onKeyUp={this._onkeyup} onClick={this._click} onFocusIn={this.innerFocusIn} + // uncomment after preact version in updated + // onCompositionStart={this._onCompositionStart} + // onCompositionEnd={this._onCompositionEnd} /> {this._effectiveShowClearIcon && diff --git a/packages/main/src/MultiComboBox.ts b/packages/main/src/MultiComboBox.ts index cf45801924c9..512f268d343d 100644 --- a/packages/main/src/MultiComboBox.ts +++ b/packages/main/src/MultiComboBox.ts @@ -140,6 +140,8 @@ type MultiComboboxItemWithSelection = { selected: boolean, }; +type CompositionEventHandler = (e?: CompositionEvent) => void; + /** * @class * @@ -479,6 +481,14 @@ class MultiComboBox extends UI5Element implements IFormInputElement { @property({ type: Array }) _linksListenersArray: Array<(args: any) => void> = []; + /** + * Indicates whether IME composition is currently active + * @default false + * @private + */ + @property({ type: Boolean, noAttribute: true }) + _isComposing = false; + /** * Defines the component items. * @public @@ -533,6 +543,8 @@ class MultiComboBox extends UI5Element implements IFormInputElement { _itemsBeforeOpen: Array; selectedItems: Array; _valueStateLinks: Array; + _onCompositionStartBound: CompositionEventHandler; + _onCompositionEndBound: CompositionEventHandler; @i18n("@ui5/webcomponents") static i18nBundle: I18nBundle; @@ -584,15 +596,19 @@ class MultiComboBox extends UI5Element implements IFormInputElement { this._lastValue = this.getAttribute("value") || ""; this.currentItemIdx = -1; this._valueStateLinks = []; + this._onCompositionStartBound = this._onCompositionStart.bind(this); + this._onCompositionEndBound = this._onCompositionEnd.bind(this); } onEnterDOM() { ResizeHandler.register(this, this._handleResizeBound); + this._addCompositionEventListeners(); } onExitDOM() { ResizeHandler.deregister(this, this._handleResizeBound); this._removeLinksEventListeners(); + this._removeCompositionEventListeners(); } _handleResize() { @@ -1689,6 +1705,32 @@ class MultiComboBox extends UI5Element implements IFormInputElement { }); } + _addCompositionEventListeners() { + const input = this._innerInput; + if (!input) { return; } + + this._removeCompositionEventListeners(); + + input.addEventListener("compositionstart", this._onCompositionStartBound); + input.addEventListener("compositionend", this._onCompositionEndBound); + } + + _removeCompositionEventListeners() { + const input = this._innerInput; + if (!input) { return; } + + input.removeEventListener("compositionstart", this._onCompositionStartBound); + input.removeEventListener("compositionend", this._onCompositionEndBound); + } + + _onCompositionStart() { + this._isComposing = true; + } + + _onCompositionEnd() { + this._isComposing = false; + } + onBeforeRendering() { const input = this._innerInput; const autoCompletedChars = input && (input.selectionEnd || 0) - (input.selectionStart || 0); @@ -1728,7 +1770,7 @@ class MultiComboBox extends UI5Element implements IFormInputElement { // Keep the original typed in text intact this.valueBeforeAutoComplete = value; - item && this._handleTypeAhead(item, value); + item && !this._isComposing && this._handleTypeAhead(item, value); } if (this._shouldFilterItems) { diff --git a/packages/main/test/pages/ComboBox.html b/packages/main/test/pages/ComboBox.html index 8ee4c6dfb0c3..ecd79fdb8fc6 100644 --- a/packages/main/test/pages/ComboBox.html +++ b/packages/main/test/pages/ComboBox.html @@ -184,6 +184,12 @@ + + + + + +
@@ -218,6 +224,10 @@ + + + +
@@ -232,6 +242,10 @@ + + + +
@@ -252,6 +266,10 @@ + + + + @@ -263,6 +281,10 @@ + + + + @@ -274,6 +296,10 @@

ComboBox in Compact

+ + + + @@ -286,6 +312,10 @@

ComboBox in Compact

+ + + + @@ -296,6 +326,10 @@

ComboBox in Compact

+ + + + @@ -310,6 +344,10 @@

ComboBox in Compact

+ + + + @@ -344,6 +382,10 @@

ComboBox in Compact

+ + + + diff --git a/packages/main/test/pages/Input.html b/packages/main/test/pages/Input.html index 22811f18cf15..3ced1a1781e8 100644 --- a/packages/main/test/pages/Input.html +++ b/packages/main/test/pages/Input.html @@ -593,6 +593,42 @@

Input - open suggestions picker

{ "key": "Antigua", "text": "Antigua and Barbuda" } ] }, + /* + * Korean + * "이 ㅚ" + */ + { + "key": "ㅇ", + "text": "ㅇ", + "items": [ + { "key": "이", "text": "이 ㅚ" }, + { "key": "이 ㅚ", "text": "이 ㅚㅈ" }, + { "key": "잊", "text": "잊ㅈㅅ" }, + { "key": "이즈", "text": "이즈ㅈㅅ" }, + ] + }, + /* + * Japanese + * 東京 + */ + { + key: "か", + text: "か", + items: [ + { key: "からの", text: "東京" } + ] + }, + /* + * Chinese + * 廿人大卜 + */ + { + "key": "廿", + "text": "廿", + "items": [ + { "key": "廿人", "text": "廿人大卜" } + ] + }, { "key": "B", "text": "B", diff --git a/packages/main/test/pages/MultiComboBox.html b/packages/main/test/pages/MultiComboBox.html index 9a4fb56fdbc7..8e9589c08313 100644 --- a/packages/main/test/pages/MultiComboBox.html +++ b/packages/main/test/pages/MultiComboBox.html @@ -70,6 +70,12 @@ + + + + + +

diff --git a/packages/main/test/pages/MultiInput.html b/packages/main/test/pages/MultiInput.html index c7d16c221764..f5e171055a79 100644 --- a/packages/main/test/pages/MultiInput.html +++ b/packages/main/test/pages/MultiInput.html @@ -286,6 +286,10 @@

Tokens + Suggestions

+ + + +
Information message. This is a Link. Extra long text used as an information message. Extra long text used as an information message - 2. Extra long text used as an information message - 3.