@progress/kendo-angular-listbox
Version:
Kendo UI for Angular ListBox
390 lines (389 loc) • 19.2 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, 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 }] });