diff --git a/src/cdk/a11y/key-manager/list-key-manager.spec.ts b/src/cdk/a11y/key-manager/list-key-manager.spec.ts index 8dd47c94db2a..f4cff3efa4f3 100644 --- a/src/cdk/a11y/key-manager/list-key-manager.spec.ts +++ b/src/cdk/a11y/key-manager/list-key-manager.spec.ts @@ -150,6 +150,7 @@ describe('Key managers', () => { keyManager.setActiveItem(0); itemList.reset([new FakeFocusable('zero'), ...itemList.toArray()]); + itemList.notifyOnChanges(); keyManager.setActiveItem(0); expect(spy).toHaveBeenCalledTimes(1); @@ -342,6 +343,7 @@ describe('Key managers', () => { const items = itemList.toArray(); items[1].disabled = true; itemList.reset(items); + itemList.notifyOnChanges(); // Next event should skip past disabled item from 0 to 2 keyManager.onKeydown(this.nextKeyEvent); @@ -367,6 +369,7 @@ describe('Key managers', () => { items[1].disabled = undefined; items[2].disabled = undefined; itemList.reset(items); + itemList.notifyOnChanges(); keyManager.onKeydown(this.nextKeyEvent); expect(keyManager.activeItemIndex) @@ -416,6 +419,7 @@ describe('Key managers', () => { const items = itemList.toArray(); items[2].disabled = true; itemList.reset(items); + itemList.notifyOnChanges(); keyManager.onKeydown(this.nextKeyEvent); expect(keyManager.activeItemIndex) @@ -558,6 +562,7 @@ describe('Key managers', () => { const items = itemList.toArray(); items[0].disabled = true; itemList.reset(items); + itemList.notifyOnChanges(); keyManager.setFirstItemActive(); expect(keyManager.activeItemIndex) @@ -580,6 +585,7 @@ describe('Key managers', () => { const items = itemList.toArray(); items[2].disabled = true; itemList.reset(items); + itemList.notifyOnChanges(); keyManager.setLastItemActive(); expect(keyManager.activeItemIndex) @@ -602,6 +608,7 @@ describe('Key managers', () => { const items = itemList.toArray(); items[1].disabled = true; itemList.reset(items); + itemList.notifyOnChanges(); expect(keyManager.activeItemIndex) .withContext(`Expected first item of the list to be active.`) @@ -629,6 +636,7 @@ describe('Key managers', () => { const items = itemList.toArray(); items[1].disabled = true; itemList.reset(items); + itemList.notifyOnChanges(); keyManager.onKeydown(fakeKeyEvents.downArrow); keyManager.onKeydown(fakeKeyEvents.downArrow); @@ -706,6 +714,7 @@ describe('Key managers', () => { const items = itemList.toArray(); items.forEach(item => (item.disabled = true)); itemList.reset(items); + itemList.notifyOnChanges(); keyManager.onKeydown(fakeKeyEvents.downArrow); }); @@ -730,6 +739,7 @@ describe('Key managers', () => { const items = itemList.toArray(); items[1].disabled = true; itemList.reset(items); + itemList.notifyOnChanges(); expect(keyManager.activeItemIndex).toBe(0); @@ -744,6 +754,7 @@ describe('Key managers', () => { const items = itemList.toArray(); items[1].skipItem = true; itemList.reset(items); + itemList.notifyOnChanges(); expect(keyManager.activeItemIndex).toBe(0); @@ -839,6 +850,7 @@ describe('Key managers', () => { new FakeFocusable('две'), new FakeFocusable('три'), ]); + itemList.notifyOnChanges(); const keyboardEvent = createKeyboardEvent('keydown', 68, 'д'); @@ -854,6 +866,7 @@ describe('Key managers', () => { new FakeFocusable('321'), new FakeFocusable('`!?'), ]); + itemList.notifyOnChanges(); keyManager.onKeydown(createKeyboardEvent('keydown', 192, '`')); // types "`" tick(debounceInterval); @@ -874,6 +887,7 @@ describe('Key managers', () => { const items = itemList.toArray(); items[0].disabled = true; itemList.reset(items); + itemList.notifyOnChanges(); keyManager.onKeydown(createKeyboardEvent('keydown', 79, 'o')); // types "o" tick(debounceInterval); @@ -889,6 +903,7 @@ describe('Key managers', () => { new FakeFocusable('Boromir'), new FakeFocusable('Aragorn'), ]); + itemList.notifyOnChanges(); keyManager.setActiveItem(1); keyManager.onKeydown(createKeyboardEvent('keydown', 66, 'b')); @@ -905,6 +920,7 @@ describe('Key managers', () => { new FakeFocusable('Boromir'), new FakeFocusable('Aragorn'), ]); + itemList.notifyOnChanges(); keyManager.setActiveItem(3); keyManager.onKeydown(createKeyboardEvent('keydown', 66, 'b')); diff --git a/src/cdk/a11y/key-manager/list-key-manager.ts b/src/cdk/a11y/key-manager/list-key-manager.ts index 49522b7d82ab..4929c95c3b03 100644 --- a/src/cdk/a11y/key-manager/list-key-manager.ts +++ b/src/cdk/a11y/key-manager/list-key-manager.ts @@ -14,17 +14,13 @@ import { LEFT_ARROW, RIGHT_ARROW, TAB, - A, - Z, - ZERO, - NINE, hasModifierKey, HOME, END, PAGE_UP, PAGE_DOWN, } from '@angular/cdk/keycodes'; -import {debounceTime, filter, map, tap} from 'rxjs/operators'; +import {Typeahead} from './typeahead'; /** This interface is for items that can be passed to a ListKeyManager. */ export interface ListKeyManagerOption { @@ -46,7 +42,6 @@ export class ListKeyManager { private _activeItemIndex = -1; private _activeItem: T | null = null; private _wrap = false; - private readonly _letterKeyStream = new Subject(); private _typeaheadSubscription = Subscription.EMPTY; private _itemChangesSubscription?: Subscription; private _vertical = true; @@ -54,6 +49,7 @@ export class ListKeyManager { private _allowedModifierKeys: ListKeyManagerModifierKey[] = []; private _homeAndEnd = false; private _pageUpAndDown = {enabled: false, delta: 10}; + private _typeahead?: Typeahead; /** * Predicate function that can be used to check whether an item should be skipped @@ -61,21 +57,20 @@ export class ListKeyManager { */ private _skipPredicateFn = (item: T) => item.disabled; - // Buffer for the letters that the user has pressed when the typeahead option is turned on. - private _pressedLetters: string[] = []; - constructor(private _items: QueryList | T[]) { // We allow for the items to be an array 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._itemChangesSubscription = _items.changes.subscribe((newItems: QueryList) => { + const itemArray = newItems.toArray(); + this._typeahead?.setItems(itemArray); if (this._activeItem) { - const itemArray = newItems.toArray(); const newIndex = itemArray.indexOf(this._activeItem); if (newIndex > -1 && newIndex !== this._activeItemIndex) { this._activeItemIndex = newIndex; + this._typeahead?.setCurrentSelectedItemIndex(newIndex); } } }); @@ -144,53 +139,24 @@ export class ListKeyManager { * @param debounceInterval Time to wait after the last keystroke before setting the active item. */ withTypeAhead(debounceInterval: number = 200): this { - if ( - (typeof ngDevMode === 'undefined' || ngDevMode) && - this._items.length && - this._items.some(item => typeof item.getLabel !== 'function') - ) { - throw Error('ListKeyManager items in typeahead mode must implement the `getLabel` method.'); - } - 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._letterKeyStream - .pipe( - tap(letter => this._pressedLetters.push(letter)), - debounceTime(debounceInterval), - filter(() => this._pressedLetters.length > 0), - map(() => this._pressedLetters.join('')), - ) - .subscribe(inputString => { - const items = this._getItemsArray(); - - // 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; - } - } + const items = this._getItemsArray(); + this._typeahead = new Typeahead(items, { + debounceInterval: typeof debounceInterval === 'number' ? debounceInterval : undefined, + skipPredicate: item => this._skipPredicateFn(item), + }); - this._pressedLetters = []; - }); + this._typeaheadSubscription = this._typeahead.selectedItem.subscribe(item => { + this.setActiveItem(item); + }); return this; } /** Cancels the current typeahead sequence. */ cancelTypeahead(): this { - this._pressedLetters = []; + this._typeahead?.reset(); return this; } @@ -322,13 +288,7 @@ export class ListKeyManager { default: if (isModifierAllowed || hasModifierKey(event, 'shiftKey')) { - // 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)); - } + this._typeahead?.handleKey(event); } // Note that we return here, in order to avoid preventing @@ -336,7 +296,7 @@ export class ListKeyManager { return; } - this._pressedLetters = []; + this._typeahead?.reset(); event.preventDefault(); } @@ -352,7 +312,7 @@ export class ListKeyManager { /** Gets whether the user is currently typing into the manager using the typeahead feature. */ isTyping(): boolean { - return this._pressedLetters.length > 0; + return !!this._typeahead && this._typeahead.isTyping(); } /** Sets the active item to the first enabled item in the list. */ @@ -397,16 +357,16 @@ export class ListKeyManager { // Explicitly check for `null` and `undefined` because other falsy values are valid. this._activeItem = activeItem == null ? null : activeItem; this._activeItemIndex = index; + this._typeahead?.setCurrentSelectedItemIndex(index); } /** Cleans up the key manager. */ destroy() { this._typeaheadSubscription.unsubscribe(); this._itemChangesSubscription?.unsubscribe(); - this._letterKeyStream.complete(); + this._typeahead?.destroy(); this.tabOut.complete(); this.change.complete(); - this._pressedLetters = []; } /** 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 be94dc3d467f..3ad1e0253141 100644 --- a/src/cdk/a11y/key-manager/noop-tree-key-manager.ts +++ b/src/cdk/a11y/key-manager/noop-tree-key-manager.ts @@ -34,6 +34,10 @@ export class NoopTreeKeyManager implements TreeKey // implementation that does not emit to streams. readonly change = new Subject(); + destroy() { + this.change.complete(); + } + onKeydown() { // 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 f63d8225b377..3327f648f2a5 100644 --- a/src/cdk/a11y/key-manager/tree-key-manager-strategy.ts +++ b/src/cdk/a11y/key-manager/tree-key-manager-strategy.ts @@ -92,6 +92,11 @@ export interface TreeKeyManagerStrategy { /** Stream that emits any time the focused item changes. */ readonly change: Subject; + /** + * Cleans up the key manager. + */ + destroy(): void; + /** * Handles a keyboard event on the tree. * diff --git a/src/cdk/a11y/key-manager/tree-key-manager.ts b/src/cdk/a11y/key-manager/tree-key-manager.ts index 1f9b06661067..07d623e00feb 100644 --- a/src/cdk/a11y/key-manager/tree-key-manager.ts +++ b/src/cdk/a11y/key-manager/tree-key-manager.ts @@ -16,22 +16,17 @@ import { SPACE, TAB, UP_ARROW, - A, - Z, - ZERO, - NINE, } from '@angular/cdk/keycodes'; import {InjectionToken, QueryList} from '@angular/core'; -import {of as observableOf, isObservable, Observable, Subject, Subscription} from 'rxjs'; -import {debounceTime, filter, map, take, tap} from 'rxjs/operators'; +import {Observable, Subject, Subscription, isObservable, of as observableOf} from 'rxjs'; +import {take} from 'rxjs/operators'; import { TreeKeyManagerFactory, TreeKeyManagerItem, TreeKeyManagerOptions, TreeKeyManagerStrategy, } from './tree-key-manager-strategy'; - -const DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL_MS = 200; +import {Typeahead} from './typeahead'; function coerceObservable(data: T | Observable): Observable { if (!isObservable(data)) { @@ -50,8 +45,6 @@ export class TreeKeyManager implements TreeKeyMana private _activeItem: T | null = null; private _activationFollowsFocus = false; private _horizontal: 'ltr' | 'rtl' = 'ltr'; - 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. @@ -64,11 +57,11 @@ export class TreeKeyManager implements TreeKeyMana /** 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[] = []; + private _typeahead?: Typeahead; + private _typeaheadSubscription = Subscription.EMPTY; + private _hasInitialFocused = false; private _initialFocus() { @@ -100,12 +93,14 @@ export class TreeKeyManager implements TreeKeyMana this._items = items.toArray(); items.changes.subscribe((newItems: QueryList) => { this._items = newItems.toArray(); + this._typeahead?.setItems(this._items); this._updateActiveItemIndex(this._items); this._initialFocus(); }); } else if (isObservable(items)) { items.subscribe(newItems => { this._items = newItems; + this._typeahead?.setItems(newItems); this._updateActiveItemIndex(newItems); this._initialFocus(); }); @@ -127,18 +122,20 @@ export class TreeKeyManager implements TreeKeyMana this._trackByFn = config.trackBy; } if (typeof config.typeAheadDebounceInterval !== 'undefined') { - const typeAheadInterval = - typeof config.typeAheadDebounceInterval === 'number' - ? config.typeAheadDebounceInterval - : DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL_MS; - - this._setTypeAhead(typeAheadInterval); + this._setTypeAhead(config.typeAheadDebounceInterval); } } /** Stream that emits any time the focused item changes. */ readonly change = new Subject(); + /** Cleans up the key manager. */ + destroy() { + this._typeaheadSubscription.unsubscribe(); + this._typeahead?.destroy(); + this.change.complete(); + } + /** * Handles a keyboard event on the tree. * @param event Keyboard event that represents the user interaction with the tree. @@ -188,21 +185,14 @@ export class TreeKeyManager implements TreeKeyMana 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 + this._typeahead?.handleKey(event); + // 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 = []; + this._typeahead?.reset(); event.preventDefault(); } @@ -268,6 +258,7 @@ export class TreeKeyManager implements TreeKeyMana const previousActiveItem = this._activeItem; this._activeItem = activeItem ?? null; this._activeItemIndex = index; + this._typeahead?.setCurrentSelectedItemIndex(index); this._activeItem?.focus(); previousActiveItem?.unfocus(); @@ -294,50 +285,19 @@ export class TreeKeyManager implements TreeKeyMana if (newIndex > -1 && newIndex !== this._activeItemIndex) { this._activeItemIndex = newIndex; + this._typeahead?.setCurrentSelectedItemIndex(newIndex); } } - 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._letterKeyStream - .pipe( - tap(letter => this._pressedLetters.push(letter)), - debounceTime(debounceInterval), - filter(() => this._pressedLetters.length > 0), - map(() => this._pressedLetters.join('').toLocaleUpperCase()), - ) - .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 < this._items.length + 1; i++) { - const index = (this._activeItemIndex + i) % this._items.length; - const item = this._items[index]; - - if ( - !this._skipPredicateFn(item) && - item.getLabel?.().toLocaleUpperCase().trim().indexOf(inputString) === 0 - ) { - this.focusItem(index); - break; - } - } + private _setTypeAhead(debounceInterval: number | boolean) { + this._typeahead = new Typeahead(this._items, { + debounceInterval: typeof debounceInterval === 'number' ? debounceInterval : undefined, + skipPredicate: item => this._skipPredicateFn(item), + }); - this._pressedLetters = []; - }); + this._typeaheadSubscription = this._typeahead.selectedItem.subscribe(item => { + this.focusItem(item); + }); } private _findNextAvailableItemIndex(startingIndex: number) { diff --git a/src/cdk/a11y/key-manager/typeahead.ts b/src/cdk/a11y/key-manager/typeahead.ts new file mode 100644 index 000000000000..7b1c3afdc1f0 --- /dev/null +++ b/src/cdk/a11y/key-manager/typeahead.ts @@ -0,0 +1,125 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {A, NINE, Z, ZERO} from '@angular/cdk/keycodes'; +import {Subject, Observable} from 'rxjs'; +import {debounceTime, filter, map, tap} from 'rxjs/operators'; + +const DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL_MS = 200; + +interface TypeaheadItem { + getLabel?(): string; +} + +interface TypeaheadConfig { + debounceInterval?: number; + skipPredicate?: (item: T) => boolean | undefined; +} + +export class Typeahead { + private readonly _letterKeyStream = new Subject(); + private _items: T[] = []; + private _selectedItemIndex = -1; + + /** Buffer for the letters that the user has pressed */ + private _pressedLetters: string[] = []; + + private _skipPredicateFn?: (item: T) => boolean | undefined; + + private readonly _selectedItem = new Subject(); + readonly selectedItem: Observable = this._selectedItem; + + constructor(initialItems: T[], config?: TypeaheadConfig) { + const typeAheadInterval = + typeof config?.debounceInterval === 'number' + ? config.debounceInterval + : DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL_MS; + + if (config?.skipPredicate) { + this._skipPredicateFn = config.skipPredicate; + } + + if ( + (typeof ngDevMode === 'undefined' || ngDevMode) && + initialItems.length && + initialItems.some(item => typeof item.getLabel !== 'function') + ) { + throw new Error('KeyManager items in typeahead mode must implement the `getLabel` method.'); + } + + this.setItems(initialItems); + this._setupKeyHandler(typeAheadInterval); + } + + destroy() { + this._pressedLetters = []; + this._letterKeyStream.complete(); + this._selectedItem.complete(); + } + + setCurrentSelectedItemIndex(index: number) { + this._selectedItemIndex = index; + } + + setItems(items: T[]) { + this._items = items; + } + + handleKey(event: KeyboardEvent): void { + const keyCode = event.keyCode; + + // 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)); + } + } + + /** Gets whether the user is currently typing into the manager using the typeahead feature. */ + isTyping(): boolean { + return this._pressedLetters.length > 0; + } + + /** Resets the currently stored sequence of typed letters. */ + reset(): void { + this._pressedLetters = []; + } + + private _setupKeyHandler(typeAheadInterval: number) { + // 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._letterKeyStream + .pipe( + tap(letter => this._pressedLetters.push(letter)), + debounceTime(typeAheadInterval), + filter(() => this._pressedLetters.length > 0), + map(() => this._pressedLetters.join('').toLocaleUpperCase()), + ) + .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 < this._items.length + 1; i++) { + const index = (this._selectedItemIndex + i) % this._items.length; + const item = this._items[index]; + + if ( + !this._skipPredicateFn?.(item) && + item.getLabel?.().toLocaleUpperCase().trim().indexOf(inputString) === 0 + ) { + this._selectedItem.next(item); + break; + } + } + + this._pressedLetters = []; + }); + } +} diff --git a/src/cdk/tree/tree.ts b/src/cdk/tree/tree.ts index f6fd87df22ff..81916d4392cc 100644 --- a/src/cdk/tree/tree.ts +++ b/src/cdk/tree/tree.ts @@ -281,6 +281,8 @@ export class CdkTree this._dataSubscription.unsubscribe(); this._dataSubscription = null; } + + this._keyManager.destroy(); } ngOnInit() { 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 e530167b68ec..f3da6fbbd01d 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 @@ -151,6 +151,10 @@ export class VimTreeKeyManager implements TreeKeyM } } + destroy() { + this.change.complete(); + } + /** Stream that emits any time the focused item changes. */ readonly change = new Subject(); diff --git a/tools/public_api_guard/cdk/a11y.md b/tools/public_api_guard/cdk/a11y.md index d8ade23c6940..81b7568e1456 100644 --- a/tools/public_api_guard/cdk/a11y.md +++ b/tools/public_api_guard/cdk/a11y.md @@ -431,6 +431,8 @@ export class NoopTreeKeyManager implements TreeKey // (undocumented) readonly change: Subject; // (undocumented) + destroy(): void; + // (undocumented) focusItem(): void; // (undocumented) getActiveItem(): null; @@ -467,6 +469,7 @@ export const TREE_KEY_MANAGER_FACTORY_PROVIDER: { export class TreeKeyManager implements TreeKeyManagerStrategy { constructor(items: Observable | QueryList | T[], config: TreeKeyManagerOptions); readonly change: Subject; + destroy(): void; focusItem(index: number, options?: { emitChangeEvent?: boolean; }): void; @@ -512,6 +515,7 @@ export interface TreeKeyManagerOptions { // @public (undocumented) export interface TreeKeyManagerStrategy { readonly change: Subject; + destroy(): void; focusItem(index: number, options?: { emitChangeEvent?: boolean; }): void;