UNPKG

@angular/cdk

Version:

Angular Material Component Development Kit

361 lines (358 loc) 14.4 kB
import { signal, QueryList, isSignal, effect } from '@angular/core'; import { Subscription, Subject } from 'rxjs'; import { T as Typeahead } from './typeahead-9ZW4Dtsf.mjs'; import { hasModifierKey } from './keycodes.mjs'; import { P as PAGE_DOWN, a as PAGE_UP, E as END, H as HOME, L as LEFT_ARROW, R as RIGHT_ARROW, U as UP_ARROW, D as DOWN_ARROW, T as TAB } from './keycodes-CpHkExLC.mjs'; /** * This class manages keyboard events for selectable lists. If you pass it a query list * of items, it will set the active item correctly when arrow events occur. */ class ListKeyManager { _items; _activeItemIndex = -1; _activeItem = signal(null); _wrap = false; _typeaheadSubscription = Subscription.EMPTY; _itemChangesSubscription; _vertical = true; _horizontal; _allowedModifierKeys = []; _homeAndEnd = false; _pageUpAndDown = { enabled: false, delta: 10 }; _effectRef; _typeahead; /** * Predicate function that can be used to check whether an item should be skipped * by the key manager. By default, disabled items are skipped. */ _skipPredicateFn = (item) => item.disabled; constructor(_items, injector) { this._items = _items; // 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) => this._itemsChanged(newItems.toArray())); } else if (isSignal(_items)) { if (!injector && (typeof ngDevMode === 'undefined' || ngDevMode)) { throw new Error('ListKeyManager constructed with a signal must receive an injector'); } this._effectRef = effect(() => this._itemsChanged(_items()), { injector }); } } /** * Stream that emits any time the TAB key is pressed, so components can react * when focus is shifted off of the list. */ tabOut = new Subject(); /** Stream that emits whenever the active item of the list manager changes. */ change = new Subject(); /** * Sets the predicate function that determines which items should be skipped by the * list key manager. * @param predicate Function that determines whether the given item should be skipped. */ skipPredicate(predicate) { this._skipPredicateFn = predicate; return this; } /** * Configures wrapping mode, which determines whether the active item will wrap to * the other end of list when there are no more items in the given direction. * @param shouldWrap Whether the list should wrap when reaching the end. */ withWrap(shouldWrap = true) { this._wrap = shouldWrap; return this; } /** * Configures whether the key manager should be able to move the selection vertically. * @param enabled Whether vertical selection should be enabled. */ withVerticalOrientation(enabled = true) { this._vertical = enabled; return this; } /** * Configures the key manager to move the selection horizontally. * Passing in `null` will disable horizontal movement. * @param direction Direction in which the selection can be moved. */ withHorizontalOrientation(direction) { this._horizontal = direction; return this; } /** * Modifier keys which are allowed to be held down and whose default actions will be prevented * as the user is pressing the arrow keys. Defaults to not allowing any modifier keys. */ withAllowedModifierKeys(keys) { this._allowedModifierKeys = keys; return this; } /** * Turns on typeahead mode which allows users to set the active item by typing. * @param debounceInterval Time to wait after the last keystroke before setting the active item. */ withTypeAhead(debounceInterval = 200) { if (typeof ngDevMode === 'undefined' || ngDevMode) { const items = this._getItemsArray(); if (items.length > 0 && items.some(item => typeof item.getLabel !== 'function')) { throw Error('ListKeyManager items in typeahead mode must implement the `getLabel` method.'); } } this._typeaheadSubscription.unsubscribe(); const items = this._getItemsArray(); this._typeahead = new Typeahead(items, { debounceInterval: typeof debounceInterval === 'number' ? debounceInterval : undefined, skipPredicate: item => this._skipPredicateFn(item), }); this._typeaheadSubscription = this._typeahead.selectedItem.subscribe(item => { this.setActiveItem(item); }); return this; } /** Cancels the current typeahead sequence. */ cancelTypeahead() { this._typeahead?.reset(); return this; } /** * Configures the key manager to activate the first and last items * respectively when the Home or End key is pressed. * @param enabled Whether pressing the Home or End key activates the first/last item. */ withHomeAndEnd(enabled = true) { this._homeAndEnd = enabled; return this; } /** * Configures the key manager to activate every 10th, configured or first/last element in up/down direction * respectively when the Page-Up or Page-Down key is pressed. * @param enabled Whether pressing the Page-Up or Page-Down key activates the first/last item. * @param delta Whether pressing the Home or End key activates the first/last item. */ withPageUpDown(enabled = true, delta = 10) { this._pageUpAndDown = { enabled, delta }; return this; } setActiveItem(item) { const previousActiveItem = this._activeItem(); this.updateActiveItem(item); if (this._activeItem() !== previousActiveItem) { this.change.next(this._activeItemIndex); } } /** * Sets the active item depending on the key event passed in. * @param event Keyboard event to be used for determining which element should be active. */ onKeydown(event) { const keyCode = event.keyCode; const modifiers = ['altKey', 'ctrlKey', 'metaKey', 'shiftKey']; const isModifierAllowed = modifiers.every(modifier => { return !event[modifier] || this._allowedModifierKeys.indexOf(modifier) > -1; }); switch (keyCode) { case TAB: this.tabOut.next(); return; case DOWN_ARROW: if (this._vertical && isModifierAllowed) { this.setNextItemActive(); break; } else { return; } case UP_ARROW: if (this._vertical && isModifierAllowed) { this.setPreviousItemActive(); break; } else { return; } case RIGHT_ARROW: if (this._horizontal && isModifierAllowed) { this._horizontal === 'rtl' ? this.setPreviousItemActive() : this.setNextItemActive(); break; } else { return; } case LEFT_ARROW: if (this._horizontal && isModifierAllowed) { this._horizontal === 'rtl' ? this.setNextItemActive() : this.setPreviousItemActive(); break; } else { return; } case HOME: if (this._homeAndEnd && isModifierAllowed) { this.setFirstItemActive(); break; } else { return; } case END: if (this._homeAndEnd && isModifierAllowed) { this.setLastItemActive(); break; } else { return; } case PAGE_UP: if (this._pageUpAndDown.enabled && isModifierAllowed) { const targetIndex = this._activeItemIndex - this._pageUpAndDown.delta; this._setActiveItemByIndex(targetIndex > 0 ? targetIndex : 0, 1); break; } else { return; } case PAGE_DOWN: if (this._pageUpAndDown.enabled && isModifierAllowed) { const targetIndex = this._activeItemIndex + this._pageUpAndDown.delta; const itemsLength = this._getItemsArray().length; this._setActiveItemByIndex(targetIndex < itemsLength ? targetIndex : itemsLength - 1, -1); break; } else { return; } default: if (isModifierAllowed || hasModifierKey(event, 'shiftKey')) { this._typeahead?.handleKey(event); } // Note that we return here, in order to avoid preventing // the default action of non-navigational keys. return; } this._typeahead?.reset(); event.preventDefault(); } /** Index of the currently active item. */ get activeItemIndex() { return this._activeItemIndex; } /** The active item. */ get activeItem() { return this._activeItem(); } /** Gets whether the user is currently typing into the manager using the typeahead feature. */ isTyping() { return !!this._typeahead && this._typeahead.isTyping(); } /** Sets the active item to the first enabled item in the list. */ setFirstItemActive() { this._setActiveItemByIndex(0, 1); } /** Sets the active item to the last enabled item in the list. */ setLastItemActive() { this._setActiveItemByIndex(this._getItemsArray().length - 1, -1); } /** Sets the active item to the next enabled item in the list. */ setNextItemActive() { this._activeItemIndex < 0 ? this.setFirstItemActive() : this._setActiveItemByDelta(1); } /** Sets the active item to a previous enabled item in the list. */ setPreviousItemActive() { this._activeItemIndex < 0 && this._wrap ? this.setLastItemActive() : this._setActiveItemByDelta(-1); } updateActiveItem(item) { const itemArray = this._getItemsArray(); const index = typeof item === 'number' ? item : itemArray.indexOf(item); const activeItem = itemArray[index]; // Explicitly check for `null` and `undefined` because other falsy values are valid. this._activeItem.set(activeItem == null ? null : activeItem); this._activeItemIndex = index; this._typeahead?.setCurrentSelectedItemIndex(index); } /** Cleans up the key manager. */ destroy() { this._typeaheadSubscription.unsubscribe(); this._itemChangesSubscription?.unsubscribe(); this._effectRef?.destroy(); this._typeahead?.destroy(); this.tabOut.complete(); this.change.complete(); } /** * This method sets the active item, given a list of items and the delta between the * currently active item and the new active item. It will calculate differently * depending on whether wrap mode is turned on. */ _setActiveItemByDelta(delta) { this._wrap ? this._setActiveInWrapMode(delta) : this._setActiveInDefaultMode(delta); } /** * Sets the active item properly given "wrap" mode. In other words, it will continue to move * down the list until it finds an item that is not disabled, and it will wrap if it * encounters either end of the list. */ _setActiveInWrapMode(delta) { const items = this._getItemsArray(); for (let i = 1; i <= items.length; i++) { const index = (this._activeItemIndex + delta * i + items.length) % items.length; const item = items[index]; if (!this._skipPredicateFn(item)) { this.setActiveItem(index); return; } } } /** * Sets the active item properly given the default mode. In other words, it will * continue to move down the list until it finds an item that is not disabled. If * it encounters either end of the list, it will stop and not wrap. */ _setActiveInDefaultMode(delta) { this._setActiveItemByIndex(this._activeItemIndex + delta, delta); } /** * Sets the active item to the first enabled item starting at the index specified. If the * item is disabled, it will move in the fallbackDelta direction until it either * finds an enabled item or encounters the end of the list. */ _setActiveItemByIndex(index, fallbackDelta) { const items = this._getItemsArray(); if (!items[index]) { return; } while (this._skipPredicateFn(items[index])) { index += fallbackDelta; if (!items[index]) { return; } } this.setActiveItem(index); } /** Returns the items as an array. */ _getItemsArray() { if (isSignal(this._items)) { return this._items(); } return this._items instanceof QueryList ? this._items.toArray() : this._items; } /** Callback for when the items have changed. */ _itemsChanged(newItems) { this._typeahead?.setItems(newItems); const activeItem = this._activeItem(); if (activeItem) { const newIndex = newItems.indexOf(activeItem); if (newIndex > -1 && newIndex !== this._activeItemIndex) { this._activeItemIndex = newIndex; this._typeahead?.setCurrentSelectedItemIndex(newIndex); } } } } export { ListKeyManager as L }; //# sourceMappingURL=list-key-manager-CyOIXo8P.mjs.map