diff --git a/packages/main/cypress/specs/Input.cy.tsx b/packages/main/cypress/specs/Input.cy.tsx
index ee14e9829d84..2e0fe702a76c 100644
--- a/packages/main/cypress/specs/Input.cy.tsx
+++ b/packages/main/cypress/specs/Input.cy.tsx
@@ -1964,7 +1964,7 @@ describe("Input general interaction", () => {
.should("be.focused");
cy.get("@inputEl")
- .realType("a");
+ .realType("A");
cy.get("@inputEl")
.should("have.value", "Adam D");
@@ -1985,7 +1985,7 @@ describe("Input general interaction", () => {
);
cy.get("#input-custom-flat").shadow().find("input").as("input");
- cy.get("@input").click().realType("a");
+ cy.get("@input").click().realType("A");
cy.get("@input").should("have.value", "Albania");
cy.get("@input").then($input => {
@@ -3227,4 +3227,203 @@ describe("Input built-in filtering", () => {
.eq(1)
.should("have.attr", "hidden");
});
+
+ describe("Typeahead capitalization handling", () => {
+ it("should preserve user's typed capitalization during typeahead and use original suggestion capitalization when accepted", () => {
+ cy.mount(
+
+
+
+
+
+ );
+
+ cy.get("#capitalization-test")
+ .shadow()
+ .find("input")
+ .as("input");
+
+ // Type lowercase 'a' - should show 'apple' with user's lowercase 'a'
+ cy.get("@input")
+ .realClick()
+ .realType("a");
+
+ cy.get("@input")
+ .should("have.value", "apple");
+
+ // Verify text selection (typeahead highlighting)
+ cy.get("@input")
+ .then($input => {
+ const input = $input[0] as HTMLInputElement;
+ expect(input.selectionStart).to.equal(1);
+ expect(input.selectionEnd).to.equal(5);
+ });
+
+ // Press Enter to accept - should use original suggestion capitalization "Apple"
+ cy.realPress("Enter");
+
+ cy.get("@input")
+ .should("have.value", "Apple");
+ });
+
+ it("should preserve uppercase typed letters during typeahead", () => {
+ cy.mount(
+
+
+
+
+ );
+
+ cy.get("#capitalization-test-upper")
+ .shadow()
+ .find("input")
+ .as("input");
+
+ // Type uppercase 'A' - should show 'Apple' with user's uppercase 'A'
+ cy.get("@input")
+ .realClick()
+ .realType("A");
+
+ cy.get("@input")
+ .should("have.value", "Apple");
+
+ // Press Enter to accept - should use original suggestion capitalization "apple"
+ cy.realPress("Enter");
+
+ cy.get("@input")
+ .should("have.value", "apple");
+ });
+
+ it("should match suggestions regardless of capitalization and use original on Enter", () => {
+ cy.mount(
+
+
+
+
+ );
+
+ cy.get("#exact-match-test")
+ .shadow()
+ .find("input")
+ .as("input");
+
+ // Type "Ap" matching suggestion "ap"
+ cy.get("@input")
+ .realClick()
+ .realType("Ap");
+
+ // During typing, user's capitalization is preserved
+ cy.get("@input")
+ .should("have.value", "Ap");
+
+ // Press Enter - should use original suggestion capitalization "ap"
+ cy.realPress("Enter");
+
+ cy.get("@input")
+ .should("have.value", "ap");
+ });
+
+ it("should preserve user's typed capitalization through multiple characters", () => {
+ cy.mount(
+
+
+
+
+ );
+
+ cy.get("#multi-char-test")
+ .shadow()
+ .find("input")
+ .as("input");
+
+ // Type "bAn" with mixed capitalization
+ cy.get("@input")
+ .realClick()
+ .realType("bAn");
+
+ // Should show suggestion with user's typed capitalization
+ cy.get("@input")
+ .should("have.value", "bAnANA");
+
+ // Press Enter - should use original suggestion capitalization "BANANA"
+ cy.realPress("Enter");
+
+ cy.get("@input")
+ .should("have.value", "BANANA");
+ });
+
+ it("should work with selection-change event and preserve original capitalization", () => {
+ const onChangeSpy = cy.spy().as("onChange");
+ const onSelectionChangeSpy = cy.spy().as("onSelectionChange");
+
+ cy.mount(
+
+
+
+
+ );
+
+ cy.get("#selection-change-test")
+ .shadow()
+ .find("input")
+ .as("input");
+
+ // Type lowercase 'o'
+ cy.get("@input")
+ .realClick()
+ .realType("o");
+
+ cy.get("@input")
+ .should("have.value", "orange");
+
+ // Press Enter to trigger selection-change
+ cy.realPress("Enter");
+
+ // Value should be original suggestion capitalization
+ cy.get("@input")
+ .should("have.value", "Orange");
+
+ // Verify both events were called
+ cy.get("@onChange").should("have.been.calledOnce");
+ cy.get("@onSelectionChange").should("have.been.calledOnce");
+ });
+
+ it("should clear matched item on Escape and restore typed value", () => {
+ cy.mount(
+
+
+
+ );
+
+ cy.get("#escape-test")
+ .shadow()
+ .find("input")
+ .as("input");
+
+ // Type 'a' to trigger typeahead
+ cy.get("@input")
+ .realClick()
+ .realType("a");
+
+ cy.get("@input")
+ .should("have.value", "apple");
+
+ // Press Escape to cancel autocomplete
+ cy.realPress("Escape");
+
+ cy.get("@input")
+ .should("have.value", "a");
+
+ // Now press Enter - should not select anything
+ cy.realPress("Enter");
+
+ cy.get("@input")
+ .should("have.value", "a");
+ });
+ });
});
diff --git a/packages/main/cypress/specs/Input.mobile.cy.tsx b/packages/main/cypress/specs/Input.mobile.cy.tsx
index 60c97f9e96da..31be35f278d8 100644
--- a/packages/main/cypress/specs/Input.mobile.cy.tsx
+++ b/packages/main/cypress/specs/Input.mobile.cy.tsx
@@ -272,7 +272,7 @@ describe("Typeahead", () => {
.ui5ResponsivePopoverOpened();
cy.get("#myInput2").shadow().find(".ui5-input-inner-phone").should("be.focused");
- cy.get("#myInput2").shadow().find(".ui5-input-inner-phone").realType("c");
+ cy.get("#myInput2").shadow().find(".ui5-input-inner-phone").realType("C");
cy.get("#myInput2").shadow().find(".ui5-input-inner-phone").should("have.value", "Cozy");
});
@@ -313,7 +313,7 @@ describe("Typeahead", () => {
.ui5ResponsivePopoverOpened();
cy.get("#input-custom-flat").shadow().find(".ui5-input-inner-phone").should("be.focused");
- cy.get("#input-custom-flat").shadow().find(".ui5-input-inner-phone").realType("a");
+ cy.get("#input-custom-flat").shadow().find(".ui5-input-inner-phone").realType("A");
cy.get("#input-custom-flat").shadow().find(".ui5-input-inner-phone").should("have.value", "Albania");
});
});
diff --git a/packages/main/cypress/specs/MultiComboBox.mobile.cy.tsx b/packages/main/cypress/specs/MultiComboBox.mobile.cy.tsx
index 67726bb4707d..e6503a754606 100644
--- a/packages/main/cypress/specs/MultiComboBox.mobile.cy.tsx
+++ b/packages/main/cypress/specs/MultiComboBox.mobile.cy.tsx
@@ -197,7 +197,7 @@ describe("Typeahead", () => {
.find("[ui5-input]")
.as("respPopoverInput")
.realClick()
- .realType("c");
+ .realType("C");
cy.get("@respPopoverInput")
.should("have.value", "Cosy");
diff --git a/packages/main/cypress/specs/MultiInput.cy.tsx b/packages/main/cypress/specs/MultiInput.cy.tsx
index 277705c8672e..1d2304846cc4 100644
--- a/packages/main/cypress/specs/MultiInput.cy.tsx
+++ b/packages/main/cypress/specs/MultiInput.cy.tsx
@@ -575,11 +575,78 @@ describe("MultiInput tokens", () => {
.realClick();
cy.get("@input")
- .type("b");
+ .type("B");
cy.get("[ui5-multi-input]")
.should("have.attr", "value", "Bulgaria");
});
+
+ it("should not select multiple suggestions when switching between typed values", () => {
+ cy.mount(
+
+
+ Bulgaria
+
+
+ Canada
+
+
+ Germany
+
+
+ Austria
+
+
+ );
+
+ cy.get("[ui5-multi-input]")
+ .shadow()
+ .find("input")
+ .as("input");
+
+ cy.get("@input")
+ .realClick()
+ .realType("Bul");
+
+ // Wait for popover to open
+ cy.get("[ui5-multi-input]")
+ .shadow()
+ .find("[ui5-responsive-popover]")
+ .ui5ResponsivePopoverOpened();
+
+ // Bulgaria is first item (index 0), check it's selected
+ cy.get("[ui5-multi-input]")
+ .find("[ui5-suggestion-item-custom]")
+ .eq(0)
+ .should("have.prop", "selected", true);
+
+ // Other items should not be selected
+ cy.get("[ui5-multi-input]")
+ .find("[ui5-suggestion-item-custom]")
+ .eq(1)
+ .should("have.prop", "selected", false);
+
+ // Clear and type "Cana"
+ cy.get("@input")
+ .clear()
+ .realType("Cana");
+
+ // Canada is second item (index 1), check it's selected
+ cy.get("[ui5-multi-input]")
+ .find("[ui5-suggestion-item-custom]")
+ .eq(1)
+ .should("have.prop", "selected", true);
+
+ // Bulgaria (index 0) should NOT be selected anymore
+ cy.get("[ui5-multi-input]")
+ .find("[ui5-suggestion-item-custom]")
+ .eq(0)
+ .should("have.prop", "selected", false);
+ });
});
describe("MultiInput Truncated Token", () => {
diff --git a/packages/main/src/Input.ts b/packages/main/src/Input.ts
index 335175c39cba..115142055ac7 100644
--- a/packages/main/src/Input.ts
+++ b/packages/main/src/Input.ts
@@ -637,6 +637,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement
_clearIconClicked?: boolean;
_focusedAfterClear: boolean;
_changeToBeFired?: boolean; // used to wait change event firing after suggestion item selection
+ _matchedSuggestionItem?: IInputSuggestionItemSelectable; // stores the original matched suggestion for preserving case
_performTextSelection?: boolean;
_isLatestValueFromSuggestions: boolean;
_isChangeTriggeredBySuggestion: boolean;
@@ -801,6 +802,8 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement
this._handleTypeAhead(item);
}
this._selectMatchingItem(item);
+ } else {
+ this._matchedSuggestionItem = undefined;
}
}
}
@@ -1027,9 +1030,13 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement
// if a group item is focused, this is false
const suggestionItemPressed = !!(this.Suggestions?.onEnter(e));
const innerInput = this.getInputDOMRefSync()!;
- const matchingItem = this._selectableItems.find(item => {
- return item.text === this.value;
- });
+
+ let matchingItem = this._matchedSuggestionItem;
+ if (!matchingItem) {
+ matchingItem = this._selectableItems.find(item => {
+ return item.text?.toLowerCase() === this.value.toLowerCase();
+ });
+ }
if (matchingItem) {
const itemText = matchingItem.text || "";
@@ -1090,6 +1097,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement
const isAutoCompleted = innerInput.selectionEnd! - innerInput.selectionStart! > 0;
this.isTyping = false;
+ this._matchedSuggestionItem = undefined;
if (this.value !== this.previousValue && this.value !== this.lastConfirmedValue && !this.open) {
this.value = this.lastConfirmedValue ? this.lastConfirmedValue : this.previousValue;
@@ -1325,6 +1333,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement
_selectMatchingItem(item: IInputSuggestionItemSelectable) {
item.selected = true;
+ this._matchedSuggestionItem = item;
}
_filterItems(value: string) {
@@ -1374,11 +1383,15 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement
}
_handleTypeAhead(item: IInputSuggestionItemSelectable) {
- const value = item.text ? item.text : "";
+ const suggestionText = item.text ? item.text : "";
+ const typedValue = this.typedInValue;
- this.value = value;
- this._performTextSelection = true;
+ // Preserve the user's typed input case during typing
+ if (suggestionText.toLowerCase().startsWith(typedValue.toLowerCase())) {
+ this.value = typedValue + suggestionText.substring(typedValue.length);
+ }
+ this._performTextSelection = true;
this._shouldAutocomplete = false;
}
@@ -1527,7 +1540,17 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement
return;
}
- const itemText = item.text || "";
+ let originalItem = item;
+ if (this._matchedSuggestionItem) {
+ const matchedText = this._matchedSuggestionItem.text?.toLowerCase() || "";
+ const itemText = item.text?.toLowerCase() || "";
+ // Only use matched item if keyboard navigation or if it's the same item (case-insensitive)
+ if (keyboardUsed || matchedText === itemText) {
+ originalItem = this._matchedSuggestionItem;
+ }
+ }
+
+ const itemText = originalItem.text || "";
const fireChange = keyboardUsed
? this.valueBeforeItemSelection !== itemText : this.previousValue !== itemText;
@@ -1549,6 +1572,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement
}
this.valueBeforeSelectionStart = "";
+ this._matchedSuggestionItem = undefined;
this.isTyping = false;
this.open = false;
@@ -1563,6 +1587,11 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement
this.value = itemValue || "";
this._performTextSelection = true;
+
+ // Update the matched item when navigating with arrows to preserve correct case on Enter
+ if (!this._isGroupItem(item)) {
+ this._matchedSuggestionItem = item as IInputSuggestionItemSelectable;
+ }
}
fireEventByAction(action: INPUT_ACTIONS, e: InputEvent) {
diff --git a/packages/main/src/Token.ts b/packages/main/src/Token.ts
index 352119238981..fca069913e6d 100644
--- a/packages/main/src/Token.ts
+++ b/packages/main/src/Token.ts
@@ -13,7 +13,7 @@ import {
} from "@ui5/webcomponents-base/dist/Keys.js";
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
-import { TOKEN_ARIA_DELETABLE, TOKEN_ARIA_LABEL, TOKEN_ARIA_REMOVE } from "./generated/i18n/i18n-defaults.js";
+import { TOKEN_ARIA_DELETE, TOKEN_ARIA_DELETABLE, TOKEN_ARIA_LABEL } from "./generated/i18n/i18n-defaults.js";
import type { IIcon } from "./Icon.js";
import type { IToken } from "./MultiInput.js";
@@ -195,7 +195,7 @@ class Token extends UI5Element implements IToken {
}
get tokenDeletableText() {
- return Token.i18nBundle.getText(TOKEN_ARIA_REMOVE);
+ return Token.i18nBundle.getText(TOKEN_ARIA_DELETE);
}
get textDom() {
diff --git a/packages/main/src/features/InputSuggestions.ts b/packages/main/src/features/InputSuggestions.ts
index d15b871f92c1..ad625d42c8c8 100644
--- a/packages/main/src/features/InputSuggestions.ts
+++ b/packages/main/src/features/InputSuggestions.ts
@@ -387,7 +387,7 @@ class Suggestions {
_deselectItems() {
const items = this._getItems();
items.forEach(item => {
- if (item.hasAttribute("ui5-suggestion-item")) {
+ if (item.hasAttribute("ui5-suggestion-item") || item.hasAttribute("ui5-suggestion-item-custom")) {
(item as SuggestionItem).selected = false;
}
diff --git a/packages/main/src/i18n/messagebundle.properties b/packages/main/src/i18n/messagebundle.properties
index dabc09a707dd..7ec2adfba0a5 100644
--- a/packages/main/src/i18n/messagebundle.properties
+++ b/packages/main/src/i18n/messagebundle.properties
@@ -617,7 +617,7 @@ DATETIME_PICKER_TIME_BUTTON=Time
TOKEN_ARIA_DELETABLE=Deletable
#XACT: ARIA announcement for token removal
-TOKEN_ARIA_REMOVE=Remove
+TOKEN_ARIA_DELETE=Delete
#XACT: ARIA announcement for token label
TOKEN_ARIA_LABEL=Token
diff --git a/packages/main/test/pages/Input.html b/packages/main/test/pages/Input.html
index 12b97241c8c0..e0c9cca883a2 100644
--- a/packages/main/test/pages/Input.html
+++ b/packages/main/test/pages/Input.html
@@ -609,8 +609,17 @@ Input Composition
Check Validity
-
+ Capitalization suggestions
+
+
+
+
+
+
+
+
+