Skip to content
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
145 changes: 73 additions & 72 deletions src/material/form-field/directives/line-ripple.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,73 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import {Directive, ElementRef, NgZone, OnDestroy, Renderer2, inject} from '@angular/core';

/** Class added when the line ripple is active. */
const ACTIVATE_CLASS = 'mdc-line-ripple--active';

/** Class added when the line ripple is being deactivated. */
const DEACTIVATING_CLASS = 'mdc-line-ripple--deactivating';

/**
* Internal directive that creates an instance of the MDC line-ripple component. Using a
* directive allows us to conditionally render a line-ripple in the template without having
* to manually create and destroy the `MDCLineRipple` component whenever the condition changes.
*
* The directive sets up the styles for the line-ripple and provides an API for activating
* and deactivating the line-ripple.
*/
@Directive({
selector: 'div[matFormFieldLineRipple]',
host: {
'class': 'mdc-line-ripple',
},
})
export class MatFormFieldLineRipple implements OnDestroy {
private _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
private _cleanupTransitionEnd!: () => void;

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

constructor() {
const ngZone = inject(NgZone);
const renderer = inject(Renderer2);

ngZone.runOutsideAngular(() => {
this._cleanupTransitionEnd = renderer.listen(
this._elementRef.nativeElement,
'transitionend',
this._handleTransitionEnd,
);
});
}

activate() {
const classList = this._elementRef.nativeElement.classList;
classList.remove(DEACTIVATING_CLASS);
classList.add(ACTIVATE_CLASS);
}

deactivate() {
this._elementRef.nativeElement.classList.add(DEACTIVATING_CLASS);
}

private _handleTransitionEnd = (event: TransitionEvent) => {
const classList = this._elementRef.nativeElement.classList;
const isDeactivating = classList.contains(DEACTIVATING_CLASS);

if (event.propertyName === 'opacity' && isDeactivating) {
classList.remove(ACTIVATE_CLASS, DEACTIVATING_CLASS);
}
};

ngOnDestroy() {
this._cleanupTransitionEnd();
}
}
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import {Directive, ElementRef, NgZone, OnDestroy, Renderer2, inject} from '@angular/core';

/** Class added when the line ripple is active. */
const ACTIVATE_CLASS = 'mdc-line-ripple--active';

/** Class added when the line ripple is being deactivated. */
const DEACTIVATING_CLASS = 'mdc-line-ripple--deactivating';

/**
* Internal directive that creates an instance of the MDC line-ripple component. Using a
* directive allows us to conditionally render a line-ripple in the template without having
* to manually create and destroy the `MDCLineRipple` component whenever the condition changes.
*
* The directive sets up the styles for the line-ripple and provides an API for activating
* and deactivating the line-ripple.
*/
@Directive({
selector: 'div[matFormFieldLineRipple]',
host: {
'class': 'mdc-line-ripple',
'aria-hidden': 'true',
},
})
export class MatFormFieldLineRipple implements OnDestroy {
private _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
private _cleanupTransitionEnd!: () => void;

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

constructor() {
const ngZone = inject(NgZone);
const renderer = inject(Renderer2);

ngZone.runOutsideAngular(() => {
this._cleanupTransitionEnd = renderer.listen(
this._elementRef.nativeElement,
'transitionend',
this._handleTransitionEnd,
);
});
}

activate() {
const classList = this._elementRef.nativeElement.classList;
classList.remove(DEACTIVATING_CLASS);
classList.add(ACTIVATE_CLASS);
}

deactivate() {
this._elementRef.nativeElement.classList.add(DEACTIVATING_CLASS);
}

private _handleTransitionEnd = (event: TransitionEvent) => {
const classList = this._elementRef.nativeElement.classList;
const isDeactivating = classList.contains(DEACTIVATING_CLASS);

if (event.propertyName === 'opacity' && isDeactivating) {
classList.remove(ACTIVATE_CLASS, DEACTIVATING_CLASS);
}
};

ngOnDestroy() {
this._cleanupTransitionEnd();
}
}
244 changes: 122 additions & 122 deletions src/material/form-field/form-field.html
Original file line number Diff line number Diff line change
@@ -1,122 +1,122 @@
<ng-template #labelTemplate>
<!--
MDC recommends that the text-field is a `<label>` element. This rather complicates the
setup because it would require every form-field control to explicitly set `aria-labelledby`.
This is because the `<label>` itself contains more than the actual label (e.g. prefix, suffix
or other projected content), and screen readers could potentially read out undesired content.
Excluding elements from being printed out requires them to be marked with `aria-hidden`, or
the form control is set to a scoped element for the label (using `aria-labelledby`). Both of
these options seem to complicate the setup because we know exactly what content is rendered
as part of the label, and we don't want to spend resources on walking through projected content
to set `aria-hidden`. Nor do we want to set `aria-labelledby` on every form control if we could
simply link the label to the control using the label `for` attribute.
-->
@if (_hasFloatingLabel()) {
<label
matFormFieldFloatingLabel
[floating]="_shouldLabelFloat()"
[monitorResize]="_hasOutline()"
[id]="_labelId"
[attr.for]="_control.disableAutomaticLabeling ? null : _control.id"
>
<ng-content select="mat-label"></ng-content>
<!--
We set the required marker as a separate element, in order to make it easier to target if
apps want to override it and to be able to set `aria-hidden` so that screen readers don't
pick it up.
-->
@if (!hideRequiredMarker && _control.required) {
<span
aria-hidden="true"
class="mat-mdc-form-field-required-marker mdc-floating-label--required"
></span>
}
</label>
}
</ng-template>

