Skip to content

Commit 10b8353

Browse files
crisbetojelbourn
authored andcommitted
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.
1 parent 18f46cf commit 10b8353

File tree

6 files changed

+115
-63
lines changed

6 files changed

+115
-63
lines changed

src/demo-app/select/select-demo.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ <h4>Looks</h4>
8484
<mat-form-field appearance="fill">
8585
<mat-label>Fill</mat-label>
8686
<select matNativeControl required>
87+
<option value="" selected></option>
8788
<option value="volvo">Volvo</option>
8889
<option value="saab">Saab</option>
8990
<option value="mercedes">Mercedes</option>
@@ -93,6 +94,7 @@ <h4>Looks</h4>
9394
<mat-form-field appearance="outline">
9495
<mat-label>Outline</mat-label>
9596
<select matNativeControl>
97+
<option value="" selected></option>
9698
<option value="volvo">volvo</option>
9799
<option value="saab">Saab</option>
98100
<option value="mercedes">Mercedes</option>

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,11 @@ $mat-form-field-default-infix-width: 180px !default;
217217
display: block;
218218
}
219219

220+
// Element that can used to reliably align content in relation to the form field control.
221+
.mat-form-field-control-wrapper {
222+
position: relative;
223+
}
224+
220225
.mat-form-field._mat-animation-noopable {
221226
.mat-form-field-label,
222227
.mat-form-field-ripple {

src/lib/input/_input-theme.scss

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@
1111
$warn: map-get($theme, warn);
1212
$foreground: map-get($theme, foreground);
1313

14-
.mat-input-element:disabled {
14+
.mat-form-field-type-mat-native-select .mat-form-field-infix::after {
15+
color: mat-color($foreground, secondary-text);
16+
}
17+
18+
.mat-input-element:disabled,
19+
.mat-form-field-type-mat-native-select.mat-form-field-disabled .mat-form-field-infix::after {
1520
color: mat-color($foreground, disabled-text);
1621
}
1722

@@ -31,6 +36,10 @@
3136
.mat-form-field-invalid .mat-input-element {
3237
caret-color: mat-color($warn);
3338
}
39+
40+
.mat-form-field-type-mat-native-select.mat-form-field-invalid .mat-form-field-infix::after {
41+
color: mat-color($warn);
42+
}
3443
}
3544

3645
@mixin mat-input-typography($config) {

src/lib/input/input.scss

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -122,29 +122,14 @@ textarea.mat-input-element {
122122
margin: -2px 0;
123123
}
124124

125-
// URL-encoded Material Design select arrow SVG.
126-
$mat-native-select-arrow-svg: '' +
127-
'data:image/svg+xml;charset=utf8,%3Csvg%20width%3D%2210%22%20height%3D%225%22%20' +
128-
'viewBox%3D%227%2010%2010%205%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fs' +
129-
'vg%22%3E%3Cpath%20fill%3D%22%230%22%20fill-rule%3D%22evenodd%22%20opacity%3D%22' +
130-
'.54%22%20d%3D%22M7%2010l5%205%205-5z%22%2F%3E%3C%2Fsvg%3E';
131-
132125
// Remove the native select down arrow and replace it with material design arrow
133126
select.mat-input-element {
134127
-moz-appearance: none;
135128
-webkit-appearance: none;
136129
position: relative;
137130
background-color: transparent;
138-
background-repeat: no-repeat;
139131
display: inline-flex;
140132
box-sizing: border-box;
141-
background-position: right center;
142-
143-
// Native multi-selects are rendered inline which
144-
// means that they shouldn't have a dropdown arrow.
145-
&:not([multiple]) {
146-
background-image: url($mat-native-select-arrow-svg);
147-
}
148133

149134
&::-ms-expand {
150135
display: none;
@@ -156,9 +141,34 @@ select.mat-input-element {
156141
&::-moz-focus-inner {
157142
border: 0;
158143
}
144+
}
145+
146+
.mat-form-field-type-mat-native-select {
147+
$arrow-size: 5px;
148+
149+
.mat-form-field-infix::after {
150+
content: '';
151+
width: 0;
152+
height: 0;
153+
border-left: $arrow-size solid transparent;
154+
border-right: $arrow-size solid transparent;
155+
border-top: $arrow-size solid;
156+
position: absolute;
157+
top: 50%;
158+
right: 0;
159+
margin-top: -$arrow-size / 2;
160+
161+
[dir='rtl'] & {
162+
right: auto;
163+
left: 0;
164+
}
165+
}
159166

160-
[dir='rtl'] & {
161-
background-position: left center;
167+
&.mat-form-field-appearance-outline .mat-form-field-infix::after {
168+
margin-top: -$arrow-size;
162169
}
163-
}
164170

171+
&.mat-form-field-appearance-fill .mat-form-field-infix::after {
172+
margin-top: -$arrow-size * 2;
173+
}
174+
}

src/lib/input/input.spec.ts

Lines changed: 59 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -218,51 +218,51 @@ describe('MatInput without forms', () => {
218218
const labelElement: HTMLInputElement =
219219
fixture.debugElement.query(By.css('label')).nativeElement;
220220

221-
expect(inputElement.id).toBe('test-id');
222-
expect(labelElement.getAttribute('for')).toBe('test-id');
223-
}));
221+
expect(inputElement.id).toBe('test-id');
222+
expect(labelElement.getAttribute('for')).toBe('test-id');
223+
}));
224224

225-
it('validates there\'s only one hint label per side', fakeAsync(() => {
226-
let fixture = createComponent(MatInputInvalidHintTestController);
227-
228-
expect(() => {
229-
try {
230-
fixture.detectChanges();
231-
flush();
232-
} catch {
233-
flush();
234-
}
235-
}).toThrowError(
236-
wrappedErrorMessage(getMatFormFieldDuplicatedHintError('start')));
237-
}));
225+
it('validates there\'s only one hint label per side', fakeAsync(() => {
226+
let fixture = createComponent(MatInputInvalidHintTestController);
238227

239-
it('validates there\'s only one hint label per side (attribute)', fakeAsync(() => {
240-
let fixture = createComponent(MatInputInvalidHint2TestController);
241-
242-
expect(() => {
243-
try {
244-
fixture.detectChanges();
245-
flush();
246-
} catch {
247-
flush();
248-
}
249-
}).toThrowError(
250-
wrappedErrorMessage(getMatFormFieldDuplicatedHintError('start')));
251-
}));
228+
expect(() => {
229+
try {
230+
fixture.detectChanges();
231+
flush();
232+
} catch {
233+
flush();
234+
}
235+
}).toThrowError(
236+
wrappedErrorMessage(getMatFormFieldDuplicatedHintError('start')));
237+
}));
252238

253-
it('validates there\'s only one placeholder', fakeAsync(() => {
254-
let fixture = createComponent(MatInputInvalidPlaceholderTestController);
255-
256-
expect(() => {
257-
try {
258-
fixture.detectChanges();
259-
flush();
260-
} catch {
261-
flush();
262-
}
263-
}).toThrowError(
264-
wrappedErrorMessage(getMatFormFieldPlaceholderConflictError()));
265-
}));
239+
it('validates there\'s only one hint label per side (attribute)', fakeAsync(() => {
240+
let fixture = createComponent(MatInputInvalidHint2TestController);
241+
242+
expect(() => {
243+
try {
244+
fixture.detectChanges();
245+
flush();
246+
} catch {
247+
flush();
248+
}
249+
}).toThrowError(
250+
wrappedErrorMessage(getMatFormFieldDuplicatedHintError('start')));
251+
}));
252+
253+
it('validates there\'s only one placeholder', fakeAsync(() => {
254+
let fixture = createComponent(MatInputInvalidPlaceholderTestController);
255+
256+
expect(() => {
257+
try {
258+
fixture.detectChanges();
259+
flush();
260+
} catch {
261+
flush();
262+
}
263+
}).toThrowError(
264+
wrappedErrorMessage(getMatFormFieldPlaceholderConflictError()));
265+
}));
266266

267267
it('validates that matInput child is present', fakeAsync(() => {
268268
let fixture = createComponent(MatInputMissingMatInputTestController);
@@ -463,6 +463,15 @@ describe('MatInput without forms', () => {
463463
expect(selectEl.disabled).toBe(true);
464464
}));
465465

466+
it('should add a class to the form field if it has a native select', fakeAsync(() => {
467+
const fixture = createComponent(MatInputSelect);
468+
fixture.detectChanges();
469+
470+
const formField = fixture.debugElement.query(By.css('.mat-form-field')).nativeElement;
471+
472+
expect(formField.classList).toContain('mat-form-field-type-mat-native-select');
473+
}));
474+
466475
it('supports the required attribute as binding', fakeAsync(() => {
467476
let fixture = createComponent(MatInputWithRequired);
468477
fixture.detectChanges();
@@ -872,6 +881,14 @@ describe('MatInput without forms', () => {
872881
expect(container.classList).not.toContain('mat-form-field-should-float');
873882
});
874883

884+
it('should not add the native select class if the control is not a native select', () => {
885+
const fixture = createComponent(MatInputWithId);
886+
fixture.detectChanges();
887+
const formField = fixture.debugElement.query(By.css('mat-form-field')).nativeElement;
888+
889+
expect(formField.classList).not.toContain('mat-form-field-type-mat-native-select');
890+
});
891+
875892
});
876893

877894
describe('MatInput with forms', () => {

src/lib/input/input.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,10 +232,14 @@ export class MatInput extends _MatInputMixinBase implements MatFormFieldControl<
232232
@Optional() @Self() @Inject(MAT_INPUT_VALUE_ACCESSOR) inputValueAccessor: any,
233233
private _autofillMonitor: AutofillMonitor,
234234
ngZone: NgZone) {
235+
235236
super(_defaultErrorStateMatcher, _parentForm, _parentFormGroup, ngControl);
237+
238+
const element = this._elementRef.nativeElement;
239+
236240
// If no input value accessor was explicitly specified, use the element as the input value
237241
// accessor.
238-
this._inputValueAccessor = inputValueAccessor || this._elementRef.nativeElement;
242+
this._inputValueAccessor = inputValueAccessor || element;
239243

240244
this._previousNativeValue = this.value;
241245

@@ -262,7 +266,12 @@ export class MatInput extends _MatInputMixinBase implements MatFormFieldControl<
262266
}
263267

264268
this._isServer = !this._platform.isBrowser;
265-
this._isNativeSelect = this._elementRef.nativeElement.nodeName.toLowerCase() === 'select';
269+
this._isNativeSelect = element.nodeName.toLowerCase() === 'select';
270+
271+
if (this._isNativeSelect) {
272+
this.controlType = (element as HTMLSelectElement).multiple ? 'mat-native-select-multiple' :
273+
'mat-native-select';
274+
}
266275
}
267276

268277
ngOnInit() {

0 commit comments

Comments
 (0)