UNPKG

@progress/kendo-angular-listbox

Version:
390 lines (389 loc) 19.2 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ /* eslint-disable @typescript-eslint/no-inferrable-types */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { EventEmitter, Injectable, NgZone, Renderer2 } from "@angular/core"; import { Keys, normalizeKeys } from "@progress/kendo-angular-common"; import { take } from "rxjs/operators"; import * as i0 from "@angular/core"; /** * @hidden */ export class KeyboardNavigationService { renderer; zone; selectedListboxItemIndex = 0; focusedListboxItemIndex = 0; focusedToolIndex = 0; onDeleteEvent = new EventEmitter(); onMoveSelectedItem = new EventEmitter(); onTransferAllEvent = new EventEmitter(); onShiftSelectedItem = new EventEmitter(); onSelectionChange = new EventEmitter(); onSelectAll = new EventEmitter(); onSelectToEnd = new EventEmitter(); constructor(renderer, zone) { this.renderer = renderer; this.zone = zone; } onKeyDown(event, toolsRef, toolbar, childListbox, parentListbox, listboxItems, currentListbox) { const target = event.target; const keyCode = normalizeKeys(event); const ctrlOrMetaKey = event.ctrlKey || event.metaKey; const parentListboxToolbar = parentListbox?.selectedTools; const tool = toolsRef.find(elem => elem.element === target); const activeToolbar = toolbar.length > 0 ? toolbar : parentListboxToolbar; if (toolsRef.length > 0 || parentListbox?.tools.toArray().length > 0) { const focusNextTool = (keyCode === Keys.ArrowDown || keyCode === Keys.ArrowRight); const focusPrevTool = (keyCode === Keys.ArrowUp || keyCode === Keys.ArrowLeft); if ((focusNextTool || focusPrevTool) && tool) { const dir = focusPrevTool ? 'up' : 'down'; this.handleToolbarArrows(toolsRef, dir); } else if (keyCode === Keys.F10) { event.preventDefault(); this.onF10Key(toolsRef); } else if (keyCode === Keys.Delete && activeToolbar.some(tool => tool.name === 'remove')) { this.onDeleteEvent.emit(this.selectedListboxItemIndex); } } if (ctrlOrMetaKey && (event.key === 'a' || event.key === 'A')) { event.preventDefault(); this.onSelectAll.emit(); return; } if (ctrlOrMetaKey && event.shiftKey && (keyCode === Keys.Home || keyCode === Keys.End)) { event.preventDefault(); const direction = keyCode === Keys.Home ? 'home' : 'end'; this.onSelectToEnd.emit({ direction }); return; } const isTargetListboxItem = listboxItems.find(elem => elem.nativeElement === target); if (isTargetListboxItem) { let isTransferToolVisible; if (activeToolbar) { isTransferToolVisible = activeToolbar.some(tool => tool.name.startsWith('transfer')); } if ((keyCode === Keys.ArrowRight || keyCode === Keys.ArrowLeft) && ctrlOrMetaKey && isTransferToolVisible) { this.onArrowLeftOrRight(keyCode, parentListbox, childListbox, event, listboxItems, currentListbox); } else if ((keyCode === Keys.ArrowUp || keyCode === Keys.ArrowDown)) { this.onArrowUpOrDown(keyCode, ctrlOrMetaKey, event, activeToolbar, listboxItems); } else if ((event.metaKey && keyCode === Keys.Enter) || (event.ctrlKey && keyCode === Keys.Space)) { this.onSelectChange(event, listboxItems); } else if (keyCode === Keys.Space) { this.onSpaceKey(event, listboxItems); } } } changeTabindex(previousItem, currentItem, shouldBlur = true) { if (previousItem) { this.renderer.setAttribute(previousItem, 'tabindex', '-1'); if (shouldBlur) { previousItem.blur(); } } if (currentItem) { this.renderer.setAttribute(currentItem, 'tabindex', '0'); currentItem.focus(); } } handleToolbarArrows(toolsRef, dir) { const topReached = dir === 'up' && this.focusedToolIndex <= 0; const bottomReached = dir === 'down' && this.focusedToolIndex >= toolsRef.length - 1; if (topReached || bottomReached) { return; } const offset = dir === 'up' ? -1 : 1; this.focusedToolIndex += offset; const prevItem = toolsRef[this.focusedToolIndex + (offset * -1)]?.element; const currentItem = toolsRef[this.focusedToolIndex]?.element; this.changeTabindex(prevItem, currentItem); } onSpaceKey(event, listboxItems) { event.stopImmediatePropagation(); event.preventDefault(); event.stopPropagation(); const ctrlKey = event.ctrlKey || event.metaKey; const shiftKey = event.shiftKey; if (this.selectedListboxItemIndex !== this.focusedListboxItemIndex) { const previousItem = listboxItems[this.selectedListboxItemIndex]?.nativeElement; const currentItem = listboxItems[this.focusedListboxItemIndex]?.nativeElement; if (this.isItemDisabled(currentItem)) { return; } this.changeTabindex(previousItem, currentItem); } this.onSelectionChange.emit({ index: this.focusedListboxItemIndex, prevIndex: this.selectedListboxItemIndex, ctrlKey, shiftKey }); this.selectedListboxItemIndex = this.focusedListboxItemIndex; } onArrowUpOrDown(keyCode, ctrlOrMetaKey, event, activeToolbar, listboxItems) { event.preventDefault(); const dir = keyCode === Keys.ArrowUp ? 'moveUp' : 'moveDown'; if (ctrlOrMetaKey) { let isMoveToolVisible; if (activeToolbar) { isMoveToolVisible = activeToolbar.some(tool => tool.name.startsWith('move')); } if (event.shiftKey && isMoveToolVisible) { this.onMoveSelectedItem.emit(dir); return; } this.changeFocusedItem(dir, listboxItems); return; } if (event.shiftKey) { this.onShiftArrow(dir, listboxItems); return; } dir === 'moveUp' ? this.onArrowUp(listboxItems) : this.onArrowDown(listboxItems); if (this.selectedListboxItemIndex !== this.focusedListboxItemIndex) { this.onSelectionChange.emit({ index: this.selectedListboxItemIndex, prevIndex: this.focusedListboxItemIndex }); this.focusedListboxItemIndex = this.selectedListboxItemIndex; } } onArrowLeftOrRight(keyCode, parentListbox, childListbox, event, listboxItems, currentListbox) { event.preventDefault(); if (event.shiftKey) { this.transferAllItems(keyCode, childListbox, parentListbox); return; } const isArrowRight = keyCode === Keys.ArrowRight; const sourceListbox = isArrowRight ? currentListbox : childListbox || parentListbox?.childListbox; const hasSelection = sourceListbox?.selectedIndices && sourceListbox.selectedIndices.length > 0; if (hasSelection) { this.transferItem(keyCode, childListbox, parentListbox, listboxItems); } } onSelectChange(event, listboxItems) { event.stopImmediatePropagation(); event.preventDefault(); const areIndexesEqual = this.selectedListboxItemIndex === this.focusedListboxItemIndex; const canDeselect = (this.selectedListboxItemIndex || this.selectedListboxItemIndex === 0) && areIndexesEqual; let previousItem; let currentItem; let prevIndex; if (canDeselect) { previousItem = listboxItems[this.selectedListboxItemIndex]?.nativeElement; this.selectedListboxItemIndex = null; } else { previousItem = listboxItems[this.selectedListboxItemIndex]?.nativeElement; currentItem = listboxItems[this.focusedListboxItemIndex]?.nativeElement; if (this.isItemDisabled(currentItem)) { return; } prevIndex = this.selectedListboxItemIndex; this.selectedListboxItemIndex = this.focusedListboxItemIndex; } this.changeTabindex(previousItem, currentItem, !!currentItem); const ctrlKey = event.ctrlKey || event.metaKey; this.onSelectionChange.emit({ index: this.selectedListboxItemIndex, prevIndex, ctrlKey }); } onF10Key(tools) { if (this.focusedToolIndex && this.focusedToolIndex > -1) { if (this.focusedToolIndex >= tools.length) { tools[tools.length - 1].element.focus(); } else { tools[this.focusedToolIndex].element.focus(); } } else { tools[0]?.element.focus(); } } transferAllItems(keyCode, childListbox, parentListbox) { const isArrowRight = keyCode === Keys.ArrowRight; const actionToPerform = isArrowRight ? 'transferAllTo' : 'transferAllFrom'; this.onTransferAllEvent.emit(actionToPerform); const adjustTabindex = (items) => { items.forEach(item => { if (item.nativeElement.getAttribute('tabindex') === '0') { this.changeTabindex(item.nativeElement, null); } }); }; this.zone.onStable.pipe(take(1)).subscribe(() => { const childListboxNav = childListbox?.keyboardNavigationService || parentListbox?.childListbox.keyboardNavigationService; let currentItem; if (isArrowRight) { if (childListbox) { const childListBoxItems = childListbox.listboxItems.toArray(); const childListboxItemsLength = childListBoxItems.length - 1; currentItem = childListBoxItems[childListboxItemsLength].nativeElement; childListboxNav.focusedListboxItemIndex = childListboxItemsLength; childListboxNav.selectedListboxItemIndex = childListboxItemsLength; this.focusedListboxItemIndex = 0; this.selectedListboxItemIndex = 0; adjustTabindex(childListBoxItems); } } else { if (parentListbox) { const parentListboxNav = parentListbox.keyboardNavigationService; const parentListBoxItems = parentListbox.listboxItems.toArray(); const parentListboxItemsLength = parentListBoxItems.length - 1; currentItem = parentListBoxItems[parentListboxItemsLength].nativeElement; parentListboxNav.focusedListboxItemIndex = parentListboxItemsLength; parentListboxNav.selectedListboxItemIndex = parentListboxItemsLength; childListboxNav.focusedListboxItemIndex = 0; childListboxNav.selectedListboxItemIndex = 0; adjustTabindex(parentListBoxItems); } } this.changeTabindex(null, currentItem); }); } transferItem(keyCode, childListbox, parentListbox, listboxItems) { const isArrowRight = keyCode === Keys.ArrowRight; const actionToPerform = isArrowRight ? 'transferTo' : 'transferFrom'; this.onShiftSelectedItem.emit(actionToPerform); const adjustTabindex = (items, firstItem, currentItem) => { items.forEach(item => { if (item.nativeElement.getAttribute('tabindex') === '0') { this.changeTabindex(item.nativeElement, firstItem); } }); this.changeTabindex(null, currentItem); }; this.zone.onStable.pipe(take(1)).subscribe(() => { if (isArrowRight) { if (childListbox) { const childListBoxItems = childListbox.listboxItems.toArray(); const childListboxNav = childListbox.keyboardNavigationService; const childListboxItemsLength = childListbox.listboxItems.length - 1; const parentListboxFirstItem = listboxItems[0].nativeElement; const currentItem = childListBoxItems[childListboxItemsLength].nativeElement; childListboxNav.focusedListboxItemIndex = childListboxItemsLength; childListboxNav.selectedListboxItemIndex = childListboxItemsLength; this.focusedListboxItemIndex = 0; this.selectedListboxItemIndex = 0; adjustTabindex(childListBoxItems, parentListboxFirstItem, currentItem); } } else { if (parentListbox) { const parentListBoxItems = parentListbox.listboxItems.toArray(); const childListboxNav = parentListbox.childListbox.keyboardNavigationService; const parentListboxNav = parentListbox.keyboardNavigationService; const parentListboxItemsLength = parentListbox.listboxItems.length - 1; const childListboxFirstItem = listboxItems[0].nativeElement; const currentItem = parentListBoxItems[parentListboxItemsLength].nativeElement; parentListboxNav.focusedListboxItemIndex = parentListboxItemsLength; parentListboxNav.selectedListboxItemIndex = parentListboxItemsLength; childListboxNav.focusedListboxItemIndex = 0; childListboxNav.selectedListboxItemIndex = 0; adjustTabindex(parentListBoxItems, childListboxFirstItem, currentItem); } } }); } changeFocusedItem(dir, listboxItems) { const previousIndex = this.focusedListboxItemIndex; const previousItem = listboxItems[previousIndex].nativeElement; if (this.focusedListboxItemIndex > 0 && dir === 'moveUp') { this.focusedListboxItemIndex -= 1; } else if (this.focusedListboxItemIndex < listboxItems.length - 1 && dir === 'moveDown') { this.focusedListboxItemIndex += 1; } const currentItem = listboxItems[this.focusedListboxItemIndex].nativeElement; this.changeTabindex(previousItem, currentItem); } onShiftArrow(dir, listboxItems) { const previousFocusIndex = this.focusedListboxItemIndex; if (dir === 'moveUp' && this.focusedListboxItemIndex > 0) { this.focusedListboxItemIndex -= 1; } else if (dir === 'moveDown' && this.focusedListboxItemIndex < listboxItems.length - 1) { this.focusedListboxItemIndex += 1; } if (previousFocusIndex !== this.focusedListboxItemIndex) { const previousItem = listboxItems[previousFocusIndex]?.nativeElement; let currentItem = listboxItems[this.focusedListboxItemIndex]?.nativeElement; if (this.isItemDisabled(currentItem)) { const step = dir === 'moveDown' ? 1 : -1; const nextEnabledIndex = this.findNextEnabledIndex(this.focusedListboxItemIndex, listboxItems, step); if (nextEnabledIndex === -1) { this.focusedListboxItemIndex = previousFocusIndex; return; } this.focusedListboxItemIndex = nextEnabledIndex; currentItem = listboxItems[this.focusedListboxItemIndex]?.nativeElement; } this.changeTabindex(previousItem, currentItem); this.onSelectionChange.emit({ index: this.focusedListboxItemIndex, prevIndex: this.selectedListboxItemIndex, shiftKey: true }); this.selectedListboxItemIndex = this.focusedListboxItemIndex; } } onArrowDown(listboxItems) { if (this.focusedListboxItemIndex < listboxItems.length - 1) { this.selectedListboxItemIndex = this.focusedListboxItemIndex + 1; const previousItem = listboxItems[this.focusedListboxItemIndex]?.nativeElement; let currentItem = listboxItems[this.selectedListboxItemIndex]?.nativeElement; if (this.isItemDisabled(currentItem)) { currentItem = this.calculateNextActiveItem(listboxItems, 1); if (!currentItem) { return; } } this.changeTabindex(previousItem, currentItem); } } onArrowUp(listboxItems) { if (this.focusedListboxItemIndex > 0) { this.selectedListboxItemIndex = this.focusedListboxItemIndex - 1; const previousItem = listboxItems[this.focusedListboxItemIndex]?.nativeElement; let currentItem = listboxItems[this.selectedListboxItemIndex]?.nativeElement; if (this.isItemDisabled(currentItem)) { currentItem = this.calculateNextActiveItem(listboxItems, -1); if (!currentItem) { return; } } this.changeTabindex(previousItem, currentItem); } } isItemDisabled(item) { return item.getAttribute('aria-disabled') === 'true'; } findNextEnabledIndex(startIndex, listboxItems, step) { let index = startIndex; while (index >= 0 && index < listboxItems.length) { const item = listboxItems[index]?.nativeElement; if (!this.isItemDisabled(item)) { return index; } index += step; } return -1; } calculateNextActiveItem(listboxItems, step) { this.selectedListboxItemIndex = this.findNextEnabledIndex(this.selectedListboxItemIndex, listboxItems, step); if (this.selectedListboxItemIndex === -1) { this.selectedListboxItemIndex = this.focusedListboxItemIndex; return null; } return listboxItems[this.selectedListboxItemIndex]?.nativeElement; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: KeyboardNavigationService, deps: [{ token: i0.Renderer2 }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: KeyboardNavigationService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: KeyboardNavigationService, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: i0.Renderer2 }, { type: i0.NgZone }] });