sass-references/angular-material/material/form-field/directives/floating-label.ts

155 lines
5.0 KiB
TypeScript

/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {
Directive,
ElementRef,
inject,
Input,
NgZone,
OnDestroy,
InjectionToken,
} from '@angular/core';
import {SharedResizeObserver} from '@angular/cdk/observers/private';
import {Subscription} from 'rxjs';
/** An interface that the parent form-field should implement to receive resize events. */
export interface FloatingLabelParent {
_handleLabelResized(): void;
}
/** An injion token for the parent form-field. */
export const FLOATING_LABEL_PARENT = new InjectionToken<FloatingLabelParent>('FloatingLabelParent');
/**
* Internal directive that maintains a MDC floating label. This directive does not
* use the `MDCFloatingLabelFoundation` class, as it is not worth the size cost of
* including it just to measure the label width and toggle some classes.
*
* The use of a directive allows us to conditionally render a floating label in the
* template without having to manually manage instantiation and destruction of the
* floating label component based on.
*
* The component is responsible for setting up the floating label styles, measuring label
* width for the outline notch, and providing inputs that can be used to toggle the
* label's floating or required state.
*/
@Directive({
selector: 'label[matFormFieldFloatingLabel]',
host: {
'class': 'mdc-floating-label mat-mdc-floating-label',
'[class.mdc-floating-label--float-above]': 'floating',
},
})
export class MatFormFieldFloatingLabel implements OnDestroy {
private _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
/** Whether the label is floating. */
@Input()
get floating() {
return this._floating;
}
set floating(value: boolean) {
this._floating = value;
if (this.monitorResize) {
this._handleResize();
}
}
private _floating = false;
/** Whether to monitor for resize events on the floating label. */
@Input()
get monitorResize() {
return this._monitorResize;
}
set monitorResize(value: boolean) {
this._monitorResize = value;
if (this._monitorResize) {
this._subscribeToResize();
} else {
this._resizeSubscription.unsubscribe();
}
}
private _monitorResize = false;
/** The shared ResizeObserver. */
private _resizeObserver = inject(SharedResizeObserver);
/** The Angular zone. */
private _ngZone = inject(NgZone);
/** The parent form-field. */
private _parent = inject(FLOATING_LABEL_PARENT);
/** The current resize event subscription. */
private _resizeSubscription = new Subscription();
constructor(...args: unknown[]);
constructor() {}
ngOnDestroy() {
this._resizeSubscription.unsubscribe();
}
/** Gets the width of the label. Used for the outline notch. */
getWidth(): number {
return estimateScrollWidth(this._elementRef.nativeElement);
}
/** Gets the HTML element for the floating label. */
get element(): HTMLElement {
return this._elementRef.nativeElement;
}
/** Handles resize events from the ResizeObserver. */
private _handleResize() {
// In the case where the label grows in size, the following sequence of events occurs:
// 1. The label grows by 1px triggering the ResizeObserver
// 2. The notch is expanded to accommodate the entire label
// 3. The label expands to its full width, triggering the ResizeObserver again
//
// This is expected, but If we allow this to all happen within the same macro task it causes an
// error: `ResizeObserver loop limit exceeded`. Therefore we push the notch resize out until
// the next macro task.
setTimeout(() => this._parent._handleLabelResized());
}
/** Subscribes to resize events. */
private _subscribeToResize() {
this._resizeSubscription.unsubscribe();
this._ngZone.runOutsideAngular(() => {
this._resizeSubscription = this._resizeObserver
.observe(this._elementRef.nativeElement, {box: 'border-box'})
.subscribe(() => this._handleResize());
});
}
}
/**
* Estimates the scroll width of an element.
* via https://github.com/material-components/material-components-web/blob/c0a11ef0d000a098fd0c372be8f12d6a99302855/packages/mdc-dom/ponyfill.ts
*/
function estimateScrollWidth(element: HTMLElement): number {
// Check the offsetParent. If the element inherits display: none from any
// parent, the offsetParent property will be null (see
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent).
// This check ensures we only clone the node when necessary.
const htmlEl = element as HTMLElement;
if (htmlEl.offsetParent !== null) {
return htmlEl.scrollWidth;
}
const clone = htmlEl.cloneNode(true) as HTMLElement;
clone.style.setProperty('position', 'absolute');
clone.style.setProperty('transform', 'translate(-9999px, -9999px)');
document.documentElement.appendChild(clone);
const scrollWidth = clone.scrollWidth;
clone.remove();
return scrollWidth;
}