From 1c01f3efe679aef8b6503477a7120bcfa86493c9 Mon Sep 17 00:00:00 2001 From: Nikita Guryev Date: Thu, 2 Jul 2026 14:37:43 +0300 Subject: [PATCH 1/3] fix(tags): prevent focus-back loop in eventCoalescing mode (#DS-5236) --- .../tags/tag-list.component.spec.ts | 33 +++++++++++++++++++ .../components/tags/tag-list.component.ts | 1 + 2 files changed, 34 insertions(+) diff --git a/packages/components/tags/tag-list.component.spec.ts b/packages/components/tags/tag-list.component.spec.ts index f6392b536..ed756100d 100644 --- a/packages/components/tags/tag-list.component.spec.ts +++ b/packages/components/tags/tag-list.component.spec.ts @@ -1414,6 +1414,39 @@ describe(KbqTagList.name, () => { ); }); + it('should synchronously set native tabIndex to -1 on tabOut to prevent focus-back loop', fakeAsync(() => { + const fixture = createStandaloneComponent(TestTagList); + const { debugElement, componentInstance } = fixture; + const tagListEl = getTagListElement(debugElement); + + expect(tagListEl.tabIndex).toBe(0); + + componentInstance.tagList().keyManager.onKeydown(createKeyboardEvent('keydown', TAB)); + + // Native DOM must be -1 immediately — before any CD cycle — so the browser does not see + // the list host as a tab stop when processing the Tab key event. Without this, sync writes + // the host holds `tabIndex=0`, and the browser re-focuses it, triggering + // (focus) → setFirstItemActive() loop (reproducible with provideZoneChangeDetection({ eventCoalescing: true })). + expect(tagListEl.tabIndex).toBe(-1); + })); + + it('should restore tabIndex to userTabIndex after tabOut', fakeAsync(() => { + const fixture = createStandaloneComponent(TestTagList); + const { componentInstance } = fixture; + const tagList = componentInstance.tagList(); + + tagList.tabIndex = 3; + fixture.detectChanges(); + + tagList.keyManager.onKeydown(createKeyboardEvent('keydown', TAB)); + + expect(tagList.tabIndex).toBe(-1); + + tick(); + + expect(tagList.tabIndex).toBe(3); + })); + it('should be draggable when draggable is enabled', () => { const fixture = createStandaloneComponent(TestTagList); const { debugElement, componentInstance } = fixture; diff --git a/packages/components/tags/tag-list.component.ts b/packages/components/tags/tag-list.component.ts index fbd2de623..2cdf64617 100644 --- a/packages/components/tags/tag-list.component.ts +++ b/packages/components/tags/tag-list.component.ts @@ -457,6 +457,7 @@ export class KbqTagList // it back to the first tag when the user tabs out. this.keyManager.tabOut.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { this._tabIndex = -1; + this.elementRef.nativeElement.tabIndex = -1; setTimeout(() => { this._tabIndex = this.userTabIndex || 0; From 2a640f467eca02423df9ec6be5e9d082a2653868 Mon Sep 17 00:00:00 2001 From: Nikita Guryev Date: Thu, 2 Jul 2026 17:25:07 +0300 Subject: [PATCH 2/3] chore: after review --- packages/components/tags/tag-list.component.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/components/tags/tag-list.component.spec.ts b/packages/components/tags/tag-list.component.spec.ts index ed756100d..0480a1da5 100644 --- a/packages/components/tags/tag-list.component.spec.ts +++ b/packages/components/tags/tag-list.component.spec.ts @@ -1428,6 +1428,8 @@ describe(KbqTagList.name, () => { // the host holds `tabIndex=0`, and the browser re-focuses it, triggering // (focus) → setFirstItemActive() loop (reproducible with provideZoneChangeDetection({ eventCoalescing: true })). expect(tagListEl.tabIndex).toBe(-1); + + tick(); })); it('should restore tabIndex to userTabIndex after tabOut', fakeAsync(() => { From c275dcce773e0dcb6e8876b2fe9666370ade1f34 Mon Sep 17 00:00:00 2001 From: Nikita Guryev Date: Fri, 3 Jul 2026 10:15:10 +0300 Subject: [PATCH 3/3] chore: after review --- packages/components/tags/tag-list.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/tags/tag-list.component.ts b/packages/components/tags/tag-list.component.ts index 2cdf64617..bf8e21b6f 100644 --- a/packages/components/tags/tag-list.component.ts +++ b/packages/components/tags/tag-list.component.ts @@ -457,6 +457,7 @@ export class KbqTagList // it back to the first tag when the user tabs out. this.keyManager.tabOut.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { this._tabIndex = -1; + // Direct DOM write since the binding update is deferred with eventCoalescing. this.elementRef.nativeElement.tabIndex = -1; setTimeout(() => {