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
14 changes: 14 additions & 0 deletions .changeset/combobox-change-event-detail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@spectrum-web-components/combobox': minor
---

**Added**: Enhanced `<sp-combobox>` change event to include both `value` and `itemText` in the event detail, enabling consumers to access both the unique identifier and display text of the selected option.

**Fixed**: Resolved issue where selecting an option would incorrectly match the first option with the same display text. The component now correctly preserves the selected option's value even when multiple options share similar display text.

```js
combobox.addEventListener('change', (event) => {
const { value, itemText } = event.detail;
console.log(`Selected: ${itemText} (ID: ${value})`);
});
```
37 changes: 33 additions & 4 deletions 1st-gen/packages/combobox/src/Combobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export type ComboboxOption = {
* @element sp-combobox
* @slot - Supply Menu Item elements to the default slot in order to populate the available options
* @slot tooltip - Tooltip to to be applied to the the Picker Button
* @fires change - Announces that a selection has been made. The event detail contains `value` and `itemText` of the selected option.
*/
export class Combobox extends Textfield {
public static override get styles(): CSSResultArray {
Expand Down Expand Up @@ -105,6 +106,13 @@ export class Combobox extends Textfield {

private itemValue = '';

/**
* Tracks the value when an item is selected from the menu dropdown.
* This ensures we preserve the exact selected value even when multiple
* options have the same itemText.
*/
private _menuSelectedValue: string = '';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You dont' need this. You can replace this with a boolean. You are already setting this.itemValue in line 313.. Instead add a flag here

private _valueSetByMenu = false;


/**
* An array of options to present in the Menu provided while typing into the input
*/
Expand Down Expand Up @@ -302,13 +310,29 @@ export class Combobox extends Textfield {
const selected = (this.options || this.optionEls).find(
(item) => item.value === target?.value
);
this.itemValue = selected?.value || '';
this._menuSelectedValue = selected?.value || '';
Copy link
Copy Markdown
Contributor

@Rajdeepc Rajdeepc Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need of creating a shadow copy here. Raise the guard so shouldUpdate doesn't overwrite it via itemText lookup.

this.itemValue = selected?.value || '';
this._valueSetByMenu = true;

this.value = selected?.itemText || '';
this.handleChange();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sp-menu fires its own change event with bubbles: true, composed: true. That event propagates up through the shadow DOM and reaches consumers listening on <sp-combobox>. event.preventDefault() does not stop propagation -- it only prevents the browser's default action.
Then this.handleChange() dispatches a second CustomEvent('change', ...). Consumers will receive two change events per selection: the menu's original plain Event (no detail) and the new CustomEvent (with detail).
Please call event.stopPropagation() to prevent the menu's change from leaking out.

event.preventDefault();
this.open = false;
this._returnItems();
this.focus();
}

protected override handleChange(): void {
this.dispatchEvent(
new CustomEvent('change', {
bubbles: true,
composed: true,
detail: {
value: this.itemValue,
itemText: this.value,
},
})
);
}

Comment on lines +323 to +335
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The combobox documentation should be updated to reflect this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overriding handleChange will suppress base class behaviour too. Do we want this?
This is risky. The base class is doing important housekeeping (state updates, validation, other events). My approach would be to use super.handleChange() unless you intentionally want to replace that logic.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Texfield is already dispatching change event at @change handler. This is duplicate semantics.
Check here:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to dispatch change event with detail like text and value, which is not added in Textfield. These details are needed for the user to handle UI on their side.

Suppose, we have a requirement to store the itemText selected in the text box and pass it to API on a CTA click.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The combobox documentation should be updated to reflect this.

Can you please let me know where do we need to update the docs?

Copy link
Copy Markdown
Contributor

@nikkimk nikkimk Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The combobox documentation should be updated to reflect this.

Can you please let me know where do we need to update the docs?

@preethialuru 1st-gen/packages/combobox/README.md

Add a section just above the accessibility section, something like this:

### Behaviors

#### Change Event

When the selection changes, a `change` event is fired. The event detail contains `value` and `itemText` of the selected option.

```html demo
<sp-combobox id="employee" label="Employee">
    <sp-menu-item value="emp-1042">Alex Johnson (Engineering)</sp-menu-item>
    <sp-menu-item value="emp-2187">Alex Johnson (Marketing)</sp-menu-item>
    <sp-menu-item value="emp-3301">Sam Chen (Design)</sp-menu-item>
    <sp-menu-item value="emp-4455">Jordan Lee (Sales)</sp-menu-item>
</sp-combobox>
<script>
    document.getElementById('employee').addEventListener('change', (event) => {
        alert(`${event.detail.value}: ${event.detail.itemText}`);
    });
</script>
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@preethialuru Your use case is valid but I see ther eis big problem changing the event contract. It effects all change events not just menu selection. If you need itemValue we can expose it as a public reactive property rather than changing the event contract. You can add a public @Property for the selected option's value:

@property({ type: String, attribute: 'selected-value' })
public selectedValue = '';

Then you can capture it on your app side by

combobox.addEventListener('change', () => {
    const itemText = combobox.value;           // display text
    const value = combobox.selectedValue;      // underlying ID
    api.save({ id: value, name: itemText });
});

This would be non-breaking and will work for all scenarios.

public handleClosed(): void {
this.open = false;
this.overlayOpen = false;
Expand Down Expand Up @@ -339,10 +363,15 @@ export class Combobox extends Textfield {
}
if (changed.has('value')) {
this.filterAvailableOptions();
this.itemValue =
this.availableOptions.find(
(option) => option.itemText === this.value
)?.value ?? '';
if (this._menuSelectedValue) {
this.itemValue = this._menuSelectedValue;
this._menuSelectedValue = '';
} else {
const allOptions = this.options || this.optionEls;
this.itemValue =
allOptions.find((option) => option.itemText === this.value)
?.value ?? '';
}
Comment on lines +366 to +374
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check the boolean guard instead of reading a shadow copy of the value

if (this._valueSetByMenu) {
            // itemValue was already set by handleMenuChange; just lower the guard.
            this._valueSetByMenu = false;
        } else {
            // Value changed from typing or programmatic set -- resolve itemValue
            // by looking up the matching option.
            const allOptions = this.options || this.optionEls;
            this.itemValue =
                allOptions.find((option) => option.itemText === this.value)
                    ?.value ?? '';
        }

}
return super.shouldUpdate(changed);
}
Expand Down
49 changes: 49 additions & 0 deletions 1st-gen/packages/combobox/stories/combobox.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,52 @@ controlled.parameters = {
// Disables Chromatic's snapshotting on a global level
chromatic: { disableSnapshot: true },
};

/**
* Demonstrates the change event providing both value (unique ID) and itemText.
* This is useful when options share similar names but have distinct identifiers,
* such as contacts with the same name in different departments.
*/
export const changeEventWithValueAndItemText = (): TemplateResult => {
const contactOptions: ComboboxOption[] = [
{ value: 'emp-1042', itemText: 'Alex Johnson (Engineering)' },
{ value: 'emp-2187', itemText: 'Alex Johnson (Marketing)' },
{ value: 'emp-3301', itemText: 'Sam Chen (Design)' },
{ value: 'emp-4455', itemText: 'Jordan Lee (Sales)' },
];

const handleChange = (event: CustomEvent): void => {
const { value, itemText } = event.detail;

// Update the output display
const output = document.getElementById('change-event-output');
if (output) {
output.textContent = `Employee ID: "${value}", Name: "${itemText}"`;
}
};

return html`
<div style="display: flex; flex-direction: column; gap: 16px;">
<sp-field-label for="combobox-contact-test">
Select a team member
</sp-field-label>
<sp-combobox
id="combobox-contact-test"
.options=${contactOptions}
@change=${handleChange}
style="width: 280px;"
></sp-combobox>
<div>
<strong>Selected:</strong>
<code id="change-event-output">None yet</code>
</div>
<p style="color: gray; font-size: 12px;">
The change event provides both the unique employee ID (value)
and the display name (itemText) for use in your application.
</p>
</div>
`;
};
changeEventWithValueAndItemText.swc_vrt = {
skip: true,
};
Loading