Skip to content

Commit ad897c4

Browse files
committed
fix(material/form-field): add hasFloatingLabel input and update classes if mat-label is added and removed dynamically
Currently, when `mat-Label` is added dynamically initially its not visible in DOM, this fix will add/remove classes for the same. Fixes angular#29939
1 parent 4e0ea8e commit ad897c4

File tree

5 files changed

+79
-12
lines changed

5 files changed

+79
-12
lines changed

goldens/material/form-field/index.api.md

+2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ import * as i2 from '@angular/cdk/observers';
1616
import { InjectionToken } from '@angular/core';
1717
import { NgControl } from '@angular/forms';
1818
import { Observable } from 'rxjs';
19+
import { OnChanges } from '@angular/core';
1920
import { OnDestroy } from '@angular/core';
2021
import { QueryList } from '@angular/core';
22+
import { SimpleChanges } from '@angular/core';
2123

2224
// @public
2325
export type FloatLabelType = 'always' | 'auto';

goldens/material/input/index.api.md

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { OnChanges } from '@angular/core';
2525
import { OnDestroy } from '@angular/core';
2626
import { Platform } from '@angular/cdk/platform';
2727
import { QueryList } from '@angular/core';
28+
import { SimpleChanges } from '@angular/core';
2829
import { Subject } from 'rxjs';
2930
import { WritableSignal } from '@angular/core';
3031

src/material/form-field/directives/notched-outline.ts

