Skip to content

Commit 4707cff

Browse files
authored
Merge branch '21.2.x' into ganastasov/fix-17275-21.2.x
2 parents b447ba8 + bde6e04 commit 4707cff

4 files changed

Lines changed: 116 additions & 7 deletions

File tree

projects/igniteui-angular/combo/src/combo/combo-dropdown.component.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export class IgxComboDropDownComponent extends IgxDropDownComponent implements I
1717
public combo = inject<IgxComboBase>(IGX_COMBO_COMPONENT);
1818
protected comboAPI = inject(IgxComboAPIService);
1919

20+
private _activeDescendantId: string | null = null;
21+
2022
/** @hidden @internal */
2123
@Input({ transform: booleanAttribute })
2224
public singleMode = false;
@@ -54,6 +56,38 @@ export class IgxComboDropDownComponent extends IgxDropDownComponent implements I
5456
return null;
5557
}
5658

59+
/**
60+
* @hidden @internal
61+
*/
62+
public override get focusedItem(): IgxDropDownItemBaseDirective | null {
63+
return super.focusedItem;
64+
}
65+
66+
/**
67+
* @hidden @internal
68+
* Returns a stable aria-activedescendant id, unaffected by virtual scroll position.
69+
* The base class computes this from the live focusedItem getter, which reads from the
70+
* children QueryList. During virtual scroll the QueryList is recycled, so the getter
71+
* can return null mid-CD-cycle causing NG0100 in zoneless apps. The id is cached instead.
72+
*/
73+
public override get activeDescendant(): string | null {
74+
return this._activeDescendantId;
75+
}
76+
77+
/** @hidden @internal */
78+
public override set focusedItem(item: IgxDropDownItemBaseDirective | null) {
79+
if (!item) {
80+
this._activeDescendantId = null;
81+
} else if (item.id !== undefined) {
82+
this._activeDescendantId = item.id;
83+
} else {
84+
// Virtual { value, index } object passed by navigateItem() under virtual scrolling.
85+
const resolved = this.children?.find(e => e.index === item.index);
86+
this._activeDescendantId = resolved?.id ?? null;
87+
}
88+
super.focusedItem = item;
89+
}
90+
5791
/**
5892
* Get all non-header items
5993
*
@@ -141,6 +175,7 @@ export class IgxComboDropDownComponent extends IgxDropDownComponent implements I
141175
return;
142176
}
143177
this.comboAPI.set_selected_item(item.itemID);
178+
this._activeDescendantId = item.id ?? null;
144179
this._focusedItem = item;
145180
this.combo.setActiveDescendant();
146181
}

projects/igniteui-angular/combo/src/combo/combo-item.component.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,17 @@ export class IgxComboItemComponent extends IgxDropDownItemComponent {
6666
* @hidden
6767
* @internal
6868
*/
69-
public get disableTransitions() {
70-
return this.comboAPI.disableTransitions;
69+
public override ngDoCheck(): void {
70+
// Sync state from services once per CD cycle so template bindings return stable field values
71+
this._selected = !this.isHeader && this.value != null && this.comboAPI.is_item_selected(this.itemID);
72+
this._disableTransitions = this.comboAPI.disableTransitions;
7173
}
7274

7375
/**
7476
* @hidden
7577
*/
7678
public override get selected(): boolean {
77-
return this.comboAPI.is_item_selected(this.itemID);
79+
return this._selected;
7880
}
7981

8082
public override set selected(value: boolean) {
@@ -84,6 +86,16 @@ export class IgxComboItemComponent extends IgxDropDownItemComponent {
8486
this._selected = value;
8587
}
8688

89+
/**
90+
* @hidden
91+
* @internal
92+
*/
93+
public get disableTransitions() {
94+
return this._disableTransitions;
95+
}
96+
97+
private _disableTransitions = false;
98+
8799
/**
88100
* @hidden
89101
*/

projects/igniteui-angular/combo/src/combo/combo.component.spec.ts

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { AsyncPipe } from '@angular/common';
2-
import { AfterViewInit, ChangeDetectorRef, Component, DebugElement, ElementRef, Injectable, Injector, OnDestroy, OnInit, ViewChild, inject } from '@angular/core';
2+
import { AfterViewInit, ChangeDetectorRef, Component, DebugElement, ElementRef, Injectable, Injector, OnDestroy, OnInit, ViewChild, inject, provideZonelessChangeDetection } from '@angular/core';
33
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
44
import {
55
FormsModule, NgForm, NgModel, ReactiveFormsModule, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators
@@ -2712,7 +2712,7 @@ describe('igxCombo', () => {
27122712
fixture.detectChanges();
27132713
expect(combo.dropdown.headers[0].element.nativeElement.innerText).toEqual('New England')
27142714
});
2715-
it('should sort groups with diacritics correctly', async() => {
2715+
it('should sort groups with diacritics correctly', async () => {
27162716
combo.data = [
27172717
{ field: "Alaska", region: "Méxícó" },
27182718
{ field: "California", region: "Méxícó" },
@@ -3659,6 +3659,68 @@ describe('igxCombo', () => {
36593659
}));
36603660
});
36613661
});
3662+
3663+
describe('Zoneless', () => {
3664+
beforeEach(async () => {
3665+
TestBed.resetTestingModule();
3666+
await TestBed.configureTestingModule({
3667+
imports: [
3668+
NoopAnimationsModule,
3669+
IgxComboSampleComponent,
3670+
],
3671+
providers: [
3672+
provideZonelessChangeDetection(),
3673+
]
3674+
}).compileComponents();
3675+
3676+
fixture = TestBed.createComponent(IgxComboSampleComponent);
3677+
fixture.detectChanges();
3678+
combo = fixture.componentInstance.combo;
3679+
});
3680+
3681+
it('should not reproduce NG0100 when virtualized combo items update on scroll - issue #17310', fakeAsync(() => {
3682+
combo.open();
3683+
tick();
3684+
fixture.detectChanges();
3685+
3686+
const scrollEl = combo.virtualScrollContainer.getScroll();
3687+
expect(scrollEl).toBeTruthy();
3688+
3689+
scrollEl.scrollTop = 300;
3690+
scrollEl.dispatchEvent(new Event('scroll'));
3691+
tick(100);
3692+
3693+
expect(() => {
3694+
3695+
fixture.detectChanges();
3696+
}).not.toThrowError(/NG0100|ExpressionChangedAfterItHasBeenCheckedError/);
3697+
}));
3698+
3699+
it('should not reproduce NG0100 related to active descendant on scroll - issue #17310', fakeAsync(() => {
3700+
combo.toggle();
3701+
tick();
3702+
fixture.detectChanges();
3703+
3704+
const dropdownContent = fixture.debugElement.query(By.css(`.${CSS_CLASS_CONTENT}`));
3705+
dropdownContent.triggerEventHandler('focus', {});
3706+
tick();
3707+
fixture.detectChanges();
3708+
3709+
const activeDescendantId = combo.dropdown.activeDescendant;
3710+
expect(activeDescendantId).toBeTruthy();
3711+
3712+
fixture.detectChanges();
3713+
3714+
expect(() => {
3715+
const scrollEl = combo.virtualScrollContainer.getScroll();
3716+
scrollEl.scrollTop = 1000;
3717+
scrollEl.dispatchEvent(new Event('scroll'));
3718+
3719+
tick(100);
3720+
fixture.detectChanges();
3721+
}).not.toThrowError(/NG0100|ExpressionChangedAfterItHasBeenCheckedError/);
3722+
}));
3723+
});
36623724
});
36633725
});
36643726

projects/igniteui-angular/drop-down/src/drop-down/drop-down.base.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,8 @@ export abstract class IgxDropDownBaseDirective implements IDropDownList, OnInit
172172
* This is used to update the `aria-activedescendant` attribute of
173173
* the IgxDropDownNavigationDirective host element.
174174
*/
175-
public get activeDescendant (): string {
176-
return this.focusedItem ? this.focusedItem.id : null;
175+
public get activeDescendant (): string | null {
176+
return this.focusedItem?.id ?? null;
177177
}
178178

179179
/**

0 commit comments

Comments
 (0)