Skip to content

Commit

Permalink
fix(cdk/tree): implement typeahead label
Browse files Browse the repository at this point in the history
Implement typeahead labels for Tree. Follow the established pattern that
Menu and List use.
 - implement CdkTreeNode#cdkTreeNodeTypeaheadLabel
 - implement CdkTreeNode#getLabel
  • Loading branch information
zarend committed Dec 8, 2023
1 parent ee50445 commit e90ed30
Show file tree
Hide file tree
Showing 16 changed files with 174 additions and 9 deletions.
146 changes: 144 additions & 2 deletions src/cdk/tree/tree.spec.ts
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 {ComponentFixture, TestBed} from '@angular/core/testing';
import {ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing';
import {
Component,
ErrorHandler,
Expand All @@ -15,14 +15,16 @@ import {
EventEmitter,
ViewChildren,
QueryList,
ElementRef,
} from '@angular/core';
import {CollectionViewer, DataSource} from '@angular/cdk/collections';
import {Directionality, Direction} from '@angular/cdk/bidi';
import {combineLatest, BehaviorSubject, Observable} from 'rxjs';
import {combineLatest, BehaviorSubject, Observable, of} 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 {B, C, T} from '../keycodes';

/**
* This is a cloned version of `tree.spec.ts` that contains all the same tests,
Expand Down Expand Up @@ -1285,6 +1287,91 @@ describe('CdkTree', () => {
});
});
});

describe('typeahead', () => {
describe('Tree with default configuration', () => {
let fixture: ComponentFixture<FlatTreeWithThreeNodes>;
let component: FlatTreeWithThreeNodes;

beforeEach(() => {
configureCdkTreeTestingModule([FlatTreeWithThreeNodes]);
fixture = TestBed.createComponent(FlatTreeWithThreeNodes);
fixture.detectChanges();

component = fixture.componentInstance;
});
describe(`when pressing 'b'`, () => {
beforeEach(fakeAsync(() => {
component.tree.nativeElement.dispatchEvent(createKeyboardEvent('keydown', B));
fixture.detectChanges();
tick(1000);
}));

it('focuses banana', () => {
expect(document.activeElement)
.withContext('expecting banana to be focused')
.toBe(component.treeNodes.get(1)?.nativeElement!);
});
});
});

describe('Tree with cdkTreeNodeTypeaheadlabel Input binding', () => {
let fixture: ComponentFixture<TypeaheadLabelFlatTreeWithThreeNodes>;
let component: TypeaheadLabelFlatTreeWithThreeNodes;

beforeEach(() => {
configureCdkTreeTestingModule([TypeaheadLabelFlatTreeWithThreeNodes]);
fixture = TestBed.createComponent(TypeaheadLabelFlatTreeWithThreeNodes);
fixture.detectChanges();

component = fixture.componentInstance;
});

describe(`when pressing 'b'`, () => {
beforeEach(fakeAsync(() => {
component.tree.nativeElement.dispatchEvent(createKeyboardEvent('keydown', B));
fixture.detectChanges();
tick(1000);
}));

it('focuses banana', fakeAsync(() => {
component.tree.nativeElement.dispatchEvent(createKeyboardEvent('keydown', B));
fixture.detectChanges();
tick(1000);

expect(document.activeElement)
.withContext('expecting banana to be focused')
.toBe(component.treeNodes.get(1)?.nativeElement!);
}));
});

describe(`when pressing 'c'`, () => {
beforeEach(fakeAsync(() => {
component.tree.nativeElement.dispatchEvent(createKeyboardEvent('keydown', C));
fixture.detectChanges();
tick(1000);
}));
it('does not move focus', () => {
expect(document.activeElement)
.withContext('expecting document body to be focused')
.toBe(document.body);
});
});

describe(`when pressing 't'`, () => {
beforeEach(fakeAsync(() => {
component.tree.nativeElement.dispatchEvent(createKeyboardEvent('keydown', T));
fixture.detectChanges();
tick(1000);
}));
it('focuses focuses cherry', () => {
expect(document.activeElement)
.withContext('expecting cherry to be focused')
.toBe(component.treeNodes.get(2)?.nativeElement!);
});
});
});
});
});

export class TestData {
Expand Down Expand Up @@ -1872,3 +1959,58 @@ class NestedCdkTreeAppWithTrackBy {

@ViewChild(CdkTree) tree: CdkTree<TestData>;
}

class MinimalTestData {
constructor(
public name: string,
public typeaheadLabel: string | null = null,
) {}
children: MinimalTestData[] = [];
}

@Component({
template: `
<cdk-tree #tree [dataSource]="dataSource" [childrenAccessor]="getChildren">
<cdk-tree-node #node *cdkTreeNodeDef="let node"
[cdkTreeNodeTypeaheadLabel]="node.typeaheadLabel">
{{node.name}}
</cdk-tree-node>
</cdk-tree>
`,
})
class TypeaheadLabelFlatTreeWithThreeNodes {
isExpandable = (node: MinimalTestData) => node.children.length > 0;
getChildren = (node: MinimalTestData) => node.children;

dataSource = of([
new MinimalTestData('apple'),
new MinimalTestData('banana'),
new MinimalTestData('cherry', 'typeahead'),
]);

@ViewChild('tree', {read: ElementRef}) tree: ElementRef<HTMLElement>;
@ViewChildren('node') treeNodes: QueryList<ElementRef<HTMLElement>>;
}

@Component({
template: `
<cdk-tree #tree [dataSource]="dataSource" [childrenAccessor]="getChildren">
<cdk-tree-node #node *cdkTreeNodeDef="let node">
{{node.name}}
</cdk-tree-node>
</cdk-tree>
`,
})
class FlatTreeWithThreeNodes {
isExpandable = (node: MinimalTestData) => node.children.length > 0;
getChildren = (node: MinimalTestData) => node.children;

dataSource = of([
new MinimalTestData('apple'),
new MinimalTestData('banana'),
new MinimalTestData('cherry'),
]);

@ViewChild('tree', {read: ElementRef}) tree: ElementRef<HTMLElement>;
@ViewChildren('node') treeNodes: QueryList<ElementRef<HTMLElement>>;
}
10 changes: 10 additions & 0 deletions src/cdk/tree/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1151,6 +1151,16 @@ export class CdkTreeNode<T, K = T> implements OnDestroy, OnInit, TreeKeyManagerI
*/
@Input({transform: booleanAttribute}) isDisabled: boolean;

/**
* The text used to locate this item during typeahead. If not specified, the `textContent` will
* will be used.
*/
@Input('cdkTreeNodeTypeaheadLabel') typeaheadLabel: string | null;

getLabel(): string {
return this.typeaheadLabel || this._elementRef.nativeElement.textContent?.trim() || '';
}

/** This emits when the node has been programatically activated or activated by keyboard. */
@Output()
readonly activation: EventEmitter<T> = new EventEmitter<T>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<cdk-tree-node
*cdkTreeNodeDef="let node"
cdkTreeNodePadding
[cdkTreeNodeTypeaheadLabel]="node.raw.name"
[isExpandable]="node.isExpandable()"
(expandedChange)="onExpand(node, $event)">
<!-- Spinner when node is loading children; this replaces the expand button. -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<!-- This is the tree node template for expandable nodes -->
<cdk-tree-node *cdkTreeNodeDef="let node; when: hasChild" cdkTreeNodePadding
cdkTreeNodeToggle
[cdkTreeNodeTypeaheadLabel]="node.name"
[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 @@ -11,6 +11,7 @@
<!-- This is the tree node template for expandable nodes -->
<cdk-tree-node *cdkTreeNodeDef="let node; when: hasChild" cdkTreeNodePadding
cdkTreeNodeToggle
[cdkTreeNodeTypeaheadLabel]="node.name"
[style.display]="shouldRender(node) ? 'flex' : 'none'"
[isDisabled]="!shouldRender(node)"
[isExpandable]="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<!-- This is the tree node template for expandable nodes -->
<cdk-tree-node *cdkTreeNodeDef="let node; when: hasChild" cdkTreeNodePadding
cdkTreeNodeToggle
[cdkTreeNodeTypeaheadLabel]="node.name"
[style.display]="shouldRender(node) ? 'flex' : 'none'"
[isDisabled]="!shouldRender(node)"
[isExpandable]="node.expandable"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</cdk-tree-node>
<!-- This is the tree node template for expandable nodes -->
<cdk-tree-node *cdkTreeNodeDef="let node; when: hasChild" cdkTreeNodePadding
cdkTreeNodeToggle
cdkTreeNodeToggle [cdkTreeNodeTypeaheadLabel]="node.name"
[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 @@ -10,6 +10,7 @@
<cdk-nested-tree-node #treeNode="cdkNestedTreeNode"
cdkTreeNodeToggle
*cdkTreeNodeDef="let node; when: hasChild"
[cdkTreeNodeTypeaheadLabel]="node.name"
isExpandable
class="example-tree-node">
<button mat-icon-button [attr.aria-label]="'Toggle ' + node.name" cdkTreeNodeToggle>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<!-- This is the tree node template for expandable nodes -->
<cdk-nested-tree-node #treeNode="cdkNestedTreeNode"
cdkTreeNodeToggle
[cdkTreeNodeTypeaheadLabel]="node.name"
*cdkTreeNodeDef="let node; when: hasChild"
isExpandable
class="example-tree-node">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<!-- This is the tree node template for expandable nodes -->
<cdk-nested-tree-node #treeNode="cdkNestedTreeNode"
cdkTreeNodeToggle
[cdkTreeNodeTypeaheadLabel]="node.name"
*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,8 @@
<button mat-icon-button disabled></button>
{{node.item}}
</mat-tree-node>
<mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding matTreeNodeToggle>
<mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding matTreeNodeToggle
[cdkTreeNodeTypeaheadLabel]="node.item">
<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
Expand Up @@ -6,7 +6,8 @@
{{node.name}}
</mat-tree-node>
<!-- This is the tree node template for expandable nodes -->
<mat-tree-node *matTreeNodeDef="let node;when: hasChild" matTreeNodePadding matTreeNodeToggle>
<mat-tree-node *matTreeNodeDef="let node;when: hasChild" matTreeNodePadding matTreeNodeToggle
[cdkTreeNodeTypeaheadLabel]="node.name">
<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
Expand Up @@ -6,7 +6,8 @@
{{node.name}}
</mat-tree-node>
<!-- This is the tree node template for expandable nodes -->
<mat-tree-node *matTreeNodeDef="let node;when: hasChild" matTreeNodePadding matTreeNodeToggle>
<mat-tree-node *matTreeNodeDef="let node;when: hasChild" matTreeNodePadding matTreeNodeToggle
[cdkTreeNodeTypeaheadLabel]="node.name">
<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
Expand Up @@ -7,7 +7,7 @@

<!-- expandable node -->
<mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding matTreeNodeToggle
(expandedChange)="loadChildren(node)">
(expandedChange)="loadChildren(node)" [cdkTreeNodeTypeaheadLabel]="node.name">
<button mat-icon-button
[attr.aria-label]="'Toggle ' + node.name"
matTreeNodeToggle>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<!-- This is the tree node template for expandable nodes -->
<mat-nested-tree-node
*matTreeNodeDef="let node; when: hasChild"
matTreeNodeToggle>
matTreeNodeToggle [cdkTreeNodeTypeaheadLabel]="node.name">
<div class="mat-tree-node">
<button mat-icon-button matTreeNodeToggle
[attr.aria-label]="'Toggle ' + node.name">
Expand Down
5 changes: 4 additions & 1 deletion tools/public_api_guard/cdk/tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ export class CdkTreeNode<T, K = T> implements OnDestroy, OnInit, TreeKeyManagerI
// (undocumented)
getChildren(): CdkTreeNode<T, K>[] | Observable<CdkTreeNode<T, K>[]>;
// (undocumented)
getLabel(): string;
// (undocumented)
getParent(): CdkTreeNode<T, K> | null;
_getPositionInSet(): number;
_getSetSize(): number;
Expand Down Expand Up @@ -197,9 +199,10 @@ export class CdkTreeNode<T, K = T> implements OnDestroy, OnInit, TreeKeyManagerI
protected _tabindex: number | null;
// (undocumented)
protected _tree: CdkTree<T, K>;
typeaheadLabel: string | null;
unfocus(): void;
// (undocumented)
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkTreeNode<any, any>, "cdk-tree-node", ["cdkTreeNode"], { "role": { "alias": "role"; "required": false; }; "isExpandable": { "alias": "isExpandable"; "required": false; }; "isExpanded": { "alias": "isExpanded"; "required": false; }; "isDisabled": { "alias": "isDisabled"; "required": false; }; }, { "activation": "activation"; "expandedChange": "expandedChange"; }, never, never, true, never>;
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkTreeNode<any, any>, "cdk-tree-node", ["cdkTreeNode"], { "role": { "alias": "role"; "required": false; }; "isExpandable": { "alias": "isExpandable"; "required": false; }; "isExpanded": { "alias": "isExpanded"; "required": false; }; "isDisabled": { "alias": "isDisabled"; "required": false; }; "typeaheadLabel": { "alias": "cdkTreeNodeTypeaheadLabel"; "required": false; }; }, { "activation": "activation"; "expandedChange": "expandedChange"; }, never, never, true, never>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<CdkTreeNode<any, any>, never>;
}
Expand Down

0 comments on commit e90ed30

Please sign in to comment.