From a9bc7f4a3c542348a7c89a0df10ca75fe231ad45 Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:22 -0500 Subject: [PATCH 1/8] feat(cdk/a11y): implement typeahead (needs test) --- src/cdk/a11y/key-manager/tree-key-manager.ts | 76 +++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/src/cdk/a11y/key-manager/tree-key-manager.ts b/src/cdk/a11y/key-manager/tree-key-manager.ts index 3d39eaabdc24..8b8a6cca108f 100644 --- a/src/cdk/a11y/key-manager/tree-key-manager.ts +++ b/src/cdk/a11y/key-manager/tree-key-manager.ts @@ -18,8 +18,17 @@ import { UP_ARROW, } from '@angular/cdk/keycodes'; import {QueryList} from '@angular/core'; -import {isObservable, of as observableOf, Observable, Subject} from 'rxjs'; -import {take} from 'rxjs/operators'; +import { + of as observableOf, + isObservable, + Observable, + Subject, + Subscription, + throwError, +} from 'rxjs'; +import {debounceTime, filter, map, switchMap, take, tap} from 'rxjs/operators'; + +const DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL_MS = 200; function coerceObservable(data: T | Observable): Observable { if (!isObservable(data)) { @@ -111,6 +120,8 @@ export class TreeKeyManager { private _activeItem: T | null = null; private _activationFollowsFocus = false; private _horizontal: 'ltr' | 'rtl' = 'ltr'; + private readonly _letterKeyStream = new Subject(); + private _typeaheadSubscription = Subscription.EMPTY; /** * Predicate function that can be used to check whether an item should be skipped @@ -121,6 +132,9 @@ export class TreeKeyManager { /** Function to determine equivalent items. */ private _trackByFn: (item: T) => unknown = (item: T) => item; + /** Buffer for the letters that the user has pressed when the typeahead option is turned on. */ + private _pressedLetters: string[] = []; + private _items: T[] = []; constructor({ @@ -143,6 +157,13 @@ export class TreeKeyManager { if (typeof activationFollowsFocus !== 'undefined') { this._activationFollowsFocus = activationFollowsFocus; } + if (typeof typeAheadDebounceInterval !== 'undefined') { + this._setTypeAhead( + typeof typeAheadDebounceInterval === 'number' + ? typeAheadDebounceInterval + : DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL_MS, + ); + } // 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 @@ -288,6 +309,57 @@ export class TreeKeyManager { } } + private _setTypeAhead(debounceInterval: number) { + this._typeaheadSubscription.unsubscribe(); + + // Debounce the presses of non-navigational keys, collect the ones that correspond to letters + // and convert those letters back into a string. Afterwards find the first item that starts + // with that string and select it. + this._typeaheadSubscription = this._getItems() + .pipe( + switchMap(items => { + if ( + (typeof ngDevMode === 'undefined' || ngDevMode) && + items.length && + items.some(item => typeof item.getLabel !== 'function') + ) { + return throwError( + new Error( + 'TreeKeyManager items in typeahead mode must implement the `getLabel` method.', + ), + ); + } + return observableOf(items) as Observable>; + }), + switchMap(items => { + return this._letterKeyStream.pipe( + tap(letter => this._pressedLetters.push(letter)), + debounceTime(debounceInterval), + filter(() => this._pressedLetters.length > 0), + map(() => [this._pressedLetters.join(''), items] as const), + ); + }), + ) + .subscribe(([inputString, items]) => { + // Start at 1 because we want to start searching at the item immediately + // following the current active item. + for (let i = 1; i < items.length + 1; i++) { + const index = (this._activeItemIndex + i) % items.length; + const item = items[index]; + + if ( + !this._skipPredicateFn(item) && + item.getLabel().toUpperCase().trim().indexOf(inputString) === 0 + ) { + this._setActiveItem(index); + break; + } + } + + this._pressedLetters = []; + }); + } + //// Navigational methods private _focusFirstItem() { From 87d773e4602079697aef75ea782fdbff5efee7ac Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:22 -0500 Subject: [PATCH 2/8] feat(cdk/a11y): handle typeahead in keydown handler --- src/cdk/a11y/key-manager/tree-key-manager.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/cdk/a11y/key-manager/tree-key-manager.ts b/src/cdk/a11y/key-manager/tree-key-manager.ts index 8b8a6cca108f..29a170c28af2 100644 --- a/src/cdk/a11y/key-manager/tree-key-manager.ts +++ b/src/cdk/a11y/key-manager/tree-key-manager.ts @@ -243,11 +243,21 @@ export class TreeKeyManager { break; } + // Attempt to use the `event.key` which also maps it to the user's keyboard language, + // otherwise fall back to resolving alphanumeric characters via the keyCode. + if (event.key && event.key.length === 1) { + this._letterKeyStream.next(event.key.toLocaleUpperCase()); + } else if ((keyCode >= A && keyCode <= Z) || (keyCode >= ZERO && keyCode <= NINE)) { + this._letterKeyStream.next(String.fromCharCode(keyCode)); + } + // NB: return here, in order to avoid preventing the default action of non-navigational // keys or resetting the buffer of pressed letters. return; } + // Reset the typeahead since the user has used a navigational key. + this._pressedLetters = []; event.preventDefault(); } From 0f0c3a6c222b5e09decb9be9300c41fbe5071bfb Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:22 -0500 Subject: [PATCH 3/8] feat(cdk/a11y): fix typeahead build errors --- src/cdk/a11y/key-manager/tree-key-manager.ts | 51 ++++++++------------ 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/src/cdk/a11y/key-manager/tree-key-manager.ts b/src/cdk/a11y/key-manager/tree-key-manager.ts index 29a170c28af2..10a033ff374f 100644 --- a/src/cdk/a11y/key-manager/tree-key-manager.ts +++ b/src/cdk/a11y/key-manager/tree-key-manager.ts @@ -24,9 +24,8 @@ import { Observable, Subject, Subscription, - throwError, } from 'rxjs'; -import {debounceTime, filter, map, switchMap, take, tap} from 'rxjs/operators'; +import {debounceTime, filter, map, take, tap} from 'rxjs/operators'; const DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL_MS = 200; @@ -322,44 +321,36 @@ export class TreeKeyManager { private _setTypeAhead(debounceInterval: number) { this._typeaheadSubscription.unsubscribe(); + if ( + (typeof ngDevMode === 'undefined' || ngDevMode) && + this._items.length && + this._items.some(item => typeof item.getLabel !== 'function') + ) { + throw new Error( + 'TreeKeyManager items in typeahead mode must implement the `getLabel` method.', + ); + } + // Debounce the presses of non-navigational keys, collect the ones that correspond to letters // and convert those letters back into a string. Afterwards find the first item that starts // with that string and select it. - this._typeaheadSubscription = this._getItems() + this._typeaheadSubscription = this._letterKeyStream .pipe( - switchMap(items => { - if ( - (typeof ngDevMode === 'undefined' || ngDevMode) && - items.length && - items.some(item => typeof item.getLabel !== 'function') - ) { - return throwError( - new Error( - 'TreeKeyManager items in typeahead mode must implement the `getLabel` method.', - ), - ); - } - return observableOf(items) as Observable>; - }), - switchMap(items => { - return this._letterKeyStream.pipe( - tap(letter => this._pressedLetters.push(letter)), - debounceTime(debounceInterval), - filter(() => this._pressedLetters.length > 0), - map(() => [this._pressedLetters.join(''), items] as const), - ); - }), + tap(letter => this._pressedLetters.push(letter)), + debounceTime(debounceInterval), + filter(() => this._pressedLetters.length > 0), + map(() => this._pressedLetters.join('')), ) - .subscribe(([inputString, items]) => { + .subscribe(inputString => { // Start at 1 because we want to start searching at the item immediately // following the current active item. - for (let i = 1; i < items.length + 1; i++) { - const index = (this._activeItemIndex + i) % items.length; - const item = items[index]; + for (let i = 1; i < this._items.length + 1; i++) { + const index = (this._activeItemIndex + i) % this._items.length; + const item = this._items[index]; if ( !this._skipPredicateFn(item) && - item.getLabel().toUpperCase().trim().indexOf(inputString) === 0 + item.getLabel?.().toUpperCase().trim().indexOf(inputString) === 0 ) { this._setActiveItem(index); break; From c2de6292574eb2882736757ca28c401637444c7a Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:22 -0500 Subject: [PATCH 4/8] feat(cdk/a11y): add tests for typeahead --- .../a11y/key-manager/tree-key-manager.spec.ts | 576 +++++++++++++++++- src/cdk/a11y/key-manager/tree-key-manager.ts | 48 +- 2 files changed, 585 insertions(+), 39 deletions(-) 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 5d9cb4803330..6e1b04bddb35 100644 --- a/src/cdk/a11y/key-manager/tree-key-manager.spec.ts +++ b/src/cdk/a11y/key-manager/tree-key-manager.spec.ts @@ -115,14 +115,19 @@ describe('TreeKeyManager', () => { FakeArrayTreeKeyManagerItem | FakeObservableTreeKeyManagerItem >; + let parentItem: FakeArrayTreeKeyManagerItem | FakeObservableTreeKeyManagerItem; // index 0 + let childItem: FakeArrayTreeKeyManagerItem | FakeObservableTreeKeyManagerItem; // index 1 + let childItemWithNoChildren: FakeArrayTreeKeyManagerItem | FakeObservableTreeKeyManagerItem; // index 3 + let lastItem: FakeArrayTreeKeyManagerItem | FakeObservableTreeKeyManagerItem; // index 5 + beforeEach(() => { itemList = new QueryList(); - const parent1 = new itemParam.constructor('parent1'); - const parent1Child1 = new itemParam.constructor('parent1Child1'); - const parent1Child1Child1 = new itemParam.constructor('parent1Child1Child1'); - const parent1Child2 = new itemParam.constructor('parent1Child2'); - const parent2 = new itemParam.constructor('parent2'); - const parent2Child1 = new itemParam.constructor('parent2Child1'); + const parent1 = new itemParam.constructor('one'); + const parent1Child1 = new itemParam.constructor('two'); + const parent1Child1Child1 = new itemParam.constructor('three'); + const parent1Child2 = new itemParam.constructor('four'); + const parent2 = new itemParam.constructor('five'); + const parent2Child1 = new itemParam.constructor('six'); parent1._children = [parent1Child1, parent1Child2]; parent1Child1._parent = parent1; @@ -155,16 +160,12 @@ describe('TreeKeyManager', () => { keyManager.onClick(itemList.get(0)!); expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(0); - expect(keyManager.getActiveItem()?.getLabel()) - .withContext('active item label') - .toBe('parent1'); + expect(keyManager.getActiveItem()?.getLabel()).withContext('active item label').toBe('one'); itemList.reset([new FakeObservableTreeKeyManagerItem('parent0'), ...itemList.toArray()]); itemList.notifyOnChanges(); expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(1); - expect(keyManager.getActiveItem()?.getLabel()) - .withContext('active item label') - .toBe('parent1'); + expect(keyManager.getActiveItem()?.getLabel()).withContext('active item label').toBe('one'); }); describe('Key events', () => { @@ -728,6 +729,557 @@ describe('TreeKeyManager', () => { }); } }); + + describe('typeahead mode', () => { + const debounceInterval = 300; + + beforeEach(() => { + keyManager = new TreeKeyManager({ + items: itemList, + typeAheadDebounceInterval: debounceInterval, + }); + }); + + it('should throw if the items do not implement the getLabel method', () => { + const invalidQueryList = new QueryList(); + invalidQueryList.reset([{disabled: false}]); + + expect( + () => + new TreeKeyManager({ + items: invalidQueryList, + typeAheadDebounceInterval: true, + }), + ).toThrowError(/must implement/); + }); + + it('should debounce the input key presses', fakeAsync(() => { + keyManager.onKeydown(createKeyboardEvent('keydown', 79, 'o')); // types "o" + tick(1); + keyManager.onKeydown(createKeyboardEvent('keydown', 78, 'n')); // types "n" + tick(1); + keyManager.onKeydown(createKeyboardEvent('keydown', 69, 'e')); // types "e" + + expect(keyManager.getActiveItemIndex()) + .withContext('active item index, before debounce interval') + .not.toBe(0); + + tick(debounceInterval - 1); + + expect(keyManager.getActiveItemIndex()) + .withContext('active item index, after partial debounce interval') + .not.toBe(0); + + tick(1); + + expect(keyManager.getActiveItemIndex()) + .withContext('active item index, after full debounce interval') + .toBe(0); + })); + + it('uses a default debounce interval', fakeAsync(() => { + const defaultInterval = 200; + keyManager = new TreeKeyManager({ + items: itemList, + typeAheadDebounceInterval: true, + }); + + keyManager.onKeydown(createKeyboardEvent('keydown', 79, 'o')); // types "o" + tick(1); + keyManager.onKeydown(createKeyboardEvent('keydown', 78, 'n')); // types "n" + tick(1); + keyManager.onKeydown(createKeyboardEvent('keydown', 69, 'e')); // types "e" + + expect(keyManager.getActiveItemIndex()) + .withContext('active item index, before debounce interval') + .not.toBe(0); + + tick(defaultInterval - 1); + + expect(keyManager.getActiveItemIndex()) + .withContext('active item index, after partial debounce interval') + .not.toBe(0); + + tick(1); + + expect(keyManager.getActiveItemIndex()) + .withContext('active item index, after full debounce interval') + .toBe(0); + })); + + it('should focus the first item that starts with a letter', fakeAsync(() => { + keyManager.onKeydown(createKeyboardEvent('keydown', 84, 't')); // types "t" + + tick(debounceInterval); + + expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(1); + })); + + it('should focus the first item that starts with sequence of letters', fakeAsync(() => { + keyManager.onKeydown(createKeyboardEvent('keydown', 84, 't')); // types "t" + keyManager.onKeydown(createKeyboardEvent('keydown', 72, 'h')); // types "h" + + tick(debounceInterval); + + expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(2); + })); + + it('should cancel any pending timers if a navigation key is pressed', fakeAsync(() => { + keyManager.onKeydown(createKeyboardEvent('keydown', 84, 't')); // types "t" + keyManager.onKeydown(createKeyboardEvent('keydown', 72, 'h')); // types "h" + keyManager.onKeydown(fakeKeyEvents.downArrow); + + tick(debounceInterval); + + expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(0); + })); + + it('should handle non-English input', fakeAsync(() => { + itemList.reset([ + new itemParam.constructor('едно'), + new itemParam.constructor('две'), + new itemParam.constructor('три'), + ]); + itemList.notifyOnChanges(); + + const keyboardEvent = createKeyboardEvent('keydown', 68, 'д'); + + keyManager.onKeydown(keyboardEvent); // types "д" + tick(debounceInterval); + + expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(1); + })); + + it('should handle non-letter characters', fakeAsync(() => { + itemList.reset([ + new itemParam.constructor('[]'), + new itemParam.constructor('321'), + new itemParam.constructor('`!?'), + ]); + itemList.notifyOnChanges(); + + keyManager.onKeydown(createKeyboardEvent('keydown', 192, '`')); // types "`" + tick(debounceInterval); + expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(2); + + keyManager.onKeydown(createKeyboardEvent('keydown', 51, '3')); // types "3" + tick(debounceInterval); + expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(1); + + keyManager.onKeydown(createKeyboardEvent('keydown', 219, '[')); // types "[" + tick(debounceInterval); + expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(0); + })); + + it('should not focus disabled items', fakeAsync(() => { + expect(keyManager.getActiveItemIndex()).withContext('initial active item index').toBe(-1); + + parentItem.isDisabled = true; + + keyManager.onKeydown(createKeyboardEvent('keydown', 79, 'o')); // types "o" + tick(debounceInterval); + + expect(keyManager.getActiveItemIndex()).withContext('initial active item index').toBe(-1); + })); + + it('should start looking for matches after the active item', fakeAsync(() => { + const frodo = new itemParam.constructor('Frodo'); + itemList.reset([ + new itemParam.constructor('Bilbo'), + frodo, + new itemParam.constructor('Pippin'), + new itemParam.constructor('Boromir'), + new itemParam.constructor('Aragorn'), + ]); + itemList.notifyOnChanges(); + + keyManager.onClick(frodo); + keyManager.onKeydown(createKeyboardEvent('keydown', 66, 'b')); + tick(debounceInterval); + + expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(3); + })); + + it('should wrap back around if there were no matches after the active item', fakeAsync(() => { + const boromir = new itemParam.constructor('Boromir'); + itemList.reset([ + new itemParam.constructor('Bilbo'), + new itemParam.constructor('Frodo'), + new itemParam.constructor('Pippin'), + boromir, + new itemParam.constructor('Aragorn'), + ]); + itemList.notifyOnChanges(); + + keyManager.onClick(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.onKeydown(createKeyboardEvent('keydown', 79, 'o')); + tick(debounceInterval); + + expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(0); + })); + + it('should be able to select the first item', fakeAsync(() => { + keyManager.onKeydown(createKeyboardEvent('keydown', 79, 'o')); + tick(debounceInterval); + + expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(0); + })); + + it('should not do anything if there is no match', fakeAsync(() => { + keyManager.onKeydown(createKeyboardEvent('keydown', 87, 'w')); + tick(debounceInterval); + + expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(-1); + })); + }); }); } +<<<<<<< HEAD +======= + + // describe('programmatic focus', () => { + // it('should setActiveItem()', () => { + // expect(keyManager.activeItemIndex) + // .withContext(`Expected first item of the list to be active.`) + // .toBe(0); + // + // keyManager.setActiveItem(1); + // expect(keyManager.activeItemIndex) + // .withContext(`Expected activeItemIndex to be updated when setActiveItem() was called.`) + // .toBe(1); + // }); + // + // it('should be able to set the active item by reference', () => { + // expect(keyManager.activeItemIndex) + // .withContext(`Expected first item of the list to be active.`) + // .toBe(0); + // + // keyManager.setActiveItem(itemList.toArray()[2]); + // expect(keyManager.activeItemIndex) + // .withContext(`Expected activeItemIndex to be updated.`) + // .toBe(2); + // }); + // + // it('should be able to set the active item without emitting an event', () => { + // const spy = jasmine.createSpy('change spy'); + // const subscription = keyManager.change.subscribe(spy); + // + // expect(keyManager.activeItemIndex).toBe(0); + // + // keyManager.updateActiveItem(2); + // + // expect(keyManager.activeItemIndex).toBe(2); + // expect(spy).not.toHaveBeenCalled(); + // + // subscription.unsubscribe(); + // }); + // + // it('should expose the active item correctly', () => { + // keyManager.onKeydown(fakeKeyEvents.downArrow); + // + // expect(keyManager.activeItemIndex) + // .withContext('Expected active item to be the second option.') + // .toBe(1); + // expect(keyManager.activeItem) + // .withContext('Expected the active item to match the second option.') + // .toBe(itemList.toArray()[1]); + // + // keyManager.onKeydown(fakeKeyEvents.downArrow); + // expect(keyManager.activeItemIndex) + // .withContext('Expected active item to be the third option.') + // .toBe(2); + // expect(keyManager.activeItem) + // .withContext('Expected the active item ID to match the third option.') + // .toBe(itemList.toArray()[2]); + // }); + // + // it('should setFirstItemActive()', () => { + // keyManager.onKeydown(fakeKeyEvents.downArrow); + // keyManager.onKeydown(fakeKeyEvents.downArrow); + // expect(keyManager.activeItemIndex) + // .withContext(`Expected last item of the list to be active.`) + // .toBe(2); + // + // keyManager.setFirstItemActive(); + // expect(keyManager.activeItemIndex) + // .withContext(`Expected setFirstItemActive() to set the active item to the first item.`) + // .toBe(0); + // }); + // + // it('should set the active item to the second item if the first one is disabled', () => { + // const items = itemList.toArray(); + // items[0].disabled = true; + // itemList.reset(items); + // + // keyManager.setFirstItemActive(); + // expect(keyManager.activeItemIndex) + // .withContext(`Expected the second item to be active if the first was disabled.`) + // .toBe(1); + // }); + // + // it('should setLastItemActive()', () => { + // expect(keyManager.activeItemIndex) + // .withContext(`Expected first item of the list to be active.`) + // .toBe(0); + // + // keyManager.setLastItemActive(); + // expect(keyManager.activeItemIndex) + // .withContext(`Expected setLastItemActive() to set the active item to the last item.`) + // .toBe(2); + // }); + // + // it('should set the active item to the second to last item if the last is disabled', () => { + // const items = itemList.toArray(); + // items[2].disabled = true; + // itemList.reset(items); + // + // keyManager.setLastItemActive(); + // expect(keyManager.activeItemIndex) + // .withContext(`Expected the second to last item to be active if the last was disabled.`) + // .toBe(1); + // }); + // + // it('should setNextItemActive()', () => { + // expect(keyManager.activeItemIndex) + // .withContext(`Expected first item of the list to be active.`) + // .toBe(0); + // + // keyManager.setNextItemActive(); + // expect(keyManager.activeItemIndex) + // .withContext(`Expected setNextItemActive() to set the active item to the next item.`) + // .toBe(1); + // }); + // + // it('should set the active item to the next enabled item if next is disabled', () => { + // const items = itemList.toArray(); + // items[1].disabled = true; + // itemList.reset(items); + // + // expect(keyManager.activeItemIndex) + // .withContext(`Expected first item of the list to be active.`) + // .toBe(0); + // + // keyManager.setNextItemActive(); + // expect(keyManager.activeItemIndex) + // .withContext(`Expected setNextItemActive() to only set enabled items as active.`) + // .toBe(2); + // }); + // + // it('should setPreviousItemActive()', () => { + // keyManager.onKeydown(fakeKeyEvents.downArrow); + // expect(keyManager.activeItemIndex) + // .withContext(`Expected second item of the list to be active.`) + // .toBe(1); + // + // keyManager.setPreviousItemActive(); + // expect(keyManager.activeItemIndex) + // .withContext(`Expected setPreviousItemActive() to set the active item to the previous.`) + // .toBe(0); + // }); + // + // it('should skip disabled items when setPreviousItemActive() is called', () => { + // const items = itemList.toArray(); + // items[1].disabled = true; + // itemList.reset(items); + // + // keyManager.onKeydown(fakeKeyEvents.downArrow); + // keyManager.onKeydown(fakeKeyEvents.downArrow); + // expect(keyManager.activeItemIndex) + // .withContext(`Expected third item of the list to be active.`) + // .toBe(2); + // + // keyManager.setPreviousItemActive(); + // expect(keyManager.activeItemIndex) + // .withContext(`Expected setPreviousItemActive() to skip the disabled item.`) + // .toBe(0); + // }); + // + // it('should not emit an event if the item did not change', () => { + // const spy = jasmine.createSpy('change spy'); + // const subscription = keyManager.change.subscribe(spy); + // + // keyManager.setActiveItem(2); + // keyManager.setActiveItem(2); + // + // expect(spy).toHaveBeenCalledTimes(1); + // + // subscription.unsubscribe(); + // }); + // }); + // + // describe('wrap mode', () => { + // it('should return itself to allow chaining', () => { + // expect(keyManager.withWrap()) + // .withContext(`Expected withWrap() to return an instance of ListKeyManager.`) + // .toEqual(keyManager); + // }); + // + // it('should wrap focus when arrow keying past items while in wrap mode', () => { + // keyManager.withWrap(); + // keyManager.onKeydown(fakeKeyEvents.downArrow); + // keyManager.onKeydown(fakeKeyEvents.downArrow); + // + // expect(keyManager.activeItemIndex).withContext('Expected last item to be active.').toBe(2); + // + // // this down arrow moves down past the end of the list + // keyManager.onKeydown(fakeKeyEvents.downArrow); + // expect(keyManager.activeItemIndex) + // .withContext('Expected active item to wrap to beginning.') + // .toBe(0); + // + // // this up arrow moves up past the beginning of the list + // keyManager.onKeydown(fakeKeyEvents.upArrow); + // expect(keyManager.activeItemIndex) + // .withContext('Expected active item to wrap to end.') + // .toBe(2); + // }); + // + // it('should set last item active when up arrow is pressed if no active item', () => { + // keyManager.withWrap(); + // keyManager.setActiveItem(-1); + // keyManager.onKeydown(fakeKeyEvents.upArrow); + // + // expect(keyManager.activeItemIndex) + // .withContext('Expected last item to be active on up arrow if no active item.') + // .toBe(2); + // expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(0); + // expect(keyManager.setActiveItem).toHaveBeenCalledWith(2); + // + // keyManager.onKeydown(fakeKeyEvents.downArrow); + // expect(keyManager.activeItemIndex) + // .withContext('Expected active item to be 0 after wrapping back to beginning.') + // .toBe(0); + // expect(keyManager.setActiveItem).toHaveBeenCalledWith(0); + // }); + // + // // This test should pass if all items are disabled and the down arrow key got pressed. + // // If the test setup crashes or this test times out, this test can be considered as failed. + // it('should not get into an infinite loop if all items are disabled', () => { + // keyManager.withWrap(); + // keyManager.setActiveItem(0); + // const items = itemList.toArray(); + // items.forEach(item => (item.disabled = true)); + // itemList.reset(items); + // + // keyManager.onKeydown(fakeKeyEvents.downArrow); + // }); + // + // it('should be able to disable wrapping', () => { + // keyManager.withWrap(); + // keyManager.setFirstItemActive(); + // keyManager.onKeydown(fakeKeyEvents.upArrow); + // + // expect(keyManager.activeItemIndex).toBe(itemList.length - 1); + // + // keyManager.withWrap(false); + // keyManager.setFirstItemActive(); + // keyManager.onKeydown(fakeKeyEvents.upArrow); + // + // expect(keyManager.activeItemIndex).toBe(0); + // }); + // }); + // + // describe('skip predicate', () => { + // it('should skip disabled items by default', () => { + // const items = itemList.toArray(); + // items[1].disabled = true; + // itemList.reset(items); + // + // expect(keyManager.activeItemIndex).toBe(0); + // + // keyManager.onKeydown(fakeKeyEvents.downArrow); + // + // expect(keyManager.activeItemIndex).toBe(2); + // }); + // + // it('should be able to skip items with a custom predicate', () => { + // keyManager.skipPredicate(item => item.skipItem); + // + // const items = itemList.toArray(); + // items[1].skipItem = true; + // itemList.reset(items); + // + // expect(keyManager.activeItemIndex).toBe(0); + // + // keyManager.onKeydown(fakeKeyEvents.downArrow); + // + // expect(keyManager.activeItemIndex).toBe(2); + // }); + // }); + // + // + // let keyManager: FocusKeyManager; + // + // beforeEach(() => { + // itemList.reset([new FakeFocusable(), new FakeFocusable(), new FakeFocusable()]); + // keyManager = new FocusKeyManager(itemList); + // + // // first item is already focused + // keyManager.setFirstItemActive(); + // + // spyOn(itemList.toArray()[0], 'focus'); + // spyOn(itemList.toArray()[1], 'focus'); + // spyOn(itemList.toArray()[2], 'focus'); + // }); + // + // it('should focus subsequent items when down arrow is pressed', () => { + // keyManager.onKeydown(fakeKeyEvents.downArrow); + // + // expect(itemList.toArray()[0].focus).not.toHaveBeenCalled(); + // expect(itemList.toArray()[1].focus).toHaveBeenCalledTimes(1); + // expect(itemList.toArray()[2].focus).not.toHaveBeenCalled(); + // + // keyManager.onKeydown(fakeKeyEvents.downArrow); + // expect(itemList.toArray()[0].focus).not.toHaveBeenCalled(); + // expect(itemList.toArray()[1].focus).toHaveBeenCalledTimes(1); + // expect(itemList.toArray()[2].focus).toHaveBeenCalledTimes(1); + // }); + // + // it('should focus previous items when up arrow is pressed', () => { + // keyManager.onKeydown(fakeKeyEvents.downArrow); + // + // expect(itemList.toArray()[0].focus).not.toHaveBeenCalled(); + // expect(itemList.toArray()[1].focus).toHaveBeenCalledTimes(1); + // + // keyManager.onKeydown(fakeKeyEvents.upArrow); + // + // expect(itemList.toArray()[0].focus).toHaveBeenCalledTimes(1); + // expect(itemList.toArray()[1].focus).toHaveBeenCalledTimes(1); + // }); + // + // it('should allow setting the focused item without calling focus', () => { + // expect(keyManager.activeItemIndex) + // .withContext(`Expected first item of the list to be active.`) + // .toBe(0); + // + // keyManager.updateActiveItem(1); + // expect(keyManager.activeItemIndex) + // .withContext(`Expected activeItemIndex to update after calling updateActiveItem().`) + // .toBe(1); + // expect(itemList.toArray()[1].focus).not.toHaveBeenCalledTimes(1); + // }); + // + // it('should be able to set the focus origin', () => { + // keyManager.setFocusOrigin('mouse'); + // + // keyManager.onKeydown(fakeKeyEvents.downArrow); + // expect(itemList.toArray()[1].focus).toHaveBeenCalledWith('mouse'); + // + // keyManager.onKeydown(fakeKeyEvents.downArrow); + // expect(itemList.toArray()[2].focus).toHaveBeenCalledWith('mouse'); + // + // keyManager.setFocusOrigin('keyboard'); + // + // keyManager.onKeydown(fakeKeyEvents.upArrow); + // expect(itemList.toArray()[1].focus).toHaveBeenCalledWith('keyboard'); + // }); +>>>>>>> ff6a4790f (feat(cdk/a11y): add tests for typeahead) }); diff --git a/src/cdk/a11y/key-manager/tree-key-manager.ts b/src/cdk/a11y/key-manager/tree-key-manager.ts index 10a033ff374f..9c22322277ac 100644 --- a/src/cdk/a11y/key-manager/tree-key-manager.ts +++ b/src/cdk/a11y/key-manager/tree-key-manager.ts @@ -18,13 +18,7 @@ import { UP_ARROW, } from '@angular/cdk/keycodes'; import {QueryList} from '@angular/core'; -import { - of as observableOf, - isObservable, - Observable, - Subject, - Subscription, -} from 'rxjs'; +import {of as observableOf, isObservable, Observable, Subject, Subscription} from 'rxjs'; import {debounceTime, filter, map, take, tap} from 'rxjs/operators'; const DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL_MS = 200; @@ -144,6 +138,24 @@ export class TreeKeyManager { activationFollowsFocus, typeAheadDebounceInterval, }: 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 + // items aren't being collected via `ViewChildren` or `ContentChildren`). + if (items instanceof QueryList) { + this._items = items.toArray(); + items.changes.subscribe((newItems: QueryList) => { + this._items = newItems.toArray(); + this._updateActiveItemIndex(this._items); + }); + } else if (isObservable(items)) { + items.subscribe(newItems => { + this._items = newItems; + this._updateActiveItemIndex(newItems); + }); + } else { + this._items = items; + } + if (typeof skipPredicate !== 'undefined') { this._skipPredicateFn = skipPredicate; } @@ -163,24 +175,6 @@ export class TreeKeyManager { : DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL_MS, ); } - - // 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 - // items aren't being collected via `ViewChildren` or `ContentChildren`). - if (items instanceof QueryList) { - this._items = items.toArray(); - items.changes.subscribe((newItems: QueryList) => { - this._items = newItems.toArray(); - this._updateActiveItemIndex(this._items); - }); - } else if (isObservable(items)) { - items.subscribe(newItems => { - this._items = newItems; - this._updateActiveItemIndex(newItems); - }); - } else { - this._items = items; - } } /** @@ -339,7 +333,7 @@ export class TreeKeyManager { tap(letter => this._pressedLetters.push(letter)), debounceTime(debounceInterval), filter(() => this._pressedLetters.length > 0), - map(() => this._pressedLetters.join('')), + map(() => this._pressedLetters.join('').toLocaleUpperCase()), ) .subscribe(inputString => { // Start at 1 because we want to start searching at the item immediately @@ -350,7 +344,7 @@ export class TreeKeyManager { if ( !this._skipPredicateFn(item) && - item.getLabel?.().toUpperCase().trim().indexOf(inputString) === 0 + item.getLabel?.().toLocaleUpperCase().trim().indexOf(inputString) === 0 ) { this._setActiveItem(index); break; From 9e188075bb8d7607ff8441541d966912fa9663ac Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:22 -0500 Subject: [PATCH 5/8] feat(cdk/a11y): add TreeKeyManager to public a11y API --- src/cdk/a11y/public-api.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cdk/a11y/public-api.ts b/src/cdk/a11y/public-api.ts index af4e24404387..ea64d62578b7 100644 --- a/src/cdk/a11y/public-api.ts +++ b/src/cdk/a11y/public-api.ts @@ -10,6 +10,7 @@ export * from './aria-describer/aria-reference'; export * from './key-manager/activedescendant-key-manager'; export * from './key-manager/focus-key-manager'; export * from './key-manager/list-key-manager'; +export * from './key-manager/tree-key-manager'; export * from './focus-trap/configurable-focus-trap'; export * from './focus-trap/configurable-focus-trap-config'; export * from './focus-trap/configurable-focus-trap-factory'; From 6500c9b138946c39e7686177ef1a59f283f67d3d Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Tue, 16 May 2023 19:32:44 +0000 Subject: [PATCH 6/8] fix(cdk/a11y): tree key manager build errors/weird merge --- .../a11y/key-manager/tree-key-manager.spec.ts | 341 +----------------- src/cdk/a11y/key-manager/tree-key-manager.ts | 4 + 2 files changed, 5 insertions(+), 340 deletions(-) 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 6e1b04bddb35..90f66e51a842 100644 --- a/src/cdk/a11y/key-manager/tree-key-manager.spec.ts +++ b/src/cdk/a11y/key-manager/tree-key-manager.spec.ts @@ -15,6 +15,7 @@ import {QueryList} from '@angular/core'; import {take} from 'rxjs/operators'; import {TreeKeyManager, TreeKeyManagerItem} from './tree-key-manager'; import {Observable, of as observableOf, Subscription} from 'rxjs'; +import {fakeAsync, tick} from '@angular/core/testing'; class FakeBaseTreeKeyManagerItem { _isExpanded = false; @@ -942,344 +943,4 @@ describe('TreeKeyManager', () => { }); }); } -<<<<<<< HEAD -======= - - // describe('programmatic focus', () => { - // it('should setActiveItem()', () => { - // expect(keyManager.activeItemIndex) - // .withContext(`Expected first item of the list to be active.`) - // .toBe(0); - // - // keyManager.setActiveItem(1); - // expect(keyManager.activeItemIndex) - // .withContext(`Expected activeItemIndex to be updated when setActiveItem() was called.`) - // .toBe(1); - // }); - // - // it('should be able to set the active item by reference', () => { - // expect(keyManager.activeItemIndex) - // .withContext(`Expected first item of the list to be active.`) - // .toBe(0); - // - // keyManager.setActiveItem(itemList.toArray()[2]); - // expect(keyManager.activeItemIndex) - // .withContext(`Expected activeItemIndex to be updated.`) - // .toBe(2); - // }); - // - // it('should be able to set the active item without emitting an event', () => { - // const spy = jasmine.createSpy('change spy'); - // const subscription = keyManager.change.subscribe(spy); - // - // expect(keyManager.activeItemIndex).toBe(0); - // - // keyManager.updateActiveItem(2); - // - // expect(keyManager.activeItemIndex).toBe(2); - // expect(spy).not.toHaveBeenCalled(); - // - // subscription.unsubscribe(); - // }); - // - // it('should expose the active item correctly', () => { - // keyManager.onKeydown(fakeKeyEvents.downArrow); - // - // expect(keyManager.activeItemIndex) - // .withContext('Expected active item to be the second option.') - // .toBe(1); - // expect(keyManager.activeItem) - // .withContext('Expected the active item to match the second option.') - // .toBe(itemList.toArray()[1]); - // - // keyManager.onKeydown(fakeKeyEvents.downArrow); - // expect(keyManager.activeItemIndex) - // .withContext('Expected active item to be the third option.') - // .toBe(2); - // expect(keyManager.activeItem) - // .withContext('Expected the active item ID to match the third option.') - // .toBe(itemList.toArray()[2]); - // }); - // - // it('should setFirstItemActive()', () => { - // keyManager.onKeydown(fakeKeyEvents.downArrow); - // keyManager.onKeydown(fakeKeyEvents.downArrow); - // expect(keyManager.activeItemIndex) - // .withContext(`Expected last item of the list to be active.`) - // .toBe(2); - // - // keyManager.setFirstItemActive(); - // expect(keyManager.activeItemIndex) - // .withContext(`Expected setFirstItemActive() to set the active item to the first item.`) - // .toBe(0); - // }); - // - // it('should set the active item to the second item if the first one is disabled', () => { - // const items = itemList.toArray(); - // items[0].disabled = true; - // itemList.reset(items); - // - // keyManager.setFirstItemActive(); - // expect(keyManager.activeItemIndex) - // .withContext(`Expected the second item to be active if the first was disabled.`) - // .toBe(1); - // }); - // - // it('should setLastItemActive()', () => { - // expect(keyManager.activeItemIndex) - // .withContext(`Expected first item of the list to be active.`) - // .toBe(0); - // - // keyManager.setLastItemActive(); - // expect(keyManager.activeItemIndex) - // .withContext(`Expected setLastItemActive() to set the active item to the last item.`) - // .toBe(2); - // }); - // - // it('should set the active item to the second to last item if the last is disabled', () => { - // const items = itemList.toArray(); - // items[2].disabled = true; - // itemList.reset(items); - // - // keyManager.setLastItemActive(); - // expect(keyManager.activeItemIndex) - // .withContext(`Expected the second to last item to be active if the last was disabled.`) - // .toBe(1); - // }); - // - // it('should setNextItemActive()', () => { - // expect(keyManager.activeItemIndex) - // .withContext(`Expected first item of the list to be active.`) - // .toBe(0); - // - // keyManager.setNextItemActive(); - // expect(keyManager.activeItemIndex) - // .withContext(`Expected setNextItemActive() to set the active item to the next item.`) - // .toBe(1); - // }); - // - // it('should set the active item to the next enabled item if next is disabled', () => { - // const items = itemList.toArray(); - // items[1].disabled = true; - // itemList.reset(items); - // - // expect(keyManager.activeItemIndex) - // .withContext(`Expected first item of the list to be active.`) - // .toBe(0); - // - // keyManager.setNextItemActive(); - // expect(keyManager.activeItemIndex) - // .withContext(`Expected setNextItemActive() to only set enabled items as active.`) - // .toBe(2); - // }); - // - // it('should setPreviousItemActive()', () => { - // keyManager.onKeydown(fakeKeyEvents.downArrow); - // expect(keyManager.activeItemIndex) - // .withContext(`Expected second item of the list to be active.`) - // .toBe(1); - // - // keyManager.setPreviousItemActive(); - // expect(keyManager.activeItemIndex) - // .withContext(`Expected setPreviousItemActive() to set the active item to the previous.`) - // .toBe(0); - // }); - // - // it('should skip disabled items when setPreviousItemActive() is called', () => { - // const items = itemList.toArray(); - // items[1].disabled = true; - // itemList.reset(items); - // - // keyManager.onKeydown(fakeKeyEvents.downArrow); - // keyManager.onKeydown(fakeKeyEvents.downArrow); - // expect(keyManager.activeItemIndex) - // .withContext(`Expected third item of the list to be active.`) - // .toBe(2); - // - // keyManager.setPreviousItemActive(); - // expect(keyManager.activeItemIndex) - // .withContext(`Expected setPreviousItemActive() to skip the disabled item.`) - // .toBe(0); - // }); - // - // it('should not emit an event if the item did not change', () => { - // const spy = jasmine.createSpy('change spy'); - // const subscription = keyManager.change.subscribe(spy); - // - // keyManager.setActiveItem(2); - // keyManager.setActiveItem(2); - // - // expect(spy).toHaveBeenCalledTimes(1); - // - // subscription.unsubscribe(); - // }); - // }); - // - // describe('wrap mode', () => { - // it('should return itself to allow chaining', () => { - // expect(keyManager.withWrap()) - // .withContext(`Expected withWrap() to return an instance of ListKeyManager.`) - // .toEqual(keyManager); - // }); - // - // it('should wrap focus when arrow keying past items while in wrap mode', () => { - // keyManager.withWrap(); - // keyManager.onKeydown(fakeKeyEvents.downArrow); - // keyManager.onKeydown(fakeKeyEvents.downArrow); - // - // expect(keyManager.activeItemIndex).withContext('Expected last item to be active.').toBe(2); - // - // // this down arrow moves down past the end of the list - // keyManager.onKeydown(fakeKeyEvents.downArrow); - // expect(keyManager.activeItemIndex) - // .withContext('Expected active item to wrap to beginning.') - // .toBe(0); - // - // // this up arrow moves up past the beginning of the list - // keyManager.onKeydown(fakeKeyEvents.upArrow); - // expect(keyManager.activeItemIndex) - // .withContext('Expected active item to wrap to end.') - // .toBe(2); - // }); - // - // it('should set last item active when up arrow is pressed if no active item', () => { - // keyManager.withWrap(); - // keyManager.setActiveItem(-1); - // keyManager.onKeydown(fakeKeyEvents.upArrow); - // - // expect(keyManager.activeItemIndex) - // .withContext('Expected last item to be active on up arrow if no active item.') - // .toBe(2); - // expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(0); - // expect(keyManager.setActiveItem).toHaveBeenCalledWith(2); - // - // keyManager.onKeydown(fakeKeyEvents.downArrow); - // expect(keyManager.activeItemIndex) - // .withContext('Expected active item to be 0 after wrapping back to beginning.') - // .toBe(0); - // expect(keyManager.setActiveItem).toHaveBeenCalledWith(0); - // }); - // - // // This test should pass if all items are disabled and the down arrow key got pressed. - // // If the test setup crashes or this test times out, this test can be considered as failed. - // it('should not get into an infinite loop if all items are disabled', () => { - // keyManager.withWrap(); - // keyManager.setActiveItem(0); - // const items = itemList.toArray(); - // items.forEach(item => (item.disabled = true)); - // itemList.reset(items); - // - // keyManager.onKeydown(fakeKeyEvents.downArrow); - // }); - // - // it('should be able to disable wrapping', () => { - // keyManager.withWrap(); - // keyManager.setFirstItemActive(); - // keyManager.onKeydown(fakeKeyEvents.upArrow); - // - // expect(keyManager.activeItemIndex).toBe(itemList.length - 1); - // - // keyManager.withWrap(false); - // keyManager.setFirstItemActive(); - // keyManager.onKeydown(fakeKeyEvents.upArrow); - // - // expect(keyManager.activeItemIndex).toBe(0); - // }); - // }); - // - // describe('skip predicate', () => { - // it('should skip disabled items by default', () => { - // const items = itemList.toArray(); - // items[1].disabled = true; - // itemList.reset(items); - // - // expect(keyManager.activeItemIndex).toBe(0); - // - // keyManager.onKeydown(fakeKeyEvents.downArrow); - // - // expect(keyManager.activeItemIndex).toBe(2); - // }); - // - // it('should be able to skip items with a custom predicate', () => { - // keyManager.skipPredicate(item => item.skipItem); - // - // const items = itemList.toArray(); - // items[1].skipItem = true; - // itemList.reset(items); - // - // expect(keyManager.activeItemIndex).toBe(0); - // - // keyManager.onKeydown(fakeKeyEvents.downArrow); - // - // expect(keyManager.activeItemIndex).toBe(2); - // }); - // }); - // - // - // let keyManager: FocusKeyManager; - // - // beforeEach(() => { - // itemList.reset([new FakeFocusable(), new FakeFocusable(), new FakeFocusable()]); - // keyManager = new FocusKeyManager(itemList); - // - // // first item is already focused - // keyManager.setFirstItemActive(); - // - // spyOn(itemList.toArray()[0], 'focus'); - // spyOn(itemList.toArray()[1], 'focus'); - // spyOn(itemList.toArray()[2], 'focus'); - // }); - // - // it('should focus subsequent items when down arrow is pressed', () => { - // keyManager.onKeydown(fakeKeyEvents.downArrow); - // - // expect(itemList.toArray()[0].focus).not.toHaveBeenCalled(); - // expect(itemList.toArray()[1].focus).toHaveBeenCalledTimes(1); - // expect(itemList.toArray()[2].focus).not.toHaveBeenCalled(); - // - // keyManager.onKeydown(fakeKeyEvents.downArrow); - // expect(itemList.toArray()[0].focus).not.toHaveBeenCalled(); - // expect(itemList.toArray()[1].focus).toHaveBeenCalledTimes(1); - // expect(itemList.toArray()[2].focus).toHaveBeenCalledTimes(1); - // }); - // - // it('should focus previous items when up arrow is pressed', () => { - // keyManager.onKeydown(fakeKeyEvents.downArrow); - // - // expect(itemList.toArray()[0].focus).not.toHaveBeenCalled(); - // expect(itemList.toArray()[1].focus).toHaveBeenCalledTimes(1); - // - // keyManager.onKeydown(fakeKeyEvents.upArrow); - // - // expect(itemList.toArray()[0].focus).toHaveBeenCalledTimes(1); - // expect(itemList.toArray()[1].focus).toHaveBeenCalledTimes(1); - // }); - // - // it('should allow setting the focused item without calling focus', () => { - // expect(keyManager.activeItemIndex) - // .withContext(`Expected first item of the list to be active.`) - // .toBe(0); - // - // keyManager.updateActiveItem(1); - // expect(keyManager.activeItemIndex) - // .withContext(`Expected activeItemIndex to update after calling updateActiveItem().`) - // .toBe(1); - // expect(itemList.toArray()[1].focus).not.toHaveBeenCalledTimes(1); - // }); - // - // it('should be able to set the focus origin', () => { - // keyManager.setFocusOrigin('mouse'); - // - // keyManager.onKeydown(fakeKeyEvents.downArrow); - // expect(itemList.toArray()[1].focus).toHaveBeenCalledWith('mouse'); - // - // keyManager.onKeydown(fakeKeyEvents.downArrow); - // expect(itemList.toArray()[2].focus).toHaveBeenCalledWith('mouse'); - // - // keyManager.setFocusOrigin('keyboard'); - // - // keyManager.onKeydown(fakeKeyEvents.upArrow); - // expect(itemList.toArray()[1].focus).toHaveBeenCalledWith('keyboard'); - // }); ->>>>>>> ff6a4790f (feat(cdk/a11y): add tests for typeahead) }); diff --git a/src/cdk/a11y/key-manager/tree-key-manager.ts b/src/cdk/a11y/key-manager/tree-key-manager.ts index 9c22322277ac..e998ece5d102 100644 --- a/src/cdk/a11y/key-manager/tree-key-manager.ts +++ b/src/cdk/a11y/key-manager/tree-key-manager.ts @@ -16,6 +16,10 @@ import { SPACE, TAB, UP_ARROW, + A, + Z, + ZERO, + NINE, } from '@angular/cdk/keycodes'; import {QueryList} from '@angular/core'; import {of as observableOf, isObservable, Observable, Subject, Subscription} from 'rxjs'; From a0c5ffcaa7ea79c333c1f84c5ffaf0c5b3abd5bb Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Tue, 16 May 2023 19:40:27 +0000 Subject: [PATCH 7/8] feat(cdk/a11y): fix api goldens --- tools/public_api_guard/cdk/a11y.md | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tools/public_api_guard/cdk/a11y.md b/tools/public_api_guard/cdk/a11y.md index a2b136168ad7..95ea88805610 100644 --- a/tools/public_api_guard/cdk/a11y.md +++ b/tools/public_api_guard/cdk/a11y.md @@ -424,6 +424,41 @@ export interface RegisteredMessage { // @public export function removeAriaReferencedId(el: Element, attr: `aria-${string}`, id: string): void; +// @public +export class TreeKeyManager { + constructor({ items, skipPredicate, trackBy, horizontalOrientation, activationFollowsFocus, typeAheadDebounceInterval, }: TreeKeyManagerOptions); + readonly change: Subject; + getActiveItem(): T | null; + getActiveItemIndex(): number | null; + onClick(treeItem: T): void; + onKeydown(event: KeyboardEvent): void; + readonly tabOut: Subject; +} + +// @public +export interface TreeKeyManagerItem { + activate(): void; + collapse(): void; + expand(): void; + focus(): void; + getChildren(): TreeKeyManagerItem[] | Observable; + getLabel?(): string; + getParent(): TreeKeyManagerItem | null; + isDisabled?: (() => boolean) | boolean; + isExpanded: (() => boolean) | boolean; +} + +// @public (undocumented) +export interface TreeKeyManagerOptions { + activationFollowsFocus?: boolean; + horizontalOrientation?: 'rtl' | 'ltr'; + // (undocumented) + items: Observable | QueryList | T[]; + skipPredicate?: (item: T) => boolean; + trackBy?: (treeItem: T) => unknown; + typeAheadDebounceInterval?: true | number; +} + // (No @packageDocumentation comment for this package) ``` From 04b06377a423fe6b111b990b67b703aac9cb233f Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Thu, 18 May 2023 19:11:48 +0000 Subject: [PATCH 8/8] fix(cdk/a11y): fix tests --- src/cdk/a11y/key-manager/tree-key-manager.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) 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 90f66e51a842..cceea2b40dec 100644 --- a/src/cdk/a11y/key-manager/tree-key-manager.spec.ts +++ b/src/cdk/a11y/key-manager/tree-key-manager.spec.ts @@ -138,6 +138,11 @@ describe('TreeKeyManager', () => { parent2._children = [parent2Child1]; parent2Child1._parent = parent2; + parentItem = parent1; + childItem = parent1Child1; + childItemWithNoChildren = parent1Child2; + lastItem = parent2Child1; + itemList.reset([ parent1, parent1Child1,