Skip to content

fix(material/form-field): add hasFloatingLabel input and update classes if mat-label is added and removed dynamically #30849

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions goldens/material/form-field/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import * as i2 from '@angular/cdk/observers';
import { InjectionToken } from '@angular/core';
import { NgControl } from '@angular/forms';
import { Observable } from 'rxjs';
import { OnChanges } from '@angular/core';
import { OnDestroy } from '@angular/core';
import { QueryList } from '@angular/core';
import { SimpleChanges } from '@angular/core';

// @public
export type FloatLabelType = 'always' | 'auto';
Expand Down
1 change: 1 addition & 0 deletions goldens/material/input/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { OnChanges } from '@angular/core';
import { OnDestroy } from '@angular/core';
import { Platform } from '@angular/cdk/platform';
import { QueryList } from '@angular/core';
import { SimpleChanges } from '@angular/core';
import { Subject } from 'rxjs';
import { WritableSignal } from '@angular/core';

Expand Down
29 changes: 25 additions & 4 deletions src/material/form-field/directives/notched-outline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
ElementRef,
Input,
NgZone,
OnChanges,
SimpleChanges,
ViewChild,
ViewEncapsulation,
inject,
Expand All @@ -36,22 +38,30 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
})
export class MatFormFieldNotchedOutline implements AfterViewInit {
export class MatFormFieldNotchedOutline implements AfterViewInit, OnChanges {
private _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
private _ngZone = inject(NgZone);

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

/** Whether the floating label is present. */
@Input('matFormFieldHasFloatingLabel') hasFloatingLabel: boolean = false;

@ViewChild('notch') _notch: ElementRef;

/** Gets the HTML element for the floating label. */
get element(): HTMLElement {
return this._elementRef.nativeElement;
}

constructor(...args: unknown[]);
constructor() {}

ngAfterViewInit(): void {
const label = this._elementRef.nativeElement.querySelector<HTMLElement>('.mdc-floating-label');
const label = this.element.querySelector<HTMLElement>('.mdc-floating-label');
if (label) {
this._elementRef.nativeElement.classList.add('mdc-notched-outline--upgraded');
this.element.classList.add('mdc-notched-outline--upgraded');

if (typeof requestAnimationFrame === 'function') {
label.style.transitionDuration = '0s';
Expand All @@ -60,7 +70,18 @@ export class MatFormFieldNotchedOutline implements AfterViewInit {
});
}
} else {
this._elementRef.nativeElement.classList.add('mdc-notched-outline--no-label');
this.element.classList.add('mdc-notched-outline--no-label');
}
}

ngOnChanges(changes: SimpleChanges) {
if (
changes['hasFloatingLabel'] &&
this.hasFloatingLabel &&
this.element.classList.contains('mdc-notched-outline--no-label')
) {
this.element.classList.add('mdc-notched-outline--upgraded');
this.element.classList.remove('mdc-notched-outline--no-label');
}
}

Expand Down
19 changes: 11 additions & 8 deletions src/material/form-field/form-field.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@
}
<div class="mat-mdc-form-field-flex">
@if (_hasOutline()) {
<div matFormFieldNotchedOutline [matFormFieldNotchedOutlineOpen]="_shouldLabelFloat()">
<div
matFormFieldNotchedOutline
[matFormFieldNotchedOutlineOpen]="_shouldLabelFloat()"
[matFormFieldHasFloatingLabel]="_hasFloatingLabel()">
@if (!_forceDisplayInfixLabel()) {
<ng-template [ngTemplateOutlet]="labelTemplate"></ng-template>
}
Expand Down Expand Up @@ -96,20 +99,20 @@
</div>

<div
class="mat-mdc-form-field-subscript-wrapper mat-mdc-form-field-bottom-align"
[class.mat-mdc-form-field-subscript-dynamic-size]="subscriptSizing === 'dynamic'"
>
class="mat-mdc-form-field-subscript-wrapper mat-mdc-form-field-bottom-align"
[class.mat-mdc-form-field-subscript-dynamic-size]="subscriptSizing === 'dynamic'">
@let subscriptMessageType = _getSubscriptMessageType();

<!--
Use a single permanent wrapper for both hints and errors so aria-live works correctly,
as having it appear post render will not consistently work. We also do not want to add
additional divs as it causes styling regressions.
-->
<div aria-atomic="true" aria-live="polite"
[class.mat-mdc-form-field-error-wrapper]="subscriptMessageType === 'error'"
[class.mat-mdc-form-field-hint-wrapper]="subscriptMessageType === 'hint'"
>
<div
aria-atomic="true"
aria-live="polite"
[class.mat-mdc-form-field-error-wrapper]="subscriptMessageType === 'error'"
[class.mat-mdc-form-field-hint-wrapper]="subscriptMessageType === 'hint'">
@switch (subscriptMessageType) {
@case ('error') {
<ng-content select="mat-error, [matError]"></ng-content>
Expand Down
40 changes: 40 additions & 0 deletions src/material/input/input.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
import {MatIconModule} from '../icon';
import {By} from '@angular/platform-browser';
import {MAT_INPUT_VALUE_ACCESSOR, MatInput, MatInputModule} from './index';
import {MatFormFieldNotchedOutline} from '../form-field/directives/notched-outline';

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

it('should show outline label correctly based on initial condition to false', fakeAsync(() => {
const fixture = createComponent(MatInputOutlineWithConditionalLabel);
fixture.detectChanges();
tick(16);

const notchedOutline: HTMLElement = fixture.debugElement.query(
By.directive(MatFormFieldNotchedOutline),
).nativeElement;

console.log('notchedOutline', notchedOutline.classList);

expect(notchedOutline.classList).toContain('mdc-notched-outline--no-label');
expect(notchedOutline.classList).not.toContain('mdc-notched-outline--upgraded');

fixture.componentInstance.showLabel = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
tick(16);

expect(notchedOutline.classList).not.toContain('mdc-notched-outline--no-label');
expect(notchedOutline.classList).toContain('mdc-notched-outline--upgraded');
}));

it('supports user binding to aria-describedby', fakeAsync(() => {
let fixture = createComponent(MatInputWithSubscriptAndAriaDescribedBy);

Expand Down Expand Up @@ -2178,6 +2202,22 @@ class MatInputWithAppearance {
appearance: MatFormFieldAppearance;
}

@Component({
template: `
<mat-form-field appearance="outline">
@if(showLabel) {
<mat-label>My Label</mat-label>
}
<input matInput placeholder="Placeholder">
</mat-form-field>
`,
standalone: false,
})
class MatInputOutlineWithConditionalLabel {
@ViewChild(MatFormField) formField: MatFormField;
showLabel: boolean = false;
}

@Component({
template: `
<mat-form-field [subscriptSizing]="sizing">
Expand Down
Loading