Skip to content

Commit

Permalink
feat(select): make select work inside form-field (#6488)
Browse files Browse the repository at this point in the history
mmalerba authored and jelbourn committed Sep 21, 2017

Verified

This commit was signed with the committer’s verified signature.
thaJeztah Sebastiaan van Stijn
1 parent f9b5ccd commit d914cc4
Showing 29 changed files with 1,038 additions and 954 deletions.
100 changes: 59 additions & 41 deletions src/demo-app/a11y/select/select-a11y.html
Original file line number Diff line number Diff line change
@@ -2,71 +2,89 @@
<h2>Single selection</h2>
<p>Select your favorite color</p>

<md-select placeholder="Colors" [(ngModel)]="selectedColor">
<md-option *ngFor="let color of colors" [value]="color.value">
{{ color.label }}
</md-option>
</md-select>
<md-form-field>
<md-select placeholder="Colors" [(ngModel)]="selectedColor">
<md-option *ngFor="let color of colors" [value]="color.value">
{{ color.label }}
</md-option>
</md-select>
</md-form-field>
</section>

<section>
<h2>Multiple selection</h2>
<p>Pick toppings for your pizza</p>

<md-select placeholder="Toppings" [(ngModel)]="selectedTopping" multiple>
<md-option *ngFor="let topping of toppings" [value]="topping.value">
{{ topping.label }}
</md-option>
</md-select>
<md-form-field>
<md-select placeholder="Toppings" [(ngModel)]="selectedTopping" multiple>
<md-option *ngFor="let topping of toppings" [value]="topping.value">
{{ topping.label }}
</md-option>
</md-select>
</md-form-field>
</section>

<section>
<h2>Grouped options</h2>
<p>Pick your Pokemon</p>

<md-select placeholder="Pokemon" [(ngModel)]="selectedPokemon">
<md-optgroup *ngFor="let group of pokemon" [label]="group.label">
<md-option *ngFor="let creature of group.pokemon" [value]="creature.value">
{{ creature.label }}
</md-option>
</md-optgroup>
</md-select>
<md-form-field>
<md-select placeholder="Pokemon" [(ngModel)]="selectedPokemon">
<md-optgroup *ngFor="let group of pokemon" [label]="group.label">
<md-option *ngFor="let creature of group.pokemon" [value]="creature.value">
{{ creature.label }}
</md-option>
</md-optgroup>
</md-select>
</md-form-field>
</section>

<section>
<h2>Colors</h2>

<div class="select-a11y-spacer">
<md-select placeholder="ZIP code" color="primary">
<md-option value="2000">2000</md-option>
<md-option value="2100">2100</md-option>
</md-select>
<md-form-field color="primary">
<md-select placeholder="ZIP code">
<md-option value="2000">2000</md-option>
<md-option value="2100">2100</md-option>
</md-select>
</md-form-field>

<md-select placeholder="State" color="accent">
<md-option value="alaska">Alaska</md-option>
<md-option value="alabama">Alabama</md-option>
</md-select>
<md-form-field color="accent">
<md-select placeholder="State">
<md-option value="alaska">Alaska</md-option>
<md-option value="alabama">Alabama</md-option>
</md-select>
</md-form-field>

<md-select placeholder="Language" color="warn">
<md-option value="english">English</md-option>
<md-option value="spanish">Spanish</md-option>
</md-select>
<md-form-field color="warn">
<md-select placeholder="Language">
<md-option value="english">English</md-option>
<md-option value="spanish">Spanish</md-option>
</md-select>
</md-form-field>
</div>

<div class="select-a11y-spacer">
<md-select placeholder="Digimon" color="primary" multiple>
<md-option value="mihiramon">Mihiramon</md-option>
<md-option value="sandiramon">Sandiramon</md-option>
</md-select>
<md-form-field color="primary">
<md-select placeholder="Digimon" multiple>
<md-option value="mihiramon">Mihiramon</md-option>
<md-option value="sandiramon">Sandiramon</md-option>
</md-select>
</md-form-field>

<md-select placeholder="Drink" color="accent" multiple>
<md-option value="water">Water</md-option>
<md-option value="coke">Coke</md-option>
</md-select>
<md-form-field color="accent">
<md-select placeholder="Drink" multiple>
<md-option value="water">Water</md-option>
<md-option value="coke">Coke</md-option>
</md-select>
</md-form-field>

<md-select placeholder="Theme" color="warn" multiple>
<md-option value="light">Light</md-option>
<md-option value="dark">Dark</md-option>
</md-select>
<md-form-field color="warn">
<md-select placeholder="Theme" multiple>
<md-option value="light">Light</md-option>
<md-option value="dark">Dark</md-option>
</md-select>
</md-form-field>
</div>
</section>
18 changes: 12 additions & 6 deletions src/demo-app/baseline/baseline-demo.html
Original file line number Diff line number Diff line change
@@ -14,9 +14,12 @@
<input mdInput placeholder="Input" value="Text Input">
</md-form-field>
| Text 5 |
<md-select placeholder="Select">
<md-option value="option">Option</md-option>
</md-select>
<md-form-field>
<md-select placeholder="This placeholder is really really really long">
<md-option value="option">Option</md-option>
<md-option value="long">This option is really really really long</md-option>
</md-select>
</md-form-field>
| Text 6 |
<md-form-field>
<textarea mdInput placeholder="Input" mdTextareaAutosize>Textarea&#10;Line 2</textarea>
@@ -42,9 +45,12 @@ <h1>
<input mdInput placeholder="Input" value="Text Input">
</md-form-field>
| Text 5 |
<md-select placeholder="Select">
<md-option value="option">Option</md-option>
</md-select>
<md-form-field>
<md-select placeholder="This placeholder is really really really long">
<md-option value="option">Option</md-option>
<md-option value="long">This option is really really really long</md-option>
</md-select>
</md-form-field>
| Text 6 |
<md-form-field>
<textarea mdInput placeholder="Input" mdTextareaAutosize>Textarea&#10;Line 2</textarea>
12 changes: 7 additions & 5 deletions src/demo-app/dialog/dialog-demo.html
Original file line number Diff line number Diff line change
@@ -56,11 +56,13 @@ <h2>Dialog backdrop</h2>
<h2>Other options</h2>

<p>
<md-select placeholder="Button alignment" [(ngModel)]="actionsAlignment">
<md-option>Start</md-option>
<md-option value="end">End</md-option>
<md-option value="center">Center</md-option>
</md-select>
<md-form-field>
<md-select placeholder="Button alignment" [(ngModel)]="actionsAlignment">
<md-option>Start</md-option>
<md-option value="end">End</md-option>
<md-option value="center">Center</md-option>
</md-select>
</md-form-field>
</p>

<p>
105 changes: 62 additions & 43 deletions src/demo-app/select/select-demo.html
Original file line number Diff line number Diff line change
@@ -6,13 +6,18 @@
<md-card>
<md-card-subtitle>ngModel</md-card-subtitle>

<md-select placeholder="Drink" [color]="drinksTheme" [(ngModel)]="currentDrink" [required]="drinksRequired"
[disabled]="drinksDisabled" [floatPlaceholder]="floatPlaceholder" #drinkControl="ngModel">
<md-option>None</md-option>
<md-option *ngFor="let drink of drinks" [value]="drink.value" [disabled]="drink.disabled">
{{ drink.viewValue }}
</md-option>
</md-select>
<md-form-field [floatPlaceholder]="floatPlaceholder" [color]="drinksTheme">
<md-select placeholder="Drink" [(ngModel)]="currentDrink" [required]="drinksRequired"
[disabled]="drinksDisabled" #drinkControl="ngModel">
<md-option>None</md-option>
<md-option *ngFor="let drink of drinks" [value]="drink.value" [disabled]="drink.disabled">
{{ drink.viewValue }}
</md-option>
</md-select>
<md-icon mdPrefix class="demo-drink-icon">local_drink</md-icon>
<md-hint>Pick a drink!</md-hint>
<md-error>You must make a selection</md-error>
</md-form-field>
<p> Value: {{ currentDrink }} </p>
<p> Touched: {{ drinkControl.touched }} </p>
<p> Dirty: {{ drinkControl.dirty }} </p>
@@ -28,7 +33,9 @@
<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>
<option *ngFor="let theme of availableThemes" [value]="theme.value">
{{theme.name}}
</option>
</select>
</p>

@@ -42,12 +49,14 @@
<md-card-subtitle>Multiple selection</md-card-subtitle>

<md-card-content>
<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 }}
</md-option>
</md-select>
<md-form-field [color]="pokemonTheme">
<md-select multiple placeholder="Pokemon" [(ngModel)]="currentPokemon"
[required]="pokemonRequired" [disabled]="pokemonDisabled" #pokemonControl="ngModel">
<md-option *ngFor="let creature of pokemon" [value]="creature.value">
{{ creature.viewValue }}
</md-option>
</md-select>
</md-form-field>
<p> Value: {{ currentPokemon }} </p>
<p> Touched: {{ pokemonControl.touched }} </p>
<p> Dirty: {{ pokemonControl.dirty }} </p>
@@ -68,12 +77,14 @@
<md-card>
<md-card-subtitle>Without Angular forms</md-card-subtitle>