<div
class="mat-mdc-text-field-wrapper mdc-text-field"
#textField
[class.mdc-text-field--filled]="!_hasOutline()"
[class.mdc-text-field--outlined]="_hasOutline()"
[class.mdc-text-field--no-label]="!_hasFloatingLabel()"
[class.mdc-text-field--disabled]="_control.disabled"
[class.mdc-text-field--invalid]="_control.errorState"
(click)="_control.onContainerClick($event)"
>
@if (!_hasOutline() && !_control.disabled) {
<div class="mat-mdc-form-field-focus-overlay"></div>
}
<div class="mat-mdc-form-field-flex">
@if (_hasOutline()) {
<div matFormFieldNotchedOutline [matFormFieldNotchedOutlineOpen]="_shouldLabelFloat()">
@if (!_forceDisplayInfixLabel()) {
<ng-template [ngTemplateOutlet]="labelTemplate"></ng-template>
}
</div>
}

@if (_hasIconPrefix) {
<div class="mat-mdc-form-field-icon-prefix" #iconPrefixContainer>
<ng-content select="[matPrefix], [matIconPrefix]"></ng-content>
</div>
}

@if (_hasTextPrefix) {
<div class="mat-mdc-form-field-text-prefix" #textPrefixContainer>
<ng-content select="[matTextPrefix]"></ng-content>
</div>
}

<div class="mat-mdc-form-field-infix">
@if (!_hasOutline() || _forceDisplayInfixLabel()) {
<ng-template [ngTemplateOutlet]="labelTemplate"></ng-template>
}

<ng-content></ng-content>
</div>

@if (_hasTextSuffix) {
<div class="mat-mdc-form-field-text-suffix" #textSuffixContainer>
<ng-content select="[matTextSuffix]"></ng-content>
</div>
}

@if (_hasIconSuffix) {
<div class="mat-mdc-form-field-icon-suffix" #iconSuffixContainer>
<ng-content select="[matSuffix], [matIconSuffix]"></ng-content>
</div>
}
</div>

@if (!_hasOutline()) {
<div matFormFieldLineRipple></div>
}
</div>

<div aria-atomic="true" aria-live="polite"
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();

