@progress/kendo-angular-label
Version:
Kendo UI Label for Angular
347 lines (346 loc) • 14.7 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 { 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 }]
}] } });