Skip to content

Commit 10a6ea4

Browse files
committed
perf(material/form-field): split DOM accesses into read and write (#31086)
In the outlined appearance we have to check the width of prefixes and suffixes and then write it to the floating label. These changes split it into `read` and `write` phases to reduce the amount of layout thrashing. (cherry picked from commit a86c547)
1 parent cbffce8 commit 10a6ea4

File tree

1 file changed

+47
-30
lines changed

1 file changed

+47
-30
lines changed

src/material/form-field/form-field.ts

+47-30
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@ export const MAT_FORM_FIELD_DEFAULT_OPTIONS = new InjectionToken<MatFormFieldDef
106106
'MAT_FORM_FIELD_DEFAULT_OPTIONS',
107107
);
108108

109+
/** Styles that are to be applied to the label elements in the outlined appearance. */
110+
type OutlinedLabelStyles =
111+
| [floatingLabelTransform: string, notchedOutlineWidth: number | null]
112+
| null;
113+
109114
/** Default appearance used by the form field. */
110115
const DEFAULT_APPEARANCE: MatFormFieldAppearance = 'fill';
111116

@@ -567,27 +572,27 @@ export class MatFormField
567572
* trigger the label offset update.
568573
*/
569574
private _syncOutlineLabelOffset() {
570-
// Whenever the prefix changes, schedule an update of the label offset.
571-
// TODO(mmalerba): Split this into separate `afterRender` calls using the `EarlyRead` and
572-
// `Write` phases.
573-
afterRenderEffect(() => {
574-
if (this._appearanceSignal() === 'outline') {
575-
this._updateOutlineLabelOffset();
576-
if (!globalThis.ResizeObserver) {
577-
return;
575+
afterRenderEffect({
576+
earlyRead: () => {
577+
if (this._appearanceSignal() !== 'outline') {
578+
this._outlineLabelOffsetResizeObserver?.disconnect();
579+
return null;
578580
}
579581

580582
// Setup a resize observer to monitor changes to the size of the prefix / suffix and
581583
// readjust the label offset.
582-
this._outlineLabelOffsetResizeObserver ||= new globalThis.ResizeObserver(() =>
583-
this._updateOutlineLabelOffset(),
584-
);
585-
for (const el of this._prefixSuffixContainers()) {
586-
this._outlineLabelOffsetResizeObserver.observe(el, {box: 'border-box'});
584+
if (globalThis.ResizeObserver) {
585+
this._outlineLabelOffsetResizeObserver ||= new globalThis.ResizeObserver(() => {
586+
this._writeOutlinedLabelStyles(this._getOutlinedLabelOffset());
587+
});
588+
for (const el of this._prefixSuffixContainers()) {
589+
this._outlineLabelOffsetResizeObserver.observe(el, {box: 'border-box'});
590+
}
587591
}
588-
} else {
589-
this._outlineLabelOffsetResizeObserver?.disconnect();
590-
}
592+
593+
return this._getOutlinedLabelOffset();
594+
},
595+
write: labelStyles => this._writeOutlinedLabelStyles(labelStyles()),
591596
});
592597
}
593598

@@ -740,30 +745,28 @@ export class MatFormField
740745
}
741746

742747
/**
743-
* Updates the horizontal offset of the label in the outline appearance. In the outline
748+
* Calculates the horizontal offset of the label in the outline appearance. In the outline
744749
* appearance, the notched-outline and label are not relative to the infix container because
745750
* the outline intends to surround prefixes, suffixes and the infix. This means that the
746751
* floating label by default overlaps prefixes in the docked state. To avoid this, we need to
747752
* horizontally offset the label by the width of the prefix container. The MDC text-field does
748753
* not need to do this because they use a fixed width for prefixes. Hence, they can simply
749754
* incorporate the horizontal offset into their default text-field styles.
750755
*/
751-
private _updateOutlineLabelOffset() {
756+
private _getOutlinedLabelOffset(): OutlinedLabelStyles {
752757
const dir = this._dir.valueSignal();
753758
if (!this._hasOutline() || !this._floatingLabel) {
754-
return;
759+
return null;
755760
}
756-
const floatingLabel = this._floatingLabel.element;
757761
// If no prefix is displayed, reset the outline label offset from potential
758762
// previous label offset updates.
759-
if (!(this._iconPrefixContainer || this._textPrefixContainer)) {
760-
floatingLabel.style.transform = '';
761-
return;
763+
if (!this._iconPrefixContainer && !this._textPrefixContainer) {
764+
return ['', null];
762765
}
763766
// If the form field is not attached to the DOM yet (e.g. in a tab), we defer
764767
// the label offset update until the zone stabilizes.
765768
if (!this._isAttachedToDom()) {
766-
return;
769+
return null;
767770
}
768771
const iconPrefixContainer = this._iconPrefixContainer?.nativeElement;
769772
const textPrefixContainer = this._textPrefixContainer?.nativeElement;
@@ -783,19 +786,33 @@ export class MatFormField
783786
// Update the translateX of the floating label to account for the prefix container,
784787
// but allow the CSS to override this setting via a CSS variable when the label is
785788
// floating.
786-
floatingLabel.style.transform = `var(
787-
--mat-mdc-form-field-label-transform,
788-
${FLOATING_LABEL_DEFAULT_DOCKED_TRANSFORM} translateX(${labelHorizontalOffset})
789-
)`;
789+
const floatingLabelTransform =
790+
'var(--mat-mdc-form-field-label-transform, ' +
791+
`${FLOATING_LABEL_DEFAULT_DOCKED_TRANSFORM} translateX(${labelHorizontalOffset}))`;
790792

791793
// Prevent the label from overlapping the suffix when in resting position.
792-
const prefixAndSuffixWidth =
794+
const notchedOutlineWidth =
793795
iconPrefixContainerWidth +
794796
textPrefixContainerWidth +
795797
iconSuffixContainerWidth +
796798
textSuffixContainerWidth;
797799

798-
this._notchedOutline?._setMaxWidth(prefixAndSuffixWidth);
800+
return [floatingLabelTransform, notchedOutlineWidth];
801+
}
802+
803+
/** Writes the styles produced by `_getOutlineLabelOffset` synchronously to the DOM. */
804+
private _writeOutlinedLabelStyles(styles: OutlinedLabelStyles): void {
805+
if (styles !== null) {
806+
const [floatingLabelTransform, notchedOutlineWidth] = styles;
807+
808+
if (this._floatingLabel) {
809+
this._floatingLabel.element.style.transform = floatingLabelTransform;
810+
}
811+
812+
if (notchedOutlineWidth !== null) {
813+
this._notchedOutline?._setMaxWidth(notchedOutlineWidth);
814+
}
815+
}
799816
}
800817

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

0 commit comments

Comments
 (0)