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
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
@@ -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';
1 change: 1 addition & 0 deletions goldens/material/input/index.api.md
Original file line number Diff line number Diff line change
@@ -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';

34 changes: 28 additions & 6 deletions src/material/form-field/directives/notched-outline.ts
Original file line number Diff line number Diff line change
@@ -13,6 +13,8 @@ import {
ElementRef,
Input,
NgZone,
OnChanges,
SimpleChanges,
ViewChild,
ViewEncapsulation,
inject,
@@ -36,21 +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<HTMLElement>;

ngAfterViewInit(): void {
const element = this._elementRef.nativeElement;
const label = element.querySelector<HTMLElement>('.mdc-floating-label');
/** Gets the HTML element for the floating label. */
get element(): HTMLElement {
return this._elementRef.nativeElement;
}

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

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

if (typeof requestAnimationFrame === 'function') {
label.style.transitionDuration = '0s';
@@ -59,7 +70,18 @@ export class MatFormFieldNotchedOutline implements AfterViewInit {
});
}
} else {
element.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');
}
}

19 changes: 11 additions & 8 deletions src/material/form-field/form-field.html
Original file line number Diff line number Diff line change
@@ -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>
}
@@ -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>
40 changes: 40 additions & 0 deletions src/material/input/input.spec.ts
Original file line number Diff line number Diff line change
@@ -29,6 +29,7 @@ import {
} from '../form-field';
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', () => {
beforeEach(() => {
@@ -625,6 +626,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 = TestBed.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(() => {
const fixture = TestBed.createComponent(MatInputWithSubscriptAndAriaDescribedBy);

@@ -2170,6 +2194,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>
`,
imports: [MatInputModule],
})
class MatInputOutlineWithConditionalLabel {
@ViewChild(MatFormField) formField: MatFormField;
showLabel: boolean = false;
}

@Component({
template: `
<mat-form-field [subscriptSizing]="sizing">
Loading
Oops, something went wrong.