Skip to content
214 changes: 214 additions & 0 deletions packages/main/cypress/specs/ComboBox.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<ComboBox>
<ComboBoxItem text="Bahrain" value="bh"/>
<ComboBoxItem text="Belgium" value="be"/>
<ComboBoxItem text="Bulgaria" value="bg"/>
</ComboBox>
);

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<ResponsivePopover>("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(
<ComboBox id="combo-grouping">
<ComboBoxItemGroup headerText="Group 1">
<ComboBoxItem text="Item 1" value="1.1"/>
<ComboBoxItem text="Item 1" value="1.2"/>
</ComboBoxItemGroup>
</ComboBox>
);

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<ResponsivePopover>("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(
<ComboBox filter="None">
<ComboBoxItem text="Argentina" value="ar"/>
<ComboBoxItem text="Bahrain" value="bh"/>
<ComboBoxItem text="Belgium" value="be"/>
<ComboBoxItem text="Bulgaria" value="bg"/>
</ComboBox>
);

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<ResponsivePopover>("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(
<ComboBox>
<ComboBoxItem text="Item" value="1"/>
<ComboBoxItem text="Item" value="2"/>
<ComboBoxItem text="Item" value="3"/>
</ComboBox>
);

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<ResponsivePopover>("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", () => {
Expand Down Expand Up @@ -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(
<ComboBox onSelectionChange={selectionChangeSpy}>
<ComboBoxItem text="Bulgaria" value="bg"></ComboBoxItem>
<ComboBoxItem text="Brazil" value="br"></ComboBoxItem>
<ComboBoxItem text="China" value="ch"></ComboBoxItem>
<ComboBoxItem text="Germany" value="de"></ComboBoxItem>
</ComboBox>
);

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(
<>
Expand Down
53 changes: 51 additions & 2 deletions packages/main/src/ComboBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -457,6 +467,7 @@ class ComboBox extends UI5Element implements IFormInputElement {
_lastValue: string;
_selectedItemText = "";
_userTypedValue = "";
_useSelectedValue: boolean = false;
_valueStateLinks: Array<HTMLElement> = [];
_composition?: InputComposition;
@i18n("@ui5/webcomponents")
Expand Down Expand Up @@ -512,6 +523,10 @@ class ComboBox extends UI5Element implements IFormInputElement {
this.valueStateOpen = false;
}

if (this.selectedValue) {
this._useSelectedValue = true;
}

this._selectMatchingItem();
this._initialRendering = false;

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

Expand Down Expand Up @@ -1155,7 +1172,13 @@ class ComboBox extends UI5Element implements IFormInputElement {
const matchingItems: Array<IComboBoxItem> = 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];
}
}
Expand All @@ -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;

Expand All @@ -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;
}
}
});

Expand All @@ -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) {
Expand Down Expand Up @@ -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", {
Expand Down
10 changes: 10 additions & 0 deletions packages/main/src/ComboBoxItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading