@progress/kendo-angular-layout
Version:
Kendo UI for Angular Layout Package - a collection of components to create professional application layoyts
190 lines (189 loc) • 9.02 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 no-unused-expressions */
import { Injectable, NgZone, Renderer2 } from '@angular/core';
import { Keys } from '@progress/kendo-angular-common';
import { LocalizationService } from '@progress/kendo-angular-l10n';
import { BehaviorSubject } from 'rxjs';
import { focusableSelector } from '@progress/kendo-angular-common';
import { getCurrentCol, shouldReorder, shouldResize } from './util';
import { TileLayoutResizeEvent } from './resize-event';
import { TileLayoutReorderEvent } from './reorder-event';
import * as i0 from "@angular/core";
import * as i1 from "@progress/kendo-angular-l10n";
/**
* @hidden
*/
export class TileLayoutKeyboardNavigationService {
zone;
renderer;
localization;
navigable = new BehaviorSubject(false);
owner;
mousedown;
localizationSubscription;
rtl;
lastFocused;
constructor(zone, renderer, localization) {
this.zone = zone;
this.renderer = renderer;
this.localization = localization;
this.localizationSubscription = this.localization.changes.subscribe(({ rtl }) => this.rtl = rtl);
}
ngOnDestroy() {
this.localizationSubscription.unsubscribe();
}
onKeyDown(event, elem, focusableItems, settings) {
const keyCode = event.keyCode;
const isTileFocused = document.activeElement === elem;
const focusedTile = settings.items.find(item => item.elem.nativeElement === elem);
const col = getCurrentCol(focusedTile, settings, this.rtl);
const isArrow = [Keys.ArrowLeft, Keys.ArrowRight, Keys.ArrowDown, Keys.ArrowUp].some(key => key === event.keyCode);
this.lastFocused = focusedTile;
if (keyCode === Keys.Enter && isTileFocused && focusableItems.length > 0) {
this.changeTabIndex('0', elem, focusableItems);
focusableItems[0].focus();
}
else if (keyCode === Keys.Escape) {
this.changeTabIndex('-1', elem, focusableItems);
elem.focus();
}
else if (isArrow && (event.ctrlKey || event.metaKey) && isTileFocused && focusedTile.isResizable) {
event.preventDefault();
this.zone.run(() => {
this.resizeItem(keyCode, focusedTile, settings, col);
});
}
else if (isArrow && event.shiftKey && isTileFocused && focusedTile.isReorderable) {
this.zone.run(() => {
this.reorderItem(keyCode, focusedTile, settings, col);
});
}
else if (keyCode === Keys.Tab) {
if (!isTileFocused) {
this.keepFocusWithinComponent(event, elem);
}
else {
const dir = event.shiftKey ? -1 : 1;
const nextFocusableTileOrder = focusedTile.order + dir;
if (nextFocusableTileOrder < 0 || nextFocusableTileOrder >= settings.items.length) {
const first = settings.items[0];
const last = settings.items[settings.items.length - 1];
if (dir > 0) {
last.focus();
}
else {
first.focus();
}
return;
}
event.preventDefault();
this.lastFocused = settings.items.find(item => item.order === nextFocusableTileOrder);
this.lastFocused?.focus();
}
}
}
onFocusOut(event, elem, focusableItems) {
const isTargetFocusable = focusableItems.includes(event.target);
const isRelatedTargetFocusable = focusableItems.includes(event.relatedTarget);
if (isTargetFocusable && !isRelatedTargetFocusable) {
this.changeTabIndex('-1', elem, focusableItems);
event.relatedTarget?.focus();
}
}
onMousedown(event, elem, focusableItems, tile) {
this.mousedown = true;
const isTargetFocusable = focusableItems.includes(event.target);
this.lastFocused = tile;
if (isTargetFocusable) {
this.changeTabIndex('0', elem, focusableItems);
event.target.focus();
}
}
changeTabIndex(tabIndex, elem, focusableItems) {
this.renderer.setAttribute(elem, 'tabindex', tabIndex === '0' ? '-1' : '0');
focusableItems.forEach((focusItem) => {
this.renderer.setAttribute(focusItem, 'tabindex', tabIndex);
});
}
getAllFocusableChildren(parent) {
return Array.from(parent.querySelectorAll(focusableSelector)).filter((element) => element.offsetParent !== null);
}
returnFocus() {
this.lastFocused ? this.lastFocused.focus() : this.owner.items.find(item => item.order === 0).focus();
}
resizeItem(keyCode, focusedTile, settings, col) {
const { resizeRight, resizeLeft, resizeDown, resizeUp } = shouldResize(keyCode, col, focusedTile, settings);
const resizeHorizontal = resizeLeft || resizeRight;
const resizeVertical = resizeDown || resizeUp;
const resizeDir = resizeLeft || resizeUp ? -1 : 1;
if (!(resizeHorizontal || resizeVertical)) {
return;
}
const resizeEvent = new TileLayoutResizeEvent(focusedTile, this.owner.items ? this.owner.items.toArray() : [], focusedTile.rowSpan + resizeDir, focusedTile.rowSpan, focusedTile.colSpan + resizeDir, focusedTile.colSpan);
this.owner.resize.emit(resizeEvent);
if (!resizeEvent.isDefaultPrevented()) {
if (resizeHorizontal) {
focusedTile.colSpan += resizeDir;
}
else if (resizeVertical) {
focusedTile.rowSpan += resizeDir;
}
}
}
reorderItem(keyCode, focusedTile, settings, col) {
const { reorderLeft, reorderRight } = shouldReorder(keyCode, col, focusedTile, settings);
if (!(reorderLeft || reorderRight)) {
return;
}
const reorder = (dir) => {
const relatedTile = this.targetTile(focusedTile, settings.items, dir);
if (relatedTile) {
relatedTile.order -= dir;
if (relatedTile.col) {
relatedTile.col -= dir;
}
focusedTile.order += dir;
if (focusedTile.col) {
focusedTile.col += dir;
}
}
};
const reorderDir = reorderRight ? 1 : -1;
const reorderEvent = new TileLayoutReorderEvent(focusedTile, this.owner.items ? this.owner.items.toArray() : [], focusedTile.order + reorderDir, focusedTile.order, focusedTile.col ? focusedTile.col + reorderDir : undefined, focusedTile.col, focusedTile.row, focusedTile.row);
this.owner.reorder.next(reorderEvent);
if (!reorderEvent.isDefaultPrevented()) {
reorder(reorderDir);
}
}
keepFocusWithinComponent(event, wrapper) {
const [firstFocusable, lastFocusable] = this.getFirstAndLastFocusable(wrapper);
const tabAfterLastFocusable = !event.shiftKey && event.target === lastFocusable;
const shiftTabAfterFirstFocusable = event.shiftKey && event.target === firstFocusable;
if (tabAfterLastFocusable) {
event.preventDefault();
firstFocusable.focus();
wrapper.blur();
}
if (shiftTabAfterFirstFocusable) {
event.preventDefault();
lastFocusable.focus();
}
}
getFirstAndLastFocusable(parent) {
const all = this.getAllFocusableChildren(parent);
const firstFocusable = all.length > 0 ? all[0] : parent;
const lastFocusable = all.length > 0 ? all[all.length - 1] : parent;
return [firstFocusable, lastFocusable];
}
targetTile(focusedTile, items, offset) {
return items.find(item => item.order === focusedTile.order + offset);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TileLayoutKeyboardNavigationService, deps: [{ token: i0.NgZone }, { token: i0.Renderer2 }, { token: i1.LocalizationService }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TileLayoutKeyboardNavigationService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TileLayoutKeyboardNavigationService, decorators: [{
type: Injectable
}], ctorParameters: function () { return [{ type: i0.NgZone }, { type: i0.Renderer2 }, { type: i1.LocalizationService }]; } });