From 4bce052753b764e56ca9567bdad48e332cd3cad9 Mon Sep 17 00:00:00 2001 From: Zach Arend Date: Thu, 16 Nov 2023 19:55:41 +0000 Subject: [PATCH] fix(cdk/tree): misc focus management fixes Fix miscelaneous focus management details in response to PR feedback. * omit tabindex attribute on role="tree" element * Remove TreeKeyManagerStrategy#onInitialFocus * Implement focus/unfocus pattern for TreeKeyManagerOption and use a binding to set tabindex on tree nodes * algin with https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#focusabilityofdisabledcontrols * Add tree-using-legacy-key-manager.spec.ts --- .../a11y/key-manager/noop-tree-key-manager.ts | 4 - .../key-manager/tree-key-manager-strategy.ts | 13 +- .../a11y/key-manager/tree-key-manager.spec.ts | 181 ++++++------------ src/cdk/a11y/key-manager/tree-key-manager.ts | 46 +++-- src/cdk/tree/tree-with-tree-control.spec.ts | 115 +++++------ src/cdk/tree/tree.spec.ts | 161 ++++++++-------- src/cdk/tree/tree.ts | 72 ++----- .../cdk-tree-custom-key-manager-example.ts | 27 ++- src/components-examples/cdk/tree/index.ts | 1 - .../material/tree/index.ts | 1 + ...ree-legacy-keyboard-interface-example.css} | 0 ...ee-legacy-keyboard-interface-example.html} | 12 +- ...tree-legacy-keyboard-interface-example.ts} | 13 +- src/dev-app/tree/tree-demo.html | 2 +- src/dev-app/tree/tree-demo.ts | 4 +- src/material/tree/BUILD.bazel | 1 + src/material/tree/node.ts | 20 +- .../tree-using-legacy-key-manager.spec.ts | 91 +++++++++ .../tree/tree-using-tree-control.spec.ts | 110 +++++------ src/material/tree/tree.spec.ts | 102 ++++------ tools/public_api_guard/cdk/a11y.md | 55 +++--- tools/public_api_guard/cdk/tree.md | 7 +- tools/public_api_guard/material/tree.md | 10 +- 23 files changed, 509 insertions(+), 539 deletions(-) rename src/components-examples/{cdk/tree/cdk-tree-legacy-keyboard-interface/cdk-tree-legacy-keyboard-interface-example.css => material/tree/tree-legacy-keyboard-interface/tree-legacy-keyboard-interface-example.css} (100%) rename src/components-examples/{cdk/tree/cdk-tree-legacy-keyboard-interface/cdk-tree-legacy-keyboard-interface-example.html => material/tree/tree-legacy-keyboard-interface/tree-legacy-keyboard-interface-example.html} (67%) rename src/components-examples/{cdk/tree/cdk-tree-legacy-keyboard-interface/cdk-tree-legacy-keyboard-interface-example.ts => material/tree/tree-legacy-keyboard-interface/tree-legacy-keyboard-interface-example.ts} (82%) create mode 100644 src/material/tree/tree-using-legacy-key-manager.spec.ts diff --git a/src/cdk/a11y/key-manager/noop-tree-key-manager.ts b/src/cdk/a11y/key-manager/noop-tree-key-manager.ts index 72117bd1ef8c..be94dc3d467f 100644 --- a/src/cdk/a11y/key-manager/noop-tree-key-manager.ts +++ b/src/cdk/a11y/key-manager/noop-tree-key-manager.ts @@ -50,10 +50,6 @@ export class NoopTreeKeyManager implements TreeKey return null; } - onInitialFocus() { - // noop - } - focusItem() { // noop } diff --git a/src/cdk/a11y/key-manager/tree-key-manager-strategy.ts b/src/cdk/a11y/key-manager/tree-key-manager-strategy.ts index 2a14640cae42..f63d8225b377 100644 --- a/src/cdk/a11y/key-manager/tree-key-manager-strategy.ts +++ b/src/cdk/a11y/key-manager/tree-key-manager-strategy.ts @@ -39,6 +39,11 @@ export interface TreeKeyManagerItem { * Focuses the item. This should provide some indication to the user that this item is focused. */ focus(): void; + + /** + * Unfocus the item. This should remove the focus state. + */ + unfocus(): void; } /** @@ -100,14 +105,6 @@ export interface TreeKeyManagerStrategy { /** The currently active item. */ getActiveItem(): T | null; - /** - * Called the first time the Tree component is focused. This method will only be called once over - * the lifetime of the Tree component. - * - * Intended to be used to focus the first item in the tree. - */ - onInitialFocus(): void; - /** * Focus the provided item by index. * diff --git a/src/cdk/a11y/key-manager/tree-key-manager.spec.ts b/src/cdk/a11y/key-manager/tree-key-manager.spec.ts index fe9edccc829f..338aead9c0e4 100644 --- a/src/cdk/a11y/key-manager/tree-key-manager.spec.ts +++ b/src/cdk/a11y/key-manager/tree-key-manager.spec.ts @@ -17,22 +17,27 @@ import {TreeKeyManagerItem} from './tree-key-manager-strategy'; import {Observable, of as observableOf, Subscription} from 'rxjs'; import {fakeAsync, tick} from '@angular/core/testing'; -class FakeBaseTreeKeyManagerItem { +class FakeBaseTreeKeyManagerItem implements TreeKeyManagerItem { _isExpanded = false; _parent: FakeBaseTreeKeyManagerItem | null = null; _children: FakeBaseTreeKeyManagerItem[] = []; - isDisabled?: boolean = false; + isDisabled?: boolean; skipItem?: boolean = false; - constructor(private _label: string) {} + constructor( + private _label: string, + _isDisabled: boolean | undefined = false, + ) { + this.isDisabled = _isDisabled; + } getLabel(): string { return this._label; } activate(): void {} - getParent(): this | null { - return this._parent as this | null; + getParent(): TreeKeyManagerItem | null { + return this._parent; } isExpanded(): boolean { return this._isExpanded; @@ -44,20 +49,21 @@ class FakeBaseTreeKeyManagerItem { this._isExpanded = true; } focus(): void {} + unfocus(): void {} + getChildren(): TreeKeyManagerItem[] | Observable { + return this._children; + } } -class FakeArrayTreeKeyManagerItem extends FakeBaseTreeKeyManagerItem implements TreeKeyManagerItem { - getChildren(): FakeArrayTreeKeyManagerItem[] { - return this._children as FakeArrayTreeKeyManagerItem[]; +class FakeArrayTreeKeyManagerItem extends FakeBaseTreeKeyManagerItem { + override getChildren() { + return this._children; } } -class FakeObservableTreeKeyManagerItem - extends FakeBaseTreeKeyManagerItem - implements TreeKeyManagerItem -{ - getChildren(): Observable { - return observableOf(this._children as FakeObservableTreeKeyManagerItem[]); +class FakeObservableTreeKeyManagerItem extends FakeBaseTreeKeyManagerItem { + override getChildren() { + return observableOf(this._children); } } @@ -65,6 +71,7 @@ interface ItemConstructorTestContext { description: string; constructor: new ( label: string, + disabled?: boolean, ) => FakeArrayTreeKeyManagerItem | FakeObservableTreeKeyManagerItem; } @@ -155,10 +162,29 @@ describe('TreeKeyManager', () => { keyManager = new TreeKeyManager< FakeObservableTreeKeyManagerItem | FakeArrayTreeKeyManagerItem >(itemList, {}); + itemList.notifyOnChanges(); }); - it('should start off the activeItem as null', () => { - expect(keyManager.getActiveItem()).withContext('active item').toBeNull(); + it('should intialize with at least one active item', () => { + expect(keyManager.getActiveItem()).withContext('has an active item').not.toBeNull(); + }); + + describe('when all items are disabled', () => { + beforeEach(() => { + itemList.reset([ + new itemParam.constructor('Bilbo', true), + new itemParam.constructor('Frodo', true), + new itemParam.constructor('Pippin', true), + ]); + keyManager = new TreeKeyManager< + FakeObservableTreeKeyManagerItem | FakeArrayTreeKeyManagerItem + >(itemList, {}); + itemList.notifyOnChanges(); + }); + + it('initializes with the first item activated', () => { + expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(0); + }); }); it('should maintain the active item if the amount of items changes', () => { @@ -207,16 +233,14 @@ describe('TreeKeyManager', () => { subscription.unsubscribe(); }); - it('should activate the first item when pressing down on a clean key manager', () => { - expect(keyManager.getActiveItemIndex()) - .withContext('default focused item index') - .toBe(-1); + it('should activate the second item when pressing down on a clean key manager', () => { + expect(keyManager.getActiveItemIndex()).withContext('default focused item index').toBe(0); keyManager.onKeydown(fakeKeyEvents.downArrow); expect(keyManager.getActiveItemIndex()) .withContext('focused item index, after down arrow') - .toBe(0); + .toBe(1); }); it('should not prevent the default keyboard action when pressing tab', () => { @@ -248,16 +272,6 @@ describe('TreeKeyManager', () => { expect(keyManager.getActiveItemIndex()).toBe(0); }); - it('should focus the first non-disabled item when Home is pressed', () => { - itemList.get(0)!.isDisabled = true; - keyManager.focusItem(itemList.get(2)!); - expect(keyManager.getActiveItemIndex()).toBe(2); - - keyManager.onKeydown(fakeKeyEvents.home); - - expect(keyManager.getActiveItemIndex()).toBe(1); - }); - it('should focus the last item when End is pressed', () => { keyManager.focusItem(itemList.get(0)!); expect(keyManager.getActiveItemIndex()).toBe(0); @@ -266,14 +280,14 @@ describe('TreeKeyManager', () => { expect(keyManager.getActiveItemIndex()).toBe(itemList.length - 1); }); - it('should focus the last non-disabled item when End is pressed', () => { + it('when last item is disabled, should focus the last item when End is pressed', () => { itemList.get(itemList.length - 1)!.isDisabled = true; keyManager.focusItem(itemList.get(0)!); expect(keyManager.getActiveItemIndex()).toBe(0); keyManager.onKeydown(fakeKeyEvents.end); - expect(keyManager.getActiveItemIndex()).toBe(itemList.length - 2); + expect(keyManager.getActiveItemIndex()).toBe(itemList.length - 1); }); }); @@ -301,14 +315,6 @@ describe('TreeKeyManager', () => { subscription.unsubscribe(); }); - it('should set first item active when the down key is pressed if no active item', () => { - keyManager.onKeydown(fakeKeyEvents.downArrow); - - expect(keyManager.getActiveItemIndex()) - .withContext('active item index, after down key if active item was null') - .toBe(0); - }); - it('should set previous item as active when the up key is pressed', () => { keyManager.focusItem(itemList.get(0)!); @@ -331,42 +337,19 @@ describe('TreeKeyManager', () => { subscription.unsubscribe(); }); - it('should do nothing when the up key is pressed if no active item', () => { - const spy = jasmine.createSpy('change spy'); - const subscription = keyManager.change.subscribe(spy); - keyManager.onKeydown(fakeKeyEvents.upArrow); - - expect(keyManager.getActiveItemIndex()) - .withContext('active item index, if up event occurs and no active item.') - .toBe(-1); - expect(spy).not.toHaveBeenCalled(); - subscription.unsubscribe(); - }); - - it('should skip disabled items', () => { + it('should skip navigate to disabled items', () => { itemList.get(1)!.isDisabled = true; keyManager.focusItem(itemList.get(0)!); - const spy = jasmine.createSpy('change spy'); - const subscription = keyManager.change.subscribe(spy); - // down event should skip past disabled item from 0 to 2 keyManager.onKeydown(fakeKeyEvents.downArrow); - expect(keyManager.getActiveItemIndex()) - .withContext('active item index, skipping past disabled item on down event.') - .toBe(2); - expect(spy).not.toHaveBeenCalledWith(itemList.get(0)); - expect(spy).not.toHaveBeenCalledWith(itemList.get(1)); - expect(spy).toHaveBeenCalledWith(itemList.get(2)); + expect(keyManager.getActiveItemIndex()).toBe(1); + + keyManager.onKeydown(fakeKeyEvents.downArrow); + expect(keyManager.getActiveItemIndex()).toBe(2); // up event should skip past disabled item from 2 to 0 keyManager.onKeydown(fakeKeyEvents.upArrow); - expect(keyManager.getActiveItemIndex()) - .withContext('active item index, skipping past disabled item on up event.') - .toBe(0); - expect(spy).toHaveBeenCalledWith(itemList.get(0)); - expect(spy).not.toHaveBeenCalledWith(itemList.get(1)); - expect(spy).toHaveBeenCalledWith(itemList.get(2)); - subscription.unsubscribe(); + expect(keyManager.getActiveItemIndex()).toBe(1); }); it('should work normally when disabled property does not exist', () => { @@ -421,23 +404,6 @@ describe('TreeKeyManager', () => { .toBe(0); }); - it('should not move active item to end when the last item is disabled', () => { - itemList.get(itemList.length - 1)!.isDisabled = true; - - keyManager.focusItem(itemList.get(itemList.length - 2)!); - expect(keyManager.getActiveItemIndex()) - .withContext('active item index, last non-disabled item selected') - .toBe(itemList.length - 2); - - // This down key event would set active item to the last item, which is disabled - keyManager.onKeydown(fakeKeyEvents.downArrow); - expect(keyManager.getActiveItemIndex()) - .withContext( - 'active item index, last non-disabled item still selected, after down event', - ) - .toBe(itemList.length - 2); - }); - it('should prevent the default keyboard action of handled events', () => { expect(fakeKeyEvents.downArrow.defaultPrevented).toBe(false); keyManager.onKeydown(fakeKeyEvents.downArrow); @@ -530,19 +496,9 @@ describe('TreeKeyManager', () => { }); describe('if the current item is expanded', () => { - let spy: jasmine.Spy; - let subscription: Subscription; - beforeEach(() => { keyManager.focusItem(parentItem); parentItem._isExpanded = true; - - spy = jasmine.createSpy('change spy'); - subscription = keyManager.change.subscribe(spy); - }); - - afterEach(() => { - subscription.unsubscribe(); }); it('when the expand key is pressed, moves to the first child', () => { @@ -551,13 +507,11 @@ describe('TreeKeyManager', () => { expect(keyManager.getActiveItemIndex()) .withContext('active item index, after one expand key event.') .toBe(1); - expect(spy).not.toHaveBeenCalledWith(parentItem); - expect(spy).toHaveBeenCalledWith(childItem); }); it( 'when the expand key is pressed, and the first child is disabled, ' + - 'moves to the first non-disabled child', + 'moves to the first child', () => { childItem.isDisabled = true; @@ -565,16 +519,13 @@ describe('TreeKeyManager', () => { expect(keyManager.getActiveItemIndex()) .withContext('active item index, after one expand key event.') - .toBe(3); - expect(spy).not.toHaveBeenCalledWith(parentItem); - expect(spy).not.toHaveBeenCalledWith(childItem); - expect(spy).toHaveBeenCalledWith(childItemWithNoChildren); + .toBe(1); }, ); it( 'when the expand key is pressed, and all children are disabled, ' + - 'does not change the active item', + 'it activates first child', () => { childItem.isDisabled = true; childItemWithNoChildren.isDisabled = true; @@ -583,8 +534,7 @@ describe('TreeKeyManager', () => { expect(keyManager.getActiveItemIndex()) .withContext('active item index, after one expand key event.') - .toBe(0); - expect(spy).not.toHaveBeenCalled(); + .toBe(1); }, ); @@ -610,7 +560,6 @@ describe('TreeKeyManager', () => { expect(keyManager.getActiveItemIndex()) .withContext('active item index, after one collapse key event.') .toBe(0); - expect(spy).not.toHaveBeenCalled(); }); }); @@ -693,7 +642,7 @@ describe('TreeKeyManager', () => { .toBe(0); }); - it('when the collapse key is pressed, and the parent is disabled, does nothing', () => { + it('when the collapse key is pressed, and the parent is disabled, focuses parent', () => { expect(keyManager.getActiveItemIndex()) .withContext('active item index, initial') .toBe(1); @@ -703,7 +652,7 @@ describe('TreeKeyManager', () => { expect(keyManager.getActiveItemIndex()) .withContext('active item index, after one collapse key event.') - .toBe(1); + .toBe(0); }); }); @@ -878,7 +827,7 @@ describe('TreeKeyManager', () => { expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(0); })); - it('should not focus disabled items', fakeAsync(() => { + it('should allow focus to disabled items', fakeAsync(() => { expect(keyManager.getActiveItemIndex()).withContext('initial active item index').toBe(-1); parentItem.isDisabled = true; @@ -886,7 +835,7 @@ describe('TreeKeyManager', () => { keyManager.onKeydown(createKeyboardEvent('keydown', 79, 'o')); // types "o" tick(debounceInterval); - expect(keyManager.getActiveItemIndex()).withContext('initial active item index').toBe(-1); + expect(keyManager.getActiveItemIndex()).withContext('initial active item index').toBe(0); })); it('should start looking for matches after the active item', fakeAsync(() => { @@ -949,10 +898,6 @@ describe('TreeKeyManager', () => { }); describe('focusItem', () => { - beforeEach(() => { - keyManager.onInitialFocus(); - }); - it('should focus the provided index', () => { expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(0); @@ -999,7 +944,7 @@ describe('TreeKeyManager', () => { keyManager = new TreeKeyManager(itemList, { skipPredicate: item => item.skipItem ?? false, }); - keyManager.onInitialFocus(); + itemList.notifyOnChanges(); }); it('should be able to skip items with a custom predicate', () => { @@ -1014,8 +959,6 @@ describe('TreeKeyManager', () => { describe('focus', () => { beforeEach(() => { - keyManager.onInitialFocus(); - for (const item of itemList) { spyOn(item, 'focus'); } diff --git a/src/cdk/a11y/key-manager/tree-key-manager.ts b/src/cdk/a11y/key-manager/tree-key-manager.ts index e719471255ac..2115d7b284e2 100644 --- a/src/cdk/a11y/key-manager/tree-key-manager.ts +++ b/src/cdk/a11y/key-manager/tree-key-manager.ts @@ -53,11 +53,13 @@ export class TreeKeyManager implements TreeKeyMana private readonly _letterKeyStream = new Subject(); private _typeaheadSubscription = Subscription.EMPTY; + // Keep tree items focusable when disabled. Align with + // https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#focusabilityofdisabledcontrols. /** * Predicate function that can be used to check whether an item should be skipped - * by the key manager. By default, disabled items are skipped. + * by the key manager. */ - private _skipPredicateFn = (item: T) => this._isItemDisabled(item); + private _skipPredicateFn = (_item: T) => false; /** Function to determine equivalent items. */ private _trackByFn: (item: T) => unknown = (item: T) => item; @@ -67,6 +69,29 @@ export class TreeKeyManager implements TreeKeyMana private _items: T[] = []; + private _hasInitialFocused = false; + + private _initialFocus() { + if (this._hasInitialFocused) { + return; + } + + if (!this._items.length) { + return; + } + + let focusIndex = 0; + for (let i = 0; i < this._items.length; i++) { + if (!this._skipPredicateFn(this._items[i]) && !this._isItemDisabled(this._items[i])) { + focusIndex = i; + break; + } + } + + this.focusItem(focusIndex); + this._hasInitialFocused = true; + } + constructor(items: Observable | QueryList | T[], config: TreeKeyManagerOptions) { // We allow for the items to be an array or Observable because, in some cases, the consumer may // not have access to a QueryList of the items they want to manage (e.g. when the @@ -76,14 +101,17 @@ export class TreeKeyManager implements TreeKeyMana items.changes.subscribe((newItems: QueryList) => { this._items = newItems.toArray(); this._updateActiveItemIndex(this._items); + this._initialFocus(); }); } else if (isObservable(items)) { items.subscribe(newItems => { this._items = newItems; this._updateActiveItemIndex(newItems); + this._initialFocus(); }); } else { this._items = items; + this._initialFocus(); } if (typeof config.activationFollowsFocus === 'boolean') { @@ -188,14 +216,6 @@ export class TreeKeyManager implements TreeKeyMana return this._activeItem; } - /** - * Focus the initial element; this is intended to be called when the tree is focused for - * the first time. - */ - onInitialFocus(): void { - this._focusFirstItem(); - } - /** Focus the first available item. */ private _focusFirstItem(): void { this.focusItem(this._findNextAvailableItemIndex(-1)); @@ -245,14 +265,18 @@ export class TreeKeyManager implements TreeKeyMana return; } + const previousActiveItem = this._activeItem; this._activeItem = activeItem ?? null; this._activeItemIndex = index; + this._activeItem?.focus(); + previousActiveItem?.unfocus(); + if (options.emitChangeEvent) { // Emit to `change` stream as required by TreeKeyManagerStrategy interface. this.change.next(this._activeItem); } - this._activeItem?.focus(); + if (this._activationFollowsFocus) { this._activateCurrentItem(); } diff --git a/src/cdk/tree/tree-with-tree-control.spec.ts b/src/cdk/tree/tree-with-tree-control.spec.ts index 2566da86d598..bb2a8495799a 100644 --- a/src/cdk/tree/tree-with-tree-control.spec.ts +++ b/src/cdk/tree/tree-with-tree-control.spec.ts @@ -758,9 +758,13 @@ describe('CdkTree', () => { }); it('with the right aria-expanded attrs', () => { - expect(getNodeAttributes(getNodes(treeElement), 'aria-expanded')) + expect( + getNodes(treeElement) + .map(x => `${x.getAttribute('aria-expanded')}`) + .join(', '), + ) .withContext('aria-expanded attributes') - .toEqual(['false', 'false', 'false']); + .toEqual('false, false, false'); component.toggleRecursively = false; let data = dataSource.data; @@ -773,9 +777,13 @@ describe('CdkTree', () => { // NB: only four elements are present here; children are not present // in DOM unless the parent node is expanded. - expect(getNodeAttributes(getNodes(treeElement), 'aria-expanded')) + expect( + getNodes(treeElement) + .map(x => `${x.getAttribute('aria-expanded')}`) + .join(', '), + ) .withContext('aria-expanded attributes') - .toEqual(['false', 'true', 'false', 'false']); + .toEqual('false, true, false, false'); }); it('should expand/collapse the node multiple times using keyboard', () => { @@ -1145,67 +1153,51 @@ describe('CdkTree', () => { }); describe('focus management', () => { - it('the tree is tabbable when no element is active', () => { - expect(treeElement.getAttribute('tabindex')).toBe('0'); - }); - - it('the tree is not tabbable when an element is active', () => { - // activate the second child by clicking on it - nodes[1].click(); - - expect(treeElement.getAttribute('tabindex')).toBe(null); - }); - it('sets tabindex on the latest activated item, with all others "-1"', () => { // activate the second child by clicking on it nodes[1].click(); + fixture.detectChanges(); - expect(getNodeAttributes(nodes, 'tabindex')).toEqual(['-1', '0', '-1', '-1', '-1', '-1']); + expect(nodes.map(x => `${x.getAttribute('tabindex')}`).join(', ')).toEqual( + '-1, 0, -1, -1, -1, -1', + ); // activate the first child by clicking on it nodes[0].click(); + fixture.detectChanges(); - expect(getNodeAttributes(nodes, 'tabindex')).toEqual(['0', '-1', '-1', '-1', '-1', '-1']); + expect(nodes.map(x => `${x.getAttribute('tabindex')}`).join(', ')).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(); + fixture.detectChanges(); expect(document.activeElement).toBe(nodes[1]); // blur the currently active element (which we just checked is the above node) nodes[1].blur(); + fixture.detectChanges(); - expect(treeElement.getAttribute('tabindex')).toBe(null); - expect(getNodeAttributes(nodes, 'tabindex')).toEqual(['-1', '0', '-1', '-1', '-1', '-1']); + expect( + getNodes(treeElement) + .map(x => `${x.getAttribute('tabindex')}`) + .join(', '), + ).toEqual('-1, 0, -1, -1, -1, -1'); }); it('ignores clicks on disabled items', () => { - dataSource.data[0].isDisabled = true; + dataSource.data[1].isDisabled = true; fixture.detectChanges(); - // attempt to click on the first child - nodes[0].click(); - - expect(treeElement.getAttribute('tabindex')).toBe('0'); - expect(getNodeAttributes(nodes, 'tabindex')).toEqual(['-1', '-1', '-1', '-1', '-1', '-1']); - }); - - describe('when no item is currently active', () => { - it('redirects focus to the first item when the tree is focused', () => { - treeElement.focus(); - - expect(document.activeElement).toBe(nodes[0]); - }); - - it('redirects focus to the first non-disabled item when the tree is focused', () => { - dataSource.data[0].isDisabled = true; - fixture.detectChanges(); - - treeElement.focus(); + nodes[1].click(); + fixture.detectChanges(); - expect(document.activeElement).toBe(nodes[1]); - }); + expect(nodes.map(x => `${x.getAttribute('tabindex')}`).join(', ')).toEqual( + '0, -1, -1, -1, -1, -1', + ); }); }); @@ -1215,43 +1207,40 @@ describe('CdkTree', () => { }); it('sets the treeitem role on all nodes', () => { - expect(getNodeAttributes(nodes, 'role')).toEqual([ - 'treeitem', - 'treeitem', - 'treeitem', - 'treeitem', - 'treeitem', - 'treeitem', - ]); + expect( + getNodes(treeElement) + .map(x => `${x.getAttribute('role')}`) + .join(', '), + ).toEqual('treeitem, treeitem, treeitem, treeitem, treeitem, treeitem'); }); it('sets aria attributes for tree nodes', () => { - expect(getNodeAttributes(nodes, 'aria-expanded')) + expect(nodes.map(x => `${x.getAttribute('aria-expanded')}`).join(', ')) .withContext('aria-expanded attributes') - .toEqual([null, 'false', 'false', null, null, null]); - expect(getNodeAttributes(nodes, 'aria-level')) + .toEqual('null, false, false, null, null, null'); + expect(nodes.map(x => `${x.getAttribute('aria-level')}`).join(', ')) .withContext('aria-level attributes') - .toEqual(['1', '1', '2', '3', '3', '1']); - expect(getNodeAttributes(nodes, 'aria-posinset')) + .toEqual('1, 1, 2, 3, 3, 1'); + expect(nodes.map(x => `${x.getAttribute('aria-posinset')}`).join(', ')) .withContext('aria-posinset attributes') - .toEqual(['1', '2', '1', '1', '2', '3']); - expect(getNodeAttributes(nodes, 'aria-setsize')) + .toEqual('1, 2, 1, 1, 2, 3'); + expect(nodes.map(x => `${x.getAttribute('aria-setsize')}`).join(', ')) .withContext('aria-setsize attributes') - .toEqual(['3', '3', '1', '2', '2', '3']); + .toEqual('3, 3, 1, 2, 2, 3'); }); it('changes aria-expanded status when expanded or collapsed', () => { tree.expand(dataSource.data[1]); fixture.detectChanges(); - expect(getNodeAttributes(nodes, 'aria-expanded')) + expect(nodes.map(x => `${x.getAttribute('aria-expanded')}`).join(', ')) .withContext('aria-expanded attributes') - .toEqual([null, 'true', 'false', null, null, null]); + .toEqual('null, true, false, null, null, null'); tree.collapse(dataSource.data[1]); fixture.detectChanges(); - expect(getNodeAttributes(nodes, 'aria-expanded')) + expect(nodes.map(x => `${x.getAttribute('aria-expanded')}`).join(', ')) .withContext('aria-expanded attributes') - .toEqual([null, 'false', 'false', null, null, null]); + .toEqual('null, false, false, null, null, null'); }); }); }); @@ -1429,10 +1418,6 @@ function expectNestedTreeToMatch(treeElement: Element, ...expectedTree: any[]) { } } -function getNodeAttributes(nodes: HTMLElement[], attribute: string) { - return nodes.map(node => node.getAttribute(attribute)); -} - @Component({ template: ` diff --git a/src/cdk/tree/tree.spec.ts b/src/cdk/tree/tree.spec.ts index 9d7b2c86c63d..3e8e33d81daa 100644 --- a/src/cdk/tree/tree.spec.ts +++ b/src/cdk/tree/tree.spec.ts @@ -28,7 +28,7 @@ import {createKeyboardEvent} from '@angular/cdk/testing/testbed/fake-events'; * This is a cloned version of `tree.spec.ts` that contains all the same tests, * but modifies them to use the newer API. */ -describe('CdkTree redesign', () => { +describe('CdkTree', () => { /** Represents an indent for expectNestedTreeToMatch */ const _ = {}; let dataSource: FakeDataSource; @@ -61,34 +61,36 @@ describe('CdkTree redesign', () => { }).compileComponents(); } - it('should clear out the `mostRecentTreeNode` on destroy', () => { - configureCdkTreeTestingModule([SimpleCdkTreeApp]); - const fixture = TestBed.createComponent(SimpleCdkTreeApp); - fixture.detectChanges(); + describe('onDestroy', () => { + it('should clear out the `mostRecentTreeNode` on destroy', () => { + configureCdkTreeTestingModule([SimpleCdkTreeApp]); + const fixture = TestBed.createComponent(SimpleCdkTreeApp); + fixture.detectChanges(); - // Cast the assertions to a boolean to avoid Jasmine going into an - // infinite loop when stringifying the object, if the test starts failing. - expect(!!CdkTreeNode.mostRecentTreeNode).toBe(true); + // Cast the assertions to a boolean to avoid Jasmine going into an + // infinite loop when stringifying the object, if the test starts failing. + expect(!!CdkTreeNode.mostRecentTreeNode).toBe(true); - fixture.destroy(); + fixture.destroy(); - expect(!!CdkTreeNode.mostRecentTreeNode).toBe(false); - }); + expect(!!CdkTreeNode.mostRecentTreeNode).toBe(false); + }); - it('should complete the viewChange stream on destroy', () => { - configureCdkTreeTestingModule([SimpleCdkTreeApp]); - const fixture = TestBed.createComponent(SimpleCdkTreeApp); - fixture.detectChanges(); - const spy = jasmine.createSpy('completeSpy'); - const subscription = fixture.componentInstance.tree.viewChange.subscribe({complete: spy}); + it('should complete the viewChange stream on destroy', () => { + configureCdkTreeTestingModule([SimpleCdkTreeApp]); + const fixture = TestBed.createComponent(SimpleCdkTreeApp); + fixture.detectChanges(); + const spy = jasmine.createSpy('completeSpy'); + const subscription = fixture.componentInstance.tree.viewChange.subscribe({complete: spy}); - fixture.destroy(); - expect(spy).toHaveBeenCalled(); - subscription.unsubscribe(); + fixture.destroy(); + expect(spy).toHaveBeenCalled(); + subscription.unsubscribe(); + }); }); describe('flat tree', () => { - describe('should initialize', () => { + describe('displaying a flat tree', () => { let fixture: ComponentFixture; let component: SimpleCdkTreeApp; @@ -104,19 +106,19 @@ describe('CdkTree redesign', () => { treeElement = fixture.nativeElement.querySelector('cdk-tree'); }); - it('with a connected data source', () => { + it('connects the datasource', () => { expect(tree.dataSource).toBe(dataSource); expect(dataSource.isConnected).toBe(true); }); - it('with rendered dataNodes', () => { + it('renders at least one node', () => { const nodes = getNodes(treeElement); expect(nodes).withContext('Expect nodes to be defined').toBeDefined(); expect(nodes[0].classList).toContain('customNodeClass'); }); - it('with the right data', () => { + it('renders nodes that match the datasource', () => { expect(dataSource.data.length).toBe(3); let data = dataSource.data; @@ -145,7 +147,7 @@ describe('CdkTree redesign', () => { ); }); - it('should be able to use units different from px for the indentation', () => { + it('indents when given an indentation of 15rem', () => { component.indent = '15rem'; fixture.detectChanges(); @@ -161,7 +163,7 @@ describe('CdkTree redesign', () => { ); }); - it('should default to px if no unit is set for string value indentation', () => { + it('indents in units of pixel when no unit is given', () => { component.indent = '17'; fixture.detectChanges(); @@ -193,7 +195,7 @@ describe('CdkTree redesign', () => { ); }); - it('should reset the opposite direction padding if the direction changes', () => { + it('should reset element.styel to the opposite direction padding if the direction changes', () => { const node = getNodes(treeElement)[0]; component.indent = 10; @@ -770,7 +772,7 @@ describe('CdkTree redesign', () => { }); it('with the right aria-expanded attrs', () => { - expect(getNodeAttributes(getNodes(treeElement), 'aria-expanded')) + expect(getNodes(treeElement).map(x => x.getAttribute('aria-expanded'))) .withContext('aria-expanded attributes') .toEqual([null, null, null]); @@ -785,7 +787,7 @@ describe('CdkTree redesign', () => { // NB: only four elements are present here; children are not present // in DOM unless the parent node is expanded. - expect(getNodeAttributes(getNodes(treeElement), 'aria-expanded')) + expect(getNodes(treeElement).map(x => x.getAttribute('aria-expanded'))) .withContext('aria-expanded attributes') .toEqual([null, 'true', 'false', null]); }); @@ -1150,79 +1152,90 @@ describe('CdkTree redesign', () => { }); describe('focus management', () => { - it('the tree is tabbable when no element is active', () => { - expect(treeElement.getAttribute('tabindex')).toBe('0'); + beforeEach(() => { + fixture.destroy(); + + fixture = TestBed.createComponent(StaticNestedCdkTreeApp); + + component = fixture.componentInstance; + dataSource = component.dataSource as FakeDataSource; + tree = component.tree; + treeElement = fixture.nativeElement.querySelector('cdk-tree'); + nodes = getNodes(treeElement); + + dataSource.clear(); + + dataSource.data = [ + new TestData('cheese'), + new TestData('pepperoni'), + new TestData('anchovie'), + ]; + + fixture.detectChanges(); + nodes = getNodes(treeElement); + }); + + it('the tree does not have tabindex attribute', () => { + expect(treeElement.hasAttribute('tabindex')).toBeFalse(); }); - it('the tree is not tabbable when an element is active', () => { + it('the tree does not have a tabindex when an element is active', () => { // activate the second child by clicking on it nodes[1].click(); + fixture.detectChanges(); + + expect(treeElement.hasAttribute('tabindex')).toBeFalse(); + }); - expect(treeElement.getAttribute('tabindex')).toBe(null); + it('sets the tabindex to the first item by default', () => { + expect(nodes.map(x => x.getAttribute('tabindex')).join(', ')).toEqual('0, -1, -1'); }); it('sets tabindex on the latest activated item, with all others "-1"', () => { // activate the second child by clicking on it nodes[1].click(); + fixture.detectChanges(); - expect(getNodeAttributes(nodes, 'tabindex')).toEqual(['-1', '0', '-1', '-1', '-1', '-1']); - - // activate the first child by clicking on it - nodes[0].click(); - - expect(getNodeAttributes(nodes, 'tabindex')).toEqual(['0', '-1', '-1', '-1', '-1', '-1']); + expect(nodes.map(x => x.getAttribute('tabindex')).join(', ')).toEqual('-1, 0, -1'); }); it('maintains tabindex when a node is programatically focused', () => { // activate the second child by programatically focusing it nodes[1].focus(); + fixture.detectChanges(); - expect(getNodeAttributes(nodes, 'tabindex')).toEqual(['-1', '0', '-1', '-1', '-1', '-1']); + expect(nodes.map(x => x.getAttribute('tabindex')).join(', ')).toEqual('-1, 0, -1'); // activate the first child by programatically focusing it nodes[0].focus(); + fixture.detectChanges(); - expect(getNodeAttributes(nodes, 'tabindex')).toEqual(['0', '-1', '-1', '-1', '-1', '-1']); + expect(nodes.map(x => x.getAttribute('tabindex')).join(', ')).toEqual('0, -1, -1'); }); it('maintains tabindex when component is blurred', () => { // activate the second child by clicking on it nodes[1].click(); + fixture.detectChanges(); expect(document.activeElement).toBe(nodes[1]); // blur the currently active element (which we just checked is the above node) nodes[1].blur(); + fixture.detectChanges(); - expect(treeElement.getAttribute('tabindex')).toBe(null); - expect(getNodeAttributes(nodes, 'tabindex')).toEqual(['-1', '0', '-1', '-1', '-1', '-1']); + expect(nodes.map(x => x.getAttribute('tabindex')).join(', ')).toEqual('-1, 0, -1'); }); it('ignores clicks on disabled items', () => { - dataSource.data[0].isDisabled = true; + dataSource.data[1].isDisabled = true; fixture.detectChanges(); + expect(nodes.map(x => x.getAttribute('tabindex')).join(', ')).toEqual('0, -1, -1'); // attempt to click on the first child - nodes[0].click(); - - expect(treeElement.getAttribute('tabindex')).toBe('0'); - expect(getNodeAttributes(nodes, 'tabindex')).toEqual(['-1', '-1', '-1', '-1', '-1', '-1']); - }); - - describe('when no item is currently active', () => { - it('redirects focus to the first item when the tree is focused', () => { - treeElement.focus(); - - expect(document.activeElement).toBe(nodes[0]); - }); - - it('redirects focus to the first non-disabled item when the tree is focused', () => { - dataSource.data[0].isDisabled = true; - fixture.detectChanges(); - - treeElement.focus(); + nodes[1].click(); + fixture.detectChanges(); - expect(document.activeElement).toBe(nodes[1]); - }); + expect(nodes.map(x => x.getAttribute('tabindex')).join(', ')).toEqual('0, -1, -1'); }); }); @@ -1232,7 +1245,7 @@ describe('CdkTree redesign', () => { }); it('sets the treeitem role on all nodes', () => { - expect(getNodeAttributes(nodes, 'role')).toEqual([ + expect(nodes.map(x => x.getAttribute('role'))).toEqual([ 'treeitem', 'treeitem', 'treeitem', @@ -1243,16 +1256,16 @@ describe('CdkTree redesign', () => { }); it('sets aria attributes for tree nodes', () => { - expect(getNodeAttributes(nodes, 'aria-expanded')) + expect(nodes.map(x => x.getAttribute('aria-expanded'))) .withContext('aria-expanded attributes') .toEqual([null, 'false', 'false', null, null, null]); - expect(getNodeAttributes(nodes, 'aria-level')) + expect(nodes.map(x => x.getAttribute('aria-level'))) .withContext('aria-level attributes') .toEqual(['1', '1', '2', '3', '3', '1']); - expect(getNodeAttributes(nodes, 'aria-posinset')) + expect(nodes.map(x => x.getAttribute('aria-posinset'))) .withContext('aria-posinset attributes') .toEqual(['1', '2', '1', '1', '2', '3']); - expect(getNodeAttributes(nodes, 'aria-setsize')) + expect(nodes.map(x => x.getAttribute('aria-setsize'))) .withContext('aria-setsize attributes') .toEqual(['3', '3', '1', '2', '2', '3']); }); @@ -1260,13 +1273,13 @@ describe('CdkTree redesign', () => { it('changes aria-expanded status when expanded or collapsed', () => { tree.expand(dataSource.data[1]); fixture.detectChanges(); - expect(getNodeAttributes(nodes, 'aria-expanded')) + expect(nodes.map(x => x.getAttribute('aria-expanded'))) .withContext('aria-expanded attributes') .toEqual([null, 'true', 'false', null, null, null]); tree.collapse(dataSource.data[1]); fixture.detectChanges(); - expect(getNodeAttributes(nodes, 'aria-expanded')) + expect(nodes.map(x => x.getAttribute('aria-expanded'))) .withContext('aria-expanded attributes') .toEqual([null, 'false', 'false', null, null, null]); }); @@ -1283,7 +1296,7 @@ export class TestData { isDisabled?: boolean; readonly observableChildren: BehaviorSubject; - constructor(pizzaTopping: string, pizzaCheese: string, pizzaBase: string, level: number = 1) { + constructor(pizzaTopping: string, pizzaCheese = '', pizzaBase = '', level: number = 1) { this.pizzaTopping = pizzaTopping; this.pizzaCheese = pizzaCheese; this.pizzaBase = pizzaBase; @@ -1466,10 +1479,6 @@ function expectNestedTreeToMatch(treeElement: Element, ...expectedTree: any[]) { } } -function getNodeAttributes(nodes: HTMLElement[], attribute: string) { - return nodes.map(node => node.getAttribute(attribute)); -} - @Component({ template: ` = 'class': 'cdk-tree', 'role': 'tree', '(keydown)': '_sendKeydownToKeyManager($event)', - '(focus)': '_focusInitialTreeItem()', }, encapsulation: ViewEncapsulation.None, @@ -319,23 +308,6 @@ export class CdkTree } } - /** - * Sets the tabIndex on the host element. - * - * NB: we don't set this as a host binding since children being activated - * (e.g. on user click) doesn't trigger this component's change detection. - */ - _setTabIndex() { - // If the `TreeKeyManager` has no active item, then we know that we need to focus the initial - // item when the tree is focused. We set the tabindex to be `0` so that we can capture - // the focus event and redirect it. Otherwise, we unset it. - if (!this._keyManager.getActiveItem()) { - this._elementRef.nativeElement.setAttribute('tabindex', '0'); - } else { - this._elementRef.nativeElement.removeAttribute('tabindex'); - } - } - /** * Switch to the provided data source by resetting the data and unsubscribing from the current * render change subscription if one exists. If the data source is null, interpret this by @@ -474,18 +446,6 @@ export class CdkTree }; this._keyManager = this._keyManagerFactory(items, keyManagerOptions); - - this._keyManager.change - .pipe(startWith(null), pairwise(), takeUntil(this._onDestroy)) - .subscribe(([prev, next]) => { - prev?._setTabUnfocusable(); - next?._setTabFocusable(); - }); - - this._keyManager.change.pipe(startWith(null), takeUntil(this._onDestroy)).subscribe(() => { - // refresh the tabindex when the active item changes. - this._setTabIndex(); - }); } private _initializeDataDiffer() { @@ -864,14 +824,6 @@ export class CdkTree this._keyManager.onKeydown(event); } - /** `focus` event handler; this focuses the initial item if there isn't already one available. */ - _focusInitialTreeItem() { - if (this._keyManager.getActiveItem()) { - return; - } - this._keyManager.onInitialFocus(); - } - /** Gets all nested descendants of a given node. */ private _getDescendants(dataNode: T): Observable { if (this.treeControl) { @@ -1140,13 +1092,15 @@ export class CdkTree '[attr.aria-level]': 'level + 1', '[attr.aria-posinset]': '_getPositionInSet()', '[attr.aria-setsize]': '_getSetSize()', - 'tabindex': '-1', + '[tabindex]': '_tabindex', 'role': 'treeitem', '(click)': '_focusItem()', '(focus)': '_focusItem()', }, }) export class CdkTreeNode implements OnDestroy, OnInit, TreeKeyManagerItem { + protected _tabindex: number | null = -1; + _changeDetectorRef = inject(ChangeDetectorRef); /** @@ -1317,7 +1271,17 @@ export class CdkTreeNode implements OnDestroy, OnInit, TreeKeyManagerI /** Focuses this data node. Implemented for TreeKeyManagerItem. */ focus(): void { + this._tabindex = 0; this._elementRef.nativeElement.focus(); + + this._changeDetectorRef.markForCheck(); + } + + /** Defocus this data node. */ + unfocus(): void { + this._tabindex = -1; + + this._changeDetectorRef.markForCheck(); } /** Emits an activation event. Implemented for TreeKeyManagerItem. */ @@ -1342,14 +1306,6 @@ export class CdkTreeNode implements OnDestroy, OnInit, TreeKeyManagerI } } - _setTabFocusable() { - this._elementRef.nativeElement.setAttribute('tabindex', '0'); - } - - _setTabUnfocusable() { - this._elementRef.nativeElement.setAttribute('tabindex', '-1'); - } - _focusItem() { if (this.isDisabled) { return; diff --git a/src/components-examples/cdk/tree/cdk-tree-custom-key-manager/cdk-tree-custom-key-manager-example.ts b/src/components-examples/cdk/tree/cdk-tree-custom-key-manager/cdk-tree-custom-key-manager-example.ts index f57d7cfa920e..e530167b68ec 100644 --- a/src/components-examples/cdk/tree/cdk-tree-custom-key-manager/cdk-tree-custom-key-manager-example.ts +++ b/src/components-examples/cdk/tree/cdk-tree-custom-key-manager/cdk-tree-custom-key-manager-example.ts @@ -111,6 +111,22 @@ export class VimTreeKeyManager implements TreeKeyM private _items: T[] = []; + private _hasInitialFocused = false; + + private _initialFocus() { + if (this._hasInitialFocused) { + return; + } + + if (!this._items.length) { + return; + } + + this._focusFirstItem(); + + this._hasInitialFocused = true; + } + // TreeKeyManagerOptions not implemented. constructor(items: Observable | QueryList | T[]) { // We allow for the items to be an array or Observable because, in some cases, the consumer may @@ -121,14 +137,17 @@ export class VimTreeKeyManager implements TreeKeyM items.changes.subscribe((newItems: QueryList) => { this._items = newItems.toArray(); this._updateActiveItemIndex(this._items); + this._initialFocus(); }); } else if (isObservable(items)) { items.subscribe(newItems => { this._items = newItems; this._updateActiveItemIndex(newItems); + this._initialFocus(); }); } else { this._items = items; + this._initialFocus(); } } @@ -192,14 +211,6 @@ export class VimTreeKeyManager implements TreeKeyM return this._activeItem; } - /** - * Focus the initial element; this is intended to be called when the tree is focused for - * the first time. - */ - onInitialFocus(): void { - this._focusFirstItem(); - } - /** * Focus the provided item by index. * @param index The index of the item to focus. diff --git a/src/components-examples/cdk/tree/index.ts b/src/components-examples/cdk/tree/index.ts index 5fe7af83353d..a147b8e0d4b6 100644 --- a/src/components-examples/cdk/tree/index.ts +++ b/src/components-examples/cdk/tree/index.ts @@ -6,4 +6,3 @@ export {CdkTreeNestedChildrenAccessorExample} from './cdk-tree-nested-children-a export {CdkTreeNestedExample} from './cdk-tree-nested/cdk-tree-nested-example'; export {CdkTreeComplexExample} from './cdk-tree-complex/cdk-tree-complex-example'; export {CdkTreeCustomKeyManagerExample} from './cdk-tree-custom-key-manager/cdk-tree-custom-key-manager-example'; -export {CdkTreeLegacyKeyboardInterfaceExample} from './cdk-tree-legacy-keyboard-interface/cdk-tree-legacy-keyboard-interface-example'; diff --git a/src/components-examples/material/tree/index.ts b/src/components-examples/material/tree/index.ts index dd5eed1712d9..efc466c63794 100644 --- a/src/components-examples/material/tree/index.ts +++ b/src/components-examples/material/tree/index.ts @@ -3,3 +3,4 @@ export {TreeFlatOverviewExample} from './tree-flat-overview/tree-flat-overview-e export {TreeHarnessExample} from './tree-harness/tree-harness-example'; export {TreeLoadmoreExample} from './tree-loadmore/tree-loadmore-example'; export {TreeNestedOverviewExample} from './tree-nested-overview/tree-nested-overview-example'; +export {TreeLegacyKeyboardInterfaceExample} from './tree-legacy-keyboard-interface/tree-legacy-keyboard-interface-example'; diff --git a/src/components-examples/cdk/tree/cdk-tree-legacy-keyboard-interface/cdk-tree-legacy-keyboard-interface-example.css b/src/components-examples/material/tree/tree-legacy-keyboard-interface/tree-legacy-keyboard-interface-example.css similarity index 100% rename from src/components-examples/cdk/tree/cdk-tree-legacy-keyboard-interface/cdk-tree-legacy-keyboard-interface-example.css rename to src/components-examples/material/tree/tree-legacy-keyboard-interface/tree-legacy-keyboard-interface-example.css diff --git a/src/components-examples/cdk/tree/cdk-tree-legacy-keyboard-interface/cdk-tree-legacy-keyboard-interface-example.html b/src/components-examples/material/tree/tree-legacy-keyboard-interface/tree-legacy-keyboard-interface-example.html similarity index 67% rename from src/components-examples/cdk/tree/cdk-tree-legacy-keyboard-interface/cdk-tree-legacy-keyboard-interface-example.html rename to src/components-examples/material/tree/tree-legacy-keyboard-interface/tree-legacy-keyboard-interface-example.html index 26671ecd3381..dc133a66ca8e 100644 --- a/src/components-examples/cdk/tree/cdk-tree-legacy-keyboard-interface/cdk-tree-legacy-keyboard-interface-example.html +++ b/src/components-examples/material/tree/tree-legacy-keyboard-interface/tree-legacy-keyboard-interface-example.html @@ -1,18 +1,18 @@ - + - {{node.name}} - + - {{node.name}} - - + + diff --git a/src/components-examples/cdk/tree/cdk-tree-legacy-keyboard-interface/cdk-tree-legacy-keyboard-interface-example.ts b/src/components-examples/material/tree/tree-legacy-keyboard-interface/tree-legacy-keyboard-interface-example.ts similarity index 82% rename from src/components-examples/cdk/tree/cdk-tree-legacy-keyboard-interface/cdk-tree-legacy-keyboard-interface-example.ts rename to src/components-examples/material/tree/tree-legacy-keyboard-interface/tree-legacy-keyboard-interface-example.ts index ae7bafc9e69d..bbe854a191ea 100644 --- a/src/components-examples/cdk/tree/cdk-tree-legacy-keyboard-interface/cdk-tree-legacy-keyboard-interface-example.ts +++ b/src/components-examples/material/tree/tree-legacy-keyboard-interface/tree-legacy-keyboard-interface-example.ts @@ -1,9 +1,10 @@ import {ChangeDetectionStrategy, Component} from '@angular/core'; import {ArrayDataSource} from '@angular/cdk/collections'; -import {FlatTreeControl, CdkTreeModule} from '@angular/cdk/tree'; +import {FlatTreeControl} from '@angular/cdk/tree'; import {MatIconModule} from '@angular/material/icon'; import {MatButtonModule} from '@angular/material/button'; import {NOOP_TREE_KEY_MANAGER_FACTORY_PROVIDER} from '@angular/cdk/a11y'; +import {MatTreeModule} from '@angular/material/tree'; const TREE_DATA: ExampleFlatNode[] = [ { @@ -74,15 +75,15 @@ interface ExampleFlatNode { * @title Tree with flat nodes */ @Component({ - selector: 'cdk-tree-legacy-keyboard-interface-example', - templateUrl: 'cdk-tree-legacy-keyboard-interface-example.html', - styleUrls: ['cdk-tree-legacy-keyboard-interface-example.css'], + selector: 'tree-legacy-keyboard-interface-example', + templateUrl: 'tree-legacy-keyboard-interface-example.html', + styleUrls: ['tree-legacy-keyboard-interface-example.css'], standalone: true, - imports: [CdkTreeModule, MatButtonModule, MatIconModule], + imports: [MatTreeModule, MatButtonModule, MatIconModule], providers: [NOOP_TREE_KEY_MANAGER_FACTORY_PROVIDER], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CdkTreeLegacyKeyboardInterfaceExample { +export class TreeLegacyKeyboardInterfaceExample { treeControl = new FlatTreeControl( node => node.level, node => node.expandable, diff --git a/src/dev-app/tree/tree-demo.html b/src/dev-app/tree/tree-demo.html index 55a59636d204..52d4216c2aee 100644 --- a/src/dev-app/tree/tree-demo.html +++ b/src/dev-app/tree/tree-demo.html @@ -49,6 +49,6 @@ Legacy Keyboard Interface - + diff --git a/src/dev-app/tree/tree-demo.ts b/src/dev-app/tree/tree-demo.ts index 695858753b12..614ed8494be9 100644 --- a/src/dev-app/tree/tree-demo.ts +++ b/src/dev-app/tree/tree-demo.ts @@ -17,12 +17,12 @@ import { CdkTreeFlatChildrenAccessorExample, CdkTreeComplexExample, CdkTreeCustomKeyManagerExample, - CdkTreeLegacyKeyboardInterfaceExample, } from '@angular/components-examples/cdk/tree'; import { TreeDynamicExample, TreeFlatOverviewExample, TreeHarnessExample, + TreeLegacyKeyboardInterfaceExample, TreeLoadmoreExample, TreeNestedOverviewExample, } from '@angular/components-examples/material/tree'; @@ -48,7 +48,6 @@ import {MatTreeModule} from '@angular/material/tree'; CdkTreeNestedExample, CdkTreeFlatChildrenAccessorExample, CdkTreeFlatLevelAccessorExample, - CdkTreeLegacyKeyboardInterfaceExample, CdkTreeNestedChildrenAccessorExample, CdkTreeNestedLevelAccessorExample, CdkTreeComplexExample, @@ -57,6 +56,7 @@ import {MatTreeModule} from '@angular/material/tree'; TreeDynamicExample, TreeFlatOverviewExample, TreeHarnessExample, + TreeLegacyKeyboardInterfaceExample, TreeLoadmoreExample, TreeNestedOverviewExample, MatButtonModule, diff --git a/src/material/tree/BUILD.bazel b/src/material/tree/BUILD.bazel index 0d7c23940276..dfd5b373bd73 100644 --- a/src/material/tree/BUILD.bazel +++ b/src/material/tree/BUILD.bazel @@ -50,6 +50,7 @@ ng_test_library( ), deps = [ ":tree", + "//src/cdk/a11y", "//src/cdk/tree", "@npm//rxjs", ], diff --git a/src/material/tree/node.ts b/src/material/tree/node.ts index a1be3ba7f2bc..c4aa1be2d584 100644 --- a/src/material/tree/node.ts +++ b/src/material/tree/node.ts @@ -51,7 +51,7 @@ function isNoopTreeKeyManager( '[attr.aria-posinset]': '_getPositionInSet()', '[attr.aria-setsize]': '_getSetSize()', '(click)': '_focusItem()', - 'tabindex': '_getTabindexAttribute()', + '[tabindex]': '_getTabindexAttribute()', }, }) export class MatTreeNode extends CdkTreeNode implements OnInit, OnDestroy { @@ -63,18 +63,18 @@ export class MatTreeNode extends CdkTreeNode implements OnInit, * an unexpected state. Tabindex to be removed in a future version. * @breaking-change 19.0.0 Remove this attribute. */ - @Input({ transform: (value: unknown) => (value == null ? 0 : numberAttribute(value)), + alias: 'tabIndex', }) - get tabIndex(): number { - return this.isDisabled ? -1 : this._tabIndex; + get tabIndexInputBinding(): number { + return this._tabIndexInputBinding; } - set tabIndex(value: number) { + set tabIndexInputBinding(value: number) { // If the specified tabIndex value is null or undefined, fall back to the default value. - this._tabIndex = value; + this._tabIndexInputBinding = value; } - private _tabIndex: number; + private _tabIndexInputBinding: number; /** * The default tabindex of the tree node. @@ -88,9 +88,9 @@ export class MatTreeNode extends CdkTreeNode implements OnInit, protected _getTabindexAttribute() { if (isNoopTreeKeyManager(this._tree._keyManager)) { - return this.tabIndex; + return this.tabIndexInputBinding; } - return -1; + return this._tabindex; } /** @@ -122,7 +122,7 @@ export class MatTreeNode extends CdkTreeNode implements OnInit, ) { super(elementRef, tree); - this.tabIndex = Number(tabIndex) || this.defaultTabIndex; + this.tabIndexInputBinding = Number(tabIndex) || this.defaultTabIndex; } // This is a workaround for https://github.com/angular/angular/issues/23091 diff --git a/src/material/tree/tree-using-legacy-key-manager.spec.ts b/src/material/tree/tree-using-legacy-key-manager.spec.ts new file mode 100644 index 000000000000..91b772c18970 --- /dev/null +++ b/src/material/tree/tree-using-legacy-key-manager.spec.ts @@ -0,0 +1,91 @@ +import {Component, ElementRef, QueryList, ViewChild, ViewChildren} from '@angular/core'; +import {of} from 'rxjs'; +import {MatTree} from './tree'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {MatTreeModule} from './tree-module'; +import {NOOP_TREE_KEY_MANAGER_FACTORY_PROVIDER} from '@angular/cdk/a11y'; + +describe('MatTree when provided LegacyTreeKeyManager', () => { + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [MatTreeModule], + declarations: [SimpleMatTreeApp], + providers: [NOOP_TREE_KEY_MANAGER_FACTORY_PROVIDER], + }).compileComponents(); + + fixture = TestBed.createComponent(SimpleMatTreeApp); + fixture.detectChanges(); + }); + + describe('when nodes do not have a tabindex set', () => { + it('Should render tabindex attribute of 0', () => { + const treeItems = fixture.componentInstance.treeNodes; + + expect(treeItems.map(x => `${x.nativeElement.getAttribute('tabindex')}`).join(', ')) + .withContext('tabindex of tree nodes') + .toEqual('0, 0, 0'); + }); + }); + + describe('when nodes have TabIndex Input binding of 42', () => { + it('Should render tabindex attribute of 42.', () => { + fixture.componentInstance.tabIndexInputBinding = 42; + fixture.detectChanges(); + + const treeItems = fixture.componentInstance.treeNodes; + + expect(treeItems.map(x => `${x.nativeElement.getAttribute('tabindex')}`).join(', ')) + .withContext('tabindex of tree nodes') + .toEqual('42, 42, 42'); + }); + }); + + describe('when nodes have tabindex attribute binding of 2', () => { + it('should render tabindex attribute of 2', () => { + fixture.componentInstance.tabindexAttributeBinding = '2'; + fixture.detectChanges(); + + const treeItems = fixture.componentInstance.treeNodes; + + expect(treeItems.map(x => `${x.nativeElement.getAttribute('tabindex')}`).join(', ')) + .withContext('tabindex of tree nodes') + .toEqual('2, 2, 2'); + }); + }); +}); + +class MinimalTestData { + constructor(public name: string) {} + children: MinimalTestData[] = []; +} + +@Component({ + template: ` + + + {{node.name}} + + + `, +}) +class SimpleMatTreeApp { + isExpandable = (node: MinimalTestData) => node.children.length > 0; + getChildren = (node: MinimalTestData) => node.children; + + dataSource = of([ + new MinimalTestData('lettuce'), + new MinimalTestData('tomato'), + new MinimalTestData('onion'), + ]); + + /** Value passed to tabIndex Input binding of each tree node. Null by default. */ + tabIndexInputBinding: number | null = null; + /** Value passed to tabindex attribute binding of each tree node. Null by default. */ + tabindexAttributeBinding: string | null = null; + + @ViewChild(MatTree) tree: MatTree; + @ViewChildren('node') treeNodes: QueryList>; +} diff --git a/src/material/tree/tree-using-tree-control.spec.ts b/src/material/tree/tree-using-tree-control.spec.ts index c96cf1bc6e4c..909ddbf62c4a 100644 --- a/src/material/tree/tree-using-tree-control.spec.ts +++ b/src/material/tree/tree-using-tree-control.spec.ts @@ -439,9 +439,13 @@ describe('MatTree', () => { }); it('with the right aria-expanded attrs', () => { - expect(getNodeAttributes(getNodes(treeElement), 'aria-expanded')) + expect( + getNodes(treeElement) + .map(x => `${x.getAttribute('aria-expanded')}`) + .join(', '), + ) .withContext('aria-expanded attributes') - .toEqual(['false', 'false', 'false']); + .toEqual('false, false, false'); component.toggleRecursively = false; const data = underlyingDataSource.data; @@ -454,9 +458,13 @@ describe('MatTree', () => { // NB: only four elements are present here; children are not present // in DOM unless the parent node is expanded. - expect(getNodeAttributes(getNodes(treeElement), 'aria-expanded')) + expect( + getNodes(treeElement) + .map(x => `${x.getAttribute('aria-expanded')}`) + .join(', '), + ) .withContext('aria-expanded attributes') - .toEqual(['false', 'true', 'false', 'false']); + .toEqual('false, true, false, false'); }); it('should expand/collapse the node', () => { @@ -573,67 +581,50 @@ describe('MatTree', () => { }); describe('focus management', () => { - it('the tree is tabbable when no element is active', () => { - expect(treeElement.getAttribute('tabindex')).toBe('0'); - }); - - it('the tree is not tabbable when an element is active', () => { - // activate the second child by clicking on it - nodes[1].click(); - - expect(treeElement.getAttribute('tabindex')).toBe(null); - }); - it('sets tabindex on the latest activated item, with all others "-1"', () => { // activate the second child by clicking on it nodes[1].click(); + fixture.detectChanges(); - expect(getNodeAttributes(nodes, 'tabindex')).toEqual(['-1', '0', '-1', '-1', '-1', '-1']); + expect(nodes.map(x => x.getAttribute('tabindex')).join(', ')).toEqual( + '-1, 0, -1, -1, -1, -1', + ); // activate the first child by clicking on it nodes[0].click(); + fixture.detectChanges(); - expect(getNodeAttributes(nodes, 'tabindex')).toEqual(['0', '-1', '-1', '-1', '-1', '-1']); + expect(nodes.map(x => x.getAttribute('tabindex')).join(', ')).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(); + fixture.detectChanges(); expect(document.activeElement).toBe(nodes[1]); // blur the currently active element (which we just checked is the above node) nodes[1].blur(); + fixture.detectChanges(); - expect(treeElement.getAttribute('tabindex')).toBe(null); - expect(getNodeAttributes(nodes, 'tabindex')).toEqual(['-1', '0', '-1', '-1', '-1', '-1']); + expect(nodes.map(x => x.getAttribute('tabindex')).join(', ')).toEqual( + '-1, 0, -1, -1, -1, -1', + ); }); it('ignores clicks on disabled items', () => { - underlyingDataSource.data[0].isDisabled = true; + underlyingDataSource.data[1].isDisabled = true; fixture.detectChanges(); // attempt to click on the first child - nodes[0].click(); - - expect(treeElement.getAttribute('tabindex')).toBe('0'); - expect(getNodeAttributes(nodes, 'tabindex')).toEqual(['-1', '-1', '-1', '-1', '-1', '-1']); - }); - - describe('when no item is currently active', () => { - it('redirects focus to the first item when the tree is focused', () => { - treeElement.focus(); - - expect(document.activeElement).toBe(nodes[0]); - }); - - it('redirects focus to the first non-disabled item when the tree is focused', () => { - underlyingDataSource.data[0].isDisabled = true; - fixture.detectChanges(); - - treeElement.focus(); + nodes[1].click(); + fixture.detectChanges(); - expect(document.activeElement).toBe(nodes[1]); - }); + expect(nodes.map(x => x.getAttribute('tabindex')).join(', ')).toEqual( + '0, -1, -1, -1, -1, -1', + ); }); }); @@ -643,43 +634,38 @@ describe('MatTree', () => { }); it('sets the treeitem role on all nodes', () => { - expect(getNodeAttributes(nodes, 'role')).toEqual([ - 'treeitem', - 'treeitem', - 'treeitem', - 'treeitem', - 'treeitem', - 'treeitem', - ]); + expect(nodes.map(x => x.getAttribute('role')).join(', ')).toEqual( + 'treeitem, treeitem, treeitem, treeitem, treeitem, treeitem', + ); }); it('sets aria attributes for tree nodes', () => { - expect(getNodeAttributes(nodes, 'aria-expanded')) + expect(nodes.map(x => `${x.getAttribute('aria-expanded')}`).join(', ')) .withContext('aria-expanded attributes') - .toEqual(['false', 'false', 'false', 'false', 'false', 'false']); - expect(getNodeAttributes(nodes, 'aria-level')) + .toEqual('false, false, false, false, false, false'); + expect(nodes.map(x => `${x.getAttribute('aria-level')}`).join(', ')) .withContext('aria-level attributes') - .toEqual(['1', '1', '2', '3', '3', '1']); - expect(getNodeAttributes(nodes, 'aria-posinset')) + .toEqual('1, 1, 2, 3, 3, 1'); + expect(nodes.map(x => `${x.getAttribute('aria-posinset')}`).join(', ')) .withContext('aria-posinset attributes') - .toEqual(['1', '2', '1', '1', '2', '3']); - expect(getNodeAttributes(nodes, 'aria-setsize')) + .toEqual('1, 2, 1, 1, 2, 3'); + expect(nodes.map(x => `${x.getAttribute('aria-setsize')}`).join(', ')) .withContext('aria-setsize attributes') - .toEqual(['3', '3', '1', '2', '2', '3']); + .toEqual('3, 3, 1, 2, 2, 3'); }); it('changes aria-expanded status when expanded or collapsed', () => { tree.expand(underlyingDataSource.data[1]); fixture.detectChanges(); - expect(getNodeAttributes(nodes, 'aria-expanded')) + expect(nodes.map(x => `${x.getAttribute('aria-expanded')}`).join(', ')) .withContext('aria-expanded attributes') - .toEqual(['false', 'true', 'false', 'false', 'false', 'false']); + .toEqual('false, true, false, false, false, false'); tree.collapse(underlyingDataSource.data[1]); fixture.detectChanges(); - expect(getNodeAttributes(nodes, 'aria-expanded')) + expect(nodes.map(x => `${x.getAttribute('aria-expanded')}`).join(', ')) .withContext('aria-expanded attributes') - .toEqual(['false', 'false', 'false', 'false', 'false', 'false']); + .toEqual('false, false, false, false, false, false'); }); }); }); @@ -870,10 +856,6 @@ function expectNestedTreeToMatch(treeElement: Element, ...expectedTree: any[]) { } } -function getNodeAttributes(nodes: HTMLElement[], attribute: string) { - return nodes.map(node => node.getAttribute(attribute)); -} - @Component({ template: ` diff --git a/src/material/tree/tree.spec.ts b/src/material/tree/tree.spec.ts index 211c978de260..46a72fca1f35 100644 --- a/src/material/tree/tree.spec.ts +++ b/src/material/tree/tree.spec.ts @@ -428,9 +428,11 @@ describe('MatTree', () => { }); it('with the right aria-expanded attrs', () => { - expect(getNodeAttributes(getNodes(treeElement), 'aria-expanded')) - .withContext('aria-expanded attributes') - .toEqual([null, null, null]); + expect( + getNodes(treeElement) + .map(x => `${x.getAttribute('aria-expanded')}`) + .join(', '), + ).toEqual('null, null, null'); component.toggleRecursively = false; const data = underlyingDataSource.data; @@ -443,9 +445,13 @@ describe('MatTree', () => { // NB: only four elements are present here; children are not present // in DOM unless the parent node is expanded. - expect(getNodeAttributes(getNodes(treeElement), 'aria-expanded')) + expect( + getNodes(treeElement) + .map(x => `${x.getAttribute('aria-expanded')}`) + .join(', '), + ) .withContext('aria-expanded attributes') - .toEqual([null, 'true', 'false', null]); + .toEqual('null, true, false, null'); }); it('should expand/collapse the node', () => { @@ -560,39 +566,41 @@ describe('MatTree', () => { }); describe('focus management', () => { - it('the tree is tabbable when no element is active', () => { - expect(treeElement.getAttribute('tabindex')).toBe('0'); - }); - - it('the tree is not tabbable when an element is active', () => { - // activate the second child by clicking on it - nodes[1].click(); - - expect(treeElement.getAttribute('tabindex')).toBe(null); - }); - it('sets tabindex on the latest activated item, with all others "-1"', () => { // activate the second child by clicking on it nodes[1].click(); + fixture.detectChanges(); - expect(getNodeAttributes(nodes, 'tabindex')).toEqual(['-1', '0', '-1', '-1', '-1', '-1']); + expect(nodes.map(x => x.getAttribute('tabindex')).join(', ')).toEqual( + '-1, 0, -1, -1, -1, -1', + ); // activate the first child by clicking on it nodes[0].click(); + fixture.detectChanges(); - expect(getNodeAttributes(nodes, 'tabindex')).toEqual(['0', '-1', '-1', '-1', '-1', '-1']); + expect(nodes.map(x => x.getAttribute('tabindex')).join(', ')).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(); + fixture.detectChanges(); + + expect(nodes.map(x => x.getAttribute('tabindex')).join(', ')).toEqual( + '-1, 0, -1, -1, -1, -1', + ); expect(document.activeElement).toBe(nodes[1]); // blur the currently active element (which we just checked is the above node) nodes[1].blur(); + fixture.detectChanges(); - expect(treeElement.getAttribute('tabindex')).toBe(null); - expect(getNodeAttributes(nodes, 'tabindex')).toEqual(['-1', '0', '-1', '-1', '-1', '-1']); + expect(nodes.map(x => x.getAttribute('tabindex')).join(', ')).toEqual( + '-1, 0, -1, -1, -1, -1', + ); }); it('ignores clicks on disabled items', () => { @@ -600,30 +608,11 @@ describe('MatTree', () => { fixture.detectChanges(); // attempt to click on the first child - nodes[0].click(); - - expect(treeElement.getAttribute('tabindex')).toBe('0'); - expect(getNodeAttributes(nodes, 'tabindex')).toEqual(['-1', '-1', '-1', '-1', '-1', '-1']); - }); - - describe('when no item is currently active', () => { - it('redirects focus to the first item when the tree is focused', () => { - treeElement.focus(); - - fixture.detectChanges(); - - expect(document.activeElement).toEqual(nodes[0]); - }); - - it('redirects focus to the first non-disabled item when the tree is focused', () => { - underlyingDataSource.data[0].isDisabled = true; - fixture.detectChanges(); - - treeElement.focus(); - fixture.detectChanges(); + nodes[1].click(); - expect(document.activeElement).toEqual(nodes[1]); - }); + expect(nodes.map(x => x.getAttribute('tabindex')).join(', ')).toEqual( + '0, -1, -1, -1, -1, -1', + ); }); }); @@ -633,27 +622,22 @@ describe('MatTree', () => { }); it('sets the treeitem role on all nodes', () => { - expect(getNodeAttributes(nodes, 'role')).toEqual([ - 'treeitem', - 'treeitem', - 'treeitem', - 'treeitem', - 'treeitem', - 'treeitem', - ]); + expect(nodes.map(x => x.getAttribute('role')).join(', ')).toEqual( + 'treeitem, treeitem, treeitem, treeitem, treeitem, treeitem', + ); }); it('sets aria attributes for tree nodes', () => { - expect(getNodeAttributes(nodes, 'aria-expanded')) + expect(nodes.map(x => x.getAttribute('aria-expanded'))) .withContext('aria-expanded attributes') .toEqual([null, 'false', 'false', null, null, null]); - expect(getNodeAttributes(nodes, 'aria-level')) + expect(nodes.map(x => x.getAttribute('aria-level'))) .withContext('aria-level attributes') .toEqual(['1', '1', '2', '3', '3', '1']); - expect(getNodeAttributes(nodes, 'aria-posinset')) + expect(nodes.map(x => x.getAttribute('aria-posinset'))) .withContext('aria-posinset attributes') .toEqual(['1', '2', '1', '1', '2', '3']); - expect(getNodeAttributes(nodes, 'aria-setsize')) + expect(nodes.map(x => x.getAttribute('aria-setsize'))) .withContext('aria-setsize attributes') .toEqual(['3', '3', '1', '2', '2', '3']); }); @@ -661,13 +645,13 @@ describe('MatTree', () => { it('changes aria-expanded status when expanded or collapsed', () => { tree.expand(underlyingDataSource.data[1]); fixture.detectChanges(); - expect(getNodeAttributes(nodes, 'aria-expanded')) + expect(nodes.map(x => x.getAttribute('aria-expanded'))) .withContext('aria-expanded attributes') .toEqual([null, 'true', 'false', null, null, null]); tree.collapse(underlyingDataSource.data[1]); fixture.detectChanges(); - expect(getNodeAttributes(nodes, 'aria-expanded')) + expect(nodes.map(x => x.getAttribute('aria-expanded'))) .withContext('aria-expanded attributes') .toEqual([null, 'false', 'false', null, null, null]); }); @@ -860,10 +844,6 @@ function expectNestedTreeToMatch(treeElement: Element, ...expectedTree: any[]) { } } -function getNodeAttributes(nodes: HTMLElement[], attribute: string) { - return nodes.map(node => node.getAttribute(attribute)); -} - @Component({ template: ` diff --git a/tools/public_api_guard/cdk/a11y.md b/tools/public_api_guard/cdk/a11y.md index 570a23823e68..094d487b4c60 100644 --- a/tools/public_api_guard/cdk/a11y.md +++ b/tools/public_api_guard/cdk/a11y.md @@ -345,33 +345,6 @@ export class IsFocusableConfig { ignoreVisibility: boolean; } -// @public @deprecated -export function LEGACY_TREE_KEY_MANAGER_FACTORY(): TreeKeyManagerFactory; - -// @public @deprecated -export const LEGACY_TREE_KEY_MANAGER_FACTORY_PROVIDER: { - provide: InjectionToken>; - useFactory: typeof LEGACY_TREE_KEY_MANAGER_FACTORY; -}; - -// @public @deprecated -export class LegacyTreeKeyManager implements TreeKeyManagerStrategy { - // (undocumented) - readonly change: Subject; - // (undocumented) - focusItem(): void; - // (undocumented) - getActiveItem(): null; - // (undocumented) - getActiveItemIndex(): null; - // (undocumented) - readonly _isLegacyTreeKeyManager = true; - // (undocumented) - onInitialFocus(): void; - // (undocumented) - onKeydown(): void; -} - // @public export class ListKeyManager { constructor(_items: QueryList | T[]); @@ -444,6 +417,31 @@ export interface LiveAnnouncerDefaultOptions { // @public @deprecated export const MESSAGES_CONTAINER_ID = "cdk-describedby-message-container"; +// @public @deprecated +export function NOOP_TREE_KEY_MANAGER_FACTORY(): TreeKeyManagerFactory; + +// @public @deprecated +export const NOOP_TREE_KEY_MANAGER_FACTORY_PROVIDER: { + provide: InjectionToken>; + useFactory: typeof NOOP_TREE_KEY_MANAGER_FACTORY; +}; + +// @public @deprecated +export class NoopTreeKeyManager implements TreeKeyManagerStrategy { + // (undocumented) + readonly change: Subject; + // (undocumented) + focusItem(): void; + // (undocumented) + getActiveItem(): null; + // (undocumented) + getActiveItemIndex(): null; + // (undocumented) + readonly _isNoopTreeKeyManager = true; + // (undocumented) + onKeydown(): void; +} + // @public export interface RegisteredMessage { messageElement: Element; @@ -482,7 +480,6 @@ export class TreeKeyManager implements TreeKeyMana }): void; getActiveItem(): T | null; getActiveItemIndex(): number | null; - onInitialFocus(): void; onKeydown(event: KeyboardEvent): void; } @@ -500,6 +497,7 @@ export interface TreeKeyManagerItem { getParent(): TreeKeyManagerItem | null; isDisabled?: (() => boolean) | boolean; isExpanded: (() => boolean) | boolean; + unfocus(): void; } // @public @@ -526,7 +524,6 @@ export interface TreeKeyManagerStrategy { }): void; getActiveItem(): T | null; getActiveItemIndex(): number | null; - onInitialFocus(): void; onKeydown(event: KeyboardEvent): void; } diff --git a/tools/public_api_guard/cdk/tree.md b/tools/public_api_guard/cdk/tree.md index 710c613d0ece..6caabe904f23 100644 --- a/tools/public_api_guard/cdk/tree.md +++ b/tools/public_api_guard/cdk/tree.md @@ -88,7 +88,6 @@ export class CdkTree implements AfterContentChecked, AfterContentInit, expandAll(): void; expandDescendants(dataNode: T): void; expansionKey?: (dataNode: T) => K; - _focusInitialTreeItem(): void; _getChildrenAccessor(): ((dataNode: T) => T[] | Observable | null | undefined) | undefined; _getDirectChildren(dataNode: T): Observable; _getLevel(node: T): number | undefined; @@ -117,7 +116,6 @@ export class CdkTree implements AfterContentChecked, AfterContentInit, renderNodeChanges(data: readonly T[], dataDiffer?: IterableDiffer, viewContainer?: ViewContainerRef, parentData?: T): void; _sendKeydownToKeyManager(event: KeyboardEvent): void; _setNodeTypeIfUnset(nodeType: 'flat' | 'nested'): void; - _setTabIndex(): void; toggle(dataNode: T): void; toggleDescendants(dataNode: T): void; trackBy: TrackByFunction; @@ -196,11 +194,10 @@ export class CdkTreeNode implements OnDestroy, OnInit, TreeKeyManagerI get role(): 'treeitem' | 'group'; set role(_role: 'treeitem' | 'group'); // (undocumented) - _setTabFocusable(): void; - // (undocumented) - _setTabUnfocusable(): void; + protected _tabindex: number | null; // (undocumented) protected _tree: CdkTree; + unfocus(): void; // (undocumented) static ɵdir: i0.ɵɵDirectiveDeclaration, "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, false, never>; // (undocumented) diff --git a/tools/public_api_guard/material/tree.md b/tools/public_api_guard/material/tree.md index 2f65f84ad891..494026b90443 100644 --- a/tools/public_api_guard/material/tree.md +++ b/tools/public_api_guard/material/tree.md @@ -123,20 +123,20 @@ export class MatTreeNode extends CdkTreeNode implements OnInit, get disabled(): boolean; set disabled(value: boolean); // (undocumented) - protected _getTabindexAttribute(): number; + protected _getTabindexAttribute(): number | null; // (undocumented) static ngAcceptInputType_disabled: unknown; // (undocumented) - static ngAcceptInputType_tabIndex: unknown; + static ngAcceptInputType_tabIndexInputBinding: unknown; // (undocumented) ngOnDestroy(): void; // (undocumented) ngOnInit(): void; // @deprecated - get tabIndex(): number; - set tabIndex(value: number); + get tabIndexInputBinding(): number; + set tabIndexInputBinding(value: number); // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration, "mat-tree-node", ["matTreeNode"], { "tabIndex": { "alias": "tabIndex"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; }, { "activation": "activation"; "expandedChange": "expandedChange"; }, never, never, false, never>; + static ɵdir: i0.ɵɵDirectiveDeclaration, "mat-tree-node", ["matTreeNode"], { "tabIndexInputBinding": { "alias": "tabIndex"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; }, { "activation": "activation"; "expandedChange": "expandedChange"; }, never, never, false, never>; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration, [null, null, { attribute: "tabindex"; }]>; }