From ec644b0f9fcc619cb0040e90ab92cfc8f8309137 Mon Sep 17 00:00:00 2001 From: Radoslav Karaivanov Date: Tue, 12 May 2026 11:37:28 +0300 Subject: [PATCH 1/2] fix(combo): Invalid data state on tab out in single selection Closes #2221 --- src/components/combo/combo.spec.ts | 30 +++++++++++++++++++ .../combo/controllers/navigation.ts | 9 ++++++ 2 files changed, 39 insertions(+) diff --git a/src/components/combo/combo.spec.ts b/src/components/combo/combo.spec.ts index c22932bbf..696e2328c 100644 --- a/src/components/combo/combo.spec.ts +++ b/src/components/combo/combo.spec.ts @@ -1417,6 +1417,36 @@ describe('Combo', () => { // The dropdown should remain open expect(combo.open).to.be.true; }); + + it('issue 2221 - invalid state when tabbing out of a single select combo', async () => { + combo.singleSelect = true; + await combo.show(); + await list.layoutComplete; + + // Has matches, selects first on tab out + await filterCombo('sof'); + expect(items(combo)).lengthOf(1); + + simulateKeyboard(input, tabKey); + await elementUpdated(combo); + + expect(combo.open).to.be.false; + expect(combo.value).to.eql(['BG01']); + + combo.deselect(); + await combo.show(); + await list.layoutComplete; + + // No matches, should clear value on tab out + await filterCombo('xxx'); + expect(items(combo)).to.be.empty; + + simulateKeyboard(input, tabKey); + await elementUpdated(combo); + + expect(combo.open).to.be.false; + expect(combo.value).to.be.empty; + }); }); describe('Form integration', () => { diff --git a/src/components/combo/controllers/navigation.ts b/src/components/combo/controllers/navigation.ts index 85f1f02b2..8a5ba0b7d 100644 --- a/src/components/combo/controllers/navigation.ts +++ b/src/components/combo/controllers/navigation.ts @@ -151,6 +151,15 @@ export class ComboNavigationController { // before the Shift+Tab behavior kicks in. this._host.focus(); } + + if (this._host.singleSelect) { + if (this.active > -1) { + this._config.interactions.select(this.active); + } else { + this._config.interactions.clearSelection(); + } + } + await this._config.interactions.hide(); } }; From b26f379a60c31827d04513af806e7c6ec0b56801 Mon Sep 17 00:00:00 2001 From: Radoslav Karaivanov Date: Tue, 12 May 2026 12:48:30 +0300 Subject: [PATCH 2/2] fix(combo): restore display value on blur in single-select mode (#2221) - Do not call clearSelection() on Tab when active === -1 in single-select mode; an existing selection is preserved when no navigation occurred - Sync the native input value after _syncValueFromSelection so the main input reflects the current selection in single-select mode - Clear the native input value on blur when there is no selection, discarding any partial search text the user typed - Add tests covering the three affected scenarios --- src/components/combo/combo.spec.ts | 54 +++++++++++++++++++ src/components/combo/combo.ts | 9 +++- .../combo/controllers/navigation.ts | 8 +-- 3 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/components/combo/combo.spec.ts b/src/components/combo/combo.spec.ts index 696e2328c..03ded9857 100644 --- a/src/components/combo/combo.spec.ts +++ b/src/components/combo/combo.spec.ts @@ -17,6 +17,7 @@ import { first } from '../common/util.js'; import { createFormAssociatedTestBed, isFocused, + simulateBlur, simulateClick, simulateKeyboard, simulatePointerDown, @@ -1447,6 +1448,59 @@ describe('Combo', () => { expect(combo.open).to.be.false; expect(combo.value).to.be.empty; }); + + it('issue 2221 - tabbing out with an existing selection preserves the selection', async () => { + combo.singleSelect = true; + combo.select('BG01'); + await elementUpdated(combo); + + expect(input.value).to.equal('Sofia'); + + await combo.show(); + await list.layoutComplete; + + simulateKeyboard(input, tabKey); + await elementUpdated(combo); + + expect(combo.open).to.be.false; + expect(combo.value).to.eql(['BG01']); + expect(input.value).to.equal('Sofia'); + }); + + it('issue 2221 - clicking outside with partial text and no selection clears the input', async () => { + combo.singleSelect = true; + await combo.show(); + await list.layoutComplete; + + await filterCombo('sof'); + expect(items(combo)).lengthOf(1); + + // Simulate click outside by dispatching blur without confirming a selection + simulateBlur(combo); + await elementUpdated(combo); + + expect(combo.value).to.be.empty; + expect(input.value).to.equal(''); + }); + + it('issue 2221 - clicking outside after partial search over existing selection clears the input', async () => { + combo.singleSelect = true; + combo.select('BG01'); + await elementUpdated(combo); + + expect(input.value).to.equal('Sofia'); + + // Typing clears the existing selection immediately + await filterCombo('sof'); + expect(combo.value).to.be.empty; + + // Clicking outside (blur) should clear the partial text + simulateBlur(combo); + await elementUpdated(combo); + + expect(combo.value).to.be.empty; + expect(input.value).to.equal(''); + }); }); describe('Form integration', () => { diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 0b0e08409..0b2c54dc1 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -643,6 +643,12 @@ export default class IgcComboComponent< return canClose; } + private _setSingleSelectionDisplayValue(value: string): void { + if (this.singleSelect && this._inputRef.value) { + this._inputRef.value.value = value; + } + } + //#endregion // #region Selection helpers @@ -817,10 +823,10 @@ export default class IgcComboComponent< this._displayValue = this._getValues(this._selected, this.displayKey).join( ', ' ); - this._formValue.setValueAndFormState(values); if (!initial) { + this._setSingleSelectionDisplayValue(this._displayValue); this._validate(); this._listRef.value?.requestUpdate(); } @@ -891,6 +897,7 @@ export default class IgcComboComponent< protected override _handleBlur(): void { if (isEmpty(this._selected)) { this._searchTerm = ''; + this._setSingleSelectionDisplayValue(''); } super._handleBlur(); } diff --git a/src/components/combo/controllers/navigation.ts b/src/components/combo/controllers/navigation.ts index 8a5ba0b7d..d602e3076 100644 --- a/src/components/combo/controllers/navigation.ts +++ b/src/components/combo/controllers/navigation.ts @@ -152,12 +152,8 @@ export class ComboNavigationController { this._host.focus(); } - if (this._host.singleSelect) { - if (this.active > -1) { - this._config.interactions.select(this.active); - } else { - this._config.interactions.clearSelection(); - } + if (this._host.singleSelect && this.active > -1) { + this._config.interactions.select(this.active); } await this._config.interactions.hide();