diff --git a/src/dev-app/mdc-input/mdc-input-demo.html b/src/dev-app/mdc-input/mdc-input-demo.html index 287aa6a46d13..df513e182cc2 100644 --- a/src/dev-app/mdc-input/mdc-input-demo.html +++ b/src/dev-app/mdc-input/mdc-input-demo.html @@ -128,6 +128,14 @@

Text

.00 +

Text (always outline)

+ + Amount + + + .00 + +

Icons

Amount @@ -640,14 +648,31 @@

<textarea> with bindable autosize

Tab 1 input
+ + + Amount + + + .00 + Tab 2 input + + + Amount + + + + .00 + + + diff --git a/src/dev-app/mdc-input/mdc-input-demo.ts b/src/dev-app/mdc-input/mdc-input-demo.ts index 207ffb19942d..516e56f0e2b4 100644 --- a/src/dev-app/mdc-input/mdc-input-demo.ts +++ b/src/dev-app/mdc-input/mdc-input-demo.ts @@ -37,6 +37,7 @@ export class MdcInputDemo { prefixSuffixAppearance: MatFormFieldAppearance = 'fill'; placeholderTestControl = new FormControl('', Validators.required); options: string[] = ['One', 'Two', 'Three']; + showSecondPrefix = false; name: string; errorMessageExample1: string; diff --git a/src/material-experimental/mdc-form-field/BUILD.bazel b/src/material-experimental/mdc-form-field/BUILD.bazel index 2d99d1978734..cf87f04e676d 100644 --- a/src/material-experimental/mdc-form-field/BUILD.bazel +++ b/src/material-experimental/mdc-form-field/BUILD.bazel @@ -18,6 +18,7 @@ ng_module( assets = [":form_field_scss"] + glob(["**/*.html"]), module_name = "@angular/material-experimental/mdc-form-field", deps = [ + "//src/cdk/bidi", "//src/cdk/observers", "//src/cdk/platform", "//src/material/core", diff --git a/src/material-experimental/mdc-form-field/_mdc-text-field-structure-overrides.scss b/src/material-experimental/mdc-form-field/_mdc-text-field-structure-overrides.scss index e3e0a77e27f0..7931560eb736 100644 --- a/src/material-experimental/mdc-form-field/_mdc-text-field-structure-overrides.scss +++ b/src/material-experimental/mdc-form-field/_mdc-text-field-structure-overrides.scss @@ -1,3 +1,5 @@ +@import '@material/notched-outline/variables.import'; +@import '@material/textfield/variables.import'; @import 'form-field-sizing'; // Mixin that can be included to override the default MDC text-field @@ -29,36 +31,32 @@ // spacing as we support arbitrary form-field controls which aren't necessarily matching // the "mdc-text-field__input" class. Note: We need the first selector to overwrite the // default no-label MDC padding styles which are set with a very high specificity. - .mdc-text-field--no-label:not(.mdc-text-field--outlined):not(.mdc-text-field--textarea) + .mdc-text-field--no-label:not(.mdc-text-field--textarea) .mat-mdc-input-element.mdc-text-field__input, - .mat-mdc-input-element { + .mat-mdc-text-field-wrapper .mat-mdc-input-element { padding: 0; } - // MDC changes the vertical spacing of the input if there is no label. Since we moved - // the spacing of the input to the parent infix container (to support custom controls), - // we need to replicate these styles for the infix container. The goal is that the input - // placeholder vertically aligns with floating labels of other filled inputs. Note that - // outline appearance currently still relies on the input spacing due to a notched-outline - // limitation. TODO: https://github.com/material-components/material-components-web/issues/5326 - .mdc-text-field--no-label:not(.mdc-text-field--outlined):not(.mdc-text-field--textarea) - .mat-mdc-form-field-infix { - padding-top: $mat-form-field-no-label-padding-top; - padding-bottom: $mat-form-field-no-label-padding-bottom; - } - // MDC adds vertical spacing to inputs. We removed this spacing and intend to add it // to the infix container. This is necessary to ensure that custom form-field controls - // also have the proper Material Design spacing to the label and bottom-line. Note that - // outline appearance currently still relies on the input spacing due to a notched-outline - // limitation. TODO: https://github.com/material-components/material-components-web/issues/5326 - .mat-mdc-text-field-wrapper:not(.mdc-text-field--outlined) .mat-mdc-form-field-infix { + // also have the proper Material Design spacing to the label and bottom-line. + .mat-mdc-text-field-wrapper .mat-mdc-form-field-infix { // Apply the default text-field input padding to the infix container. We removed the // padding from the input elements in order to support arbitrary form-field controls. padding-top: $mat-form-field-with-label-input-padding-top; padding-bottom: $mat-form-field-with-label-input-padding-bottom; } + // MDC changes the vertical spacing of the input if there is no label, or in the outline + // appearance. This is because the input should vertically align with other inputs which use + // a floating label. To achieve this, we add custom vertical spacing to the infix container + // that differs from the vertical spacing for text-field's with a floating label. + .mdc-text-field--no-label:not(.mdc-text-field--textarea) .mat-mdc-form-field-infix, + .mat-mdc-text-field-wrapper.mdc-text-field--outlined .mat-mdc-form-field-infix { + padding-top: $mat-form-field-no-label-padding-top; + padding-bottom: $mat-form-field-no-label-padding-bottom; + } + // Root element of the mdc-text-field. As explained in the height overwrites above, MDC // sets a default height on the text-field root element. This is not desired since we // want the element to be able to expand as needed. @@ -79,22 +77,33 @@ opacity: 1; } - // The additional nesting is a temporary until the notched-outline is decoupled from the - // floating label. See https://github.com/material-components/material-components-web/issues/5326 - // TODO(devversion): Remove this workaround/nesting once the feature is available. - .mat-mdc-text-field-wrapper:not(.mdc-text-field--outlined) { - // We removed the horizontal inset on input elements, but need to re-add the spacing to - // the actual form-field flex container that contains the prefixes, suffixes and infix. - .mat-mdc-form-field-flex { - padding: 0 $mdc-text-field-input-padding; - } + // We removed the horizontal inset on input elements, but need to re-add the spacing to + // the actual form-field flex container that contains the prefixes, suffixes and infix. + .mat-mdc-text-field-wrapper .mat-mdc-form-field-flex { + padding: 0 $mdc-text-field-input-padding; + } + + // Since we moved the horizontal spacing from the input to the form-field flex container + // and the MDC floating label tries to account for the horizontal spacing, we need to reset + // the shifting since there is no padding the label needs to account for. Note that we do not + // want do this for labels in the notched-outline since MDC keeps those labels relative to + // the notched outline container, and already applies a specific horizontal spacing which + // we do not want to overwrite. + .mat-mdc-form-field-infix .mdc-floating-label { + left: 0; + right: 0; + } - // Since we moved the horizontal spacing from the input to the form-field flex container - // and the MDC floating label tries to account for the horizontal spacing, we need to reset - // the shifting since there is no padding the label needs to account for. - .mdc-floating-label { - left: 0; - } + // For the outline appearance, we re-create the active floating label transform. This is + // necessary because the transform for docked floating labels can be updated to account for + // the width of prefix container. We need to re-create these styles with `!important` because + // the horizontal adjustment for the label is applied through inline styles, and we want to + // make sure that the label can still float as expected. It should be okay using `!important` + // because it's unlikely that developers commonly overwrite the floating label transform. + .mat-mdc-text-field-wrapper.mdc-text-field--outlined .mdc-floating-label--float-above { + // This transform has been extracted from the MDC text-field styles. We can't access it + // through a variable because MDC generates this label transform through a mixin. + transform: translateY(-$mdc-text-field-outlined-label-position-y) scale(0.75) !important; } // MDC sets the input elements in outline appearance to "display: flex". There seems to diff --git a/src/material-experimental/mdc-form-field/_mdc-text-field-textarea-overrides.scss b/src/material-experimental/mdc-form-field/_mdc-text-field-textarea-overrides.scss index 5714cbde6a11..893b8ee12ed0 100644 --- a/src/material-experimental/mdc-form-field/_mdc-text-field-textarea-overrides.scss +++ b/src/material-experimental/mdc-form-field/_mdc-text-field-textarea-overrides.scss @@ -34,18 +34,4 @@ .mat-mdc-text-field-wrapper .mdc-floating-label { top: $mat-form-field-baseline; } - - // In the outline appearance, the textarea needs slightly reduced top spacing because - // the label overflows the outline by 50%. Additionally, horizontal spacing needs to be - // added since the outline is part of the "infix" and we need to account for the outline. - // TODO(devversion): horizontal spacing and extra specificity can be removed once the - // following feature is available: material-components-web#5326. - .mat-mdc-form-field > .mat-mdc-text-field-wrapper.mdc-text-field--outlined - .mat-mdc-form-field-infix .mat-mdc-textarea-input { - margin-top: $mat-form-field-outline-top-spacing; - padding: { - left: $mdc-text-field-input-padding; - right: $mdc-text-field-input-padding; - } - } } 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 c9d091154c35..7468961c6603 100644 --- a/src/material-experimental/mdc-form-field/directives/floating-label.ts +++ b/src/material-experimental/mdc-form-field/directives/floating-label.ts @@ -24,7 +24,6 @@ import {MDCFloatingLabel} from '@material/floating-label'; }, }) export class MatFormFieldFloatingLabel extends MDCFloatingLabel implements OnDestroy { - @Input() get floating() { return this._floating; } set floating(shouldFloat: boolean) { @@ -35,11 +34,16 @@ export class MatFormFieldFloatingLabel extends MDCFloatingLabel implements OnDes } private _floating = false; - constructor(elementRef: ElementRef) { - super(elementRef.nativeElement); + constructor(private _elementRef: ElementRef) { + super(_elementRef.nativeElement); } ngOnDestroy() { this.destroy(); } + + /** Gets the HTML element for the floating label. */ + get element(): HTMLElement { + return this._elementRef.nativeElement; + } } diff --git a/src/material-experimental/mdc-form-field/form-field.html b/src/material-experimental/mdc-form-field/form-field.html index 854de64c1267..badc5d4e3bbc 100644 --- a/src/material-experimental/mdc-form-field/form-field.html +++ b/src/material-experimental/mdc-form-field/form-field.html @@ -26,21 +26,21 @@ [class.mdc-text-field--invalid]="_control.errorState" (click)="_control.onContainerClick && _control.onContainerClick($event)">
-
+
+ + + +
+ +
- - + - -
- -
-
diff --git a/src/material-experimental/mdc-form-field/form-field.ts b/src/material-experimental/mdc-form-field/form-field.ts index b370d7ad1b66..55345795a0b5 100644 --- a/src/material-experimental/mdc-form-field/form-field.ts +++ b/src/material-experimental/mdc-form-field/form-field.ts @@ -5,6 +5,8 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import {Directionality} from '@angular/cdk/bidi'; +import {Platform} from '@angular/cdk/platform'; import { AfterContentChecked, AfterContentInit, @@ -15,9 +17,11 @@ import { ContentChild, ContentChildren, ElementRef, - Inject, InjectionToken, + Inject, + InjectionToken, Input, isDevMode, + NgZone, OnDestroy, Optional, QueryList, @@ -80,6 +84,13 @@ const DEFAULT_APPEARANCE: MatFormFieldAppearance = 'fill'; /** Default appearance used by the form-field. */ const DEFAULT_FLOAT_LABEL: FloatLabelType = 'auto'; +/** + * Default transform for docked floating labels in a MDC text-field. This value has been + * extracted from the MDC text-field styles because we programmatically modify the docked + * label transform, but do not want to accidentally discard the default label transform. + */ +const FLOATING_LABEL_DEFAULT_DOCKED_TRANSFORM = `translateY(-50%)`; + /** Container for form controls that applies Material Design styling and behavior. */ @Component({ selector: 'mat-form-field', @@ -116,6 +127,7 @@ const DEFAULT_FLOAT_LABEL: FloatLabelType = 'auto'; export class MatFormField implements AfterViewInit, OnDestroy, AfterContentChecked, AfterContentInit { @ViewChild('textField') _textField: ElementRef; + @ViewChild('prefixContainer') _prefixContainer: ElementRef; @ViewChild(MatFormFieldFloatingLabel) _floatingLabel: MatFormFieldFloatingLabel|undefined; @ViewChild(MatFormFieldNotchedOutline) _notchedOutline: MatFormFieldNotchedOutline|undefined; @ViewChild(MatFormFieldLineRipple) _lineRipple: MatFormFieldLineRipple|undefined; @@ -156,7 +168,14 @@ export class MatFormField implements AfterViewInit, OnDestroy, AfterContentCheck @Input() get appearance(): MatFormFieldAppearance { return this._appearance; } set appearance(value: MatFormFieldAppearance) { + const oldValue = this._appearance; this._appearance = value || (this._defaults && this._defaults.appearance) || DEFAULT_APPEARANCE; + // If the appearance has been switched to `outline`, the label offset needs to be updated. + // The update can happen once the view has been re-checked, but not immediately because + // the view has not been updated and the notched-outline floating label is not present. + if (this._appearance === 'outline' && this._appearance !== oldValue) { + this._needsOutlineLabelOffsetUpdateOnStable = true; + } } private _appearance: MatFormFieldAppearance = DEFAULT_APPEARANCE; @@ -191,6 +210,7 @@ export class MatFormField implements AfterViewInit, OnDestroy, AfterContentCheck 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), @@ -257,6 +277,9 @@ export class MatFormField implements AfterViewInit, OnDestroy, AfterContentCheck constructor(private _elementRef: ElementRef, private _changeDetectorRef: ChangeDetectorRef, + private _ngZone: NgZone, + private _dir: Directionality, + private _platform: Platform, @Optional() @Inject(MAT_FORM_FIELD_DEFAULT_OPTIONS) private _defaults?: MatFormFieldDefaultOptions, @Optional() @Inject(MAT_LABEL_GLOBAL_OPTIONS) private _labelOptions?: LabelOptions, @@ -302,6 +325,7 @@ export class MatFormField implements AfterViewInit, OnDestroy, AfterContentCheck this._assertFormFieldControl(); this._initializeControl(); this._initializeSubscript(); + this._initializeOutlineLabelOffsetSubscriptions(); } ngAfterContentChecked() { @@ -389,6 +413,7 @@ export class MatFormField implements AfterViewInit, OnDestroy, AfterContentCheck throw getMatFormFieldMissingControlError(); } } + private _updateFocusState() { // Usually the MDC foundation would call "activateFocus" and "deactivateFocus" whenever // certain DOM events are emitted. This is not possible in our implementation of the @@ -404,6 +429,34 @@ export class MatFormField implements AfterViewInit, OnDestroy, AfterContentCheck } } + /** + * The floating label in the docked state needs to account for prefixes. The horizontal offset + * is calculated whenever the appearance changes to `outline`, the prefixes change, or when the + * form-field is added to the DOM. This method sets up all subscriptions which are needed to + * trigger the label offset update. In general, we want to avoid performing measurements often, + * so we rely on the `NgZone` as indicator when the offset should be recalculated, instead of + * checking every change detection cycle. + */ + private _initializeOutlineLabelOffsetSubscriptions() { + // Whenever the prefix changes, schedule an update of the label offset. + this._prefixChildren.changes.pipe(takeUntil(this._destroyed)) + .subscribe(() => this._needsOutlineLabelOffsetUpdateOnStable = true); + + // Note that we have to run outside of the `NgZone` explicitly, in order to avoid + // throwing users into an infinite loop if `zone-patch-rxjs` is included. + this._ngZone.runOutsideAngular(() => { + this._ngZone.onStable.asObservable().pipe(takeUntil(this._destroyed)).subscribe(() => { + if (this._needsOutlineLabelOffsetUpdateOnStable) { + this._needsOutlineLabelOffsetUpdateOnStable = false; + this._updateOutlineLabelOffset(); + } + }); + }); + + this._dir.change.pipe(takeUntil(this._destroyed)) + .subscribe(() => this._needsOutlineLabelOffsetUpdateOnStable = true); + } + _rerenderOutlineNotch() { if (this._floatingLabel && this._hasOutline()) { this._foundation.notchOutline(this._shouldLabelFloat()); @@ -419,6 +472,18 @@ export class MatFormField implements AfterViewInit, OnDestroy, AfterContentCheck return this.appearance === 'outline'; } + /** + * Whether the label should display in the infix. Labels in the outline appearance are + * displayed as part of the notched-outline and are horizontally offset to account for + * form-field prefix content. This won't work in server side rendering since we cannot + * measure the width of the prefix container. To make the docked label appear as if the + * right offset has been calculated, we forcibly render the label inside the infix. Since + * the label is part of the infix, the label cannot overflow the prefix content. + */ + _forceDisplayInfixLabel() { + return !this._platform.isBrowser && this._prefixContainer && !this._shouldLabelFloat(); + } + _hasFloatingLabel() { return !!this._labelChildNonStatic || !!this._labelChildStatic; } @@ -501,4 +566,52 @@ export class MatFormField implements AfterViewInit, OnDestroy, AfterContentCheck this._control.setDescribedByIds(ids); } } + + /** + * Updates the horizontal offset of the label in the outline appearance. In the outline + * appearance, the notched-outline and label are not relative to the infix container because + * the outline intends to surround prefixes, suffixes and the infix. This means that the + * floating label by default overlaps prefixes in the docked state. To avoid this, we need to + * horizontally offset the label by the width of the prefix container. The MDC text-field does + * not need to do this because they use a fixed width for prefixes. Hence, they can simply + * incorporate the horizontal offset into their default text-field styles. + */ + private _updateOutlineLabelOffset() { + if (!this._platform.isBrowser || !this._hasOutline() || !this._prefixContainer || + !this._floatingLabel) { + return; + } + // If the form-field is not attached to the DOM yet (e.g. in a tab), we defer + // the label offset update until the zone stabilizes. + if (!this._isAttachedToDom()) { + this._needsOutlineLabelOffsetUpdateOnStable = true; + return; + } + + const floatingLabel = this._floatingLabel.element; + const prefixContainer = this._prefixContainer.nativeElement as HTMLElement; + // If the directionality is RTL, the x-axis transform needs to be inverted. This + // is because `transformX` does not change based on the page directionality. + const labelHorizontalOffset = + (this._dir.value === 'rtl' ? -1 : 1) * prefixContainer.getBoundingClientRect().width; + + // Update the transform the floating label to account for the prefix container. Note + // that we do not want to overwrite the default transform for docked floating labels. + floatingLabel.style.transform = + `${FLOATING_LABEL_DEFAULT_DOCKED_TRANSFORM} translateX(${labelHorizontalOffset}px)`; + } + + /** Checks whether the form field is attached to the DOM. */ + private _isAttachedToDom(): boolean { + const element: HTMLElement = this._elementRef.nativeElement; + if (element.getRootNode) { + const rootNode = element.getRootNode(); + // If the element is inside the DOM the root node will be either the document + // or the closest shadow root, otherwise it'll be the element itself. + return rootNode && rootNode !== element; + } + // Otherwise fall back to checking if it's in the document. This doesn't account for + // shadow DOM, however browser that support shadow DOM should support `getRootNode` as well. + return document.documentElement!.contains(element); + } } diff --git a/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.html b/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.html index 015d8e742849..bbcfe0c3bbb6 100644 --- a/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.html +++ b/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.html @@ -60,13 +60,13 @@

MDC slide-toggle

with a label -

Slider

+

MDC Slider

-

Tabs

+

MDC Tabs