<md-select placeholder="Digimon" [(value)]="currentDigimon">
<md-option>None</md-option>
<md-option *ngFor="let creature of digimon" [value]="creature.value">
{{ creature.viewValue }}
</md-option>
</md-select>
<md-form-field>
<md-select placeholder="Digimon" [(value)]="currentDigimon">
<md-option>None</md-option>
<md-option *ngFor="let creature of digimon" [value]="creature.value">
{{ creature.viewValue }}
</md-option>
</md-select>
</md-form-field>

<p>Value: {{ currentDigimon }}</p>

@@ -85,30 +96,34 @@
<md-card-subtitle>Option groups</md-card-subtitle>

<md-card-content>
<md-select placeholder="Pokemon" [(ngModel)]="currentPokemonFromGroup">
<md-optgroup *ngFor="let group of pokemonGroups" [label]="group.name"
[disabled]="group.disabled">
<md-option *ngFor="let creature of group.pokemon" [value]="creature.value">
{{ creature.viewValue }}
</md-option>
</md-optgroup>
</md-select>
<md-form-field>
<md-select placeholder="Pokemon" [(ngModel)]="currentPokemonFromGroup">
<md-optgroup *ngFor="let group of pokemonGroups" [label]="group.name"
[disabled]="group.disabled">
<md-option *ngFor="let creature of group.pokemon" [value]="creature.value">
{{ creature.viewValue }}
</md-option>
</md-optgroup>
</md-select>
</md-form-field>
</md-card-content>
</md-card>


<md-card>
<md-card-subtitle>compareWith</md-card-subtitle>
<md-card-content>
<md-select placeholder="Drink" [color]="drinksTheme"
[(ngModel)]="currentDrinkObject"
[required]="drinkObjectRequired"
[compareWith]="compareByValue ? compareDrinkObjectsByValue : compareByReference"
#drinkObjectControl="ngModel">
<md-option *ngFor="let drink of drinks" [value]="drink" [disabled]="drink.disabled">
{{ drink.viewValue }}
</md-option>
</md-select>
<md-form-field [color]="drinksTheme">
<md-select placeholder="Drink"
[(ngModel)]="currentDrinkObject"
[required]="drinkObjectRequired"
[compareWith]="compareByValue ? compareDrinkObjectsByValue : compareByReference"
#drinkObjectControl="ngModel">
<md-option *ngFor="let drink of drinks" [value]="drink" [disabled]="drink.disabled">
{{ drink.viewValue }}
</md-option>
</md-select>
</md-form-field>
<p> Value: {{ currentDrinkObject | json }} </p>
<p> Touched: {{ drinkObjectControl.touched }} </p>
<p> Dirty: {{ drinkObjectControl.dirty }} </p>
@@ -130,9 +145,11 @@
<md-card-subtitle>formControl</md-card-subtitle>

<md-card-content>
<md-select placeholder="Food i would like to eat" [formControl]="foodControl">
<md-option *ngFor="let food of foods" [value]="food.value"> {{ food.viewValue }}</md-option>
</md-select>
<md-form-field>
<md-select placeholder="Food i would like to eat" [formControl]="foodControl">
<md-option *ngFor="let food of foods" [value]="food.value"> {{ food.viewValue }}</md-option>
</md-select>
</md-form-field>
<p> Value: {{ foodControl.value }} </p>
<p> Touched: {{ foodControl.touched }} </p>
<p> Dirty: {{ foodControl.dirty }} </p>
@@ -149,9 +166,11 @@
<md-card-subtitle>Change event</md-card-subtitle>

<md-card-content>
<md-select placeholder="Starter Pokemon" (change)="latestChangeEvent = $event">
<md-option *ngFor="let creature of pokemon" [value]="creature.value">{{ creature.viewValue }}</md-option>
</md-select>
<md-form-field>
<md-select placeholder="Starter Pokemon" (change)="latestChangeEvent = $event">
<md-option *ngFor="let creature of pokemon" [value]="creature.value">{{ creature.viewValue }}</md-option>
</md-select>
</md-form-field>

<p> Change event value: {{ latestChangeEvent?.value }} </p>
</md-card-content>
4 changes: 4 additions & 0 deletions src/demo-app/select/select-demo.scss
Original file line number Diff line number Diff line change
@@ -7,4 +7,8 @@
margin: 24px;
}

.demo-drink-icon {
vertical-align: bottom;
padding-right: 0.25em;
}
}
26 changes: 15 additions & 11 deletions src/demo-app/snack-bar/snack-bar-demo.html
Original file line number Diff line number Diff line change
@@ -5,17 +5,21 @@ <h1>SnackBar demo</h1>
</div>
<div>
<div>Position in page: </div>
<md-select [(ngModel)]="horizontalPosition">
<md-option value="start">Start</md-option>
<md-option value="end">End</md-option>
<md-option value="left">Left</md-option>
<md-option value="right">Right</md-option>
<md-option value="center">Center</md-option>
</md-select>
<md-select [(ngModel)]="verticalPosition">
<md-option value="top">Top</md-option>
<md-option value="bottom">Bottom</md-option>
</md-select>
<md-form-field>
<md-select [(ngModel)]="horizontalPosition">
<md-option value="start">Start</md-option>
<md-option value="end">End</md-option>
<md-option value="left">Left</md-option>
<md-option value="right">Right</md-option>
<md-option value="center">Center</md-option>
</md-select>
</md-form-field>
<md-form-field>
<md-select [(ngModel)]="verticalPosition">
<md-option value="top">Top</md-option>
<md-option value="bottom">Bottom</md-option>
</md-select>
</md-form-field>
</div>
<div>
<md-checkbox [(ngModel)]="action">
9 changes: 9 additions & 0 deletions src/lib/chips/chip-list.ts
Original file line number Diff line number Diff line change
@@ -81,6 +81,7 @@ export class MdChipListChange {
})
export class MdChipList implements MdFormFieldControl<any>, ControlValueAccessor,
AfterContentInit, OnInit, OnDestroy {
readonly controlType = 'mat-chip-list';

/**
* Stream that emits whenever the state of the input changes such that the wrapping `MdFormField`
@@ -238,6 +239,10 @@ export class MdChipList implements MdFormFieldControl<any>, ControlValueAccessor
return (!this._chipInput || this._chipInput.empty) && this.chips.length === 0;
}

get shouldPlaceholderFloat(): boolean {
return this.empty;
}

/** Whether this chip-list is disabled. */
@Input()
get disabled() { return this.ngControl ? this.ngControl.disabled : this._disabled; }
@@ -387,6 +392,10 @@ export class MdChipList implements MdFormFieldControl<any>, ControlValueAccessor
this.stateChanges.next();
}

onContainerClick() {
this.focus();
}

/**
* Focuses the the first non-disabled chip in this chip list, or the associated input when there
* are no eligible chips.
8 changes: 8 additions & 0 deletions src/lib/core/option/_option.scss
Original file line number Diff line number Diff line change
@@ -29,6 +29,14 @@
}
}

// Collapses unwanted whitespace created by newlines in code like the following:
// <md-option>
// {{value}}
// </md-option>
.mat-option-text {
display: inline-block;
}

.mat-option-ripple {
@include mat-fill;

3 changes: 2 additions & 1 deletion src/lib/core/option/option.html
Original file line number Diff line number Diff line change
@@ -3,7 +3,8 @@
[state]="selected ? 'checked' : ''" [disabled]="disabled"></mat-pseudo-checkbox>
</span>

<ng-content></ng-content>
<span class="mat-option-text"><ng-content></ng-content></span>

<div class="mat-option-ripple" mat-ripple
[matRippleTrigger]="_getHostElement()"
[matRippleDisabled]="disabled || disableRipple">
33 changes: 14 additions & 19 deletions src/lib/form-field/_form-field-theme.scss
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@

// Placeholder colors. Required is used for the `*` star shown in the placeholder.
$placeholder-color: mat-color($foreground, secondary-text);
$floating-placeholder-color: mat-color($primary);
$focused-placeholder-color: mat-color($primary);
$required-placeholder-color: mat-color($accent);

// Underline colors.
@@ -38,7 +38,7 @@
}

.mat-focused .mat-form-field-placeholder {
color: $floating-placeholder-color;
color: $focused-placeholder-color;

&.mat-accent {
color: $underline-color-accent;
@@ -49,11 +49,8 @@
}
}

.mat-form-field-autofill-control:-webkit-autofill + .mat-form-field-placeholder,
.mat-focused .mat-form-field-placeholder.mat-form-field-float {
.mat-form-field-required-marker {
color: $required-placeholder-color;
}
.mat-focused .mat-form-field-required-marker {
color: $required-placeholder-color;
}

.mat-form-field-underline {
@@ -84,7 +81,7 @@
color: $underline-color-warn;

&.mat-accent,
&.mat-form-field-float .mat-form-field-required-marker {
.mat-form-field-required-marker {
color: $underline-color-warn;
}
}
@@ -109,8 +106,7 @@
translateZ(0.001px);
// 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)
scale($font-scale);
-ms-transform: translateY(-$infix-margin-top - $infix-padding) scale($font-scale);

width: 100% / $font-scale;
}
@@ -184,11 +180,17 @@
border-top: $infix-margin-top solid transparent;
}

.mat-form-field-autofill-control {
&:-webkit-autofill + .mat-form-field-placeholder-wrapper .mat-form-field-float {
.mat-form-field-can-float {
&.mat-form-field-should-float .mat-form-field-placeholder {
@include _mat-form-field-placeholder-floating(
$subscript-font-scale, $infix-padding, $infix-margin-top);
}

.mat-form-field-autofill-control:-webkit-autofill + .mat-form-field-placeholder-wrapper
.mat-form-field-placeholder {
@include _mat-form-field-placeholder-floating(
$subscript-font-scale, $infix-padding, $infix-margin-top);
}
}

.mat-form-field-placeholder-wrapper {
@@ -198,13 +200,6 @@

.mat-form-field-placeholder {
top: $infix-margin-top + $infix-padding;

// Show the placeholder above the control when it's not empty, or focused.
&.mat-form-field-float:not(.mat-form-field-empty),
.mat-focused &.mat-form-field-float {
@include _mat-form-field-placeholder-floating($subscript-font-scale,
$infix-padding, $infix-margin-top);
}
}

.mat-form-field-underline {
14 changes: 12 additions & 2 deletions src/lib/form-field/form-field-control.ts
Original file line number Diff line number Diff line change
@@ -36,6 +36,9 @@ export abstract class MdFormFieldControl<T> {
/** Whether the control is empty. */
readonly empty: boolean;

