Skip to content

Commit 0c26354

Browse files
devversionandrewseguin
authored andcommitted
fix(material-experimental/mdc-form-field): fix baseline and handle custom controls better (#18161)
* Fixes the baseline of form-field controls and their inputs. Previously the baseline was incorrect due to flex alignment. * Improves support for custom form-field controls by ensuring that spacing applied to MDC inputs, also applies to custom controls. * The same will be needed for the outline appearance, but unfortunately we cannot apply any spacing to the infix until we find a solution for: material-components/material-components-web#5326
1 parent 500d235 commit 0c26354

14 files changed

+357
-26
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package(default_visibility = ["//visibility:public"])
2+
3+
load("//tools:defaults.bzl", "ng_module")
4+
5+
ng_module(
6+
name = "mdc-form-field",
7+
srcs = glob(["**/*.ts"]),
8+
assets = glob([
9+
"**/*.html",
10+
"**/*.css",
11+
]),
12+
module_name = "@angular/components-examples/material-experimental/mdc-form-field",
13+
deps = [
14+
"//src/cdk/a11y",
15+
"//src/cdk/coercion",
16+
"//src/material-experimental/mdc-form-field",
17+
"//src/material-experimental/mdc-input",
18+
"//src/material/icon",
19+
"@npm//@angular/common",
20+
"@npm//@angular/forms",
21+
"@npm//rxjs",
22+
],
23+
)
24+
25+
filegroup(
26+
name = "source-files",
27+
srcs = glob([
28+
"**/*.html",
29+
"**/*.css",
30+
"**/*.ts",
31+
]),
32+
)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {CommonModule} from '@angular/common';
2+
import {NgModule} from '@angular/core';
3+
import {ReactiveFormsModule} from '@angular/forms';
4+
import {MatFormFieldModule} from '@angular/material-experimental/mdc-form-field';
5+
import {MatIconModule} from '@angular/material/icon';
6+
import {
7+
FormFieldCustomControlExample,
8+
MyTelInput
9+
} from './mdc-form-field-custom-control/form-field-custom-control-example';
10+
11+
export {
12+
FormFieldCustomControlExample,
13+
MyTelInput,
14+
};
15+
16+
const EXAMPLES = [
17+
FormFieldCustomControlExample,
18+
];
19+
20+
@NgModule({
21+
imports: [
22+
CommonModule,
23+
MatFormFieldModule,
24+
MatIconModule,
25+
ReactiveFormsModule,
26+
],
27+
declarations: [...EXAMPLES, MyTelInput],
28+
exports: EXAMPLES,
29+
})
30+
export class MdcFormFieldExamplesModule {
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
.example-tel-input-container {
2+
display: flex;
3+
}
4+
5+
.example-tel-input-element {
6+
border: none;
7+
background: none;
8+
padding: 0;
9+
outline: none;
10+
font: inherit;
11+
text-align: center;
12+
}
13+
14+
.example-tel-input-spacer {
15+
opacity: 0;
16+
transition: opacity 200ms;
17+
}
18+
19+
:host.example-floating .example-tel-input-spacer {
20+
opacity: 1;
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<div [formGroup]="parts" class="example-tel-input-container">
2+
<input class="example-tel-input-element" formControlName="area" size="3"
3+
aria-label="Area code" (input)="_handleInput()">
4+
<span class="example-tel-input-spacer">&ndash;</span>
5+
<input class="example-tel-input-element" formControlName="exchange" size="3"
6+
aria-label="Exchange code" (input)="_handleInput()">
7+
<span class="example-tel-input-spacer">&ndash;</span>
8+
<input class="example-tel-input-element" formControlName="subscriber" size="4"
9+
aria-label="Subscriber number" (input)="_handleInput()">
10+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/** No CSS for this example */
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<mat-form-field>
2+
<mat-label>Phone number</mat-label>
3+
<example-tel-input required></example-tel-input>
4+
<mat-icon matSuffix>phone</mat-icon>
5+
<mat-hint>Include area code</mat-hint>
6+
</mat-form-field>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import {FocusMonitor} from '@angular/cdk/a11y';
2+
import {coerceBooleanProperty} from '@angular/cdk/coercion';
3+
import {Component, ElementRef, Input, OnDestroy, Optional, Self} from '@angular/core';
4+
import {FormBuilder, FormGroup, ControlValueAccessor, NgControl} from '@angular/forms';
5+
import {MatFormFieldControl} from '@angular/material-experimental/mdc-form-field';
6+
import {Subject} from 'rxjs';
7+
8+
/** @title Form field with custom telephone number input control. */
9+
@Component({
10+
selector: 'form-field-custom-control-example',
11+
templateUrl: 'form-field-custom-control-example.html',
12+
styleUrls: ['form-field-custom-control-example.css'],
13+
})
14+
export class FormFieldCustomControlExample {}
15+
16+
/** Data structure for holding telephone number. */
17+
export class MyTel {
18+
constructor(public area: string, public exchange: string, public subscriber: string) {}
19+
}
20+
21+
/** Custom `MatFormFieldControl` for telephone number input. */
22+
@Component({
23+
selector: 'example-tel-input',
24+
templateUrl: 'example-tel-input-example.html',
25+
styleUrls: ['example-tel-input-example.css'],
26+
providers: [{provide: MatFormFieldControl, useExisting: MyTelInput}],
27+
host: {
28+
'[class.example-floating]': 'shouldLabelFloat',
29+
'[id]': 'id',
30+
'[attr.aria-describedby]': 'describedBy',
31+
}
32+
})
33+
export class MyTelInput implements ControlValueAccessor, MatFormFieldControl<MyTel>, OnDestroy {
34+
static nextId = 0;
35+
36+
parts: FormGroup;
37+
stateChanges = new Subject<void>();
38+
focused = false;
39+
errorState = false;
40+
controlType = 'example-tel-input';
41+
id = `example-tel-input-${MyTelInput.nextId++}`;
42+
describedBy = '';
43+
onChange = (_: any) => {};
44+
onTouched = () => {};
45+
46+
get empty() {
47+
const {value: {area, exchange, subscriber}} = this.parts;
48+
49+
return !area && !exchange && !subscriber;
50+
}
51+
52+
get shouldLabelFloat() { return this.focused || !this.empty; }
53+
54+
@Input()
55+
get placeholder(): string { return this._placeholder; }
56+
set placeholder(value: string) {
57+
this._placeholder = value;
58+
this.stateChanges.next();
59+
}
60+
private _placeholder: string;
61+
62+
@Input()
63+
get required(): boolean { return this._required; }
64+
set required(value: boolean) {
65+
this._required = coerceBooleanProperty(value);
66+
this.stateChanges.next();
67+
}
68+
private _required = false;
69+
70+
@Input()
71+
get disabled(): boolean { return this._disabled; }
72+
set disabled(value: boolean) {
73+
this._disabled = coerceBooleanProperty(value);
74+
this._disabled ? this.parts.disable() : this.parts.enable();
75+
this.stateChanges.next();
76+
}
77+
private _disabled = false;
78+
79+
@Input()
80+
get value(): MyTel | null {
81+
const {value: {area, exchange, subscriber}} = this.parts;
82+
if (area.length === 3 && exchange.length === 3 && subscriber.length === 4) {
83+
return new MyTel(area, exchange, subscriber);
84+
}
85+
return null;
86+
}
87+
set value(tel: MyTel | null) {
88+
const {area, exchange, subscriber} = tel || new MyTel('', '', '');
89+
this.parts.setValue({area, exchange, subscriber});
90+
this.stateChanges.next();
91+
}
92+
93+
constructor(
94+
formBuilder: FormBuilder,
95+
private _focusMonitor: FocusMonitor,
96+
private _elementRef: ElementRef<HTMLElement>,
97+
@Optional() @Self() public ngControl: NgControl) {
98+
99+
this.parts = formBuilder.group({
100+
area: '',
101+
exchange: '',
102+
subscriber: '',
103+
});
104+
105+
_focusMonitor.monitor(_elementRef, true).subscribe(origin => {
106+
if (this.focused && !origin) {
107+
this.onTouched();
108+
}
109+
this.focused = !!origin;
110+
this.stateChanges.next();
111+
});
112+
113+
if (this.ngControl != null) {
114+
this.ngControl.valueAccessor = this;
115+
}
116+
}
117+
118+
ngOnDestroy() {
119+
this.stateChanges.complete();
120+
this._focusMonitor.stopMonitoring(this._elementRef);
121+
}
122+
123+
setDescribedByIds(ids: string[]) {
124+
this.describedBy = ids.join(' ');
125+
}
126+
127+
onContainerClick(event: MouseEvent) {
128+
if ((event.target as Element).tagName.toLowerCase() != 'input') {
129+
this._elementRef.nativeElement.querySelector('input')!.focus();
130+
}
131+
}
132+
133+
writeValue(tel: MyTel | null): void {
134+
this.value = tel;
135+
}
136+
137+
registerOnChange(fn: any): void {
138+
this.onChange = fn;
139+
}
140+
141+
registerOnTouched(fn: any): void {
142+
this.onTouched = fn;
143+
}
144+
145+
setDisabledState(isDisabled: boolean): void {
146+
this.disabled = isDisabled;
147+
}
148+
149+
_handleInput(): void {
150+
this.onChange(this.parts.value);
151+
}
152+
153+
static ngAcceptInputType_disabled: boolean | string | null | undefined;
154+
static ngAcceptInputType_required: boolean | string | null | undefined;
155+
}

src/dev-app/mdc-input/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ ng_module(
1010
"mdc-input-demo.html",
1111
],
1212
deps = [
13+
"//src/components-examples/material-experimental/mdc-form-field",
1314
"//src/material-experimental/mdc-form-field",
1415
"//src/material-experimental/mdc-input",
1516
"//src/material/autocomplete",

src/dev-app/mdc-input/mdc-input-demo-module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
*/
88

99
import {CommonModule} from '@angular/common';
10+
import {
11+
MdcFormFieldExamplesModule
12+
} from '@angular/components-examples/material-experimental/mdc-form-field';
1013
import {NgModule} from '@angular/core';
1114
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
1215
import {MatFormFieldModule} from '@angular/material-experimental/mdc-form-field';
@@ -36,6 +39,7 @@ import {MdcInputDemo} from './mdc-input-demo';
3639
MatInputModule,
3740
MatTabsModule,
3841
MatToolbarModule,
42+
MdcFormFieldExamplesModule,
3943
ReactiveFormsModule,
4044
RouterModule.forChild([{path: '', component: MdcInputDemo}]),
4145
],

src/dev-app/mdc-input/mdc-input-demo.html

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,3 +650,23 @@ <h3>&lt;textarea&gt; with bindable autosize </h3>
650650
</mat-tab-group>
651651
</mat-card-content>
652652
</mat-card>
653+
654+
<mat-card class="demo-card demo-basic">
655+
<mat-toolbar color="primary">Baseline</mat-toolbar>
656+
<mat-card-content>
657+
<span style="display: inline-block; margin-top: 20px">Shifted text</span>
658+
<mat-form-field>
659+
<mat-label>Label</mat-label>
660+
<input matInput>
661+
</mat-form-field>
662+
</mat-card-content>
663+
</mat-card>
664+
665+
666+
<mat-card class="demo-card demo-basic">
667+
<mat-toolbar color="primary">Examples</mat-toolbar>
668+
<mat-card-content>
669+
<h4>Custom control</h4>
670+
<form-field-custom-control-example></form-field-custom-control-example>
671+
</mat-card-content>
672+
</mat-card>

src/material-experimental/mdc-form-field/_form-field-sizing.scss

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,23 @@ $mat-form-field-default-infix-width: 180px !default;
1515
// Minimum amount of space between start and end hints in the subscript. MDC does not
1616
// have built-in support for hints.
1717
$mat-form-field-hint-min-space: 1em !default;
18+
19+
// Vertical spacing of the text-field if there is no label. MDC hard-codes the spacing
20+
// into their styles, but their spacing variables would not work for our form-field
21+
// structure anyway. This is because MDC's input elements are larger than the text, and
22+
// their padding variables are calculated with respect to the vertical empty space of the
23+
// inputs. We take the explicit numbers provided by the Material Design specification.
24+
// https://material.io/components/text-fields/#specs
25+
$mat-form-field-no-label-padding-bottom: 16px;
26+
$mat-form-field-no-label-padding-top: 20px;
27+
28+
// Vertical spacing of the text-field if there is a label. MDC hard-codes the spacing
29+
// into their styles, but their spacing variables would not work for our form-field
30+
// structure anyway. This is because MDC's input elements are larger than the text, and
31+
// their padding variables are calculated with respect to the vertical empty space of the
32+
// inputs. We take the numbers provided by the Material Design specification. **Note** that
33+
// the drawn values in the spec are slightly shifted because the spec assumes that typed input
34+
// text exceeds the input element boundaries. We account for this since typed input text does
35+
// not overflow in browsers by default.
36+
$mat-form-field-with-label-input-padding-top: 24px;
37+
$mat-form-field-with-label-input-padding-bottom: 12px;

0 commit comments

Comments
 (0)