Skip to content

Commit 2f113b6

Browse files
committed
WIP: attempt to use ResizeObserver for layout offset calculation
1 parent d4cccb7 commit 2f113b6

File tree

4 files changed

+61
-42
lines changed

4 files changed

+61
-42
lines changed

src/cdk/bidi/dir.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {Directive, Output, Input, EventEmitter, AfterContentInit, OnDestroy} from '@angular/core';
9+
import {
10+
AfterContentInit,
11+
Directive,
12+
EventEmitter,
13+
Input,
14+
OnDestroy,
15+
Output,
16+
signal,
17+
} from '@angular/core';
1018

1119
import {Direction, Directionality, _resolveDirectionality} from './directionality';
1220

@@ -23,9 +31,6 @@ import {Direction, Directionality, _resolveDirectionality} from './directionalit
2331
exportAs: 'dir',
2432
})
2533
export class Dir implements Directionality, AfterContentInit, OnDestroy {
26-
/** Normalized direction that accounts for invalid/unsupported values. */
27-
private _dir: Direction = 'ltr';
28-
2934
/** Whether the `value` has been set to its initial value. */
3035
private _isInitialized: boolean = false;
3136

@@ -38,19 +43,19 @@ export class Dir implements Directionality, AfterContentInit, OnDestroy {
3843
/** @docs-private */
3944
@Input()
4045
get dir(): Direction {
41-
return this._dir;
46+
return this.valueSignal();
4247
}
4348
set dir(value: Direction | 'auto') {
44-
const previousValue = this._dir;
49+
const previousValue = this.valueSignal();
4550

4651
// Note: `_resolveDirectionality` resolves the language based on the browser's language,
4752
// whereas the browser does it based on the content of the element. Since doing so based
4853
// on the content can be expensive, for now we're doing the simpler matching.
49-
this._dir = _resolveDirectionality(value);
54+
this.valueSignal.set(_resolveDirectionality(value));
5055
this._rawDir = value;
5156

52-
if (previousValue !== this._dir && this._isInitialized) {
53-
this.change.emit(this._dir);
57+
if (previousValue !== this.valueSignal() && this._isInitialized) {
58+
this.change.emit(this.valueSignal());
5459
}
5560
}
5661

@@ -59,6 +64,8 @@ export class Dir implements Directionality, AfterContentInit, OnDestroy {
5964
return this.dir;
6065
}
6166

67+
readonly valueSignal = signal<Direction>('ltr');
68+
6269
/** Initialize once default value has been set. */
6370
ngAfterContentInit() {
6471
this._isInitialized = true;

src/cdk/bidi/directionality.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {EventEmitter, Injectable, OnDestroy, inject} from '@angular/core';
9+
import {EventEmitter, Injectable, OnDestroy, inject, signal} from '@angular/core';
1010
import {DIR_DOCUMENT} from './dir-document-token';
1111

1212
export type Direction = 'ltr' | 'rtl';
@@ -33,7 +33,14 @@ export function _resolveDirectionality(rawValue: string): Direction {
3333
@Injectable({providedIn: 'root'})
3434
export class Directionality implements OnDestroy {
3535
/** The current 'ltr' or 'rtl' value. */
36-
readonly value: Direction = 'ltr';
36+
get value() {
37+
return this.valueSignal();
38+
}
39+
40+
/**
41+
* The current 'ltr' or 'rtl' value.
42+
*/
43+
readonly valueSignal = signal<Direction>('ltr');
3744

3845
/** Stream that emits whenever the 'ltr' / 'rtl' state changes. */
3946
readonly change = new EventEmitter<Direction>();
@@ -46,7 +53,7 @@ export class Directionality implements OnDestroy {
4653
if (_document) {
4754
const bodyDir = _document.body ? _document.body.dir : null;
4855
const htmlDir = _document.documentElement ? _document.documentElement.dir : null;
49-
this.value = _resolveDirectionality(bodyDir || htmlDir || 'ltr');
56+
this.valueSignal.set(_resolveDirectionality(bodyDir || htmlDir || 'ltr'));
5057
}
5158
}
5259

src/dev-app/dev-app/dev-app-directionality.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,21 @@
77
*/
88

99
import {Direction, Directionality} from '@angular/cdk/bidi';
10-
import {EventEmitter, Injectable, OnDestroy} from '@angular/core';
10+
import {EventEmitter, Injectable, OnDestroy, signal} from '@angular/core';
1111

1212
@Injectable()
1313
export class DevAppDirectionality implements Directionality, OnDestroy {
1414
readonly change = new EventEmitter<Direction>();
1515

1616
get value(): Direction {
17-
return this._value;
17+
return this.valueSignal();
1818
}
1919
set value(value: Direction) {
20-
this._value = value;
20+
this.valueSignal.set(value);
2121
this.change.next(value);
2222
}
23-
private _value: Direction = 'ltr';
23+
24+
valueSignal = signal<Direction>('ltr');
2425

2526
ngOnDestroy() {
2627
this.change.complete();

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

Lines changed: 30 additions & 26 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,16 @@ import {
2728
QueryList,
2829
ViewChild,
2930
ViewEncapsulation,
30-
afterRender,
31+
afterRenderEffect,
3132
computed,
3233
contentChild,
3334
inject,
35+
signal,
3436
} from '@angular/core';
3537
import {AbstractControlDirective, ValidatorFn} from '@angular/forms';
36-
import {_animationsDisabled, ThemePalette} from '../core';
37-
import {_IdGenerator} from '@angular/cdk/a11y';
3838
import {Subject, Subscription, merge} from 'rxjs';
39-
import {map, pairwise, takeUntil, filter, startWith} from 'rxjs/operators';
39+
import {filter, map, pairwise, startWith, takeUntil} from 'rxjs/operators';
40+
import {ThemePalette, _animationsDisabled} from '../core';
4041
import {MAT_ERROR, MatError} from './directives/error';
4142
import {
4243
FLOATING_LABEL_PARENT,
@@ -250,10 +251,9 @@ export class MatFormField
250251
/** The form field appearance style. */
251252
@Input()
252253
get appearance(): MatFormFieldAppearance {
253-
return this._appearance;
254+
return this._appearanceSignal();
254255
}
255256
set appearance(value: MatFormFieldAppearance) {
256-
const oldValue = this._appearance;
257257
const newAppearance = value || this._defaults?.appearance || DEFAULT_APPEARANCE;
258258
if (typeof ngDevMode === 'undefined' || ngDevMode) {
259259
if (newAppearance !== 'fill' && newAppearance !== 'outline') {
@@ -262,15 +262,9 @@ export class MatFormField
262262
);
263263
}
264264
}
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-
}
265+
this._appearanceSignal.set(newAppearance);
272266
}
273-
private _appearance: MatFormFieldAppearance = DEFAULT_APPEARANCE;
267+
private _appearanceSignal = signal(DEFAULT_APPEARANCE);
274268

275269
/**
276270
* Whether the form field should reserve space for one line of hint/error text (default)
@@ -319,7 +313,6 @@ export class MatFormField
319313
private _destroyed = new Subject<void>();
320314
private _isFocused: boolean | null = null;
321315
private _explicitFormFieldControl: MatFormFieldControl<any>;
322-
private _needsOutlineLabelOffsetUpdate = false;
323316
private _previousControl: MatFormFieldControl<unknown> | null = null;
324317
private _previousControlValidatorFn: ValidatorFn | null = null;
325318
private _stateChanges: Subscription | undefined;
@@ -399,6 +392,7 @@ export class MatFormField
399392
}
400393

401394
ngOnDestroy() {
395+
this._outlineLabelOffsetResizeObserver.disconnect();
402396
this._stateChanges?.unsubscribe();
403397
this._valueChanges?.unsubscribe();
404398
this._describedByChanges?.unsubscribe();
@@ -546,6 +540,10 @@ export class MatFormField
546540
);
547541
}
548542

543+
private _outlineLabelOffsetResizeObserver = new ResizeObserver(() =>
544+
this._updateOutlineLabelOffset(),
545+
);
546+
549547
/**
550548
* The floating label in the docked state needs to account for prefixes. The horizontal offset
551549
* is calculated whenever the appearance changes to `outline`, the prefixes change, or when the
@@ -554,26 +552,33 @@ export class MatFormField
554552
*/
555553
private _initializeOutlineLabelOffsetSubscriptions() {
556554
// 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-
560555
// TODO(mmalerba): Split this into separate `afterRender` calls using the `EarlyRead` and
561556
// `Write` phases.
562-
afterRender(
557+
afterRenderEffect(
563558
() => {
564-
if (this._needsOutlineLabelOffsetUpdate) {
565-
this._needsOutlineLabelOffsetUpdate = false;
559+
if (this._appearanceSignal() === 'outline') {
560+
// Trigger effect on directionality changes.
561+
this._dir.valueSignal();
562+
const prefixSuffixEls = [
563+
this._textPrefixContainer,
564+
this._iconPrefixContainer,
565+
this._textSuffixContainer,
566+
this._iconSuffixContainer,
567+
]
568+
.filter(e => e != null)
569+
.map(e => e.nativeElement);
570+
for (const el of prefixSuffixEls) {
571+
this._outlineLabelOffsetResizeObserver.observe(el, {box: 'border-box'});
572+
}
566573
this._updateOutlineLabelOffset();
574+
} else {
575+
this._outlineLabelOffsetResizeObserver.disconnect();
567576
}
568577
},
569578
{
570579
injector: this._injector,
571580
},
572581
);
573-
574-
this._dir.change
575-
.pipe(takeUntil(this._destroyed))
576-
.subscribe(() => (this._needsOutlineLabelOffsetUpdate = true));
577582
}
578583

579584
/** Whether the floating label should always float or not. */
@@ -732,7 +737,6 @@ export class MatFormField
732737
// If the form field is not attached to the DOM yet (e.g. in a tab), we defer
733738
// the label offset update until the zone stabilizes.
734739
if (!this._isAttachedToDom()) {
735-
this._needsOutlineLabelOffsetUpdate = true;
736740
return;
737741
}
738742
const iconPrefixContainer = this._iconPrefixContainer?.nativeElement;

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy