/** * @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 {Directionality} from '@angular/cdk/bidi'; import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; import {Platform} from '@angular/cdk/platform'; import {NgTemplateOutlet} from '@angular/common'; import { ANIMATION_MODULE_TYPE, AfterContentChecked, AfterContentInit, AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ContentChildren, ElementRef, InjectionToken, Injector, Input, OnDestroy, QueryList, ViewChild, ViewEncapsulation, afterRender, computed, contentChild, inject, } from '@angular/core'; import {AbstractControlDirective} from '@angular/forms'; import {ThemePalette} from '@angular/material/core'; import {_IdGenerator} from '@angular/cdk/a11y'; import {Subject, Subscription, merge} from 'rxjs'; import {map, pairwise, takeUntil, filter, startWith} from 'rxjs/operators'; import {MAT_ERROR, MatError} from './directives/error'; import { FLOATING_LABEL_PARENT, FloatingLabelParent, MatFormFieldFloatingLabel, } from './directives/floating-label'; import {MatHint} from './directives/hint'; import {MatLabel} from './directives/label'; import {MatFormFieldLineRipple} from './directives/line-ripple'; import {MatFormFieldNotchedOutline} from './directives/notched-outline'; import {MAT_PREFIX, MatPrefix} from './directives/prefix'; import {MAT_SUFFIX, MatSuffix} from './directives/suffix'; import {matFormFieldAnimations} from './form-field-animations'; import {MatFormFieldControl as _MatFormFieldControl} from './form-field-control'; import { getMatFormFieldDuplicatedHintError, getMatFormFieldMissingControlError, } from './form-field-errors'; /** Type for the available floatLabel values. */ export type FloatLabelType = 'always' | 'auto'; /** Possible appearance styles for the form field. */ export type MatFormFieldAppearance = 'fill' | 'outline'; /** Behaviors for how the subscript height is set. */ export type SubscriptSizing = 'fixed' | 'dynamic'; /** * Represents the default options for the form field that can be configured * using the `MAT_FORM_FIELD_DEFAULT_OPTIONS` injection token. */ export interface MatFormFieldDefaultOptions { /** Default form field appearance style. */ appearance?: MatFormFieldAppearance; /** * Default theme color of the form field. This API is supported in M2 themes only, it has no * effect in M3 themes. * * For information on applying color variants in M3, see * https://material.angular.io/guide/theming#using-component-color-variants */ color?: ThemePalette; /** Whether the required marker should be hidden by default. */ hideRequiredMarker?: boolean; /** * Whether the label for form fields should by default float `always`, * `never`, or `auto` (only when necessary). */ floatLabel?: FloatLabelType; /** Whether the form field should reserve space for one line by default. */ subscriptSizing?: SubscriptSizing; } /** * Injection token that can be used to inject an instances of `MatFormField`. It serves * as alternative token to the actual `MatFormField` class which would cause unnecessary * retention of the `MatFormField` class and its component metadata. */ export const MAT_FORM_FIELD = new InjectionToken('MatFormField'); /** * Injection token that can be used to configure the * default options for all form field within an app. */ export const MAT_FORM_FIELD_DEFAULT_OPTIONS = new InjectionToken( 'MAT_FORM_FIELD_DEFAULT_OPTIONS', ); /** Default appearance used by the form field. */ const DEFAULT_APPEARANCE: MatFormFieldAppearance = 'fill'; /** * Whether the label for form fields should by default float `always`, * `never`, or `auto`. */ const DEFAULT_FLOAT_LABEL: FloatLabelType = 'auto'; /** Default way that the subscript element height is set. */ const DEFAULT_SUBSCRIPT_SIZING: SubscriptSizing = 'fixed'; /** * Default transform for docked floating labels in a MDC text-field. This value has been * extracted from the MDC text-field styles because we programmatically modify the docked * label transform, but do not want to accidentally discard the default label transform. */ const FLOATING_LABEL_DEFAULT_DOCKED_TRANSFORM = `translateY(-50%)`; /** * Despite `MatFormFieldControl` being an abstract class, most of our usages enforce its shape * using `implements` instead of `extends`. This appears to be problematic when Closure compiler * is configured to use type information to rename properties, because it can't figure out which * class properties are coming from. This interface seems to work around the issue while preserving * our type safety (alternative being using `any` everywhere). * @docs-private */ interface MatFormFieldControl extends _MatFormFieldControl {} /** Container for form controls that applies Material Design styling and behavior. */ @Component({ selector: 'mat-form-field', exportAs: 'matFormField', templateUrl: './form-field.html', styleUrl: './form-field.css', animations: [matFormFieldAnimations.transitionMessages], host: { 'class': 'mat-mdc-form-field', '[class.mat-mdc-form-field-label-always-float]': '_shouldAlwaysFloat()', '[class.mat-mdc-form-field-has-icon-prefix]': '_hasIconPrefix', '[class.mat-mdc-form-field-has-icon-suffix]': '_hasIconSuffix', // Note that these classes reuse the same names as the non-MDC version, because they can be // considered a public API since custom form controls may use them to style themselves. // See https://github.com/angular/components/pull/20502#discussion_r486124901. '[class.mat-form-field-invalid]': '_control.errorState', '[class.mat-form-field-disabled]': '_control.disabled', '[class.mat-form-field-autofilled]': '_control.autofilled', '[class.mat-form-field-no-animations]': '_animationMode === "NoopAnimations"', '[class.mat-form-field-appearance-fill]': 'appearance == "fill"', '[class.mat-form-field-appearance-outline]': 'appearance == "outline"', '[class.mat-form-field-hide-placeholder]': '_hasFloatingLabel() && !_shouldLabelFloat()', '[class.mat-focused]': '_control.focused', '[class.mat-primary]': 'color !== "accent" && color !== "warn"', '[class.mat-accent]': 'color === "accent"', '[class.mat-warn]': 'color === "warn"', '[class.ng-untouched]': '_shouldForward("untouched")', '[class.ng-touched]': '_shouldForward("touched")', '[class.ng-pristine]': '_shouldForward("pristine")', '[class.ng-dirty]': '_shouldForward("dirty")', '[class.ng-valid]': '_shouldForward("valid")', '[class.ng-invalid]': '_shouldForward("invalid")', '[class.ng-pending]': '_shouldForward("pending")', }, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, providers: [ {provide: MAT_FORM_FIELD, useExisting: MatFormField}, {provide: FLOATING_LABEL_PARENT, useExisting: MatFormField}, ], imports: [ MatFormFieldFloatingLabel, MatFormFieldNotchedOutline, NgTemplateOutlet, MatFormFieldLineRipple, MatHint, ], }) export class MatFormField implements FloatingLabelParent, AfterContentInit, AfterContentChecked, AfterViewInit, OnDestroy { _elementRef = inject(ElementRef); private _changeDetectorRef = inject(ChangeDetectorRef); private _dir = inject(Directionality); private _platform = inject(Platform); private _idGenerator = inject(_IdGenerator); private _defaults = inject(MAT_FORM_FIELD_DEFAULT_OPTIONS, { optional: true, }); _animationMode = inject(ANIMATION_MODULE_TYPE, {optional: true}); @ViewChild('textField') _textField: ElementRef; @ViewChild('iconPrefixContainer') _iconPrefixContainer: ElementRef; @ViewChild('textPrefixContainer') _textPrefixContainer: ElementRef; @ViewChild('iconSuffixContainer') _iconSuffixContainer: ElementRef; @ViewChild('textSuffixContainer') _textSuffixContainer: ElementRef; @ViewChild(MatFormFieldFloatingLabel) _floatingLabel: MatFormFieldFloatingLabel | undefined; @ViewChild(MatFormFieldNotchedOutline) _notchedOutline: MatFormFieldNotchedOutline | undefined; @ViewChild(MatFormFieldLineRipple) _lineRipple: MatFormFieldLineRipple | undefined; @ContentChild(_MatFormFieldControl) _formFieldControl: MatFormFieldControl; @ContentChildren(MAT_PREFIX, {descendants: true}) _prefixChildren: QueryList; @ContentChildren(MAT_SUFFIX, {descendants: true}) _suffixChildren: QueryList; @ContentChildren(MAT_ERROR, {descendants: true}) _errorChildren: QueryList; @ContentChildren(MatHint, {descendants: true}) _hintChildren: QueryList; private readonly _labelChild = contentChild(MatLabel); /** Whether the required marker should be hidden. */ @Input() get hideRequiredMarker(): boolean { return this._hideRequiredMarker; } set hideRequiredMarker(value: BooleanInput) { this._hideRequiredMarker = coerceBooleanProperty(value); } private _hideRequiredMarker = false; /** * Theme color of the form field. This API is supported in M2 themes only, it * has no effect in M3 themes. * * For information on applying color variants in M3, see * https://material.angular.io/guide/theming#using-component-color-variants. */ @Input() color: ThemePalette = 'primary'; /** Whether the label should always float or float as the user types. */ @Input() get floatLabel(): FloatLabelType { return this._floatLabel || this._defaults?.floatLabel || DEFAULT_FLOAT_LABEL; } set floatLabel(value: FloatLabelType) { if (value !== this._floatLabel) { this._floatLabel = value; // For backwards compatibility. Custom form field controls or directives might set // the "floatLabel" input and expect the form field view to be updated automatically. // e.g. autocomplete trigger. Ideally we'd get rid of this and the consumers would just // emit the "stateChanges" observable. TODO(devversion): consider removing. this._changeDetectorRef.markForCheck(); } } private _floatLabel: FloatLabelType; /** The form field appearance style. */ @Input() get appearance(): MatFormFieldAppearance { return this._appearance; } set appearance(value: MatFormFieldAppearance) { const oldValue = this._appearance; const newAppearance = value || this._defaults?.appearance || DEFAULT_APPEARANCE; if (typeof ngDevMode === 'undefined' || ngDevMode) { if (newAppearance !== 'fill' && newAppearance !== 'outline') { throw new Error( `MatFormField: Invalid appearance "${newAppearance}", valid values are "fill" or "outline".`, ); } } this._appearance = newAppearance; if (this._appearance === 'outline' && this._appearance !== oldValue) { // If the appearance has been switched to `outline`, the label offset needs to be updated. // The update can happen once the view has been re-checked, but not immediately because // the view has not been updated and the notched-outline floating label is not present. this._needsOutlineLabelOffsetUpdate = true; } } private _appearance: MatFormFieldAppearance = DEFAULT_APPEARANCE; /** * Whether the form field should reserve space for one line of hint/error text (default) * or to have the spacing grow from 0px as needed based on the size of the hint/error content. * Note that when using dynamic sizing, layout shifts will occur when hint/error text changes. */ @Input() get subscriptSizing(): SubscriptSizing { return this._subscriptSizing || this._defaults?.subscriptSizing || DEFAULT_SUBSCRIPT_SIZING; } set subscriptSizing(value: SubscriptSizing) { this._subscriptSizing = value || this._defaults?.subscriptSizing || DEFAULT_SUBSCRIPT_SIZING; } private _subscriptSizing: SubscriptSizing | null = null; /** Text for the form field hint. */ @Input() get hintLabel(): string { return this._hintLabel; } set hintLabel(value: string) { this._hintLabel = value; this._processHints(); } private _hintLabel = ''; _hasIconPrefix = false; _hasTextPrefix = false; _hasIconSuffix = false; _hasTextSuffix = false; // Unique id for the internal form field label. readonly _labelId = this._idGenerator.getId('mat-mdc-form-field-label-'); // Unique id for the hint label. readonly _hintLabelId = this._idGenerator.getId('mat-mdc-hint-'); /** State of the mat-hint and mat-error animations. */ _subscriptAnimationState = ''; /** Gets the current form field control */ get _control(): MatFormFieldControl { return this._explicitFormFieldControl || this._formFieldControl; } set _control(value) { this._explicitFormFieldControl = value; } private _destroyed = new Subject(); private _isFocused: boolean | null = null; private _explicitFormFieldControl: MatFormFieldControl; private _needsOutlineLabelOffsetUpdate = false; private _previousControl: MatFormFieldControl | null = null; private _stateChanges: Subscription | undefined; private _valueChanges: Subscription | undefined; private _describedByChanges: Subscription | undefined; private _injector = inject(Injector); constructor(...args: unknown[]); constructor() { const defaults = this._defaults; if (defaults) { if (defaults.appearance) { this.appearance = defaults.appearance; } this._hideRequiredMarker = Boolean(defaults?.hideRequiredMarker); if (defaults.color) { this.color = defaults.color; } } } ngAfterViewInit() { // Initial focus state sync. This happens rarely, but we want to account for // it in case the form field control has "focused" set to true on init. this._updateFocusState(); // Enable animations now. This ensures we don't animate on initial render. this._subscriptAnimationState = 'enter'; // Because the above changes a value used in the template after it was checked, we need // to trigger CD or the change might not be reflected if there is no other CD scheduled. this._changeDetectorRef.detectChanges(); } ngAfterContentInit() { this._assertFormFieldControl(); this._initializeSubscript(); this._initializePrefixAndSuffix(); this._initializeOutlineLabelOffsetSubscriptions(); } ngAfterContentChecked() { this._assertFormFieldControl(); if (this._control !== this._previousControl) { this._initializeControl(this._previousControl); this._previousControl = this._control; } } ngOnDestroy() { this._stateChanges?.unsubscribe(); this._valueChanges?.unsubscribe(); this._describedByChanges?.unsubscribe(); this._destroyed.next(); this._destroyed.complete(); } /** * Gets the id of the label element. If no label is present, returns `null`. */ getLabelId = computed(() => (this._hasFloatingLabel() ? this._labelId : null)); /** * Gets an ElementRef for the element that a overlay attached to the form field * should be positioned relative to. */ getConnectedOverlayOrigin(): ElementRef { return this._textField || this._elementRef; } /** Animates the placeholder up and locks it in position. */ _animateAndLockLabel(): void { // This is for backwards compatibility only. Consumers of the form field might use // this method. e.g. the autocomplete trigger. This method has been added to the non-MDC // form field because setting "floatLabel" to "always" caused the label to float without // animation. This is different in MDC where the label always animates, so this method // is no longer necessary. There doesn't seem any benefit in adding logic to allow changing // the floating label state without animations. The non-MDC implementation was inconsistent // because it always animates if "floatLabel" is set away from "always". // TODO(devversion): consider removing this method when releasing the MDC form field. if (this._hasFloatingLabel()) { this.floatLabel = 'always'; } } /** Initializes the registered form field control. */ private _initializeControl(previousControl: MatFormFieldControl | null) { const control = this._control; const classPrefix = 'mat-mdc-form-field-type-'; if (previousControl) { this._elementRef.nativeElement.classList.remove(classPrefix + previousControl.controlType); } if (control.controlType) { this._elementRef.nativeElement.classList.add(classPrefix + control.controlType); } // Subscribe to changes in the child control state in order to update the form field UI. this._stateChanges?.unsubscribe(); this._stateChanges = control.stateChanges.subscribe(() => { this._updateFocusState(); this._changeDetectorRef.markForCheck(); }); // Updating the `aria-describedby` touches the DOM. Only do it if it actually needs to change. this._describedByChanges?.unsubscribe(); this._describedByChanges = control.stateChanges .pipe( startWith([undefined, undefined] as const), map(() => [control.errorState, control.userAriaDescribedBy] as const), pairwise(), filter(([[prevErrorState, prevDescribedBy], [currentErrorState, currentDescribedBy]]) => { return prevErrorState !== currentErrorState || prevDescribedBy !== currentDescribedBy; }), ) .subscribe(() => this._syncDescribedByIds()); this._valueChanges?.unsubscribe(); // Run change detection if the value changes. if (control.ngControl && control.ngControl.valueChanges) { this._valueChanges = control.ngControl.valueChanges .pipe(takeUntil(this._destroyed)) .subscribe(() => this._changeDetectorRef.markForCheck()); } } private _checkPrefixAndSuffixTypes() { this._hasIconPrefix = !!this._prefixChildren.find(p => !p._isText); this._hasTextPrefix = !!this._prefixChildren.find(p => p._isText); this._hasIconSuffix = !!this._suffixChildren.find(s => !s._isText); this._hasTextSuffix = !!this._suffixChildren.find(s => s._isText); } /** Initializes the prefix and suffix containers. */ private _initializePrefixAndSuffix() { this._checkPrefixAndSuffixTypes(); // Mark the form field as dirty whenever the prefix or suffix children change. This // is necessary because we conditionally display the prefix/suffix containers based // on whether there is projected content. merge(this._prefixChildren.changes, this._suffixChildren.changes).subscribe(() => { this._checkPrefixAndSuffixTypes(); this._changeDetectorRef.markForCheck(); }); } /** * Initializes the subscript by validating hints and synchronizing "aria-describedby" ids * with the custom form field control. Also subscribes to hint and error changes in order * to be able to validate and synchronize ids on change. */ private _initializeSubscript() { // Re-validate when the number of hints changes. this._hintChildren.changes.subscribe(() => { this._processHints(); this._changeDetectorRef.markForCheck(); }); // Update the aria-described by when the number of errors changes. this._errorChildren.changes.subscribe(() => { this._syncDescribedByIds(); this._changeDetectorRef.markForCheck(); }); // Initial mat-hint validation and subscript describedByIds sync. this._validateHints(); this._syncDescribedByIds(); } /** Throws an error if the form field's control is missing. */ private _assertFormFieldControl() { if (!this._control && (typeof ngDevMode === 'undefined' || ngDevMode)) { throw getMatFormFieldMissingControlError(); } } private _updateFocusState() { // Usually the MDC foundation would call "activateFocus" and "deactivateFocus" whenever // certain DOM events are emitted. This is not possible in our implementation of the // form field because we support abstract form field controls which are not necessarily // of type input, nor do we have a reference to a native form field control element. Instead // we handle the focus by checking if the abstract form field control focused state changes. if (this._control.focused && !this._isFocused) { this._isFocused = true; this._lineRipple?.activate(); } else if (!this._control.focused && (this._isFocused || this._isFocused === null)) { this._isFocused = false; this._lineRipple?.deactivate(); } this._textField?.nativeElement.classList.toggle( 'mdc-text-field--focused', this._control.focused, ); } /** * The floating label in the docked state needs to account for prefixes. The horizontal offset * is calculated whenever the appearance changes to `outline`, the prefixes change, or when the * form field is added to the DOM. This method sets up all subscriptions which are needed to * trigger the label offset update. */ private _initializeOutlineLabelOffsetSubscriptions() { // Whenever the prefix changes, schedule an update of the label offset. // TODO(mmalerba): Use ResizeObserver to better support dynamically changing prefix content. this._prefixChildren.changes.subscribe(() => (this._needsOutlineLabelOffsetUpdate = true)); // TODO(mmalerba): Split this into separate `afterRender` calls using the `EarlyRead` and // `Write` phases. afterRender( () => { if (this._needsOutlineLabelOffsetUpdate) { this._needsOutlineLabelOffsetUpdate = false; this._updateOutlineLabelOffset(); } }, { injector: this._injector, }, ); this._dir.change .pipe(takeUntil(this._destroyed)) .subscribe(() => (this._needsOutlineLabelOffsetUpdate = true)); } /** Whether the floating label should always float or not. */ _shouldAlwaysFloat() { return this.floatLabel === 'always'; } _hasOutline() { return this.appearance === 'outline'; } /** * Whether the label should display in the infix. Labels in the outline appearance are * displayed as part of the notched-outline and are horizontally offset to account for * form field prefix content. This won't work in server side rendering since we cannot * measure the width of the prefix container. To make the docked label appear as if the * right offset has been calculated, we forcibly render the label inside the infix. Since * the label is part of the infix, the label cannot overflow the prefix content. */ _forceDisplayInfixLabel() { return !this._platform.isBrowser && this._prefixChildren.length && !this._shouldLabelFloat(); } _hasFloatingLabel = computed(() => !!this._labelChild()); _shouldLabelFloat(): boolean { if (!this._hasFloatingLabel()) { return false; } return this._control.shouldLabelFloat || this._shouldAlwaysFloat(); } /** * Determines whether a class from the AbstractControlDirective * should be forwarded to the host element. */ _shouldForward(prop: keyof AbstractControlDirective): boolean { const control = this._control ? this._control.ngControl : null; return control && control[prop]; } /** Determines whether to display hints or errors. */ _getDisplayedMessages(): 'error' | 'hint' { return this._errorChildren && this._errorChildren.length > 0 && this._control.errorState ? 'error' : 'hint'; } /** Handle label resize events. */ _handleLabelResized() { this._refreshOutlineNotchWidth(); } /** Refreshes the width of the outline-notch, if present. */ _refreshOutlineNotchWidth() { if (!this._hasOutline() || !this._floatingLabel || !this._shouldLabelFloat()) { this._notchedOutline?._setNotchWidth(0); } else { this._notchedOutline?._setNotchWidth(this._floatingLabel.getWidth()); } } /** Does any extra processing that is required when handling the hints. */ private _processHints() { this._validateHints(); this._syncDescribedByIds(); } /** * Ensure that there is a maximum of one of each "mat-hint" alignment specified. The hint * label specified set through the input is being considered as "start" aligned. * * This method is a noop if Angular runs in production mode. */ private _validateHints() { if (this._hintChildren && (typeof ngDevMode === 'undefined' || ngDevMode)) { let startHint: MatHint; let endHint: MatHint; this._hintChildren.forEach((hint: MatHint) => { if (hint.align === 'start') { if (startHint || this.hintLabel) { throw getMatFormFieldDuplicatedHintError('start'); } startHint = hint; } else if (hint.align === 'end') { if (endHint) { throw getMatFormFieldDuplicatedHintError('end'); } endHint = hint; } }); } } /** * Sets the list of element IDs that describe the child control. This allows the control to update * its `aria-describedby` attribute accordingly. */ private _syncDescribedByIds() { if (this._control) { let ids: string[] = []; // TODO(wagnermaciel): Remove the type check when we find the root cause of this bug. if ( this._control.userAriaDescribedBy && typeof this._control.userAriaDescribedBy === 'string' ) { ids.push(...this._control.userAriaDescribedBy.split(' ')); } if (this._getDisplayedMessages() === 'hint') { const startHint = this._hintChildren ? this._hintChildren.find(hint => hint.align === 'start') : null; const endHint = this._hintChildren ? this._hintChildren.find(hint => hint.align === 'end') : null; if (startHint) { ids.push(startHint.id); } else if (this._hintLabel) { ids.push(this._hintLabelId); } if (endHint) { ids.push(endHint.id); } } else if (this._errorChildren) { ids.push(...this._errorChildren.map(error => error.id)); } this._control.setDescribedByIds(ids); } } /** * Updates the horizontal offset of the label in the outline appearance. In the outline * appearance, the notched-outline and label are not relative to the infix container because * the outline intends to surround prefixes, suffixes and the infix. This means that the * floating label by default overlaps prefixes in the docked state. To avoid this, we need to * horizontally offset the label by the width of the prefix container. The MDC text-field does * not need to do this because they use a fixed width for prefixes. Hence, they can simply * incorporate the horizontal offset into their default text-field styles. */ private _updateOutlineLabelOffset() { if (!this._hasOutline() || !this._floatingLabel) { return; } const floatingLabel = this._floatingLabel.element; // If no prefix is displayed, reset the outline label offset from potential // previous label offset updates. if (!(this._iconPrefixContainer || this._textPrefixContainer)) { floatingLabel.style.transform = ''; return; } // If the form field is not attached to the DOM yet (e.g. in a tab), we defer // the label offset update until the zone stabilizes. if (!this._isAttachedToDom()) { this._needsOutlineLabelOffsetUpdate = true; return; } const iconPrefixContainer = this._iconPrefixContainer?.nativeElement; const textPrefixContainer = this._textPrefixContainer?.nativeElement; const iconSuffixContainer = this._iconSuffixContainer?.nativeElement; const textSuffixContainer = this._textSuffixContainer?.nativeElement; const iconPrefixContainerWidth = iconPrefixContainer?.getBoundingClientRect().width ?? 0; const textPrefixContainerWidth = textPrefixContainer?.getBoundingClientRect().width ?? 0; const iconSuffixContainerWidth = iconSuffixContainer?.getBoundingClientRect().width ?? 0; const textSuffixContainerWidth = textSuffixContainer?.getBoundingClientRect().width ?? 0; // If the directionality is RTL, the x-axis transform needs to be inverted. This // is because `transformX` does not change based on the page directionality. const negate = this._dir.value === 'rtl' ? '-1' : '1'; const prefixWidth = `${iconPrefixContainerWidth + textPrefixContainerWidth}px`; const labelOffset = `var(--mat-mdc-form-field-label-offset-x, 0px)`; const labelHorizontalOffset = `calc(${negate} * (${prefixWidth} + ${labelOffset}))`; // Update the translateX of the floating label to account for the prefix container, // but allow the CSS to override this setting via a CSS variable when the label is // floating. floatingLabel.style.transform = `var( --mat-mdc-form-field-label-transform, ${FLOATING_LABEL_DEFAULT_DOCKED_TRANSFORM} translateX(${labelHorizontalOffset}) )`; // Prevent the label from overlapping the suffix when in resting position. const prefixAndSuffixWidth = iconPrefixContainerWidth + textPrefixContainerWidth + iconSuffixContainerWidth + textSuffixContainerWidth; this._elementRef.nativeElement.style.setProperty( '--mat-form-field-notch-max-width', `calc(100% - ${prefixAndSuffixWidth}px)`, ); } /** Checks whether the form field is attached to the DOM. */ private _isAttachedToDom(): boolean { const element: HTMLElement = this._elementRef.nativeElement; if (element.getRootNode) { const rootNode = element.getRootNode(); // If the element is inside the DOM the root node will be either the document // or the closest shadow root, otherwise it'll be the element itself. return rootNode && rootNode !== element; } // Otherwise fall back to checking if it's in the document. This doesn't account for // shadow DOM, however browser that support shadow DOM should support `getRootNode` as well. return document.documentElement!.contains(element); } }