From b93e0fd1048a615446a61b25b26897cafbb92097 Mon Sep 17 00:00:00 2001 From: Jullierme Barros Date: Fri, 19 Jul 2024 10:50:12 -0300 Subject: [PATCH] fix(material/form-field): hiding a label after it has been shown leaves a blank space There is a method _shouldLabelFloat that determines if the label should float. A check was added `_hasFloatingLabel` to see if a floating label exists before deciding whether the label should float. Examples were added at the end of the input-demo file, where you can see inputs without labels (both fixed and dynamic). Removing the solution also allows you to simulate the described error. Unit tests were added to validate the solution. Fixes #29401 --- src/dev-app/input/input-demo.html | 35 ++++++++++++ src/dev-app/input/input-demo.ts | 8 ++- src/material/form-field/form-field.ts | 5 +- src/material/input/input.spec.ts | 81 ++++++++++++++++++++++++++- 4 files changed, 126 insertions(+), 3 deletions(-) diff --git a/src/dev-app/input/input-demo.html b/src/dev-app/input/input-demo.html index d41d02055512..3fbdf726be63 100644 --- a/src/dev-app/input/input-demo.html +++ b/src/dev-app/input/input-demo.html @@ -880,6 +880,41 @@

Custom control

+ + Without label + +

Label removed

+

+ + @if (hasLabel$ | async){ + My input + } + + +

+

+ + @if (hasLabel$ | async){ + My input + } + + +

+

No defined label

+

+ + + +

+

+ + + +

+
+
+ + diff --git a/src/dev-app/input/input-demo.ts b/src/dev-app/input/input-demo.ts index 36caf31c80e0..49d5af853834 100644 --- a/src/dev-app/input/input-demo.ts +++ b/src/dev-app/input/input-demo.ts @@ -29,6 +29,7 @@ import {MatIconModule} from '@angular/material/icon'; import {MatTabsModule} from '@angular/material/tabs'; import {MatToolbarModule} from '@angular/material/toolbar'; import {MatTooltipModule} from '@angular/material/tooltip'; +import {BehaviorSubject} from 'rxjs'; let max = 5; @@ -100,8 +101,13 @@ export class InputDemo { fillAppearance: string; outlineAppearance: string; + hasLabel$ = new BehaviorSubject(true); + constructor() { - setTimeout(() => this.delayedFormControl.setValue('hello'), 100); + setTimeout(() => { + this.delayedFormControl.setValue('hello'); + this.hasLabel$.next(false); + }, 100); } addABunch(n: number) { diff --git a/src/material/form-field/form-field.ts b/src/material/form-field/form-field.ts index 13f6f13cab70..914f89a32d2e 100644 --- a/src/material/form-field/form-field.ts +++ b/src/material/form-field/form-field.ts @@ -555,7 +555,10 @@ export class MatFormField _hasFloatingLabel = computed(() => !!this._labelChild()); - _shouldLabelFloat() { + _shouldLabelFloat(): boolean { + if (!this._hasFloatingLabel()) { + return false; + } return this._control.shouldLabelFloat || this._shouldAlwaysFloat(); } diff --git a/src/material/input/input.spec.ts b/src/material/input/input.spec.ts index 7e51bfa7aa8b..4158c0162001 100644 --- a/src/material/input/input.spec.ts +++ b/src/material/input/input.spec.ts @@ -157,7 +157,8 @@ describe('MatMdcInput without forms', () => { fixture.detectChanges(); expect(formField._control.empty).toBe(false); - expect(formField._shouldLabelFloat()).toBe(true); + // should not float label if there is no label + expect(formField._shouldLabelFloat()).toBe(false); })); it('should not be empty when the value set before view init', fakeAsync(() => { @@ -1531,6 +1532,62 @@ describe('MatFormField default options', () => { ).toBe(true); }); }); +describe('MatFormField without label', () => { + it('should not float the label when no label is defined.', () => { + let fixture = createComponent(MatInputWithoutDefinedLabel); + fixture.detectChanges(); + + const inputEl = fixture.debugElement.query(By.css('input'))!; + const formField = fixture.debugElement.query(By.directive(MatFormField))! + .componentInstance as MatFormField; + + // Update the value of the input and set focus. + inputEl.nativeElement.value = 'Text'; + fixture.detectChanges(); + + // should not float label if there is no label + expect(formField._shouldLabelFloat()).toBe(false); + }); + + it('should not float the label when the label is removed after it has been shown', () => { + let fixture = createComponent(MatInputWithCondictionalLabel); + fixture.detectChanges(); + const inputEl = fixture.debugElement.query(By.css('input'))!; + const formField = fixture.debugElement.query(By.directive(MatFormField))! + .componentInstance as MatFormField; + + // initially, label is present + expect(fixture.nativeElement.querySelector('label')).not.toBeNull(); + + // removing label after it has been shown + fixture.componentInstance.hasLabel = false; + inputEl.nativeElement.value = 'Text'; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + + // now expected to not have a label + expect(fixture.nativeElement.querySelector('label')).toBeNull(); + // should not float label since there is no label + expect(formField._shouldLabelFloat()).toBe(false); + }); + + it('should float the label when the label is not removed', () => { + let fixture = createComponent(MatInputWithCondictionalLabel); + fixture.detectChanges(); + const inputEl = fixture.debugElement.query(By.css('input'))!; + const formField = fixture.debugElement.query(By.directive(MatFormField))! + .componentInstance as MatFormField; + + inputEl.nativeElement.value = 'Text'; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + + // Expected to have a label + expect(fixture.nativeElement.querySelector('label')).not.toBeNull(); + // should float label since there is a label + expect(formField._shouldLabelFloat()).toBe(true); + }); +}); function configureTestingModule( component: Type, @@ -1787,6 +1844,28 @@ class MatInputWithDynamicLabel { shouldFloat: 'always' | 'auto' = 'always'; } +@Component({ + template: ` + + + + `, +}) +class MatInputWithoutDefinedLabel {} + +@Component({ + template: ` + + @if (hasLabel) { + Label + } + + `, +}) +class MatInputWithCondictionalLabel { + hasLabel = true; +} + @Component({ template: `