Skip to content

Commit

Permalink
feat(select): allow setting the theme color
Browse files Browse the repository at this point in the history
* 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 angular#3923.
  • Loading branch information
crisbeto committed Apr 8, 2017
1 parent b1e5ff6 commit 3bbdd97
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 24 deletions.
16 changes: 14 additions & 2 deletions src/demo-app/select/select-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<md-card>
<md-card-subtitle>ngModel</md-card-subtitle>

<md-select placeholder="Drink" [(ngModel)]="currentDrink" [required]="drinksRequired" [disabled]="drinksDisabled"
<md-select placeholder="Drink" [color]="drinksTheme" [(ngModel)]="currentDrink" [required]="drinksRequired" [disabled]="drinksDisabled"
[floatPlaceholder]="floatPlaceholder" #drinkControl="ngModel">
<md-option *ngFor="let drink of drinks" [value]="drink.value" [disabled]="drink.disabled">
{{ drink.viewValue }}
Expand All @@ -22,6 +22,12 @@
<option value="never">Never</option>
</select>
</p>
<p>
<label for="drinks-theme">Theme:</label>
<select [(ngModel)]="drinksTheme" id="drinks-theme">
<option *ngFor="let theme of availableThemes" [value]="theme.value">{{ theme.name }}</option>
</select>
</p>

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

<md-card-content>
<md-select multiple placeholder="Pokemon" [(ngModel)]="currentPokemon"
<md-select multiple [color]="pokemonTheme" placeholder="Pokemon" [(ngModel)]="currentPokemon"
[required]="pokemonRequired" [disabled]="pokemonDisabled" #pokemonControl="ngModel">
<md-option *ngFor="let creature of pokemon" [value]="creature.value">
{{ creature.viewValue }}
Expand All @@ -43,6 +49,12 @@
<p> Touched: {{ pokemonControl.touched }} </p>
<p> Dirty: {{ pokemonControl.dirty }} </p>
<p> Status: {{ pokemonControl.control?.status }} </p>
<p>
<label for="pokemon-theme">Theme:</label>
<select [(ngModel)]="pokemonTheme" id="pokemon-theme">
<option *ngFor="let theme of availableThemes" [value]="theme.value">{{ theme.name }}</option>
</select>
</p>
<button md-button (click)="setPokemonValue()">SET VALUE</button>
<button md-button (click)="pokemonRequired=!pokemonRequired">TOGGLE REQUIRED</button>
<button md-button (click)="pokemonDisabled=!pokemonDisabled">TOGGLE DISABLED</button>
Expand Down
8 changes: 8 additions & 0 deletions src/demo-app/select/select-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export class SelectDemo {
latestChangeEvent: MdSelectChange;
floatPlaceholder: string = 'auto';
foodControl = new FormControl('pizza-1');
drinksTheme = 'primary';
pokemonTheme = 'primary';

foods = [
{value: 'steak-0', viewValue: 'Steak'},
Expand Down Expand Up @@ -48,6 +50,12 @@ export class SelectDemo {
{value: 'psyduck-6', viewValue: 'Psyduck'},
];

availableThemes = [
{value: 'primary', name: 'Primary' },
{value: 'accent', name: 'Accent' },
{value: 'warn', name: 'Warn' }
];

toggleDisabled() {
this.foodControl.enabled ? this.foodControl.disable() : this.foodControl.enable();
}
Expand Down
14 changes: 13 additions & 1 deletion src/lib/core/option/_option-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,26 @@
$foreground: map-get($theme, foreground);
$background: map-get($theme, background);
$primary: map-get($theme, primary);
$accent: map-get($theme, accent);
$warn: map-get($theme, warn);

.mat-option {
&:hover:not(.mat-option-disabled), &:focus:not(.mat-option-disabled) {
background: mat-color($background, hover);
}

&.mat-selected {
color: mat-color($primary);
&.mat-primary, .mat-primary & {
color: mat-color($primary);
}

&.mat-accent, .mat-accent & {
color: mat-color($accent);
}

&.mat-warn, .mat-warn & {
color: mat-color($warn);
}

// In multiple mode there is a checkbox to show that the option is selected.
&:not(.mat-option-multiple) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@
}

.mat-pseudo-checkbox-checked, .mat-pseudo-checkbox-indeterminate {
&.mat-primary {
&.mat-primary, .mat-primary & {
background: mat-color($primary, 500);
}

&.mat-accent {
&.mat-accent, .mat-accent & {
background: mat-color($accent, 500);
}

&.mat-warn {
&.mat-warn, .mat-warn & {
background: mat-color($warn, 500);
}

Expand Down
45 changes: 33 additions & 12 deletions src/lib/select/_select-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,43 @@
$foreground: map-get($theme, foreground);
$background: map-get($theme, background);
$primary: map-get($theme, primary);
$accent: map-get($theme, accent);
$warn: map-get($theme, warn);

.mat-select-trigger {
color: mat-color($foreground, hint-text);
.mat-select:focus:not(.mat-select-disabled) {
&.mat-primary {
.mat-select-trigger, .mat-select-arrow {
color: mat-color($primary);
}

.mat-select-underline {
background-color: mat-color($primary);
}
}

&.mat-accent {
.mat-select-trigger, .mat-select-arrow {
color: mat-color($accent);
}

.mat-select-underline {
background-color: mat-color($accent);
}
}

&.mat-warn {
.mat-select-trigger, .mat-select-arrow {
color: mat-color($warn);
}

.mat-select:focus:not(.mat-select-disabled) & {
color: mat-color($primary);
.mat-select-underline {
background-color: mat-color($warn);
}
}
}

.mat-select-trigger {
color: mat-color($foreground, hint-text);

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

.mat-select:focus:not(.mat-select-disabled) & {
background-color: mat-color($primary);
}

.mat-select:not(:focus).ng-invalid.ng-touched:not(.mat-select-disabled) & {
background-color: mat-color($warn);
}
Expand All @@ -34,10 +59,6 @@
.mat-select-arrow {
color: mat-color($foreground, hint-text);

.mat-select:focus:not(.mat-select-disabled) & {
color: mat-color($primary);
}

.mat-select:not(:focus).ng-invalid.ng-touched:not(.mat-select-disabled) & {
color: mat-color($warn);
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/select/select.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
[offsetY]="_offsetY" [offsetX]="_offsetX" (attach)="_setScrollTop()">
<div class="mat-select-panel" [@transformPanel]="'showing'" (@transformPanel.done)="_onPanelDone()"
(keydown)="_keyManager.onKeydown($event)" [style.transformOrigin]="_transformOrigin"
[class.mat-select-panel-done-animating]="_panelDoneAnimating">
[class.mat-select-panel-done-animating]="_panelDoneAnimating" [ngClass]="'mat-' + color">
<div class="mat-select-content" [@fadeInContent]="'showing'" (@fadeInContent.done)="_onFadeInDone()">
<ng-content></ng-content>
</div>
Expand Down
66 changes: 65 additions & 1 deletion src/lib/select/select.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ describe('MdSelect', () => {
BasicSelectOnPush,
BasicSelectOnPushPreselected,
SelectWithPlainTabindex,
SelectEarlyAccessSibling
SelectEarlyAccessSibling,
BasicSelectWithTheming
],
providers: [
{provide: OverlayContainer, useFactory: () => {
Expand Down Expand Up @@ -1654,6 +1655,55 @@ describe('MdSelect', () => {

});

describe('theming', () => {
let fixture: ComponentFixture<BasicSelectWithTheming>;
let testInstance: BasicSelectWithTheming;
let selectElement: HTMLElement;

beforeEach(async(() => {
fixture = TestBed.createComponent(BasicSelectWithTheming);
testInstance = fixture.componentInstance;
fixture.detectChanges();

selectElement = fixture.debugElement.query(By.css('.mat-select')).nativeElement;
}));

it('should default to the primary theme', () => {
expect(fixture.componentInstance.select.color).toBe('primary');
expect(selectElement.classList).toContain('mat-primary');
});

it('should be able to override the theme', () => {
fixture.componentInstance.theme = 'accent';
fixture.detectChanges();

expect(fixture.componentInstance.select.color).toBe('accent');
expect(selectElement.classList).toContain('mat-accent');
expect(selectElement.classList).not.toContain('mat-primary');
});

it('should not be able to set a blank theme', () => {
fixture.componentInstance.theme = '';
fixture.detectChanges();

expect(fixture.componentInstance.select.color).toBe('primary');
expect(selectElement.classList).toContain('mat-primary');
});

it('should pass the theme to the panel', () => {
fixture.componentInstance.theme = 'warn';
fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement.click();
fixture.detectChanges();

const panel = overlayContainerElement.querySelector('.mat-select-panel');

expect(fixture.componentInstance.select.color).toBe('warn');
expect(selectElement.classList).toContain('mat-warn');
expect(panel.classList).toContain('mat-warn');
});

});

});


Expand Down Expand Up @@ -1963,6 +2013,20 @@ class SelectWithPlainTabindex { }
})
class SelectEarlyAccessSibling { }

@Component({
selector: 'basic-select-with-theming',
template: `
<md-select placeholder="Food" [color]="theme">
<md-option value="steak-0">Steak</md-option>
<md-option value="pizza-1">Pizza</md-option>
</md-select>
`
})
class BasicSelectWithTheming {
@ViewChild(MdSelect) select: MdSelect;
theme: string;
}


class FakeViewportRuler {
getViewportRect() {
Expand Down
23 changes: 19 additions & 4 deletions src/lib/select/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
Optional,
Output,
QueryList,
Renderer,
Renderer2,
Self,
ViewEncapsulation,
ViewChild,
Expand Down Expand Up @@ -111,7 +111,7 @@ export type MdSelectFloatPlaceholderType = 'always' | 'never' | 'auto';
'[class.mat-select-disabled]': 'disabled',
'[class.mat-select]': 'true',
'(keydown)': '_handleKeydown($event)',
'(blur)': '_onBlur()'
'(blur)': '_onBlur()',
},
animations: [
transformPlaceholder,
Expand Down Expand Up @@ -157,6 +157,9 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal
/** Tab index for the element. */
private _tabIndex: number;

/** Theme color for the component. */
private _color: string;

/**
* The width of the trigger. Must be saved to set the min width of the overlay panel
* and the width of the selected value.
Expand Down Expand Up @@ -287,6 +290,17 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal
/** Input that can be used to specify the `aria-labelledby` attribute. */
@Input('aria-labelledby') ariaLabelledby: string = '';

/** Theme color for the component. */
@Input()
get color(): string { return this._color; }
set color(value: string) {
if (value && value !== this._color) {
this._renderer.removeClass(this._element.nativeElement, `mat-${this._color}`);
this._renderer.addClass(this._element.nativeElement, `mat-${value}`);
this._color = value;
}
}

/** Combined stream of all of the child options' change events. */
get optionSelectionChanges(): Observable<MdOptionSelectionChange> {
return Observable.merge(...this.options.map(option => option.onSelectionChange));
Expand All @@ -301,7 +315,7 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal
/** Event emitted when the selected value has been changed by the user. */
@Output() change: EventEmitter<MdSelectChange> = new EventEmitter<MdSelectChange>();

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

ngOnInit() {
this._selectionModel = new SelectionModel<MdOption>(this.multiple, null, false);
this.color = this.color || 'primary';
}

ngAfterContentInit() {
Expand Down Expand Up @@ -681,7 +696,7 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal

/** Focuses the host element when the panel closes. */
private _focusHost(): void {
this._renderer.invokeElementMethod(this._element.nativeElement, 'focus');
this._element.nativeElement.focus();
}

/** Gets the index of the provided option in the option list. */
Expand Down

0 comments on commit 3bbdd97

Please sign in to comment.