Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export class IgxComboDropDownComponent extends IgxDropDownComponent implements I
public combo = inject<IgxComboBase>(IGX_COMBO_COMPONENT);
protected comboAPI = inject(IgxComboAPIService);

private _activeDescendantId: string | null = null;

/** @hidden @internal */
@Input({ transform: booleanAttribute })
public singleMode = false;
Expand Down Expand Up @@ -54,6 +56,38 @@ export class IgxComboDropDownComponent extends IgxDropDownComponent implements I
return null;
}

/**
* @hidden @internal
*/
public override get focusedItem(): IgxDropDownItemBaseDirective | null {
return super.focusedItem;
}

/**
* @hidden @internal
* Returns a stable aria-activedescendant id, unaffected by virtual scroll position.
* The base class computes this from the live focusedItem getter, which reads from the
* children QueryList. During virtual scroll the QueryList is recycled, so the getter
* can return null mid-CD-cycle causing NG0100 in zoneless apps. The id is cached instead.
*/
public override get activeDescendant(): string | null {
return this._activeDescendantId;
}

/** @hidden @internal */
public override set focusedItem(item: IgxDropDownItemBaseDirective | null) {
if (!item) {
this._activeDescendantId = null;
Comment thread
ddaribo marked this conversation as resolved.
} else if (item.id !== undefined) {
this._activeDescendantId = item.id;
} else {
// Virtual { value, index } object passed by navigateItem() under virtual scrolling.
const resolved = this.children?.find(e => e.index === item.index);
this._activeDescendantId = resolved?.id ?? null;
}
super.focusedItem = item;
}

/**
* Get all non-header items
*
Expand Down Expand Up @@ -141,6 +175,7 @@ export class IgxComboDropDownComponent extends IgxDropDownComponent implements I
return;
}
this.comboAPI.set_selected_item(item.itemID);
this._activeDescendantId = item.id ?? null;
this._focusedItem = item;
this.combo.setActiveDescendant();
}
Expand Down
18 changes: 15 additions & 3 deletions projects/igniteui-angular/combo/src/combo/combo-item.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,17 @@ export class IgxComboItemComponent extends IgxDropDownItemComponent {
* @hidden
* @internal
*/
public get disableTransitions() {
return this.comboAPI.disableTransitions;
public override ngDoCheck(): void {
// Sync state from services once per CD cycle so template bindings return stable field values
this._selected = !this.isHeader && this.value != null && this.comboAPI.is_item_selected(this.itemID);
this._disableTransitions = this.comboAPI.disableTransitions;
}

/**
* @hidden
*/
public override get selected(): boolean {
return this.comboAPI.is_item_selected(this.itemID);
return this._selected;
}

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

/**
* @hidden
* @internal
*/
public get disableTransitions() {
return this._disableTransitions;
}

private _disableTransitions = false;

/**
* @hidden
*/
Expand Down
66 changes: 64 additions & 2 deletions projects/igniteui-angular/combo/src/combo/combo.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AsyncPipe } from '@angular/common';
import { AfterViewInit, ChangeDetectorRef, Component, DebugElement, ElementRef, Injectable, Injector, OnDestroy, OnInit, ViewChild, inject } from '@angular/core';
import { AfterViewInit, ChangeDetectorRef, Component, DebugElement, ElementRef, Injectable, Injector, OnDestroy, OnInit, ViewChild, inject, provideZonelessChangeDetection } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
import {
FormsModule, NgForm, NgModel, ReactiveFormsModule, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators
Expand Down Expand Up @@ -2712,7 +2712,7 @@ describe('igxCombo', () => {
fixture.detectChanges();
expect(combo.dropdown.headers[0].element.nativeElement.innerText).toEqual('New England')
});
it('should sort groups with diacritics correctly', async() => {
it('should sort groups with diacritics correctly', async () => {
combo.data = [
{ field: "Alaska", region: "Méxícó" },
{ field: "California", region: "Méxícó" },
Expand Down Expand Up @@ -3659,6 +3659,68 @@ describe('igxCombo', () => {
}));
});
});

describe('Zoneless', () => {
beforeEach(async () => {
TestBed.resetTestingModule();
await TestBed.configureTestingModule({
imports: [
NoopAnimationsModule,
IgxComboSampleComponent,
],
providers: [
provideZonelessChangeDetection(),
]
}).compileComponents();

fixture = TestBed.createComponent(IgxComboSampleComponent);
fixture.detectChanges();
combo = fixture.componentInstance.combo;
});

it('should not reproduce NG0100 when virtualized combo items update on scroll - issue #17310', fakeAsync(() => {
combo.open();
tick();
fixture.detectChanges();

const scrollEl = combo.virtualScrollContainer.getScroll();
expect(scrollEl).toBeTruthy();

scrollEl.scrollTop = 300;
scrollEl.dispatchEvent(new Event('scroll'));
tick(100);

expect(() => {

fixture.detectChanges();
}).not.toThrowError(/NG0100|ExpressionChangedAfterItHasBeenCheckedError/);
}));

it('should not reproduce NG0100 related to active descendant on scroll - issue #17310', fakeAsync(() => {
combo.toggle();
tick();
fixture.detectChanges();

const dropdownContent = fixture.debugElement.query(By.css(`.${CSS_CLASS_CONTENT}`));
dropdownContent.triggerEventHandler('focus', {});
tick();
fixture.detectChanges();

const activeDescendantId = combo.dropdown.activeDescendant;
expect(activeDescendantId).toBeTruthy();

fixture.detectChanges();

expect(() => {
const scrollEl = combo.virtualScrollContainer.getScroll();
scrollEl.scrollTop = 1000;
scrollEl.dispatchEvent(new Event('scroll'));

tick(100);
fixture.detectChanges();
}).not.toThrowError(/NG0100|ExpressionChangedAfterItHasBeenCheckedError/);
}));
});
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,8 @@ export abstract class IgxDropDownBaseDirective implements IDropDownList, OnInit
* This is used to update the `aria-activedescendant` attribute of
* the IgxDropDownNavigationDirective host element.
*/
public get activeDescendant (): string {
return this.focusedItem ? this.focusedItem.id : null;
public get activeDescendant (): string | null {
return this.focusedItem?.id ?? null;
}

/**
Expand Down
Loading