diff --git a/packages/main/cypress/specs/ComboBox.cy.tsx b/packages/main/cypress/specs/ComboBox.cy.tsx
index df370ad08172..629ec0e8f897 100644
--- a/packages/main/cypress/specs/ComboBox.cy.tsx
+++ b/packages/main/cypress/specs/ComboBox.cy.tsx
@@ -2626,6 +2626,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(
<>
@@ -3129,3 +3179,124 @@ describe("Validation inside a form", () => {
.should("have.been.calledOnce");
});
});
+
+describe("SelectedValue API", () => {
+ it("should clear selectedValue when clear icon is clicked", () => {
+ cy.mount(
+
+
+
+
+
+ );
+
+ cy.get("[ui5-combobox]")
+ .as("combo")
+ .should("have.attr", "selected-value", "DE")
+ .should("have.attr", "value", "Germany");
+
+ // Click the clear icon
+ cy.get("@combo")
+ .shadow()
+ .find(".ui5-input-clear-icon-wrapper")
+ .realClick();
+
+ cy.get("@combo")
+ .should("have.attr", "value", "")
+ .should("not.have.attr", "selected-value");
+ });
+
+ it("should correctly select items with same text but different values", () => {
+ cy.mount(
+
+
+
+
+
+ );
+
+ cy.get("[ui5-combobox]")
+ .as("combo")
+ .invoke('on', 'ui5-selection-change', cy.spy().as('selectionChangeSpy'));
+
+ // Open dropdown and click first John Smith (Sales)
+ cy.get("@combo")
+ .shadow()
+ .find("[ui5-icon]")
+ .realClick();
+
+ cy.get("[ui5-cb-item]").eq(0).realClick();
+
+ cy.get("@combo")
+ .should("have.attr", "value", "John Smith")
+ .should("have.attr", "selected-value", "emp-101");
+
+ // Open dropdown and click second John Smith (Engineering)
+ cy.get("@combo")
+ .shadow()
+ .find("[ui5-icon]")
+ .realClick();
+
+ cy.get("[ui5-cb-item]").eq(1).realClick();
+
+ cy.get("@combo")
+ .should("have.attr", "value", "John Smith")
+ .should("have.attr", "selected-value", "emp-205");
+
+ cy.get("@selectionChangeSpy").should("have.been.calledTwice");
+ });
+
+ it("should return item value in formFormattedValue for form submission", () => {
+ cy.mount(
+
+ );
+
+ cy.get("[ui5-combobox]")
+ .as("combo")
+ .then(($combo) => {
+ const comboBox = $combo[0] as ComboBox;
+ // formFormattedValue should return the item's value, not the display text
+ expect(comboBox.formFormattedValue).to.equal("DE");
+ });
+
+ // Change selection to France
+ cy.get("@combo")
+ .shadow()
+ .find("[ui5-icon]")
+ .realClick();
+
+ cy.get("[ui5-cb-item]").eq(2).realClick();
+
+ cy.get("@combo")
+ .then(($combo) => {
+ const comboBox = $combo[0] as ComboBox;
+ expect(comboBox.formFormattedValue).to.equal("FR");
+ });
+ });
+
+ it("should fallback to display text in formFormattedValue when item has no value", () => {
+ cy.mount(
+
+ );
+
+ cy.get("[ui5-combobox]")
+ .as("combo")
+ .then(($combo) => {
+ const comboBox = $combo[0] as ComboBox;
+ // Without item values, formFormattedValue should return the display text
+ expect(comboBox.formFormattedValue).to.equal("Germany");
+ });
+ });
+});
diff --git a/packages/main/src/ComboBox.ts b/packages/main/src/ComboBox.ts
index a5c814721e51..fdaf7ed03ac5 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,25 @@ class ComboBox extends UI5Element implements IFormInputElement {
@property()
value = "";
+ /**
+ * Defines the selected item's value.
+ *
+ * Use this property together with the `value` property on `ui5-cb-item` to:
+ * - Select an item programmatically by its unique identifier
+ * - Handle items with identical display text but different underlying values
+ * - Submit machine-readable values in forms (the `value` property is submitted instead of the display text)
+ *
+ * When set, the ComboBox finds and selects the item whose `value` matches this property
+ * and whose `text` matches the ComboBox's `value` (display text).
+ *
+ * **Note:** This replaces the deprecated `selected` property on `ui5-cb-item`.
+ * @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 +477,7 @@ class ComboBox extends UI5Element implements IFormInputElement {
_lastValue: string;
_selectedItemText = "";
_userTypedValue = "";
+ _useSelectedValue: boolean = false;
_valueStateLinks: Array = [];
_composition?: InputComposition;
@i18n("@ui5/webcomponents")
@@ -476,6 +497,15 @@ class ComboBox extends UI5Element implements IFormInputElement {
}
get formFormattedValue() {
+ // Find the selected item
+ const selectedItem = this._getItems().find(item => item.selected && !item.isGroupItem) as ComboBoxItem | undefined;
+
+ // If selected item has a value property, return it (like Select does)
+ if (selectedItem && selectedItem.value !== undefined) {
+ return selectedItem.value;
+ }
+
+ // Fallback to display text (backward compatibility)
return this.value;
}
@@ -512,6 +542,10 @@ class ComboBox extends UI5Element implements IFormInputElement {
this.valueStateOpen = false;
}
+ if (this.selectedValue) {
+ this._useSelectedValue = true;
+ }
+
this._selectMatchingItem();
this._initialRendering = false;
@@ -841,12 +875,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;
}
@@ -1162,7 +1198,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];
}
}
@@ -1173,11 +1215,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;
@@ -1197,7 +1244,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;
+ }
}
});
@@ -1214,6 +1273,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) {
@@ -1266,6 +1331,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", {
@@ -1308,6 +1376,9 @@ class ComboBox extends UI5Element implements IFormInputElement {
}
this.value = "";
+ if (this._useSelectedValue) {
+ this.selectedValue = undefined;
+ }
this.fireDecoratorEvent("input");
if (this._isPhone) {
diff --git a/packages/main/src/ComboBoxItem.ts b/packages/main/src/ComboBoxItem.ts
index a59d7530ec98..253f633ccdd1 100644
--- a/packages/main/src/ComboBoxItem.ts
+++ b/packages/main/src/ComboBoxItem.ts
@@ -45,6 +45,30 @@ class ComboBoxItem extends ListItemBase implements IComboBoxItem {
@property({ type: Boolean, noAttribute: true })
_isVisible = false;
+ /**
+ * Defines the value of the `ui5-cb-item`.
+ *
+ * Use this property to associate a unique identifier or machine-readable value with the item,
+ * separate from the display text. This enables:
+ * - Selecting items programmatically via `selectedValue` on the ComboBox
+ * - Submitting machine-readable values in forms
+ * - Distinguishing between items with identical display text
+ *
+ * **Example:**
+ * ```html
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * @default undefined
+ * @public
+ * @since 2.19.0
+ */
+ @property()
+ value?: string;
+
/**
* Indicates whether the item is focssed
* @protected
@@ -55,6 +79,7 @@ class ComboBoxItem extends ListItemBase implements IComboBoxItem {
/**
* Indicates whether the item is selected
* @protected
+ * @deprecated use value property of the item and selectedValue property of the ComboBox instead
*/
@property({ type: Boolean })
selected = false;
diff --git a/packages/main/test/pages/ComboBox.html b/packages/main/test/pages/ComboBox.html
index 27cf28276793..7d0fe039d9e1 100644
--- a/packages/main/test/pages/ComboBox.html
+++ b/packages/main/test/pages/ComboBox.html
@@ -27,7 +27,7 @@
Select country:
-
+
@@ -322,6 +322,17 @@ ComboBox in Compact
+
+ ComboBox - items with same text but different values
+
+
+
+
+
+
+
+
+
ComboBox Composition
diff --git a/packages/website/docs/_components_pages/main/ComboBox/ComboBox.mdx b/packages/website/docs/_components_pages/main/ComboBox/ComboBox.mdx
index a7564efccaf0..3087fbf3259c 100644
--- a/packages/website/docs/_components_pages/main/ComboBox/ComboBox.mdx
+++ b/packages/website/docs/_components_pages/main/ComboBox/ComboBox.mdx
@@ -8,6 +8,8 @@ import Filters from "../../../_samples/main/ComboBox/Filters/Filters.md";
import TwoColumnsLayout from "../../../_samples/main/ComboBox/TwoColumnsLayout/TwoColumnsLayout.md";
import Grouping from "../../../_samples/main/ComboBox/Grouping/Grouping.md";
import SuggestionsWrapping from "../../../_samples/main/ComboBox/SuggestionsWrapping/SuggestionsWrapping.md";
+import SelectedValue from "../../../_samples/main/ComboBox/SelectedValue/SelectedValue.md";
+import SameTextDifferentValues from "../../../_samples/main/ComboBox/SameTextDifferentValues/SameTextDifferentValues.md";
<%COMPONENT_OVERVIEW%>
@@ -42,4 +44,16 @@ Grouping of items can be implented via the ui5-cb-group-item web component.
### Items Text Wrapping
The sample demonstrates how the text of the items wrap when too long.
-
\ No newline at end of file
+
+
+### Selected Value
+Use the `value` property on items and `selected-value` on the ComboBox to decouple the display text from the underlying selection value.
+This is useful for form submission, programmatic selection, and handling items with identical display text.
+
+
+
+### Items with Same Text but Different Values
+When you have multiple items with identical display text (e.g., employees with the same name), use the `value` property to uniquely identify each item.
+The `additional-text` property helps users distinguish between items visually.
+
+
\ No newline at end of file
diff --git a/packages/website/docs/_samples/main/ComboBox/SameTextDifferentValues/SameTextDifferentValues.md b/packages/website/docs/_samples/main/ComboBox/SameTextDifferentValues/SameTextDifferentValues.md
new file mode 100644
index 000000000000..17798ecc59ab
--- /dev/null
+++ b/packages/website/docs/_samples/main/ComboBox/SameTextDifferentValues/SameTextDifferentValues.md
@@ -0,0 +1,4 @@
+import html from '!!raw-loader!./sample.html';
+import js from '!!raw-loader!./main.js';
+
+
diff --git a/packages/website/docs/_samples/main/ComboBox/SameTextDifferentValues/main.js b/packages/website/docs/_samples/main/ComboBox/SameTextDifferentValues/main.js
new file mode 100644
index 000000000000..5b4450886d02
--- /dev/null
+++ b/packages/website/docs/_samples/main/ComboBox/SameTextDifferentValues/main.js
@@ -0,0 +1,20 @@
+import "@ui5/webcomponents/dist/ComboBox.js";
+import "@ui5/webcomponents/dist/ComboBoxItem.js";
+
+const combo = document.getElementById("employee-combo");
+const employeeId = document.getElementById("employee-id");
+const employeeName = document.getElementById("employee-name");
+const employeeDept = document.getElementById("employee-dept");
+
+combo.addEventListener("selection-change", (event) => {
+ const item = event.detail.item;
+ if (item) {
+ employeeId.textContent = item.value;
+ employeeName.textContent = item.text;
+ employeeDept.textContent = item.additionalText;
+ } else {
+ employeeId.textContent = "-";
+ employeeName.textContent = "-";
+ employeeDept.textContent = "-";
+ }
+});
diff --git a/packages/website/docs/_samples/main/ComboBox/SameTextDifferentValues/sample.html b/packages/website/docs/_samples/main/ComboBox/SameTextDifferentValues/sample.html
new file mode 100644
index 000000000000..aea438a5a03e
--- /dev/null
+++ b/packages/website/docs/_samples/main/ComboBox/SameTextDifferentValues/sample.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+ Sample
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Employee ID: -
+
Name: -
+
Department: -
+
+
+
+
+
+
+
+
diff --git a/packages/website/docs/_samples/main/ComboBox/SelectedValue/SelectedValue.md b/packages/website/docs/_samples/main/ComboBox/SelectedValue/SelectedValue.md
new file mode 100644
index 000000000000..17798ecc59ab
--- /dev/null
+++ b/packages/website/docs/_samples/main/ComboBox/SelectedValue/SelectedValue.md
@@ -0,0 +1,4 @@
+import html from '!!raw-loader!./sample.html';
+import js from '!!raw-loader!./main.js';
+
+
diff --git a/packages/website/docs/_samples/main/ComboBox/SelectedValue/main.js b/packages/website/docs/_samples/main/ComboBox/SelectedValue/main.js
new file mode 100644
index 000000000000..2de51c714b2c
--- /dev/null
+++ b/packages/website/docs/_samples/main/ComboBox/SelectedValue/main.js
@@ -0,0 +1,14 @@
+import "@ui5/webcomponents/dist/ComboBox.js";
+import "@ui5/webcomponents/dist/ComboBoxItem.js";
+
+const combo = document.getElementById("country-combo");
+const output = document.getElementById("selected-value");
+
+combo.addEventListener("selection-change", (event) => {
+ const item = event.detail.item;
+ if (item) {
+ output.textContent = item.value || "(no value)";
+ } else {
+ output.textContent = "(none)";
+ }
+});
diff --git a/packages/website/docs/_samples/main/ComboBox/SelectedValue/sample.html b/packages/website/docs/_samples/main/ComboBox/SelectedValue/sample.html
new file mode 100644
index 000000000000..d62d1c5671df
--- /dev/null
+++ b/packages/website/docs/_samples/main/ComboBox/SelectedValue/sample.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+ Sample
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Selected value: DE
+
+
+
+
+
+
+
+