/** Whether the `MdFormField` label should try to float. */
readonly shouldPlaceholderFloat: boolean;

/** Whether the control is required. */
readonly required: boolean;

@@ -45,9 +48,16 @@ export abstract class MdFormFieldControl<T> {
/** Whether the control is in an error state. */
readonly errorState: boolean;

/**
* An optional name for the control type that can be used to distinguish `md-form-field` elements
* based on their control type. The form field will add a class,
* `mat-form-field-type-{{controlType}}` to its root element.
*/
readonly controlType?: string;

/** Sets the list of element IDs that currently describe this control. */
abstract setDescribedByIds(ids: string[]): void;

/** Focuses this control. */
abstract focus(): void;
/** Handles a click on the control's container. */
abstract onContainerClick(event: MouseEvent): void;
}
5 changes: 2 additions & 3 deletions src/lib/form-field/form-field.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<div class="mat-input-wrapper mat-form-field-wrapper">
<div class="mat-input-flex mat-form-field-flex" #connectionContainer>
<div class="mat-input-flex mat-form-field-flex" #connectionContainer
(click)="_control.onContainerClick && _control.onContainerClick($event)">
<div class="mat-input-prefix mat-form-field-prefix" *ngIf="_prefixChildren.length">
<ng-content select="[mdPrefix], [matPrefix]"></ng-content>
</div>
@@ -15,8 +16,6 @@
[attr.aria-owns]="_control.id"
[class.mat-empty]="_control.empty && !_shouldAlwaysFloat"
[class.mat-form-field-empty]="_control.empty && !_shouldAlwaysFloat"
[class.mat-float]="_canPlaceholderFloat"
[class.mat-form-field-float]="_canPlaceholderFloat"
[class.mat-accent]="color == 'accent'"
[class.mat-warn]="color == 'warn'"
#placeholder
46 changes: 23 additions & 23 deletions src/lib/form-field/form-field.scss
Original file line number Diff line number Diff line change
@@ -59,23 +59,7 @@ $mat-form-field-underline-height: 1px !default;
display: block;
position: relative;
flex: auto;
}

// Pseudo-class for Chrome and Safari auto-fill to move the placeholder to the floating position.
// 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 md-form-field
// classes take over to fulfill this behaviour. Assumes the autofill is non-empty.
.mat-form-field-autofill-control:-webkit-autofill + .mat-form-field-placeholder-wrapper {
// The control is still technically empty at this point, so we need to hide non-floating
// placeholders to prevent overlapping with the autofilled value.
.mat-form-field-placeholder {
display: none;
}

.mat-form-field-float {
display: block;
transition: none;
}
min-width: 0;
}

// Used to hide the placeholder overflow on IE, since IE doesn't take transform into account when
@@ -120,19 +104,35 @@ $mat-form-field-underline-height: 1px !default;
// Hide the placeholder initially, and only show it when it's floating or the control is empty.
display: none;

&.mat-form-field-empty,
&.mat-form-field-float:not(.mat-form-field-empty),
.mat-focused &.mat-form-field-float {
display: block;
}

[dir='rtl'] & {
transform-origin: 100% 0;
left: auto;
right: 0;
}
}

.mat-form-field-empty.mat-form-field-placeholder,
.mat-form-field-can-float.mat-form-field-should-float .mat-form-field-placeholder {
display: block;
}

// Pseudo-class for Chrome and Safari auto-fill to move the placeholder to the floating position.
// 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 md-form-field
// classes take over to fulfill this behaviour.
.mat-form-field-autofill-control:-webkit-autofill + .mat-form-field-placeholder-wrapper
.mat-form-field-placeholder {
// The form field will be considered empty if it is autofilled, and therefore the placeholder will
// be shown. Therefore we need to override it to hidden...
display: none;

// ...and re-show it only if it's able to float.
.mat-form-field-can-float & {
display: block;
transition: none;
}
}

