UNPKG

@progress/kendo-angular-label

Version:
158 lines (157 loc) 7.19 kB
/**----------------------------------------------------------------------------------------- * 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'] }] } });