Skip to content

Commit

Permalink
fix(cdk/tree): update active item on focus
Browse files Browse the repository at this point in the history
Update CdkTreeNode, TreeKeyManager and Tree "Load More" example
regarding progrmatic focus.

 - In CdkTreeNode, update the active item on the focus event.
 - expose TreeKeyManager.updateActiveItem
 - remove TreeKeyManager.onClick
 - In "Load More" example, when clicking "Load More", focus first node
   that is added.
  • Loading branch information
zarend committed Oct 9, 2023
1 parent a34ad99 commit 704bc1d
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 117 deletions.
46 changes: 23 additions & 23 deletions src/cdk/a11y/key-manager/tree-key-manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ describe('TreeKeyManager', () => {
});

it('should maintain the active item if the amount of items changes', () => {
keyManager.onClick(itemList.get(0)!);
keyManager.setActiveItem(itemList.get(0)!);

expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(0);
expect(keyManager.getActiveItem()?.getLabel())
Expand Down Expand Up @@ -199,7 +199,7 @@ describe('TreeKeyManager', () => {
});

it('should emit an event whenever the active item changes', () => {
keyManager.onClick(itemList.get(0)!);
keyManager.setActiveItem(itemList.get(0)!);

const spy = jasmine.createSpy('change spy');
const subscription = keyManager.change.subscribe(spy);
Expand All @@ -214,14 +214,14 @@ describe('TreeKeyManager', () => {
});

it('should emit if the active item changed, but not the active index', () => {
keyManager.onClick(itemList.get(0)!);
keyManager.setActiveItem(itemList.get(0)!);

const spy = jasmine.createSpy('change spy');
const subscription = keyManager.change.subscribe(spy);

itemList.reset([new itemParam.constructor('zero'), ...itemList.toArray()]);
itemList.notifyOnChanges();
keyManager.onClick(itemList.get(0)!);
keyManager.setActiveItem(itemList.get(0)!);

expect(spy).toHaveBeenCalledTimes(1);
subscription.unsubscribe();
Expand All @@ -248,7 +248,7 @@ describe('TreeKeyManager', () => {
});

it('should not do anything for unsupported key presses', () => {
keyManager.onClick(itemList.get(1)!);
keyManager.setActiveItem(itemList.get(1)!);

expect(keyManager.getActiveItemIndex()).toBe(1);
expect(fakeKeyEvents.unsupported.defaultPrevented).toBe(false);
Expand All @@ -260,7 +260,7 @@ describe('TreeKeyManager', () => {
});

it('should focus the first item when Home is pressed', () => {
keyManager.onClick(itemList.get(1)!);
keyManager.setActiveItem(itemList.get(1)!);
expect(keyManager.getActiveItemIndex()).toBe(1);

keyManager.onKeydown(fakeKeyEvents.home);
Expand All @@ -270,7 +270,7 @@ describe('TreeKeyManager', () => {

it('should focus the first non-disabled item when Home is pressed', () => {
itemList.get(0)!.isDisabled = true;
keyManager.onClick(itemList.get(2)!);
keyManager.setActiveItem(itemList.get(2)!);
expect(keyManager.getActiveItemIndex()).toBe(2);

keyManager.onKeydown(fakeKeyEvents.home);
Expand All @@ -279,7 +279,7 @@ describe('TreeKeyManager', () => {
});

it('should focus the last item when End is pressed', () => {
keyManager.onClick(itemList.get(0)!);
keyManager.setActiveItem(itemList.get(0)!);
expect(keyManager.getActiveItemIndex()).toBe(0);

keyManager.onKeydown(fakeKeyEvents.end);
Expand All @@ -288,7 +288,7 @@ describe('TreeKeyManager', () => {

it('should focus the last non-disabled item when End is pressed', () => {
itemList.get(itemList.length - 1)!.isDisabled = true;
keyManager.onClick(itemList.get(0)!);
keyManager.setActiveItem(itemList.get(0)!);
expect(keyManager.getActiveItemIndex()).toBe(0);

keyManager.onKeydown(fakeKeyEvents.end);
Expand All @@ -299,7 +299,7 @@ describe('TreeKeyManager', () => {

describe('up/down key events', () => {
it('should set subsequent items as active when the down key is pressed', () => {
keyManager.onClick(itemList.get(0)!);
keyManager.setActiveItem(itemList.get(0)!);

const spy = jasmine.createSpy('change spy');
const subscription = keyManager.change.subscribe(spy);
Expand Down Expand Up @@ -330,7 +330,7 @@ describe('TreeKeyManager', () => {
});

it('should set previous item as active when the up key is pressed', () => {
keyManager.onClick(itemList.get(0)!);
keyManager.setActiveItem(itemList.get(0)!);

const spy = jasmine.createSpy('change spy');
const subscription = keyManager.change.subscribe(spy);
Expand Down Expand Up @@ -365,7 +365,7 @@ describe('TreeKeyManager', () => {

it('should skip disabled items', () => {
itemList.get(1)!.isDisabled = true;
keyManager.onClick(itemList.get(0)!);
keyManager.setActiveItem(itemList.get(0)!);

const spy = jasmine.createSpy('change spy');
const subscription = keyManager.change.subscribe(spy);
Expand Down Expand Up @@ -393,7 +393,7 @@ describe('TreeKeyManager', () => {
itemList.get(0)!.isDisabled = undefined;
itemList.get(1)!.isDisabled = undefined;
itemList.get(2)!.isDisabled = undefined;
keyManager.onClick(itemList.get(0)!);
keyManager.setActiveItem(itemList.get(0)!);

const spy = jasmine.createSpy('change spy');
const subscription = keyManager.change.subscribe(spy);
Expand All @@ -416,7 +416,7 @@ describe('TreeKeyManager', () => {
});

it('should not move active item past either end of the list', () => {
keyManager.onClick(itemList.get(itemList.length - 1)!);
keyManager.setActiveItem(itemList.get(itemList.length - 1)!);

expect(keyManager.getActiveItemIndex())
.withContext('active item index, selecting the last item')
Expand All @@ -428,7 +428,7 @@ describe('TreeKeyManager', () => {
.withContext('active item index, last item still selected after a down event')
.toBe(itemList.length - 1);

keyManager.onClick(itemList.get(0)!);
keyManager.setActiveItem(itemList.get(0)!);
keyManager.onKeydown(fakeKeyEvents.upArrow);
expect(keyManager.getActiveItemIndex())
.withContext('active item index, selecting the first item')
Expand All @@ -444,7 +444,7 @@ describe('TreeKeyManager', () => {
it('should not move active item to end when the last item is disabled', () => {
itemList.get(itemList.length - 1)!.isDisabled = true;

keyManager.onClick(itemList.get(itemList.length - 2)!);
keyManager.setActiveItem(itemList.get(itemList.length - 2)!);
expect(keyManager.getActiveItemIndex())
.withContext('active item index, last non-disabled item selected')
.toBe(itemList.length - 2);
Expand Down Expand Up @@ -555,7 +555,7 @@ describe('TreeKeyManager', () => {
let subscription: Subscription;

beforeEach(() => {
keyManager.onClick(parentItem);
keyManager.setActiveItem(parentItem);
parentItem._isExpanded = true;

spy = jasmine.createSpy('change spy');
Expand Down Expand Up @@ -640,7 +640,7 @@ describe('TreeKeyManager', () => {
let subscription: Subscription;

beforeEach(() => {
keyManager.onClick(childItemWithNoChildren);
keyManager.setActiveItem(childItemWithNoChildren);
childItemWithNoChildren._isExpanded = true;

spy = jasmine.createSpy('change spy');
Expand All @@ -666,7 +666,7 @@ describe('TreeKeyManager', () => {
let subscription: Subscription;

beforeEach(() => {
keyManager.onClick(childItem);
keyManager.setActiveItem(childItem);
childItem._isExpanded = false;

spy = jasmine.createSpy('change spy');
Expand Down Expand Up @@ -733,7 +733,7 @@ describe('TreeKeyManager', () => {
let subscription: Subscription;

beforeEach(() => {
keyManager.onClick(parentItem);
keyManager.setActiveItem(parentItem);
parentItem._isExpanded = false;

spy = jasmine.createSpy('change spy');
Expand Down Expand Up @@ -924,7 +924,7 @@ describe('TreeKeyManager', () => {
]);
itemList.notifyOnChanges();

keyManager.onClick(frodo);
keyManager.setActiveItem(frodo);
keyManager.onKeydown(createKeyboardEvent('keydown', 66, 'b'));
tick(debounceInterval);

Expand All @@ -942,15 +942,15 @@ describe('TreeKeyManager', () => {
]);
itemList.notifyOnChanges();

keyManager.onClick(boromir);
keyManager.setActiveItem(boromir);
keyManager.onKeydown(createKeyboardEvent('keydown', 66, 'b'));
tick(debounceInterval);

expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(0);
}));

it('should wrap back around if the last item is active', fakeAsync(() => {
keyManager.onClick(lastItem);
keyManager.setActiveItem(lastItem);
keyManager.onKeydown(createKeyboardEvent('keydown', 79, 'o'));
tick(debounceInterval);

Expand Down
32 changes: 12 additions & 20 deletions src/cdk/a11y/key-manager/tree-key-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,14 +261,6 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
event.preventDefault();
}

/**
* Handles a mouse click on a particular tree item.
* @param treeItem The item that was clicked by the user.
*/
onClick(treeItem: T) {
this._setActiveItem(treeItem);
}

/** Index of the currently active item. */
getActiveItemIndex(): number | null {
return this._activeItemIndex;
Expand Down Expand Up @@ -300,7 +292,7 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
*/
focusItem(item: T, options?: {emitChangeEvent?: boolean}): void;
focusItem(itemOrIndex: number | T, options?: {emitChangeEvent?: boolean}): void {
this._setActiveItem(itemOrIndex, options);
this.setActiveItem(itemOrIndex, options);
}

/** Focus the first available item. */
Expand All @@ -323,10 +315,10 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
this._focusPreviousItem();
}

private _setActiveItem(index: number, options?: {emitChangeEvent?: boolean}): void;
private _setActiveItem(item: T, options?: {emitChangeEvent?: boolean}): void;
private _setActiveItem(itemOrIndex: number | T, options?: {emitChangeEvent?: boolean}): void;
private _setActiveItem(itemOrIndex: number | T, options: {emitChangeEvent?: boolean} = {}) {
setActiveItem(index: number, options?: {emitChangeEvent?: boolean}): void;
setActiveItem(item: T, options?: {emitChangeEvent?: boolean}): void;
setActiveItem(itemOrIndex: number | T, options?: {emitChangeEvent?: boolean}): void;
setActiveItem(itemOrIndex: number | T, options: {emitChangeEvent?: boolean} = {}) {
// Set default options
options.emitChangeEvent ??= true;

Expand Down Expand Up @@ -406,7 +398,7 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
!this._skipPredicateFn(item) &&
item.getLabel?.().toLocaleUpperCase().trim().indexOf(inputString) === 0
) {
this._setActiveItem(index);
this.setActiveItem(index);
break;
}
}
Expand All @@ -418,19 +410,19 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
//// Navigational methods

private _focusFirstItem() {
this._setActiveItem(this._findNextAvailableItemIndex(-1));
this.setActiveItem(this._findNextAvailableItemIndex(-1));
}

private _focusLastItem() {
this._setActiveItem(this._findPreviousAvailableItemIndex(this._items.length));
this.setActiveItem(this._findPreviousAvailableItemIndex(this._items.length));
}

private _focusPreviousItem() {
this._setActiveItem(this._findPreviousAvailableItemIndex(this._activeItemIndex));
this.setActiveItem(this._findPreviousAvailableItemIndex(this._activeItemIndex));
}

private _focusNextItem() {
this._setActiveItem(this._findNextAvailableItemIndex(this._activeItemIndex));
this.setActiveItem(this._findNextAvailableItemIndex(this._activeItemIndex));
}

private _findNextAvailableItemIndex(startingIndex: number) {
Expand Down Expand Up @@ -466,7 +458,7 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
if (!parent || this._skipPredicateFn(parent as T)) {
return;
}
this._setActiveItem(parent as T);
this.setActiveItem(parent as T);
}
}

Expand All @@ -488,7 +480,7 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
if (!firstChild) {
return;
}
this._setActiveItem(firstChild as T);
this.setActiveItem(firstChild as T);
});
}
}
Expand Down
12 changes: 12 additions & 0 deletions src/cdk/tree/tree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1174,6 +1174,18 @@ describe('CdkTree redesign', () => {
expect(getNodeAttributes(nodes, 'tabindex')).toEqual(['0', '-1', '-1', '-1', '-1', '-1']);
});

it('maintains tabindex when a node is programatically focused', () => {
// activate the second child by programatically focusing it
nodes[1].focus();

expect(getNodeAttributes(nodes, 'tabindex')).toEqual(['-1', '0', '-1', '-1', '-1', '-1']);

// activate the first child by programatically focusing it
nodes[0].focus();

expect(getNodeAttributes(nodes, 'tabindex')).toEqual(['0', '-1', '-1', '-1', '-1', '-1']);
});

it('maintains tabindex when component is blurred', () => {
// activate the second child by clicking on it
nodes[1].click();
Expand Down
3 changes: 2 additions & 1 deletion src/cdk/tree/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,7 @@ export class CdkTree<T, K = T>
'tabindex': '-1',
'role': 'treeitem',
'(click)': '_setActiveItem()',
'(focus)': '_setActiveItem()',
},
})
export class CdkTreeNode<T, K = T> implements OnDestroy, OnInit, TreeKeyManagerItem {
Expand Down Expand Up @@ -1290,7 +1291,7 @@ export class CdkTreeNode<T, K = T> implements OnDestroy, OnInit, TreeKeyManagerI
if (this.isDisabled) {
return;
}
this._tree._keyManager.onClick(this);
this._tree._keyManager.setActiveItem(this);
}

_emitExpansionState(expanded: boolean) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,25 @@
<!-- Leaf node -->
<mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding>
<button mat-icon-button disabled></button>
{{node.item}}
{{node.name}}
</mat-tree-node>

<!-- expandable node -->
<mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding matTreeNodeToggle
(expandedChange)="loadChildren(node)">
<button mat-icon-button
[attr.aria-label]="'Toggle ' + node.item"
[attr.aria-label]="'Toggle ' + node.name"
matTreeNodeToggle>
<mat-icon class="mat-icon-rtl-mirror">
{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
</mat-icon>
</button>
{{node.item}}
{{node.name}}
</mat-tree-node>

<mat-tree-node class="example-load-more" *matTreeNodeDef="let node; when: isLoadMore"
role="button" (click)="loadMoreOnClick($event, node)"
(keydown)="loadMoreOnEnterOrSpace($event, node)">
Load more...
role="button" (click)="loadOnClick($event, node)"
(keydown)="loadOnKeypress($event, node)">
Load more of {{node.parent}}...
</mat-tree-node>
</mat-tree>
Loading

0 comments on commit 704bc1d

Please sign in to comment.