+25-4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {
1313
ElementRef,
1414
Input,
1515
NgZone,
16+
OnChanges,
17+
SimpleChanges,
1618
ViewChild,
1719
ViewEncapsulation,
1820
inject,
@@ -36,22 +38,30 @@ import {
3638
changeDetection: ChangeDetectionStrategy.OnPush,
3739
encapsulation: ViewEncapsulation.None,
3840
})
39-
export class MatFormFieldNotchedOutline implements AfterViewInit {
41+
export class MatFormFieldNotchedOutline implements AfterViewInit, OnChanges {
4042
private _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
4143
private _ngZone = inject(NgZone);
4244

4345
/** Whether the notch should be opened. */
4446
@Input('matFormFieldNotchedOutlineOpen') open: boolean = false;
4547

48+
/** Whether the floating label is present. */
49+
@Input('matFormFieldHasFloatingLabel') hasFloatingLabel: boolean = false;
50+
4651
@ViewChild('notch') _notch: ElementRef;
4752

53+
/** Gets the HTML element for the floating label. */
54+
get element(): HTMLElement {
55+
return this._elementRef.nativeElement;
56+
}
57+
4858
constructor(...args: unknown[]);
4959
constructor() {}
5060

5161
ngAfterViewInit(): void {
52-
const label = this._elementRef.nativeElement.querySelector<HTMLElement>('.mdc-floating-label');
62+
const label = this.element.querySelector<HTMLElement>('.mdc-floating-label');
5363
if (label) {
54-
this._elementRef.nativeElement.classList.add('mdc-notched-outline--upgraded');
64+
this.element.classList.add('mdc-notched-outline--upgraded');
5565

5666
if (typeof requestAnimationFrame === 'function') {
5767
label.style.transitionDuration = '0s';
@@ -60,7 +70,18 @@ export class MatFormFieldNotchedOutline implements AfterViewInit {
6070
});
6171
}
6272
} else {
63-
this._elementRef.nativeElement.classList.add('mdc-notched-outline--no-label');
73+
this.element.classList.add('mdc-notched-outline--no-label');
74+
}
75+
}
76+
77+
ngOnChanges(changes: SimpleChanges) {
78+
if (
79+
changes['hasFloatingLabel'] &&
80+
this.hasFloatingLabel &&
81+
this.element.classList.contains('mdc-notched-outline--no-label')
82+
) {
83+
this.element.classList.add('mdc-notched-outline--upgraded');
84+
this.element.classList.remove('mdc-notched-outline--no-label');
6485
}
6586
}
6687

src/material/form-field/form-field.html

+11-8
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@
5050
}
5151
<div class="mat-mdc-form-field-flex">
5252
@if (_hasOutline()) {
53-
<div matFormFieldNotchedOutline [matFormFieldNotchedOutlineOpen]="_shouldLabelFloat()">
53+
<div
54+
matFormFieldNotchedOutline
55+
[matFormFieldNotchedOutlineOpen]="_shouldLabelFloat()"
56+
[matFormFieldHasFloatingLabel]="_hasFloatingLabel()">
5457
@if (!_forceDisplayInfixLabel()) {
5558
<ng-template [ngTemplateOutlet]="labelTemplate"></ng-template>
5659
}
@@ -96,20 +99,20 @@
9699
</div>
97100

98101
<div
99-
class="mat-mdc-form-field-subscript-wrapper mat-mdc-form-field-bottom-align"
100-
[class.mat-mdc-form-field-subscript-dynamic-size]="subscriptSizing === 'dynamic'"
101-
>
102+
class="mat-mdc-form-field-subscript-wrapper mat-mdc-form-field-bottom-align"
103+
[class.mat-mdc-form-field-subscript-dynamic-size]="subscriptSizing === 'dynamic'">
102104
@let subscriptMessageType = _getSubscriptMessageType();
103105

104106
<!--
105107
Use a single permanent wrapper for both hints and errors so aria-live works correctly,
106108
as having it appear post render will not consistently work. We also do not want to add
107109
additional divs as it causes styling regressions.
108110
-->
109-
<div aria-atomic="true" aria-live="polite"
110-
[class.mat-mdc-form-field-error-wrapper]="subscriptMessageType === 'error'"
111-
[class.mat-mdc-form-field-hint-wrapper]="subscriptMessageType === 'hint'"
112-
>
111+
<div
112+
aria-atomic="true"
113+
aria-live="polite"
114+
[class.mat-mdc-form-field-error-wrapper]="subscriptMessageType === 'error'"
115+
[class.mat-mdc-form-field-hint-wrapper]="subscriptMessageType === 'hint'">
113116
@switch (subscriptMessageType) {
114117
@case ('error') {
115118
<ng-content select="mat-error, [matError]"></ng-content>

src/material/input/input.spec.ts

+40
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
import {MatIconModule} from '../icon';
3434
import {By} from '@angular/platform-browser';
3535
import {MAT_INPUT_VALUE_ACCESSOR, MatInput, MatInputModule} from './index';
36+
import {MatFormFieldNotchedOutline} from '../form-field/directives/notched-outline';
3637

3738
describe('MatMdcInput without forms', () => {
3839
it('should default to floating labels', fakeAsync(() => {
@@ -607,6 +608,29 @@ describe('MatMdcInput without forms', () => {
607608
expect(input.getAttribute('aria-describedby')).toBe(`initial ${hintId}`);
608609
}));
609610

611+
it('should show outline label correctly based on initial condition to false', fakeAsync(() => {
612+
const fixture = createComponent(MatInputOutlineWithConditionalLabel);
613+
fixture.detectChanges();
614+
tick(16);
615+
616+
const notchedOutline: HTMLElement = fixture.debugElement.query(
617+
By.directive(MatFormFieldNotchedOutline),
618+
).nativeElement;
619+
620+
console.log('notchedOutline', notchedOutline.classList);
621+
622+
expect(notchedOutline.classList).toContain('mdc-notched-outline--no-label');
623+
expect(notchedOutline.classList).not.toContain('mdc-notched-outline--upgraded');
624+
625+
fixture.componentInstance.showLabel = true;
626+
fixture.changeDetectorRef.markForCheck();
627+
fixture.detectChanges();
628+
tick(16);
629+
630+
expect(notchedOutline.classList).not.toContain('mdc-notched-outline--no-label');
631+
expect(notchedOutline.classList).toContain('mdc-notched-outline--upgraded');
632+
}));
633+
610634
it('supports user binding to aria-describedby', fakeAsync(() => {
611635
let fixture = createComponent(MatInputWithSubscriptAndAriaDescribedBy);
612636

@@ -2178,6 +2202,22 @@ class MatInputWithAppearance {
21782202
appearance: MatFormFieldAppearance;
21792203
}
21802204

2205+
@Component({
2206+
template: `
2207+
<mat-form-field appearance="outline">
2208+
@if(showLabel) {
2209+
<mat-label>My Label</mat-label>
2210+
}
2211+
<input matInput placeholder="Placeholder">
2212+
</mat-form-field>
2213+
`,
2214+
standalone: false,
2215+
})
2216+
class MatInputOutlineWithConditionalLabel {
2217+
@ViewChild(MatFormField) formField: MatFormField;
2218+
showLabel: boolean = false;
2219+
}
2220+
21812221
@Component({
21822222
template: `
21832223
<mat-form-field [subscriptSizing]="sizing">

0 commit comments

Comments
 (0)