From 966a160c5651870ac278671cf8da8c655cea0c26 Mon Sep 17 00:00:00 2001 From: mmalerba Date: Thu, 25 Jan 2018 16:15:41 -0800 Subject: [PATCH 01/13] feat(form-field): support for different spec variants (#9366) * feat(form-field): implement hover state [Based on the spec](https://material.io/guidelines/components/text-fields.html#text-fields-states) form fields should have a hover state where the underline gets darkened while the user is hovering and gets replaced by the theme color after the focus the input. * branch css logic * extract underline css into standard variant * box variant underline * box variant spacing * add legacy variant * fix select ellipsis * move hover state changes out of legacy variant * add variants section to input demo * use filter/backface-visibility to address label jumpiness * address comments * fix box variant text fuzziness * remove bluriness fixes that aren't needed anymore * address comments * remove the floatLabel=never option in the new variants * variant --> appearance * add tests for new label & placeholder behavior * update demo to use mat-label * fix bazel build --- .../a11y/autocomplete/autocomplete-a11y.html | 3 +- .../a11y/datepicker/datepicker-a11y.html | 16 +- .../a11y/dialog/dialog-address-form-a11y.html | 24 ++- src/demo-app/a11y/input/input-a11y.html | 20 +- src/demo-app/a11y/select/select-a11y.html | 27 ++- .../autocomplete/autocomplete-demo.html | 8 +- src/demo-app/baseline/baseline-demo.html | 18 +- src/demo-app/datepicker/datepicker-demo.html | 27 +-- src/demo-app/dialog/dialog-demo.html | 42 ++-- src/demo-app/dialog/dialog-demo.ts | 3 +- src/demo-app/expansion/expansion-demo.html | 6 +- src/demo-app/input/input-demo.html | 192 +++++++++++++----- src/demo-app/input/input-demo.ts | 4 + src/demo-app/ripple/ripple-demo.html | 6 +- src/demo-app/select/select-demo.html | 28 ++- src/demo-app/snack-bar/snack-bar-demo.html | 4 +- src/demo-app/stepper/stepper-demo.html | 48 +++-- src/demo-app/table/table-demo.html | 3 +- src/demo-app/tabs/tabs-demo.html | 15 +- src/lib/form-field/BUILD.bazel | 44 +++- src/lib/form-field/_form-field-box-theme.scss | 93 +++++++++ .../form-field/_form-field-legacy-theme.scss | 123 +++++++++++ .../_form-field-standard-theme.scss | 26 +++ src/lib/form-field/_form-field-theme.scss | 74 ++++--- src/lib/form-field/form-field-box.scss | 63 ++++++ src/lib/form-field/form-field-legacy.scss | 36 ++++ src/lib/form-field/form-field-standard.scss | 47 +++++ src/lib/form-field/form-field.scss | 60 +++--- src/lib/form-field/form-field.ts | 45 +++- src/lib/input/input.spec.ts | 89 +++++++- src/lib/select/select.scss | 7 + 31 files changed, 952 insertions(+), 249 deletions(-) create mode 100644 src/lib/form-field/_form-field-box-theme.scss create mode 100644 src/lib/form-field/_form-field-legacy-theme.scss create mode 100644 src/lib/form-field/_form-field-standard-theme.scss create mode 100644 src/lib/form-field/form-field-box.scss create mode 100644 src/lib/form-field/form-field-legacy.scss create mode 100644 src/lib/form-field/form-field-standard.scss diff --git a/src/demo-app/a11y/autocomplete/autocomplete-a11y.html b/src/demo-app/a11y/autocomplete/autocomplete-a11y.html index e84762443315..95c5df20d85e 100644 --- a/src/demo-app/a11y/autocomplete/autocomplete-a11y.html +++ b/src/demo-app/a11y/autocomplete/autocomplete-a11y.html @@ -2,7 +2,8 @@

Filtering and selection

Select your favorite state

- Search for a state + diff --git a/src/demo-app/a11y/datepicker/datepicker-a11y.html b/src/demo-app/a11y/datepicker/datepicker-a11y.html index 9b1301e668ac..48692bf20fbd 100644 --- a/src/demo-app/a11y/datepicker/datepicker-a11y.html +++ b/src/demo-app/a11y/datepicker/datepicker-a11y.html @@ -1,13 +1,13 @@

Choose a date (e.g. choose your date of birth)

+ Date of birth + required> @@ -44,14 +44,14 @@

Choose a date with touch UI (e.g. choose a payment date on mobile)

Choose date with startAt, min and max (e.g. schedule a departing and returning flight)

+ Departure date + required> @@ -65,13 +65,13 @@

Choose date with startAt, min and max (e.g. schedule a departing and returni + Return date + [max]="maxTripDate"> @@ -89,6 +89,7 @@

Choose date with startAt, min and max (e.g. schedule a departing and returni

Choose date with date filter (e.g. schedule a doctor's appointment)

+ Appointment date Choose date with date filter (e.g. schedule a doctor's appointment)

[min]="minAppointmentDate" [max]="maxAppointmentDate" [matDatepickerFilter]="weekdaysOnly" - required - placeholder="Appointment date"> + required> diff --git a/src/demo-app/a11y/dialog/dialog-address-form-a11y.html b/src/demo-app/a11y/dialog/dialog-address-form-a11y.html index abbeb3440645..7c0644ef6b0c 100644 --- a/src/demo-app/a11y/dialog/dialog-address-form-a11y.html +++ b/src/demo-app/a11y/dialog/dialog-address-form-a11y.html @@ -3,36 +3,44 @@

Company

- + Company (disabled) +
- + First name + - + Long last name that will be truncated +

- + Address + - + Address 2 +

- + City + - + State + - + Postal code + {{postalCode.value.length}} / 5
diff --git a/src/demo-app/a11y/input/input-a11y.html b/src/demo-app/a11y/input/input-a11y.html index cb9d19dfb345..006b85707ac3 100644 --- a/src/demo-app/a11y/input/input-a11y.html +++ b/src/demo-app/a11y/input/input-a11y.html @@ -1,18 +1,20 @@

Basic input box (e.g. name field)

- + First name + - + Last name +

Input with hint (e.g. password field)

- + Password +

[max]="maxDate" [matDatepickerFilter]="filterOdd ? dateFilter : null" [disabled]="inputDisabled" - placeholder="Pick a date" (dateInput)="onDateInput($event)" (dateChange)="onDateChange($event)"> Input disabled datepicker

+ Input disabled + [matDatepickerFilter]="filterOdd ? dateFilter : null" disabled> @@ -98,9 +101,9 @@

Input disabled via FormControl

+ FormControl disabled + [max]="maxDate" [matDatepickerFilter]="filterOdd ? dateFilter : null"> @@ -114,9 +117,9 @@

Input disabled, datepicker popup enabled

+ Input disabled, datepicker enabled + [max]="maxDate" [matDatepickerFilter]="filterOdd ? dateFilter : null"> @@ -126,9 +129,9 @@

Datepicker with value property binding

+ Value binding + [max]="maxDate" [matDatepickerFilter]="filterOdd ? dateFilter : null"> diff --git a/src/demo-app/dialog/dialog-demo.html b/src/demo-app/dialog/dialog-demo.html index dcb68eb03872..a4e2fba1eecb 100644 --- a/src/demo-app/dialog/dialog-demo.html +++ b/src/demo-app/dialog/dialog-demo.html @@ -16,28 +16,34 @@

Dialog dimensions

- + Width + - + Height +

- + Min width + - + Min height +

- + Max width + - + Max height +

@@ -45,19 +51,23 @@

Dialog position

- + Top + - + Bottom +

- + Left + - + Right +

@@ -65,7 +75,8 @@

Dialog backdrop

- + Backdrop class +

@@ -75,7 +86,8 @@

Other options

- + Button alignment + Start End Center @@ -85,7 +97,8 @@

Other options

- + Dialog message +

@@ -104,7 +117,8 @@

Other options

It's Jazz!

- + How much? +

{{ data.message }}

diff --git a/src/demo-app/dialog/dialog-demo.ts b/src/demo-app/dialog/dialog-demo.ts index 8e4ba9e3bd7a..b519069b7dc3 100644 --- a/src/demo-app/dialog/dialog-demo.ts +++ b/src/demo-app/dialog/dialog-demo.ts @@ -92,7 +92,8 @@ export class DialogDemo {

It's Jazz!

- + How much? +

{{ data.message }}

diff --git a/src/demo-app/expansion/expansion-demo.html b/src/demo-app/expansion/expansion-demo.html index 9e4ecb03f322..d3b0a94b375f 100644 --- a/src/demo-app/expansion/expansion-demo.html +++ b/src/demo-app/expansion/expansion-demo.html @@ -19,11 +19,13 @@

Single Expansion Panel


- + Collapsed height + - + Expanded height +
diff --git a/src/demo-app/input/input-demo.html b/src/demo-app/input/input-demo.html index d61f52c99508..c9932a43e58a 100644 --- a/src/demo-app/input/input-demo.html +++ b/src/demo-app/input/input-demo.html @@ -3,44 +3,51 @@ - + Company (disabled) +
- + First name + - + Long last name that will be truncated +

- + Address + - + Address 2 +

@@ -377,10 +410,13 @@

Textarea

Forms - + + Reactive + - + Template +
- + Delayed value +
@@ -509,10 +546,10 @@

Regular <textarea>

<textarea> with mat-form-field

+ Autosized textarea + matAutosizeMaxRows="10">
@@ -526,3 +563,56 @@

<textarea> with ngModel

+ + + Appearance + + + Legacy appearance + + This field is required + Please type something here + + + + Standard appearance + + This field is required + Please type something here + + + + Box appearance + + This field is required + Please type something here + + +
- + City + - + State + - + Postal code + mode_edit {{postalCode.value.length}} / 5 @@ -59,12 +66,14 @@

Regular

- + Example + This field is required - + Email + This field is required @@ -77,7 +86,8 @@

Regular

With hint

- + Example + This field is required Please type something here @@ -87,8 +97,8 @@

With hint

Inside a form

- + Example + This field is required @@ -97,8 +107,8 @@

Inside a form

With a custom error function

+ Example @@ -113,21 +123,24 @@

With a custom error function

Text

- + Amount + .00

Icons

- + Amount + attach_money mode_edit

Icon buttons

- + Amount + @@ -139,37 +152,46 @@

Icon buttons

Input

- + Default color + - + Accent color + - + Warn color +

Textarea

- + Default color + - + Accent color + - + Warn color +

With error

- + Default color + This field is required - + Accent color + This field is required - + Warn color + This field is required
@@ -181,9 +203,9 @@

With error

Input

+ Character count (100 max) @@ -195,9 +217,9 @@

Input

Textarea

+ Character count (100 max) {{characterCountTextareaHintExample.value.length}} / 100 @@ -212,7 +234,8 @@

Textarea

Hello  - + First name + , how are you? @@ -220,35 +243,40 @@

Textarea

- + Disabled field + - + Required field +

- + 100% width label +

- + Character count (100 max) + {{input.value.length}} / 100

- + Show hint label +

- - I favorite bold placeholder - + + I favorite bold label + I also home italic hint labels @@ -256,13 +284,15 @@

Textarea

- + Show hint label with character count + {{hintLabelWithCharCount.value.length}}

- + Enter text to count + Enter some text to count how many characters are in it. The character count is shown on the right. @@ -305,7 +335,8 @@

Textarea

- + Label +

@@ -318,17 +349,20 @@

Textarea

- + Prefixed +

Example: 
- + Suffixed + .00 €
Both: - + Email address + email   @gmail.com @@ -358,10 +392,9 @@

Textarea

{{i+1}} + Value
+ + + +
+ + Legacy appearance + + This field is required + Please type something here + + + + Standard appearance + + This field is required + Please type something here + + + + Box appearance + + This field is required + Please type something here + +
+
+ diff --git a/src/demo-app/input/input-demo.ts b/src/demo-app/input/input-demo.ts index 67cc69667381..04ed022cb3b6 100644 --- a/src/demo-app/input/input-demo.ts +++ b/src/demo-app/input/input-demo.ts @@ -52,6 +52,10 @@ export class InputDemo { delayedFormControl = new FormControl(''); model = 'hello'; + legacyAppearance: string; + standardAppearance: string; + boxAppearance: string; + constructor() { setTimeout(() => this.delayedFormControl.setValue('hello'), 100); } diff --git a/src/demo-app/ripple/ripple-demo.html b/src/demo-app/ripple/ripple-demo.html index fec5235a4f22..cb5a387431fb 100644 --- a/src/demo-app/ripple/ripple-demo.html +++ b/src/demo-app/ripple/ripple-demo.html @@ -28,10 +28,12 @@
- + Ripple radius + - + Ripple color +
diff --git a/src/demo-app/select/select-demo.html b/src/demo-app/select/select-demo.html index dd1ca63c25a5..0d199f581348 100644 --- a/src/demo-app/select/select-demo.html +++ b/src/demo-app/select/select-demo.html @@ -7,7 +7,8 @@ ngModel - Drink + None @@ -50,7 +51,8 @@ - Pokemon + {{ creature.viewValue }} @@ -78,7 +80,8 @@ Without Angular forms - + Digimon + None {{ creature.viewValue }} @@ -97,7 +100,8 @@ - + Pokemon + @@ -114,11 +118,11 @@ compareWith - + Drink + {{ drink.viewValue }} @@ -146,7 +150,8 @@ - + Food I would like to eat + {{ food.viewValue }} @@ -167,7 +172,8 @@ - + Starter pokemon + {{ creature.viewValue }} diff --git a/src/demo-app/snack-bar/snack-bar-demo.html b/src/demo-app/snack-bar/snack-bar-demo.html index 1bdb4819e7aa..9593e6034324 100644 --- a/src/demo-app/snack-bar/snack-bar-demo.html +++ b/src/demo-app/snack-bar/snack-bar-demo.html @@ -25,10 +25,10 @@

SnackBar demo

Show button on snack bar

+ Snack bar action label
@@ -37,10 +37,10 @@

SnackBar demo

Auto hide after duration

+ Auto hide duration in ms
diff --git a/src/demo-app/stepper/stepper-demo.html b/src/demo-app/stepper/stepper-demo.html index cd81828ebfc1..b1a0c020fdcc 100644 --- a/src/demo-app/stepper/stepper-demo.html +++ b/src/demo-app/stepper/stepper-demo.html @@ -6,12 +6,14 @@

Linear Vertical Stepper Demo using a single form

Fill out your name - + First name + This field is required - + Last name + This field is required
@@ -24,7 +26,8 @@

Linear Vertical Stepper Demo using a single form

Fill out your email address
- + Email address + The input is invalid.
@@ -50,11 +53,13 @@

Linear Horizontal Stepper Demo using a different form for each step

Fill out your name - + First name + This field is required - + Last name + This field is required
@@ -67,7 +72,8 @@

Linear Horizontal Stepper Demo using a different form for each step

Fill out your email address - + Email address + The input is invalid
@@ -95,11 +101,13 @@

Vertical Stepper Demo

Fill out your name - + First name + - + Last name +
@@ -111,7 +119,8 @@

Vertical Stepper Demo

Fill out your phone number
- + Phone number +
@@ -124,7 +133,8 @@

Vertical Stepper Demo

Fill out your address
- + Address +
@@ -145,11 +155,13 @@

Horizontal Stepper Demo with Text Label

- + First name + - + Last name +
@@ -158,7 +170,8 @@

Horizontal Stepper Demo with Text Label

- + Phone number +
@@ -168,7 +181,8 @@

Horizontal Stepper Demo with Text Label

- + Address +
@@ -189,7 +203,8 @@

Horizontal Stepper Demo with Templated Label

{{step.label}} - + Answer +
@@ -202,7 +217,8 @@

Stepper with autosize textarea

- + Autosize textarea + diff --git a/src/demo-app/table/table-demo.html b/src/demo-app/table/table-demo.html index 2dee28f66b9c..7edfc1d94437 100644 --- a/src/demo-app/table/table-demo.html +++ b/src/demo-app/table/table-demo.html @@ -232,7 +232,8 @@

MatTable Using 'When' Rows for Interactive Details

MatTable With MatTableDataSource Example

- + Filter users +
diff --git a/src/demo-app/tabs/tabs-demo.html b/src/demo-app/tabs/tabs-demo.html index 8b85ef763f3d..8c1ac8261ad0 100644 --- a/src/demo-app/tabs/tabs-demo.html +++ b/src/demo-app/tabs/tabs-demo.html @@ -89,7 +89,8 @@

Tab Group Demo - Dynamic Tabs



- + Tab label +

@@ -187,7 +189,8 @@

Tab Group Demo - Fixed Height



- + Tab label +
@@ -217,7 +220,8 @@

Async Tabs



- + Tab label +
@@ -283,7 +287,8 @@

Tabs with autosize textarea

- + Autosize textarea +
diff --git a/src/lib/form-field/BUILD.bazel b/src/lib/form-field/BUILD.bazel index f88d4b2e648d..77b35dda754e 100644 --- a/src/lib/form-field/BUILD.bazel +++ b/src/lib/form-field/BUILD.bazel @@ -9,6 +9,9 @@ ng_module( module_name = "@angular/material/form-field", assets = [ ":form_field_css", + ":form_field_box_css", + ":form_field_legacy_css", + ":form_field_standard_css", "//src/lib/input:input_css" ], deps = [ @@ -19,14 +22,31 @@ ng_module( tsconfig = ":tsconfig-build.json", ) - sass_binary( name = "form_field_scss", src = "form-field.scss", deps = ["//src/lib/core:core_scss_lib"], ) -# TODO(jelbourn): remove this when sass_binary supports specifying an output filename and dir. +sass_binary( + name = "form_field_box_scss", + src = "form-field-box.scss", + deps = ["//src/lib/core:core_scss_lib"], +) + +sass_binary( + name = "form_field_legacy_scss", + src = "form-field-legacy.scss", + deps = ["//src/lib/core:core_scss_lib"], +) + +sass_binary( + name = "form_field_standard_scss", + src = "form-field-standard.scss", + deps = ["//src/lib/core:core_scss_lib"], +) + +# TODO(jelbourn): remove these when sass_binary supports specifying an output filename and dir. # Copy the output of the sass_binary such that the filename and path match what we expect. genrule( name = "form_field_css", @@ -35,3 +55,23 @@ genrule( cmd = "cat $(locations :form_field_scss) > $@", ) +genrule( + name = "form_field_box_css", + srcs = [":form_field_box_scss"], + outs = ["form-field-box.css"], + cmd = "cat $(locations :form_field_box_scss) > $@", +) + +genrule( + name = "form_field_legacy_css", + srcs = [":form_field_legacy_scss"], + outs = ["form-field-legacy.css"], + cmd = "cat $(locations :form_field_legacy_scss) > $@", +) + +genrule( + name = "form_field_standard_css", + srcs = [":form_field_standard_scss"], + outs = ["form-field-standard.css"], + cmd = "cat $(locations :form_field_standard_scss) > $@", +) diff --git a/src/lib/form-field/_form-field-box-theme.scss b/src/lib/form-field/_form-field-box-theme.scss new file mode 100644 index 000000000000..7242c2307a0f --- /dev/null +++ b/src/lib/form-field/_form-field-box-theme.scss @@ -0,0 +1,93 @@ +@import '../core/theming/palette'; +@import '../core/theming/theming'; +@import '../core/style/form-common'; +@import '../core/typography/typography-utils'; + + +// Theme styles that only apply to the box appearance of the form-field. + +@mixin mat-form-field-box-theme($theme) { + $foreground: map-get($theme, foreground); + $is-dark-theme: map-get($theme, is-dark); + + $box-background: mat-color($foreground, base, if($is-dark-theme, 0.1, 0.06)); + $box-disabled-background: mat-color($foreground, base, if($is-dark-theme, 0.05, 0.03)); + $underline-color: mat-color($foreground, divider, if($is-dark-theme, 0.7, 0.42)); + + .mat-form-field-appearance-box { + .mat-form-field-flex { + background-color: $box-background; + } + + &.mat-form-field-disabled .mat-form-field-flex { + background-color: $box-disabled-background; + } + + .mat-form-field-underline::before { + background-color: $underline-color; + } + + &.mat-form-field-disabled .mat-form-field-underline::before { + background-color: transparent; + } + } +} + +// Used to make instances of the _mat-form-field-label-floating mixin negligibly different, +// and prevent Google's CSS Optimizer from collapsing the declarations. This is needed because some +// of the selectors contain pseudo-classes not recognized in all browsers. If a browser encounters +// an unknown pseudo-class it will discard the entire rule set. +$mat-form-field-box-dedupe: 0; + +// Applies a floating label above the form field control itself. +@mixin _mat-form-field-box-label-floating($font-scale, $infix-padding, $infix-margin-top) { + transform: translateY(-$infix-margin-top - $infix-padding + $mat-form-field-box-dedupe) + scale($font-scale); + width: 100% / $font-scale + $mat-form-field-box-dedupe; + + $mat-form-field-box-dedupe: $mat-form-field-box-dedupe + 0.00001 !global; +} + +@mixin mat-form-field-box-typography($config) { + // The unit-less line-height from the font config. + $line-height: mat-line-height($config, input); + // The amount to scale the font for the floating label and subscript. + $subscript-font-scale: 0.75; + // The padding on the infix. Mocks show half of the text size. + $infix-padding: 0.5em; + // The margin applied to the form-field-infix to reserve space for the floating label. + $infix-margin-top: 1em * $line-height * $subscript-font-scale; + // The amount we offset the label in the box appearance. + $box-appearance-label-offset: -0.5em * $line-height; + + .mat-form-field-appearance-box { + .mat-form-field-label { + margin-top: $box-appearance-label-offset; + } + + &.mat-form-field-can-float { + &.mat-form-field-should-float .mat-form-field-label, + .mat-input-server:focus + .mat-form-field-label-wrapper .mat-form-field-label { + @include _mat-form-field-label-floating( + $subscript-font-scale, $infix-padding + $box-appearance-label-offset, + $infix-margin-top); + } + + .mat-form-field-autofill-control:-webkit-autofill + .mat-form-field-label-wrapper + .mat-form-field-label { + @include _mat-form-field-label-floating( + $subscript-font-scale, $infix-padding + $box-appearance-label-offset, + $infix-margin-top); + } + + // Server-side rendered matInput with a label attribute but label not shown + // (used as a pure CSS stand-in for mat-form-field-should-float). + .mat-input-server[label]:not(:label-shown) + .mat-form-field-label-wrapper + .mat-form-field-label { + @include _mat-form-field-label-floating( + $subscript-font-scale, $infix-padding + $box-appearance-label-offset, + $infix-margin-top); + } + } + } +} diff --git a/src/lib/form-field/_form-field-legacy-theme.scss b/src/lib/form-field/_form-field-legacy-theme.scss new file mode 100644 index 000000000000..6e09a89f1579 --- /dev/null +++ b/src/lib/form-field/_form-field-legacy-theme.scss @@ -0,0 +1,123 @@ +@import '../core/theming/palette'; +@import '../core/theming/theming'; +@import '../core/style/form-common'; +@import '../core/typography/typography-utils'; + + +// Theme styles that only apply to the legacy appearance of the form-field. + +@mixin mat-form-field-legacy-theme($theme) { + $foreground: map-get($theme, foreground); + $is-dark-theme: map-get($theme, is-dark); + + $underline-color: mat-color($foreground, divider, if($is-dark-theme, 0.7, 0.42)); + + .mat-form-field-appearance-legacy { + .mat-form-field-underline { + background-color: $underline-color; + } + + &.mat-form-field-disabled .mat-form-field-underline { + @include mat-control-disabled-underline($underline-color); + } + } +} + +// Used to make instances of the _mat-form-field-label-floating mixin negligibly different, +// and prevent Google's CSS Optimizer from collapsing the declarations. This is needed because some +// of the selectors contain pseudo-classes not recognized in all browsers. If a browser encounters +// an unknown pseudo-class it will discard the entire rule set. +$mat-form-field-legacy-dedupe: 0; + +// Applies a floating label above the form field control itself. +@mixin _mat-form-field-legacy-label-floating($font-scale, $infix-padding, $infix-margin-top) { + // We use perspective to fix the text blurriness as described here: + // http://www.useragentman.com/blog/2014/05/04/fixing-typography-inside-of-2-d-css-transforms/ + // This results in a small jitter after the label floats on Firefox, which the + // translateZ fixes. + transform: translateY(-$infix-margin-top - $infix-padding) scale($font-scale) perspective(100px) + translateZ(0.001px + $mat-form-field-legacy-dedupe); + // The tricks above used to smooth out the animation on chrome and firefox actually make things + // worse on IE, so we don't include them in the IE version. + -ms-transform: translateY(-$infix-margin-top - $infix-padding + $mat-form-field-legacy-dedupe) + scale($font-scale); + + width: 100% / $font-scale + $mat-form-field-legacy-dedupe; + + $mat-form-field-legacy-dedupe: $mat-form-field-legacy-dedupe + 0.00001 !global; +} + +@mixin mat-form-field-legacy-typography($config) { + // The unit-less line-height from the font config. + $line-height: mat-line-height($config, input); + // The amount to scale the font for the floating label and subscript. + $subscript-font-scale: 0.75; + // The amount of space between the top of the line and the top of the actual text + // (as a fraction of the font-size). + $line-spacing: ($line-height - 1) / 2; + // The padding on the infix. Mocks show half of the text size, but seem to measure from the edge + // of the text itself, not the edge of the line; therefore we subtract off the line spacing. + $infix-padding: 0.5em - $line-spacing; + // The margin applied to the form-field-infix to reserve space for the floating label. + $infix-margin-top: 1em * $line-height * $subscript-font-scale; + // The space between the bottom of the .mat-form-field-flex area and the subscript wrapper. + // Mocks show half of the text size, but this margin is applied to an element with the subscript + // text font size, so we need to divide by the scale factor to make it half of the original text + // size. We again need to subtract off the line spacing since the mocks measure to the edge of the + // text, not the edge of the line. + $subscript-margin-top: 0.5em / $subscript-font-scale - ($line-spacing * 2); + // The padding applied to the form-field-wrapper to reserve space for the subscript, since it's + // absolutely positioned. This is a combination of the subscript's margin and line-height, but we + // need to multiply by the subscript font scale factor since the wrapper has a larger font size. + $wrapper-padding-bottom: ($subscript-margin-top + $line-height) * $subscript-font-scale; + + .mat-form-field-appearance-legacy { + .mat-form-field-wrapper { + padding-bottom: $wrapper-padding-bottom; + } + + .mat-form-field-infix { + padding: $infix-padding 0; + } + + &.mat-form-field-can-float { + &.mat-form-field-should-float .mat-form-field-label, + .mat-input-server:focus + .mat-form-field-label-wrapper .mat-form-field-label { + @include _mat-form-field-legacy-label-floating( + $subscript-font-scale, $infix-padding, $infix-margin-top); + } + + .mat-form-field-autofill-control:-webkit-autofill + .mat-form-field-label-wrapper + .mat-form-field-label { + @include _mat-form-field-legacy-label-floating( + $subscript-font-scale, $infix-padding, $infix-margin-top); + } + + // Server-side rendered matInput with a label attribute but label not shown + // (used as a pure CSS stand-in for mat-form-field-should-float). + .mat-input-server[label]:not(:label-shown) + .mat-form-field-label-wrapper + .mat-form-field-label { + @include _mat-form-field-legacy-label-floating( + $subscript-font-scale, $infix-padding, $infix-margin-top); + } + } + + .mat-form-field-label { + top: $infix-margin-top + $infix-padding; + } + + .mat-form-field-underline { + // We want the underline to start at the end of the content box, not the padding box, + // so we move it up by the padding amount. + bottom: $wrapper-padding-bottom; + } + + .mat-form-field-subscript-wrapper { + margin-top: $subscript-margin-top; + + // We want the subscript to start at the end of the content box, not the padding box, + // so we move it up by the padding amount (adjusted for the smaller font size); + top: calc(100% - #{$wrapper-padding-bottom / $subscript-font-scale}); + } + } +} diff --git a/src/lib/form-field/_form-field-standard-theme.scss b/src/lib/form-field/_form-field-standard-theme.scss new file mode 100644 index 000000000000..01d7e123c3e5 --- /dev/null +++ b/src/lib/form-field/_form-field-standard-theme.scss @@ -0,0 +1,26 @@ +@import '../core/theming/palette'; +@import '../core/theming/theming'; +@import '../core/style/form-common'; +@import '../core/typography/typography-utils'; + + +// Theme styles that only apply to the standard appearance of the form-field. + +@mixin mat-form-field-standard-theme($theme) { + $foreground: map-get($theme, foreground); + $is-dark-theme: map-get($theme, is-dark); + + $underline-color: mat-color($foreground, divider, if($is-dark-theme, 0.7, 0.42)); + + .mat-form-field-appearance-standard { + .mat-form-field-underline { + background-color: $underline-color; + } + + &.mat-form-field-disabled .mat-form-field-underline { + @include mat-control-disabled-underline($underline-color); + } + } +} + +@mixin mat-form-field-standard-typography($config) {} diff --git a/src/lib/form-field/_form-field-theme.scss b/src/lib/form-field/_form-field-theme.scss index e40a05e72964..71fab72c3919 100644 --- a/src/lib/form-field/_form-field-theme.scss +++ b/src/lib/form-field/_form-field-theme.scss @@ -2,8 +2,13 @@ @import '../core/theming/theming'; @import '../core/style/form-common'; @import '../core/typography/typography-utils'; +@import 'form-field-box-theme.scss'; +@import 'form-field-legacy-theme.scss'; +@import 'form-field-standard-theme.scss'; +// Theme styles that apply to all appearances of the form-field. + @mixin mat-form-field-theme($theme) { $primary: map-get($theme, primary); $accent: map-get($theme, accent); @@ -18,7 +23,7 @@ $required-label-color: mat-color($accent); // Underline colors. - $underline-color: mat-color($foreground, divider, if($is-dark-theme, 0.7, 0.42)); + $underline-color-base: mat-color($foreground, divider, if($is-dark-theme, 1, 0.87)); $underline-color-accent: mat-color($accent); $underline-color-warn: mat-color($warn); $underline-focused-color: mat-color($primary); @@ -47,30 +52,28 @@ color: $required-label-color; } - .mat-form-field-underline { - background-color: $underline-color; - } - - .mat-form-field-disabled .mat-form-field-underline { - @include mat-control-disabled-underline($underline-color); + .mat-form-field-ripple { + background-color: $underline-color-base; } - .mat-form-field-ripple { - background-color: $underline-focused-color; + .mat-form-field.mat-focused { + .mat-form-field-ripple { + background-color: $underline-focused-color; - &.mat-accent { - background-color: $underline-color-accent; - } + &.mat-accent { + background-color: $underline-color-accent; + } - &.mat-warn { - background-color: $underline-color-warn; + &.mat-warn { + background-color: $underline-color-warn; + } } } // Styling for the error state of the form field. Note that while the same can be // achieved with the ng-* classes, we use this approach in order to ensure that the same // logic is used to style the error state and to show the error messages. - .mat-form-field-invalid { + .mat-form-field.mat-form-field-invalid { .mat-form-field-label { color: $underline-color-warn; @@ -88,29 +91,25 @@ .mat-error { color: $underline-color-warn; } + + @include mat-form-field-legacy-theme($theme); + @include mat-form-field-standard-theme($theme); + @include mat-form-field-box-theme($theme); } // Used to make instances of the _mat-form-field-label-floating mixin negligibly different, // and prevent Google's CSS Optimizer from collapsing the declarations. This is needed because some // of the selectors contain pseudo-classes not recognized in all browsers. If a browser encounters // an unknown pseudo-class it will discard the entire rule set. -$dedupe: 0; +$mat-form-field-dedupe: 0; // Applies a floating label above the form field control itself. @mixin _mat-form-field-label-floating($font-scale, $infix-padding, $infix-margin-top) { - // We use perspective to fix the text blurriness as described here: - // http://www.useragentman.com/blog/2014/05/04/fixing-typography-inside-of-2-d-css-transforms/ - // This results in a small jitter after the label floats on Firefox, which the - // translateZ fixes. - transform: translateY(-$infix-margin-top - $infix-padding) scale($font-scale) perspective(100px) - translateZ(0.001px + $dedupe); - // The tricks above used to smooth out the animation on chrome and firefox actually make things - // worse on IE, so we don't include them in the IE version. - -ms-transform: translateY(-$infix-margin-top - $infix-padding + $dedupe) scale($font-scale); - - width: 100% / $font-scale + $dedupe; - - $dedupe: $dedupe + 0.00001 !global; + transform: translateY(-$infix-margin-top - $infix-padding + $mat-form-field-dedupe) + scale($font-scale); + width: 100% / $font-scale + $mat-form-field-dedupe; + + $mat-form-field-dedupe: $mat-form-field-dedupe + 0.00001 !global; } @mixin mat-form-field-typography($config) { @@ -122,12 +121,8 @@ $dedupe: 0; // The amount to scale the font for the prefix and suffix icons. $prefix-suffix-icon-font-scale: 1.5; - // The amount of space between the top of the line and the top of the actual text - // (as a fraction of the font-size). - $line-spacing: ($line-height - 1) / 2; - // The padding on the infix. Mocks show half of the text size, but seem to measure from the edge - // of the text itself, not the edge of the line; therefore we subtract off the line spacing. - $infix-padding: 0.5em - $line-spacing; + // The padding on the infix. Mocks show half of the text size. + $infix-padding: 0.5em; // The margin applied to the form-field-infix to reserve space for the floating label. $infix-margin-top: 1em * $line-height * $subscript-font-scale; // Font size to use for the label and subscript text. @@ -137,9 +132,8 @@ $dedupe: 0; // The space between the bottom of the .mat-form-field-flex area and the subscript wrapper. // Mocks show half of the text size, but this margin is applied to an element with the subscript // text font size, so we need to divide by the scale factor to make it half of the original text - // size. We again need to subtract off the line spacing since the mocks measure to the edge of the - // text, not the edge of the line. - $subscript-margin-top: 0.5em / $subscript-font-scale - ($line-spacing * 2); + // size. + $subscript-margin-top: 0.5em / $subscript-font-scale; // The padding applied to the form-field-wrapper to reserve space for the subscript, since it's // absolutely positioned. This is a combination of the subscript's margin and line-height, but we // need to multiply by the subscript font scale factor since the wrapper has a larger font size. @@ -224,4 +218,8 @@ $dedupe: 0; // so we move it up by the padding amount (adjusted for the smaller font size); top: calc(100% - #{$wrapper-padding-bottom / $subscript-font-scale}); } + + @include mat-form-field-legacy-typography($config); + @include mat-form-field-standard-typography($config); + @include mat-form-field-box-typography($config); } diff --git a/src/lib/form-field/form-field-box.scss b/src/lib/form-field/form-field-box.scss new file mode 100644 index 000000000000..a939dd6bfba1 --- /dev/null +++ b/src/lib/form-field/form-field-box.scss @@ -0,0 +1,63 @@ +@import '../core/style/variables'; +@import '../core/style/vendor-prefixes'; + + +// Styles that only apply to the box appearance of the form-field. + +// The border radius for the form field box. +$mat-form-field-box-border-radius: 4px !default; +// The height of the underline at the bottom of the form field box. +$mat-form-field-box-underline-height: 2px !default; +// The horizontal padding between the edge of the form field box and the start of the text. +$mat-form-field-box-side-padding: 1em !default; +// The vertical padding between the edge of the form field box and the start of the text as well as +// between the floating label and the value. +$mat-form-field-box-line-spacing: 0.5em !default; +// The scale of the subscript and floating label text w.r.t the value text. +$mat-form-field-box-subscript-font-scale: 0.75 !default; +// The horizontal padding between the edge of the subscript box and the start of the subscript text. +$mat-form-field-box-subscript-padding: + $mat-form-field-box-side-padding / $mat-form-field-box-subscript-font-scale; + + +.mat-form-field-appearance-box { + .mat-form-field-flex { + border-radius: $mat-form-field-box-border-radius; + padding: $mat-form-field-box-line-spacing $mat-form-field-box-side-padding 0 + $mat-form-field-box-side-padding; + } + + .mat-form-field-underline { + height: $mat-form-field-box-border-radius; + border-radius: 0 0 $mat-form-field-box-border-radius $mat-form-field-box-border-radius; + overflow: hidden; + } + + .mat-form-field-underline::before { + content: ''; + display: block; + position: absolute; + bottom: 0; + height: $mat-form-field-box-underline-height; + width: 100%; + } + + .mat-form-field-ripple { + bottom: 0; + height: $mat-form-field-box-underline-height; + } + + // Note that we need this specific of a selector because we don't want + // the hover effect to show when the user hovers over the hints. + &:not(.mat-form-field-disabled) .mat-form-field-flex:hover ~ .mat-form-field-underline { + .mat-form-field-ripple { + opacity: 1; + transform: none; + transition: opacity 600ms $swift-ease-out-timing-function; + } + } + + .mat-form-field-subscript-wrapper { + padding: 0 $mat-form-field-box-subscript-padding; + } +} diff --git a/src/lib/form-field/form-field-legacy.scss b/src/lib/form-field/form-field-legacy.scss new file mode 100644 index 000000000000..301fea946862 --- /dev/null +++ b/src/lib/form-field/form-field-legacy.scss @@ -0,0 +1,36 @@ +@import '../core/style/variables'; +@import '../core/style/vendor-prefixes'; + + +// Styles that only apply to the legacy appearance of the form-field. + +// The height of the underline. +$mat-form-field-legacy-underline-height: 1px !default; + + +.mat-form-field-appearance-legacy { + .mat-form-field-label { + transform: perspective(100px); + -ms-transform: none; + } + + // The underline is what's shown under the control, its prefix and its suffix. + // The ripple is the blue animation coming on top of it. + .mat-form-field-underline { + height: $mat-form-field-legacy-underline-height; + } + + .mat-form-field-ripple { + top: 0; + height: $mat-form-field-legacy-underline-height * 2; + } + + &.mat-form-field-disabled .mat-form-field-underline { + background-position: 0; + background-color: transparent; + } + + &.mat-form-field-invalid:not(.mat-focused) .mat-form-field-ripple { + height: $mat-form-field-legacy-underline-height; + } +} diff --git a/src/lib/form-field/form-field-standard.scss b/src/lib/form-field/form-field-standard.scss new file mode 100644 index 000000000000..a79a00c00612 --- /dev/null +++ b/src/lib/form-field/form-field-standard.scss @@ -0,0 +1,47 @@ +@import '../core/style/variables'; +@import '../core/style/vendor-prefixes'; + + +// Styles that only apply to the standard appearance of the form-field. + +// The height of the underline. +$mat-form-field-standard-underline-height: 1px !default; +// The bottom margin of the underline (used to push it up to align with box appearance underline). +$mat-form-field-standard-underline-margin-bottom: 1px !default; +// The padding between the top of the form field and the label text (used to align the standard +// form field with the box appearance). +$mat-form-field-standard-padding-top: 0.5em !default; + + +.mat-form-field-appearance-standard { + .mat-form-field-flex { + padding-top: $mat-form-field-standard-padding-top; + } + + // The underline is what's shown under the control, its prefix and its suffix. + // The ripple is the blue animation coming on top of it. + .mat-form-field-underline { + height: $mat-form-field-standard-underline-height; + margin-bottom: $mat-form-field-standard-underline-margin-bottom; + } + + .mat-form-field-ripple { + top: 0; + height: $mat-form-field-standard-underline-height * 2; + } + + &.mat-form-field-disabled .mat-form-field-underline { + background-position: 0; + background-color: transparent; + } + + // Note that we need this specific of a selector because we don't want + // the hover effect to show when the user hovers over the hints. + &:not(.mat-form-field-disabled) .mat-form-field-flex:hover ~ .mat-form-field-underline { + .mat-form-field-ripple { + opacity: 1; + transform: none; + transition: opacity 600ms $swift-ease-out-timing-function; + } + } +} diff --git a/src/lib/form-field/form-field.scss b/src/lib/form-field/form-field.scss index 974a46997586..5e07a9513420 100644 --- a/src/lib/form-field/form-field.scss +++ b/src/lib/form-field/form-field.scss @@ -2,10 +2,10 @@ @import '../core/style/vendor-prefixes'; +// Styles that apply to all appearances of the form-field. + // Min amount of space between start and end hint. $mat-form-field-hint-min-space: 1em !default; -// The height of the underline. -$mat-form-field-underline-height: 1px !default; // Infix stretches to fit the container, but naturally wants to be this wide. We set this in order // to have a a consistent natural size for the various types of controls that can go in a form // field. @@ -35,6 +35,7 @@ $mat-form-field-default-infix-width: 180px !default; .mat-form-field-flex { display: inline-flex; align-items: baseline; + box-sizing: border-box; width: 100%; } @@ -96,10 +97,6 @@ $mat-form-field-default-infix-width: 180px !default; text-overflow: ellipsis; overflow: hidden; - // The perspective helps smooth out animations on Chrome and Firefox but isn't needed on IE. - transform: perspective(100px); - -ms-transform: none; - transform-origin: 0 0; transition: transform $swift-ease-out-duration $swift-ease-out-timing-function, color $swift-ease-out-duration $swift-ease-out-timing-function, @@ -155,49 +152,38 @@ $mat-form-field-default-infix-width: 180px !default; transition: none; } -// The underline is what's shown under the control, its prefix and its suffix. -// The ripple is the blue animation coming on top of it. .mat-form-field-underline { position: absolute; - height: $mat-form-field-underline-height; width: 100%; + // Need this so that the underline doesn't block the hover effect. + pointer-events: none; +} - .mat-form-field-disabled & { - background-position: 0; - background-color: transparent; - } +.mat-form-field-ripple { + position: absolute; + left: 0; + width: 100%; + transform-origin: 50%; + transform: scaleX(0.5); + opacity: 0; + transition: background-color $swift-ease-in-duration $swift-ease-in-timing-function; +} +.mat-form-field.mat-focused, +.mat-form-field.mat-form-field-invalid { .mat-form-field-ripple { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: $mat-form-field-underline-height * 2; - transform-origin: 50%; - transform: scaleX(0.5); - visibility: hidden; - opacity: 0; - transition: background-color $swift-ease-in-duration $swift-ease-in-timing-function; - - .mat-form-field-invalid:not(.mat-focused) & { - height: $mat-form-field-underline-height; - } - - .mat-focused &, - .mat-form-field-invalid & { - visibility: visible; - opacity: 1; - transform: scaleX(1); - transition: transform 300ms $swift-ease-out-timing-function, - opacity 100ms $swift-ease-out-timing-function, - background-color 300ms $swift-ease-out-timing-function; - } + opacity: 1; + transform: scaleX(1); + transition: transform 300ms $swift-ease-out-timing-function, + opacity 100ms $swift-ease-out-timing-function, + background-color 300ms $swift-ease-out-timing-function; } } // Wrapper for the hints and error messages. .mat-form-field-subscript-wrapper { position: absolute; + box-sizing: border-box; width: 100%; overflow: hidden; // prevents multi-line errors from overlapping the control } diff --git a/src/lib/form-field/form-field.ts b/src/lib/form-field/form-field.ts index b512c0b37cb1..e373b96e844e 100644 --- a/src/lib/form-field/form-field.ts +++ b/src/lib/form-field/form-field.ts @@ -62,6 +62,9 @@ export const _MatFormFieldMixinBase = mixinColor(MatFormFieldBase, 'primary'); let nextUniqueId = 0; +export type MatFormFieldAppearance = 'legacy' | 'standard' | 'box'; + + /** Container for form controls that applies Material Design styling and behavior. */ @Component({ moduleId: module.id, @@ -72,10 +75,19 @@ let nextUniqueId = 0; // MatInput is a directive and can't have styles, so we need to include its styles here. // The MatInput styles are fairly minimal so it shouldn't be a big deal for people who // aren't using MatInput. - styleUrls: ['form-field.css', '../input/input.css'], + styleUrls: [ + 'form-field.css', + 'form-field-box.css', + 'form-field-legacy.css', + 'form-field-standard.css', + '../input/input.css', + ], animations: [matFormFieldAnimations.transitionMessages], host: { 'class': 'mat-input-container mat-form-field', + '[class.mat-form-field-appearance-standard]': 'appearance == "standard"', + '[class.mat-form-field-appearance-box]': 'appearance == "box"', + '[class.mat-form-field-appearance-legacy]': 'appearance == "legacy"', '[class.mat-input-invalid]': '_control.errorState', '[class.mat-form-field-invalid]': '_control.errorState', '[class.mat-form-field-can-float]': '_canLabelFloat', @@ -101,6 +113,9 @@ export class MatFormField extends _MatFormFieldMixinBase implements AfterContentInit, AfterContentChecked, AfterViewInit, CanColor { private _labelOptions: LabelOptions; + /** The form-field appearance style. */ + @Input() appearance: MatFormFieldAppearance = 'legacy'; + /** * @deprecated Use `color` instead. * @deletion-target 6.0.0 @@ -122,11 +137,11 @@ export class MatFormField extends _MatFormFieldMixinBase /** Whether the floating label should always float or not. */ get _shouldAlwaysFloat(): boolean { - return this._floatLabel === 'always' && !this._showAlwaysAnimate; + return this.floatLabel === 'always' && !this._showAlwaysAnimate; } /** Whether the label can float or not. */ - get _canLabelFloat(): boolean { return this._floatLabel !== 'never'; } + get _canLabelFloat(): boolean { return this.floatLabel !== 'never'; } /** State of the mat-hint and mat-error animations. */ _subscriptAnimationState: string = ''; @@ -149,12 +164,21 @@ export class MatFormField extends _MatFormFieldMixinBase * @deletion-target 6.0.0 */ @Input() - get floatPlaceholder(): FloatLabelType { return this._floatLabel; } + get floatPlaceholder(): FloatLabelType { return this.floatLabel; } set floatPlaceholder(value: FloatLabelType) { this.floatLabel = value; } - /** Whether the label should always float, never float or float as the user types. */ + /** + * Whether the label should always float, never float or float as the user types. + * + * Note: only the legacy appearance supports the `never` option. `never` was originally added as a + * way to make the floating label emulate the behavior of a standard input placeholder. However + * the form field now supports both floating labels and placeholders. Therefore in the non-legacy + * appearances the `never` option has been disabled in favor of just using the placeholder. + */ @Input() - get floatLabel(): FloatLabelType { return this._floatLabel; } + get floatLabel(): FloatLabelType { + return this.appearance !== 'legacy' && this._floatLabel === 'never' ? 'auto' : this._floatLabel; + } set floatLabel(value: FloatLabelType) { if (value !== this._floatLabel) { this._floatLabel = value || this._labelOptions.float || 'auto'; @@ -250,11 +274,14 @@ export class MatFormField extends _MatFormFieldMixinBase } _hideControlPlaceholder() { - return !this._hasLabel() || !this._shouldLabelFloat(); + // In the legacy appearance the placeholder is promoted to a label if no label is given. + return this.appearance === 'legacy' && !this._hasLabel() || + this._hasLabel() && !this._shouldLabelFloat(); } _hasFloatingLabel() { - return this._hasLabel() || this._hasPlaceholder(); + // In the legacy appearance the placeholder is promoted to a label if no label is given. + return this._hasLabel() || this.appearance === 'legacy' && this._hasPlaceholder(); } /** Determines whether to display hints or errors. */ @@ -267,7 +294,7 @@ export class MatFormField extends _MatFormFieldMixinBase _animateAndLockLabel(): void { if (this._hasFloatingLabel() && this._canLabelFloat) { this._showAlwaysAnimate = true; - this._floatLabel = 'always'; + this.floatLabel = 'always'; fromEvent(this._label.nativeElement, 'transitionend').pipe(take(1)).subscribe(() => { this._showAlwaysAnimate = false; diff --git a/src/lib/input/input.spec.ts b/src/lib/input/input.spec.ts index 8b384c3a7d7a..4a9dde9a514e 100644 --- a/src/lib/input/input.spec.ts +++ b/src/lib/input/input.spec.ts @@ -1,7 +1,7 @@ import {Platform, PlatformModule} from '@angular/cdk/platform'; import {createFakeEvent, dispatchFakeEvent, wrappedErrorMessage} from '@angular/cdk/testing'; import {ChangeDetectionStrategy, Component, ViewChild} from '@angular/core'; -import {ComponentFixture, inject, TestBed, fakeAsync, flush} from '@angular/core/testing'; +import {ComponentFixture, fakeAsync, flush, inject, TestBed} from '@angular/core/testing'; import { FormControl, FormGroup, @@ -12,16 +12,17 @@ import { Validators, } from '@angular/forms'; import { - MAT_LABEL_GLOBAL_OPTIONS, - ShowOnDirtyErrorStateMatcher, ErrorStateMatcher, FloatLabelType, + MAT_LABEL_GLOBAL_OPTIONS, + ShowOnDirtyErrorStateMatcher, } from '@angular/material/core'; import { getMatFormFieldDuplicatedHintError, getMatFormFieldMissingControlError, getMatFormFieldPlaceholderConflictError, MatFormField, + MatFormFieldAppearance, MatFormFieldModule, } from '@angular/material/form-field'; import {By} from '@angular/platform-browser'; @@ -1147,6 +1148,76 @@ describe('MatInput with forms', () => { }); +describe('MatInput with appearance', () => { + const nonLegacyAppearances: MatFormFieldAppearance[] = ['standard', 'box']; + let fixture: ComponentFixture; + let testComponent: MatInputWithAppearance; + let containerEl: HTMLElement; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [ + FormsModule, + MatFormFieldModule, + MatInputModule, + NoopAnimationsModule, + PlatformModule, + ReactiveFormsModule, + ], + declarations: [ + MatInputWithAppearance, + ], + }); + + TestBed.compileComponents(); + })); + + beforeEach(fakeAsync(() => { + fixture = TestBed.createComponent(MatInputWithAppearance); + fixture.detectChanges(); + testComponent = fixture.componentInstance; + containerEl = fixture.debugElement.query(By.css('mat-form-field')).nativeElement; + })); + + it('legacy appearance should promote placeholder to label', fakeAsync(() => { + testComponent.appearance = 'legacy'; + fixture.detectChanges(); + + expect(containerEl.classList).toContain('mat-form-field-appearance-legacy'); + expect(testComponent.formField._hasFloatingLabel()).toBe(true); + expect(testComponent.formField._hideControlPlaceholder()).toBe(true); + })); + + it('non-legacy appearances should not promote placeholder to label', fakeAsync(() => { + for (let appearance of nonLegacyAppearances) { + testComponent.appearance = appearance; + fixture.detectChanges(); + + expect(containerEl.classList).toContain(`mat-form-field-appearance-${appearance}`); + expect(testComponent.formField._hasFloatingLabel()).toBe(false); + expect(testComponent.formField._hideControlPlaceholder()).toBe(false); + } + })); + + it('legacy appearance should respect float never', fakeAsync(() => { + testComponent.appearance = 'legacy'; + fixture.detectChanges(); + + expect(containerEl.classList).toContain('mat-form-field-appearance-legacy'); + expect(testComponent.formField.floatLabel).toBe('never'); + })); + + it('non-legacy appearances should not respect float never', fakeAsync(() => { + for (let appearance of nonLegacyAppearances) { + testComponent.appearance = appearance; + fixture.detectChanges(); + + expect(containerEl.classList).toContain(`mat-form-field-appearance-${appearance}`); + expect(testComponent.formField.floatLabel).toBe('auto'); + } + })); +}); + @Component({ template: ` @@ -1485,3 +1556,15 @@ class MatInputWithReadonlyInput {} class MatInputWithLabelAndPlaceholder { floatLabel: FloatLabelType; } + +@Component({ + template: ` + + + + ` +}) +class MatInputWithAppearance { + @ViewChild(MatFormField) formField: MatFormField; + appearance: MatFormFieldAppearance; +} diff --git a/src/lib/select/select.scss b/src/lib/select/select.scss index 7939c435dbd7..1a8305512d7b 100644 --- a/src/lib/select/select.scss +++ b/src/lib/select/select.scss @@ -45,6 +45,11 @@ $mat-select-placeholder-arrow-space: 2 * ($mat-select-arrow-size + $mat-select-a .mat-select-arrow-wrapper { display: table-cell; vertical-align: middle; + + // When used in a box appearance form-field the arrow should be centered in the box. + .mat-form-field-appearance-box & { + transform: translateY(-50%); + } } .mat-select-arrow { @@ -100,5 +105,7 @@ $mat-select-placeholder-arrow-space: 2 * ($mat-select-arrow-size + $mat-select-a // Remove the transition to prevent the placeholder // from overlapping when the label comes back down. transition: none; + // Prevents the '...' from showing on the parent element. + display: block; } } From c9e237be772e39f97f3d54a141d9da200c37c730 Mon Sep 17 00:00:00 2001 From: tinayuangao Date: Thu, 25 Jan 2018 16:33:38 -0800 Subject: [PATCH 02/13] feat(chips): Add chip avatar and chip trailing icon (#9557) * feat(chips): Add chip avatar and chip trailing icon * Removed MatBasicChip and MatStandardChip * Add mat-chip-trailing-icon style to MatChipRemove and add examples in demo --- src/demo-app/chips/chips-demo.html | 52 ++++++++++- src/lib/chips/_chips-theme.scss | 5 +- src/lib/chips/chip-list.ts | 2 + src/lib/chips/chip.ts | 49 ++++++++-- src/lib/chips/chips-module.ts | 14 ++- src/lib/chips/chips.scss | 143 +++++++++++++++++++---------- src/lib/chips/public-api.ts | 1 - 7 files changed, 204 insertions(+), 62 deletions(-) diff --git a/src/demo-app/chips/chips-demo.html b/src/demo-app/chips/chips-demo.html index 3ad365c44110..692d23e40da9 100644 --- a/src/demo-app/chips/chips-demo.html +++ b/src/demo-app/chips/chips-demo.html @@ -8,7 +8,7 @@

Simple

Chip 1 Chip 2 - Chip 3 + Chip 3

Unstyled

@@ -31,6 +31,56 @@

Advanced

{{message}}
+ +

With avatar and icons

+ + + + + home + Home + cancel + + + P + Portel + cancel + + + + M + Molly + + + + Koby + cancel + + + + Razzle + + + + + Mal + + + + + Husi + cancel + + + + Good + star + + + Bad + star_border + + diff --git a/src/lib/chips/_chips-theme.scss b/src/lib/chips/_chips-theme.scss index fd5ff8f264e6..26110c83d688 100644 --- a/src/lib/chips/_chips-theme.scss +++ b/src/lib/chips/_chips-theme.scss @@ -37,11 +37,11 @@ $mat-chip-remove-font-size: 18px; $unselected-foreground: mat-color($foreground, text); - .mat-chip:not(.mat-basic-chip) { + .mat-chip.mat-standard-chip { @include mat-chips-color($unselected-foreground, $unselected-background); } - .mat-chip.mat-chip-selected { + .mat-chip.mat-standard-chip.mat-chip-selected { &.mat-primary { @include mat-chips-theme-color($primary); @@ -62,6 +62,7 @@ $mat-chip-remove-font-size: 18px; font-size: $mat-chip-font-size; line-height: $mat-chip-line-height; + .mat-chip-trailing-icon.mat-icon, .mat-chip-remove.mat-icon { font-size: $mat-chip-remove-font-size; } diff --git a/src/lib/chips/chip-list.ts b/src/lib/chips/chip-list.ts index 172ba1b5fc6f..15fc84581419 100644 --- a/src/lib/chips/chip-list.ts +++ b/src/lib/chips/chip-list.ts @@ -21,6 +21,7 @@ import { DoCheck, ElementRef, EventEmitter, + Inject, Input, OnDestroy, OnInit, @@ -747,3 +748,4 @@ export class MatChipList extends _MatChipListMixinBase implements MatFormFieldCo }); } } + diff --git a/src/lib/chips/chip.ts b/src/lib/chips/chip.ts index b17954f17b77..8d131f3cfdcc 100644 --- a/src/lib/chips/chip.ts +++ b/src/lib/chips/chip.ts @@ -10,9 +10,11 @@ import {FocusableOption} from '@angular/cdk/a11y'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; import {BACKSPACE, DELETE, SPACE} from '@angular/cdk/keycodes'; import { + ContentChild, Directive, ElementRef, EventEmitter, + forwardRef, Input, OnDestroy, Output, @@ -47,17 +49,27 @@ export class MatChipBase { export const _MatChipMixinBase = mixinColor(mixinDisabled(MatChipBase), 'primary'); +const CHIP_ATTRIBUTE_NAMES = ['mat-basic-chip']; /** - * Dummy directive to add CSS class to basic chips. + * Dummy directive to add CSS class to chip avatar. * @docs-private */ @Directive({ - selector: `mat-basic-chip, [mat-basic-chip]`, - host: {'class': 'mat-basic-chip'}, + selector: 'mat-chip-avatar, [matChipAvatar]', + host: {'class': 'mat-chip-avatar'} }) -export class MatBasicChip { -} +export class MatChipAvatar {} + +/** + * Dummy directive to add CSS class to chip trailing icon. + * @docs-private + */ +@Directive({ + selector: 'mat-chip-trailing-icon, [matChipTrailingIcon]', + host: {'class': 'mat-chip-trailing-icon'} +}) +export class MatChipTrailingIcon {} /** * Material design styled Chip component. Used inside the MatChipList component. @@ -71,6 +83,8 @@ export class MatBasicChip { '[attr.tabindex]': 'disabled ? null : -1', 'role': 'option', '[class.mat-chip-selected]': 'selected', + '[class.mat-chip-with-avatar]': 'avatar', + '[class.mat-chip-with-trailing-icon]': 'trailingIcon || removeIcon', '[attr.disabled]': 'disabled || null', '[attr.aria-disabled]': 'disabled.toString()', '[attr.aria-selected]': 'ariaSelected', @@ -86,6 +100,15 @@ export class MatChip extends _MatChipMixinBase implements FocusableOption, OnDes /** Whether the chip has focus. */ _hasFocus: boolean = false; + /** The chip avatar */ + @ContentChild(MatChipAvatar) avatar: MatChipAvatar; + + /** The chip's trailing icon. */ + @ContentChild(MatChipTrailingIcon) trailingIcon: MatChipTrailingIcon; + + /** The chip's remove toggler. */ + @ContentChild(forwardRef(() => MatChipRemove)) removeIcon: MatChipRemove; + /** Whether the chip is selected. */ @Input() get selected(): boolean { return this._selected; } @@ -167,6 +190,20 @@ export class MatChip extends _MatChipMixinBase implements FocusableOption, OnDes constructor(public _elementRef: ElementRef) { super(_elementRef); + + this._addHostClassName(); + } + + _addHostClassName() { + // Add class for the different chips + for (const attr of CHIP_ATTRIBUTE_NAMES) { + if (this._elementRef.nativeElement.hasAttribute(attr) || + this._elementRef.nativeElement.tagName.toLowerCase() === attr) { + (this._elementRef.nativeElement as HTMLElement).classList.add(attr); + return; + } + } + (this._elementRef.nativeElement as HTMLElement).classList.add('mat-standard-chip'); } ngOnDestroy() { @@ -297,7 +334,7 @@ export class MatChip extends _MatChipMixinBase implements FocusableOption, OnDes @Directive({ selector: '[matChipRemove]', host: { - 'class': 'mat-chip-remove', + 'class': 'mat-chip-remove mat-chip-trailing-icon', '(click)': '_handleClick()', } }) diff --git a/src/lib/chips/chips-module.ts b/src/lib/chips/chips-module.ts index 3a8ca64e0f81..8c4cb7131cc9 100644 --- a/src/lib/chips/chips-module.ts +++ b/src/lib/chips/chips-module.ts @@ -9,14 +9,22 @@ import {NgModule} from '@angular/core'; import {ErrorStateMatcher} from '@angular/material/core'; import {MatChipList} from './chip-list'; -import {MatBasicChip, MatChip, MatChipRemove} from './chip'; +import {MatChip, MatChipRemove, MatChipAvatar, MatChipTrailingIcon} from './chip'; import {MatChipInput} from './chip-input'; +const CHIP_DECLARATIONS = [ + MatChipList, + MatChip, + MatChipInput, + MatChipRemove, + MatChipAvatar, + MatChipTrailingIcon, +]; @NgModule({ imports: [], - exports: [MatChipList, MatChip, MatChipInput, MatChipRemove, MatChipRemove, MatBasicChip], - declarations: [MatChipList, MatChip, MatChipInput, MatChipRemove, MatChipRemove, MatBasicChip], + exports: CHIP_DECLARATIONS, + declarations: CHIP_DECLARATIONS, providers: [ErrorStateMatcher] }) export class MatChipsModule {} diff --git a/src/lib/chips/chips.scss b/src/lib/chips/chips.scss index 541e9cc2bb13..de53e33fd911 100644 --- a/src/lib/chips/chips.scss +++ b/src/lib/chips/chips.scss @@ -3,22 +3,24 @@ $mat-chip-vertical-padding: 7px; $mat-chip-horizontal-padding: 12px; -$mat-chip-remove-margin-before: 6px; -$mat-chip-remove-margin-after: -4px; -$mat-chips-chip-margin: 8px; +$mat-chip-remove-vertical-padding: 7px; +$mat-chip-remove-before-margin: 7px; +$mat-chip-remove-after-padding: 7px; + +$mat-chip-avatar-vertical-padding: 0; +$mat-chip-avatar-before-padding: 0; +$mat-chip-avatar-after-margin: 8px; + +$mat-chips-chip-margin: 4px; $mat-chip-input-width: 150px; $mat-chip-input-margin: 3px; -.mat-chip-list-wrapper { - display: flex; - flex-direction: row; - flex-wrap: wrap; - align-items: baseline; -} +$mat-chip-avatar-size: 32px; +$mat-chip-remove-size: 18px; -.mat-chip:not(.mat-basic-chip) { +.mat-standard-chip { @include mat-elevation-transition; display: inline-flex; padding: $mat-chip-vertical-padding $mat-chip-horizontal-padding; @@ -26,28 +28,13 @@ $mat-chip-input-margin: 3px; align-items: center; cursor: default; - // Apply a margin to adjacent sibling chips. - & + & { - margin: 0 0 0 $mat-chips-chip-margin; - - [dir='rtl'] & { - margin: 0 $mat-chips-chip-margin 0 0; - } - } - - .mat-form-field-prefix & { - &:last-child { - margin-right: $mat-chips-chip-margin; - } - - [dir='rtl'] &:last-child { - margin-left: $mat-chips-chip-margin; - } + .mat-chip-list-wrapper & { + margin: $mat-chips-chip-margin; } .mat-chip-remove.mat-icon { - width: 1em; - height: 1em; + width: $mat-chip-remove-size; + height: $mat-chip-remove-size; } &:focus { @@ -58,42 +45,100 @@ $mat-chip-input-margin: 3px; @include cdk-high-contrast { outline: solid 1px; } -} -.mat-chip-list-stacked .mat-chip-list-wrapper { - display: block; + &.mat-chip-with-trailing-icon.mat-chip-with-avatar, + &.mat-chip-with-avatar { + padding-top: $mat-chip-avatar-vertical-padding; + padding-bottom: $mat-chip-avatar-vertical-padding; + } + + &.mat-chip-with-trailing-icon.mat-chip-with-avatar { + padding-right: $mat-chip-remove-after-padding; + padding-left: $mat-chip-avatar-before-padding; + + [dir='rtl'] & { + padding-left: $mat-chip-remove-after-padding; + padding-right: $mat-chip-avatar-before-padding; + } + } + + &.mat-chip-with-trailing-icon { + padding-top: $mat-chip-remove-vertical-padding; + padding-bottom: $mat-chip-remove-vertical-padding; + padding-right: $mat-chip-remove-after-padding; + padding-left: $mat-chip-horizontal-padding; - .mat-chip:not(.mat-basic-chip) { - display: block; - margin: 0; - margin-bottom: $mat-chips-chip-margin; + [dir='rtl'] & { + padding-left: $mat-chip-remove-after-padding; + padding-right: $mat-chip-horizontal-padding; + } + } + + &.mat-chip-with-avatar { + padding-left: $mat-chip-avatar-before-padding; + padding-right: $mat-chip-horizontal-padding; + + [dir='rtl'] & { + padding-right: $mat-chip-avatar-before-padding; + padding-left: $mat-chip-horizontal-padding; + } + } + + .mat-chip-avatar { + width: $mat-chip-avatar-size; + height: $mat-chip-avatar-size; + margin-right: $mat-chip-avatar-after-margin; + margin-left: 0; [dir='rtl'] & { - margin: 0; - margin-bottom: $mat-chips-chip-margin; + margin-left: $mat-chip-avatar-after-margin; + margin-right: 0; } + } + + .mat-chip-remove, + .mat-chip-trailing-icon { + width: $mat-chip-remove-size; + height: $mat-chip-remove-size; + cursor: pointer; + } - &:last-child, [dir='rtl'] &:last-child { - margin-bottom: 0; + .mat-chip-remove, + .mat-chip-trailing-icon { + margin-left: $mat-chip-remove-before-margin; + margin-right: 0; + + [dir='rtl'] & { + margin-right: $mat-chip-remove-before-margin; + margin-left: 0; } } } -.mat-form-field-prefix .mat-chip-list-wrapper { - margin-bottom: $mat-chips-chip-margin; +.mat-chip-list-wrapper { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; } -.mat-chip-remove { - margin-right: $mat-chip-remove-margin-after; - margin-left: $mat-chip-remove-margin-before; - cursor: pointer; +.mat-chip-list-stacked .mat-chip-list-wrapper { + flex-direction: column; + align-items: flex-start; - [dir='rtl'] & { - margin-right: $mat-chip-remove-margin-before; - margin-left: $mat-chip-remove-margin-after; + .mat-standard-chip { + width: 100%; } } +.mat-chip-avatar { + border-radius: 50%; + justify-content: center; + align-items: center; + display: flex; + overflow: hidden; +} + input.mat-chip-input { width: $mat-chip-input-width; margin: $mat-chip-input-margin; diff --git a/src/lib/chips/public-api.ts b/src/lib/chips/public-api.ts index a93755ac6646..940403e4c0be 100644 --- a/src/lib/chips/public-api.ts +++ b/src/lib/chips/public-api.ts @@ -10,4 +10,3 @@ export * from './chips-module'; export * from './chip-list'; export * from './chip'; export * from './chip-input'; - From bce55094ae4f623e89d43e6e758811bc847fe18c Mon Sep 17 00:00:00 2001 From: mmalerba Date: Fri, 26 Jan 2018 19:59:55 -0800 Subject: [PATCH 03/13] fix(form-field): rename box to fill and tweak the styles a bit (#9636) * make some tweaks to the box appearance * rename 'box' appearance to 'fill' --- src/demo-app/input/input-demo.html | 12 ++-- src/demo-app/input/input-demo.ts | 2 +- src/lib/form-field/BUILD.bazel | 14 ++-- ...theme.scss => _form-field-fill-theme.scss} | 64 ++++++++++++------- src/lib/form-field/_form-field-theme.scss | 10 +-- src/lib/form-field/form-field-box.scss | 63 ------------------ src/lib/form-field/form-field-fill.scss | 59 +++++++++++++++++ src/lib/form-field/form-field-standard.scss | 7 +- src/lib/form-field/form-field.ts | 6 +- src/lib/input/input.spec.ts | 2 +- src/lib/select/select.scss | 2 +- 11 files changed, 125 insertions(+), 116 deletions(-) rename src/lib/form-field/{_form-field-box-theme.scss => _form-field-fill-theme.scss} (57%) delete mode 100644 src/lib/form-field/form-field-box.scss create mode 100644 src/lib/form-field/form-field-fill.scss diff --git a/src/demo-app/input/input-demo.html b/src/demo-app/input/input-demo.html index c9932a43e58a..7b07ca716324 100644 --- a/src/demo-app/input/input-demo.html +++ b/src/demo-app/input/input-demo.html @@ -581,9 +581,9 @@

<textarea> with ngModel

Please type something here
- - Box appearance - + + Fill appearance + This field is required Please type something here @@ -606,9 +606,9 @@

<textarea> with ngModel

- - Box appearance - + + Fill appearance + This field is required Please type something here diff --git a/src/demo-app/input/input-demo.ts b/src/demo-app/input/input-demo.ts index 04ed022cb3b6..b42a9f66f8fe 100644 --- a/src/demo-app/input/input-demo.ts +++ b/src/demo-app/input/input-demo.ts @@ -54,7 +54,7 @@ export class InputDemo { legacyAppearance: string; standardAppearance: string; - boxAppearance: string; + fillAppearance: string; constructor() { setTimeout(() => this.delayedFormControl.setValue('hello'), 100); diff --git a/src/lib/form-field/BUILD.bazel b/src/lib/form-field/BUILD.bazel index 77b35dda754e..dc5c0269df82 100644 --- a/src/lib/form-field/BUILD.bazel +++ b/src/lib/form-field/BUILD.bazel @@ -9,7 +9,7 @@ ng_module( module_name = "@angular/material/form-field", assets = [ ":form_field_css", - ":form_field_box_css", + ":form_field_fill_css", ":form_field_legacy_css", ":form_field_standard_css", "//src/lib/input:input_css" @@ -29,8 +29,8 @@ sass_binary( ) sass_binary( - name = "form_field_box_scss", - src = "form-field-box.scss", + name = "form_field_fill_scss", + src = "form-field-fill.scss", deps = ["//src/lib/core:core_scss_lib"], ) @@ -56,10 +56,10 @@ genrule( ) genrule( - name = "form_field_box_css", - srcs = [":form_field_box_scss"], - outs = ["form-field-box.css"], - cmd = "cat $(locations :form_field_box_scss) > $@", + name = "form_field_fill_css", + srcs = [":form_field_fill_scss"], + outs = ["form-field-fill.css"], + cmd = "cat $(locations :form_field_fill_scss) > $@", ) genrule( diff --git a/src/lib/form-field/_form-field-box-theme.scss b/src/lib/form-field/_form-field-fill-theme.scss similarity index 57% rename from src/lib/form-field/_form-field-box-theme.scss rename to src/lib/form-field/_form-field-fill-theme.scss index 7242c2307a0f..70a9b0bef171 100644 --- a/src/lib/form-field/_form-field-box-theme.scss +++ b/src/lib/form-field/_form-field-fill-theme.scss @@ -4,31 +4,38 @@ @import '../core/typography/typography-utils'; -// Theme styles that only apply to the box appearance of the form-field. +// Theme styles that only apply to the fill appearance of the form-field. -@mixin mat-form-field-box-theme($theme) { +@mixin mat-form-field-fill-theme($theme) { $foreground: map-get($theme, foreground); $is-dark-theme: map-get($theme, is-dark); - $box-background: mat-color($foreground, base, if($is-dark-theme, 0.1, 0.06)); - $box-disabled-background: mat-color($foreground, base, if($is-dark-theme, 0.05, 0.03)); - $underline-color: mat-color($foreground, divider, if($is-dark-theme, 0.7, 0.42)); + $fill-background: mat-color($foreground, base, if($is-dark-theme, 0.1, 0.04)); + $fill-disabled-background: mat-color($foreground, base, if($is-dark-theme, 0.05, 0.02)); + $underline-color: mat-color($foreground, divider, if($is-dark-theme, 0.5, 0.42)); + $label-disabled-color: mat-color($foreground, disabled-text); - .mat-form-field-appearance-box { + .mat-form-field-appearance-fill { .mat-form-field-flex { - background-color: $box-background; + background-color: $fill-background; } &.mat-form-field-disabled .mat-form-field-flex { - background-color: $box-disabled-background; + background-color: $fill-disabled-background; } .mat-form-field-underline::before { background-color: $underline-color; } - &.mat-form-field-disabled .mat-form-field-underline::before { - background-color: transparent; + &.mat-form-field-disabled { + .mat-form-field-label { + color: $label-disabled-color; + } + + .mat-form-field-underline::before { + background-color: transparent; + } } } } @@ -37,46 +44,55 @@ // and prevent Google's CSS Optimizer from collapsing the declarations. This is needed because some // of the selectors contain pseudo-classes not recognized in all browsers. If a browser encounters // an unknown pseudo-class it will discard the entire rule set. -$mat-form-field-box-dedupe: 0; +$mat-form-field-fill-dedupe: 0; // Applies a floating label above the form field control itself. -@mixin _mat-form-field-box-label-floating($font-scale, $infix-padding, $infix-margin-top) { - transform: translateY(-$infix-margin-top - $infix-padding + $mat-form-field-box-dedupe) +@mixin _mat-form-field-fill-label-floating($font-scale, $infix-padding, $infix-margin-top) { + transform: translateY(-$infix-margin-top - $infix-padding + $mat-form-field-fill-dedupe) scale($font-scale); - width: 100% / $font-scale + $mat-form-field-box-dedupe; + width: 100% / $font-scale + $mat-form-field-fill-dedupe; - $mat-form-field-box-dedupe: $mat-form-field-box-dedupe + 0.00001 !global; + $mat-form-field-fill-dedupe: $mat-form-field-fill-dedupe + 0.00001 !global; } -@mixin mat-form-field-box-typography($config) { +@mixin mat-form-field-fill-typography($config) { // The unit-less line-height from the font config. $line-height: mat-line-height($config, input); // The amount to scale the font for the floating label and subscript. $subscript-font-scale: 0.75; // The padding on the infix. Mocks show half of the text size. - $infix-padding: 0.5em; + $infix-padding-top: 0.25em; + $infix-padding-bottom: 0.75em; // The margin applied to the form-field-infix to reserve space for the floating label. $infix-margin-top: 1em * $line-height * $subscript-font-scale; - // The amount we offset the label in the box appearance. - $box-appearance-label-offset: -0.5em * $line-height; + // The amount we offset the label in the fill appearance. + $fill-appearance-label-offset: -0.5em * $line-height; + + .mat-form-field-appearance-fill { + .mat-form-field-infix { + padding: $infix-padding-top 0 $infix-padding-bottom 0; + } + + .mat-form-field-label { + margin-top: $fill-appearance-label-offset; + } - .mat-form-field-appearance-box { .mat-form-field-label { - margin-top: $box-appearance-label-offset; + top: $infix-margin-top + $infix-padding-top; } &.mat-form-field-can-float { &.mat-form-field-should-float .mat-form-field-label, .mat-input-server:focus + .mat-form-field-label-wrapper .mat-form-field-label { @include _mat-form-field-label-floating( - $subscript-font-scale, $infix-padding + $box-appearance-label-offset, + $subscript-font-scale, $infix-padding-top + $fill-appearance-label-offset, $infix-margin-top); } .mat-form-field-autofill-control:-webkit-autofill + .mat-form-field-label-wrapper .mat-form-field-label { @include _mat-form-field-label-floating( - $subscript-font-scale, $infix-padding + $box-appearance-label-offset, + $subscript-font-scale, $infix-padding-top + $fill-appearance-label-offset, $infix-margin-top); } @@ -85,7 +101,7 @@ $mat-form-field-box-dedupe: 0; .mat-input-server[label]:not(:label-shown) + .mat-form-field-label-wrapper .mat-form-field-label { @include _mat-form-field-label-floating( - $subscript-font-scale, $infix-padding + $box-appearance-label-offset, + $subscript-font-scale, $infix-padding-top + $fill-appearance-label-offset, $infix-margin-top); } } diff --git a/src/lib/form-field/_form-field-theme.scss b/src/lib/form-field/_form-field-theme.scss index 71fab72c3919..6864684b3364 100644 --- a/src/lib/form-field/_form-field-theme.scss +++ b/src/lib/form-field/_form-field-theme.scss @@ -2,7 +2,7 @@ @import '../core/theming/theming'; @import '../core/style/form-common'; @import '../core/typography/typography-utils'; -@import 'form-field-box-theme.scss'; +@import 'form-field-fill-theme.scss'; @import 'form-field-legacy-theme.scss'; @import 'form-field-standard-theme.scss'; @@ -18,7 +18,7 @@ $is-dark-theme: map-get($theme, is-dark); // Label colors. Required is used for the `*` star shown in the label. - $label-color: mat-color($foreground, secondary-text); + $label-color: mat-color($foreground, secondary-text, if($is-dark-theme, 0.7, 0.6)); $focused-label-color: mat-color($primary); $required-label-color: mat-color($accent); @@ -33,7 +33,7 @@ } .mat-hint { - color: mat-color($foreground, secondary-text); + color: $label-color; } .mat-focused .mat-form-field-label { @@ -94,7 +94,7 @@ @include mat-form-field-legacy-theme($theme); @include mat-form-field-standard-theme($theme); - @include mat-form-field-box-theme($theme); + @include mat-form-field-fill-theme($theme); } // Used to make instances of the _mat-form-field-label-floating mixin negligibly different, @@ -221,5 +221,5 @@ $mat-form-field-dedupe: 0; @include mat-form-field-legacy-typography($config); @include mat-form-field-standard-typography($config); - @include mat-form-field-box-typography($config); + @include mat-form-field-fill-typography($config); } diff --git a/src/lib/form-field/form-field-box.scss b/src/lib/form-field/form-field-box.scss deleted file mode 100644 index a939dd6bfba1..000000000000 --- a/src/lib/form-field/form-field-box.scss +++ /dev/null @@ -1,63 +0,0 @@ -@import '../core/style/variables'; -@import '../core/style/vendor-prefixes'; - - -// Styles that only apply to the box appearance of the form-field. - -// The border radius for the form field box. -$mat-form-field-box-border-radius: 4px !default; -// The height of the underline at the bottom of the form field box. -$mat-form-field-box-underline-height: 2px !default; -// The horizontal padding between the edge of the form field box and the start of the text. -$mat-form-field-box-side-padding: 1em !default; -// The vertical padding between the edge of the form field box and the start of the text as well as -// between the floating label and the value. -$mat-form-field-box-line-spacing: 0.5em !default; -// The scale of the subscript and floating label text w.r.t the value text. -$mat-form-field-box-subscript-font-scale: 0.75 !default; -// The horizontal padding between the edge of the subscript box and the start of the subscript text. -$mat-form-field-box-subscript-padding: - $mat-form-field-box-side-padding / $mat-form-field-box-subscript-font-scale; - - -.mat-form-field-appearance-box { - .mat-form-field-flex { - border-radius: $mat-form-field-box-border-radius; - padding: $mat-form-field-box-line-spacing $mat-form-field-box-side-padding 0 - $mat-form-field-box-side-padding; - } - - .mat-form-field-underline { - height: $mat-form-field-box-border-radius; - border-radius: 0 0 $mat-form-field-box-border-radius $mat-form-field-box-border-radius; - overflow: hidden; - } - - .mat-form-field-underline::before { - content: ''; - display: block; - position: absolute; - bottom: 0; - height: $mat-form-field-box-underline-height; - width: 100%; - } - - .mat-form-field-ripple { - bottom: 0; - height: $mat-form-field-box-underline-height; - } - - // Note that we need this specific of a selector because we don't want - // the hover effect to show when the user hovers over the hints. - &:not(.mat-form-field-disabled) .mat-form-field-flex:hover ~ .mat-form-field-underline { - .mat-form-field-ripple { - opacity: 1; - transform: none; - transition: opacity 600ms $swift-ease-out-timing-function; - } - } - - .mat-form-field-subscript-wrapper { - padding: 0 $mat-form-field-box-subscript-padding; - } -} diff --git a/src/lib/form-field/form-field-fill.scss b/src/lib/form-field/form-field-fill.scss new file mode 100644 index 000000000000..28c4a321a0ad --- /dev/null +++ b/src/lib/form-field/form-field-fill.scss @@ -0,0 +1,59 @@ +@import '../core/style/variables'; +@import '../core/style/vendor-prefixes'; + + +// Styles that only apply to the fill appearance of the form-field. + +// The border radius for the form field fill box. +$mat-form-field-fill-border-radius: 4px !default; +// The height of the underline at the bottom of the form field fill box. +$mat-form-field-fill-underline-height: 1px !default; +// The height of the underline ripple at the bottom of the form field fill box. +$mat-form-field-fill-underline-ripple-height: 2px !default; +// The horizontal padding between the edge of the form field fill box and the start of the text. +$mat-form-field-fill-side-padding: 0.75em !default; +// The vertical padding between the edge of the form field fill box and the start of the text as +// well as between the floating label and the value. +$mat-form-field-fill-line-spacing: 0.75em !default; +// The scale of the subscript and floating label text w.r.t the value text. +$mat-form-field-fill-subscript-font-scale: 0.75 !default; +// The horizontal padding between the edge of the subscript box and the start of the subscript text. +$mat-form-field-fill-subscript-padding: + $mat-form-field-fill-side-padding / $mat-form-field-fill-subscript-font-scale; + + +.mat-form-field-appearance-fill { + .mat-form-field-flex { + border-radius: $mat-form-field-fill-border-radius $mat-form-field-fill-border-radius 0 0; + padding: $mat-form-field-fill-line-spacing $mat-form-field-fill-side-padding 0 + $mat-form-field-fill-side-padding; + } + + .mat-form-field-underline::before { + content: ''; + display: block; + position: absolute; + bottom: 0; + height: $mat-form-field-fill-underline-height; + width: 100%; + } + + .mat-form-field-ripple { + bottom: 0; + height: $mat-form-field-fill-underline-ripple-height; + } + + // Note that we need this specific of a selector because we don't want + // the hover effect to show when the user hovers over the hints. + &:not(.mat-form-field-disabled) .mat-form-field-flex:hover ~ .mat-form-field-underline { + .mat-form-field-ripple { + opacity: 1; + transform: none; + transition: opacity 600ms $swift-ease-out-timing-function; + } + } + + .mat-form-field-subscript-wrapper { + padding: 0 $mat-form-field-fill-subscript-padding; + } +} diff --git a/src/lib/form-field/form-field-standard.scss b/src/lib/form-field/form-field-standard.scss index a79a00c00612..079ea0ba7267 100644 --- a/src/lib/form-field/form-field-standard.scss +++ b/src/lib/form-field/form-field-standard.scss @@ -6,10 +6,8 @@ // The height of the underline. $mat-form-field-standard-underline-height: 1px !default; -// The bottom margin of the underline (used to push it up to align with box appearance underline). -$mat-form-field-standard-underline-margin-bottom: 1px !default; // The padding between the top of the form field and the label text (used to align the standard -// form field with the box appearance). +// form field with the fill appearance). $mat-form-field-standard-padding-top: 0.5em !default; @@ -22,11 +20,10 @@ $mat-form-field-standard-padding-top: 0.5em !default; // The ripple is the blue animation coming on top of it. .mat-form-field-underline { height: $mat-form-field-standard-underline-height; - margin-bottom: $mat-form-field-standard-underline-margin-bottom; } .mat-form-field-ripple { - top: 0; + bottom: 0; height: $mat-form-field-standard-underline-height * 2; } diff --git a/src/lib/form-field/form-field.ts b/src/lib/form-field/form-field.ts index e373b96e844e..e7d8feffa6ab 100644 --- a/src/lib/form-field/form-field.ts +++ b/src/lib/form-field/form-field.ts @@ -62,7 +62,7 @@ export const _MatFormFieldMixinBase = mixinColor(MatFormFieldBase, 'primary'); let nextUniqueId = 0; -export type MatFormFieldAppearance = 'legacy' | 'standard' | 'box'; +export type MatFormFieldAppearance = 'legacy' | 'standard' | 'fill'; /** Container for form controls that applies Material Design styling and behavior. */ @@ -77,7 +77,7 @@ export type MatFormFieldAppearance = 'legacy' | 'standard' | 'box'; // aren't using MatInput. styleUrls: [ 'form-field.css', - 'form-field-box.css', + 'form-field-fill.css', 'form-field-legacy.css', 'form-field-standard.css', '../input/input.css', @@ -86,7 +86,7 @@ export type MatFormFieldAppearance = 'legacy' | 'standard' | 'box'; host: { 'class': 'mat-input-container mat-form-field', '[class.mat-form-field-appearance-standard]': 'appearance == "standard"', - '[class.mat-form-field-appearance-box]': 'appearance == "box"', + '[class.mat-form-field-appearance-fill]': 'appearance == "fill"', '[class.mat-form-field-appearance-legacy]': 'appearance == "legacy"', '[class.mat-input-invalid]': '_control.errorState', '[class.mat-form-field-invalid]': '_control.errorState', diff --git a/src/lib/input/input.spec.ts b/src/lib/input/input.spec.ts index 4a9dde9a514e..fea0b126b848 100644 --- a/src/lib/input/input.spec.ts +++ b/src/lib/input/input.spec.ts @@ -1149,7 +1149,7 @@ describe('MatInput with forms', () => { }); describe('MatInput with appearance', () => { - const nonLegacyAppearances: MatFormFieldAppearance[] = ['standard', 'box']; + const nonLegacyAppearances: MatFormFieldAppearance[] = ['standard', 'fill']; let fixture: ComponentFixture; let testComponent: MatInputWithAppearance; let containerEl: HTMLElement; diff --git a/src/lib/select/select.scss b/src/lib/select/select.scss index 1a8305512d7b..8225a0f73950 100644 --- a/src/lib/select/select.scss +++ b/src/lib/select/select.scss @@ -47,7 +47,7 @@ $mat-select-placeholder-arrow-space: 2 * ($mat-select-arrow-size + $mat-select-a vertical-align: middle; // When used in a box appearance form-field the arrow should be centered in the box. - .mat-form-field-appearance-box & { + .mat-form-field-appearance-fill & { transform: translateY(-50%); } } From 12d16b1c5617a3c7b07299b5edf60431429035ae Mon Sep 17 00:00:00 2001 From: mmalerba Date: Wed, 31 Jan 2018 11:57:40 -0800 Subject: [PATCH 04/13] 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 --- src/demo-app/input/input-demo.html | 15 ++ src/demo-app/input/input-demo.ts | 1 + src/lib/autocomplete/autocomplete-trigger.ts | 2 +- src/lib/datepicker/datepicker-input.ts | 15 +- src/lib/datepicker/datepicker.spec.ts | 4 +- src/lib/datepicker/datepicker.ts | 8 +- src/lib/form-field/BUILD.bazel | 14 ++ .../form-field/_form-field-fill-theme.scss | 18 +-- .../form-field/_form-field-outline-theme.scss | 143 ++++++++++++++++++ src/lib/form-field/_form-field-theme.scss | 3 + src/lib/form-field/form-field-outline.scss | 133 ++++++++++++++++ src/lib/form-field/form-field-standard.scss | 4 +- src/lib/form-field/form-field.html | 18 ++- src/lib/form-field/form-field.scss | 4 +- src/lib/form-field/form-field.ts | 67 +++++++- 15 files changed, 409 insertions(+), 40 deletions(-) create mode 100644 src/lib/form-field/_form-field-outline-theme.scss create mode 100644 src/lib/form-field/form-field-outline.scss diff --git a/src/demo-app/input/input-demo.html b/src/demo-app/input/input-demo.html index 7b07ca716324..2703447a2a52 100644 --- a/src/demo-app/input/input-demo.html +++ b/src/demo-app/input/input-demo.html @@ -588,6 +588,13 @@

<textarea> with ngModel

Please type something here
+ + Outline appearance + + This field is required + Please type something here + + +
@@ -613,6 +620,14 @@

<textarea> with ngModel

Please type something here
+ + Outline appearance + + This field is required + Please type something here + +
diff --git a/src/demo-app/input/input-demo.ts b/src/demo-app/input/input-demo.ts index b42a9f66f8fe..3e6839b15e5b 100644 --- a/src/demo-app/input/input-demo.ts +++ b/src/demo-app/input/input-demo.ts @@ -55,6 +55,7 @@ export class InputDemo { legacyAppearance: string; standardAppearance: string; fillAppearance: string; + outlineAppearance: string; constructor() { setTimeout(() => this.delayedFormControl.setValue('hello'), 100); diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index 8efa9fe4d238..8cce323c18a5 100644 --- a/src/lib/autocomplete/autocomplete-trigger.ts +++ b/src/lib/autocomplete/autocomplete-trigger.ts @@ -512,7 +512,7 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { } private _getConnectedElement(): ElementRef { - return this._formField ? this._formField._connectionContainerRef : this._element; + return this._formField ? this._formField.getConnectedOverlayOrigin() : this._element; } /** Returns the width of the input element, so the panel width can match it. */ diff --git a/src/lib/datepicker/datepicker-input.ts b/src/lib/datepicker/datepicker-input.ts index b938ecfef4cd..daa6db14f980 100644 --- a/src/lib/datepicker/datepicker-input.ts +++ b/src/lib/datepicker/datepicker-input.ts @@ -273,20 +273,17 @@ export class MatDatepickerInput implements AfterContentInit, ControlValueAcce return this._validator ? this._validator(c) : null; } - /** - * Gets the element that the datepicker popup should be connected to. - * @return The element to connect the popup to. - */ + /** @deletion-target 7.0.0 Use `getConnectedOverlayOrigin` instead */ getPopupConnectionElementRef(): ElementRef { - return this._formField ? this._formField.underlineRef : this._elementRef; + return this.getConnectedOverlayOrigin(); } /** - * Determines the offset to be used when the calendar goes into a fallback position. - * Primarily used to prevent the calendar from overlapping the input. + * Gets the element that the datepicker popup should be connected to. + * @return The element to connect the popup to. */ - _getPopupFallbackOffset(): number { - return this._formField ? -this._formField._inputContainerRef.nativeElement.clientHeight : 0; + getConnectedOverlayOrigin(): ElementRef { + return this._formField ? this._formField.getConnectedOverlayOrigin() : this._elementRef; } // Implemented as part of ControlValueAccessor. diff --git a/src/lib/datepicker/datepicker.spec.ts b/src/lib/datepicker/datepicker.spec.ts index 311f8897c75c..e2d8549f08a7 100644 --- a/src/lib/datepicker/datepicker.spec.ts +++ b/src/lib/datepicker/datepicker.spec.ts @@ -273,7 +273,7 @@ describe('MatDatepicker', () => { }); it('should attach popup to native input', () => { - let attachToRef = testComponent.datepickerInput.getPopupConnectionElementRef(); + let attachToRef = testComponent.datepickerInput.getConnectedOverlayOrigin(); expect(attachToRef.nativeElement.tagName.toLowerCase()) .toBe('input', 'popup should be attached to native input'); }); @@ -791,7 +791,7 @@ describe('MatDatepicker', () => { })); it('should attach popup to mat-form-field underline', () => { - let attachToRef = testComponent.datepickerInput.getPopupConnectionElementRef(); + let attachToRef = testComponent.datepickerInput.getConnectedOverlayOrigin(); expect(attachToRef.nativeElement.classList.contains('mat-form-field-underline')) .toBe(true, 'popup should be attached to mat-form-field underline'); }); diff --git a/src/lib/datepicker/datepicker.ts b/src/lib/datepicker/datepicker.ts index 00fb9c046578..6f8883e30a82 100644 --- a/src/lib/datepicker/datepicker.ts +++ b/src/lib/datepicker/datepicker.ts @@ -378,18 +378,14 @@ export class MatDatepicker implements OnDestroy { /** Create the popup PositionStrategy. */ private _createPopupPositionStrategy(): PositionStrategy { - const fallbackOffset = this._datepickerInput._getPopupFallbackOffset(); - return this._overlay.position() - .connectedTo(this._datepickerInput.getPopupConnectionElementRef(), + .connectedTo(this._datepickerInput.getConnectedOverlayOrigin(), {originX: 'start', originY: 'bottom'}, {overlayX: 'start', overlayY: 'top'} ) .withFallbackPosition( {originX: 'start', originY: 'top'}, {overlayX: 'start', overlayY: 'bottom'}, - undefined, - fallbackOffset ) .withFallbackPosition( {originX: 'end', originY: 'bottom'}, @@ -398,8 +394,6 @@ export class MatDatepicker implements OnDestroy { .withFallbackPosition( {originX: 'end', originY: 'top'}, {overlayX: 'end', overlayY: 'bottom'}, - undefined, - fallbackOffset ); } diff --git a/src/lib/form-field/BUILD.bazel b/src/lib/form-field/BUILD.bazel index dc5c0269df82..89e88cea7b10 100644 --- a/src/lib/form-field/BUILD.bazel +++ b/src/lib/form-field/BUILD.bazel @@ -11,6 +11,7 @@ ng_module( ":form_field_css", ":form_field_fill_css", ":form_field_legacy_css", + ":form_field_outline_css", ":form_field_standard_css", "//src/lib/input:input_css" ], @@ -40,6 +41,12 @@ sass_binary( deps = ["//src/lib/core:core_scss_lib"], ) +sass_binary( + name = "form_field_outline_scss", + src = "form-field-outline.scss", + deps = ["//src/lib/core:core_scss_lib"], +) + sass_binary( name = "form_field_standard_scss", src = "form-field-standard.scss", @@ -69,6 +76,13 @@ genrule( cmd = "cat $(locations :form_field_legacy_scss) > $@", ) +genrule( + name = "form_field_outline_css", + srcs = [":form_field_outline_scss"], + outs = ["form-field-outline.css"], + cmd = "cat $(locations :form_field_outline_scss) > $@", +) + genrule( name = "form_field_standard_css", srcs = [":form_field_standard_scss"], diff --git a/src/lib/form-field/_form-field-fill-theme.scss b/src/lib/form-field/_form-field-fill-theme.scss index 70a9b0bef171..6e2d6d090d9f 100644 --- a/src/lib/form-field/_form-field-fill-theme.scss +++ b/src/lib/form-field/_form-field-fill-theme.scss @@ -60,38 +60,36 @@ $mat-form-field-fill-dedupe: 0; $line-height: mat-line-height($config, input); // The amount to scale the font for the floating label and subscript. $subscript-font-scale: 0.75; - // The padding on the infix. Mocks show half of the text size. + // The padding on top of the infix. $infix-padding-top: 0.25em; + // The padding below the infix. $infix-padding-bottom: 0.75em; // The margin applied to the form-field-infix to reserve space for the floating label. $infix-margin-top: 1em * $line-height * $subscript-font-scale; - // The amount we offset the label in the fill appearance. - $fill-appearance-label-offset: -0.5em * $line-height; + // The amount we offset the label from the input text in the fill appearance. + $fill-appearance-label-offset: -0.5em; .mat-form-field-appearance-fill { .mat-form-field-infix { padding: $infix-padding-top 0 $infix-padding-bottom 0; } - .mat-form-field-label { - margin-top: $fill-appearance-label-offset; - } - .mat-form-field-label { top: $infix-margin-top + $infix-padding-top; + margin-top: $fill-appearance-label-offset; } &.mat-form-field-can-float { &.mat-form-field-should-float .mat-form-field-label, .mat-input-server:focus + .mat-form-field-label-wrapper .mat-form-field-label { - @include _mat-form-field-label-floating( + @include _mat-form-field-fill-label-floating( $subscript-font-scale, $infix-padding-top + $fill-appearance-label-offset, $infix-margin-top); } .mat-form-field-autofill-control:-webkit-autofill + .mat-form-field-label-wrapper .mat-form-field-label { - @include _mat-form-field-label-floating( + @include _mat-form-field-fill-label-floating( $subscript-font-scale, $infix-padding-top + $fill-appearance-label-offset, $infix-margin-top); } @@ -100,7 +98,7 @@ $mat-form-field-fill-dedupe: 0; // (used as a pure CSS stand-in for mat-form-field-should-float). .mat-input-server[label]:not(:label-shown) + .mat-form-field-label-wrapper .mat-form-field-label { - @include _mat-form-field-label-floating( + @include _mat-form-field-fill-label-floating( $subscript-font-scale, $infix-padding-top + $fill-appearance-label-offset, $infix-margin-top); } diff --git a/src/lib/form-field/_form-field-outline-theme.scss b/src/lib/form-field/_form-field-outline-theme.scss new file mode 100644 index 000000000000..b3064feaead5 --- /dev/null +++ b/src/lib/form-field/_form-field-outline-theme.scss @@ -0,0 +1,143 @@ +@import '../core/theming/palette'; +@import '../core/theming/theming'; +@import '../core/style/form-common'; +@import '../core/typography/typography-utils'; + + +// Theme styles that only apply to the outline appearance of the form-field. + +@mixin mat-form-field-outline-theme($theme) { + $primary: map-get($theme, primary); + $accent: map-get($theme, accent); + $warn: map-get($theme, warn); + $foreground: map-get($theme, foreground); + $is-dark-theme: map-get($theme, is-dark); + + $label-disabled-color: mat-color($foreground, disabled-text); + $outline-color: mat-color($foreground, divider, if($is-dark-theme, 0.30, 0.12)); + $outline-color-hover: mat-color($foreground, divider, if($is-dark-theme, 1, 0.87)); + $outline-color-primary: mat-color($primary); + $outline-color-accent: mat-color($accent); + $outline-color-warn: mat-color($warn); + $outline-color-disabled: mat-color($foreground, divider, if($is-dark-theme, 0.15, 0.06)); + + .mat-form-field-appearance-outline { + .mat-form-field-outline { + color: $outline-color; + } + + .mat-form-field-outline-thick { + color: $outline-color-hover; + } + + &.mat-focused { + .mat-form-field-outline-thick { + color: $outline-color-primary; + } + + &.mat-accent .mat-form-field-outline-thick { + color: $outline-color-accent; + } + + &.mat-warn .mat-form-field-outline-thick { + color: $outline-color-warn; + } + } + + // Class repeated so that rule is specific enough to override focused accent color case. + &.mat-form-field-invalid.mat-form-field-invalid { + .mat-form-field-outline-thick { + color: $outline-color-warn; + } + } + + &.mat-form-field-disabled { + .mat-form-field-label { + color: $label-disabled-color; + } + + .mat-form-field-outline { + color: $outline-color-disabled; + } + } + } +} + +// Used to make instances of the _mat-form-field-label-floating mixin negligibly different, +// and prevent Google's CSS Optimizer from collapsing the declarations. This is needed because some +// of the selectors contain pseudo-classes not recognized in all browsers. If a browser encounters +// an unknown pseudo-class it will discard the entire rule set. +$mat-form-field-outline-dedupe: 0; + +// Applies a floating label above the form field control itself. +@mixin _mat-form-field-outline-label-floating($font-scale, $infix-padding, $infix-margin-top) { + transform: translateY(-$infix-margin-top - $infix-padding + $mat-form-field-outline-dedupe) + scale($font-scale); + width: 100% / $font-scale + $mat-form-field-outline-dedupe; + + $mat-form-field-fill-dedupe: $mat-form-field-outline-dedupe + 0.00001 !global; +} + +@mixin mat-form-field-outline-typography($config) { + // The unit-less line-height from the font config. + $line-height: mat-line-height($config, input); + // The amount to scale the font for the floating label and subscript. + $subscript-font-scale: 0.75; + // The padding above and below the infix. + $infix-padding: 1em; + // The margin applied to the form-field-infix to reserve space for the floating label. + $infix-margin-top: 1em * $line-height * $subscript-font-scale; + // The space between the bottom of the .mat-form-field-flex area and the subscript wrapper. + // Mocks show half of the text size, but this margin is applied to an element with the subscript + // text font size, so we need to divide by the scale factor to make it half of the original text + // size. + $subscript-margin-top: 0.5em / $subscript-font-scale; + // The padding applied to the form-field-wrapper to reserve space for the subscript, since it's + // absolutely positioned. This is a combination of the subscript's margin and line-height, but we + // need to multiply by the subscript font scale factor since the wrapper has a larger font size. + $wrapper-padding-bottom: ($subscript-margin-top + $line-height) * $subscript-font-scale; + // The amount we offset the label from the input text in the outline appearance. + $outline-appearance-label-offset: -0.25em; + + .mat-form-field-appearance-outline { + .mat-form-field-infix { + padding: $infix-padding 0 $infix-padding 0; + } + + .mat-form-field-outline { + // We want the bottom of the outline to start at the end of the content box, not the padding + // box, so we move it up by the padding amount. + bottom: $wrapper-padding-bottom; + } + + .mat-form-field-label { + top: $infix-margin-top + $infix-padding; + margin-top: $outline-appearance-label-offset; + } + + &.mat-form-field-can-float { + &.mat-form-field-should-float .mat-form-field-label, + .mat-input-server:focus + .mat-form-field-label-wrapper .mat-form-field-label { + @include _mat-form-field-outline-label-floating( + $subscript-font-scale, $infix-padding + $outline-appearance-label-offset, + $infix-margin-top); + } + + .mat-form-field-autofill-control:-webkit-autofill + .mat-form-field-label-wrapper + .mat-form-field-label { + @include _mat-form-field-outline-label-floating( + $subscript-font-scale, $infix-padding + $outline-appearance-label-offset, + $infix-margin-top); + } + + // Server-side rendered matInput with a label attribute but label not shown + // (used as a pure CSS stand-in for mat-form-field-should-float). + .mat-input-server[label]:not(:label-shown) + .mat-form-field-label-wrapper + .mat-form-field-label { + @include _mat-form-field-outline-label-floating( + $subscript-font-scale, $infix-padding + $outline-appearance-label-offset, + $infix-margin-top); + } + } + } +} diff --git a/src/lib/form-field/_form-field-theme.scss b/src/lib/form-field/_form-field-theme.scss index 6864684b3364..94b8b156a5f9 100644 --- a/src/lib/form-field/_form-field-theme.scss +++ b/src/lib/form-field/_form-field-theme.scss @@ -4,6 +4,7 @@ @import '../core/typography/typography-utils'; @import 'form-field-fill-theme.scss'; @import 'form-field-legacy-theme.scss'; +@import 'form-field-outline-theme.scss'; @import 'form-field-standard-theme.scss'; @@ -95,6 +96,7 @@ @include mat-form-field-legacy-theme($theme); @include mat-form-field-standard-theme($theme); @include mat-form-field-fill-theme($theme); + @include mat-form-field-outline-theme($theme); } // Used to make instances of the _mat-form-field-label-floating mixin negligibly different, @@ -222,4 +224,5 @@ $mat-form-field-dedupe: 0; @include mat-form-field-legacy-typography($config); @include mat-form-field-standard-typography($config); @include mat-form-field-fill-typography($config); + @include mat-form-field-outline-typography($config); } diff --git a/src/lib/form-field/form-field-outline.scss b/src/lib/form-field/form-field-outline.scss new file mode 100644 index 000000000000..a4958a19cefe --- /dev/null +++ b/src/lib/form-field/form-field-outline.scss @@ -0,0 +1,133 @@ +@import '../core/style/variables'; +@import '../core/style/vendor-prefixes'; + +// Styles that only apply to the outline appearance of the form-field. + +// The horizontal padding between the edge of the form field fill box and the start of the text. +$mat-form-field-outline-side-padding: 0.75em !default; +// The scale of the subscript and floating label text w.r.t the value text. +$mat-form-field-outline-subscript-font-scale: 0.75 !default; +// The amount of overlap between the label and the outline. +$mat-form-field-outline-label-overlap: 0.25em; +// The border radius of the outline. +$mat-form-field-outline-border-radius: 5px; +// The width of the outline. +$mat-form-field-outline-width: 1px; +// The width of the thick outline (used for focus, hover, and error state). +$mat-form-field-outline-thick-width: 2px; +// The horizontal padding between the edge of the subscript box and the start of the subscript text. +$mat-form-field-outline-subscript-padding: + $mat-form-field-outline-side-padding / $mat-form-field-outline-subscript-font-scale; + + +.mat-form-field-appearance-outline { + // We need to add margin to the top to reserve space in the layout for the label which sticks out + // of the top. In order to ensure that the outline appearance aligns nicely with other form-field + // appearances when center-aligned, we also need to add the same amount of margin to the bottom. + .mat-form-field-wrapper { + margin: $mat-form-field-outline-label-overlap 0; + } + + .mat-form-field-flex { + padding: 0 $mat-form-field-outline-side-padding 0 $mat-form-field-outline-side-padding; + margin-top: -$mat-form-field-outline-label-overlap; + } + + .mat-form-field-outline { + display: flex; + position: absolute; + top: 0; + left: 0; + right: 0; + pointer-events: none; + } + + .mat-form-field-outline-start, + .mat-form-field-outline-end { + border: $mat-form-field-outline-width solid currentColor; + min-width: $mat-form-field-outline-border-radius; + } + + .mat-form-field-outline-start { + border-radius: $mat-form-field-outline-border-radius 0 0 $mat-form-field-outline-border-radius; + border-right-style: none; + + [dir="rtl"] & { + border-right-style: solid; + border-left-style: none; + border-radius: 0 $mat-form-field-outline-border-radius $mat-form-field-outline-border-radius + 0; + } + } + + .mat-form-field-outline-end { + border-radius: 0 $mat-form-field-outline-border-radius $mat-form-field-outline-border-radius 0; + border-left-style: none; + flex-grow: 1; + + [dir="rtl"] & { + border-left-style: solid; + border-right-style: none; + border-radius: $mat-form-field-outline-border-radius 0 0 + $mat-form-field-outline-border-radius; + } + } + + .mat-form-field-outline-gap { + border-bottom: $mat-form-field-outline-width solid currentColor; + + &::before { + content: ''; + display: block; + width: 100%; + border-top: $mat-form-field-outline-width solid currentColor; + opacity: 1; + transition: opacity 300ms $swift-ease-out-timing-function; + } + } + + &.mat-form-field-can-float.mat-form-field-should-float { + .mat-form-field-outline-gap::before { + opacity: 0; + } + } + + .mat-form-field-outline-thick { + opacity: 0; + + .mat-form-field-outline-start, + .mat-form-field-outline-end, + .mat-form-field-outline-gap, + .mat-form-field-outline-gap::before { + border-width: $mat-form-field-outline-thick-width; + transition: border-color 300ms $swift-ease-out-timing-function, + opacity 300ms $swift-ease-out-timing-function; + } + } + + &.mat-focused, + &.mat-form-field-invalid { + .mat-form-field-outline { + opacity: 0; + transition: opacity 100ms $swift-ease-out-timing-function; + } + .mat-form-field-outline-thick { + opacity: 1; + } + } + + &:not(.mat-form-field-disabled) .mat-form-field-flex:hover { + & ~ .mat-form-field-outline { + opacity: 0; + transition: opacity 600ms $swift-ease-out-timing-function; + } + + & ~ .mat-form-field-outline-thick { + opacity: 1; + } + } + + .mat-form-field-subscript-wrapper { + padding: 0 $mat-form-field-outline-subscript-padding; + } +} diff --git a/src/lib/form-field/form-field-standard.scss b/src/lib/form-field/form-field-standard.scss index 079ea0ba7267..99e846cb2860 100644 --- a/src/lib/form-field/form-field-standard.scss +++ b/src/lib/form-field/form-field-standard.scss @@ -7,8 +7,8 @@ // The height of the underline. $mat-form-field-standard-underline-height: 1px !default; // The padding between the top of the form field and the label text (used to align the standard -// form field with the fill appearance). -$mat-form-field-standard-padding-top: 0.5em !default; +// form field with the fill and outline appearances). +$mat-form-field-standard-padding-top: 0.75em !default; .mat-form-field-appearance-standard { diff --git a/src/lib/form-field/form-field.html b/src/lib/form-field/form-field.html index f34857418536..3f2429919c64 100644 --- a/src/lib/form-field/form-field.html +++ b/src/lib/form-field/form-field.html @@ -53,12 +53,28 @@
-
+ +
+ + +
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/lib/form-field/form-field.scss b/src/lib/form-field/form-field.scss index 5e07a9513420..ad133c0c69d9 100644 --- a/src/lib/form-field/form-field.scss +++ b/src/lib/form-field/form-field.scss @@ -99,8 +99,8 @@ $mat-form-field-default-infix-width: 180px !default; transform-origin: 0 0; transition: transform $swift-ease-out-duration $swift-ease-out-timing-function, - color $swift-ease-out-duration $swift-ease-out-timing-function, - width $swift-ease-out-duration $swift-ease-out-timing-function; + color $swift-ease-out-duration $swift-ease-out-timing-function, + width $swift-ease-out-duration $swift-ease-out-timing-function; // Hide the label initially, and only show it when it's floating or the control is empty. display: none; diff --git a/src/lib/form-field/form-field.ts b/src/lib/form-field/form-field.ts index e7d8feffa6ab..97acbaf6e200 100644 --- a/src/lib/form-field/form-field.ts +++ b/src/lib/form-field/form-field.ts @@ -48,6 +48,12 @@ import {MatLabel} from './label'; import {MatPlaceholder} from './placeholder'; import {MatPrefix} from './prefix'; import {MatSuffix} from './suffix'; +import {Directionality} from '@angular/cdk/bidi'; + + +let nextUniqueId = 0; +const floatingLabelScale = 0.75; +const outlineGapPadding = 5; // Boilerplate for applying mixins to MatFormField. @@ -56,13 +62,11 @@ export class MatFormFieldBase { constructor(public _elementRef: ElementRef) { } } -export const _MatFormFieldMixinBase = mixinColor(MatFormFieldBase, 'primary'); - -let nextUniqueId = 0; +export const _MatFormFieldMixinBase = mixinColor(MatFormFieldBase, 'primary'); -export type MatFormFieldAppearance = 'legacy' | 'standard' | 'fill'; +export type MatFormFieldAppearance = 'legacy' | 'standard' | 'fill' | 'outline'; /** Container for form controls that applies Material Design styling and behavior. */ @@ -79,6 +83,7 @@ export type MatFormFieldAppearance = 'legacy' | 'standard' | 'fill'; 'form-field.css', 'form-field-fill.css', 'form-field-legacy.css', + 'form-field-outline.css', 'form-field-standard.css', '../input/input.css', ], @@ -87,6 +92,7 @@ export type MatFormFieldAppearance = 'legacy' | 'standard' | 'fill'; 'class': 'mat-input-container mat-form-field', '[class.mat-form-field-appearance-standard]': 'appearance == "standard"', '[class.mat-form-field-appearance-fill]': 'appearance == "fill"', + '[class.mat-form-field-appearance-outline]': 'appearance == "outline"', '[class.mat-form-field-appearance-legacy]': 'appearance == "legacy"', '[class.mat-input-invalid]': '_control.errorState', '[class.mat-form-field-invalid]': '_control.errorState', @@ -95,6 +101,8 @@ export type MatFormFieldAppearance = 'legacy' | 'standard' | 'fill'; '[class.mat-form-field-hide-placeholder]': '_hideControlPlaceholder()', '[class.mat-form-field-disabled]': '_control.disabled', '[class.mat-focused]': '_control.focused', + '[class.mat-accent]': 'color == "accent"', + '[class.mat-warn]': 'color == "warn"', '[class.ng-untouched]': '_shouldForward("untouched")', '[class.ng-touched]': '_shouldForward("touched")', '[class.ng-pristine]': '_shouldForward("pristine")', @@ -187,8 +195,13 @@ export class MatFormField extends _MatFormFieldMixinBase } private _floatLabel: FloatLabelType; - /** Reference to the form field's underline element. */ + _outlineGapWidth = 0; + + _outlineGapStart = 0; + + /** @deletion-target 7.0.0 */ @ViewChild('underline') underlineRef: ElementRef; + @ViewChild('connectionContainer') _connectionContainerRef: ElementRef; @ViewChild('inputContainer') _inputContainerRef: ElementRef; @ViewChild('label') private _label: ElementRef; @@ -203,13 +216,22 @@ export class MatFormField extends _MatFormFieldMixinBase constructor( public _elementRef: ElementRef, private _changeDetectorRef: ChangeDetectorRef, - @Optional() @Inject(MAT_LABEL_GLOBAL_OPTIONS) labelOptions: LabelOptions) { + @Optional() @Inject(MAT_LABEL_GLOBAL_OPTIONS) labelOptions: LabelOptions, + @Optional() private _dir: Directionality) { super(_elementRef); this._labelOptions = labelOptions ? labelOptions : {}; this.floatLabel = this._labelOptions.float || 'auto'; } + /** + * Gets an ElementRef for the element that a overlay attached to the form-field should be + * positioned relative to. + */ + getConnectedOverlayOrigin(): ElementRef { + return this._connectionContainerRef || this._elementRef; + } + ngAfterContentInit() { this._validateControlChild(); if (this._control.controlType) { @@ -242,6 +264,11 @@ export class MatFormField extends _MatFormFieldMixinBase this._syncDescribedByIds(); this._changeDetectorRef.markForCheck(); }); + + Promise.resolve().then(() => { + this._updateOutlineGap(); + this._changeDetectorRef.detectChanges(); + }); } ngAfterContentChecked() { @@ -381,4 +408,32 @@ export class MatFormField extends _MatFormFieldMixinBase throw getMatFormFieldMissingControlError(); } } + + /** + * Updates the width and position of the gap in the outline. Only relevant for the outline + * appearance. + */ + private _updateOutlineGap() { + if (this.appearance === 'outline' && this._label && this._label.nativeElement.children.length) { + const containerStart = this._getStartEnd( + this._connectionContainerRef.nativeElement.getBoundingClientRect()); + const labelStart = this._getStartEnd( + this._label.nativeElement.children[0].getBoundingClientRect()); + let labelWidth = 0; + for (const child of this._label.nativeElement.children) { + labelWidth += child.offsetWidth; + } + this._outlineGapStart = labelStart - containerStart - outlineGapPadding; + this._outlineGapWidth = labelWidth * floatingLabelScale + outlineGapPadding * 2; + } else { + this._outlineGapStart = 0; + this._outlineGapWidth = 0; + } + this._changeDetectorRef.markForCheck(); + } + + /** Gets the start end of the rect considering the current directionality. */ + private _getStartEnd(rect: ClientRect): number { + return this._dir && this._dir.value === 'rtl' ? rect.right : rect.left; + } } From 75a6c68379ae5e6ba1c5fbae6de3c7311b9d2170 Mon Sep 17 00:00:00 2001 From: mmalerba Date: Thu, 1 Feb 2018 14:44:01 -0800 Subject: [PATCH 05/13] fix(form-field): correct prefix & suffix icons as well as select arrow for various form field appearances (#9743) --- src/demo-app/input/input-demo.scss | 4 +++ src/lib/button/button.scss | 13 ++++++++++ src/lib/chips/chip-list.ts | 12 +++------ src/lib/datepicker/datepicker-toggle.scss | 25 ++++++++++++++++--- .../form-field/_form-field-outline-theme.scss | 2 +- src/lib/form-field/form-field-legacy.scss | 18 +++++++++++++ src/lib/form-field/form-field-outline.scss | 9 +++++-- src/lib/form-field/form-field.scss | 16 +----------- src/lib/icon/icon.scss | 13 ++++++++++ src/lib/select/select.scss | 10 ++++++-- 10 files changed, 89 insertions(+), 33 deletions(-) diff --git a/src/demo-app/input/input-demo.scss b/src/demo-app/input/input-demo.scss index db8cb556622d..d7761343e44b 100644 --- a/src/demo-app/input/input-demo.scss +++ b/src/demo-app/input/input-demo.scss @@ -10,6 +10,10 @@ .demo-card { margin: 16px; + + mat-card-content { + font-size: 16px; + } } .demo-text-align-end { diff --git a/src/lib/button/button.scss b/src/lib/button/button.scss index 0f39d8791bab..cdddea5707b7 100644 --- a/src/lib/button/button.scss +++ b/src/lib/button/button.scss @@ -67,6 +67,19 @@ } } +// Align icon-buttons correctly inside of standard, fill, and outline form-field appearances. +.mat-form-field:not(.mat-form-field-appearance-legacy) { + .mat-form-field-prefix, + .mat-form-field-suffix { + .mat-icon-button { + display: block; + font-size: inherit; + width: 2.5em; + height: 2.5em; + } + } +} + // The text and icon should be vertical aligned inside a button .mat-button, .mat-raised-button, .mat-icon-button, .mat-fab, .mat-mini-fab { color: currentColor; diff --git a/src/lib/chips/chip-list.ts b/src/lib/chips/chip-list.ts index 15fc84581419..f7aa330b07a1 100644 --- a/src/lib/chips/chip-list.ts +++ b/src/lib/chips/chip-list.ts @@ -11,7 +11,6 @@ import {Directionality} from '@angular/cdk/bidi'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; import {SelectionModel} from '@angular/cdk/collections'; import {BACKSPACE} from '@angular/cdk/keycodes'; -import {startWith} from 'rxjs/operators/startWith'; import { AfterContentInit, ChangeDetectionStrategy, @@ -21,7 +20,6 @@ import { DoCheck, ElementRef, EventEmitter, - Inject, Input, OnDestroy, OnInit, @@ -31,16 +29,12 @@ import { Self, ViewEncapsulation, } from '@angular/core'; -import { - ControlValueAccessor, - FormGroupDirective, - NgControl, - NgForm -} from '@angular/forms'; -import {ErrorStateMatcher, mixinErrorState, CanUpdateErrorState} from '@angular/material/core'; +import {ControlValueAccessor, FormGroupDirective, NgControl, NgForm} from '@angular/forms'; +import {CanUpdateErrorState, ErrorStateMatcher, mixinErrorState} from '@angular/material/core'; import {MatFormFieldControl} from '@angular/material/form-field'; import {Observable} from 'rxjs/Observable'; import {merge} from 'rxjs/observable/merge'; +import {startWith} from 'rxjs/operators/startWith'; import {Subscription} from 'rxjs/Subscription'; import {MatChip, MatChipEvent, MatChipSelectionChange} from './chip'; import {MatChipInput} from './chip-input'; diff --git a/src/lib/datepicker/datepicker-toggle.scss b/src/lib/datepicker/datepicker-toggle.scss index 839458707005..87d954d5b6d7 100644 --- a/src/lib/datepicker/datepicker-toggle.scss +++ b/src/lib/datepicker/datepicker-toggle.scss @@ -1,6 +1,23 @@ -.mat-datepicker-toggle-default-icon { - .mat-form-field-prefix &, - .mat-form-field-suffix & { - width: 1em; +.mat-form-field-appearance-legacy { + .mat-form-field-prefix, + .mat-form-field-suffix { + .mat-datepicker-toggle-default-icon { + width: 1em; + } + } +} + +.mat-form-field:not(.mat-form-field-appearance-legacy) { + .mat-form-field-prefix, + .mat-form-field-suffix { + .mat-datepicker-toggle-default-icon { + display: block; + width: 1.5em; + height: 1.5em; + } + + .mat-icon-button .mat-datepicker-toggle-default-icon { + margin: auto; + } } } diff --git a/src/lib/form-field/_form-field-outline-theme.scss b/src/lib/form-field/_form-field-outline-theme.scss index b3064feaead5..c534faa22207 100644 --- a/src/lib/form-field/_form-field-outline-theme.scss +++ b/src/lib/form-field/_form-field-outline-theme.scss @@ -14,7 +14,7 @@ $is-dark-theme: map-get($theme, is-dark); $label-disabled-color: mat-color($foreground, disabled-text); - $outline-color: mat-color($foreground, divider, if($is-dark-theme, 0.30, 0.12)); + $outline-color: mat-color($foreground, divider, if($is-dark-theme, 0.3, 0.12)); $outline-color-hover: mat-color($foreground, divider, if($is-dark-theme, 1, 0.87)); $outline-color-primary: mat-color($primary); $outline-color-accent: mat-color($accent); diff --git a/src/lib/form-field/form-field-legacy.scss b/src/lib/form-field/form-field-legacy.scss index 301fea946862..52c901489169 100644 --- a/src/lib/form-field/form-field-legacy.scss +++ b/src/lib/form-field/form-field-legacy.scss @@ -14,6 +14,24 @@ $mat-form-field-legacy-underline-height: 1px !default; -ms-transform: none; } + .mat-form-field-prefix, + .mat-form-field-suffix { + // Allow icons in a prefix or suffix to adapt to the correct size. + .mat-icon { + width: 1em; + } + + // Allow icon buttons in a prefix or suffix to adapt to the correct size. + .mat-icon-button { + font: inherit; + vertical-align: baseline; + + .mat-icon { + font-size: inherit; + } + } + } + // The underline is what's shown under the control, its prefix and its suffix. // The ripple is the blue animation coming on top of it. .mat-form-field-underline { diff --git a/src/lib/form-field/form-field-outline.scss b/src/lib/form-field/form-field-outline.scss index a4958a19cefe..d3f08c98607d 100644 --- a/src/lib/form-field/form-field-outline.scss +++ b/src/lib/form-field/form-field-outline.scss @@ -33,6 +33,11 @@ $mat-form-field-outline-subscript-padding: margin-top: -$mat-form-field-outline-label-overlap; } + .mat-form-field-prefix, + .mat-form-field-suffix { + top: $mat-form-field-outline-label-overlap; + } + .mat-form-field-outline { display: flex; position: absolute; @@ -52,7 +57,7 @@ $mat-form-field-outline-subscript-padding: border-radius: $mat-form-field-outline-border-radius 0 0 $mat-form-field-outline-border-radius; border-right-style: none; - [dir="rtl"] & { + [dir='rtl'] & { border-right-style: solid; border-left-style: none; border-radius: 0 $mat-form-field-outline-border-radius $mat-form-field-outline-border-radius @@ -65,7 +70,7 @@ $mat-form-field-outline-subscript-padding: border-left-style: none; flex-grow: 1; - [dir="rtl"] & { + [dir='rtl'] & { border-left-style: solid; border-right-style: none; border-radius: $mat-form-field-outline-border-radius 0 0 diff --git a/src/lib/form-field/form-field.scss b/src/lib/form-field/form-field.scss index ad133c0c69d9..b08fbdee23a2 100644 --- a/src/lib/form-field/form-field.scss +++ b/src/lib/form-field/form-field.scss @@ -43,21 +43,7 @@ $mat-form-field-default-infix-width: 180px !default; .mat-form-field-suffix { white-space: nowrap; flex: none; - - // Allow icons in a prefix or suffix to adapt to the correct size. - .mat-icon { - width: 1em; - } - - // Allow icon buttons in a prefix or suffix to adapt to the correct size. - .mat-icon-button { - font: inherit; - vertical-align: baseline; - - .mat-icon { - font-size: inherit; - } - } + position: relative; } .mat-form-field-infix { diff --git a/src/lib/icon/icon.scss b/src/lib/icon/icon.scss index 54bafa4ff9a8..21b38398017b 100644 --- a/src/lib/icon/icon.scss +++ b/src/lib/icon/icon.scss @@ -11,3 +11,16 @@ $mat-icon-size: 24px !default; height: $mat-icon-size; width: $mat-icon-size; } + +.mat-form-field:not(.mat-form-field-appearance-legacy) { + .mat-form-field-prefix, + .mat-form-field-suffix { + .mat-icon { + display: block; + } + + .mat-icon-button .mat-icon { + margin: auto; + } + } +} diff --git a/src/lib/select/select.scss b/src/lib/select/select.scss index 8225a0f73950..5f8ccd5c0e6e 100644 --- a/src/lib/select/select.scss +++ b/src/lib/select/select.scss @@ -46,10 +46,16 @@ $mat-select-placeholder-arrow-space: 2 * ($mat-select-arrow-size + $mat-select-a display: table-cell; vertical-align: middle; - // When used in a box appearance form-field the arrow should be centered in the box. - .mat-form-field-appearance-fill & { + // When used in a box or standard appearance form-field the arrow should be shifted up 50%. + .mat-form-field-appearance-fill &, + .mat-form-field-appearance-standard & { transform: translateY(-50%); } + + // When used in a outline form-field the arrow should be shifted up 25%. + .mat-form-field-appearance-outline & { + transform: translateY(-25%); + } } .mat-select-arrow { From 0981c2372ccbf9b49618cb7286fc44c9a3a4c366 Mon Sep 17 00:00:00 2001 From: mmalerba Date: Fri, 2 Feb 2018 09:55:01 -0800 Subject: [PATCH 06/13] feat(input): add utilities for custom styling and monitoring state of input autofill (#9719) * add utility for monitoring input autofill * add scss mixin for styling input autofill colors * tests * move everything from cdk to MatInputModule * address comments * add doc comments --- src/demo-app/demo-material-module.ts | 21 +-- src/demo-app/input/input-demo.html | 18 +++ src/demo-app/input/input-demo.scss | 6 + src/demo-app/input/input-demo.ts | 2 + src/lib/core/_core.scss | 2 + src/lib/input/BUILD.bazel | 1 + src/lib/input/_autofill.scss | 43 ++++++ src/lib/input/autofill-prebuilt.scss | 3 + src/lib/input/autofill.spec.ts | 192 +++++++++++++++++++++++++++ src/lib/input/autofill.ts | 135 +++++++++++++++++++ src/lib/input/input-module.ts | 7 +- src/lib/input/public-api.ts | 3 +- 12 files changed, 420 insertions(+), 13 deletions(-) create mode 100644 src/lib/input/_autofill.scss create mode 100644 src/lib/input/autofill-prebuilt.scss create mode 100644 src/lib/input/autofill.spec.ts create mode 100644 src/lib/input/autofill.ts diff --git a/src/demo-app/demo-material-module.ts b/src/demo-app/demo-material-module.ts index 33ff49f3c389..7e4ffac77e73 100644 --- a/src/demo-app/demo-material-module.ts +++ b/src/demo-app/demo-material-module.ts @@ -6,6 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ +import {A11yModule} from '@angular/cdk/a11y'; +import {CdkAccordionModule} from '@angular/cdk/accordion'; +import {BidiModule} from '@angular/cdk/bidi'; +import {ObserversModule} from '@angular/cdk/observers'; +import {OverlayModule} from '@angular/cdk/overlay'; +import {PlatformModule} from '@angular/cdk/platform'; +import {PortalModule} from '@angular/cdk/portal'; +import {CdkTableModule} from '@angular/cdk/table'; import {NgModule} from '@angular/core'; import { MatAutocompleteModule, @@ -24,31 +32,24 @@ import { MatInputModule, MatListModule, MatMenuModule, + MatNativeDateModule, MatPaginatorModule, MatProgressBarModule, MatProgressSpinnerModule, MatRadioModule, + MatRippleModule, MatSelectModule, MatSidenavModule, MatSliderModule, MatSlideToggleModule, MatSnackBarModule, MatSortModule, + MatStepperModule, MatTableModule, MatTabsModule, MatToolbarModule, MatTooltipModule, - MatStepperModule, } from '@angular/material'; -import {MatNativeDateModule, MatRippleModule} from '@angular/material'; -import {CdkTableModule} from '@angular/cdk/table'; -import {CdkAccordionModule} from '@angular/cdk/accordion'; -import {A11yModule} from '@angular/cdk/a11y'; -import {BidiModule} from '@angular/cdk/bidi'; -import {OverlayModule} from '@angular/cdk/overlay'; -import {PlatformModule} from '@angular/cdk/platform'; -import {ObserversModule} from '@angular/cdk/observers'; -import {PortalModule} from '@angular/cdk/portal'; /** * NgModule that includes all Material modules that are required to serve the demo-app. diff --git a/src/demo-app/input/input-demo.html b/src/demo-app/input/input-demo.html index 2703447a2a52..d538ad3c9194 100644 --- a/src/demo-app/input/input-demo.html +++ b/src/demo-app/input/input-demo.html @@ -631,3 +631,21 @@

<textarea> with ngModel

+ + + Autofill + + + + Use custom autofill style + + + Autofill monitored + + + + is autofilled? {{isAutofilled ? 'yes' : 'no'}} + + + diff --git a/src/demo-app/input/input-demo.scss b/src/demo-app/input/input-demo.scss index d7761343e44b..a15c23b80af5 100644 --- a/src/demo-app/input/input-demo.scss +++ b/src/demo-app/input/input-demo.scss @@ -1,3 +1,5 @@ +@import '../../../dist/packages/material/input/autofill'; + .demo-basic { padding: 0; } @@ -27,3 +29,7 @@ padding: 0; background: lightblue; } + +.demo-custom-autofill-style { + @include mat-input-autofill-color(transparent, red); +} diff --git a/src/demo-app/input/input-demo.ts b/src/demo-app/input/input-demo.ts index 3e6839b15e5b..e8989603029b 100644 --- a/src/demo-app/input/input-demo.ts +++ b/src/demo-app/input/input-demo.ts @@ -51,6 +51,8 @@ export class InputDemo { emailFormControl = new FormControl('', [Validators.required, Validators.pattern(EMAIL_REGEX)]); delayedFormControl = new FormControl(''); model = 'hello'; + isAutofilled = false; + customAutofillStyle = true; legacyAppearance: string; standardAppearance: string; diff --git a/src/lib/core/_core.scss b/src/lib/core/_core.scss index ae78d5e77828..748e3a5735b7 100644 --- a/src/lib/core/_core.scss +++ b/src/lib/core/_core.scss @@ -2,6 +2,7 @@ // up into a single flat scss file for material. @import '../../cdk/overlay/overlay'; @import '../../cdk/a11y/a11y'; +@import '../input/autofill'; // Core styles that can be used to apply material design treatments to any element. @import 'style/elevation'; @@ -26,6 +27,7 @@ @include mat-ripple(); @include cdk-a11y(); @include cdk-overlay(); + @include mat-input-autofill(); } // Mixin that renders all of the core styles that depend on the theme. diff --git a/src/lib/input/BUILD.bazel b/src/lib/input/BUILD.bazel index 6bd7b7bbe66f..413c7d8606f7 100644 --- a/src/lib/input/BUILD.bazel +++ b/src/lib/input/BUILD.bazel @@ -8,6 +8,7 @@ ng_module( srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts"]), module_name = "@angular/material/input", deps = [ + "@rxjs", "//src/lib/core", "//src/lib/form-field", "//src/cdk/coercion", diff --git a/src/lib/input/_autofill.scss b/src/lib/input/_autofill.scss new file mode 100644 index 000000000000..fc778c84a9eb --- /dev/null +++ b/src/lib/input/_autofill.scss @@ -0,0 +1,43 @@ +// Core styles that enable monitoring autofill state of inputs. +@mixin mat-input-autofill { + // Keyframes that apply no styles, but allow us to monitor when an input becomes autofilled + // by watching for the animation events that are fired when they start. + // Based on: https://medium.com/@brunn/detecting-autofilled-fields-in-javascript-aed598d25da7 + @keyframes mat-input-autofill-start {} + @keyframes mat-input-autofill-end {} + + .mat-input-autofill-monitored:-webkit-autofill { + animation-name: mat-input-autofill-start; + } + + .mat-input-autofill-monitored:not(:-webkit-autofill) { + animation-name: mat-input-autofill-end; + } +} + +// Used to generate UIDs for keyframes used to change the input autofill styles. +$mat-input-autofill-color-frame-count: 0; + +// Mixin used to apply custom background and foreground colors to an autofilled input. Based on: +// https://stackoverflow.com/questions/2781549/ +// removing-input-background-colour-for-chrome-autocomplete#answer-37432260 +@mixin mat-input-autofill-color($background, $foreground:'') { + @keyframes mat-input-autofill-color-#{$mat-input-autofill-color-frame-count} { + to { + background: $background; + @if $foreground != '' { color: $foreground; } + } + } + + &:-webkit-autofill { + animation-name: mat-input-autofill-color-#{$mat-input-autofill-color-frame-count}; + animation-fill-mode: both; + } + + &.mat-input-autofill-monitored:-webkit-autofill { + animation-name: mat-input-autofill-start, + mat-input-autofill-color-#{$mat-input-autofill-color-frame-count}; + } + + $mat-input-autofill-color-frame-count: $mat-input-autofill-color-frame-count + 1 !global; +} diff --git a/src/lib/input/autofill-prebuilt.scss b/src/lib/input/autofill-prebuilt.scss new file mode 100644 index 000000000000..39ae312c13f3 --- /dev/null +++ b/src/lib/input/autofill-prebuilt.scss @@ -0,0 +1,3 @@ +@import 'autofill'; + +@include mat-input-autofill(); diff --git a/src/lib/input/autofill.spec.ts b/src/lib/input/autofill.spec.ts new file mode 100644 index 000000000000..f175ff4efa70 --- /dev/null +++ b/src/lib/input/autofill.spec.ts @@ -0,0 +1,192 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * 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 {supportsPassiveEventListeners} from '@angular/cdk/platform'; +import {Component, ElementRef, ViewChild} from '@angular/core'; +import {ComponentFixture, inject, TestBed} from '@angular/core/testing'; +import {empty as observableEmpty} from 'rxjs/observable/empty'; +import {AutofillEvent, AutofillMonitor} from './autofill'; +import {MatInputModule} from './input-module'; + + +const listenerOptions: any = supportsPassiveEventListeners() ? {passive: true} : false; + + +describe('AutofillMonitor', () => { + let autofillMonitor: AutofillMonitor; + let fixture: ComponentFixture; + let testComponent: Inputs; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [MatInputModule], + declarations: [Inputs], + }).compileComponents(); + }); + + beforeEach(inject([AutofillMonitor], (afm: AutofillMonitor) => { + autofillMonitor = afm; + fixture = TestBed.createComponent(Inputs); + testComponent = fixture.componentInstance; + + for (const input of [testComponent.input1, testComponent.input2, testComponent.input3]) { + spyOn(input.nativeElement, 'addEventListener'); + spyOn(input.nativeElement, 'removeEventListener'); + } + + fixture.detectChanges(); + })); + + afterEach(() => { + // Call destroy to make sure we clean up all listeners. + autofillMonitor.ngOnDestroy(); + }); + + it('should add monitored class and listener upon monitoring', () => { + const inputEl = testComponent.input1.nativeElement; + expect(inputEl.addEventListener).not.toHaveBeenCalled(); + + autofillMonitor.monitor(inputEl); + expect(inputEl.classList).toContain('mat-input-autofill-monitored'); + expect(inputEl.addEventListener) + .toHaveBeenCalledWith('animationstart', jasmine.any(Function), listenerOptions); + }); + + it('should not add multiple listeners to the same element', () => { + const inputEl = testComponent.input1.nativeElement; + expect(inputEl.addEventListener).not.toHaveBeenCalled(); + + autofillMonitor.monitor(inputEl); + autofillMonitor.monitor(inputEl); + expect(inputEl.addEventListener).toHaveBeenCalledTimes(1); + }); + + it('should remove monitored class and listener upon stop monitoring', () => { + const inputEl = testComponent.input1.nativeElement; + autofillMonitor.monitor(inputEl); + expect(inputEl.classList).toContain('mat-input-autofill-monitored'); + expect(inputEl.removeEventListener).not.toHaveBeenCalled(); + + autofillMonitor.stopMonitoring(inputEl); + expect(inputEl.classList).not.toContain('mat-input-autofill-monitored'); + expect(inputEl.removeEventListener) + .toHaveBeenCalledWith('animationstart', jasmine.any(Function), listenerOptions); + }); + + it('should stop monitoring all monitored elements upon destroy', () => { + const inputEl1 = testComponent.input1.nativeElement; + const inputEl2 = testComponent.input2.nativeElement; + const inputEl3 = testComponent.input3.nativeElement; + autofillMonitor.monitor(inputEl1); + autofillMonitor.monitor(inputEl2); + autofillMonitor.monitor(inputEl3); + expect(inputEl1.removeEventListener).not.toHaveBeenCalled(); + expect(inputEl2.removeEventListener).not.toHaveBeenCalled(); + expect(inputEl3.removeEventListener).not.toHaveBeenCalled(); + + autofillMonitor.ngOnDestroy(); + expect(inputEl1.removeEventListener).toHaveBeenCalled(); + expect(inputEl2.removeEventListener).toHaveBeenCalled(); + expect(inputEl3.removeEventListener).toHaveBeenCalled(); + }); + + it('should emit and add filled class upon start animation', () => { + const inputEl = testComponent.input1.nativeElement; + let animationStartCallback: Function = () => {}; + let autofillStreamEvent: AutofillEvent | null = null; + inputEl.addEventListener.and.callFake((_, cb) => animationStartCallback = cb); + const autofillStream = autofillMonitor.monitor(inputEl); + autofillStream.subscribe(event => autofillStreamEvent = event); + expect(autofillStreamEvent).toBeNull(); + expect(inputEl.classList).not.toContain('mat-input-autofilled'); + + animationStartCallback({animationName: 'mat-input-autofill-start', target: inputEl}); + expect(inputEl.classList).toContain('mat-input-autofilled'); + expect(autofillStreamEvent).toEqual({target: inputEl, isAutofilled: true} as any); + }); + + it('should emit and remove filled class upon end animation', () => { + const inputEl = testComponent.input1.nativeElement; + let animationStartCallback: Function = () => {}; + let autofillStreamEvent: AutofillEvent | null = null; + inputEl.addEventListener.and.callFake((_, cb) => animationStartCallback = cb); + const autofillStream = autofillMonitor.monitor(inputEl); + autofillStream.subscribe(event => autofillStreamEvent = event); + animationStartCallback({animationName: 'mat-input-autofill-start', target: inputEl}); + expect(inputEl.classList).toContain('mat-input-autofilled'); + expect(autofillStreamEvent).toEqual({target: inputEl, isAutofilled: true} as any); + + animationStartCallback({animationName: 'mat-input-autofill-end', target: inputEl}); + expect(inputEl.classList).not.toContain('mat-input-autofilled'); + expect(autofillStreamEvent).toEqual({target: inputEl, isAutofilled: false} as any); + }); + + it('should cleanup filled class if monitoring stopped in autofilled state', () => { + const inputEl = testComponent.input1.nativeElement; + let animationStartCallback: Function = () => {}; + inputEl.addEventListener.and.callFake((_, cb) => animationStartCallback = cb); + autofillMonitor.monitor(inputEl); + animationStartCallback({animationName: 'mat-input-autofill-start', target: inputEl}); + expect(inputEl.classList).toContain('mat-input-autofilled'); + + autofillMonitor.stopMonitoring(inputEl); + expect(inputEl.classlist).not.toContain('mat-input-autofilled'); + }); +}); + +describe('matAutofill', () => { + let autofillMonitor: AutofillMonitor; + let fixture: ComponentFixture; + let testComponent: InputWithMatAutofilled; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [MatInputModule], + declarations: [InputWithMatAutofilled], + }).compileComponents(); + }); + + beforeEach(inject([AutofillMonitor], (afm: AutofillMonitor) => { + autofillMonitor = afm; + spyOn(autofillMonitor, 'monitor').and.returnValue(observableEmpty()); + spyOn(autofillMonitor, 'stopMonitoring'); + fixture = TestBed.createComponent(InputWithMatAutofilled); + testComponent = fixture.componentInstance; + fixture.detectChanges(); + })); + + it('should monitor host element on init', () => { + expect(autofillMonitor.monitor).toHaveBeenCalledWith(testComponent.input.nativeElement); + }); + + it('should stop monitoring host element on destroy', () => { + expect(autofillMonitor.stopMonitoring).not.toHaveBeenCalled(); + fixture.destroy(); + expect(autofillMonitor.stopMonitoring).toHaveBeenCalledWith(testComponent.input.nativeElement); + }); +}); + +@Component({ + template: ` + + + + ` +}) +class Inputs { + @ViewChild('input1') input1: ElementRef; + @ViewChild('input2') input2: ElementRef; + @ViewChild('input3') input3: ElementRef; +} + +@Component({ + template: `` +}) +class InputWithMatAutofilled { + @ViewChild('input') input: ElementRef; +} diff --git a/src/lib/input/autofill.ts b/src/lib/input/autofill.ts new file mode 100644 index 000000000000..84dd4cc5f098 --- /dev/null +++ b/src/lib/input/autofill.ts @@ -0,0 +1,135 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * 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 {Platform, supportsPassiveEventListeners} from '@angular/cdk/platform'; +import { + Directive, + ElementRef, + EventEmitter, + Injectable, + NgZone, + OnDestroy, + Output +} from '@angular/core'; +import {Observable} from 'rxjs/Observable'; +import {empty as observableEmpty} from 'rxjs/observable/empty'; +import {Subject} from 'rxjs/Subject'; + + +/** An event that is emitted when the autofill state of an input changes. */ +export type AutofillEvent = { + /** The element whose autofill state changes. */ + target: Element; + /** Whether the element is currently autofilled. */ + isAutofilled: boolean; +}; + + +/** Used to track info about currently monitored elements. */ +type MonitoredElementInfo = { + subject: Subject; + unlisten: () => void; +}; + + +/** Options to pass to the animationstart listener. */ +const listenerOptions: any = supportsPassiveEventListeners() ? {passive: true} : false; + + +/** + * An injectable service that can be used to monitor the autofill state of an input. + * Based on the following blog post: + * https://medium.com/@brunn/detecting-autofilled-fields-in-javascript-aed598d25da7 + */ +@Injectable() +export class AutofillMonitor implements OnDestroy { + private _monitoredElements = new Map(); + + constructor(private _platform: Platform, private _ngZone: NgZone) {} + + /** + * Monitor for changes in the autofill state of the given input element. + * @param element The element to monitor. + * @return A stream of autofill state changes. + */ + monitor(element: Element): Observable { + if (!this._platform.isBrowser) { + return observableEmpty(); + } + + const info = this._monitoredElements.get(element); + if (info) { + return info.subject.asObservable(); + } + + const result = new Subject(); + const listener = (event: AnimationEvent) => { + if (event.animationName === 'mat-input-autofill-start') { + element.classList.add('mat-input-autofilled'); + result.next({target: event.target as Element, isAutofilled: true}); + } else if (event.animationName === 'mat-input-autofill-end') { + element.classList.remove('mat-input-autofilled'); + result.next({target: event.target as Element, isAutofilled: false}); + } + }; + + this._ngZone.runOutsideAngular(() => { + element.addEventListener('animationstart', listener, listenerOptions); + }); + element.classList.add('mat-input-autofill-monitored'); + + this._monitoredElements.set(element, { + subject: result, + unlisten: () => { + element.removeEventListener('animationstart', listener, listenerOptions); + } + }); + + return result.asObservable(); + } + + /** + * Stop monitoring the autofill state of the given input element. + * @param element The element to stop monitoring. + */ + stopMonitoring(element: Element) { + const info = this._monitoredElements.get(element); + if (info) { + info.unlisten(); + element.classList.remove('mat-input-autofill-monitored'); + element.classList.remove('mat-input-autofilled'); + this._monitoredElements.delete(element); + } + } + + ngOnDestroy() { + this._monitoredElements.forEach(info => { + info.unlisten(); + info.subject.complete(); + }); + } +} + + +/** A directive that can be used to monitor the autofill state of an input. */ +@Directive({ + selector: '[matAutofill]', +}) +export class MatAutofill implements OnDestroy { + @Output() matAutofill = new EventEmitter(); + + constructor(private _elementRef: ElementRef, private _autofillMonitor: AutofillMonitor, + ngZone: NgZone) { + this._autofillMonitor.monitor(this._elementRef.nativeElement) + .subscribe(event => ngZone.run(() => this.matAutofill.emit(event))); + } + + ngOnDestroy() { + this._autofillMonitor.stopMonitoring(this._elementRef.nativeElement); + } +} diff --git a/src/lib/input/input-module.ts b/src/lib/input/input-module.ts index d89a13c3db5b..00f9a838637d 100644 --- a/src/lib/input/input-module.ts +++ b/src/lib/input/input-module.ts @@ -9,14 +9,16 @@ import {PlatformModule} from '@angular/cdk/platform'; import {CommonModule} from '@angular/common'; import {NgModule} from '@angular/core'; +import {ErrorStateMatcher} from '@angular/material/core'; import {MatFormFieldModule} from '@angular/material/form-field'; +import {AutofillMonitor, MatAutofill} from './autofill'; import {MatTextareaAutosize} from './autosize'; import {MatInput} from './input'; -import {ErrorStateMatcher} from '@angular/material/core'; @NgModule({ declarations: [ + MatAutofill, MatInput, MatTextareaAutosize, ], @@ -26,12 +28,13 @@ import {ErrorStateMatcher} from '@angular/material/core'; PlatformModule, ], exports: [ + MatAutofill, // We re-export the `MatFormFieldModule` since `MatInput` will almost always // be used together with `MatFormField`. MatFormFieldModule, MatInput, MatTextareaAutosize, ], - providers: [ErrorStateMatcher], + providers: [ErrorStateMatcher, AutofillMonitor], }) export class MatInputModule {} diff --git a/src/lib/input/public-api.ts b/src/lib/input/public-api.ts index 5eaaf7d50575..f2f80acf31e8 100644 --- a/src/lib/input/public-api.ts +++ b/src/lib/input/public-api.ts @@ -7,9 +7,10 @@ */ -export * from './input-module'; +export * from './autofill'; export * from './autosize'; export * from './input'; export * from './input-errors'; +export * from './input-module'; export * from './input-value-accessor'; From 0460f27d7a39eef000698b87885308f2b4d92076 Mon Sep 17 00:00:00 2001 From: mmalerba Date: Fri, 2 Feb 2018 13:26:19 -0800 Subject: [PATCH 07/13] fix(form-field): fixes for outline appearance (#9759) * use the `AutofillMonitor` in `MatInput` * Make `updateOutlineGap` public so users can call it if needed --- .../form-field/_form-field-fill-theme.scss | 7 ----- .../form-field/_form-field-legacy-theme.scss | 1 + .../form-field/_form-field-outline-theme.scss | 7 ----- src/lib/form-field/_form-field-theme.scss | 6 ----- src/lib/form-field/form-field-control.ts | 6 +++++ src/lib/form-field/form-field.scss | 1 + src/lib/form-field/form-field.ts | 5 ++-- src/lib/input/input.ts | 27 ++++++++++++++++--- 8 files changed, 35 insertions(+), 25 deletions(-) diff --git a/src/lib/form-field/_form-field-fill-theme.scss b/src/lib/form-field/_form-field-fill-theme.scss index 6e2d6d090d9f..ebb23c27552b 100644 --- a/src/lib/form-field/_form-field-fill-theme.scss +++ b/src/lib/form-field/_form-field-fill-theme.scss @@ -87,13 +87,6 @@ $mat-form-field-fill-dedupe: 0; $infix-margin-top); } - .mat-form-field-autofill-control:-webkit-autofill + .mat-form-field-label-wrapper - .mat-form-field-label { - @include _mat-form-field-fill-label-floating( - $subscript-font-scale, $infix-padding-top + $fill-appearance-label-offset, - $infix-margin-top); - } - // Server-side rendered matInput with a label attribute but label not shown // (used as a pure CSS stand-in for mat-form-field-should-float). .mat-input-server[label]:not(:label-shown) + .mat-form-field-label-wrapper diff --git a/src/lib/form-field/_form-field-legacy-theme.scss b/src/lib/form-field/_form-field-legacy-theme.scss index 6e09a89f1579..5bf738e6b147 100644 --- a/src/lib/form-field/_form-field-legacy-theme.scss +++ b/src/lib/form-field/_form-field-legacy-theme.scss @@ -87,6 +87,7 @@ $mat-form-field-legacy-dedupe: 0; $subscript-font-scale, $infix-padding, $infix-margin-top); } + // @deletion-target 7.0.0 will rely on AutofillMonitor instead. .mat-form-field-autofill-control:-webkit-autofill + .mat-form-field-label-wrapper .mat-form-field-label { @include _mat-form-field-legacy-label-floating( diff --git a/src/lib/form-field/_form-field-outline-theme.scss b/src/lib/form-field/_form-field-outline-theme.scss index c534faa22207..26c382d1f058 100644 --- a/src/lib/form-field/_form-field-outline-theme.scss +++ b/src/lib/form-field/_form-field-outline-theme.scss @@ -123,13 +123,6 @@ $mat-form-field-outline-dedupe: 0; $infix-margin-top); } - .mat-form-field-autofill-control:-webkit-autofill + .mat-form-field-label-wrapper - .mat-form-field-label { - @include _mat-form-field-outline-label-floating( - $subscript-font-scale, $infix-padding + $outline-appearance-label-offset, - $infix-margin-top); - } - // Server-side rendered matInput with a label attribute but label not shown // (used as a pure CSS stand-in for mat-form-field-should-float). .mat-input-server[label]:not(:label-shown) + .mat-form-field-label-wrapper diff --git a/src/lib/form-field/_form-field-theme.scss b/src/lib/form-field/_form-field-theme.scss index 94b8b156a5f9..94c6f6a442fd 100644 --- a/src/lib/form-field/_form-field-theme.scss +++ b/src/lib/form-field/_form-field-theme.scss @@ -182,12 +182,6 @@ $mat-form-field-dedupe: 0; $subscript-font-scale, $infix-padding, $infix-margin-top); } - .mat-form-field-autofill-control:-webkit-autofill + .mat-form-field-label-wrapper - .mat-form-field-label { - @include _mat-form-field-label-floating( - $subscript-font-scale, $infix-padding, $infix-margin-top); - } - // Server-side rendered matInput with a label attribute but label not shown // (used as a pure CSS stand-in for mat-form-field-should-float). .mat-input-server[label]:not(:label-shown) + .mat-form-field-label-wrapper diff --git a/src/lib/form-field/form-field-control.ts b/src/lib/form-field/form-field-control.ts index c19e44866187..ce003328e4e2 100644 --- a/src/lib/form-field/form-field-control.ts +++ b/src/lib/form-field/form-field-control.ts @@ -62,6 +62,12 @@ export abstract class MatFormFieldControl { */ readonly controlType?: string; + /** + * Whether the input is currently in an autofilled state. If property is not present on the + * control it is assumed to be false. + */ + readonly autofilled?: boolean; + /** Sets the list of element IDs that currently describe this control. */ abstract setDescribedByIds(ids: string[]): void; diff --git a/src/lib/form-field/form-field.scss b/src/lib/form-field/form-field.scss index b08fbdee23a2..9e242fb46dc7 100644 --- a/src/lib/form-field/form-field.scss +++ b/src/lib/form-field/form-field.scss @@ -107,6 +107,7 @@ $mat-form-field-default-infix-width: 180px !default; // This is necessary because these browsers do not actually fire any events when a form auto-fill is // occurring. Once the autofill is committed, a change event happen and the regular mat-form-field // classes take over to fulfill this behaviour. +// @deletion-target 7.0.0 will rely on AutofillMonitor instead. .mat-form-field-autofill-control:-webkit-autofill + .mat-form-field-label-wrapper .mat-form-field-label { // The form field will be considered empty if it is autofilled, and therefore the label will diff --git a/src/lib/form-field/form-field.ts b/src/lib/form-field/form-field.ts index 97acbaf6e200..763dc920231b 100644 --- a/src/lib/form-field/form-field.ts +++ b/src/lib/form-field/form-field.ts @@ -100,6 +100,7 @@ export type MatFormFieldAppearance = 'legacy' | 'standard' | 'fill' | 'outline'; '[class.mat-form-field-should-float]': '_shouldLabelFloat()', '[class.mat-form-field-hide-placeholder]': '_hideControlPlaceholder()', '[class.mat-form-field-disabled]': '_control.disabled', + '[class.mat-form-field-autofilled]': '_control.autofilled', '[class.mat-focused]': '_control.focused', '[class.mat-accent]': 'color == "accent"', '[class.mat-warn]': 'color == "warn"', @@ -266,7 +267,7 @@ export class MatFormField extends _MatFormFieldMixinBase }); Promise.resolve().then(() => { - this._updateOutlineGap(); + this.updateOutlineGap(); this._changeDetectorRef.detectChanges(); }); } @@ -413,7 +414,7 @@ export class MatFormField extends _MatFormFieldMixinBase * Updates the width and position of the gap in the outline. Only relevant for the outline * appearance. */ - private _updateOutlineGap() { + updateOutlineGap() { if (this.appearance === 'outline' && this._label && this._label.nativeElement.children.length) { const containerStart = this._getStartEnd( this._connectionContainerRef.nativeElement.getBoundingClientRect()); diff --git a/src/lib/input/input.ts b/src/lib/input/input.ts index ace8240fdff9..3cba6f22db7d 100644 --- a/src/lib/input/input.ts +++ b/src/lib/input/input.ts @@ -14,15 +14,17 @@ import { ElementRef, Inject, Input, + NgZone, OnChanges, OnDestroy, Optional, Self, } from '@angular/core'; import {FormGroupDirective, NgControl, NgForm} from '@angular/forms'; -import {ErrorStateMatcher, mixinErrorState, CanUpdateErrorState} from '@angular/material/core'; +import {CanUpdateErrorState, ErrorStateMatcher, mixinErrorState} from '@angular/material/core'; import {MatFormFieldControl} from '@angular/material/form-field'; import {Subject} from 'rxjs/Subject'; +import {AutofillMonitor} from './autofill'; import {getMatInputUnsupportedTypeError} from './input-errors'; import {MAT_INPUT_VALUE_ACCESSOR} from './input-value-accessor'; @@ -58,6 +60,9 @@ export const _MatInputMixinBase = mixinErrorState(MatInputBase); selector: `input[matInput], textarea[matInput]`, exportAs: 'matInput', host: { + /** + * @deletion-target 7.0.0 remove .mat-form-field-autofill-control in favor of AutofillMonitor. + */ 'class': 'mat-input-element mat-form-field-autofill-control', '[class.mat-input-server]': '_isServer', // Native input properties that are overwritten by Angular inputs need to be synced with @@ -105,6 +110,12 @@ export class MatInput extends _MatInputMixinBase implements MatFormFieldControl< */ controlType: string = 'mat-input'; + /** + * Implemented as part of MatFormFieldControl. + * @docs-private + */ + autofilled = false; + /** * Implemented as part of MatFormFieldControl. * @docs-private @@ -206,7 +217,9 @@ export class MatInput extends _MatInputMixinBase implements MatFormFieldControl< @Optional() _parentForm: NgForm, @Optional() _parentFormGroup: FormGroupDirective, _defaultErrorStateMatcher: ErrorStateMatcher, - @Optional() @Self() @Inject(MAT_INPUT_VALUE_ACCESSOR) inputValueAccessor: any) { + @Optional() @Self() @Inject(MAT_INPUT_VALUE_ACCESSOR) inputValueAccessor: any, + private _autofillMonitor: AutofillMonitor, + ngZone: NgZone) { super(_defaultErrorStateMatcher, _parentForm, _parentFormGroup, ngControl); // If no input value accessor was explicitly specified, use the element as the input value // accessor. @@ -233,6 +246,12 @@ export class MatInput extends _MatInputMixinBase implements MatFormFieldControl< }); } + this._autofillMonitor.monitor(this._elementRef.nativeElement) + .subscribe(event => ngZone.run(() => { + this.autofilled = event.isAutofilled; + this.stateChanges.next(); + })); + this._isServer = !this._platform.isBrowser; } @@ -242,6 +261,7 @@ export class MatInput extends _MatInputMixinBase implements MatFormFieldControl< ngOnDestroy() { this.stateChanges.complete(); + this._autofillMonitor.stopMonitoring(this._elementRef.nativeElement); } ngDoCheck() { @@ -324,7 +344,8 @@ export class MatInput extends _MatInputMixinBase implements MatFormFieldControl< * @docs-private */ get empty(): boolean { - return !this._isNeverEmpty() && !this._elementRef.nativeElement.value && !this._isBadInput(); + return !this._isNeverEmpty() && !this._elementRef.nativeElement.value && !this._isBadInput() && + !this.autofilled; } /** From 5efc03892101765957f8407d2595d305a21cfb15 Mon Sep 17 00:00:00 2001 From: tinayuangao Date: Fri, 2 Feb 2018 13:57:42 -0800 Subject: [PATCH 08/13] feat(chips): add ripple to chips (#9761) --- src/lib/chips/chip.ts | 59 ++++++++++++++++++++++++++++++++++++---- src/lib/chips/chips.scss | 5 ++++ 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/lib/chips/chip.ts b/src/lib/chips/chip.ts index 8d131f3cfdcc..6efdf10c0c32 100644 --- a/src/lib/chips/chip.ts +++ b/src/lib/chips/chip.ts @@ -9,17 +9,33 @@ import {FocusableOption} from '@angular/cdk/a11y'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; import {BACKSPACE, DELETE, SPACE} from '@angular/cdk/keycodes'; +import {Platform} from '@angular/cdk/platform'; import { ContentChild, Directive, ElementRef, EventEmitter, forwardRef, + Inject, Input, + NgZone, OnDestroy, + Optional, Output, } from '@angular/core'; -import {CanColor, CanDisable, mixinColor, mixinDisabled} from '@angular/material/core'; +import { + CanColor, + CanDisable, + CanDisableRipple, + MAT_RIPPLE_GLOBAL_OPTIONS, + mixinColor, + mixinDisabled, + mixinDisableRipple, + RippleConfig, + RippleGlobalOptions, + RippleRenderer, + RippleTarget +} from '@angular/material/core'; import {Subject} from 'rxjs/Subject'; @@ -47,7 +63,7 @@ export class MatChipBase { constructor(public _elementRef: ElementRef) {} } -export const _MatChipMixinBase = mixinColor(mixinDisabled(MatChipBase), 'primary'); +export const _MatChipMixinBase = mixinColor(mixinDisableRipple(mixinDisabled(MatChipBase)), 'primary'); const CHIP_ATTRIBUTE_NAMES = ['mat-basic-chip']; @@ -76,7 +92,7 @@ export class MatChipTrailingIcon {} */ @Directive({ selector: `mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]`, - inputs: ['color', 'disabled'], + inputs: ['color', 'disabled', 'disableRipple'], exportAs: 'matChip', host: { 'class': 'mat-chip', @@ -85,6 +101,7 @@ export class MatChipTrailingIcon {} '[class.mat-chip-selected]': 'selected', '[class.mat-chip-with-avatar]': 'avatar', '[class.mat-chip-with-trailing-icon]': 'trailingIcon || removeIcon', + '[class.mat-chip-disabled]': 'disabled', '[attr.disabled]': 'disabled || null', '[attr.aria-disabled]': 'disabled.toString()', '[attr.aria-selected]': 'ariaSelected', @@ -93,10 +110,26 @@ export class MatChipTrailingIcon {} '(focus)': '_hasFocus = true', '(blur)': '_blur()', }, - }) export class MatChip extends _MatChipMixinBase implements FocusableOption, OnDestroy, CanColor, - CanDisable { + CanDisable, CanDisableRipple, RippleTarget { + /** + * Ripple configuration for ripples that are launched on pointer down. + * @docs-private + */ + rippleConfig: RippleConfig = {}; + + /** Reference to the RippleRenderer for the chip. */ + private _chipRipple: RippleRenderer; + + /** + * Whether ripples are disabled on interaction + * @docs-private + */ + get rippleDisabled(): boolean { + return this.disabled || this.disableRipple; + } + /** Whether the chip has focus. */ _hasFocus: boolean = false; @@ -188,10 +221,23 @@ export class MatChip extends _MatChipMixinBase implements FocusableOption, OnDes return this.selectable ? this.selected.toString() : null; } - constructor(public _elementRef: ElementRef) { + constructor(public _elementRef: ElementRef, + ngZone: NgZone, + platform: Platform, + @Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) globalOptions: RippleGlobalOptions) { super(_elementRef); this._addHostClassName(); + + this._chipRipple = new RippleRenderer(this, ngZone, _elementRef, platform); + this._chipRipple.setupTriggerEvents(_elementRef.nativeElement); + + if (globalOptions) { + this.rippleConfig = { + speedFactor: globalOptions.baseSpeedFactor, + animation: globalOptions.animation, + }; + } } _addHostClassName() { @@ -208,6 +254,7 @@ export class MatChip extends _MatChipMixinBase implements FocusableOption, OnDes ngOnDestroy() { this.destroyed.emit({chip: this}); + this._chipRipple._removeTriggerEvents(); } /** Selects the chip. */ diff --git a/src/lib/chips/chips.scss b/src/lib/chips/chips.scss index de53e33fd911..812a60c10ebe 100644 --- a/src/lib/chips/chips.scss +++ b/src/lib/chips/chips.scss @@ -20,6 +20,11 @@ $mat-chip-input-margin: 3px; $mat-chip-avatar-size: 32px; $mat-chip-remove-size: 18px; +.mat-chip { + position: relative; + overflow: hidden; +} + .mat-standard-chip { @include mat-elevation-transition; display: inline-flex; From c2c4a7bdd9cd70910e2bbfb807bdc29a285e1b28 Mon Sep 17 00:00:00 2001 From: mmalerba Date: Fri, 2 Feb 2018 15:39:58 -0800 Subject: [PATCH 09/13] fix(form-field, chips): fix tests & lint (#9767) --- src/lib/autocomplete/autocomplete.spec.ts | 83 ++++++++++++----------- src/lib/chips/chip.ts | 3 +- src/lib/chips/chips-module.ts | 7 +- src/lib/datepicker/datepicker.spec.ts | 7 -- src/lib/form-field/form-field.ts | 2 +- src/lib/input/autofill.ts | 17 +++-- src/lib/input/input.ts | 20 +++--- 7 files changed, 68 insertions(+), 71 deletions(-) diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts index f54065af3f03..068cf50861c4 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -102,11 +102,11 @@ describe('MatAutocomplete', () => { let fixture: ComponentFixture; let input: HTMLInputElement; - beforeEach(() => { + beforeEach(fakeAsync(() => { fixture = createComponent(SimpleAutocomplete); fixture.detectChanges(); input = fixture.debugElement.query(By.css('input')).nativeElement; - }); + })); it('should open the panel when the input is focused', () => { expect(fixture.componentInstance.trigger.panelOpen) @@ -412,7 +412,7 @@ describe('MatAutocomplete', () => { })); }); - it('should have the correct text direction in RTL', () => { + it('should have the correct text direction in RTL', fakeAsync(() => { const rtlFixture = createComponent(SimpleAutocomplete, [ {provide: Directionality, useFactory: () => ({value: 'rtl'})}, ]); @@ -424,18 +424,18 @@ describe('MatAutocomplete', () => { const overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane')!; expect(overlayPane.getAttribute('dir')).toEqual('rtl'); - }); + })); describe('forms integration', () => { let fixture: ComponentFixture; let input: HTMLInputElement; - beforeEach(() => { + beforeEach(fakeAsync(() => { fixture = createComponent(SimpleAutocomplete); fixture.detectChanges(); input = fixture.debugElement.query(By.css('input')).nativeElement; - }); + })); it('should update control value as user types with input value', () => { fixture.componentInstance.trigger.openPanel(); @@ -616,22 +616,23 @@ describe('MatAutocomplete', () => { .toBe(true, `Expected control to become touched on blur.`); }); - it('should disable the input when used with a value accessor and without `matInput`', () => { - overlayContainer.ngOnDestroy(); - fixture.destroy(); - TestBed.resetTestingModule(); + it('should disable the input when used with a value accessor and without `matInput`', + fakeAsync(() => { + overlayContainer.ngOnDestroy(); + fixture.destroy(); + TestBed.resetTestingModule(); - const plainFixture = createComponent(PlainAutocompleteInputWithFormControl); - plainFixture.detectChanges(); - input = plainFixture.nativeElement.querySelector('input'); + const plainFixture = createComponent(PlainAutocompleteInputWithFormControl); + plainFixture.detectChanges(); + input = plainFixture.nativeElement.querySelector('input'); - expect(input.disabled).toBe(false); + expect(input.disabled).toBe(false); - plainFixture.componentInstance.formControl.disable(); - plainFixture.detectChanges(); + plainFixture.componentInstance.formControl.disable(); + plainFixture.detectChanges(); - expect(input.disabled).toBe(true); - }); + expect(input.disabled).toBe(true); + })); }); @@ -1115,12 +1116,12 @@ describe('MatAutocomplete', () => { let fixture: ComponentFixture; let input: HTMLInputElement; - beforeEach(() => { + beforeEach(fakeAsync(() => { fixture = createComponent(SimpleAutocomplete); fixture.detectChanges(); input = fixture.debugElement.query(By.css('input')).nativeElement; - }); + })); it('should set role of input to combobox', () => { expect(input.getAttribute('role')) @@ -1232,13 +1233,13 @@ describe('MatAutocomplete', () => { let input: HTMLInputElement; let inputReference: HTMLInputElement; - beforeEach(() => { + beforeEach(fakeAsync(() => { fixture = createComponent(SimpleAutocomplete); fixture.detectChanges(); input = fixture.debugElement.query(By.css('input')).nativeElement; inputReference = fixture.debugElement.query(By.css('.mat-input-flex')).nativeElement; - }); + })); it('should use below positioning by default', fakeAsync(() => { fixture.componentInstance.trigger.openPanel(); @@ -1319,10 +1320,10 @@ describe('MatAutocomplete', () => { describe('Option selection', () => { let fixture: ComponentFixture; - beforeEach(() => { + beforeEach(fakeAsync(() => { fixture = createComponent(SimpleAutocomplete); fixture.detectChanges(); - }); + })); it('should deselect any other selected option', fakeAsync(() => { fixture.componentInstance.trigger.openPanel(); @@ -1512,10 +1513,10 @@ describe('MatAutocomplete', () => { describe('without matInput', () => { let fixture: ComponentFixture; - beforeEach(() => { + beforeEach(fakeAsync(() => { fixture = createComponent(AutocompleteWithNativeInput); fixture.detectChanges(); - }); + })); it('should not throw when clicking outside', fakeAsync(() => { dispatchFakeEvent(fixture.debugElement.query(By.css('input')).nativeElement, 'focus'); @@ -1528,7 +1529,7 @@ describe('MatAutocomplete', () => { describe('misc', () => { - it('should allow basic use without any forms directives', () => { + it('should allow basic use without any forms directives', fakeAsync(() => { expect(() => { const fixture = createComponent(AutocompleteWithoutForms); fixture.detectChanges(); @@ -1536,19 +1537,20 @@ describe('MatAutocomplete', () => { const input = fixture.debugElement.query(By.css('input')).nativeElement; typeInElement('d', input); fixture.detectChanges(); + tick(); const options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; expect(options.length).toBe(1); }).not.toThrowError(); - }); + })); - it('should display an empty input when the value is undefined with ngModel', () => { + it('should display an empty input when the value is undefined with ngModel', fakeAsync(() => { const fixture = createComponent(AutocompleteWithNgModel); fixture.detectChanges(); expect(fixture.debugElement.query(By.css('input')).nativeElement.value).toBe(''); - }); + })); it('should display the number when the selected option is the number zero', fakeAsync(() => { const fixture = createComponent(AutocompleteWithNumbers); @@ -1560,7 +1562,7 @@ describe('MatAutocomplete', () => { expect(fixture.debugElement.query(By.css('input')).nativeElement.value).toBe('0'); })); - it('should work when input is wrapped in ngIf', () => { + it('should work when input is wrapped in ngIf', fakeAsync(() => { const fixture = createComponent(NgIfAutocomplete); fixture.detectChanges(); @@ -1573,9 +1575,9 @@ describe('MatAutocomplete', () => { .toContain('One', `Expected panel to display when input is focused.`); expect(overlayContainerElement.textContent) .toContain('Two', `Expected panel to display when input is focused.`); - }); + })); - it('should filter properly with ngIf after setting the active item', () => { + it('should filter properly with ngIf after setting the active item', fakeAsync(() => { const fixture = createComponent(NgIfAutocomplete); fixture.detectChanges(); @@ -1589,18 +1591,19 @@ describe('MatAutocomplete', () => { const input = fixture.debugElement.query(By.css('input')).nativeElement; typeInElement('o', input); fixture.detectChanges(); + tick(); expect(fixture.componentInstance.matOptions.length).toBe(2); - }); + })); - it('should throw if the user attempts to open the panel too early', () => { + it('should throw if the user attempts to open the panel too early', fakeAsync(() => { const fixture = createComponent(AutocompleteWithoutPanel); fixture.detectChanges(); expect(() => { fixture.componentInstance.trigger.openPanel(); }).toThrow(getMatAutocompleteMissingPanelError()); - }); + })); it('should not throw on init, even if the panel is not defined', fakeAsync(() => { expect(() => { @@ -1679,7 +1682,7 @@ describe('MatAutocomplete', () => { }); - it('should have correct width when opened', () => { + it('should have correct width when opened', fakeAsync(() => { const widthFixture = createComponent(SimpleAutocomplete); widthFixture.componentInstance.width = 300; widthFixture.detectChanges(); @@ -1702,9 +1705,9 @@ describe('MatAutocomplete', () => { // Firefox, edge return a decimal value for width, so we need to parse and round it to verify expect(Math.ceil(parseFloat(overlayPane.style.width as string))).toBe(500); - }); + })); - it('should update the width while the panel is open', () => { + it('should update the width while the panel is open', fakeAsync(() => { const widthFixture = createComponent(SimpleAutocomplete); widthFixture.componentInstance.width = 300; @@ -1726,7 +1729,7 @@ describe('MatAutocomplete', () => { widthFixture.detectChanges(); expect(Math.ceil(parseFloat(overlayPane.style.width as string))).toBe(500); - }); + })); it('should show the panel when the options are initialized later within a component with ' + 'OnPush change detection', fakeAsync(() => { diff --git a/src/lib/chips/chip.ts b/src/lib/chips/chip.ts index 6efdf10c0c32..fade4adf59ca 100644 --- a/src/lib/chips/chip.ts +++ b/src/lib/chips/chip.ts @@ -63,7 +63,8 @@ export class MatChipBase { constructor(public _elementRef: ElementRef) {} } -export const _MatChipMixinBase = mixinColor(mixinDisableRipple(mixinDisabled(MatChipBase)), 'primary'); +export const _MatChipMixinBase = + mixinColor(mixinDisableRipple(mixinDisabled(MatChipBase)), 'primary'); const CHIP_ATTRIBUTE_NAMES = ['mat-basic-chip']; diff --git a/src/lib/chips/chips-module.ts b/src/lib/chips/chips-module.ts index 8c4cb7131cc9..80e21b45a045 100644 --- a/src/lib/chips/chips-module.ts +++ b/src/lib/chips/chips-module.ts @@ -6,11 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ +import {PlatformModule} from '@angular/cdk/platform'; import {NgModule} from '@angular/core'; import {ErrorStateMatcher} from '@angular/material/core'; -import {MatChipList} from './chip-list'; -import {MatChip, MatChipRemove, MatChipAvatar, MatChipTrailingIcon} from './chip'; +import {MatChip, MatChipAvatar, MatChipRemove, MatChipTrailingIcon} from './chip'; import {MatChipInput} from './chip-input'; +import {MatChipList} from './chip-list'; const CHIP_DECLARATIONS = [ MatChipList, @@ -22,7 +23,7 @@ const CHIP_DECLARATIONS = [ ]; @NgModule({ - imports: [], + imports: [PlatformModule], exports: CHIP_DECLARATIONS, declarations: CHIP_DECLARATIONS, providers: [ErrorStateMatcher] diff --git a/src/lib/datepicker/datepicker.spec.ts b/src/lib/datepicker/datepicker.spec.ts index e2d8549f08a7..c4d916422f7f 100644 --- a/src/lib/datepicker/datepicker.spec.ts +++ b/src/lib/datepicker/datepicker.spec.ts @@ -790,12 +790,6 @@ describe('MatDatepicker', () => { fixture.detectChanges(); })); - it('should attach popup to mat-form-field underline', () => { - let attachToRef = testComponent.datepickerInput.getConnectedOverlayOrigin(); - expect(attachToRef.nativeElement.classList.contains('mat-form-field-underline')) - .toBe(true, 'popup should be attached to mat-form-field underline'); - }); - it('should float the placeholder when an invalid value is entered', () => { testComponent.datepickerInput.value = 'totally-not-a-date' as any; fixture.debugElement.nativeElement.querySelector('input').value = 'totally-not-a-date'; @@ -804,7 +798,6 @@ describe('MatDatepicker', () => { expect(fixture.debugElement.nativeElement.querySelector('mat-form-field').classList) .toContain('mat-form-field-should-float'); }); - }); describe('datepicker with min and max dates and validation', () => { diff --git a/src/lib/form-field/form-field.ts b/src/lib/form-field/form-field.ts index 763dc920231b..fd57582981af 100644 --- a/src/lib/form-field/form-field.ts +++ b/src/lib/form-field/form-field.ts @@ -289,7 +289,7 @@ export class MatFormField extends _MatFormFieldMixinBase } _hasPlaceholder() { - return !!(this._control.placeholder || this._placeholderChild); + return !!(this._control && this._control.placeholder || this._placeholderChild); } _hasLabel() { diff --git a/src/lib/input/autofill.ts b/src/lib/input/autofill.ts index 84dd4cc5f098..098d3b368f4d 100644 --- a/src/lib/input/autofill.ts +++ b/src/lib/input/autofill.ts @@ -12,8 +12,8 @@ import { ElementRef, EventEmitter, Injectable, - NgZone, OnDestroy, + OnInit, Output } from '@angular/core'; import {Observable} from 'rxjs/Observable'; @@ -50,7 +50,7 @@ const listenerOptions: any = supportsPassiveEventListeners() ? {passive: true} : export class AutofillMonitor implements OnDestroy { private _monitoredElements = new Map(); - constructor(private _platform: Platform, private _ngZone: NgZone) {} + constructor(private _platform: Platform) {} /** * Monitor for changes in the autofill state of the given input element. @@ -78,9 +78,7 @@ export class AutofillMonitor implements OnDestroy { } }; - this._ngZone.runOutsideAngular(() => { - element.addEventListener('animationstart', listener, listenerOptions); - }); + element.addEventListener('animationstart', listener, listenerOptions); element.classList.add('mat-input-autofill-monitored'); this._monitoredElements.set(element, { @@ -120,13 +118,14 @@ export class AutofillMonitor implements OnDestroy { @Directive({ selector: '[matAutofill]', }) -export class MatAutofill implements OnDestroy { +export class MatAutofill implements OnDestroy, OnInit { @Output() matAutofill = new EventEmitter(); - constructor(private _elementRef: ElementRef, private _autofillMonitor: AutofillMonitor, - ngZone: NgZone) { + constructor(private _elementRef: ElementRef, private _autofillMonitor: AutofillMonitor) {} + + ngOnInit() { this._autofillMonitor.monitor(this._elementRef.nativeElement) - .subscribe(event => ngZone.run(() => this.matAutofill.emit(event))); + .subscribe(event => this.matAutofill.emit(event)); } ngOnDestroy() { diff --git a/src/lib/input/input.ts b/src/lib/input/input.ts index 3cba6f22db7d..fc8ae24cd268 100644 --- a/src/lib/input/input.ts +++ b/src/lib/input/input.ts @@ -14,9 +14,9 @@ import { ElementRef, Inject, Input, - NgZone, OnChanges, OnDestroy, + OnInit, Optional, Self, } from '@angular/core'; @@ -82,7 +82,7 @@ export const _MatInputMixinBase = mixinErrorState(MatInputBase); providers: [{provide: MatFormFieldControl, useExisting: MatInput}], }) export class MatInput extends _MatInputMixinBase implements MatFormFieldControl, OnChanges, - OnDestroy, DoCheck, CanUpdateErrorState { + OnDestroy, OnInit, DoCheck, CanUpdateErrorState { protected _uid = `mat-input-${nextUniqueId++}`; protected _previousNativeValue: any; private _inputValueAccessor: {value: any}; @@ -218,8 +218,7 @@ export class MatInput extends _MatInputMixinBase implements MatFormFieldControl< @Optional() _parentFormGroup: FormGroupDirective, _defaultErrorStateMatcher: ErrorStateMatcher, @Optional() @Self() @Inject(MAT_INPUT_VALUE_ACCESSOR) inputValueAccessor: any, - private _autofillMonitor: AutofillMonitor, - ngZone: NgZone) { + private _autofillMonitor: AutofillMonitor) { super(_defaultErrorStateMatcher, _parentForm, _parentFormGroup, ngControl); // If no input value accessor was explicitly specified, use the element as the input value // accessor. @@ -246,15 +245,16 @@ export class MatInput extends _MatInputMixinBase implements MatFormFieldControl< }); } - this._autofillMonitor.monitor(this._elementRef.nativeElement) - .subscribe(event => ngZone.run(() => { - this.autofilled = event.isAutofilled; - this.stateChanges.next(); - })); - this._isServer = !this._platform.isBrowser; } + ngOnInit() { + this._autofillMonitor.monitor(this._elementRef.nativeElement).subscribe(event => { + this.autofilled = event.isAutofilled; + this.stateChanges.next(); + }); + } + ngOnChanges() { this.stateChanges.next(); } From 0aaee3612fe4c4757c227ef0149f736667be87e8 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Sat, 3 Feb 2018 16:07:12 -0800 Subject: [PATCH 10/13] undo change that caused darkened color for legacy form field --- src/lib/form-field/_form-field-legacy-theme.scss | 9 +++++++++ src/lib/form-field/_form-field-theme.scss | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/lib/form-field/_form-field-legacy-theme.scss b/src/lib/form-field/_form-field-legacy-theme.scss index 5bf738e6b147..684709bd67d1 100644 --- a/src/lib/form-field/_form-field-legacy-theme.scss +++ b/src/lib/form-field/_form-field-legacy-theme.scss @@ -10,9 +10,18 @@ $foreground: map-get($theme, foreground); $is-dark-theme: map-get($theme, is-dark); + $label-color: mat-color($foreground, secondary-text); $underline-color: mat-color($foreground, divider, if($is-dark-theme, 0.7, 0.42)); .mat-form-field-appearance-legacy { + .mat-form-field-label { + color: $label-color; + } + + .mat-hint { + color: $label-color; + } + .mat-form-field-underline { background-color: $underline-color; } diff --git a/src/lib/form-field/_form-field-theme.scss b/src/lib/form-field/_form-field-theme.scss index 94c6f6a442fd..16128cd3aaa1 100644 --- a/src/lib/form-field/_form-field-theme.scss +++ b/src/lib/form-field/_form-field-theme.scss @@ -37,7 +37,7 @@ color: $label-color; } - .mat-focused .mat-form-field-label { + .mat-form-field.mat-focused .mat-form-field-label { color: $focused-label-color; &.mat-accent { From 7432cccd9f4baf862a05accfe03ea550d50a78a0 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Sat, 3 Feb 2018 21:38:40 -0800 Subject: [PATCH 11/13] fix change detection --- src/lib/autocomplete/autocomplete.spec.ts | 83 +++++++++++------------ src/lib/form-field/form-field.ts | 2 +- 2 files changed, 41 insertions(+), 44 deletions(-) diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts index 068cf50861c4..f54065af3f03 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -102,11 +102,11 @@ describe('MatAutocomplete', () => { let fixture: ComponentFixture; let input: HTMLInputElement; - beforeEach(fakeAsync(() => { + beforeEach(() => { fixture = createComponent(SimpleAutocomplete); fixture.detectChanges(); input = fixture.debugElement.query(By.css('input')).nativeElement; - })); + }); it('should open the panel when the input is focused', () => { expect(fixture.componentInstance.trigger.panelOpen) @@ -412,7 +412,7 @@ describe('MatAutocomplete', () => { })); }); - it('should have the correct text direction in RTL', fakeAsync(() => { + it('should have the correct text direction in RTL', () => { const rtlFixture = createComponent(SimpleAutocomplete, [ {provide: Directionality, useFactory: () => ({value: 'rtl'})}, ]); @@ -424,18 +424,18 @@ describe('MatAutocomplete', () => { const overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane')!; expect(overlayPane.getAttribute('dir')).toEqual('rtl'); - })); + }); describe('forms integration', () => { let fixture: ComponentFixture; let input: HTMLInputElement; - beforeEach(fakeAsync(() => { + beforeEach(() => { fixture = createComponent(SimpleAutocomplete); fixture.detectChanges(); input = fixture.debugElement.query(By.css('input')).nativeElement; - })); + }); it('should update control value as user types with input value', () => { fixture.componentInstance.trigger.openPanel(); @@ -616,23 +616,22 @@ describe('MatAutocomplete', () => { .toBe(true, `Expected control to become touched on blur.`); }); - it('should disable the input when used with a value accessor and without `matInput`', - fakeAsync(() => { - overlayContainer.ngOnDestroy(); - fixture.destroy(); - TestBed.resetTestingModule(); + it('should disable the input when used with a value accessor and without `matInput`', () => { + overlayContainer.ngOnDestroy(); + fixture.destroy(); + TestBed.resetTestingModule(); - const plainFixture = createComponent(PlainAutocompleteInputWithFormControl); - plainFixture.detectChanges(); - input = plainFixture.nativeElement.querySelector('input'); + const plainFixture = createComponent(PlainAutocompleteInputWithFormControl); + plainFixture.detectChanges(); + input = plainFixture.nativeElement.querySelector('input'); - expect(input.disabled).toBe(false); + expect(input.disabled).toBe(false); - plainFixture.componentInstance.formControl.disable(); - plainFixture.detectChanges(); + plainFixture.componentInstance.formControl.disable(); + plainFixture.detectChanges(); - expect(input.disabled).toBe(true); - })); + expect(input.disabled).toBe(true); + }); }); @@ -1116,12 +1115,12 @@ describe('MatAutocomplete', () => { let fixture: ComponentFixture; let input: HTMLInputElement; - beforeEach(fakeAsync(() => { + beforeEach(() => { fixture = createComponent(SimpleAutocomplete); fixture.detectChanges(); input = fixture.debugElement.query(By.css('input')).nativeElement; - })); + }); it('should set role of input to combobox', () => { expect(input.getAttribute('role')) @@ -1233,13 +1232,13 @@ describe('MatAutocomplete', () => { let input: HTMLInputElement; let inputReference: HTMLInputElement; - beforeEach(fakeAsync(() => { + beforeEach(() => { fixture = createComponent(SimpleAutocomplete); fixture.detectChanges(); input = fixture.debugElement.query(By.css('input')).nativeElement; inputReference = fixture.debugElement.query(By.css('.mat-input-flex')).nativeElement; - })); + }); it('should use below positioning by default', fakeAsync(() => { fixture.componentInstance.trigger.openPanel(); @@ -1320,10 +1319,10 @@ describe('MatAutocomplete', () => { describe('Option selection', () => { let fixture: ComponentFixture; - beforeEach(fakeAsync(() => { + beforeEach(() => { fixture = createComponent(SimpleAutocomplete); fixture.detectChanges(); - })); + }); it('should deselect any other selected option', fakeAsync(() => { fixture.componentInstance.trigger.openPanel(); @@ -1513,10 +1512,10 @@ describe('MatAutocomplete', () => { describe('without matInput', () => { let fixture: ComponentFixture; - beforeEach(fakeAsync(() => { + beforeEach(() => { fixture = createComponent(AutocompleteWithNativeInput); fixture.detectChanges(); - })); + }); it('should not throw when clicking outside', fakeAsync(() => { dispatchFakeEvent(fixture.debugElement.query(By.css('input')).nativeElement, 'focus'); @@ -1529,7 +1528,7 @@ describe('MatAutocomplete', () => { describe('misc', () => { - it('should allow basic use without any forms directives', fakeAsync(() => { + it('should allow basic use without any forms directives', () => { expect(() => { const fixture = createComponent(AutocompleteWithoutForms); fixture.detectChanges(); @@ -1537,20 +1536,19 @@ describe('MatAutocomplete', () => { const input = fixture.debugElement.query(By.css('input')).nativeElement; typeInElement('d', input); fixture.detectChanges(); - tick(); const options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; expect(options.length).toBe(1); }).not.toThrowError(); - })); + }); - it('should display an empty input when the value is undefined with ngModel', fakeAsync(() => { + it('should display an empty input when the value is undefined with ngModel', () => { const fixture = createComponent(AutocompleteWithNgModel); fixture.detectChanges(); expect(fixture.debugElement.query(By.css('input')).nativeElement.value).toBe(''); - })); + }); it('should display the number when the selected option is the number zero', fakeAsync(() => { const fixture = createComponent(AutocompleteWithNumbers); @@ -1562,7 +1560,7 @@ describe('MatAutocomplete', () => { expect(fixture.debugElement.query(By.css('input')).nativeElement.value).toBe('0'); })); - it('should work when input is wrapped in ngIf', fakeAsync(() => { + it('should work when input is wrapped in ngIf', () => { const fixture = createComponent(NgIfAutocomplete); fixture.detectChanges(); @@ -1575,9 +1573,9 @@ describe('MatAutocomplete', () => { .toContain('One', `Expected panel to display when input is focused.`); expect(overlayContainerElement.textContent) .toContain('Two', `Expected panel to display when input is focused.`); - })); + }); - it('should filter properly with ngIf after setting the active item', fakeAsync(() => { + it('should filter properly with ngIf after setting the active item', () => { const fixture = createComponent(NgIfAutocomplete); fixture.detectChanges(); @@ -1591,19 +1589,18 @@ describe('MatAutocomplete', () => { const input = fixture.debugElement.query(By.css('input')).nativeElement; typeInElement('o', input); fixture.detectChanges(); - tick(); expect(fixture.componentInstance.matOptions.length).toBe(2); - })); + }); - it('should throw if the user attempts to open the panel too early', fakeAsync(() => { + it('should throw if the user attempts to open the panel too early', () => { const fixture = createComponent(AutocompleteWithoutPanel); fixture.detectChanges(); expect(() => { fixture.componentInstance.trigger.openPanel(); }).toThrow(getMatAutocompleteMissingPanelError()); - })); + }); it('should not throw on init, even if the panel is not defined', fakeAsync(() => { expect(() => { @@ -1682,7 +1679,7 @@ describe('MatAutocomplete', () => { }); - it('should have correct width when opened', fakeAsync(() => { + it('should have correct width when opened', () => { const widthFixture = createComponent(SimpleAutocomplete); widthFixture.componentInstance.width = 300; widthFixture.detectChanges(); @@ -1705,9 +1702,9 @@ describe('MatAutocomplete', () => { // Firefox, edge return a decimal value for width, so we need to parse and round it to verify expect(Math.ceil(parseFloat(overlayPane.style.width as string))).toBe(500); - })); + }); - it('should update the width while the panel is open', fakeAsync(() => { + it('should update the width while the panel is open', () => { const widthFixture = createComponent(SimpleAutocomplete); widthFixture.componentInstance.width = 300; @@ -1729,7 +1726,7 @@ describe('MatAutocomplete', () => { widthFixture.detectChanges(); expect(Math.ceil(parseFloat(overlayPane.style.width as string))).toBe(500); - })); + }); it('should show the panel when the options are initialized later within a component with ' + 'OnPush change detection', fakeAsync(() => { diff --git a/src/lib/form-field/form-field.ts b/src/lib/form-field/form-field.ts index fd57582981af..4ae21c79f204 100644 --- a/src/lib/form-field/form-field.ts +++ b/src/lib/form-field/form-field.ts @@ -268,7 +268,7 @@ export class MatFormField extends _MatFormFieldMixinBase Promise.resolve().then(() => { this.updateOutlineGap(); - this._changeDetectorRef.detectChanges(); + this._changeDetectorRef.markForCheck(); }); } From 9f8649fd590f10c78b28eb7cff865de5f5bfce6a Mon Sep 17 00:00:00 2001 From: tinayuangao Date: Mon, 5 Feb 2018 15:55:39 -0800 Subject: [PATCH 12/13] fix(chips): remove margin for chip list (#9793) --- src/lib/chips/chips.scss | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/lib/chips/chips.scss b/src/lib/chips/chips.scss index 812a60c10ebe..2c5b211d4ead 100644 --- a/src/lib/chips/chips.scss +++ b/src/lib/chips/chips.scss @@ -33,10 +33,6 @@ $mat-chip-remove-size: 18px; align-items: center; cursor: default; - .mat-chip-list-wrapper & { - margin: $mat-chips-chip-margin; - } - .mat-chip-remove.mat-icon { width: $mat-chip-remove-size; height: $mat-chip-remove-size; @@ -125,6 +121,12 @@ $mat-chip-remove-size: 18px; flex-direction: row; flex-wrap: wrap; align-items: center; + margin: -$mat-chips-chip-margin; + + input.mat-input-element, + .mat-standard-chip { + margin: $mat-chips-chip-margin; + } } .mat-chip-list-stacked .mat-chip-list-wrapper { From 3e15c8fa2dffd35c33039f9f577268cbdc9c989d Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 6 Feb 2018 08:41:06 -0800 Subject: [PATCH 13/13] add terminateOnPointerUp to ripple config --- src/lib/chips/chip.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/chips/chip.ts b/src/lib/chips/chip.ts index fade4adf59ca..6e6d58cf9012 100644 --- a/src/lib/chips/chip.ts +++ b/src/lib/chips/chip.ts @@ -237,6 +237,7 @@ export class MatChip extends _MatChipMixinBase implements FocusableOption, OnDes this.rippleConfig = { speedFactor: globalOptions.baseSpeedFactor, animation: globalOptions.animation, + terminateOnPointerUp: globalOptions.terminateOnPointerUp, }; } }