diff --git a/src/material/form-field/_form-field-subscript.scss b/src/material/form-field/_form-field-subscript.scss index 2f17b7950980..2e3da86d9899 100644 --- a/src/material/form-field/_form-field-subscript.scss +++ b/src/material/form-field/_form-field-subscript.scss @@ -3,6 +3,18 @@ @use '../core/tokens/token-utils'; @mixin private-form-field-subscript() { + @keyframes _mat-form-field-subscript-animation { + from { + opacity: 0; + transform: translateY(-5px); + } + + to { + opacity: 1; + transform: translateY(0); + } + } + // Wrapper for the hints and error messages. .mat-mdc-form-field-subscript-wrapper { box-sizing: border-box; @@ -17,6 +29,12 @@ left: 0; right: 0; padding: 0 16px; + opacity: 1; + transform: translateY(0); + + // Note: animation-duration gets set when animations are enabled. + // This allows us to skip the animation on init. + animation: _mat-form-field-subscript-animation 0ms cubic-bezier(0.55, 0, 0.55, 0.2); } .mat-mdc-form-field-subscript-dynamic-size { diff --git a/src/material/form-field/_mdc-text-field-structure.scss b/src/material/form-field/_mdc-text-field-structure.scss index d28b847924d2..3f00d56b11ab 100644 --- a/src/material/form-field/_mdc-text-field-structure.scss +++ b/src/material/form-field/_mdc-text-field-structure.scss @@ -611,8 +611,11 @@ } .mdc-line-ripple::after { - transition: - transform 180ms $timing-curve, - opacity 180ms $timing-curve; + transition: transform 180ms $timing-curve, opacity 180ms $timing-curve; + } + + .mat-mdc-form-field-hint-wrapper, + .mat-mdc-form-field-error-wrapper { + animation-duration: 300ms; } } diff --git a/src/material/form-field/form-field-animations.ts b/src/material/form-field/form-field-animations.ts index a28b6223d2fb..4f0d033f902a 100644 --- a/src/material/form-field/form-field-animations.ts +++ b/src/material/form-field/form-field-animations.ts @@ -17,6 +17,8 @@ import { /** * Animations used by the MatFormField. * @docs-private + * @deprecated No longer used, will be removed. + * @breaking-change 21.0.0 */ export const matFormFieldAnimations: { readonly transitionMessages: AnimationTriggerMetadata; diff --git a/src/material/form-field/form-field.html b/src/material/form-field/form-field.html index 0c62f7ca809f..f1a46075b46b 100644 --- a/src/material/form-field/form-field.html +++ b/src/material/form-field/form-field.html @@ -101,16 +101,13 @@ > @switch (_getDisplayedMessages()) { @case ('error') { -
+
} @case ('hint') { -
+
@if (hintLabel) { {{hintLabel}} } diff --git a/src/material/form-field/form-field.scss b/src/material/form-field/form-field.scss index c2787b1a1427..0dcbe41561c1 100644 --- a/src/material/form-field/form-field.scss +++ b/src/material/form-field/form-field.scss @@ -212,7 +212,7 @@ $_icon-prefix-infix-padding: 4px; // In order to make it possible for developers to disable animations for form-fields, // we only activate the animation styles if animations are not explicitly disabled. -.mat-mdc-form-field:not(.mat-form-field-no-animations) { +.mat-mdc-form-field.mat-form-field-animations-enabled { @include mdc-text-field-structure.private-text-field-animations; } diff --git a/src/material/form-field/form-field.ts b/src/material/form-field/form-field.ts index 7e871d0bd2bb..0ea473c78d41 100644 --- a/src/material/form-field/form-field.ts +++ b/src/material/form-field/form-field.ts @@ -23,6 +23,7 @@ import { InjectionToken, Injector, Input, + NgZone, OnDestroy, QueryList, ViewChild, @@ -49,7 +50,6 @@ 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, @@ -141,7 +141,6 @@ interface MatFormFieldControl extends _MatFormFieldControl {} 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()', @@ -153,7 +152,6 @@ interface MatFormFieldControl extends _MatFormFieldControl {} '[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()', @@ -191,10 +189,11 @@ export class MatFormField private _dir = inject(Directionality); private _platform = inject(Platform); private _idGenerator = inject(_IdGenerator); + private _ngZone = inject(NgZone); + private _injector = inject(Injector); 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; @@ -310,9 +309,6 @@ export class MatFormField // 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; @@ -329,8 +325,7 @@ export class MatFormField private _stateChanges: Subscription | undefined; private _valueChanges: Subscription | undefined; private _describedByChanges: Subscription | undefined; - - private _injector = inject(Injector); + protected readonly _animationsDisabled: boolean; constructor(...args: unknown[]); @@ -346,14 +341,24 @@ export class MatFormField this.color = defaults.color; } } + + this._animationsDisabled = inject(ANIMATION_MODULE_TYPE, {optional: true}) === 'NoopAnimations'; } 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'; + + if (!this._animationsDisabled) { + this._ngZone.runOutsideAngular(() => { + // Enable animations after a certain amount of time so that they don't run on init. + setTimeout(() => { + this._elementRef.nativeElement.classList.add('mat-form-field-animations-enabled'); + }, 300); + }); + } + // 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(); diff --git a/src/material/input/input.spec.ts b/src/material/input/input.spec.ts index 611cdc3320f2..3a778acbcee4 100644 --- a/src/material/input/input.spec.ts +++ b/src/material/input/input.spec.ts @@ -486,15 +486,6 @@ describe('MatMdcInput without forms', () => { expect(selectEl.disabled).toBe(true); })); - it('should add a class to the form-field if animations are disabled', () => { - configureTestingModule(MatInputWithId, {animations: false}); - const fixture = TestBed.createComponent(MatInputWithId); - fixture.detectChanges(); - - const formFieldEl = fixture.nativeElement.querySelector('.mat-mdc-form-field'); - expect(formFieldEl.classList).toContain('mat-form-field-no-animations'); - }); - it('should add a class to the form field if it has a native select', fakeAsync(() => { const fixture = createComponent(MatInputSelect); fixture.detectChanges(); diff --git a/src/material/select/select.scss b/src/material/select/select.scss index 2586a9f7b4a0..aeb1247b0f77 100644 --- a/src/material/select/select.scss +++ b/src/material/select/select.scss @@ -185,7 +185,7 @@ div.mat-mdc-select-panel { @include token-utils.create-token-slot(color, placeholder-text-color); } - .mat-form-field-no-animations &, + .mat-mdc-form-field:not(.mat-form-field-animations-enabled) &, ._mat-animation-noopable & { transition: none; } diff --git a/tools/public_api_guard/material/form-field.md b/tools/public_api_guard/material/form-field.md index f7efb046f473..539f205be870 100644 --- a/tools/public_api_guard/material/form-field.md +++ b/tools/public_api_guard/material/form-field.md @@ -65,7 +65,7 @@ export class MatFormField implements FloatingLabelParent, AfterContentInit, Afte constructor(...args: unknown[]); _animateAndLockLabel(): void; // (undocumented) - _animationMode: "NoopAnimations" | "BrowserAnimations" | null; + protected readonly _animationsDisabled: boolean; get appearance(): MatFormFieldAppearance; set appearance(value: MatFormFieldAppearance); color: ThemePalette; @@ -131,7 +131,6 @@ export class MatFormField implements FloatingLabelParent, AfterContentInit, Afte _shouldForward(prop: keyof AbstractControlDirective): boolean; // (undocumented) _shouldLabelFloat(): boolean; - _subscriptAnimationState: string; get subscriptSizing(): SubscriptSizing; set subscriptSizing(value: SubscriptSizing); // (undocumented) @@ -148,7 +147,7 @@ export class MatFormField implements FloatingLabelParent, AfterContentInit, Afte static ɵfac: i0.ɵɵFactoryDeclaration; } -// @public +// @public @deprecated export const matFormFieldAnimations: { readonly transitionMessages: AnimationTriggerMetadata; };