@progress/kendo-angular-label
Version:
Kendo UI Label for Angular
158 lines (157 loc) • 7.19 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 { getRootElement, inputElementHasAttr, isInputElement, nativeLabelForTargets } from './util';
import { Directive, Input, HostBinding, ElementRef, Renderer2, NgZone } from '@angular/core';
import { isDocumentAvailable, guid } from '@progress/kendo-angular-common';
import * as i0 from "@angular/core";
/**
* Represents the [Kendo UI Label directive for Angular]({% slug label_directive %}).
* Use the `LabelDirective` to link a focusable Angular component or HTML element to a `<label>` tag with the `[for]` property binding.
*
* To link a component with the `label` element:
* - Set the `[for]` property binding to a [template reference variable](link:site.data.urls.angular['templatesyntax']#template-reference-variables--var-), or
* - Set the `[for]` property binding to an `id` HTML string value.
*
* @example
* ```ts
* @Component({
* selector: 'my-app',
* template: `
* <div class="row example-wrapper" style="min-height: 450px;">
* <div class="col-xs-12 col-md-6 example-col">
* <label [for]="datepicker">DatePicker: </label>
* <kendo-datepicker #datepicker></kendo-datepicker>
* </div>
*
* <div class="col-xs-12 col-md-6 example-col">
* <label for="input">Input: </label>
* <input id="input" />
* </div>
* </div>
* `
* })
* class AppComponent { }
* ```
*/
export class LabelDirective {
label;
renderer;
zone;
/**
* Sets the focusable target for the label.
* Accepts a [template reference variable](link:site.data.urls.angular['templatesyntax']#template-reference-variables--var-) or an `id` HTML string value.
*/
for;
get labelFor() {
if (typeof this.for === 'string') {
return this.for;
}
if (!isDocumentAvailable()) {
return null;
}
const component = this.getFocusableComponent() || {};
if (isInputElement(component) && !inputElementHasAttr(component, 'id')) {
this.renderer.setAttribute(component, 'id', `k-${guid()}`);
}
return component.focusableId || component.id || null;
}
/**
* @hidden
* Allows the user to specify if the label CSS class should be rendered or not.
*/
labelClass = true;
clickListener;
constructor(label, renderer, zone) {
this.label = label;
this.renderer = renderer;
this.zone = zone;
}
/**
* @hidden
*/
ngAfterViewInit() {
this.setAriaLabelledby();
this.zone.runOutsideAngular(() => this.clickListener = this.renderer.listen(this.label.nativeElement, 'click', this.handleClick));
}
/**
* @hidden
*/
ngOnDestroy() {
if (this.clickListener) {
this.clickListener();
}
}
/**
* @hidden
*/
setAriaLabelledby() {
if (!isDocumentAvailable()) {
return;
}
const component = this.getFocusableComponent();
if (component && component.focusableId) {
const rootElement = getRootElement(this.label.nativeElement);
const labelTarget = rootElement.querySelector(`#${component.focusableId}`);
const labelElement = this.label.nativeElement;
const id = labelElement.id || `k-${guid()}`;
if (!labelElement.getAttribute('id')) {
this.renderer.setAttribute(labelElement, 'id', id);
}
// Editor in iframe mode needs special treatment
if (component.focusableId.startsWith('k-editor') && component.iframe) {
component.contentAreaLoaded.subscribe(() => {
this.zone.runOutsideAngular(() => {
setTimeout(() => {
const editableElement = component.container.element.nativeElement.contentDocument.body.firstElementChild;
this.renderer.setAttribute(editableElement, 'aria-label', labelElement.textContent);
});
});
});
}
if (!labelTarget) {
return;
}
const existingAriaLabelledBy = labelTarget.hasAttribute('aria-labelledby') && labelTarget.getAttribute('aria-labelledby');
// DropDowns with focusable input elements rely on the aria-labelledby attribute to set the same attribute on their popup listbox element
// On the other hand, the aria-labelledby attribute is redundant on the Input element when there is label[for] association -
// https://feedback.telerik.com/kendo-angular-ui/1648203-remove-aria-labelledby-when-native-html-elements-are-associated.
// This addresses both cases, setting a special data-kendo-label-id attribute to be used internally by other components when the aria-describedby one is not applicable.
this.renderer.setAttribute(labelTarget, nativeLabelForTargets.includes(labelTarget.tagName) ? 'data-kendo-label-id' : 'aria-labelledby', existingAriaLabelledBy && existingAriaLabelledBy !== id ? `${existingAriaLabelledBy} ${id}` : id);
}
}
getFocusableComponent() {
const target = this.for;
return target && target.focus !== undefined ? target : null;
}
handleClick = () => {
const component = this.getFocusableComponent();
if (!component) {
return;
}
if (component.focus) {
component.focus();
}
};
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: LabelDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: LabelDirective, isStandalone: true, selector: "label[for]", inputs: { for: "for", labelClass: "labelClass" }, host: { properties: { "attr.for": "this.labelFor", "class.k-label": "this.labelClass" } }, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: LabelDirective, decorators: [{
type: Directive,
args: [{
// eslint-disable-next-line @angular-eslint/directive-selector
selector: 'label[for]',
standalone: true
}]
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: i0.NgZone }]; }, propDecorators: { for: [{
type: Input
}], labelFor: [{
type: HostBinding,
args: ['attr.for']
}], labelClass: [{
type: Input
}, {
type: HostBinding,
args: ['class.k-label']
}] } });