UNPKG

@progress/kendo-angular-label

Version:
347 lines (346 loc) 14.7 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { ContentChild, Component, ElementRef, EventEmitter, HostBinding, Input, Renderer2, isDevMode, ChangeDetectorRef, Output } from '@angular/core'; import { LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n'; import { NgControl } from '@angular/forms'; import { guid, KendoInput, hasObservers, isDocumentAvailable } from '@progress/kendo-angular-common'; import { validatePackage } from '@progress/kendo-licensing'; import { packageMetadata } from '../package-metadata'; import { FloatingLabelInputAdapter } from './floating-label-input-adapter'; import { nativeLabelForTargets } from '../util'; import { NgIf, NgClass, NgStyle } from '@angular/common'; import { LocalizedMessagesDirective } from '../localization/localized-messages.directive'; import * as i0 from "@angular/core"; import * as i1 from "@progress/kendo-angular-l10n"; const isFunction = (x) => Object.prototype.toString.call(x) === '[object Function]'; /** * Represents the [Kendo UI FloatingLabel component for Angular]({% slug overview_floatinglabel %}). * Use this component to provide floating labels to `input` elements. * * The FloatingLabel supports Template and Reactive Forms. * You can use it with Kendo UI for Angular Inputs components such as `kendo-combobox`, `kendo-numerictextbox`, or `kendo-textbox`. * [See example.](slug:associate_floatinglabel) * * @example * ```html * <kendo-floatinglabel text="First name"> * <kendo-textbox></kendo-textbox> * </kendo-floatinglabel> * ``` * * @remarks * Supported children components are: {@link CustomMessagesComponent}. */ export class FloatingLabelComponent { elementRef; renderer; changeDetectorRef; localization; /** * Gets the current floating label position. */ get labelPosition() { if (!this.empty) { return 'Out'; } return this.focused ? 'Out' : 'In'; } hostClasses = true; get focusedClass() { return this.focused; } get invalidClass() { return this.invalid; } /** * @hidden */ direction; /** * Sets the CSS styles for the internal label element. * Accepts values supported by the [`ngStyle`](link:site.data.urls.angular['ngstyleapi']) directive. */ labelCssStyle; /** * Sets the CSS classes for the label element. * Accepts values supported by the [`ngClass`](link:site.data.urls.angular['ngclassapi']) directive. */ labelCssClass; /** * Sets the `id` attribute of the input inside the floating label. */ id; /** * Sets the text content of the floating label that describes the input. */ text; /** * Marks a form field as optional. When enabled, renders the `Optional` text by default. * You can customize the text by providing a custom message ([see example]({% slug label_globalization %}#toc-custom-messages)). * * @default false */ optional; /** * Fires after the FloatingLabel position changes. */ positionChange = new EventEmitter(); kendoInput; formControl; /** * @hidden */ focused = false; /** * @hidden */ empty = true; /** * @hidden */ invalid = false; /** * @hidden */ labelId = `k-${guid()}`; subscription; autoFillStarted = false; constructor(elementRef, renderer, changeDetectorRef, localization) { this.elementRef = elementRef; this.renderer = renderer; this.changeDetectorRef = changeDetectorRef; this.localization = localization; validatePackage(packageMetadata); this.direction = localization.rtl ? 'rtl' : 'ltr'; this.renderer.removeAttribute(this.elementRef.nativeElement, "id"); } /** * @hidden */ ngAfterContentInit() { if (!isDocumentAvailable()) { return; } this.validateSetup(); const control = new FloatingLabelInputAdapter(this.kendoInput || this.formControl.valueAccessor, this.formControl); this.addHandlers(control); this.setLabelFor(control); } ngAfterViewInit() { if (this.kendoInput) { this.setAriaLabelledby(this.kendoInput); } } /** * @hidden */ ngOnDestroy() { if (this.subscription) { this.subscription.unsubscribe(); } } /** * @hidden */ textFor(key) { return this.localization.get(key); } subscribe(control, eventName, handler) { if (control[eventName] instanceof EventEmitter) { const subscription = control[eventName].subscribe(handler); if (!this.subscription) { this.subscription = subscription; } else { this.subscription.add(subscription); } } } updateState() { const empty = value => { // zero is not an empty value (e.g., NumericTextBox) if (value === 0 || value === false) { return false; } // empty arrays are an empty value (e.g., MultiSelect) if (Array.isArray(value) && !value.length) { return true; } return !value; }; const formControl = this.formControl; if (formControl) { const valueAccessor = formControl.valueAccessor; if (isFunction(valueAccessor.isEmpty)) { this.empty = valueAccessor.isEmpty(); } else { this.empty = empty(formControl.value); } this.invalid = formControl.invalid && (formControl.touched || formControl.dirty); } else { this.empty = isFunction(this.kendoInput.isEmpty) ? this.kendoInput.isEmpty() : empty(this.kendoInput.value); } if (this.empty) { this.renderer.addClass(this.elementRef.nativeElement, 'k-empty'); } else { this.renderer.removeClass(this.elementRef.nativeElement, 'k-empty'); } this.changeDetectorRef.markForCheck(); } setAriaLabelledby(component) { const componentId = component.focusableId || component.id; if (componentId) { const focusableElement = this.elementRef.nativeElement.querySelector(`#${componentId}`); if (!focusableElement) { return; } const existingAriaLabelledBy = focusableElement.hasAttribute('aria-labelledby') && focusableElement.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(focusableElement, nativeLabelForTargets.includes(focusableElement.tagName) ? 'data-kendo-label-id' : 'aria-labelledby', existingAriaLabelledBy && existingAriaLabelledBy !== this.labelId ? `${existingAriaLabelledBy} ${this.labelId}` : this.labelId); } } setLabelFor(control) { const controlId = control.focusableId || control.id; if (this.id && controlId) { // input wins this.id = controlId; } else if (this.id) { control.focusableId = this.id; } else if (controlId) { this.id = controlId; } else { const id = `k-${guid()}`; control.focusableId = id; this.id = id; } } handleAutofill(control) { this.subscribe(control, 'autoFillStart', () => { this.autoFillStarted = true; this.renderer.removeClass(this.elementRef.nativeElement, 'k-empty'); }); this.subscribe(control, 'autoFillEnd', () => { if (this.autoFillStarted) { this.autoFillStarted = false; if (this.empty) { this.renderer.addClass(this.elementRef.nativeElement, 'k-empty'); } } }); } addHandlers(control) { const setFocus = (isFocused) => () => { this.focused = isFocused; this.updateState(); if (!this.empty) { return; } if (hasObservers(this.positionChange)) { this.positionChange.emit(isFocused ? 'Out' : 'In'); } }; this.subscribe(control, 'onFocus', setFocus(true)); this.subscribe(control, 'onBlur', setFocus(false)); this.handleAutofill(control); const updateState = () => this.updateState(); updateState(); this.subscribe(control, 'onValueChange', updateState); } validateSetup() { if (!this.formControl && !this.kendoInput) { if (isDevMode()) { throw new Error("The FloatingLabelComponent requires a Kendo Input component" + " or a forms-bound component to function properly."); } return; } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FloatingLabelComponent, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i0.ChangeDetectorRef }, { token: i1.LocalizationService }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: FloatingLabelComponent, isStandalone: true, selector: "kendo-floatinglabel", inputs: { labelCssStyle: "labelCssStyle", labelCssClass: "labelCssClass", id: "id", text: "text", optional: "optional" }, outputs: { positionChange: "positionChange" }, host: { properties: { "class.k-floating-label-container": "this.hostClasses", "class.k-focus": "this.focusedClass", "class.k-invalid": "this.invalidClass", "attr.dir": "this.direction" } }, providers: [ LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.floatinglabel' } ], queries: [{ propertyName: "kendoInput", first: true, predicate: KendoInput, descendants: true }, { propertyName: "formControl", first: true, predicate: NgControl, descendants: true }], exportAs: ["kendoFloatingLabel"], ngImport: i0, template: ` <ng-container kendoFloatingLabelLocalizedMessages i18n-optional="kendo.floatinglabel.optional|The text for the optional segment of a FloatingLabel component" optional="Optional" > </ng-container> <ng-content></ng-content> <label *ngIf="text" [ngClass]="labelCssClass" [ngStyle]="labelCssStyle" [for]="id" [attr.id]="labelId" class="k-floating-label"> {{ text }}<span *ngIf="optional" class="k-label-optional">({{textFor('optional')}})</span> </label> `, isInline: true, dependencies: [{ kind: "directive", type: LocalizedMessagesDirective, selector: "\n [kendoLabelLocalizedMessages],\n [kendoFloatingLabelLocalizedMessages]\n " }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FloatingLabelComponent, decorators: [{ type: Component, args: [{ selector: 'kendo-floatinglabel', exportAs: 'kendoFloatingLabel', providers: [ LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.floatinglabel' } ], template: ` <ng-container kendoFloatingLabelLocalizedMessages i18n-optional="kendo.floatinglabel.optional|The text for the optional segment of a FloatingLabel component" optional="Optional" > </ng-container> <ng-content></ng-content> <label *ngIf="text" [ngClass]="labelCssClass" [ngStyle]="labelCssStyle" [for]="id" [attr.id]="labelId" class="k-floating-label"> {{ text }}<span *ngIf="optional" class="k-label-optional">({{textFor('optional')}})</span> </label> `, standalone: true, imports: [LocalizedMessagesDirective, NgIf, NgClass, NgStyle] }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: i0.ChangeDetectorRef }, { type: i1.LocalizationService }]; }, propDecorators: { hostClasses: [{ type: HostBinding, args: ['class.k-floating-label-container'] }], focusedClass: [{ type: HostBinding, args: ['class.k-focus'] }], invalidClass: [{ type: HostBinding, args: ['class.k-invalid'] }], direction: [{ type: HostBinding, args: ['attr.dir'] }], labelCssStyle: [{ type: Input }], labelCssClass: [{ type: Input }], id: [{ type: Input }], text: [{ type: Input }], optional: [{ type: Input }], positionChange: [{ type: Output }], kendoInput: [{ type: ContentChild, args: [KendoInput, { static: false }] }], formControl: [{ type: ContentChild, args: [NgControl, { static: false }] }] } });