UNPKG

@progress/kendo-angular-common

Version:

Kendo UI for Angular - Utility Package

230 lines (229 loc) 10.9 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { Directive, ElementRef, EventEmitter, Input, NgZone, Renderer2, isDevMode } from "@angular/core"; import { isDocumentAvailable } from "../utils"; import { MultiTabStop } from "./toggle-button-tab-stop"; import { Subscription } from "rxjs"; import { take } from "rxjs/operators"; import { Keys } from "../enums"; import * as i0 from "@angular/core"; import * as i1 from "./toggle-button-tab-stop"; const tags = ['kendo-splitbutton', 'kendo-combobox', 'kendo-multicolumncombobox', 'kendo-datepicker', 'kendo-timepicker', 'kendo-datetimepicker']; /** * Includes the button that toggles the Popup in the tab sequence when applied * to a SplitButton, ComboBox, MultiComboBox, DatePicker, TimePicker, and DateTimePicker component. * ```ts-no-run * _@Component({ * selector: 'my-app', * template: ` * <kendo-combobox [data]="data" * kendoToggleButtonTabStop> * </kendo-combobox> * * <kendo-datepicker [(ngModel)]="value" * kendoToggleButtonTabStop> * </kendo-datepicker> * ` * }) * class AppComponent {} * ``` */ export class ToggleButtonTabStopDirective { hostEl; renderer; zone; hostComponent; /** * @hidden * * Allows setting the interactive state of the toggle button. * * @default true */ active; /** * Defines the value of the `aria-label` attribute of the toggle button when active. * * @default "toggle popup" */ toggleButtonAriaLabel = 'toggle popup'; button; sub = new Subscription(); focusButton; isSplitButton; observer; /** * @hidden */ constructor(hostEl, renderer, zone, hostComponent) { this.hostEl = hostEl; this.renderer = renderer; this.zone = zone; this.hostComponent = hostComponent; if (isDevMode() && tags.indexOf(hostEl.nativeElement.tagName.toLowerCase()) === -1) { console.warn(`The kendoToggleButtonTabStop directive can be applied to the following components only: ${tags}`); } } ngOnInit() { this.active = true; } ngAfterViewInit() { if (!isDocumentAvailable()) { return; } this.isSplitButton = this.hostEl.nativeElement.classList.contains('k-split-button'); if (this.active) { this.activateButton(); } if (!(this.hostComponent?.escape instanceof EventEmitter)) { return; } this.sub = this.hostComponent?.escape.subscribe(() => { this.returnFocusToToggleButton(); }); // Returns the focus to the toggle button when component is opened through it, and the Popup is closed // while the active element is within the component or popup. this.sub.add(this.hostComponent.close.subscribe((e) => { if (!e.isDefaultPrevented() && this.focusButton) { this.zone.runOutsideAngular(() => { setTimeout(() => this.focusButton = false); }); const mainFocusableElement = this.hostEl.nativeElement.querySelector('.k-split-button > .k-button:first-child, .k-input-inner'); const optionsListContainer = document.getElementById(`${mainFocusableElement.getAttribute('aria-controls')}`); const inList = !!optionsListContainer && optionsListContainer.contains(document.activeElement); const inWrapper = this.hostEl.nativeElement.contains(document.activeElement); const focusInComponent = inList || inWrapper; if (focusInComponent) { this.returnFocusToToggleButton(); } } })); } ngOnChanges(changes) { if (!isDocumentAvailable()) { return; } if (changes['active']) { changes['active'].currentValue ? this.activateButton() : this.deactivateButton(); } if (changes['toggleButtonAriaLabel']) { this.button && this.renderer.setAttribute(this.button, 'aria-label', changes['toggleButtonAriaLabel'].currentValue); } } ngOnDestroy() { this.removeListeners(); this.sub.unsubscribe(); } activateButton() { const el = this.hostEl.nativeElement; const tabindex = el.querySelector('button:not([tabindex^="-"]), input:not([tabindex^="-"])')?.getAttribute('tabindex'); this.button = el.querySelector('.k-input-button, .k-split-button-arrow'); this.button && this.renderer.setAttribute(this.button, 'tabindex', tabindex); this.button && this.renderer.setAttribute(this.button, 'aria-label', this.toggleButtonAriaLabel); this.button && this.renderer.removeAttribute(this.button, 'aria-hidden'); if (!this.observer) { this.initializeObserver(el); } this.removeListeners(); this.addListeners(); } deactivateButton() { this.button && this.renderer.setAttribute(this.button, 'tabindex', '-1'); this.button && this.renderer.setAttribute(this.button, 'aria-hidden', 'true'); this.button && this.renderer.removeAttribute(this.button, 'aria-label'); this.removeListeners(); this.observer && this.observer.disconnect(); this.observer = null; } onFocus = () => { this.renderer.setStyle(this.button, 'box-shadow', 'inset 0 0 0 1px rgba(0, 0, 0, 0.08)'); }; onBlur = () => { this.renderer.removeStyle(this.button, 'box-shadow'); }; onClick = (e) => { const splitButtonToggleEnter = e instanceof KeyboardEvent && e.keyCode === Keys.Enter; const isClick = e instanceof PointerEvent; (splitButtonToggleEnter || isClick) && (this.focusButton = true); }; onKeyDown = (e) => { if (e.keyCode === Keys.ArrowDown && e.altKey) { e.stopImmediatePropagation(); this.focusButton = true; this.button.click(); } }; addListeners() { if (this.button) { this.zone.runOutsideAngular(() => this.button.addEventListener('focus', this.onFocus)); this.zone.runOutsideAngular(() => this.button.addEventListener('blur', this.onBlur)); this.zone.runOutsideAngular(() => this.button.addEventListener('click', this.onClick)); this.isSplitButton && this.zone.runOutsideAngular(() => this.button.addEventListener('keyup', this.onClick)); this.zone.runOutsideAngular(() => this.button.addEventListener('keydown', this.onKeyDown, true)); } } removeListeners() { if (this.button) { this.zone.runOutsideAngular(() => this.button.removeEventListener('focus', this.onFocus)); this.zone.runOutsideAngular(() => this.button.removeEventListener('blur', this.onBlur)); this.zone.runOutsideAngular(() => this.button.removeEventListener('click', this.onClick)); this.isSplitButton && this.zone.runOutsideAngular(() => this.button.removeEventListener('keyup', this.onClick)); this.zone.runOutsideAngular(() => this.button.removeEventListener('keydown', this.onKeyDown)); } } focusToggleButton() { this.focusButton && this.zone.runOutsideAngular(() => this.button.focus()); this.focusButton = false; } returnFocusToToggleButton() { if (this.isSplitButton) { this.zone.onStable.pipe(take(1)).subscribe(() => { this.focusToggleButton(); }); } else { this.focusToggleButton(); } } // Keeps the `aria-controls` and `aria-expanded` attributes of the main focusable element of the component // and the toggle button element in sync. initializeObserver(element) { const mainFocusableElement = element.querySelector('.k-split-button > .k-button:first-child, .k-input-inner'); const initialExpanded = mainFocusableElement.getAttribute('aria-expanded'); const initialControls = mainFocusableElement.getAttribute('aria-controls'); this.button && this.renderer.setAttribute(this.button, 'aria-expanded', initialExpanded); this.button && initialControls && this.renderer.setAttribute(this.button, 'aria-controls', initialControls); this.zone.runOutsideAngular(() => { const mutationConfig = { attributes: true }; const callback = (mutationList) => { for (const mutation of mutationList) { if (mutation.attributeName === 'aria-expanded') { this.renderer.setAttribute(this.button, 'aria-expanded', mainFocusableElement.getAttribute('aria-expanded')); } else if (mutation.attributeName === 'aria-controls') { const controlsRef = mainFocusableElement.getAttribute('aria-controls'); !this.isSplitButton && controlsRef ? this.renderer.setAttribute(this.button, 'aria-controls', controlsRef) : this.renderer.removeAttribute(this.button, 'aria-controls'); } } }; this.observer = new MutationObserver(callback); this.observer.observe(mainFocusableElement, mutationConfig); }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ToggleButtonTabStopDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i0.NgZone }, { token: i1.MultiTabStop }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: ToggleButtonTabStopDirective, isStandalone: true, selector: "[kendoToggleButtonTabStop]", inputs: { active: ["kendoToggleButtonTabStop", "active"], toggleButtonAriaLabel: "toggleButtonAriaLabel" }, usesOnChanges: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ToggleButtonTabStopDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoToggleButtonTabStop]', standalone: true }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: i0.NgZone }, { type: i1.MultiTabStop }]; }, propDecorators: { active: [{ type: Input, args: ['kendoToggleButtonTabStop'] }], toggleButtonAriaLabel: [{ type: Input }] } });