@progress/kendo-angular-listbox
Version:
Kendo UI for Angular ListBox
289 lines (288 loc) • 15 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* 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 }]; } });