Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 201 additions & 2 deletions packages/main/cypress/specs/Input.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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 => {
Expand Down Expand Up @@ -3227,4 +3227,203 @@ describe("Input built-in filtering", () => {
.eq(1)
.should("have.attr", "hidden");
});

describe("Case-preserving suggestions", () => {
it("should preserve user's typed case during typeahead but use original suggestion case when accepted", () => {
cy.mount(
<Input id="case-test" showSuggestions>
<SuggestionItem text="Apple" />
<SuggestionItem text="Apricot" />
<SuggestionItem text="Avocado" />
</Input>
);

cy.get("#case-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 case "Apple"
cy.realPress("Enter");

cy.get("@input")
.should("have.value", "Apple");
});

it("should preserve uppercase typed characters during typeahead", () => {
cy.mount(
<Input id="case-test-upper" showSuggestions>
<SuggestionItem text="apple" />
<SuggestionItem text="apricot" />
</Input>
);

cy.get("#case-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 case "apple"
cy.realPress("Enter");

cy.get("@input")
.should("have.value", "apple");
});

it("should handle exact match with different case on Enter", () => {
cy.mount(
<Input id="exact-match-test" showSuggestions>
<SuggestionItem text="ap" />
<SuggestionItem text="Apple" />
</Input>
);

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 case is preserved
cy.get("@input")
.should("have.value", "Ap");

// Press Enter - should use original suggestion case "ap"
cy.realPress("Enter");

cy.get("@input")
.should("have.value", "ap");
});

it("should preserve original case through multiple characters typed", () => {
cy.mount(
<Input id="multi-char-test" showSuggestions>
<SuggestionItem text="BANANA" />
<SuggestionItem text="BERRY" />
</Input>
);

cy.get("#multi-char-test")
.shadow()
.find("input")
.as("input");

// Type "ban" with mixed case
cy.get("@input")
.realClick()
.realType("bAn");

// Should show suggestion with user's typed case
cy.get("@input")
.should("have.value", "bAnANA");

// Press Enter - should use original suggestion case "BANANA"
cy.realPress("Enter");

cy.get("@input")
.should("have.value", "BANANA");
});

it("should work with selection-change event and preserve original case", () => {
const onChangeSpy = cy.spy().as("onChange");
const onSelectionChangeSpy = cy.spy().as("onSelectionChange");

cy.mount(
<Input
id="selection-change-test"
showSuggestions
onChange={onChangeSpy}
onSelectionChange={onSelectionChangeSpy}
>
<SuggestionItem text="Orange" />
<SuggestionItem text="Olive" />
</Input>
);

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 case
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(
<Input id="escape-test" showSuggestions>
<SuggestionItem text="Apple" />
</Input>
);

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");
});
});
});
4 changes: 2 additions & 2 deletions packages/main/cypress/specs/Input.mobile.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});

Expand Down Expand Up @@ -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");
});
});
Expand Down
2 changes: 1 addition & 1 deletion packages/main/cypress/specs/MultiComboBox.mobile.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ describe("Typeahead", () => {
.find("[ui5-input]")
.as("respPopoverInput")
.realClick()
.realType("c");
.realType("C");

cy.get("@respPopoverInput")
.should("have.value", "Cosy");
Expand Down
2 changes: 1 addition & 1 deletion packages/main/cypress/specs/MultiInput.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ describe("MultiInput tokens", () => {
.realClick();

cy.get("@input")
.type("b");
.type("B");

cy.get("[ui5-multi-input]")
.should("have.attr", "value", "Bulgaria");
Expand Down
43 changes: 36 additions & 7 deletions packages/main/src/Input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -801,6 +802,8 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement
this._handleTypeAhead(item);
}
this._selectMatchingItem(item);
} else {
this._matchedSuggestionItem = undefined;
}
}
}
Expand Down Expand Up @@ -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 || "";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1325,6 +1333,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement

_selectMatchingItem(item: IInputSuggestionItemSelectable) {
item.selected = true;
this._matchedSuggestionItem = item;
}

_filterItems(value: string) {
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;

Expand All @@ -1549,6 +1572,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement
}

this.valueBeforeSelectionStart = "";
this._matchedSuggestionItem = undefined;

this.isTyping = false;
this.open = false;
Expand All @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions packages/main/src/Token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion packages/main/src/i18n/messagebundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,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
Expand Down
Loading
Loading