UNPKG

@progress/kendo-angular-listbox

Version:
289 lines (288 loc) 15 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 } 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(); constructor(renderer, zone) { this.renderer = renderer; this.zone = zone; } onKeyDown(event, toolsRef, toolbar, childListbox, parentListbox, listboxItems) { const target = event.target; const keyCode = event.keyCode; 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); } } 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); } 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) { if (this.selectedListboxItemIndex !== this.focusedListboxItemIndex) { 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(); const previousItem = listboxItems[this.selectedListboxItemIndex]?.nativeElement; const currentItem = listboxItems[this.focusedListboxItemIndex]?.nativeElement; this.changeTabindex(previousItem, currentItem); this.onSelectionChange.emit({ index: this.focusedListboxItemIndex, prevIndex: this.selectedListboxItemIndex }); 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; } dir === 'moveUp' ? this.onArrowUp(listboxItems) : this.onArrowDown(listboxItems); this.onSelectionChange.emit({ index: this.selectedListboxItemIndex, prevIndex: this.focusedListboxItemIndex }); this.focusedListboxItemIndex = this.selectedListboxItemIndex; } onArrowLeftOrRight(keyCode, parentListbox, childListbox, event, listboxItems) { event.preventDefault(); if (event.shiftKey) { this.transferAllItems(keyCode, childListbox, parentListbox); return; } if (this.selectedListboxItemIndex >= 0) { 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; prevIndex = this.selectedListboxItemIndex; this.selectedListboxItemIndex = this.focusedListboxItemIndex; } this.changeTabindex(previousItem, currentItem, !!currentItem); this.onSelectionChange.emit({ index: this.selectedListboxItemIndex, prevIndex }); } 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) { listboxItems[this.focusedListboxItemIndex].nativeElement.blur(); if (this.focusedListboxItemIndex > 0 && dir === 'moveUp') { this.focusedListboxItemIndex -= 1; } else if (this.focusedListboxItemIndex < listboxItems.length - 1 && dir === 'moveDown') { this.focusedListboxItemIndex += 1; } listboxItems[this.focusedListboxItemIndex].nativeElement.focus(); } onArrowDown(listboxItems) { if (this.selectedListboxItemIndex < listboxItems.length - 1) { const offset = this.selectedListboxItemIndex ? this.selectedListboxItemIndex : this.focusedListboxItemIndex; this.selectedListboxItemIndex = offset + 1; const previousItem = listboxItems[this.selectedListboxItemIndex - 1]?.nativeElement; const currentItem = listboxItems[this.selectedListboxItemIndex]?.nativeElement; this.changeTabindex(previousItem, currentItem); } } onArrowUp(listboxItems) { if (this.selectedListboxItemIndex > 0 || this.focusedListboxItemIndex > 0) { const offset = this.selectedListboxItemIndex ? this.selectedListboxItemIndex : this.focusedListboxItemIndex; this.selectedListboxItemIndex = offset - 1; const previousItem = listboxItems[this.selectedListboxItemIndex + 1]?.nativeElement; const currentItem = listboxItems[this.selectedListboxItemIndex]?.nativeElement; this.changeTabindex(previousItem, currentItem); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: KeyboardNavigationService, deps: [{ token: i0.Renderer2 }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: KeyboardNavigationService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: KeyboardNavigationService, decorators: [{ type: Injectable }], ctorParameters: function () { return [{ type: i0.Renderer2 }, { type: i0.NgZone }]; } });