Skip to content

Commit

Permalink
refactor(material/form-field): use afterNextRender to schedule outlin…
Browse files Browse the repository at this point in the history
…e updates
  • Loading branch information
mmalerba committed Feb 23, 2024
1 parent 63a764d commit 13193df
Showing 1 changed file with 47 additions and 58 deletions.
105 changes: 47 additions & 58 deletions src/material/form-field/form-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
ViewChild,
ViewEncapsulation,
ANIMATION_MODULE_TYPE,
afterNextRender,
} from '@angular/core';
import {AbstractControlDirective} from '@angular/forms';
import {ThemePalette} from '@angular/material/core';
Expand Down Expand Up @@ -245,7 +246,7 @@ export class MatFormField
// 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._needsOutlineLabelOffsetUpdateOnStable = true;
this._updateOutlineLabelOffsetAfterNextRender();
}
}
private _appearance: MatFormFieldAppearance = DEFAULT_APPEARANCE;
Expand Down Expand Up @@ -300,12 +301,15 @@ export class MatFormField
private _destroyed = new Subject<void>();
private _isFocused: boolean | null = null;
private _explicitFormFieldControl: MatFormFieldControl<any>;
private _needsOutlineLabelOffsetUpdateOnStable = false;

constructor(
public _elementRef: ElementRef,
private _changeDetectorRef: ChangeDetectorRef,
private _ngZone: NgZone,
/**
* @deprecated not needed, to be removed.
* @breaking-change 19.0.0 remove this param
*/
private _unusedNgZone: NgZone,
private _dir: Directionality,
private _platform: Platform,
@Optional()
Expand Down Expand Up @@ -485,30 +489,13 @@ export class MatFormField
* 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. In general, we want to avoid performing measurements often,
* so we rely on the `NgZone` as indicator when the offset should be recalculated, instead of
* checking every change detection cycle.
* trigger the label offset update.
*/
private _initializeOutlineLabelOffsetSubscriptions() {
// Whenever the prefix changes, schedule an update of the label offset.
this._prefixChildren.changes.subscribe(
() => (this._needsOutlineLabelOffsetUpdateOnStable = true),
);

// Note that we have to run outside of the `NgZone` explicitly, in order to avoid
// throwing users into an infinite loop if `zone-patch-rxjs` is included.
this._ngZone.runOutsideAngular(() => {
this._ngZone.onStable.pipe(takeUntil(this._destroyed)).subscribe(() => {
if (this._needsOutlineLabelOffsetUpdateOnStable) {
this._needsOutlineLabelOffsetUpdateOnStable = false;
this._updateOutlineLabelOffset();
}
});
});

this._prefixChildren.changes.subscribe(() => this._updateOutlineLabelOffsetAfterNextRender());
this._dir.change
.pipe(takeUntil(this._destroyed))
.subscribe(() => (this._needsOutlineLabelOffsetUpdateOnStable = true));
.subscribe(() => this._updateOutlineLabelOffsetAfterNextRender());
}

/** Whether the floating label should always float or not. */
Expand Down Expand Up @@ -652,41 +639,43 @@ export class MatFormField
* 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._platform.isBrowser || !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._needsOutlineLabelOffsetUpdateOnStable = true;
return;
}
const iconPrefixContainer = this._iconPrefixContainer?.nativeElement;
const textPrefixContainer = this._textPrefixContainer?.nativeElement;
const iconPrefixContainerWidth = iconPrefixContainer?.getBoundingClientRect().width ?? 0;
const textPrefixContainerWidth = textPrefixContainer?.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})
)`;
private _updateOutlineLabelOffsetAfterNextRender() {
afterNextRender(() => {
if (!this._platform.isBrowser || !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 after the next render.
if (!this._isAttachedToDom()) {
this._updateOutlineLabelOffsetAfterNextRender();
return;
}
const iconPrefixContainer = this._iconPrefixContainer?.nativeElement;
const textPrefixContainer = this._textPrefixContainer?.nativeElement;
const iconPrefixContainerWidth = iconPrefixContainer?.getBoundingClientRect().width ?? 0;
const textPrefixContainerWidth = textPrefixContainer?.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})
)`;
});
}

/** Checks whether the form field is attached to the DOM. */
Expand Down

0 comments on commit 13193df

Please sign in to comment.