From e8802e3d2f502ccd4bd839cf8aad76e2b0c56da2 Mon Sep 17 00:00:00 2001 From: lskramarov Date: Thu, 4 Jun 2026 14:34:26 +0300 Subject: [PATCH] fix(select): guard against null match elements in hidden-items calc (#DS-5115) --- .../select/select.component.spec.ts | 39 +++++++++++++++++++ .../components/select/select.component.ts | 13 ++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/packages/components/select/select.component.spec.ts b/packages/components/select/select.component.spec.ts index 9d38437a2..5d20b9530 100644 --- a/packages/components/select/select.component.spec.ts +++ b/packages/components/select/select.component.spec.ts @@ -5364,6 +5364,45 @@ describe('KbqSelect', () => { while (restore.length) restore.pop()!(); } })); + + it('should not throw when the hidden-text element is not yet in the DOM', fakeAsync(() => { + // Regression: in multiple mode with preset values restored programmatically, + // `calculateHiddenItems` may run (via setTimeout) before the trigger view has + // materialized `.kbq-select__match-hidden-text`. `querySelector` then returns null, + // and on Angular 20 `Renderer2.setStyle(null, ...)` throws. We must tolerate the + // missing element instead of crashing. + const originalQuerySelector = HTMLElement.prototype.querySelector; + + const fixtureTest = TestBed.createComponent(MultiSelectNarrow); + const componentInstance: MultiSelectNarrow = fixtureTest.componentInstance; + const triggerEl = fixtureTest.debugElement.query(By.css('.kbq-select__trigger')).nativeElement; + + fixtureTest.detectChanges(); + + triggerEl.click(); + fixtureTest.detectChanges(); + + const options: NodeListOf = overlayContainerElement.querySelectorAll('kbq-option'); + + options.item(0)?.click(); + options.item(1)?.click(); + + fixtureTest.detectChanges(); + tick(); + flush(); + + HTMLElement.prototype.querySelector = function (selectors: string) { + if (selectors === '.kbq-select__match-hidden-text') return null; + + return originalQuerySelector.call(this, selectors); + } as typeof HTMLElement.prototype.querySelector; + + try { + expect(() => componentInstance.select().calculateHiddenItems()).not.toThrow(); + } finally { + HTMLElement.prototype.querySelector = originalQuerySelector; + } + })); }); describe('option tooltip', () => { diff --git a/packages/components/select/select.component.ts b/packages/components/select/select.component.ts index 7056cdda5..59dd62000 100644 --- a/packages/components/select/select.component.ts +++ b/packages/components/select/select.component.ts @@ -1317,6 +1317,12 @@ export class KbqSelect const itemsCounter = this.trigger()!.nativeElement.querySelector('.kbq-select__match-hidden-text'); const matcherList = this.trigger()!.nativeElement.querySelector('.kbq-select__match-list'); + if (!itemsCounter || !matcherList) { + this._changeDetectorRef.markForCheck(); + + return; + } + const itemsCounterShowed = itemsCounter.offsetTop < itemsCounter.offsetHeight; const itemsCounterWidth: number = Math.floor(itemsCounter.getBoundingClientRect().width); @@ -1880,7 +1886,12 @@ export class KbqSelect private getTotalVisibleItems(): [number, number] { const triggerClone = this.buildTriggerClone(); - this._renderer.setStyle(triggerClone.querySelector('.kbq-select__match-hidden-text'), 'display', 'block'); + const hiddenText = triggerClone.querySelector('.kbq-select__match-hidden-text'); + + if (hiddenText) { + this._renderer.setStyle(hiddenText, 'display', 'block'); + } + this._renderer.appendChild(this.trigger()!.nativeElement, triggerClone); let visibleItemsCount: number = 0;