UNPKG

@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
/**----------------------------------------------------------------------------------------- * 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 }]; } });