diff --git a/packages/main/cypress/specs/ComboBox.cy.tsx b/packages/main/cypress/specs/ComboBox.cy.tsx index cbea94de23ec..f4fa21280f3a 100644 --- a/packages/main/cypress/specs/ComboBox.cy.tsx +++ b/packages/main/cypress/specs/ComboBox.cy.tsx @@ -596,6 +596,170 @@ describe("Keyboard navigation", () => { cy.get("@input").should("have.value", "b"); }); + it("updates selectedValue when navigating through items with values", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-combobox]") + .should("not.have.attr", "selected-value"); + + cy.get("[ui5-combobox]") + .shadow() + .find("input") + .as("input") + .realClick() + .realType("B"); + + cy.get("[ui5-combobox") + .shadow() + .find("ui5-responsive-popover") + .as("respPopover") + .ui5ResponsivePopoverOpened(); + + cy.get("[ui5-combobox]") + .should("have.attr", "selected-value", "bh"); + + cy.realPress("ArrowDown"); + + cy.get("[ui5-combobox]") + .should("have.attr", "selected-value", "be"); + }); + + it("does not set selectedValue when item group header is focused", () => { + cy.mount( + + + + + + + ); + + cy.get("[ui5-combobox]") + .should("not.have.attr", "selected-value"); + + cy.get("[ui5-combobox]") + .shadow() + .find("[ui5-icon]") + .realClick(); + + cy.get("[ui5-combobox") + .shadow() + .find("ui5-responsive-popover") + .as("respPopover") + .ui5ResponsivePopoverOpened(); + + cy.realPress("ArrowDown"); + + cy.get("[ui5-combobox]") + .find("[ui5-cb-item-group]") + .eq(0) + .should("have.prop", "focused"); + + cy.get("[ui5-combobox]") + .should("have.value", "") + .should("not.have.attr", "selected-value"); + + cy.realPress("ArrowDown"); + + cy.get("[ui5-combobox]") + .should("have.attr", "selected-value", "1.1") + .should("have.attr", "value", "Item 1"); + }); + + + it("updates selectedValue when navigating items with PageUp and PageDown", () => { + cy.mount( + + + + + + + ); + + cy.get("[ui5-combobox]") + .should("not.have.attr", "selected-value"); + + cy.get("[ui5-combobox]") + .shadow() + .find("[ui5-icon]") + .realClick(); + + cy.get("[ui5-combobox") + .shadow() + .find("ui5-responsive-popover") + .as("respPopover") + .ui5ResponsivePopoverOpened(); + + cy.get("[ui5-combobox]") + .should("not.have.attr", "selected-value"); + + cy.realPress("ArrowDown"); + cy.get("[ui5-combobox]") + .should("have.attr", "selected-value", "ar") + .should("have.attr", "value", "Argentina"); + + cy.realPress("PageDown"); + cy.get("[ui5-combobox]") + .should("have.attr", "selected-value", "bg") + .should("have.attr", "value", "Bulgaria"); + + cy.realPress("PageUp"); + cy.get("[ui5-combobox]") + .should("have.attr", "selected-value", "ar") + .should("have.attr", "value", "Argentina"); + }); + + it("navigates correctly between items with same text and different values", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-combobox]") + .should("not.have.attr", "selected-value"); + + cy.get("[ui5-combobox]") + .shadow() + .find("input") + .as("input") + .realClick() + .realType("I"); + + cy.get("[ui5-combobox") + .shadow() + .find("ui5-responsive-popover") + .as("respPopover") + .ui5ResponsivePopoverOpened(); + + cy.get("[ui5-combobox]") + .should("have.attr", "selected-value", "1") + .should("have.attr", "value", "Item"); + + cy.realPress("ArrowDown"); + cy.get("[ui5-combobox]") + .should("have.attr", "selected-value", "2") + .should("have.attr", "value", "Item"); + + cy.realPress("ArrowDown"); + cy.get("[ui5-combobox]") + .should("have.attr", "selected-value", "3") + .should("have.attr", "value", "Item"); + + cy.realPress("ArrowUp"); + cy.get("[ui5-combobox]") + .should("have.attr", "selected-value", "2") + .should("have.attr", "value", "Item"); + }); }); describe("Grouping", () => { @@ -2578,6 +2742,56 @@ describe("Event firing", () => { })); }); + it("fires selection-change when selectedValue changes via keyboard and input", () => { + const selectionChangeSpy = cy.stub().as("selectionChangeSpy"); + cy.mount( + + + + + + + ); + + cy.get("[ui5-combobox]") + .shadow() + .find("[ui5-icon]") + .realClick(); + + cy.realPress("ArrowDown"); + cy.get("[ui5-combobox]") + .should("have.attr", "selected-value", "bg") + .should("have.attr", "value", "Bulgaria"); + + cy.realPress("ArrowDown"); + cy.get("[ui5-combobox]") + .should("have.attr", "selected-value", "br") + .should("have.attr", "value", "Brazil"); + + cy.get("@selectionChangeSpy") + .should("be.calledTwice"); + cy.get("@selectionChangeSpy").should('have.been.calledWithMatch', Cypress.sinon.match(event => { + return event.detail.item.text === "Brazil"; + })); + + cy.get("[ui5-combobox]") + .shadow() + .find("input") + .realClick() + .realPress("Backspace"); + + cy.get("[ui5-combobox]") + .should("have.attr", "value", "Brazi") + .should("not.have.attr", "selected-value"); + + cy.get("@selectionChangeSpy") + .should("be.calledThrice"); + + cy.get("@selectionChangeSpy").should('have.been.calledWithMatch', Cypress.sinon.match(event => { + return event.detail.item === null; + })); + }); + it("should check clear icon events", () => { cy.mount( <> diff --git a/packages/main/src/ComboBox.ts b/packages/main/src/ComboBox.ts index 4654984c71d1..17c151ef2644 100644 --- a/packages/main/src/ComboBox.ts +++ b/packages/main/src/ComboBox.ts @@ -100,6 +100,7 @@ const SKIP_ITEMS_SIZE = 10; interface IComboBoxItem extends UI5Element { text?: string, headerText?: string, + value?: string, focused: boolean, isGroupItem?: boolean, selected?: boolean, @@ -235,6 +236,15 @@ class ComboBox extends UI5Element implements IFormInputElement { @property() value = ""; + /** + * Defines the selected item value. + * @default undefined + * @public + * @since 2.19.0 + */ + @property() + selectedValue?: string; + /** * Determines the name by which the component will be identified upon submission in an HTML form. * @@ -457,6 +467,7 @@ class ComboBox extends UI5Element implements IFormInputElement { _lastValue: string; _selectedItemText = ""; _userTypedValue = ""; + _useSelectedValue: boolean = false; _valueStateLinks: Array = []; _composition?: InputComposition; @i18n("@ui5/webcomponents") @@ -512,6 +523,10 @@ class ComboBox extends UI5Element implements IFormInputElement { this.valueStateOpen = false; } + if (this.selectedValue) { + this._useSelectedValue = true; + } + this._selectMatchingItem(); this._initialRendering = false; @@ -845,12 +860,14 @@ class ComboBox extends UI5Element implements IFormInputElement { if (this.open) { this._itemFocused = true; this.value = isGroupItem ? "" : currentItem.text!; + this.selectedValue = isGroupItem ? "" : currentItem?.value; this.focused = false; currentItem.focused = true; } else { this.focused = true; this.value = isGroupItem ? nextItem.text! : currentItem.text!; + this.selectedValue = currentItem.value; currentItem.focused = false; } @@ -1155,7 +1172,13 @@ class ComboBox extends UI5Element implements IFormInputElement { const matchingItems: Array = this._startsWithMatchingItems(current); if (matchingItems.length) { - const exactMatch = matchingItems.find(item => item.text === current); + let exactMatch; + if (this._useSelectedValue) { + exactMatch = matchingItems.find(item => item.value === (currentlyFocusedItem?.value || this.selectedValue) && item.text === current); + } else { + exactMatch = matchingItems.find(item => item.text === current); + } + return exactMatch ?? matchingItems[0]; } } @@ -1166,11 +1189,16 @@ class ComboBox extends UI5Element implements IFormInputElement { this.inner.value = value; this.inner.setSelectionRange(filterValue.length, value.length); this.value = value; + + if (this._useSelectedValue) { + this.selectedValue = item.value; + } } _selectMatchingItem() { const currentlyFocusedItem = this.items.find(item => item.focused); const shouldSelectionBeCleared = currentlyFocusedItem && currentlyFocusedItem.isGroupItem; + const valueToMatch = currentlyFocusedItem?.value ?? this.selectedValue; let itemToBeSelected: IComboBoxItem | undefined; let previouslySelectedItem: IComboBoxItem | undefined; @@ -1190,7 +1218,19 @@ class ComboBox extends UI5Element implements IFormInputElement { this._filteredItems.forEach(item => { if (!shouldSelectionBeCleared && !itemToBeSelected) { - itemToBeSelected = ((!item.isGroupItem && (item.text === this.value)) ? item : item?.items?.find(i => i.text === this.value)); + if (isInstanceOfComboBoxItemGroup(item)) { + if (this._useSelectedValue) { + itemToBeSelected = item.items.find(i => i.value === valueToMatch && this.value === i.text); + } else { + itemToBeSelected = item.items?.find(i => i.text === this.value); + } + } else { + if (this._useSelectedValue) { + itemToBeSelected = this.items.find(i => i.value === valueToMatch && this.value === i.text); + return; + } + itemToBeSelected = item.text === this.value ? item : undefined; + } } }); @@ -1207,6 +1247,12 @@ class ComboBox extends UI5Element implements IFormInputElement { return item; }); + if (!itemToBeSelected && this._useSelectedValue) { + this.selectedValue = undefined; + } else { + this.selectedValue = itemToBeSelected?.value; + } + const noUserInteraction = !this.focused && !this._isKeyNavigation && !this._selectionPerformed && !this._iconPressed; // Skip firing "selection-change" event if this is initial rendering or if there has been no user interaction yet if (this._initialRendering || noUserInteraction) { @@ -1259,6 +1305,9 @@ class ComboBox extends UI5Element implements IFormInputElement { } this.value = this._selectedItemText; + if (this._useSelectedValue) { + this.selectedValue = item.value; + } if (!item.selected) { this.fireDecoratorEvent("selection-change", { diff --git a/packages/main/src/ComboBoxItem.ts b/packages/main/src/ComboBoxItem.ts index a59d7530ec98..8b14bc1b55d5 100644 --- a/packages/main/src/ComboBoxItem.ts +++ b/packages/main/src/ComboBoxItem.ts @@ -45,6 +45,16 @@ class ComboBoxItem extends ListItemBase implements IComboBoxItem { @property({ type: Boolean, noAttribute: true }) _isVisible = false; + /** + * Defines the value of the `ui5-combobox-item`. + * Used for selection. Check ComboBox' selectedValue property for more information. + * @default undefined + * @public + * @since 2.19.0 + */ + @property() + value?: string; + /** * Indicates whether the item is focssed * @protected diff --git a/packages/main/test/pages/ComboBox.html b/packages/main/test/pages/ComboBox.html index abf48ebbf21c..3f9f895f520f 100644 --- a/packages/main/test/pages/ComboBox.html +++ b/packages/main/test/pages/ComboBox.html @@ -27,7 +27,7 @@
Select country: - + @@ -308,6 +308,17 @@

ComboBox in Compact

+
+ ComboBox - items with same text but different values + + + + + + + +
+

ComboBox Composition