Skip to content

Commit 42cbdfd

Browse files
committed
feat(form-field): add outline style (#9705)
* remove datepicker reliance on form-field's underlineRef * add spacing and alignment rules for outline variant * outline color & thickness * style tweaks * correctly position and size the gap * address comments
1 parent a099a6c commit 42cbdfd

15 files changed

+409
-40
lines changed

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,13 @@ <h3>&lt;textarea&gt; with ngModel</h3>
588588
<mat-hint>Please type something here</mat-hint>
589589
</mat-form-field>
590590

591+
<mat-form-field appearance="outline">
592+
<mat-label>Outline appearance</mat-label>
593+
<input matInput [(ngModel)]="outlineAppearance" required>
594+
<mat-error>This field is required</mat-error>
595+
<mat-hint>Please type something here</mat-hint>
596+
</mat-form-field>
597+
591598
<table style="width: 100%" cellspacing="0"><tr>
592599
<td>
593600
<mat-form-field appearance="legacy" style="width: 100%">
@@ -613,6 +620,14 @@ <h3>&lt;textarea&gt; with ngModel</h3>
613620
<mat-hint>Please type something here</mat-hint>
614621
</mat-form-field>
615622
</td>
623+
<td>
624+
<mat-form-field appearance="outline" style="width: 100%">
625+
<mat-label>Outline appearance</mat-label>
626+
<input matInput [(ngModel)]="outlineAppearance" required>
627+
<mat-error>This field is required</mat-error>
628+
<mat-hint>Please type something here</mat-hint>
629+
</mat-form-field>
630+
</td>
616631
</tr></table>
617632
</mat-card-content>
618633
</mat-card>

src/demo-app/input/input-demo.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export class InputDemo {
5555
legacyAppearance: string;
5656
standardAppearance: string;
5757
fillAppearance: string;
58+
outlineAppearance: string;
5859

5960
constructor() {
6061
setTimeout(() => this.delayedFormControl.setValue('hello'), 100);

src/lib/autocomplete/autocomplete-trigger.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
504504
}
505505

506506
private _getConnectedElement(): ElementRef {
507-
return this._formField ? this._formField._connectionContainerRef : this._element;
507+
return this._formField ? this._formField.getConnectedOverlayOrigin() : this._element;
508508
}
509509

510510
/** Returns the width of the input element, so the panel width can match it. */

src/lib/datepicker/datepicker-input.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -273,20 +273,17 @@ export class MatDatepickerInput<D> implements AfterContentInit, ControlValueAcce
273273
return this._validator ? this._validator(c) : null;
274274
}
275275

276-
/**
277-
* Gets the element that the datepicker popup should be connected to.
278-
* @return The element to connect the popup to.
279-
*/
276+
/** @deletion-target 7.0.0 Use `getConnectedOverlayOrigin` instead */
280277
getPopupConnectionElementRef(): ElementRef {
281-
return this._formField ? this._formField.underlineRef : this._elementRef;
278+
return this.getConnectedOverlayOrigin();
282279
}
283280

284281
/**
285-
* Determines the offset to be used when the calendar goes into a fallback position.
286-
* Primarily used to prevent the calendar from overlapping the input.
282+
* Gets the element that the datepicker popup should be connected to.
283+
* @return The element to connect the popup to.
287284
*/
288-
_getPopupFallbackOffset(): number {
289-
return this._formField ? -this._formField._inputContainerRef.nativeElement.clientHeight : 0;
285+
getConnectedOverlayOrigin(): ElementRef {
286+
return this._formField ? this._formField.getConnectedOverlayOrigin() : this._elementRef;
290287
}
291288

292289
// Implemented as part of ControlValueAccessor.

src/lib/datepicker/datepicker.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ describe('MatDatepicker', () => {
273273
});
274274

275275
it('should attach popup to native input', () => {
276-
let attachToRef = testComponent.datepickerInput.getPopupConnectionElementRef();
276+
let attachToRef = testComponent.datepickerInput.getConnectedOverlayOrigin();
277277
expect(attachToRef.nativeElement.tagName.toLowerCase())
278278
.toBe('input', 'popup should be attached to native input');
279279
});
@@ -791,7 +791,7 @@ describe('MatDatepicker', () => {
791791
}));
792792

793793
it('should attach popup to mat-form-field underline', () => {
794-
let attachToRef = testComponent.datepickerInput.getPopupConnectionElementRef();
794+
let attachToRef = testComponent.datepickerInput.getConnectedOverlayOrigin();
795795
expect(attachToRef.nativeElement.classList.contains('mat-form-field-underline'))
796796
.toBe(true, 'popup should be attached to mat-form-field underline');
797797
});

src/lib/datepicker/datepicker.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -378,18 +378,14 @@ export class MatDatepicker<D> implements OnDestroy {
378378

379379
/** Create the popup PositionStrategy. */
380380
private _createPopupPositionStrategy(): PositionStrategy {
381-
const fallbackOffset = this._datepickerInput._getPopupFallbackOffset();
382-
383381
return this._overlay.position()
384-
.connectedTo(this._datepickerInput.getPopupConnectionElementRef(),
382+
.connectedTo(this._datepickerInput.getConnectedOverlayOrigin(),
385383
{originX: 'start', originY: 'bottom'},
386384
{overlayX: 'start', overlayY: 'top'}
387385
)
388386
.withFallbackPosition(
389387
{originX: 'start', originY: 'top'},
390388
{overlayX: 'start', overlayY: 'bottom'},
391-
undefined,
392-
fallbackOffset
393389
)
394390
.withFallbackPosition(
395391
{originX: 'end', originY: 'bottom'},
@@ -398,8 +394,6 @@ export class MatDatepicker<D> implements OnDestroy {
398394
.withFallbackPosition(
399395
{originX: 'end', originY: 'top'},
400396
{overlayX: 'end', overlayY: 'bottom'},
401-
undefined,
402-
fallbackOffset
403397
);
404398
}
405399

src/lib/form-field/BUILD.bazel

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ ng_module(
1111
":form_field_css",
1212
":form_field_fill_css",
1313
":form_field_legacy_css",
14+
":form_field_outline_css",
1415
":form_field_standard_css",
1516
"//src/lib/input:input_css"
1617
],
@@ -40,6 +41,12 @@ sass_binary(
4041
deps = ["//src/lib/core:core_scss_lib"],
4142
)
4243

44+
sass_binary(
45+
name = "form_field_outline_scss",
46+
src = "form-field-outline.scss",
47+
deps = ["//src/lib/core:core_scss_lib"],
48+
)
49+
4350
sass_binary(
4451
name = "form_field_standard_scss",
4552
src = "form-field-standard.scss",
@@ -69,6 +76,13 @@ genrule(
6976
cmd = "cat $(locations :form_field_legacy_scss) > $@",
7077
)
7178

79+
genrule(
80+
name = "form_field_outline_css",
81+
srcs = [":form_field_outline_scss"],
82+
outs = ["form-field-outline.css"],
83+
cmd = "cat $(locations :form_field_outline_scss) > $@",
84+
)
85+
7286
genrule(
7387
name = "form_field_standard_css",
7488
srcs = [":form_field_standard_scss"],

src/lib/form-field/_form-field-fill-theme.scss

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,38 +60,36 @@ $mat-form-field-fill-dedupe: 0;
6060
$line-height: mat-line-height($config, input);
6161
// The amount to scale the font for the floating label and subscript.
6262
$subscript-font-scale: 0.75;
63-
// The padding on the infix. Mocks show half of the text size.
63+
// The padding on top of the infix.
6464
$infix-padding-top: 0.25em;
65+
// The padding below the infix.
6566
$infix-padding-bottom: 0.75em;
6667
// The margin applied to the form-field-infix to reserve space for the floating label.
6768
$infix-margin-top: 1em * $line-height * $subscript-font-scale;
68-
// The amount we offset the label in the fill appearance.
69-
$fill-appearance-label-offset: -0.5em * $line-height;
69+
// The amount we offset the label from the input text in the fill appearance.
70+
$fill-appearance-label-offset: -0.5em;
7071

7172
.mat-form-field-appearance-fill {
7273
.mat-form-field-infix {
7374
padding: $infix-padding-top 0 $infix-padding-bottom 0;
7475
}
7576

76-
.mat-form-field-label {
77-
margin-top: $fill-appearance-label-offset;
78-
}
79-
8077
.mat-form-field-label {
8178
top: $infix-margin-top + $infix-padding-top;
79+
margin-top: $fill-appearance-label-offset;
8280
}
8381

8482
&.mat-form-field-can-float {
8583
&.mat-form-field-should-float .mat-form-field-label,
8684
.mat-input-server:focus + .mat-form-field-label-wrapper .mat-form-field-label {
87-
@include _mat-form-field-label-floating(
85+
@include _mat-form-field-fill-label-floating(
8886
$subscript-font-scale, $infix-padding-top + $fill-appearance-label-offset,
8987
$infix-margin-top);
9088
}
9189

9290
.mat-form-field-autofill-control:-webkit-autofill + .mat-form-field-label-wrapper
9391
.mat-form-field-label {
94-
@include _mat-form-field-label-floating(
92+
@include _mat-form-field-fill-label-floating(
9593
$subscript-font-scale, $infix-padding-top + $fill-appearance-label-offset,
9694
$infix-margin-top);
9795
}
@@ -100,7 +98,7 @@ $mat-form-field-fill-dedupe: 0;
10098
// (used as a pure CSS stand-in for mat-form-field-should-float).
10199
.mat-input-server[label]:not(:label-shown) + .mat-form-field-label-wrapper
102100
.mat-form-field-label {
103-
@include _mat-form-field-label-floating(
101+
@include _mat-form-field-fill-label-floating(
104102
$subscript-font-scale, $infix-padding-top + $fill-appearance-label-offset,
105103
$infix-margin-top);
106104
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
@import '../core/theming/palette';
2+
@import '../core/theming/theming';
3+
@import '../core/style/form-common';
4+
@import '../core/typography/typography-utils';
5+
6+
7+
// Theme styles that only apply to the outline appearance of the form-field.
8+
9+
@mixin mat-form-field-outline-theme($theme) {
10+
$primary: map-get($theme, primary);
11+
$accent: map-get($theme, accent);
12+
$warn: map-get($theme, warn);
13+
$foreground: map-get($theme, foreground);
14+
$is-dark-theme: map-get($theme, is-dark);
15+
16+
$label-disabled-color: mat-color($foreground, disabled-text);
17+
$outline-color: mat-color($foreground, divider, if($is-dark-theme, 0.30, 0.12));
18+
$outline-color-hover: mat-color($foreground, divider, if($is-dark-theme, 1, 0.87));
19+
$outline-color-primary: mat-color($primary);
20+
$outline-color-accent: mat-color($accent);
21+
$outline-color-warn: mat-color($warn);
22+
$outline-color-disabled: mat-color($foreground, divider, if($is-dark-theme, 0.15, 0.06));
23+
24+
.mat-form-field-appearance-outline {
25+
.mat-form-field-outline {
26+
color: $outline-color;
27+
}
28+
29+
.mat-form-field-outline-thick {
30+
color: $outline-color-hover;
31+
}
32+
33+
&.mat-focused {
34+
.mat-form-field-outline-thick {
35+
color: $outline-color-primary;
36+
}
37+
38+
&.mat-accent .mat-form-field-outline-thick {
39+
color: $outline-color-accent;
40+
}
41+
42+
&.mat-warn .mat-form-field-outline-thick {
43+
color: $outline-color-warn;
44+
}
45+
}
46+
47+
// Class repeated so that rule is specific enough to override focused accent color case.
48+
&.mat-form-field-invalid.mat-form-field-invalid {
49+
.mat-form-field-outline-thick {
50+
color: $outline-color-warn;
51+
}
52+
}
53+
54+
&.mat-form-field-disabled {
55+
.mat-form-field-label {
56+
color: $label-disabled-color;
57+
}
58+
59+
.mat-form-field-outline {
60+
color: $outline-color-disabled;
61+
}
62+
}
63+
}
64+
}
65+
66+
// Used to make instances of the _mat-form-field-label-floating mixin negligibly different,
67+
// and prevent Google's CSS Optimizer from collapsing the declarations. This is needed because some
68+
// of the selectors contain pseudo-classes not recognized in all browsers. If a browser encounters
69+
// an unknown pseudo-class it will discard the entire rule set.
70+
$mat-form-field-outline-dedupe: 0;
71+
72+
// Applies a floating label above the form field control itself.
73+
@mixin _mat-form-field-outline-label-floating($font-scale, $infix-padding, $infix-margin-top) {
74+
transform: translateY(-$infix-margin-top - $infix-padding + $mat-form-field-outline-dedupe)
75+
scale($font-scale);
76+
width: 100% / $font-scale + $mat-form-field-outline-dedupe;
77+
78+
$mat-form-field-fill-dedupe: $mat-form-field-outline-dedupe + 0.00001 !global;
79+
}
80+
81+
@mixin mat-form-field-outline-typography($config) {
82+
// The unit-less line-height from the font config.
83+
$line-height: mat-line-height($config, input);
84+
// The amount to scale the font for the floating label and subscript.
85+
$subscript-font-scale: 0.75;
86+
// The padding above and below the infix.
87+
$infix-padding: 1em;
88+
// The margin applied to the form-field-infix to reserve space for the floating label.
89+
$infix-margin-top: 1em * $line-height * $subscript-font-scale;
90+
// The space between the bottom of the .mat-form-field-flex area and the subscript wrapper.
91+
// Mocks show half of the text size, but this margin is applied to an element with the subscript
92+
// text font size, so we need to divide by the scale factor to make it half of the original text
93+
// size.
94+
$subscript-margin-top: 0.5em / $subscript-font-scale;
95+
// The padding applied to the form-field-wrapper to reserve space for the subscript, since it's
96+
// absolutely positioned. This is a combination of the subscript's margin and line-height, but we
97+
// need to multiply by the subscript font scale factor since the wrapper has a larger font size.
98+
$wrapper-padding-bottom: ($subscript-margin-top + $line-height) * $subscript-font-scale;
99+
// The amount we offset the label from the input text in the outline appearance.
100+
$outline-appearance-label-offset: -0.25em;
101+
102+
.mat-form-field-appearance-outline {
103+
.mat-form-field-infix {
104+
padding: $infix-padding 0 $infix-padding 0;
105+
}
106+
107+
.mat-form-field-outline {
108+
// We want the bottom of the outline to start at the end of the content box, not the padding
109+
// box, so we move it up by the padding amount.
110+
bottom: $wrapper-padding-bottom;
111+
}
112+
113+
.mat-form-field-label {
114+
top: $infix-margin-top + $infix-padding;
115+
margin-top: $outline-appearance-label-offset;
116+
}
117+
118+
&.mat-form-field-can-float {
119+
&.mat-form-field-should-float .mat-form-field-label,
120+
.mat-input-server:focus + .mat-form-field-label-wrapper .mat-form-field-label {
121+
@include _mat-form-field-outline-label-floating(
122+
$subscript-font-scale, $infix-padding + $outline-appearance-label-offset,
123+
$infix-margin-top);
124+
}
125+
126+
.mat-form-field-autofill-control:-webkit-autofill + .mat-form-field-label-wrapper
127+
.mat-form-field-label {
128+
@include _mat-form-field-outline-label-floating(
129+
$subscript-font-scale, $infix-padding + $outline-appearance-label-offset,
130+
$infix-margin-top);
131+
}
132+
133+
// Server-side rendered matInput with a label attribute but label not shown
134+
// (used as a pure CSS stand-in for mat-form-field-should-float).
135+
.mat-input-server[label]:not(:label-shown) + .mat-form-field-label-wrapper
136+
.mat-form-field-label {
137+
@include _mat-form-field-outline-label-floating(
138+
$subscript-font-scale, $infix-padding + $outline-appearance-label-offset,
139+
$infix-margin-top);
140+
}
141+
}
142+
}
143+
}

src/lib/form-field/_form-field-theme.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
@import '../core/typography/typography-utils';
55
@import 'form-field-fill-theme.scss';
66
@import 'form-field-legacy-theme.scss';
7+
@import 'form-field-outline-theme.scss';
78
@import 'form-field-standard-theme.scss';
89

910

@@ -95,6 +96,7 @@
9596
@include mat-form-field-legacy-theme($theme);
9697
@include mat-form-field-standard-theme($theme);
9798
@include mat-form-field-fill-theme($theme);
99+
@include mat-form-field-outline-theme($theme);
98100
}
99101

100102
// Used to make instances of the _mat-form-field-label-floating mixin negligibly different,
@@ -222,4 +224,5 @@ $mat-form-field-dedupe: 0;
222224
@include mat-form-field-legacy-typography($config);
223225
@include mat-form-field-standard-typography($config);
224226
@include mat-form-field-fill-typography($config);
227+
@include mat-form-field-outline-typography($config);
225228
}

0 commit comments

Comments
 (0)