@progress/kendo-angular-common
Version:
Kendo UI for Angular - Utility Package
230 lines (229 loc) • 10.9 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* 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
}] } });