From c2eb11d8a985e9c315c4683a9a7522208bba9b4c Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 20 May 2022 15:13:31 +0200 Subject: [PATCH] refactor(material-experimental/mdc-form-field): remove MDC adapter usage (#24945) Refactors the MDC form field so that it doesn't use any of MDC's adapters. --- .../mdc-form-field/BUILD.bazel | 2 - .../directives/floating-label.ts | 28 +++- .../mdc-form-field/directives/line-ripple.ts | 38 +++++- .../directives/notched-outline.html | 2 +- .../directives/notched-outline.ts | 64 +++------ .../mdc-form-field/form-field.ts | 121 ++---------------- 6 files changed, 90 insertions(+), 165 deletions(-) diff --git a/src/material-experimental/mdc-form-field/BUILD.bazel b/src/material-experimental/mdc-form-field/BUILD.bazel index 3f2a8e899c2d..cbbb6da0d847 100644 --- a/src/material-experimental/mdc-form-field/BUILD.bazel +++ b/src/material-experimental/mdc-form-field/BUILD.bazel @@ -22,8 +22,6 @@ ng_module( "//src/material-experimental/mdc-core", "//src/material/form-field", "@npm//@angular/forms", - "@npm//@material/line-ripple", - "@npm//@material/textfield", "@npm//rxjs", ], ) diff --git a/src/material-experimental/mdc-form-field/directives/floating-label.ts b/src/material-experimental/mdc-form-field/directives/floating-label.ts index bf4a43b494d5..b8dfb9ef91ba 100644 --- a/src/material-experimental/mdc-form-field/directives/floating-label.ts +++ b/src/material-experimental/mdc-form-field/directives/floating-label.ts @@ -7,7 +7,6 @@ */ import {Directive, ElementRef, Input} from '@angular/core'; -import {ponyfill} from '@material/dom'; /** * Internal directive that maintains a MDC floating label. This directive does not @@ -33,11 +32,11 @@ export class MatFormFieldFloatingLabel { /** Whether the label is floating. */ @Input() floating: boolean = false; - constructor(private _elementRef: ElementRef) {} + constructor(private _elementRef: ElementRef) {} /** Gets the width of the label. Used for the outline notch. */ getWidth(): number { - return ponyfill.estimateScrollWidth(this._elementRef.nativeElement); + return estimateScrollWidth(this._elementRef.nativeElement); } /** Gets the HTML element for the floating label. */ @@ -45,3 +44,26 @@ export class MatFormFieldFloatingLabel { return this._elementRef.nativeElement; } } + +/** + * Estimates the scroll width of an element. + * via https://github.com/material-components/material-components-web/blob/c0a11ef0d000a098fd0c372be8f12d6a99302855/packages/mdc-dom/ponyfill.ts + */ +function estimateScrollWidth(element: HTMLElement): number { + // Check the offsetParent. If the element inherits display: none from any + // parent, the offsetParent property will be null (see + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent). + // This check ensures we only clone the node when necessary. + const htmlEl = element as HTMLElement; + if (htmlEl.offsetParent !== null) { + return htmlEl.scrollWidth; + } + + const clone = htmlEl.cloneNode(true) as HTMLElement; + clone.style.setProperty('position', 'absolute'); + clone.style.setProperty('transform', 'translate(-9999px, -9999px)'); + document.documentElement.appendChild(clone); + const scrollWidth = clone.scrollWidth; + clone.remove(); + return scrollWidth; +} diff --git a/src/material-experimental/mdc-form-field/directives/line-ripple.ts b/src/material-experimental/mdc-form-field/directives/line-ripple.ts index 98a048ac4dff..c4f6c79303d4 100644 --- a/src/material-experimental/mdc-form-field/directives/line-ripple.ts +++ b/src/material-experimental/mdc-form-field/directives/line-ripple.ts @@ -6,8 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, ElementRef, OnDestroy} from '@angular/core'; -import {MDCLineRipple} from '@material/line-ripple'; +import {Directive, ElementRef, NgZone, OnDestroy} 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 @@ -23,12 +28,33 @@ import {MDCLineRipple} from '@material/line-ripple'; 'class': 'mdc-line-ripple', }, }) -export class MatFormFieldLineRipple extends MDCLineRipple implements OnDestroy { - constructor(elementRef: ElementRef) { - super(elementRef.nativeElement); +export class MatFormFieldLineRipple implements OnDestroy { + constructor(private _elementRef: ElementRef, ngZone: NgZone) { + ngZone.runOutsideAngular(() => { + _elementRef.nativeElement.addEventListener('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.destroy(); + this._elementRef.nativeElement.removeEventListener('transitionend', this._handleTransitionEnd); } } diff --git a/src/material-experimental/mdc-form-field/directives/notched-outline.html b/src/material-experimental/mdc-form-field/directives/notched-outline.html index 08b287042c07..76e5276145bb 100644 --- a/src/material-experimental/mdc-form-field/directives/notched-outline.html +++ b/src/material-experimental/mdc-form-field/directives/notched-outline.html @@ -1,5 +1,5 @@
-
+
diff --git a/src/material-experimental/mdc-form-field/directives/notched-outline.ts b/src/material-experimental/mdc-form-field/directives/notched-outline.ts index 9d09ca31fe4c..2270734aad67 100644 --- a/src/material-experimental/mdc-form-field/directives/notched-outline.ts +++ b/src/material-experimental/mdc-form-field/directives/notched-outline.ts @@ -6,24 +6,18 @@ * found in the LICENSE file at https://angular.io/license */ -import {Platform} from '@angular/cdk/platform'; import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, - OnChanges, - OnDestroy, + NgZone, ViewEncapsulation, } from '@angular/core'; -import {MDCNotchedOutline} from '@material/notched-outline'; /** - * Internal component that creates an instance of the MDC notched-outline component. Using - * a directive allows us to conditionally render a notched-outline in the template without - * having to manually create and destroy the `MDCNotchedOutline` component whenever the - * appearance changes. + * Internal component that creates an instance of the MDC notched-outline component. * * The component sets up the HTML structure and styles for the notched-outline. It provides * inputs to toggle the notch state and width. @@ -40,53 +34,37 @@ import {MDCNotchedOutline} from '@material/notched-outline'; changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, }) -export class MatFormFieldNotchedOutline implements AfterViewInit, OnChanges, OnDestroy { +export class MatFormFieldNotchedOutline implements AfterViewInit { /** Width of the notch. */ @Input('matFormFieldNotchedOutlineWidth') width: number = 0; /** Whether the notch should be opened. */ @Input('matFormFieldNotchedOutlineOpen') open: boolean = false; - /** Instance of the MDC notched outline. */ - private _mdcNotchedOutline: MDCNotchedOutline | null = null; + constructor(private _elementRef: ElementRef, private _ngZone: NgZone) {} - constructor(private _elementRef: ElementRef, private _platform: Platform) {} + ngAfterViewInit(): void { + const label = this._elementRef.nativeElement.querySelector('.mdc-floating-label'); + if (label) { + this._elementRef.nativeElement.classList.add('mdc-notched-outline--upgraded'); - ngAfterViewInit() { - // The notched outline cannot be attached in the server platform. It schedules tasks - // for the next browser animation frame and relies on element client rectangles to render - // the outline notch. To avoid failures on the server, we just do not initialize it, - // but the actual notched-outline styles will be still displayed. - if (this._platform.isBrowser) { - // The notch component relies on the view to be initialized. This means - // that we cannot extend from the "MDCNotchedOutline". - this._mdcNotchedOutline = MDCNotchedOutline.attachTo(this._elementRef.nativeElement); - } - // Initial sync in case state has been updated before view initialization. - this._syncNotchedOutlineState(); - } - - ngOnChanges() { - // Whenever the width, or the open state changes, sync the notched outline to be - // based on the new values. - this._syncNotchedOutlineState(); - } - - ngOnDestroy() { - if (this._mdcNotchedOutline !== null) { - this._mdcNotchedOutline.destroy(); + if (typeof requestAnimationFrame === 'function') { + label.style.transitionDuration = '0s'; + this._ngZone.runOutsideAngular(() => { + requestAnimationFrame(() => (label.style.transitionDuration = '')); + }); + } + } else { + this._elementRef.nativeElement.classList.add('mdc-notched-outline--no-label'); } } - /** Synchronizes the notched outline state to be based on the `width` and `open` inputs. */ - private _syncNotchedOutlineState() { - if (this._mdcNotchedOutline === null) { - return; - } + _getNotchWidth() { if (this.open) { - this._mdcNotchedOutline.notch(this.width); - } else { - this._mdcNotchedOutline.closeNotch(); + const NOTCH_ELEMENT_PADDING = 8; + return `${this.width > 0 ? this.width + NOTCH_ELEMENT_PADDING : 0}px`; } + + return null; } } diff --git a/src/material-experimental/mdc-form-field/form-field.ts b/src/material-experimental/mdc-form-field/form-field.ts index f3c077045b67..b0f516563e04 100644 --- a/src/material-experimental/mdc-form-field/form-field.ts +++ b/src/material-experimental/mdc-form-field/form-field.ts @@ -37,11 +37,6 @@ import { MatFormFieldControl, } from '@angular/material/form-field'; import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations'; -import { - MDCTextFieldAdapter, - MDCTextFieldFoundation, - numbers as mdcTextFieldNumbers, -} from '@material/textfield'; import {merge, Subject} from 'rxjs'; import {takeUntil} from 'rxjs/operators'; import {MAT_ERROR, MatError} from './directives/error'; @@ -119,6 +114,9 @@ const FLOATING_LABEL_DEFAULT_DOCKED_TRANSFORM = `translateY(-50%)`; */ const WRAPPER_HORIZONTAL_PADDING = 16; +/** Amount by which to scale the label when the form field is focused. */ +const LABEL_SCALE = 0.75; + /** Container for form controls that applies Material Design styling and behavior. */ @Component({ selector: 'mat-form-field', @@ -278,90 +276,7 @@ export class MatFormField private _destroyed = new Subject(); private _isFocused: boolean | null = null; private _explicitFormFieldControl: MatFormFieldControl; - private _foundation: MDCTextFieldFoundation; private _needsOutlineLabelOffsetUpdateOnStable = false; - private _adapter: MDCTextFieldAdapter = { - addClass: className => this._textField.nativeElement.classList.add(className), - removeClass: className => this._textField.nativeElement.classList.remove(className), - hasClass: className => this._textField.nativeElement.classList.contains(className), - - hasLabel: () => this._hasFloatingLabel(), - isFocused: () => this._control.focused, - hasOutline: () => this._hasOutline(), - - // MDC text-field will call this method on focus, blur and value change. It expects us - // to update the floating label state accordingly. Though we make this a noop because we - // want to react to floating label state changes through change detection. Relying on this - // adapter method would mean that the label would not update if the custom form field control - // sets "shouldLabelFloat" to true, or if the "floatLabel" input binding changes to "always". - floatLabel: () => {}, - - // Label shaking is not supported yet. It will require a new API for form field - // controls to trigger the shaking. This can be a feature in the future. - // TODO(devversion): explore options on how to integrate label shaking. - shakeLabel: () => {}, - - // MDC by default updates the notched-outline whenever the text-field receives focus, or - // is being blurred. It also computes the label width every time the notch is opened or - // closed. This works fine in the standard MDC text-field, but not in Angular where the - // floating label could change through interpolation. We want to be able to update the - // notched outline whenever the label content changes. Additionally, relying on focus or - // blur to open and close the notch does not work for us since abstract form field controls - // have the ability to control the floating label state (i.e. `shouldLabelFloat`), and we - // want to update the notch whenever the `_shouldLabelFloat()` value changes. - getLabelWidth: () => 0, - - // We don't use `setLabelRequired` as it relies on a mutation observer for determining - // when the `required` state changes. This is not reliable and flexible enough for - // our form field, as we support custom controls and detect the required state through - // a public property in the abstract form control. - setLabelRequired: () => {}, - notchOutline: () => {}, - closeOutline: () => {}, - - activateLineRipple: () => this._lineRipple && this._lineRipple.activate(), - deactivateLineRipple: () => this._lineRipple && this._lineRipple.deactivate(), - - // The foundation tries to register events on the input. This is not matching - // our concept of abstract form field controls. We handle each event manually - // in "stateChanges" based on the form field control state. The following events - // need to be handled: focus, blur. We do not handle the "input" event since - // that one is only needed for the text-field character count, which we do - // not implement as part of the form field, but should be implemented manually - // by consumers using template bindings. - registerInputInteractionHandler: () => {}, - deregisterInputInteractionHandler: () => {}, - - // We do not have a reference to the native input since we work with abstract form field - // controls. MDC needs a reference to the native input optionally to handle character - // counting and value updating. These are both things we do not handle from within the - // form field, so we can just return null. - getNativeInput: () => null, - - // This method will never be called since we do not have the ability to add event listeners - // to the native input. This is because the form control is not necessarily an input, and - // the form field deals with abstract form controls of any type. - setLineRippleTransformOrigin: () => {}, - - // The foundation tries to register click and keyboard events on the form field to figure out - // if the input value changes through user interaction. Based on that, the foundation tries - // to focus the input. Since we do not handle the input value as part of the form field, nor - // it's guaranteed to be an input (see adapter methods above), this is a noop. - deregisterTextFieldInteractionHandler: () => {}, - registerTextFieldInteractionHandler: () => {}, - - // The foundation tries to setup a "MutationObserver" in order to watch for attributes - // like "maxlength" or "pattern" to change. The foundation will update the validity state - // based on that. We do not need this logic since we handle the validity through the - // abstract form control instance. - deregisterValidationAttributeChangeHandler: () => {}, - registerValidationAttributeChangeHandler: () => null as any, - - // Used by foundation to dynamically remove aria-describedby when the hint text - // is shown only on invalid state, which should not be applicable here. - setInputAttr: () => undefined, - removeInputAttr: () => undefined, - }; constructor( private _elementRef: ElementRef, @@ -387,24 +302,6 @@ export class MatFormField } ngAfterViewInit() { - this._foundation = new MDCTextFieldFoundation(this._adapter); - - // MDC uses the "shouldFloat" getter to know whether the label is currently floating. This - // does not match our implementation of when the label floats because we support more cases. - // For example, consumers can set "@Input floatLabel" to always, or the custom form field - // control can set "MatFormFieldControl#shouldLabelFloat" to true. To ensure that MDC knows - // when the label is floating, we overwrite the property to be based on the method we use to - // determine the current state of the floating label. - Object.defineProperty(this._foundation, 'shouldFloat', { - get: () => this._shouldLabelFloat(), - }); - - // By default, the foundation determines the validity of the text-field from the - // specified native input. Since we don't pass a native input to the foundation because - // abstract form controls are not necessarily consisting of an input, we handle the - // text-field validity through the abstract form field control state. - this._foundation.isValid = () => !this._control.errorState; - // Initial focus state sync. This happens rarely, but we want to account for // it in case the form field control has "focused" set to true on init. this._updateFocusState(); @@ -445,7 +342,6 @@ export class MatFormField } ngOnDestroy() { - this._foundation?.destroy(); this._destroyed.next(); this._destroyed.complete(); } @@ -562,11 +458,16 @@ export class MatFormField // we handle the focus by checking if the abstract form field control focused state changes. if (this._control.focused && !this._isFocused) { this._isFocused = true; - this._foundation.activateFocus(); + this._lineRipple?.activate(); } else if (!this._control.focused && (this._isFocused || this._isFocused === null)) { this._isFocused = false; - this._foundation.deactivateFocus(); + this._lineRipple?.deactivate(); } + + this._textField?.nativeElement.classList.toggle( + 'mdc-text-field--focused', + this._control.focused, + ); } /** @@ -652,7 +553,7 @@ export class MatFormField // The outline notch should be based on the label width, but needs to respect the scaling // applied to the label if it actively floats. Since the label always floats when the notch // is open, the MDC text-field floating label scaling is respected in notch width calculation. - this._outlineNotchWidth = this._floatingLabel.getWidth() * mdcTextFieldNumbers.LABEL_SCALE; + this._outlineNotchWidth = this._floatingLabel.getWidth() * LABEL_SCALE; } /** Does any extra processing that is required when handling the hints. */