Skip to content

Commit b9681be

Browse files
fix(dropdown): handle activeDescendant aria attribute manually in virtual dropdown (#17374)
* test(dropdown): Add zoneless test for virtual dropdown * fix(dropdown): manually handle activeDescendant in virtual mode dropdown * test(dropdown): add test for aria-activedescendant updates during navigation * test(dropdown): add test for aria-activedescendant updates during navigation with scroll in virtualized dropdown --------- Co-authored-by: Viktor Kombov <75325639+viktorkombov@users.noreply.github.com>
1 parent 7f7ce99 commit b9681be

3 files changed

Lines changed: 114 additions & 6 deletions

File tree

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

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

21-
private _activeDescendantId: string | null = null;
22-
2321
/** @hidden @internal */
2422
@Input({ transform: booleanAttribute })
2523
public singleMode = false;

projects/igniteui-angular/drop-down/src/drop-down/drop-down.component.spec.ts

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, ViewChild, OnInit, ElementRef, ViewChildren, QueryList, ChangeDetectorRef, DOCUMENT, ChangeDetectionStrategy } from '@angular/core';
1+
import { Component, ViewChild, OnInit, ElementRef, ViewChildren, QueryList, ChangeDetectorRef, DOCUMENT, ChangeDetectionStrategy, provideZonelessChangeDetection } from '@angular/core';
22
import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
33
import { By } from '@angular/platform-browser';
44
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
@@ -46,7 +46,7 @@ describe('IgxDropDown ', () => {
4646
const mockCdr = jasmine.createSpyObj('ChangeDetectorRef', ['markForCheck', 'detectChanges']);
4747
mockSelection.get.and.returnValue(new Set([]));
4848
const mockForOf = jasmine.createSpyObj('IgxForOfDirective', ['totalItemCount']);
49-
const mockDocument = jasmine.createSpyObj('DOCUMENT', [], { 'defaultView': { getComputedStyle: () => null }});
49+
const mockDocument = jasmine.createSpyObj('DOCUMENT', [], { 'defaultView': { getComputedStyle: () => null } });
5050

5151
beforeEach(() => {
5252
TestBed.configureTestingModule({
@@ -855,7 +855,7 @@ describe('IgxDropDown ', () => {
855855

856856
const itemToClick = fixture.debugElement.queryAll(By.css(`.${CSS_CLASS_ITEM}`))[0];
857857

858-
const event = new Event('mousedown', { });
858+
const event = new Event('mousedown', {});
859859
spyOn(event, 'preventDefault');
860860
itemToClick.triggerEventHandler('mousedown', event);
861861

@@ -1032,6 +1032,98 @@ describe('IgxDropDown ', () => {
10321032
expect(expectedScroll - acceptableDelta < scrollTop && expectedScroll + acceptableDelta > scrollTop).toBe(true);
10331033
});
10341034
});
1035+
describe('Zoneless virtualization tests', () => {
1036+
let scroll: IgxForOfDirective<any>;
1037+
beforeEach(async () => {
1038+
TestBed.resetTestingModule();
1039+
await TestBed.configureTestingModule({
1040+
imports: [
1041+
NoopAnimationsModule,
1042+
VirtualizedDropDownComponent
1043+
],
1044+
providers: [provideZonelessChangeDetection()]
1045+
}).compileComponents();
1046+
fixture = TestBed.createComponent(VirtualizedDropDownComponent);
1047+
fixture.detectChanges();
1048+
dropdown = fixture.componentInstance.dropdown;
1049+
scroll = fixture.componentInstance.virtualScroll;
1050+
});
1051+
it('should not throw when scrolling after selecting an item', async () => {
1052+
const preSelected = { value: fixture.componentInstance.items[0], index: 0 } as IgxDropDownItemBaseDirective;
1053+
dropdown.selectItem(preSelected);
1054+
1055+
dropdown.toggle();
1056+
await wait(50);
1057+
fixture.detectChanges();
1058+
1059+
scroll.getScroll().scrollTop = scroll.getScroll().scrollHeight;
1060+
await wait(50);
1061+
1062+
expect(() => fixture.detectChanges()).not.toThrow();
1063+
});
1064+
1065+
it('should update aria-activedescendant to the id of the focused item in virtualized dropdown when navigating', async () => {
1066+
const preSelected = { value: fixture.componentInstance.items[0], index: 0 } as IgxDropDownItemBaseDirective;
1067+
dropdown.selectItem(preSelected);
1068+
dropdown.toggle();
1069+
await wait(50);
1070+
fixture.detectChanges();
1071+
1072+
const targetElement = fixture.debugElement.query(By.directive(IgxButtonDirective)).nativeElement;
1073+
let focusedItem = fixture.debugElement.query(By.css(`.${CSS_CLASS_FOCUSED}`)).nativeElement;
1074+
1075+
expect(focusedItem).toBeTruthy();
1076+
let focusedItemId = focusedItem.getAttribute('id');
1077+
expect(focusedItemId).toBeTruthy();
1078+
expect(targetElement.getAttribute('aria-activedescendant')).toBe(focusedItemId);
1079+
1080+
dropdown.navigateNext();
1081+
await wait(50);
1082+
fixture.detectChanges();
1083+
1084+
focusedItem = fixture.debugElement.query(By.css(`.${CSS_CLASS_FOCUSED}`)).nativeElement;
1085+
focusedItemId = focusedItem.getAttribute('id');
1086+
expect(targetElement.getAttribute('aria-activedescendant')).toBe(focusedItemId);
1087+
1088+
dropdown.navigateFirst();
1089+
await wait(50);
1090+
fixture.detectChanges();
1091+
focusedItem = fixture.debugElement.query(By.css(`.${CSS_CLASS_FOCUSED}`)).nativeElement;
1092+
focusedItemId = focusedItem.getAttribute('id');
1093+
expect(targetElement.getAttribute('aria-activedescendant')).toBe(focusedItemId);
1094+
});
1095+
1096+
it('should update aria-activedescendant to the id of the focused item in virtualized dropdown when navigating with scrolling', async () => {
1097+
const preSelected = { value: fixture.componentInstance.items[0], index: 0 } as IgxDropDownItemBaseDirective;
1098+
dropdown.selectItem(preSelected);
1099+
dropdown.toggle();
1100+
await wait(50);
1101+
fixture.detectChanges();
1102+
1103+
const targetElement = fixture.debugElement.query(By.directive(IgxButtonDirective)).nativeElement;
1104+
let focusedItem = fixture.debugElement.query(By.css(`.${CSS_CLASS_FOCUSED}`)).nativeElement;
1105+
1106+
expect(focusedItem).toBeTruthy();
1107+
let focusedItemId = focusedItem.getAttribute('id');
1108+
expect(focusedItemId).toBeTruthy();
1109+
expect(targetElement.getAttribute('aria-activedescendant')).toBe(focusedItemId);
1110+
1111+
dropdown.navigateLast();
1112+
await wait(50);
1113+
fixture.detectChanges();
1114+
1115+
focusedItem = fixture.debugElement.query(By.css(`.${CSS_CLASS_FOCUSED}`)).nativeElement;
1116+
focusedItemId = focusedItem.getAttribute('id');
1117+
expect(targetElement.getAttribute('aria-activedescendant')).toBe(focusedItemId);
1118+
1119+
dropdown.navigateFirst();
1120+
await wait(50);
1121+
fixture.detectChanges();
1122+
focusedItem = fixture.debugElement.query(By.css(`.${CSS_CLASS_FOCUSED}`)).nativeElement;
1123+
focusedItemId = focusedItem.getAttribute('id');
1124+
expect(targetElement.getAttribute('aria-activedescendant')).toBe(focusedItemId);
1125+
});
1126+
});
10351127
describe('Rendering', () => {
10361128
describe('Accessibility', () => {
10371129
beforeEach(waitForAsync(() => {

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { IgxSelectionAPIService } from 'igniteui-angular/core';
2828
import { Subject } from 'rxjs';
2929
import { IgxDropDownItemBaseDirective } from './drop-down-item.base';
3030
import { IgxForOfToken } from 'igniteui-angular/directives';
31-
import { take } from 'rxjs/operators';
31+
import { take, takeUntil } from 'rxjs/operators';
3232
import { OverlaySettings } from 'igniteui-angular/core';
3333
import { ConnectedPositioningStrategy } from 'igniteui-angular/core';
3434

@@ -58,6 +58,7 @@ import { ConnectedPositioningStrategy } from 'igniteui-angular/core';
5858
})
5959
export class IgxDropDownComponent extends IgxDropDownBaseDirective implements IDropDownBase, OnChanges, AfterViewInit, OnDestroy {
6060
protected selection = inject(IgxSelectionAPIService);
61+
protected _activeDescendantId: string | null = null;
6162

6263
/**
6364
* @hidden
@@ -171,6 +172,7 @@ export class IgxDropDownComponent extends IgxDropDownBaseDirective implements ID
171172
if (!value) {
172173
this.selection.clear(`${this.id}-active`);
173174
this._focusedItem = null;
175+
this._activeDescendantId = null;
174176
return;
175177
}
176178
this._focusedItem = value;
@@ -183,6 +185,13 @@ export class IgxDropDownComponent extends IgxDropDownBaseDirective implements ID
183185
this.selection.set(`${this.id}-active`, new Set([this._focusedItem]));
184186
}
185187

188+
public override get activeDescendant(): string | null {
189+
if (this.virtDir) {
190+
return this._activeDescendantId;
191+
}
192+
return super.activeDescendant;
193+
}
194+
186195
public override get id(): string {
187196
return this._id;
188197
}
@@ -335,6 +344,7 @@ export class IgxDropDownComponent extends IgxDropDownBaseDirective implements ID
335344
this.skipHeader(direction);
336345
});
337346
} else {
347+
this._activeDescendantId = this.children.find(e => e.index === index)?.id ?? null;
338348
this.skipHeader(direction);
339349
}
340350
} else {
@@ -461,6 +471,13 @@ export class IgxDropDownComponent extends IgxDropDownBaseDirective implements ID
461471
public ngAfterViewInit() {
462472
if (this.virtDir) {
463473
this.virtDir.igxForItemSize = 28;
474+
this.virtDir.chunkLoad.pipe(takeUntil(this.destroy$)).subscribe(() => {
475+
const item = this._focusedItem
476+
? this.children.find(e => e.index === this._focusedItem.index)
477+
: null;
478+
this._activeDescendantId = item?.id ?? null;
479+
this.cdr.markForCheck();
480+
});
464481
}
465482
}
466483

@@ -610,6 +627,7 @@ export class IgxDropDownComponent extends IgxDropDownBaseDirective implements ID
610627
protected updateItemFocus() {
611628
if (this.selectedItem) {
612629
this.focusedItem = this.selectedItem;
630+
this._activeDescendantId = this.focusedItem?.id ?? null;
613631
this.focusItem(true);
614632
} else if (this.allowItemsFocus) {
615633
this.navigateFirst();

0 commit comments

Comments
 (0)