Skip to content

Commit aeb6f89

Browse files
fix(material/tree): Apply aria-level to all nodes (#17818)
* fix(material/tree): Apply aria-level to all nodes Previously, only leaf nodes had aria-level applied. This is an incremental change since this is an unfamiliar codebase for me. The main benefit it will have on its own is that it will allow anyone doing custom dom manipulation to know what level the node is on. Otherwise by itself there is no change in how NVDA reads nodes with children. (It currently reads them as literally "grouping"; no information about the contents is provided). This change will be necessary for a later change I'm planning, wherein the role of parent nodes will be changed from "group" to "treeitem", in accordance with how roles are applied in WAI-ARIA reference examples such as https://www.w3.org/TR/wai-aria-practices/examples/treeview/treeview-1/treeview-1b.html * change aria-level binding to one-based * change role to treeitem * always set role to treeitem * simplify logic for setting role * add follow up TODO to makr role as deprecated Co-authored-by: Annie Wang <[email protected]>
1 parent a8ab040 commit aeb6f89

File tree

5 files changed

+31
-25
lines changed

5 files changed

+31
-25
lines changed

src/cdk/tree/tree.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,15 @@ describe('CdkTree', () => {
120120
})).toBe(true);
121121
});
122122

123+
it('with the right aria-levels', () => {
124+
// add a child to the first node
125+
let data = dataSource.data;
126+
dataSource.addChild(data[0], true);
127+
128+
const ariaLevels = getNodes(treeElement).map(n => n.getAttribute('aria-level'));
129+
expect(ariaLevels).toEqual(['2', '3', '2', '2']);
130+
});
131+
123132
it('with the right data', () => {
124133
expect(dataSource.data.length).toBe(3);
125134

src/cdk/tree/tree.ts

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,18 @@ import {
2222
OnDestroy,
2323
OnInit,
2424
QueryList,
25+
TrackByFunction,
2526
ViewChild,
2627
ViewContainerRef,
27-
ViewEncapsulation,
28-
TrackByFunction
28+
ViewEncapsulation
2929
} from '@angular/core';
3030
import {
3131
BehaviorSubject,
32+
isObservable,
3233
Observable,
3334
of as observableOf,
3435
Subject,
3536
Subscription,
36-
isObservable,
3737
} from 'rxjs';
3838
import {takeUntil} from 'rxjs/operators';
3939
import {TreeControl} from './control/tree-control';
@@ -299,7 +299,7 @@ export class CdkTree<T> implements AfterContentChecked, CollectionViewer, OnDest
299299
exportAs: 'cdkTreeNode',
300300
host: {
301301
'[attr.aria-expanded]': 'isExpanded',
302-
'[attr.aria-level]': 'role === "treeitem" ? level : null',
302+
'[attr.aria-level]': 'level + 1',
303303
'[attr.role]': 'role',
304304
'class': 'cdk-tree-node',
305305
},
@@ -337,9 +337,9 @@ export class CdkTreeNode<T> implements FocusableOption, OnDestroy {
337337
}
338338

339339
/**
340-
* The role of the node should be 'group' if it's an internal node,
341-
* and 'treeitem' if it's a leaf node.
340+
* The role of the node should always be 'treeitem'.
342341
*/
342+
// TODO: mark as deprecated
343343
@Input() role: 'treeitem' | 'group' = 'treeitem';
344344

345345
constructor(protected _elementRef: ElementRef<HTMLElement>,
@@ -364,24 +364,11 @@ export class CdkTreeNode<T> implements FocusableOption, OnDestroy {
364364
this._elementRef.nativeElement.focus();
365365
}
366366

367+
// TODO: role should eventually just be set in the component host
367368
protected _setRoleFromData(): void {
368-
if (this._tree.treeControl.isExpandable) {
369-
this.role = this._tree.treeControl.isExpandable(this._data) ? 'group' : 'treeitem';
370-
} else {
371-
if (!this._tree.treeControl.getChildren) {
372-
throw getTreeControlFunctionsMissingError();
373-
}
374-
const childrenNodes = this._tree.treeControl.getChildren(this._data);
375-
if (Array.isArray(childrenNodes)) {
376-
this._setRoleFromChildren(childrenNodes as T[]);
377-
} else if (isObservable(childrenNodes)) {
378-
childrenNodes.pipe(takeUntil(this._destroyed))
379-
.subscribe(children => this._setRoleFromChildren(children));
380-
}
369+
if (!this._tree.treeControl.isExpandable && !this._tree.treeControl.getChildren) {
370+
throw getTreeControlFunctionsMissingError();
381371
}
382-
}
383-
384-
protected _setRoleFromChildren(children: T[]) {
385-
this.role = children && children.length ? 'group' : 'treeitem';
372+
this.role = 'treeitem';
386373
}
387374
}

src/material/tree/node.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const _MatTreeNodeMixinBase: HasTabIndexCtor & CanDisableCtor & typeof CdkTreeNo
4444
inputs: ['disabled', 'tabIndex'],
4545
host: {
4646
'[attr.aria-expanded]': 'isExpanded',
47-
'[attr.aria-level]': 'role === "treeitem" ? level : null',
47+
'[attr.aria-level]': 'level + 1',
4848
'[attr.role]': 'role',
4949
'class': 'mat-tree-node'
5050
},

src/material/tree/tree.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,17 @@ describe('MatTree', () => {
6464
});
6565
});
6666

67+
it('with the right aria-level attrs', () => {
68+
// add a child to the first node
69+
let data = underlyingDataSource.data;
70+
underlyingDataSource.addChild(data[2]);
71+
component.treeControl.expandAll();
72+
fixture.detectChanges();
73+
74+
const ariaLevels = getNodes(treeElement).map(n => n.getAttribute('aria-level'));
75+
expect(ariaLevels).toEqual(['1', '1', '1', '2']);
76+
});
77+
6778
it('with the right data', () => {
6879
expect(underlyingDataSource.data.length).toBe(3);
6980

tools/public_api_guard/cdk/tree.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ export declare class CdkTreeNode<T> implements FocusableOption, OnDestroy {
7474
get level(): number;
7575
role: 'treeitem' | 'group';
7676
constructor(_elementRef: ElementRef<HTMLElement>, _tree: CdkTree<T>);
77-
protected _setRoleFromChildren(children: T[]): void;
7877
protected _setRoleFromData(): void;
7978
focus(): void;
8079
ngOnDestroy(): void;

0 commit comments

Comments
 (0)