Skip to content

Commit

Permalink
fix(cdk/tree): CdkTreeNodeToggle responds to Enter and Space keys
Browse files Browse the repository at this point in the history
Implement keydown event for CdkTreeNodeToggle to perform that same
action as click when receiving Enter or Space. Update examples to
expand/collapse nodes when pressing enter.

Fix a11y issue in demos where pressing Enter when focused on a tree node
seems to not perform any action. Use CdkTreeNodeToggle to perform the
action of expanding or collaping the node.

Align with instructions in [APG Tree View
Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/):

"Enter: activates a node, i.e., performs its default action. For parent
nodes, one possible default action is to open or close the node. In
single-select trees where selection does not follow focus (see note
below), the default action is typically to select the focused node."
  • Loading branch information
zarend committed Aug 11, 2023
1 parent 2098b41 commit 78241a5
Show file tree
Hide file tree
Showing 18 changed files with 48 additions and 20 deletions.
2 changes: 2 additions & 0 deletions src/cdk/tree/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ ng_test_library(
":tree",
"//src/cdk/bidi",
"//src/cdk/collections",
"//src/cdk/keycodes",
"//src/cdk/testing/testbed",
"@npm//rxjs",
],
)
Expand Down
12 changes: 12 additions & 0 deletions src/cdk/tree/toggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,21 @@

import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
import {Directive, Input} from '@angular/core';
import {ENTER, SPACE} from '@angular/cdk/keycodes';

import {CdkTree, CdkTreeNode} from './tree';

/**
* Node toggle to expand/collapse the node.
*
* CdkTreeNodeToggle is intended only to be used on native button elements, elements with button role,
* or elements with treeitem role.
*/
@Directive({
selector: '[cdkTreeNodeToggle]',
host: {
'(click)': '_toggle($event)',
'(keydown)': '_toggleOnEnterOrSpace($event)',
'tabindex': '-1',
},
})
Expand All @@ -41,4 +46,11 @@ export class CdkTreeNodeToggle<T, K = T> {

event.stopPropagation();
}

_toggleOnEnterOrSpace(event: KeyboardEvent) {
if (event.keyCode === ENTER || event.keyCode === SPACE) {
this._toggle(event);
event.preventDefault();
}
}
}
8 changes: 5 additions & 3 deletions src/cdk/tree/tree-redesign.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ import {
ViewChildren,
QueryList,
} from '@angular/core';

import {CollectionViewer, DataSource} from '@angular/cdk/collections';
import {Directionality, Direction} from '@angular/cdk/bidi';
import {combineLatest, BehaviorSubject, Observable} from 'rxjs';
import {map} from 'rxjs/operators';

import {CdkTreeModule, CdkTreeNodePadding} from './index';
import {CdkTree, CdkTreeNode} from './tree';
import {createKeyboardEvent} from '@angular/cdk/testing/testbed/fake-events';
import {ENTER} from '@angular/cdk/keycodes';

