Skip to content

Commit 1bef7b5

Browse files
committed
WIP: attempt to use ResizeObserver for label offset calculation
1 parent 29ef3fb commit 1bef7b5

File tree

1 file changed

+49
-35
lines changed

1 file changed

+49
-35
lines changed

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

Lines changed: 49 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.dev/license
77
*/
8+
import {_IdGenerator} from '@angular/cdk/a11y';
89
import {Directionality} from '@angular/cdk/bidi';
910
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
1011
import {Platform} from '@angular/cdk/platform';
@@ -27,16 +28,17 @@ import {
2728
QueryList,
2829
ViewChild,
2930
ViewEncapsulation,
30-
afterRender,
31+
afterRenderEffect,
3132
computed,
3233
contentChild,
3334
inject,
35+
signal,
36+
viewChild,
3437
} from '@angular/core';
3538
import {AbstractControlDirective, ValidatorFn} from '@angular/forms';
36-
import {_animationsDisabled, ThemePalette} from '../core';
37-
import {_IdGenerator} from '@angular/cdk/a11y';
3839
import {Subject, Subscription, merge} from 'rxjs';
39-
import {map, pairwise, takeUntil, filter, startWith} from 'rxjs/operators';
40+
import {filter, map, pairwise, startWith, takeUntil} from 'rxjs/operators';
41+
import {ThemePalette, _animationsDisabled} from '../core';
4042
import {MAT_ERROR, MatError} from './directives/error';
4143
import {
4244
FLOATING_LABEL_PARENT,
@@ -203,6 +205,21 @@ export class MatFormField
203205
@ViewChild(MatFormFieldNotchedOutline) _notchedOutline: MatFormFieldNotchedOutline | undefined;
204206
@ViewChild(MatFormFieldLineRipple) _lineRipple: MatFormFieldLineRipple | undefined;
205207

208+
private _iconPrefixContainerSignal = viewChild<ElementRef<HTMLElement>>('iconPrefixContainer');
209+
private _textPrefixContainerSignal = viewChild<ElementRef<HTMLElement>>('textPrefixContainer');
210+
private _iconSuffixContainerSignal = viewChild<ElementRef<HTMLElement>>('iconSuffixContainer');
211+
private _textSuffixContainerSignal = viewChild<ElementRef<HTMLElement>>('textSuffixContainer');
212+
private _prefixSuffixContainers = computed(() => {
213+
return [
214+
this._iconPrefixContainerSignal(),
215+
this._textPrefixContainerSignal(),
216+
this._iconSuffixContainerSignal(),
217+
this._textSuffixContainerSignal(),
218+
]
219+
.map(container => container?.nativeElement)
220+
.filter(e => e !== undefined);
221+
});
222+
206223
@ContentChild(_MatFormFieldControl) _formFieldControl: MatFormFieldControl<any>;
207224
@ContentChildren(MAT_PREFIX, {descendants: true}) _prefixChildren: QueryList<MatPrefix>;
208225
@ContentChildren(MAT_SUFFIX, {descendants: true}) _suffixChildren: QueryList<MatSuffix>;
@@ -250,10 +267,9 @@ export class MatFormField
250267
/** The form field appearance style. */
251268
@Input()
252269
get appearance(): MatFormFieldAppearance {
253-
return this._appearance;
270+
return this._appearanceSignal();
254271
}
255272
set appearance(value: MatFormFieldAppearance) {
256-
const oldValue = this._appearance;
257273
const newAppearance = value || this._defaults?.appearance || DEFAULT_APPEARANCE;
258274
if (typeof ngDevMode === 'undefined' || ngDevMode) {
259275
if (newAppearance !== 'fill' && newAppearance !== 'outline') {
@@ -262,15 +278,9 @@ export class MatFormField
262278
);
263279
}
264280
}
265-
this._appearance = newAppearance;
266-
if (this._appearance === 'outline' && this._appearance !== oldValue) {
267-
// If the appearance has been switched to `outline`, the label offset needs to be updated.
268-
// The update can happen once the view has been re-checked, but not immediately because
269-
// the view has not been updated and the notched-outline floating label is not present.
270-
this._needsOutlineLabelOffsetUpdate = true;
271-
}
281+
this._appearanceSignal.set(newAppearance);
272282
}
273-
private _appearance: MatFormFieldAppearance = DEFAULT_APPEARANCE;
283+
private _appearanceSignal = signal(DEFAULT_APPEARANCE);
274284