@switch (subscriptMessageType) {
@case ('error') {
<div class="mat-mdc-form-field-error-wrapper">
<ng-content select="mat-error, [matError]"></ng-content>
</div>
}

@case ('hint') {
<div class="mat-mdc-form-field-hint-wrapper">
@if (hintLabel) {
<mat-hint [id]="_hintLabelId">{{hintLabel}}</mat-hint>
}
<ng-content select="mat-hint:not([align='end'])"></ng-content>
<div class="mat-mdc-form-field-hint-spacer"></div>
<ng-content select="mat-hint[align='end']"></ng-content>
</div>
}
}
</div>
<ng-template #labelTemplate>
<!--
MDC recommends that the text-field is a `<label>` element. This rather complicates the
setup because it would require every form-field control to explicitly set `aria-labelledby`.
This is because the `<label>` itself contains more than the actual label (e.g. prefix, suffix
or other projected content), and screen readers could potentially read out undesired content.
Excluding elements from being printed out requires them to be marked with `aria-hidden`, or
the form control is set to a scoped element for the label (using `aria-labelledby`). Both of
these options seem to complicate the setup because we know exactly what content is rendered
as part of the label, and we don't want to spend resources on walking through projected content
to set `aria-hidden`. Nor do we want to set `aria-labelledby` on every form control if we could
simply link the label to the control using the label `for` attribute.
-->
@if (_hasFloatingLabel()) {
<label
matFormFieldFloatingLabel
[floating]="_shouldLabelFloat()"
[monitorResize]="_hasOutline()"
[id]="_labelId"
[attr.for]="_control.disableAutomaticLabeling ? null : _control.id"
>
<ng-content select="mat-label"></ng-content>
<!--
We set the required marker as a separate element, in order to make it easier to target if
apps want to override it and to be able to set `aria-hidden` so that screen readers don't
pick it up.
-->
@if (!hideRequiredMarker && _control.required) {
<span
aria-hidden="true"
class="mat-mdc-form-field-required-marker mdc-floating-label--required"
></span>
}
</label>
}
</ng-template>
<div
class="mat-mdc-text-field-wrapper mdc-text-field"
#textField
[class.mdc-text-field--filled]="!_hasOutline()"
[class.mdc-text-field--outlined]="_hasOutline()"
[class.mdc-text-field--no-label]="!_hasFloatingLabel()"
[class.mdc-text-field--disabled]="_control.disabled"
[class.mdc-text-field--invalid]="_control.errorState"
(click)="_control.onContainerClick($event)"
>
@if (!_hasOutline() && !_control.disabled) {
<div class="mat-mdc-form-field-focus-overlay" aria-hidden="true"></div>
}
<div class="mat-mdc-form-field-flex">
@if (_hasOutline()) {
<div matFormFieldNotchedOutline [matFormFieldNotchedOutlineOpen]="_shouldLabelFloat()">
@if (!_forceDisplayInfixLabel()) {
<ng-template [ngTemplateOutlet]="labelTemplate"></ng-template>
}
</div>
}
@if (_hasIconPrefix) {
<div class="mat-mdc-form-field-icon-prefix" #iconPrefixContainer>
<ng-content select="[matPrefix], [matIconPrefix]"></ng-content>
</div>
}
@if (_hasTextPrefix) {
<div class="mat-mdc-form-field-text-prefix" #textPrefixContainer>
<ng-content select="[matTextPrefix]"></ng-content>
</div>
}
<div class="mat-mdc-form-field-infix">
@if (!_hasOutline() || _forceDisplayInfixLabel()) {
<ng-template [ngTemplateOutlet]="labelTemplate"></ng-template>
}
<ng-content></ng-content>
</div>
@if (_hasTextSuffix) {
<div class="mat-mdc-form-field-text-suffix" #textSuffixContainer>
<ng-content select="[matTextSuffix]"></ng-content>
</div>
}
@if (_hasIconSuffix) {
<div class="mat-mdc-form-field-icon-suffix" #iconSuffixContainer>
<ng-content select="[matSuffix], [matIconSuffix]"></ng-content>
</div>
}
</div>
@if (!_hasOutline()) {
<div matFormFieldLineRipple></div>
}
</div>
<div aria-atomic="true" aria-live="polite"
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();
@switch (subscriptMessageType) {
@case ('error') {
<div class="mat-mdc-form-field-error-wrapper">
<ng-content select="mat-error, [matError]"></ng-content>
</div>
}
@case ('hint') {
<div class="mat-mdc-form-field-hint-wrapper">
@if (hintLabel) {
<mat-hint [id]="_hintLabelId">{{hintLabel}}</mat-hint>
}
<ng-content select="mat-hint:not([align='end'])"></ng-content>
<div class="mat-mdc-form-field-hint-spacer" aria-hidden="true"></div>
<ng-content select="mat-hint[align='end']"></ng-content>
</div>
}
}
</div>