// Disable the placeholder animation when the control is not empty (this prevents placeholder
// animating up when the value is set programmatically).
.mat-form-field-placeholder:not(.mat-form-field-empty) {
16 changes: 13 additions & 3 deletions src/lib/form-field/form-field.ts
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ import {
Inject,
Input,
Optional,
QueryList,
QueryList, Renderer2,
ViewChild,
ViewEncapsulation,
} from '@angular/core';
@@ -72,15 +72,19 @@ let nextUniqueId = 0;
'class': 'mat-input-container mat-form-field',
'[class.mat-input-invalid]': '_control.errorState',
'[class.mat-form-field-invalid]': '_control.errorState',
'[class.mat-form-field-can-float]': '_canPlaceholderFloat',
'[class.mat-form-field-should-float]': '_control.shouldPlaceholderFloat || _shouldAlwaysFloat',
'[class.mat-focused]': '_control.focused',
'[class.mat-primary]': 'color == "primary"',
'[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")',
'[class.ng-dirty]': '_shouldForward("dirty")',
'[class.ng-valid]': '_shouldForward("valid")',
'[class.ng-invalid]': '_shouldForward("invalid")',
'[class.ng-pending]': '_shouldForward("pending")',
'(click)': '_control.focus()',
},
encapsulation: ViewEncapsulation.None,
preserveWhitespaces: false,
@@ -155,14 +159,20 @@ export class MdFormField implements AfterViewInit, AfterContentInit, AfterConten
@ContentChildren(MdSuffix) _suffixChildren: QueryList<MdSuffix>;

constructor(
public _elementRef: ElementRef, private _changeDetectorRef: ChangeDetectorRef,
public _elementRef: ElementRef,
private _renderer: Renderer2,
private _changeDetectorRef: ChangeDetectorRef,
@Optional() @Inject(MD_PLACEHOLDER_GLOBAL_OPTIONS) placeholderOptions: PlaceholderOptions) {
this._placeholderOptions = placeholderOptions ? placeholderOptions : {};
this.floatPlaceholder = this._placeholderOptions.float || 'auto';
}

