Skip to content

fix(material-experimental/mdc-form-field): fix baseline and handle custom controls better #18161

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 1 commit into from
Jan 15, 2020
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package(default_visibility = ["//visibility:public"])

load("//tools:defaults.bzl", "ng_module")

ng_module(
name = "mdc-form-field",
srcs = glob(["**/*.ts"]),
assets = glob([
"**/*.html",
"**/*.css",
]),
module_name = "@angular/components-examples/material-experimental/mdc-form-field",
deps = [
"//src/cdk/a11y",
"//src/cdk/coercion",
"//src/material-experimental/mdc-form-field",
"//src/material-experimental/mdc-input",
"//src/material/icon",
"@npm//@angular/common",
"@npm//@angular/forms",
"@npm//rxjs",
],
)

filegroup(
name = "source-files",
srcs = glob([
"**/*.html",
"**/*.css",
"**/*.ts",
]),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
import {ReactiveFormsModule} from '@angular/forms';
import {MatFormFieldModule} from '@angular/material-experimental/mdc-form-field';
import {MatIconModule} from '@angular/material/icon';
import {
FormFieldCustomControlExample,
MyTelInput
} from './mdc-form-field-custom-control/form-field-custom-control-example';

export {
FormFieldCustomControlExample,
MyTelInput,
};

const EXAMPLES = [
FormFieldCustomControlExample,
];

@NgModule({
imports: [
CommonModule,
MatFormFieldModule,
MatIconModule,
ReactiveFormsModule,
],
declarations: [...EXAMPLES, MyTelInput],
exports: EXAMPLES,
})
export class MdcFormFieldExamplesModule {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.example-tel-input-container {
display: flex;
}

.example-tel-input-element {
border: none;
background: none;
padding: 0;
outline: none;
font: inherit;
text-align: center;
}

.example-tel-input-spacer {
opacity: 0;
transition: opacity 200ms;
}

:host.example-floating .example-tel-input-spacer {
opacity: 1;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<div [formGroup]="parts" class="example-tel-input-container">
<input class="example-tel-input-element" formControlName="area" size="3"
aria-label="Area code" (input)="_handleInput()">
<span class="example-tel-input-spacer">&ndash;</span>
<input class="example-tel-input-element" formControlName="exchange" size="3"
aria-label="Exchange code" (input)="_handleInput()">
<span class="example-tel-input-spacer">&ndash;</span>
<input class="example-tel-input-element" formControlName="subscriber" size="4"
aria-label="Subscriber number" (input)="_handleInput()">
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/** No CSS for this example */
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<mat-form-field>
<mat-label>Phone number</mat-label>
<example-tel-input required></example-tel-input>
<mat-icon matSuffix>phone</mat-icon>
<mat-hint>Include area code</mat-hint>
</mat-form-field>
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import {FocusMonitor} from '@angular/cdk/a11y';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {Component, ElementRef, Input, OnDestroy, Optional, Self} from '@angular/core';
import {FormBuilder, FormGroup, ControlValueAccessor, NgControl} from '@angular/forms';
import {MatFormFieldControl} from '@angular/material-experimental/mdc-form-field';
import {Subject} from 'rxjs';

/** @title Form field with custom telephone number input control. */
@Component({
selector: 'form-field-custom-control-example',
templateUrl: 'form-field-custom-control-example.html',
styleUrls: ['form-field-custom-control-example.css'],
})
export class FormFieldCustomControlExample {}

/** Data structure for holding telephone number. */
export class MyTel {
constructor(public area: string, public exchange: string, public subscriber: string) {}
}

/** Custom `MatFormFieldControl` for telephone number input. */
@Component({
selector: 'example-tel-input',
templateUrl: 'example-tel-input-example.html',
styleUrls: ['example-tel-input-example.css'],
providers: [{provide: MatFormFieldControl, useExisting: MyTelInput}],
host: {
'[class.example-floating]': 'shouldLabelFloat',
'[id]': 'id',
'[attr.aria-describedby]': 'describedBy',
}
})
export class MyTelInput implements ControlValueAccessor, MatFormFieldControl<MyTel>, OnDestroy {
static nextId = 0;

parts: FormGroup;
stateChanges = new Subject<void>();
focused = false;
errorState = false;
controlType = 'example-tel-input';
id = `example-tel-input-${MyTelInput.nextId++}`;
describedBy = '';
onChange = (_: any) => {};
onTouched = () => {};

get empty() {
const {value: {area, exchange, subscriber}} = this.parts;

return !area && !exchange && !subscriber;
}

get shouldLabelFloat() { return this.focused || !this.empty; }

@Input()
get placeholder(): string { return this._placeholder; }
set placeholder(value: string) {
this._placeholder = value;
this.stateChanges.next();
}
private _placeholder: string;

@Input()
get required(): boolean { return this._required; }
set required(value: boolean) {
this._required = coerceBooleanProperty(value);
this.stateChanges.next();
}
private _required = false;

@Input()
get disabled(): boolean { return this._disabled; }
set disabled(value: boolean) {
this._disabled = coerceBooleanProperty(value);
this._disabled ? this.parts.disable() : this.parts.enable();
this.stateChanges.next();
}
private _disabled = false;

@Input()
get value(): MyTel | null {
const {value: {area, exchange, subscriber}} = this.parts;
if (area.length === 3 && exchange.length === 3 && subscriber.length === 4) {
return new MyTel(area, exchange, subscriber);
}
return null;
}
set value(tel: MyTel | null) {
const {area, exchange, subscriber} = tel || new MyTel('', '', '');
this.parts.setValue({area, exchange, subscriber});
this.stateChanges.next();
}

constructor(
formBuilder: FormBuilder,
private _focusMonitor: FocusMonitor,
private _elementRef: ElementRef<HTMLElement>,
@Optional() @Self() public ngControl: NgControl) {

this.parts = formBuilder.group({
area: '',
exchange: '',
subscriber: '',
});

_focusMonitor.monitor(_elementRef, true).subscribe(origin => {
if (this.focused && !origin) {
this.onTouched();
}
this.focused = !!origin;
this.stateChanges.next();
});

if (this.ngControl != null) {
this.ngControl.valueAccessor = this;
}
}

ngOnDestroy() {
this.stateChanges.complete();
this._focusMonitor.stopMonitoring(this._elementRef);
}

setDescribedByIds(ids: string[]) {
this.describedBy = ids.join(' ');
}

onContainerClick(event: MouseEvent) {
if ((event.target as Element).tagName.toLowerCase() != 'input') {
this._elementRef.nativeElement.querySelector('input')!.focus();
}
}

writeValue(tel: MyTel | null): void {
this.value = tel;
}

registerOnChange(fn: any): void {
this.onChange = fn;
}

registerOnTouched(fn: any): void {
this.onTouched = fn;
}

setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}

_handleInput(): void {
this.onChange(this.parts.value);
}

static ngAcceptInputType_disabled: boolean | string | null | undefined;
static ngAcceptInputType_required: boolean | string | null | undefined;
}
1 change: 1 addition & 0 deletions src/dev-app/mdc-input/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ ng_module(
"mdc-input-demo.html",
],
deps = [
"//src/components-examples/material-experimental/mdc-form-field",
"//src/material-experimental/mdc-form-field",
"//src/material-experimental/mdc-input",
"//src/material/autocomplete",
Expand Down
4 changes: 4 additions & 0 deletions src/dev-app/mdc-input/mdc-input-demo-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
*/

import {CommonModule} from '@angular/common';
import {
MdcFormFieldExamplesModule
} from '@angular/components-examples/material-experimental/mdc-form-field';
import {NgModule} from '@angular/core';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MatFormFieldModule} from '@angular/material-experimental/mdc-form-field';
Expand Down Expand Up @@ -36,6 +39,7 @@ import {MdcInputDemo} from './mdc-input-demo';
MatInputModule,
MatTabsModule,
MatToolbarModule,
MdcFormFieldExamplesModule,
ReactiveFormsModule,
RouterModule.forChild([{path: '', component: MdcInputDemo}]),
],
Expand Down
20 changes: 20 additions & 0 deletions src/dev-app/mdc-input/mdc-input-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -650,3 +650,23 @@ <h3>&lt;textarea&gt; with bindable autosize </h3>
</mat-tab-group>
</mat-card-content>
</mat-card>

<mat-card class="demo-card demo-basic">
<mat-toolbar color="primary">Baseline</mat-toolbar>
<mat-card-content>
<span style="display: inline-block; margin-top: 20px">Shifted text</span>
<mat-form-field>
<mat-label>Label</mat-label>
<input matInput>
</mat-form-field>
</mat-card-content>
</mat-card>


<mat-card class="demo-card demo-basic">
<mat-toolbar color="primary">Examples</mat-toolbar>
<mat-card-content>
<h4>Custom control</h4>
<form-field-custom-control-example></form-field-custom-control-example>
</mat-card-content>
</mat-card>
20 changes: 20 additions & 0 deletions src/material-experimental/mdc-form-field/_form-field-sizing.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,23 @@ $mat-form-field-default-infix-width: 180px !default;
// Minimum amount of space between start and end hints in the subscript. MDC does not
// have built-in support for hints.
$mat-form-field-hint-min-space: 1em !default;

// Vertical spacing of the text-field if there is no label. MDC hard-codes the spacing
// into their styles, but their spacing variables would not work for our form-field
// structure anyway. This is because MDC's input elements are larger than the text, and
// their padding variables are calculated with respect to the vertical empty space of the
// inputs. We take the explicit numbers provided by the Material Design specification.
// https://material.io/components/text-fields/#specs
$mat-form-field-no-label-padding-bottom: 16px;
$mat-form-field-no-label-padding-top: 20px;

// Vertical spacing of the text-field if there is a label. MDC hard-codes the spacing
// into their styles, but their spacing variables would not work for our form-field
// structure anyway. This is because MDC's input elements are larger than the text, and
// their padding variables are calculated with respect to the vertical empty space of the
// inputs. We take the numbers provided by the Material Design specification. **Note** that
// the drawn values in the spec are slightly shifted because the spec assumes that typed input
// text exceeds the input element boundaries. We account for this since typed input text does
// not overflow in browsers by default.
$mat-form-field-with-label-input-padding-top: 24px;
$mat-form-field-with-label-input-padding-bottom: 12px;
Loading