Skip to content

feat(material/form-field): add color to default options (#24438) #24440

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

Merged
Merged
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
88 changes: 52 additions & 36 deletions src/material-experimental/mdc-form-field/form-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,18 @@ export type SubscriptSizing = 'fixed' | 'dynamic';
* using the `MAT_FORM_FIELD_DEFAULT_OPTIONS` injection token.
*/
export interface MatFormFieldDefaultOptions {
/** Default form field appearance style. */
appearance?: MatFormFieldAppearance;
/** Default color of the form field. */
color?: ThemePalette;
/** Whether the required marker should be hidden by default. */
hideRequiredMarker?: boolean;
/**
* Whether the label for form fields should by default float `always`,
* `never`, or `auto` (only when necessary).
*/
floatLabel?: FloatLabelType;
/** Whether the form field should reserve space for one line by default. */
subscriptSizing?: SubscriptSizing;
}

Expand All @@ -85,10 +94,13 @@ export const MAT_FORM_FIELD_DEFAULT_OPTIONS = new InjectionToken<MatFormFieldDef

let nextUniqueId = 0;

/** Default appearance used by the form-field. */
/** Default appearance used by the form field. */
const DEFAULT_APPEARANCE: MatFormFieldAppearance = 'fill';

/** Default appearance used by the form-field. */
/**
* Whether the label for form fields should by default float `always`,
* `never`, or `auto`.
*/
const DEFAULT_FLOAT_LABEL: FloatLabelType = 'auto';

/** Default way that the subscript element height is set. */
Expand Down Expand Up @@ -147,7 +159,7 @@ const WRAPPER_HORIZONTAL_PADDING = 16;
providers: [{provide: MAT_FORM_FIELD, useExisting: MatFormField}],
})
export class MatFormField
implements AfterViewInit, OnDestroy, AfterContentChecked, AfterContentInit
implements AfterContentInit, AfterContentChecked, AfterViewInit, OnDestroy
{
@ViewChild('textField') _textField: ElementRef<HTMLElement>;
@ViewChild('iconPrefixContainer') _iconPrefixContainer: ElementRef<HTMLElement>;
Expand All @@ -172,9 +184,9 @@ export class MatFormField
set hideRequiredMarker(value: BooleanInput) {
this._hideRequiredMarker = coerceBooleanProperty(value);
}
private _hideRequiredMarker: boolean;
private _hideRequiredMarker = false;

/** The color palette for the form-field. */
/** The color palette for the form field. */
@Input() color: ThemePalette = 'primary';

/** Whether the label should always float or float as the user types. */
Expand All @@ -185,23 +197,23 @@ export class MatFormField
set floatLabel(value: FloatLabelType) {
if (value !== this._floatLabel) {
this._floatLabel = value;
// For backwards compatibility. Custom form-field controls or directives might set
// the "floatLabel" input and expect the form-field view to be updated automatically.
// For backwards compatibility. Custom form field controls or directives might set
// the "floatLabel" input and expect the form field view to be updated automatically.
// e.g. autocomplete trigger. Ideally we'd get rid of this and the consumers would just
// emit the "stateChanges" observable. TODO(devversion): consider removing.
this._changeDetectorRef.markForCheck();
}
}
private _floatLabel: FloatLabelType;

/** The form-field appearance style. */
/** The form field appearance style. */
@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;
this._appearance = value || this._defaults?.appearance || DEFAULT_APPEARANCE;
if (this._appearance === 'outline' && this._appearance !== oldValue) {
this._refreshOutlineNotchWidth();

Expand Down Expand Up @@ -280,7 +292,7 @@ export class MatFormField
// 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
// 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: () => {},

Expand All @@ -294,7 +306,7 @@ export class MatFormField
// 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
// 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,
Expand All @@ -312,28 +324,28 @@ export class MatFormField

// 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
// 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
// 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.
// 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
// 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
// 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: () => {},
Expand Down Expand Up @@ -363,19 +375,23 @@ export class MatFormField
@Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string,
@Inject(DOCUMENT) private _document?: any,
) {
if (_defaults && _defaults.appearance) {
this.appearance = _defaults.appearance;
if (_defaults) {
if (_defaults.appearance) {
this.appearance = _defaults.appearance;
}
this._hideRequiredMarker = Boolean(_defaults?.hideRequiredMarker);
if (_defaults.color) {
this.color = _defaults.color;
}
}

this._hideRequiredMarker = _defaults?.hideRequiredMarker ?? false;
}

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
// 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.
Expand All @@ -386,11 +402,11 @@ export class MatFormField
// 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.
// 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.
// it in case the form field control has "focused" set to true on init.
this._updateFocusState();
// Initial notch width update. This is needed in case the text-field label floats
// on initialization, and renders inside of the notched outline.
Expand Down Expand Up @@ -442,7 +458,7 @@ export class MatFormField
}

/**
* Gets an ElementRef for the element that a overlay attached to the form-field
* Gets an ElementRef for the element that a overlay attached to the form field
* should be positioned relative to.
*/
getConnectedOverlayOrigin(): ElementRef {
Expand All @@ -451,20 +467,20 @@ export class MatFormField

/** Animates the placeholder up and locks it in position. */
_animateAndLockLabel(): void {
// This is for backwards compatibility only. Consumers of the form-field might use
// This is for backwards compatibility only. Consumers of the form field might use
// this method. e.g. the autocomplete trigger. This method has been added to the non-MDC
// form-field because setting "floatLabel" to "always" caused the label to float without
// form field because setting "floatLabel" to "always" caused the label to float without
// animation. This is different in MDC where the label always animates, so this method
// is no longer necessary. There doesn't seem any benefit in adding logic to allow changing
// the floating label state without animations. The non-MDC implementation was inconsistent
// because it always animates if "floatLabel" is set away from "always".
// TODO(devversion): consider removing this method when releasing the MDC form-field.
// TODO(devversion): consider removing this method when releasing the MDC form field.
if (this._hasFloatingLabel()) {
this.floatLabel = 'always';
}
}

/** Initializes the registered form-field control. */
/** Initializes the registered form field control. */
private _initializeControl() {
const control = this._control;

Expand Down Expand Up @@ -499,7 +515,7 @@ export class MatFormField
/** Initializes the prefix and suffix containers. */
private _initializePrefixAndSuffix() {
this._checkPrefixAndSuffixTypes();
// Mark the form-field as dirty whenever the prefix or suffix children change. This
// Mark the form field as dirty whenever the prefix or suffix children change. This
// is necessary because we conditionally display the prefix/suffix containers based
// on whether there is projected content.
merge(this._prefixChildren.changes, this._suffixChildren.changes).subscribe(() => {
Expand All @@ -510,7 +526,7 @@ export class MatFormField

/**
* Initializes the subscript by validating hints and synchronizing "aria-describedby" ids
* with the custom form-field control. Also subscribes to hint and error changes in order
* with the custom form field control. Also subscribes to hint and error changes in order
* to be able to validate and synchronize ids on change.
*/
private _initializeSubscript() {
Expand Down Expand Up @@ -541,9 +557,9 @@ export class MatFormField
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
// form-field because we support abstract form field controls which are not necessarily
// of type input, nor do we have a reference to a native form-field control element. Instead
// we handle the focus by checking if the abstract form-field control focused state changes.
// form field because we support abstract form field controls which are not necessarily
// of type input, nor do we have a reference to a native form field control element. Instead
// 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();
Expand All @@ -556,7 +572,7 @@ export class MatFormField
/**
* 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
* 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.
Expand Down Expand Up @@ -595,7 +611,7 @@ export class MatFormField
/**
* 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
* 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.
Expand Down Expand Up @@ -729,7 +745,7 @@ export class MatFormField
floatingLabel.style.transform = '';
return;
}
// If the form-field is not attached to the DOM yet (e.g. in a tab), we defer
// 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;
Expand Down
21 changes: 21 additions & 0 deletions src/material-experimental/mdc-input/input.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1405,6 +1405,18 @@ describe('MatFormField default options', () => {
expect(fixture.componentInstance.formField.appearance).toBe('outline');
});

it('should be able to change the default color', () => {
const fixture = createComponent(MatInputSimple, [
{
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
useValue: {color: 'accent'},
},
]);
fixture.detectChanges();
const formField = fixture.nativeElement.querySelector('.mat-mdc-form-field');
expect(formField.classList).toContain('mat-accent');
});

it('defaults subscriptSizing to false', () => {
const fixture = createComponent(MatInputWithSubscriptSizing);
fixture.detectChanges();
Expand Down Expand Up @@ -2019,3 +2031,12 @@ class MatInputInsideOutsideFormField {}
class MatInputWithRequiredFormControl {
formControl = new FormControl('', [Validators.required]);
}

@Component({
template: `
<mat-form-field>
<input matInput>
</mat-form-field>
`,
})
class MatInputSimple {}
28 changes: 18 additions & 10 deletions src/material/form-field/form-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
ViewEncapsulation,
OnDestroy,
} from '@angular/core';
import {CanColor, mixinColor} from '@angular/material/core';
import {CanColor, mixinColor, ThemePalette} from '@angular/material/core';
import {fromEvent, merge, Subject} from 'rxjs';
import {startWith, take, takeUntil} from 'rxjs/operators';
import {MAT_ERROR, MatError} from './error';
Expand Down Expand Up @@ -66,18 +66,22 @@ const _MatFormFieldBase = mixinColor(
/** Possible appearance styles for the form field. */
export type MatFormFieldAppearance = 'legacy' | 'standard' | 'fill' | 'outline';

/** Possible values for the "floatLabel" form-field input. */
/** Possible values for the "floatLabel" form field input. */
export type FloatLabelType = 'always' | 'never' | 'auto';

/**
* Represents the default options for the form field that can be configured
* using the `MAT_FORM_FIELD_DEFAULT_OPTIONS` injection token.
*/
export interface MatFormFieldDefaultOptions {
/** Default form field appearance style. */
appearance?: MatFormFieldAppearance;
/** Default color of the form field. */
color?: ThemePalette;
/** Whether the required marker should be hidden by default. */
hideRequiredMarker?: boolean;
/**
* Whether the label for form-fields should by default float `always`,
* Whether the label for form fields should by default float `always`,
* `never`, or `auto` (only when necessary).
*/
floatLabel?: FloatLabelType;
Expand Down Expand Up @@ -158,15 +162,15 @@ export class MatFormField

private readonly _destroyed = new Subject<void>();

/** The form-field appearance style. */
/** The form field appearance style. */
@Input()
get appearance(): MatFormFieldAppearance {
return this._appearance;
}
set appearance(value: MatFormFieldAppearance) {
const oldValue = this._appearance;

this._appearance = value || (this._defaults && this._defaults.appearance) || 'legacy';
this._appearance = value || this._defaults?.appearance || 'legacy';

if (this._appearance === 'outline' && oldValue !== value) {
this._outlineGapCalculationNeededOnStable = true;
Expand All @@ -182,7 +186,7 @@ export class MatFormField
set hideRequiredMarker(value: BooleanInput) {
this._hideRequiredMarker = coerceBooleanProperty(value);
}
private _hideRequiredMarker: boolean;
private _hideRequiredMarker = false;

/** Override for the logic that disables the label animation in certain cases. */
private _showAlwaysAnimate = false;
Expand Down Expand Up @@ -282,9 +286,13 @@ export class MatFormField
this._animationsEnabled = _animationMode !== 'NoopAnimations';

// Set the default through here so we invoke the setter on the first run.
this.appearance = _defaults && _defaults.appearance ? _defaults.appearance : 'legacy';
this._hideRequiredMarker =
_defaults && _defaults.hideRequiredMarker != null ? _defaults.hideRequiredMarker : false;
this.appearance = _defaults?.appearance || 'legacy';
if (_defaults) {
this._hideRequiredMarker = Boolean(_defaults.hideRequiredMarker);
if (_defaults.color) {
this.color = this.defaultColor = _defaults.color;
}
}
}

/**
Expand All @@ -295,7 +303,7 @@ export class MatFormField
}

/**
* Gets an ElementRef for the element that a overlay attached to the form-field should be
* Gets an ElementRef for the element that a overlay attached to the form field should be
* positioned relative to.
*/
getConnectedOverlayOrigin(): ElementRef {
Expand Down
Loading