ngAfterContentInit() {
this._validateControlChild();
if (this._control.controlType) {
this._renderer.addClass(
this._elementRef.nativeElement, `mat-form-field-type-${this._control.controlType}`);
}

// Subscribe to changes in the child control state in order to update the form field UI.
startWith.call(this._control.stateChanges, null).subscribe(() => {
24 changes: 12 additions & 12 deletions src/lib/input/input.spec.ts
Original file line number Diff line number Diff line change
@@ -529,36 +529,36 @@ describe('MdInput without forms', function () {
fixture.detectChanges();

let inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
let labelEl = fixture.debugElement.query(By.css('label')).nativeElement;
let formFieldEl = fixture.debugElement.query(By.css('.mat-form-field')).nativeElement;

expect(labelEl.classList).not.toContain('mat-form-field-empty');
expect(labelEl.classList).toContain('mat-form-field-float');
expect(formFieldEl.classList).toContain('mat-form-field-can-float');
expect(formFieldEl.classList).toContain('mat-form-field-should-float');

fixture.componentInstance.shouldFloat = 'auto';
fixture.detectChanges();

expect(labelEl.classList).toContain('mat-form-field-empty');
expect(labelEl.classList).toContain('mat-form-field-float');
expect(formFieldEl.classList).toContain('mat-form-field-can-float');
expect(formFieldEl.classList).not.toContain('mat-form-field-should-float');

// Update the value of the input.
inputEl.value = 'Text';

// Fake behavior of the `(input)` event which should trigger a change detection.
fixture.detectChanges();

expect(labelEl.classList).not.toContain('mat-form-field-empty');
expect(labelEl.classList).toContain('mat-form-field-float');
expect(formFieldEl.classList).toContain('mat-form-field-can-float');
expect(formFieldEl.classList).toContain('mat-form-field-should-float');
});

it('should always float the placeholder when floatPlaceholder is set to true', () => {
let fixture = TestBed.createComponent(MdInputWithDynamicPlaceholder);
fixture.detectChanges();

let inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
let labelEl = fixture.debugElement.query(By.css('label')).nativeElement;
let formFieldEl = fixture.debugElement.query(By.css('.mat-form-field')).nativeElement;

expect(labelEl.classList).not.toContain('mat-form-field-empty');
expect(labelEl.classList).toContain('mat-form-field-float');
expect(formFieldEl.classList).toContain('mat-form-field-can-float');
expect(formFieldEl.classList).toContain('mat-form-field-should-float');

fixture.detectChanges();

@@ -568,8 +568,8 @@ describe('MdInput without forms', function () {
// Fake behavior of the `(input)` event which should trigger a change detection.
fixture.detectChanges();

expect(labelEl.classList).not.toContain('mat-form-field-empty');
expect(labelEl.classList).toContain('mat-form-field-float');
expect(formFieldEl.classList).toContain('mat-form-field-can-float');
expect(formFieldEl.classList).toContain('mat-form-field-should-float');
});


13 changes: 11 additions & 2 deletions src/lib/input/input.ts
Original file line number Diff line number Diff line change
@@ -55,7 +55,7 @@ let nextUniqueId = 0;
'class': 'mat-input-element mat-form-field-autofill-control',
// Native input properties that are overwritten by Angular inputs need to be synced with
// the native input element. Otherwise property bindings for those don't work.
'[id]': 'id',
'[attr.id]': 'id',
'[placeholder]': 'placeholder',
'[disabled]': 'disabled',
'[required]': 'required',
@@ -92,6 +92,9 @@ export class MdInput implements MdFormFieldControl<any>, OnChanges, OnDestroy, D
*/
stateChanges = new Subject<void>();

/** A name for this control that can be used by `md-form-field`. */
controlType = 'mat-input';

/** Whether the element is disabled. */
@Input()
get disabled() { return this.ngControl ? this.ngControl.disabled : this._disabled; }
@@ -129,6 +132,7 @@ export class MdInput implements MdFormFieldControl<any>, OnChanges, OnDestroy, D
@Input() errorStateMatcher: ErrorStateMatcher;

/** The input element's value. */
@Input()
get value() { return this._elementRef.nativeElement.value; }
set value(value: string) {
if (value !== this.value) {
@@ -197,6 +201,8 @@ export class MdInput implements MdFormFieldControl<any>, OnChanges, OnDestroy, D
}
}

focus() { this._elementRef.nativeElement.focus(); }

/** Callback for the cases where the focused state of the input changes. */
_focusChanged(isFocused: boolean) {
if (isFocused !== this.focused) {
@@ -278,9 +284,12 @@ export class MdInput implements MdFormFieldControl<any>, OnChanges, OnDestroy, D
!this._isBadInput();
}

// Implemented as part of MdFormFieldControl.
get shouldPlaceholderFloat(): boolean { return this.focused || !this.empty; }

// Implemented as part of MdFormFieldControl.
setDescribedByIds(ids: string[]) { this._ariaDescribedby = ids.join(' '); }

// Implemented as part of MdFormFieldControl.
focus() { this._elementRef.nativeElement.focus(); }
onContainerClick() { this.focus(); }
}
19 changes: 10 additions & 9 deletions src/lib/paginator/paginator.html
Original file line number Diff line number Diff line change
@@ -3,15 +3,16 @@
{{_intl.itemsPerPageLabel}}
</div>

<mat-select *ngIf="_displayedPageSizeOptions.length > 1"
class="mat-paginator-page-size-select"
[value]="pageSize"
[aria-label]="_intl.itemsPerPageLabel"
(change)="_changePageSize($event.value)">
<mat-option *ngFor="let pageSizeOption of _displayedPageSizeOptions" [value]="pageSizeOption">
{{pageSizeOption}}
</mat-option>
</mat-select>
<mat-form-field *ngIf="_displayedPageSizeOptions.length > 1">
<mat-select class="mat-paginator-page-size-select"
[value]="pageSize"
[aria-label]="_intl.itemsPerPageLabel"
(change)="_changePageSize($event.value)">
<mat-option *ngFor="let pageSizeOption of _displayedPageSizeOptions" [value]="pageSizeOption">
{{pageSizeOption}}
</mat-option>
</mat-select>
</mat-form-field>

<div *ngIf="_displayedPageSizeOptions.length <= 1">{{pageSize}}</div>
</div>
64 changes: 23 additions & 41 deletions src/lib/select/_select-theme.scss
Original file line number Diff line number Diff line change
@@ -3,17 +3,6 @@
@import '../core/style/form-common';
@import '../core/typography/typography-utils';

@mixin _mat-select-inner-content-theme($palette) {
$color: mat-color($palette);

.mat-select-trigger, .mat-select-arrow {
color: $color;
}

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

@mixin mat-select-theme($theme) {
$foreground: map-get($theme, foreground);
@@ -22,25 +11,9 @@
$accent: map-get($theme, accent);
$warn: map-get($theme, warn);
$is-dark-theme: map-get($theme, is-dark);
$underline-color: mat-color($foreground, divider, if($is-dark-theme, 0.7, 0.42));

.mat-select-trigger,
.mat-select-arrow {
color: mat-color($foreground, secondary-text);
}

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

[aria-disabled='true'] .mat-select-underline {
// Since this is a dotted line, we need to make it slightly darker to get it to stand out.
@include mat-control-disabled-underline($underline-color);
}

.mat-select-disabled .mat-select-value,
.mat-select-arrow,
.mat-select-trigger {
.mat-select-arrow {
color: mat-color($foreground, secondary-text);
}

@@ -58,35 +31,44 @@
}
}

.mat-select:focus:not(.mat-select-disabled) {
&.mat-primary {
@include _mat-select-inner-content-theme($primary);
}
.mat-form-field {
&.mat-focused {
&.mat-primary .mat-select-arrow {
color: mat-color($primary);
}

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

&.mat-accent {
@include _mat-select-inner-content-theme($accent);
&.mat-warn .mat-select-arrow {
color: mat-color($warn);
}
}

&.mat-select-required .mat-select-placeholder::after {
.mat-select.mat-select-invalid .mat-select-arrow {
color: mat-color($warn);
}

.mat-select.mat-select-disabled .mat-select-arrow {
color: mat-color($foreground, secondary-text);
}
}

.mat-select:focus:not(.mat-select-disabled).mat-warn, .mat-select-invalid {
@include _mat-select-inner-content-theme($warn);
.mat-select.mat-select-disabled .mat-select-arrow {
color: mat-color($warn);
}
}

@mixin mat-select-typography($config) {
$trigger-font-size: mat-font-size($config, subheading-2);
// The unit-less line-height from the font config.
$line-height: mat-line-height($config, input);

.mat-select {
// Reserve enough space for the floating placeholder.
padding-top: $trigger-font-size;
font-family: mat-font-family($config);
}

.mat-select-trigger {
font-size: $trigger-font-size;
height: $line-height * 1em;
}
}
2 changes: 1 addition & 1 deletion src/lib/select/public_api.ts
Original file line number Diff line number Diff line change
@@ -8,5 +8,5 @@

export * from './select-module';
export * from './select';
export {fadeInContent, transformPanel, transformPlaceholder} from './select-animations';
export * from './select-animations';
export * from './mat-exports';
19 changes: 0 additions & 19 deletions src/lib/select/select-animations.ts
Original file line number Diff line number Diff line change
@@ -22,25 +22,6 @@ import {
* The values below match the implementation of the AngularJS Material md-select animation.
*/

/**
* This animation shrinks the placeholder text to 75% of its normal size and translates
* it to either the top left corner (ltr) or top right corner (rtl) of the trigger,
* depending on the text direction of the application.
*/
export const transformPlaceholder: AnimationTriggerMetadata = trigger('transformPlaceholder', [
state('floating-ltr', style({
top: '-22px',
left: '-2px',
transform: 'scale(0.75)'
})),
state('floating-rtl', style({
top: '-22px',
left: '2px',
transform: 'scale(0.75)'
})),
transition('* => *', animate('400ms cubic-bezier(0.25, 0.8, 0.25, 1)'))
]);

/**
* This animation transforms the select's overlay panel on and off the page.
*
39 changes: 18 additions & 21 deletions src/lib/select/select.html
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
<div
cdk-overlay-origin
class="mat-select-trigger"
aria-hidden="true"
(click)="toggle()"
#origin="cdkOverlayOrigin"
#trigger>
<span
class="mat-select-placeholder"
[class.mat-floating-placeholder]="_hasValue()"
[@transformPlaceholder]="_getPlaceholderAnimationState()"
[style.opacity]="_getPlaceholderOpacity()"
[style.width.px]="_selectedValueWidth">{{ placeholder }}</span>

<span class="mat-select-value" *ngIf="_hasValue()">
<span class="mat-select-value-text" [ngSwitch]="!!customTrigger">
<div cdk-overlay-origin
class="mat-select-trigger"
aria-hidden="true"
(click)="toggle()"
#origin="cdkOverlayOrigin"
#trigger>
<div class="mat-select-value">
<!--
TODO(mmalerba): &nbsp; is currently broken in components with preserveWhitespace: false, so we
evaluate it as a JS string binding instead. Change back to &nbsp; once it works again.
-->
<ng-container *ngIf="empty">{{'\xa0'}}</ng-container>
<span class="mat-select-value-text" *ngIf="!empty" [ngSwitch]="!!customTrigger">
<span *ngSwitchDefault>{{ triggerValue }}</span>
<ng-content select="md-select-trigger, mat-select-trigger" *ngSwitchCase="true"></ng-content>
</span>
</span>
</div>

<span class="mat-select-arrow"></span>
<span class="mat-select-underline"></span>
<div class="mat-select-arrow-wrapper"><div class="mat-select-arrow"></div></div>
</div>

<ng-template
@@ -31,7 +27,7 @@
[origin]="origin"
[open]="panelOpen"
[positions]="_positions"
[minWidth]="_triggerWidth"
[minWidth]="_triggerRect?.width"
[offsetY]="_offsetY"
(backdropClick)="close()"
(attach)="_onAttached()"
@@ -44,7 +40,8 @@
(@transformPanel.done)="_onPanelDone()"
(keydown)="_handlePanelKeydown($event)"
[style.transformOrigin]="_transformOrigin"
[class.mat-select-panel-done-animating]="_panelDoneAnimating">
[class.mat-select-panel-done-animating]="_panelDoneAnimating"
[style.font-size.px]="_triggerFontSize">

<div class="mat-select-content" [@fadeInContent]="'showing'" (@fadeInContent.done)="_onFadeInDone()">
<ng-content></ng-content>
47 changes: 29 additions & 18 deletions src/lib/select/select.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
`<md-select>` is a form control for selecting a value from a set of options, similar to the native
`<select>` element. You can read more about selects in the
[Material Design spec](https://material.google.com/components/menus.html).
[Material Design spec](https://material.google.com/components/menus.html). It is designed to work
inside of an `<md-form-field>` element.

<!-- example(select-overview) -->

@@ -12,9 +13,11 @@ binding to it.

*my-comp.html*
```html
<md-select placeholder="State">
<md-option *ngFor="let state of states" [value]="state.code">{{ state.name }}</md-option>
</md-select>
<md-form-field>
<md-select placeholder="State">
<md-option *ngFor="let state of states" [value]="state.code">{{ state.name }}</md-option>
</md-select>
</md-form-field>
```

### Getting and setting the select value
@@ -24,9 +27,11 @@ any of the form directives from the core `FormsModule` or `ReactiveFormsModule`:

*my-comp.html*
```html
<md-select placeholder="State" [(ngModel)]="myState">
<md-option *ngFor="let state of states" [value]="state.code">{{ state.name }}</md-option>
</md-select>
<md-form-field>
<md-select placeholder="State" [(ngModel)]="myState">
<md-option *ngFor="let state of states" [value]="state.code">{{ state.name }}</md-option>
</md-select>
</md-form-field>
```

*my-comp.ts*
@@ -43,10 +48,12 @@ If you want one of your options to reset the select's value, you can omit specif

*my-comp.html*
```html
<md-select placeholder="State">
<md-option>None</md-option>
<md-option *ngFor="let state of states" [value]="state.code">{{ state.name }}</md-option>
</md-select>
<md-form-field>
<md-select placeholder="State">
<md-option>None</md-option>
<md-option *ngFor="let state of states" [value]="state.code">{{ state.name }}</md-option>
</md-select>
</md-form-field>
```

### Setting a static placeholder
@@ -57,9 +64,11 @@ It's possible to turn off the placeholder's floating animation using the `floatP
- `'always'`: This makes the placeholder permanently float above the input. It will not animate up or down.

```html
<md-select placeholder="State" [(ngModel)]="myState" floatPlaceholder="never">
<md-option *ngFor="let state of states" [value]="state.code">{{ state.name }}</md-option>
</md-select>
<md-form-field floatPlaceholder="never">
<md-select placeholder="State" [(ngModel)]="myState">
<md-option *ngFor="let state of states" [value]="state.code">{{ state.name }}</md-option>
</md-select>
</md-form-field>
```

Global default placeholder options can be specified by setting the `MD_PLACEHOLDER_GLOBAL_OPTIONS` provider. This setting will apply to all components that support the floating placeholder.
@@ -76,10 +85,12 @@ Global default placeholder options can be specified by setting the `MD_PLACEHOLD
If you want to display a custom trigger label inside a select, you can use the `md-select-trigger` element:

```html
<md-select placeholder="Favorite food" #select="mdSelect">
<md-select-trigger>You have selected: {{ select.selected?.viewValue }}</md-select-trigger>
<md-option *ngFor="let food of foods" [value]="food.value">{{ food.viewValue }}</md-option>
</md-select>
<md-form-field>
<md-select placeholder="Favorite food" #select="mdSelect">
<md-select-trigger>You have selected: {{ select.selected?.viewValue }}</md-select-trigger>
<md-option *ngFor="let food of foods" [value]="food.value">{{ food.viewValue }}</md-option>
</md-select>
</md-form-field>
```

Here are the available global options:
115 changes: 35 additions & 80 deletions src/lib/select/select.scss
Original file line number Diff line number Diff line change
@@ -4,23 +4,21 @@
@import '../core/style/vendor-prefixes';
@import '../../cdk/a11y/a11y';

$mat-select-trigger-height: 30px !default;
$mat-select-trigger-min-width: 112px !default;
$mat-select-arrow-size: 5px !default;
$mat-select-arrow-margin: 4px !default;
$mat-select-panel-max-height: 256px !default;
$mat-select-trigger-underline-height: 1px !default;
$mat-select-item-height: 3em !default;

$mat-select-placeholder-arrow-space: 2 * ($mat-select-arrow-size + $mat-select-arrow-margin);

.mat-select {
display: inline-block;
width: 100%;
outline: none;
}

.mat-select-trigger {
display: flex;
align-items: center;
height: $mat-select-trigger-height;
min-width: $mat-select-trigger-min-width;
display: inline-table;
cursor: pointer;
position: relative;
box-sizing: border-box;
@@ -31,84 +29,21 @@ $mat-select-trigger-underline-height: 1px !default;
}
}

.mat-select-underline {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: $mat-select-trigger-underline-height;

.mat-select:focus & {
height: $mat-select-trigger-underline-height * 2;
}

.mat-select-disabled & {
background-color: transparent;
background-position: 0 bottom;
}
}

.mat-select-placeholder {
position: relative;
padding: 0 2px;
transform-origin: left top;
flex-grow: 1;

// These values are duplicated from animation code in order to
// allow placeholders to sometimes float without animating,
// for example when the value is set programmatically.
// TODO(kara): Change when animations API supports skipping animation.
&.mat-floating-placeholder {
top: -22px;
left: -2px;
text-align: left;
transform: scale(0.75);
}

[dir='rtl'] & {
transform-origin: right top;

&.mat-floating-placeholder {
left: 2px;
text-align: right;
}
}

// TODO: Double-check accessibility of this style
.mat-select-required &::after {
content: ' *';
}
}

.mat-select-value {
position: absolute;
max-width: calc(100% - #{($mat-select-arrow-size + $mat-select-arrow-margin) * 2});
flex-grow: 1;

// Firefox and some versions of IE incorrectly keep absolutely
// positioned children of flex containers in the flex flow when calculating
// position. This has been fixed for Firefox 52, slated for early 2017.
// Bug report: https://bugzilla.mozilla.org/show_bug.cgi?id=874718
//
// In the meantime, we must adjust the position to fit the top, left, and bottom edge of the
// containing trigger element. In doing so, we can use align-items: center to allow the text to
// correctly position itself in the middle of the container.
top: 0;
left: 0;
bottom: 0;

display: flex;
align-items: center;

[dir='rtl'] & {
left: auto;
right: 0;
}
display: table-cell;
max-width: 0;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}

.mat-select-value-text {
@include mat-truncate-line();
line-height: $mat-select-trigger-height;
}

.mat-select-arrow-wrapper {
display: table-cell;
vertical-align: middle;
}

.mat-select-arrow {
@@ -131,3 +66,23 @@ $mat-select-trigger-underline-height: 1px !default;
outline: solid 1px;
}
}

// Override optgroup and option to scale based on font-size of the trigger.
.mat-select-panel {
.mat-optgroup-label,
.mat-option {
font-size: inherit;
line-height: $mat-select-item-height;
height: $mat-select-item-height;
}
}

.mat-form-field-type-mat-select {
.mat-form-field-flex {
cursor: pointer;
}

.mat-form-field-placeholder {
width: calc(100% - #{$mat-select-placeholder-arrow-space});
}
}
846 changes: 470 additions & 376 deletions src/lib/select/select.spec.ts

Large diffs are not rendered by default.

351 changes: 156 additions & 195 deletions src/lib/select/select.ts

Large diffs are not rendered by default.

12 changes: 7 additions & 5 deletions src/material-examples/select-form/select-form-example.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<form>
<md-select placeholder="Favorite food" [(ngModel)]="selectedValue" name="food">
<md-option *ngFor="let food of foods" [value]="food.value">
{{food.viewValue}}
</md-option>
</md-select>
<md-form-field>
<md-select placeholder="Favorite food" [(ngModel)]="selectedValue" name="food">
<md-option *ngFor="let food of foods" [value]="food.value">
{{food.viewValue}}
</md-option>
</md-select>
</md-form-field>

<p> Selected value: {{selectedValue}} </p>
</form>
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<md-select placeholder="Favorite food">
<md-option *ngFor="let food of foods" [value]="food.value">
{{ food.viewValue }}
</md-option>
</md-select>
<md-form-field>
<md-select placeholder="Favorite food">
<md-option *ngFor="let food of foods" [value]="food.value">
{{ food.viewValue }}
</md-option>
</md-select>
</md-form-field>
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<div class="example-tooltip-host" mdTooltip="Tooltip!" [mdTooltipPosition]="position">
<span>Show tooltip</span>
<md-select class="example-select" [(ngModel)]="position">
<md-option value="before">Before</md-option>
<md-option value="after">After</md-option>
<md-option value="above">Above</md-option>
<md-option value="below">Below</md-option>
<md-option value="left">Left</md-option>
<md-option value="right">Right</md-option>
</md-select>
<md-form-field>
<md-select class="example-select" [(ngModel)]="position">
<md-option value="before">Before</md-option>
<md-option value="after">After</md-option>
<md-option value="above">Above</md-option>
<md-option value="below">Below</md-option>
<md-option value="left">Left</md-option>
<md-option value="right">Right</md-option>
</md-select>
</md-form-field>
</div>
12 changes: 7 additions & 5 deletions src/universal-app/kitchen-sink/kitchen-sink.html
Original file line number Diff line number Diff line change
@@ -159,11 +159,13 @@ <h3>Standalone radios</h3>
<md-radio-button name="onions" disabled>Red</md-radio-button>

<h2>Select</h2>
<md-select value="ceramic">
<md-option value="glass">Glass</md-option>
<md-option value="ceramic">Ceramic</md-option>
<md-option value="steel">Steel</md-option>
</md-select>
<md-form-field>
<md-select value="ceramic">
<md-option value="glass">Glass</md-option>
<md-option value="ceramic">Ceramic</md-option>
<md-option value="steel">Steel</md-option>
</md-select>
</md-form-field>

<h2>Sidenav</h2>
<md-sidenav-container>

0 comments on commit d914cc4

Please sign in to comment.