Skip to content

Commit 9f67962

Browse files
committed
feat(select): allow setting the theme color
* Allows the user to set the theme color of a `md-select`. * Adds theming to the `md-option` component. * Tweaks the `md-pseudo-checkbox` theme to allow it to inherit the color. Fixes #3923.
1 parent 59c6289 commit 9f67962

File tree

8 files changed

+156
-24
lines changed

8 files changed

+156
-24
lines changed

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<md-card>
55
<md-card-subtitle>ngModel</md-card-subtitle>
66

7-
<md-select placeholder="Drink" [(ngModel)]="currentDrink" [required]="drinksRequired" [disabled]="drinksDisabled"
7+
<md-select placeholder="Drink" [color]="drinksTheme" [(ngModel)]="currentDrink" [required]="drinksRequired" [disabled]="drinksDisabled"
88
[floatPlaceholder]="floatPlaceholder" #drinkControl="ngModel">
99
<md-option *ngFor="let drink of drinks" [value]="drink.value" [disabled]="drink.disabled">
1010
{{ drink.viewValue }}
@@ -22,6 +22,12 @@
2222
<option value="never">Never</option>
2323
</select>
2424
</p>
25+
<p>
26+
<label for="drinks-theme">Theme:</label>
27+
<select [(ngModel)]="drinksTheme" id="drinks-theme">
28+
<option *ngFor="let theme of availableThemes" [value]="theme.value">{{ theme.name }}</option>
29+
</select>
30+
</p>
2531

2632
<button md-button (click)="currentDrink='water-2'">SET VALUE</button>
2733
<button md-button (click)="drinksRequired=!drinksRequired">TOGGLE REQUIRED</button>
@@ -33,7 +39,7 @@
3339
<md-card-subtitle>Multiple selection</md-card-subtitle>
3440

3541
<md-card-content>
36-
<md-select multiple placeholder="Pokemon" [(ngModel)]="currentPokemon"
42+
<md-select multiple [color]="pokemonTheme" placeholder="Pokemon" [(ngModel)]="currentPokemon"
3743
[required]="pokemonRequired" [disabled]="pokemonDisabled" #pokemonControl="ngModel">
3844
<md-option *ngFor="let creature of pokemon" [value]="creature.value">
3945
{{ creature.viewValue }}
@@ -43,6 +49,12 @@
4349
<p> Touched: {{ pokemonControl.touched }} </p>
4450
<p> Dirty: {{ pokemonControl.dirty }} </p>
4551
<p> Status: {{ pokemonControl.control?.status }} </p>
52+
<p>
53+
<label for="pokemon-theme">Theme:</label>
54+
<select [(ngModel)]="pokemonTheme" id="pokemon-theme">
55+
<option *ngFor="let theme of availableThemes" [value]="theme.value">{{ theme.name }}</option>
56+
</select>
57+
</p>
4658
<button md-button (click)="setPokemonValue()">SET VALUE</button>
4759
<button md-button (click)="pokemonRequired=!pokemonRequired">TOGGLE REQUIRED</button>
4860
<button md-button (click)="pokemonDisabled=!pokemonDisabled">TOGGLE DISABLED</button>

src/demo-app/select/select-demo.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export class SelectDemo {
1919
latestChangeEvent: MdSelectChange;
2020
floatPlaceholder: string = 'auto';
2121
foodControl = new FormControl('pizza-1');
22+
drinksTheme = 'primary';
23+
pokemonTheme = 'primary';
2224

2325
foods = [
2426
{value: 'steak-0', viewValue: 'Steak'},
@@ -48,6 +50,12 @@ export class SelectDemo {
4850
{value: 'psyduck-6', viewValue: 'Psyduck'},
4951
];
5052

53+
availableThemes = [
54+
{value: 'primary', name: 'Primary' },
55+
{value: 'accent', name: 'Accent' },
56+
{value: 'warn', name: 'Warn' }
57+
];
58+
5159
toggleDisabled() {
5260
this.foodControl.enabled ? this.foodControl.disable() : this.foodControl.enable();
5361
}

src/lib/core/option/_option-theme.scss

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,26 @@
55
$foreground: map-get($theme, foreground);
66
$background: map-get($theme, background);
77
$primary: map-get($theme, primary);
8+
$accent: map-get($theme, accent);
9+
$warn: map-get($theme, warn);
810

911
.mat-option {
1012
&:hover:not(.mat-option-disabled), &:focus:not(.mat-option-disabled) {
1113
background: mat-color($background, hover);
1214
}
1315

1416
&.mat-selected {
15-
color: mat-color($primary);
17+
&.mat-primary, .mat-primary & {
18+
color: mat-color($primary);
19+
}
20+
21+
&.mat-accent, .mat-accent & {
22+
color: mat-color($accent);
23+
}
24+
25+
&.mat-warn, .mat-warn & {
26+
color: mat-color($warn);
27+
}
1628

1729
// In multiple mode there is a checkbox to show that the option is selected.
1830
&:not(.mat-option-multiple) {

src/lib/core/selection/pseudo-checkbox/_pseudo-checkbox-theme.scss

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@
2424
}
2525

2626
.mat-pseudo-checkbox-checked, .mat-pseudo-checkbox-indeterminate {
27-
&.mat-primary {
27+
&.mat-primary, .mat-primary & {
2828
background: mat-color($primary, 500);
2929
}
3030

31-
&.mat-accent {
31+
&.mat-accent, .mat-accent & {
3232
background: mat-color($accent, 500);
3333
}
3434

35-
&.mat-warn {
35+
&.mat-warn, .mat-warn & {
3636
background: mat-color($warn, 500);
3737
}
3838

src/lib/select/_select-theme.scss

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,43 @@
55
$foreground: map-get($theme, foreground);
66
$background: map-get($theme, background);
77
$primary: map-get($theme, primary);
8+
$accent: map-get($theme, accent);
89
$warn: map-get($theme, warn);
910

10-
.mat-select-trigger {
11-
color: mat-color($foreground, hint-text);
11+
.mat-select:focus:not(.mat-select-disabled) {
12+
&.mat-primary {
13+
.mat-select-trigger, .mat-select-arrow {
14+
color: mat-color($primary);
15+
}
16+
17+
.mat-select-underline {
18+
background-color: mat-color($primary);
19+
}
20+
}
21+
22+
&.mat-accent {
23+
.mat-select-trigger, .mat-select-arrow {
24+
color: mat-color($accent);
25+
}
26+
27+
.mat-select-underline {
28+
background-color: mat-color($accent);
29+
}
30+
}
31+
32+
&.mat-warn {
33+
.mat-select-trigger, .mat-select-arrow {
34+
color: mat-color($warn);
35+
}
1236

13-
.mat-select:focus:not(.mat-select-disabled) & {
14-
color: mat-color($primary);
37+
.mat-select-underline {
38+
background-color: mat-color($warn);
39+
}
1540
}
41+
}
42+
43+
.mat-select-trigger {
44+
color: mat-color($foreground, hint-text);
1645

1746
.mat-select:not(:focus).ng-invalid.ng-touched:not(.mat-select-disabled) & {
1847
color: mat-color($warn);
@@ -22,10 +51,6 @@
2251
.mat-select-underline {
2352
background-color: mat-color($foreground, divider);
2453

25-
.mat-select:focus:not(.mat-select-disabled) & {
26-
background-color: mat-color($primary);
27-
}
28-
2954
.mat-select:not(:focus).ng-invalid.ng-touched:not(.mat-select-disabled) & {
3055
background-color: mat-color($warn);
3156
}
@@ -34,10 +59,6 @@
3459
.mat-select-arrow {
3560
color: mat-color($foreground, hint-text);
3661

37-
.mat-select:focus:not(.mat-select-disabled) & {
38-
color: mat-color($primary);
39-
}
40-
4162
.mat-select:not(:focus).ng-invalid.ng-touched:not(.mat-select-disabled) & {
4263
color: mat-color($warn);
4364
}

src/lib/select/select.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
[offsetY]="_offsetY" [offsetX]="_offsetX" (attach)="_setScrollTop()">
1919
<div class="mat-select-panel" [@transformPanel]="'showing'" (@transformPanel.done)="_onPanelDone()"
2020
(keydown)="_keyManager.onKeydown($event)" [style.transformOrigin]="_transformOrigin"
21-
[class.mat-select-panel-done-animating]="_panelDoneAnimating">
21+
[class.mat-select-panel-done-animating]="_panelDoneAnimating" [ngClass]="'mat-' + color">
2222
<div class="mat-select-content" [@fadeInContent]="'showing'" (@fadeInContent.done)="_onFadeInDone()">
2323
<ng-content></ng-content>
2424
</div>

src/lib/select/select.spec.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ describe('MdSelect', () => {
4747
BasicSelectOnPush,
4848
BasicSelectOnPushPreselected,
4949
SelectWithPlainTabindex,
50-
SelectEarlyAccessSibling
50+
SelectEarlyAccessSibling,
51+
BasicSelectWithTheming
5152
],
5253
providers: [
5354
{provide: OverlayContainer, useFactory: () => {
@@ -1654,6 +1655,55 @@ describe('MdSelect', () => {
16541655

16551656
});
16561657

1658+
describe('theming', () => {
1659+
let fixture: ComponentFixture<BasicSelectWithTheming>;
1660+
let testInstance: BasicSelectWithTheming;
1661+
let selectElement: HTMLElement;
1662+
1663+
beforeEach(async(() => {
1664+
fixture = TestBed.createComponent(BasicSelectWithTheming);
1665+
testInstance = fixture.componentInstance;
1666+
fixture.detectChanges();
1667+
1668+
selectElement = fixture.debugElement.query(By.css('.mat-select')).nativeElement;
1669+
}));
1670+
1671+
it('should default to the primary theme', () => {
1672+
expect(fixture.componentInstance.select.color).toBe('primary');
1673+
expect(selectElement.classList).toContain('mat-primary');
1674+
});
1675+
1676+
it('should be able to override the theme', () => {
1677+
fixture.componentInstance.theme = 'accent';
1678+
fixture.detectChanges();
1679+
1680+
expect(fixture.componentInstance.select.color).toBe('accent');
1681+
expect(selectElement.classList).toContain('mat-accent');
1682+
expect(selectElement.classList).not.toContain('mat-primary');
1683+
});
1684+
1685+
it('should not be able to set a blank theme', () => {
1686+
fixture.componentInstance.theme = '';
1687+
fixture.detectChanges();
1688+
1689+
expect(fixture.componentInstance.select.color).toBe('primary');
1690+
expect(selectElement.classList).toContain('mat-primary');
1691+
});
1692+
1693+
it('should pass the theme to the panel', () => {
1694+
fixture.componentInstance.theme = 'warn';
1695+
fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement.click();
1696+
fixture.detectChanges();
1697+
1698+
const panel = overlayContainerElement.querySelector('.mat-select-panel');
1699+
1700+
expect(fixture.componentInstance.select.color).toBe('warn');
1701+
expect(selectElement.classList).toContain('mat-warn');
1702+
expect(panel.classList).toContain('mat-warn');
1703+
});
1704+
1705+
});
1706+
16571707
});
16581708

16591709

@@ -1963,6 +2013,20 @@ class SelectWithPlainTabindex { }
19632013
})
19642014
class SelectEarlyAccessSibling { }
19652015

2016+
@Component({
2017+
selector: 'basic-select-with-theming',
2018+
template: `
2019+
<md-select placeholder="Food" [color]="theme">
2020+
<md-option value="steak-0">Steak</md-option>
2021+
<md-option value="pizza-1">Pizza</md-option>
2022+
</md-select>
2023+
`
2024+
})
2025+
class BasicSelectWithTheming {
2026+
@ViewChild(MdSelect) select: MdSelect;
2027+
theme: string;
2028+
}
2029+
19662030

19672031
class FakeViewportRuler {
19682032
getViewportRect() {

src/lib/select/select.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
Optional,
1010
Output,
1111
QueryList,
12-
Renderer,
12+
Renderer2,
1313
Self,
1414
ViewEncapsulation,
1515
ViewChild,
@@ -111,7 +111,7 @@ export type MdSelectFloatPlaceholderType = 'always' | 'never' | 'auto';
111111
'[class.mat-select-disabled]': 'disabled',
112112
'[class.mat-select]': 'true',
113113
'(keydown)': '_handleKeydown($event)',
114-
'(blur)': '_onBlur()'
114+
'(blur)': '_onBlur()',
115115
},
116116
animations: [
117117
transformPlaceholder,
@@ -157,6 +157,9 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal
157157
/** Tab index for the element. */
158158
private _tabIndex: number;
159159

160+
/** Theme color for the component. */
161+
private _color: string;
162+
160163
/**
161164
* The width of the trigger. Must be saved to set the min width of the overlay panel
162165
* and the width of the selected value.
@@ -287,6 +290,17 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal
287290
/** Input that can be used to specify the `aria-labelledby` attribute. */
288291
@Input('aria-labelledby') ariaLabelledby: string = '';
289292

293+
/** Theme color for the component. */
294+
@Input()
295+
get color(): string { return this._color; }
296+
set color(value: string) {
297+
if (value && value !== this._color) {
298+
this._renderer.removeClass(this._element.nativeElement, `mat-${this._color}`);
299+
this._renderer.addClass(this._element.nativeElement, `mat-${value}`);
300+
this._color = value;
301+
}
302+
}
303+
290304
/** Combined stream of all of the child options' change events. */
291305
get optionSelectionChanges(): Observable<MdOptionSelectionChange> {
292306
return Observable.merge(...this.options.map(option => option.onSelectionChange));
@@ -301,7 +315,7 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal
301315
/** Event emitted when the selected value has been changed by the user. */
302316
@Output() change: EventEmitter<MdSelectChange> = new EventEmitter<MdSelectChange>();
303317

304-
constructor(private _element: ElementRef, private _renderer: Renderer,
318+
constructor(private _element: ElementRef, private _renderer: Renderer2,
305319
private _viewportRuler: ViewportRuler, private _changeDetectorRef: ChangeDetectorRef,
306320
@Optional() private _dir: Dir, @Self() @Optional() public _control: NgControl,
307321
@Attribute('tabindex') tabIndex: string) {
@@ -314,6 +328,7 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal
314328

315329
ngOnInit() {
316330
this._selectionModel = new SelectionModel<MdOption>(this.multiple, null, false);
331+
this.color = this.color || 'primary';
317332
}
318333

319334
ngAfterContentInit() {
@@ -681,7 +696,7 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal
681696

682697
/** Focuses the host element when the panel closes. */
683698
private _focusHost(): void {
684-
this._renderer.invokeElementMethod(this._element.nativeElement, 'focus');
699+
this._element.nativeElement.focus();
685700
}
686701

687702
/** Gets the index of the provided option in the option list. */

0 commit comments

Comments
 (0)