/**
* This is a cloned version of `tree.spec.ts` that contains all the same tests,
Expand Down Expand Up @@ -293,7 +293,9 @@ describe('CdkTree redesign', () => {
[_, `${data[3].pizzaTopping} - ${data[3].pizzaCheese} + ${data[3].pizzaBase}`],
);

(getNodes(treeElement)[2] as HTMLElement).click();
(getNodes(treeElement)[2] as HTMLElement)!.dispatchEvent(
createKeyboardEvent('keydown', ENTER),
);
fixture.detectChanges();

const expandedNodes = getExpandedNodes(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
</cdk-tree-node>
<!-- This is the tree node template for expandable nodes -->
<cdk-tree-node *cdkTreeNodeDef="let node; when: hasChild" cdkTreeNodePadding
cdkTreeNodeToggle
[style.display]="shouldRender(node) ? 'flex' : 'none'"
[isExpandable]="true"
class="example-tree-node">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
</cdk-tree-node>
<!-- This is the tree node template for expandable nodes -->
<cdk-tree-node *cdkTreeNodeDef="let node; when: hasChild" cdkTreeNodePadding
cdkTreeNodeToggle
[style.display]="shouldRender(node) ? 'flex' : 'none'"
[isExpandable]="node.expandable"
class="example-tree-node">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
</cdk-tree-node>
<!-- This is the tree node template for expandable nodes -->
<cdk-tree-node *cdkTreeNodeDef="let node; when: hasChild" cdkTreeNodePadding
cdkTreeNodeToggle
[style.display]="shouldRender(node) ? 'flex' : 'none'"
[isDisabled]="!shouldRender(node)"
(expandedChange)="node.isExpanded = $event"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<!-- This is the tree node template for expandable nodes -->
<cdk-nested-tree-node *cdkTreeNodeDef="let node; when: hasChild"
[isExpandable]="node.expandable"
class="example-tree-node example-expandable">
class="example-tree-node example-expandable" cdkTreeNodeToggle>
<button mat-icon-button cdkTreeNodeToggle
[attr.aria-label]="'Toggle ' + node.name"
[style.visibility]="node.expandable ? 'visible' : 'hidden'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</cdk-nested-tree-node>
<!-- This is the tree node template for expandable nodes -->
<cdk-nested-tree-node #treeNode="cdkNestedTreeNode"
cdkTreeNodeToggle
*cdkTreeNodeDef="let node; when: hasChild"
isExpandable
class="example-tree-node">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<button mat-icon-button disabled></button>
{{node.item}}
</mat-tree-node>
<mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding>
<mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding cdkTreeNodeToggle>
<button mat-icon-button
[attr.aria-label]="'Toggle ' + node.item" matTreeNodeToggle>
<mat-icon class="mat-icon-rtl-mirror">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {CollectionViewer, SelectionChange, DataSource} from '@angular/cdk/collections';
import {FlatTreeControl} from '@angular/cdk/tree';
import {CdkTreeModule, FlatTreeControl} from '@angular/cdk/tree';
import {Component, Injectable} from '@angular/core';
import {BehaviorSubject, merge, Observable} from 'rxjs';
import {map} from 'rxjs/operators';
Expand Down Expand Up @@ -142,7 +142,14 @@ export class DynamicDataSource implements DataSource<DynamicFlatNode> {
templateUrl: 'tree-dynamic-example.html',
styleUrls: ['tree-dynamic-example.css'],
standalone: true,
imports: [MatTreeModule, MatButtonModule, MatIconModule, NgIf, MatProgressBarModule],
imports: [
MatTreeModule,
MatButtonModule,
MatIconModule,
NgIf,
MatProgressBarModule,
CdkTreeModule,
],
})
export class TreeDynamicExample {
constructor(database: DynamicDatabase) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
{{node.name}}
</mat-tree-node>
<!-- This is the tree node template for expandable nodes -->
<mat-tree-node *matTreeNodeDef="let node;when: hasChild" matTreeNodePadding>
<mat-tree-node *matTreeNodeDef="let node;when: hasChild" matTreeNodePadding matTreeNodeToggle>
<button mat-icon-button matTreeNodeToggle
[attr.aria-label]="'Toggle ' + node.name">
<mat-icon class="mat-icon-rtl-mirror">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {FlatTreeControl} from '@angular/cdk/tree';
import {CdkTreeModule, FlatTreeControl} from '@angular/cdk/tree';
import {Component} from '@angular/core';
import {MatTreeFlatDataSource, MatTreeFlattener, MatTreeModule} from '@angular/material/tree';
import {MatIconModule} from '@angular/material/icon';
Expand Down Expand Up @@ -47,7 +47,7 @@ interface ExampleFlatNode {
selector: 'tree-flat-overview-example',
templateUrl: 'tree-flat-overview-example.html',
standalone: true,
imports: [MatTreeModule, MatButtonModule, MatIconModule],
imports: [MatTreeModule, MatButtonModule, MatIconModule, CdkTreeModule],
})
export class TreeFlatOverviewExample {
private _transformer = (node: FoodNode, level: number) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
{{node.name}}
</mat-tree-node>
<!-- This is the tree node template for expandable nodes -->
<mat-tree-node *matTreeNodeDef="let node;when: hasChild" matTreeNodePadding>
<mat-tree-node *matTreeNodeDef="let node;when: hasChild" matTreeNodePadding cdkTreeNodeToggle>
<button mat-icon-button matTreeNodeToggle
[attr.aria-label]="'Toggle ' + node.name">
<mat-icon class="mat-icon-rtl-mirror">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {FlatTreeControl} from '@angular/cdk/tree';
import {CdkTreeModule, FlatTreeControl} from '@angular/cdk/tree';
import {Component} from '@angular/core';
import {MatTreeFlatDataSource, MatTreeFlattener, MatTreeModule} from '@angular/material/tree';
import {MatIconModule} from '@angular/material/icon';
Expand Down Expand Up @@ -38,7 +38,7 @@ interface ExampleFlatNode {
selector: 'tree-harness-example',
templateUrl: 'tree-harness-example.html',
standalone: true,
imports: [MatTreeModule, MatButtonModule, MatIconModule],
imports: [MatTreeModule, MatButtonModule, MatIconModule, CdkTreeModule],
})
export class TreeHarnessExample {
private _transformer = (node: Node, level: number) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</mat-tree-node>

<!-- expandable node -->
<mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding>
<mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding cdkTreeNodeToggle>
<button mat-icon-button
[attr.aria-label]="'Toggle ' + node.item"
(click)="loadChildren(node)"
Expand All @@ -18,7 +18,7 @@
{{node.item}}
</mat-tree-node>

<mat-tree-node *matTreeNodeDef="let node; when: isLoadMore">
<mat-tree-node *matTreeNodeDef="let node; when: isLoadMore" cdkTreeNodeToggle>
<button mat-button (click)="loadMore(node.loadMoreParentItem)">
Load more...
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {FlatTreeControl} from '@angular/cdk/tree';
import {CdkTreeModule, FlatTreeControl} from '@angular/cdk/tree';
import {Component, Injectable} from '@angular/core';
import {MatTreeFlatDataSource, MatTreeFlattener, MatTreeModule} from '@angular/material/tree';
import {BehaviorSubject, Observable} from 'rxjs';
Expand Down Expand Up @@ -102,7 +102,7 @@ export class LoadmoreDatabase {
templateUrl: 'tree-loadmore-example.html',
providers: [LoadmoreDatabase],
standalone: true,
imports: [MatTreeModule, MatButtonModule, MatIconModule],
imports: [MatTreeModule, MatButtonModule, MatIconModule, CdkTreeModule],
})
export class TreeLoadmoreExample {
nodeMap = new Map<string, LoadmoreFlatNode>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
<!-- This is the tree node template for leaf nodes -->
<!-- There is inline padding applied to this node using styles.
This padding value depends on the mat-icon-button width. -->
<mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle>
<mat-tree-node *matTreeNodeDef="let node">
{{node.name}}
</mat-tree-node>
<!-- This is the tree node template for expandable nodes -->
<mat-nested-tree-node
*matTreeNodeDef="let node; when: hasChild"
matTreeNodeToggle
isExpandable>
<div class="mat-tree-node">
<button mat-icon-button matTreeNodeToggle
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {NestedTreeControl} from '@angular/cdk/tree';
import {CdkTreeModule, NestedTreeControl} from '@angular/cdk/tree';
import {Component} from '@angular/core';
import {MatTreeNestedDataSource, MatTreeModule} from '@angular/material/tree';
import {MatIconModule} from '@angular/material/icon';
Expand Down Expand Up @@ -41,7 +41,7 @@ const TREE_DATA: FoodNode[] = [
templateUrl: 'tree-nested-overview-example.html',
styleUrls: ['tree-nested-overview-example.css'],
standalone: true,
imports: [MatTreeModule, MatButtonModule, MatIconModule],
imports: [MatTreeModule, MatButtonModule, MatIconModule, CdkTreeModule],
})
export class TreeNestedOverviewExample {
treeControl = new NestedTreeControl<FoodNode>(node => node.children);
Expand Down

0 comments on commit 78241a5

Please sign in to comment.