From 10b83534bb5c0c52c8c1f8dc361423635501562e Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 25 Sep 2018 16:10:44 +0200 Subject: [PATCH] fix(form-field): incorrect arrow color for native select (#13046) Currently the arrow for the form field's native select is done through a URL-encoded SVG set as a background image. This is inflexible, because it doesn't allow us to easily change its color, which means that we're locked into one color that doesn't account for dark themes, disabled state or whether the form field has validation errors. These changes rework the approach to use a pseudo element to render the arrow instead and to use the proper colors for the other states. --- src/demo-app/select/select-demo.html | 2 + src/lib/form-field/form-field.scss | 5 ++ src/lib/input/_input-theme.scss | 11 +++- src/lib/input/input.scss | 48 ++++++++------ src/lib/input/input.spec.ts | 95 ++++++++++++++++------------ src/lib/input/input.ts | 13 +++- 6 files changed, 113 insertions(+), 61 deletions(-) diff --git a/src/demo-app/select/select-demo.html b/src/demo-app/select/select-demo.html index b92727f38e8..9b7061c9d63 100644 --- a/src/demo-app/select/select-demo.html +++ b/src/demo-app/select/select-demo.html @@ -84,6 +84,7 @@ Fill + diff --git a/src/lib/form-field/form-field.scss b/src/lib/form-field/form-field.scss index ea19795d065..be43e14afff 100644 --- a/src/lib/form-field/form-field.scss +++ b/src/lib/form-field/form-field.scss @@ -217,6 +217,11 @@ $mat-form-field-default-infix-width: 180px !default; display: block; } +// Element that can used to reliably align content in relation to the form field control. +.mat-form-field-control-wrapper { + position: relative; +} + .mat-form-field._mat-animation-noopable { .mat-form-field-label, .mat-form-field-ripple { diff --git a/src/lib/input/_input-theme.scss b/src/lib/input/_input-theme.scss index 055be25d1ab..ecdd6b0e176 100644 --- a/src/lib/input/_input-theme.scss +++ b/src/lib/input/_input-theme.scss @@ -11,7 +11,12 @@ $warn: map-get($theme, warn); $foreground: map-get($theme, foreground); - .mat-input-element:disabled { + .mat-form-field-type-mat-native-select .mat-form-field-infix::after { + color: mat-color($foreground, secondary-text); + } + + .mat-input-element:disabled, + .mat-form-field-type-mat-native-select.mat-form-field-disabled .mat-form-field-infix::after { color: mat-color($foreground, disabled-text); } @@ -31,6 +36,10 @@ .mat-form-field-invalid .mat-input-element { caret-color: mat-color($warn); } + + .mat-form-field-type-mat-native-select.mat-form-field-invalid .mat-form-field-infix::after { + color: mat-color($warn); + } } @mixin mat-input-typography($config) { diff --git a/src/lib/input/input.scss b/src/lib/input/input.scss index c0cbae872f5..ead0f296ee6 100644 --- a/src/lib/input/input.scss +++ b/src/lib/input/input.scss @@ -122,29 +122,14 @@ textarea.mat-input-element { margin: -2px 0; } -// URL-encoded Material Design select arrow SVG. -$mat-native-select-arrow-svg: '' + - 'data:image/svg+xml;charset=utf8,%3Csvg%20width%3D%2210%22%20height%3D%225%22%20' + - 'viewBox%3D%227%2010%2010%205%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fs' + - 'vg%22%3E%3Cpath%20fill%3D%22%230%22%20fill-rule%3D%22evenodd%22%20opacity%3D%22' + - '.54%22%20d%3D%22M7%2010l5%205%205-5z%22%2F%3E%3C%2Fsvg%3E'; - // Remove the native select down arrow and replace it with material design arrow select.mat-input-element { -moz-appearance: none; -webkit-appearance: none; position: relative; background-color: transparent; - background-repeat: no-repeat; display: inline-flex; box-sizing: border-box; - background-position: right center; - - // Native multi-selects are rendered inline which - // means that they shouldn't have a dropdown arrow. - &:not([multiple]) { - background-image: url($mat-native-select-arrow-svg); - } &::-ms-expand { display: none; @@ -156,9 +141,34 @@ select.mat-input-element { &::-moz-focus-inner { border: 0; } - - [dir='rtl'] & { - background-position: left center; - } } +.mat-form-field-type-mat-native-select { + $arrow-size: 5px; + + .mat-form-field-infix::after { + content: ''; + width: 0; + height: 0; + border-left: $arrow-size solid transparent; + border-right: $arrow-size solid transparent; + border-top: $arrow-size solid; + position: absolute; + top: 50%; + right: 0; + margin-top: -$arrow-size / 2; + + [dir='rtl'] & { + right: auto; + left: 0; + } + } + + &.mat-form-field-appearance-outline .mat-form-field-infix::after { + margin-top: -$arrow-size; + } + + &.mat-form-field-appearance-fill .mat-form-field-infix::after { + margin-top: -$arrow-size * 2; + } +} diff --git a/src/lib/input/input.spec.ts b/src/lib/input/input.spec.ts index 1031e39c527..72b8c5e3aba 100644 --- a/src/lib/input/input.spec.ts +++ b/src/lib/input/input.spec.ts @@ -218,51 +218,51 @@ describe('MatInput without forms', () => { const labelElement: HTMLInputElement = fixture.debugElement.query(By.css('label')).nativeElement; - expect(inputElement.id).toBe('test-id'); - expect(labelElement.getAttribute('for')).toBe('test-id'); - })); + expect(inputElement.id).toBe('test-id'); + expect(labelElement.getAttribute('for')).toBe('test-id'); + })); - it('validates there\'s only one hint label per side', fakeAsync(() => { - let fixture = createComponent(MatInputInvalidHintTestController); + it('validates there\'s only one hint label per side', fakeAsync(() => { + let fixture = createComponent(MatInputInvalidHintTestController); - expect(() => { - try { - fixture.detectChanges(); - flush(); - } catch { - flush(); - } - }).toThrowError( - wrappedErrorMessage(getMatFormFieldDuplicatedHintError('start'))); - })); + expect(() => { + try { + fixture.detectChanges(); + flush(); + } catch { + flush(); + } + }).toThrowError( + wrappedErrorMessage(getMatFormFieldDuplicatedHintError('start'))); + })); - it('validates there\'s only one hint label per side (attribute)', fakeAsync(() => { - let fixture = createComponent(MatInputInvalidHint2TestController); + it('validates there\'s only one hint label per side (attribute)', fakeAsync(() => { + let fixture = createComponent(MatInputInvalidHint2TestController); - expect(() => { - try { - fixture.detectChanges(); - flush(); - } catch { - flush(); - } - }).toThrowError( - wrappedErrorMessage(getMatFormFieldDuplicatedHintError('start'))); - })); + expect(() => { + try { + fixture.detectChanges(); + flush(); + } catch { + flush(); + } + }).toThrowError( + wrappedErrorMessage(getMatFormFieldDuplicatedHintError('start'))); + })); - it('validates there\'s only one placeholder', fakeAsync(() => { - let fixture = createComponent(MatInputInvalidPlaceholderTestController); + it('validates there\'s only one placeholder', fakeAsync(() => { + let fixture = createComponent(MatInputInvalidPlaceholderTestController); - expect(() => { - try { - fixture.detectChanges(); - flush(); - } catch { - flush(); - } - }).toThrowError( - wrappedErrorMessage(getMatFormFieldPlaceholderConflictError())); - })); + expect(() => { + try { + fixture.detectChanges(); + flush(); + } catch { + flush(); + } + }).toThrowError( + wrappedErrorMessage(getMatFormFieldPlaceholderConflictError())); + })); it('validates that matInput child is present', fakeAsync(() => { let fixture = createComponent(MatInputMissingMatInputTestController); @@ -463,6 +463,15 @@ describe('MatInput without forms', () => { expect(selectEl.disabled).toBe(true); })); + it('should add a class to the form field if it has a native select', fakeAsync(() => { + const fixture = createComponent(MatInputSelect); + fixture.detectChanges(); + + const formField = fixture.debugElement.query(By.css('.mat-form-field')).nativeElement; + + expect(formField.classList).toContain('mat-form-field-type-mat-native-select'); + })); + it('supports the required attribute as binding', fakeAsync(() => { let fixture = createComponent(MatInputWithRequired); fixture.detectChanges(); @@ -872,6 +881,14 @@ describe('MatInput without forms', () => { expect(container.classList).not.toContain('mat-form-field-should-float'); }); + it('should not add the native select class if the control is not a native select', () => { + const fixture = createComponent(MatInputWithId); + fixture.detectChanges(); + const formField = fixture.debugElement.query(By.css('mat-form-field')).nativeElement; + + expect(formField.classList).not.toContain('mat-form-field-type-mat-native-select'); + }); + }); describe('MatInput with forms', () => { diff --git a/src/lib/input/input.ts b/src/lib/input/input.ts index db94fd81797..59835fafbbc 100644 --- a/src/lib/input/input.ts +++ b/src/lib/input/input.ts @@ -232,10 +232,14 @@ export class MatInput extends _MatInputMixinBase implements MatFormFieldControl< @Optional() @Self() @Inject(MAT_INPUT_VALUE_ACCESSOR) inputValueAccessor: any, private _autofillMonitor: AutofillMonitor, ngZone: NgZone) { + super(_defaultErrorStateMatcher, _parentForm, _parentFormGroup, ngControl); + + const element = this._elementRef.nativeElement; + // If no input value accessor was explicitly specified, use the element as the input value // accessor. - this._inputValueAccessor = inputValueAccessor || this._elementRef.nativeElement; + this._inputValueAccessor = inputValueAccessor || element; this._previousNativeValue = this.value; @@ -262,7 +266,12 @@ export class MatInput extends _MatInputMixinBase implements MatFormFieldControl< } this._isServer = !this._platform.isBrowser; - this._isNativeSelect = this._elementRef.nativeElement.nodeName.toLowerCase() === 'select'; + this._isNativeSelect = element.nodeName.toLowerCase() === 'select'; + + if (this._isNativeSelect) { + this.controlType = (element as HTMLSelectElement).multiple ? 'mat-native-select-multiple' : + 'mat-native-select'; + } } ngOnInit() {