275285
/**
276286
* Whether the form field should reserve space for one line of hint/error text (default)
@@ -319,7 +329,6 @@ export class MatFormField
319329
private _destroyed = new Subject<void>();
320330
private _isFocused: boolean | null = null;
321331
private _explicitFormFieldControl: MatFormFieldControl<any>;
322-
private _needsOutlineLabelOffsetUpdate = false;
323332
private _previousControl: MatFormFieldControl<unknown> | null = null;
324333
private _previousControlValidatorFn: ValidatorFn | null = null;
325334
private _stateChanges: Subscription | undefined;
@@ -341,6 +350,8 @@ export class MatFormField
341350
this.color = defaults.color;
342351
}
343352
}
353+
354+
this._syncOutlineLabelOffset();
344355
}
345356

346357
ngAfterViewInit() {
@@ -366,7 +377,6 @@ export class MatFormField
366377
this._assertFormFieldControl();
367378
this._initializeSubscript();
368379
this._initializePrefixAndSuffix();
369-
this._initializeOutlineLabelOffsetSubscriptions();
370380
}
371381

372382
ngAfterContentChecked() {
@@ -399,6 +409,7 @@ export class MatFormField
399409
}
400410

401411
ngOnDestroy() {
412+
this._outlineLabelOffsetResizeObserver?.disconnect();
402413
this._stateChanges?.unsubscribe();
403414
this._valueChanges?.unsubscribe();
404415
this._describedByChanges?.unsubscribe();
@@ -546,34 +557,37 @@ export class MatFormField
546557
);
547558
}
548559

560+
private _outlineLabelOffsetResizeObserver: ResizeObserver | null = null;
561+
549562
/**
550563
* The floating label in the docked state needs to account for prefixes. The horizontal offset
551564
* is calculated whenever the appearance changes to `outline`, the prefixes change, or when the
552565
* form field is added to the DOM. This method sets up all subscriptions which are needed to
553566
* trigger the label offset update.
554567
*/
555-
private _initializeOutlineLabelOffsetSubscriptions() {
568+
private _syncOutlineLabelOffset() {
556569
// Whenever the prefix changes, schedule an update of the label offset.
557-
// TODO(mmalerba): Use ResizeObserver to better support dynamically changing prefix content.
558-
this._prefixChildren.changes.subscribe(() => (this._needsOutlineLabelOffsetUpdate = true));
559-
560570
// TODO(mmalerba): Split this into separate `afterRender` calls using the `EarlyRead` and
561571
// `Write` phases.
562-
afterRender(
563-
() => {
564-
if (this._needsOutlineLabelOffsetUpdate) {
565-
this._needsOutlineLabelOffsetUpdate = false;
566-
this._updateOutlineLabelOffset();
572+
afterRenderEffect(() => {
573+
if (this._appearanceSignal() === 'outline') {
574+
this._updateOutlineLabelOffset();
575+
if (!globalThis.ResizeObserver) {
576+
return;
567577
}
568-
},
569-
{
570-
injector: this._injector,
571-
},
572-
);
573578

574-
this._dir.change
575-
.pipe(takeUntil(this._destroyed))
576-
.subscribe(() => (this._needsOutlineLabelOffsetUpdate = true));
579+
// Setup a resize observer to monitor changes to the size of the prefix / suffix and
580+
// readjust the label offset.
581+
this._outlineLabelOffsetResizeObserver ||= new globalThis.ResizeObserver(() =>
582+
this._updateOutlineLabelOffset(),
583+
);
584+
for (const el of this._prefixSuffixContainers()) {
585+
this._outlineLabelOffsetResizeObserver.observe(el, {box: 'border-box'});
586+
}
587+
} else {
588+
this._outlineLabelOffsetResizeObserver?.disconnect();
589+
}
590+
});
577591
}
578592

579593
/** Whether the floating label should always float or not. */
@@ -719,6 +733,7 @@ export class MatFormField
719733
* incorporate the horizontal offset into their default text-field styles.
720734
*/
721735
private _updateOutlineLabelOffset() {
736+
const dir = this._dir.valueSignal();
722737
if (!this._hasOutline() || !this._floatingLabel) {
723738
return;
724739
}
@@ -732,7 +747,6 @@ export class MatFormField
732747
// If the form field is not attached to the DOM yet (e.g. in a tab), we defer
733748
// the label offset update until the zone stabilizes.
734749
if (!this._isAttachedToDom()) {
735-
this._needsOutlineLabelOffsetUpdate = true;
736750
return;
737751
}
738752
const iconPrefixContainer = this._iconPrefixContainer?.nativeElement;
@@ -745,7 +759,7 @@ export class MatFormField
745759
const textSuffixContainerWidth = textSuffixContainer?.getBoundingClientRect().width ?? 0;
746760
// If the directionality is RTL, the x-axis transform needs to be inverted. This
747761
// is because `transformX` does not change based on the page directionality.
748-
const negate = this._dir.value === 'rtl' ? '-1' : '1';
762+
const negate = dir === 'rtl' ? '-1' : '1';
749763
const prefixWidth = `${iconPrefixContainerWidth + textPrefixContainerWidth}px`;
750764
const labelOffset = `var(--mat-mdc-form-field-label-offset-x, 0px)`;
751765
const labelHorizontalOffset = `calc(${negate} * (${prefixWidth} + ${labelOffset}))`;

0 commit comments

Comments
 (0)