diff --git a/src/demo-app/a11y/select/select-a11y.html b/src/demo-app/a11y/select/select-a11y.html
index 26599933be10..4ca8f71378dd 100644
--- a/src/demo-app/a11y/select/select-a11y.html
+++ b/src/demo-app/a11y/select/select-a11y.html
@@ -2,71 +2,89 @@
diff --git a/src/demo-app/dialog/dialog-demo.html b/src/demo-app/dialog/dialog-demo.html
index 37efd6e028f7..e5d40ad94b34 100644
--- a/src/demo-app/dialog/dialog-demo.html
+++ b/src/demo-app/dialog/dialog-demo.html
@@ -56,11 +56,13 @@ Dialog backdrop
Other options
-
- Start
- End
- Center
-
+
+
+ Start
+ End
+ Center
+
+
diff --git a/src/demo-app/select/select-demo.html b/src/demo-app/select/select-demo.html
index 8fad21eb8a7c..c673924d5933 100644
--- a/src/demo-app/select/select-demo.html
+++ b/src/demo-app/select/select-demo.html
@@ -6,13 +6,18 @@
ngModel
-
- None
-
- {{ drink.viewValue }}
-
-
+
+
+ None
+
+ {{ drink.viewValue }}
+
+
+ local_drink
+ Pick a drink!
+ You must make a selection
+
Value: {{ currentDrink }}
Touched: {{ drinkControl.touched }}
Dirty: {{ drinkControl.dirty }}
@@ -28,7 +33,9 @@
Theme:
- {{ theme.name }}
+
+ {{theme.name}}
+
@@ -42,12 +49,14 @@
Multiple selection
-
-
- {{ creature.viewValue }}
-
-
+
+
+
+ {{ creature.viewValue }}
+
+
+
Value: {{ currentPokemon }}
Touched: {{ pokemonControl.touched }}
Dirty: {{ pokemonControl.dirty }}
@@ -68,12 +77,14 @@
Without Angular forms
-
- None
-
- {{ creature.viewValue }}
-
-
+
+
+ None
+
+ {{ creature.viewValue }}
+
+
+
Value: {{ currentDigimon }}
@@ -85,14 +96,16 @@
Option groups
-
-
-
- {{ creature.viewValue }}
-
-
-
+
+
+
+
+ {{ creature.viewValue }}
+
+
+
+
@@ -100,15 +113,17 @@
compareWith
-
-
- {{ drink.viewValue }}
-
-
+
+
+
+ {{ drink.viewValue }}
+
+
+
Value: {{ currentDrinkObject | json }}
Touched: {{ drinkObjectControl.touched }}
Dirty: {{ drinkObjectControl.dirty }}
@@ -130,9 +145,11 @@
formControl
-
- {{ food.viewValue }}
-
+
+
+ {{ food.viewValue }}
+
+
Value: {{ foodControl.value }}
Touched: {{ foodControl.touched }}
Dirty: {{ foodControl.dirty }}
@@ -149,9 +166,11 @@
Change event
-
- {{ creature.viewValue }}
-
+
+
+ {{ creature.viewValue }}
+
+
Change event value: {{ latestChangeEvent?.value }}
diff --git a/src/demo-app/select/select-demo.scss b/src/demo-app/select/select-demo.scss
index 8380da8d2be2..f87b50cbe96b 100644
--- a/src/demo-app/select/select-demo.scss
+++ b/src/demo-app/select/select-demo.scss
@@ -7,4 +7,8 @@
margin: 24px;
}
+ .demo-drink-icon {
+ vertical-align: bottom;
+ padding-right: 0.25em;
+ }
}
diff --git a/src/demo-app/snack-bar/snack-bar-demo.html b/src/demo-app/snack-bar/snack-bar-demo.html
index 4d0634e59b74..c2f8171c1cc4 100644
--- a/src/demo-app/snack-bar/snack-bar-demo.html
+++ b/src/demo-app/snack-bar/snack-bar-demo.html
@@ -5,17 +5,21 @@ SnackBar demo
Position in page:
-
- Start
- End
- Left
- Right
- Center
-
-
- Top
- Bottom
-
+
+
+ Start
+ End
+ Left
+ Right
+ Center
+
+
+
+
+ Top
+ Bottom
+
+
diff --git a/src/lib/chips/chip-list.ts b/src/lib/chips/chip-list.ts
index caa58d318872..487dceacdf3c 100644
--- a/src/lib/chips/chip-list.ts
+++ b/src/lib/chips/chip-list.ts
@@ -81,6 +81,7 @@ export class MdChipListChange {
})
export class MdChipList implements MdFormFieldControl, 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, 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, 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.
diff --git a/src/lib/core/option/_option.scss b/src/lib/core/option/_option.scss
index bf9e20c2d3f5..109f1ecc3d99 100644
--- a/src/lib/core/option/_option.scss
+++ b/src/lib/core/option/_option.scss
@@ -29,6 +29,14 @@
}
}
+ // Collapses unwanted whitespace created by newlines in code like the following:
+ //
+ // {{value}}
+ //
+ .mat-option-text {
+ display: inline-block;
+ }
+
.mat-option-ripple {
@include mat-fill;
diff --git a/src/lib/core/option/option.html b/src/lib/core/option/option.html
index a9b7b2e63374..7bf76128c4aa 100644
--- a/src/lib/core/option/option.html
+++ b/src/lib/core/option/option.html
@@ -3,7 +3,8 @@
[state]="selected ? 'checked' : ''" [disabled]="disabled">
-
+
+
diff --git a/src/lib/form-field/_form-field-theme.scss b/src/lib/form-field/_form-field-theme.scss
index 9576bfa0e29a..010b6b368ce5 100644
--- a/src/lib/form-field/_form-field-theme.scss
+++ b/src/lib/form-field/_form-field-theme.scss
@@ -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 {
diff --git a/src/lib/form-field/form-field-control.ts b/src/lib/form-field/form-field-control.ts
index 98a6ea02089b..8bbf3b6cf12c 100644
--- a/src/lib/form-field/form-field-control.ts
+++ b/src/lib/form-field/form-field-control.ts
@@ -36,6 +36,9 @@ export abstract class MdFormFieldControl
{
/** 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 {
/** 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;
}
diff --git a/src/lib/form-field/form-field.html b/src/lib/form-field/form-field.html
index 223cb79d0922..4bf46aec7a8a 100644
--- a/src/lib/form-field/form-field.html
+++ b/src/lib/form-field/form-field.html
@@ -1,5 +1,6 @@
-
diff --git a/src/lib/select/_select-theme.scss b/src/lib/select/_select-theme.scss
index de7432ff3c30..685f8f2b82a1 100644
--- a/src/lib/select/_select-theme.scss
+++ b/src/lib/select/_select-theme.scss
@@ -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;
}
}
diff --git a/src/lib/select/public_api.ts b/src/lib/select/public_api.ts
index ff3282c457e2..01ccc2f3e727 100644
--- a/src/lib/select/public_api.ts
+++ b/src/lib/select/public_api.ts
@@ -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';
diff --git a/src/lib/select/select-animations.ts b/src/lib/select/select-animations.ts
index e92230508043..2813519212ef 100644
--- a/src/lib/select/select-animations.ts
+++ b/src/lib/select/select-animations.ts
@@ -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.
*
diff --git a/src/lib/select/select.html b/src/lib/select/select.html
index e3c5823eea05..9379f0980a2e 100644
--- a/src/lib/select/select.html
+++ b/src/lib/select/select.html
@@ -1,26 +1,22 @@
-
-
{{ placeholder }}
-
-
-
+
+
+
+ {{'\xa0'}}
+
{{ triggerValue }}
-
+
-
-
+
+ [class.mat-select-panel-done-animating]="_panelDoneAnimating"
+ [style.font-size.px]="_triggerFontSize">
diff --git a/src/lib/select/select.md b/src/lib/select/select.md
index 787c97235e3b..389060fb44c0 100644
--- a/src/lib/select/select.md
+++ b/src/lib/select/select.md
@@ -1,6 +1,7 @@
`
` is a form control for selecting a value from a set of options, similar to the native
`` 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 `` element.
@@ -12,9 +13,11 @@ binding to it.
*my-comp.html*
```html
-
- {{ state.name }}
-
+
+
+ {{ state.name }}
+
+
```
### 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
-
- {{ state.name }}
-
+
+
+ {{ state.name }}
+
+
```
*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
-
- None
- {{ state.name }}
-
+
+
+ None
+ {{ state.name }}
+
+
```
### 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
-
- {{ state.name }}
-
+
+
+ {{ state.name }}
+
+
```
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
-
- You have selected: {{ select.selected?.viewValue }}
- {{ food.viewValue }}
-
+
+
+ You have selected: {{ select.selected?.viewValue }}
+ {{ food.viewValue }}
+
+
```
Here are the available global options:
diff --git a/src/lib/select/select.scss b/src/lib/select/select.scss
index 1056cef91149..e3eb9f2c6f32 100644
--- a/src/lib/select/select.scss
+++ b/src/lib/select/select.scss
@@ -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});
+ }
+}
diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts
index b774081a9c11..b894b50a1a3c 100644
--- a/src/lib/select/select.spec.ts
+++ b/src/lib/select/select.spec.ts
@@ -1,3 +1,9 @@
+import {Directionality} from '@angular/cdk/bidi';
+import {DOWN_ARROW, END, ENTER, HOME, SPACE, TAB, UP_ARROW} from '@angular/cdk/keycodes';
+import {OverlayContainer} from '@angular/cdk/overlay';
+import {Platform} from '@angular/cdk/platform';
+import {ScrollDispatcher, ViewportRuler} from '@angular/cdk/scrolling';
+import {dispatchFakeEvent, dispatchKeyboardEvent, wrappedErrorMessage} from '@angular/cdk/testing';
import {
ChangeDetectionStrategy,
Component,
@@ -7,6 +13,7 @@ import {
ViewChild,
ViewChildren,
} from '@angular/core';
+import {async, ComponentFixture, fakeAsync, inject, TestBed, tick} from '@angular/core/testing';
import {
ControlValueAccessor,
FormControl,
@@ -17,16 +24,17 @@ import {
ReactiveFormsModule,
Validators,
} from '@angular/forms';
+import {
+ extendObject,
+ FloatPlaceholderType,
+ MD_PLACEHOLDER_GLOBAL_OPTIONS,
+ MdOption
+} from '@angular/material/core';
+import {MdFormFieldModule} from '@angular/material/form-field';
import {By} from '@angular/platform-browser';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
-import {async, ComponentFixture, fakeAsync, inject, TestBed, tick} from '@angular/core/testing';
-import {Directionality} from '@angular/cdk/bidi';
-import {DOWN_ARROW, END, ENTER, HOME, SPACE, TAB, UP_ARROW} from '@angular/cdk/keycodes';
-import {ScrollDispatcher, ViewportRuler} from '@angular/cdk/scrolling';
-import {OverlayContainer} from '@angular/cdk/overlay';
-import {dispatchFakeEvent, dispatchKeyboardEvent, wrappedErrorMessage} from '@angular/cdk/testing';
-import {Subject} from 'rxjs/Subject';
import {map} from 'rxjs/operator/map';
+import {Subject} from 'rxjs/Subject';
import {MdSelectModule} from './index';
import {MdSelect} from './select';
import {
@@ -34,12 +42,6 @@ import {
getMdSelectNonArrayValueError,
getMdSelectNonFunctionValueError
} from './select-errors';
-import {MdOption} from '@angular/material/core';
-import {
- FloatPlaceholderType,
- MD_PLACEHOLDER_GLOBAL_OPTIONS
-} from '@angular/material/core';
-import {extendObject} from '@angular/material/core';
describe('MdSelect', () => {
@@ -50,7 +52,13 @@ describe('MdSelect', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
- imports: [MdSelectModule, ReactiveFormsModule, FormsModule, NoopAnimationsModule],
+ imports: [
+ MdFormFieldModule,
+ MdSelectModule,
+ ReactiveFormsModule,
+ FormsModule,
+ NoopAnimationsModule
+ ],
declarations: [
BasicSelect,
NgModelSelect,
@@ -333,16 +341,19 @@ describe('MdSelect', () => {
describe('selection logic', () => {
let fixture: ComponentFixture;
let trigger: HTMLElement;
+ let formField: HTMLElement;
beforeEach(() => {
fixture = TestBed.createComponent(BasicSelect);
fixture.detectChanges();
trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
+ formField = fixture.debugElement.query(By.css('.mat-form-field')).nativeElement;
});
- it('should display placeholder if no option is selected', () => {
- expect(trigger.textContent!.trim()).toEqual('Food');
+ it('should not float placeholder if no option is selected', () => {
+ expect(formField.classList.contains('mat-form-field-should-float'))
+ .toBe(false, 'placeholder should not be floating');
});
it('should focus the first option if no option is selected', async(() => {
@@ -462,13 +473,10 @@ describe('MdSelect', () => {
fixture.detectChanges();
const value = fixture.debugElement.query(By.css('.mat-select-value')).nativeElement;
- const placeholder =
- fixture.debugElement.query(By.css('.mat-select-placeholder')).nativeElement;
- expect(placeholder.textContent).toContain('Food');
+ expect(formField.classList.contains('mat-form-field-should-float'))
+ .toBe(true, 'placeholder should be floating');
expect(value.textContent).toContain('Steak');
- expect(trigger.textContent).toContain('Food');
- expect(trigger.textContent).toContain('Steak');
});
it('should focus the selected option if an option is selected', async(() => {
@@ -544,6 +552,8 @@ describe('MdSelect', () => {
beforeEach(() => {
fixture = TestBed.createComponent(BasicSelect);
+ fixture.detectChanges();
+ trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
});
it('should take an initial view value with reactive forms', () => {
@@ -565,34 +575,29 @@ describe('MdSelect', () => {
`Expected option with the control's initial value to be selected.`);
});
- beforeEach(() => {
- fixture.detectChanges();
- trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
- });
-
it('should set the view value from the form', () => {
let value = fixture.debugElement.query(By.css('.mat-select-value'));
- expect(value).toBeNull('Expected trigger to start with empty value.');
+ expect(value.nativeElement.textContent.trim()).toBe('');
fixture.componentInstance.control.setValue('pizza-1');
fixture.detectChanges();
value = fixture.debugElement.query(By.css('.mat-select-value'));
expect(value.nativeElement.textContent)
- .toContain('Pizza', `Expected trigger to be populated by the control's new value.`);
+ .toContain('Pizza', `Expected trigger to be populated by the control's new value.`);
trigger.click();
fixture.detectChanges();
const options =
- overlayContainerElement.querySelectorAll('md-option') as NodeListOf;
- expect(options[1].classList)
- .toContain('mat-selected', `Expected option with the control's new value to be selected.`);
+ overlayContainerElement.querySelectorAll('md-option') as NodeListOf;
+ expect(options[1].classList).toContain('mat-selected',
+ `Expected option with the control's new value to be selected.`);
});
it('should update the form value when the view changes', () => {
expect(fixture.componentInstance.control.value)
- .toEqual(null, `Expected the control's value to be null initially.`);
+ .toEqual(null, `Expected the control's value to be empty initially.`);
trigger.click();
fixture.detectChanges();
@@ -613,7 +618,8 @@ describe('MdSelect', () => {
fixture.detectChanges();
const value = fixture.debugElement.query(By.css('.mat-select-value'));
- expect(value).toBe(null, `Expected trigger to be cleared when option value is not found.`);
+ expect(value.nativeElement.textContent.trim())
+ .toBe('', `Expected trigger to be cleared when option value is not found.`);
expect(trigger.textContent)
.not.toContain('Pizza', `Expected trigger to be cleared when option value is not found.`);
@@ -635,7 +641,8 @@ describe('MdSelect', () => {
fixture.detectChanges();
const value = fixture.debugElement.query(By.css('.mat-select-value'));
- expect(value).toBe(null, `Expected trigger to be cleared when option value is not found.`);
+ expect(value.nativeElement.textContent.trim())
+ .toBe('', `Expected trigger to be cleared when option value is not found.`);
expect(trigger.textContent)
.not.toContain('Pizza', `Expected trigger to be cleared when option value is not found.`);
@@ -705,18 +712,16 @@ describe('MdSelect', () => {
it('should set an asterisk after the placeholder if the control is required', () => {
- const placeholder =
- fixture.debugElement.query(By.css('.mat-select-placeholder')).nativeElement;
- const initialContent = getComputedStyle(placeholder, '::after').getPropertyValue('content');
-
- // must support both default cases to work in all browsers in Saucelabs
- expect(initialContent === 'none' || initialContent === '')
- .toBe(true, `Expected placeholder not to have an asterisk, as control was not required.`);
+ let requiredMarker = fixture.debugElement.query(By.css('.mat-form-field-required-marker'));
+ expect(requiredMarker)
+ .toBeNull(`Expected placeholder not to have an asterisk, as control was not required.`);
fixture.componentInstance.isRequired = true;
fixture.detectChanges();
- expect(getComputedStyle(placeholder, '::after').getPropertyValue('content'))
- .toContain('*', `Expected placeholder to have an asterisk, as control was required.`);
+
+ requiredMarker = fixture.debugElement.query(By.css('.mat-form-field-required-marker'));
+ expect(requiredMarker)
+ .not.toBeNull(`Expected placeholder to have an asterisk, as control was required.`);
});
it('should be able to programmatically select a falsy option', () => {
@@ -870,7 +875,6 @@ describe('MdSelect', () => {
});
describe('disabled behavior', () => {
-
it('should disable itself when control is disabled programmatically', () => {
const fixture = TestBed.createComponent(BasicSelect);
fixture.detectChanges();
@@ -952,7 +956,7 @@ describe('MdSelect', () => {
spyOn(fixture.componentInstance.customAccessor, 'writeValue');
fixture.detectChanges();
- expect(fixture.componentInstance.customAccessor.select._control)
+ expect(fixture.componentInstance.customAccessor.select.ngControl)
.toBe(null, 'Expected md-select NOT to inherit control from parent value accessor.');
expect(fixture.componentInstance.customAccessor.writeValue).toHaveBeenCalled();
});
@@ -962,52 +966,32 @@ describe('MdSelect', () => {
describe('animations', () => {
let fixture: ComponentFixture;
let trigger: HTMLElement;
+ let formField: HTMLElement;
beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(BasicSelect);
fixture.detectChanges();
trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
+ formField = fixture.debugElement.query(By.css('.mat-form-field')).nativeElement;
}));
it('should float the placeholder when the panel is open and unselected', () => {
- expect(fixture.componentInstance.select._getPlaceholderAnimationState())
- .toEqual('', 'Expected placeholder to initially have a normal position.');
+ expect(formField.classList.contains('mat-form-field-should-float'))
+ .toBe(false, 'Expected placeholder to initially have a normal position.');
trigger.click();
fixture.detectChanges();
- expect(fixture.componentInstance.select._getPlaceholderAnimationState())
- .toEqual('floating-ltr', 'Expected placeholder to animate up to floating position.');
+ expect(formField.classList.contains('mat-form-field-should-float'))
+ .toBe(true, 'Expected placeholder to animate up to floating position.');
const backdrop =
overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
backdrop.click();
fixture.detectChanges();
- expect(fixture.componentInstance.select._getPlaceholderAnimationState())
- .toEqual('', 'Expected placeholder to animate back down to normal position.');
- });
-
- it('should float the placeholder without animation when value is set', () => {
- fixture.componentInstance.control.setValue('pizza-1');
- fixture.detectChanges();
-
- const placeholderEl =
- fixture.debugElement.query(By.css('.mat-select-placeholder')).nativeElement;
-
- expect(placeholderEl.classList)
- .toContain('mat-floating-placeholder', 'Expected placeholder to display as floating.');
- expect(fixture.componentInstance.select._getPlaceholderAnimationState())
- .toEqual('', 'Expected animation state to be empty to avoid animation.');
- });
-
- it('should use the floating-rtl state when the dir is rtl', () => {
- dir.value = 'rtl';
-
- trigger.click();
- fixture.detectChanges();
- expect(fixture.componentInstance.select._getPlaceholderAnimationState())
- .toEqual('floating-rtl');
+ expect(formField.classList.contains('mat-form-field-should-float'))
+ .toBe(false, 'Expected placeholder to animate back down to normal position.');
});
it('should add a class to the panel when the menu is done animating', fakeAsync(() => {
@@ -1023,19 +1007,20 @@ describe('MdSelect', () => {
expect(panel.classList).toContain('mat-select-panel-done-animating');
}));
-
});
describe('positioning', () => {
let fixture: ComponentFixture;
let trigger: HTMLElement;
let select: HTMLElement;
+ let formField: HTMLElement;
beforeEach(() => {
fixture = TestBed.createComponent(BasicSelect);
fixture.detectChanges();
trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
select = fixture.debugElement.query(By.css('md-select')).nativeElement;
+ formField = fixture.debugElement.query(By.css('md-form-field')).nativeElement;
});
/**
@@ -1051,11 +1036,16 @@ describe('MdSelect', () => {
const overlayTop = overlayPane.getBoundingClientRect().top;
const options = overlayPane.querySelectorAll('md-option');
const optionTop = options[index].getBoundingClientRect().top;
+ const triggerFontSize = parseInt(window.getComputedStyle(trigger)['font-size']);
+ const triggerLineHeightEm = 1.125;
+
+ // Extra trigger height beyond the font size caused by the fact that the line-height is
+ // greater than 1em.
+ const triggerExtraLineSpaceAbove = (1 - triggerLineHeightEm) * triggerFontSize / 2;
- // The option text should align with the trigger text. Because each option is 18px
- // larger in height than the trigger, the option needs to be adjusted up 9 pixels.
expect(Math.floor(optionTop))
- .toEqual(Math.floor(triggerTop - 9), `Expected trigger to align with option ${index}.`);
+ .toBe(Math.floor(triggerTop - triggerFontSize - triggerExtraLineSpaceAbove),
+ `Expected trigger to align with option ${index}.`);
// For the animation to start at the option's center, its origin must be the distance
// from the top of the overlay to the option top + half the option height (48/2 = 24).
@@ -1068,16 +1058,14 @@ describe('MdSelect', () => {
}
describe('ample space to open', () => {
-
beforeEach(() => {
// these styles are necessary because we are first testing the overlay's position
// if there is room for it to open to its full extent in either direction.
- select.style.position = 'fixed';
- select.style.top = '285px';
- select.style.left = '20px';
+ formField.style.position = 'fixed';
+ formField.style.top = '285px';
+ formField.style.left = '20px';
});
-
it('should align the first option with the trigger text if no option is selected', () => {
trigger.click();
fixture.detectChanges();
@@ -1145,7 +1133,7 @@ describe('MdSelect', () => {
checkTriggerAlignedWithOption(7);
});
- it('should account for preceding label groups when aligning the option', () => {
+ it('should account for preceding label groups when aligning the option', async(() => {
fixture.destroy();
let groupFixture = TestBed.createComponent(SelectWithGroups);
@@ -1153,9 +1141,9 @@ describe('MdSelect', () => {
trigger = groupFixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
select = groupFixture.debugElement.query(By.css('md-select')).nativeElement;
- select.style.position = 'fixed';
- select.style.top = '200px';
- select.style.left = '100px';
+ formField.style.position = 'fixed';
+ formField.style.top = '200px';
+ formField.style.left = '100px';
// Select an option in the third group, which has a couple of group labels before it.
groupFixture.componentInstance.control.setValue('vulpix-7');
@@ -1166,29 +1154,59 @@ describe('MdSelect', () => {
const scrollContainer = document.querySelector('.cdk-overlay-pane .mat-select-panel')!;
- // The selected option should be scrolled to the center of the panel.
- // This will be its original offset from the scrollTop - half the panel height + half the
- // option height. 10 (option index + 3 group labels before it) * 48 (option height) = 480px.
- // 480 (offset from scrollTop) - 256/2 + 48/2 = 376px
- expect(Math.floor(scrollContainer.scrollTop))
- .toBe(376, `Expected overlay panel to be scrolled to center the selected option.`);
-
- checkTriggerAlignedWithOption(7, groupFixture.componentInstance.select);
- });
-
+ fixture.whenStable().then(() => {
+ // The selected option should be scrolled to the center of the panel.
+ // This will be its original offset from the scrollTop - half the panel height + half the
+ // option height. 10 (option index + 3 group labels before it) * 48 (option height) =
+ // 480px. 480 (offset from scrollTop) - 256/2 + 48/2 = 376px
+ expect(Math.floor(scrollContainer.scrollTop))
+ .toBe(376, `Expected overlay panel to be scrolled to center the selected option.`);
+
+ checkTriggerAlignedWithOption(7, groupFixture.componentInstance.select);
+ });
+ }));
});
describe('limited space to open vertically', () => {
-
beforeEach(() => {
- select.style.position = 'fixed';
- select.style.left = '20px';
+ formField.style.position = 'fixed';
+ formField.style.left = '20px';
});
- it('should adjust position of centered option if there is little space above', () => {
- // Push the select to a position with not quite enough space on the top to open
- // with the option completely centered (needs 113px at least: 256/2 - 48/2 + 9)
- select.style.top = '85px';
+ it('should adjust position of centered option if there is little space above', async(() => {
+ const selectMenuHeight = 256;
+ const selectMenuViewportPadding = 8;
+ const selectItemHeight = 48;
+ const selectedIndex = 4;
+ const fontSize = 16;
+ const lineHeightEm = 1.125;
+ const expectedExtraScroll = 5;
+
+ // Trigger element height.
+ const triggerHeight = fontSize * lineHeightEm;
+
+ // Ideal space above selected item in order to center it.
+ const idealSpaceAboveSelectedItem = (selectMenuHeight - selectItemHeight) / 2;
+
+ // Actual space above selected item.
+ const actualSpaceAboveSelectedItem = selectItemHeight * selectedIndex;
+
+ // Ideal scroll position to center.
+ const idealScrollTop = actualSpaceAboveSelectedItem - idealSpaceAboveSelectedItem;
+
+ // Top-most select-position that allows for perfect centering.
+ const topMostPositionForPerfectCentering =
+ idealSpaceAboveSelectedItem + selectMenuViewportPadding +
+ (selectItemHeight - triggerHeight) / 2;
+
+ // Position of select relative to top edge of md-form-field.
+ const formFieldTopSpace =
+ trigger.getBoundingClientRect().top - formField.getBoundingClientRect().top;
+
+ const formFieldTop =
+ topMostPositionForPerfectCentering - formFieldTopSpace - expectedExtraScroll;
+
+ formField.style.top = `${formFieldTop}px`;
// Select an option in the middle of the list
fixture.componentInstance.control.setValue('chips-4');
@@ -1199,42 +1217,81 @@ describe('MdSelect', () => {
const scrollContainer = document.querySelector('.cdk-overlay-pane .mat-select-panel')!;
- // Scroll should adjust by the difference between the top space available (85px + 8px
- // viewport padding = 77px) and the height of the panel above the option (113px).
- // 113px - 93px = 20px difference + original scrollTop 88px = 108px
- expect(scrollContainer.scrollTop)
- .toEqual(108, `Expected panel to adjust scroll position to fit in viewport.`);
+ fixture.whenStable().then(() => {
+ expect(scrollContainer.scrollTop)
+ .toEqual(idealScrollTop + 5,
+ `Expected panel to adjust scroll position to fit in viewport.`);
- checkTriggerAlignedWithOption(4);
- });
+ checkTriggerAlignedWithOption(4);
+ });
+ }));
+
+ it('should adjust position of centered option if there is little space below', async(() => {
+ const selectMenuHeight = 256;
+ const selectMenuViewportPadding = 8;
+ const selectItemHeight = 48;
+ const selectedIndex = 4;
+ const fontSize = 16;
+ const lineHeightEm = 1.125;
+ const expectedExtraScroll = 5;
+
+ // Trigger element height.
+ const triggerHeight = fontSize * lineHeightEm;
+
+ // Ideal space above selected item in order to center it.
+ const idealSpaceAboveSelectedItem = (selectMenuHeight - selectItemHeight) / 2;
+
+ // Actual space above selected item.
+ const actualSpaceAboveSelectedItem = selectItemHeight * selectedIndex;
+
+ // Ideal scroll position to center.
+ const idealScrollTop = actualSpaceAboveSelectedItem - idealSpaceAboveSelectedItem;
+
+ // Bottom-most select-position that allows for perfect centering.
+ const bottomMostPositionForPerfectCentering =
+ idealSpaceAboveSelectedItem + selectMenuViewportPadding +
+ (selectItemHeight - triggerHeight) / 2;
+
+ // Position of select relative to bottom edge of md-form-field:
+ const formFieldBottomSpace =
+ formField.getBoundingClientRect().bottom - trigger.getBoundingClientRect().bottom;
+
+ const formFieldBottom =
+ bottomMostPositionForPerfectCentering - formFieldBottomSpace - expectedExtraScroll;
- it('should adjust position of centered option if there is little space below', () => {
// Push the select to a position with not quite enough space on the bottom to open
// with the option completely centered (needs 113px at least: 256/2 - 48/2 + 9)
- select.style.bottom = '56px';
+ formField.style.bottom = `${formFieldBottom}px`;
// Select an option in the middle of the list
fixture.componentInstance.control.setValue('chips-4');
fixture.detectChanges();
- trigger.click();
- fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
- const scrollContainer = document.querySelector('.cdk-overlay-pane .mat-select-panel')!;
+ trigger.click();
+ fixture.detectChanges();
- // Scroll should adjust by the difference between the bottom space available
- // (56px from the bottom of the screen - 8px padding = 48px)
- // and the height of the panel below the option (113px).
- // 113px - 48px = 75px difference. Original scrollTop 88px - 75px = 23px
- expect(Math.ceil(scrollContainer.scrollTop))
- .toEqual(23, `Expected panel to adjust scroll position to fit in viewport.`);
+ const scrollContainer = document.querySelector('.cdk-overlay-pane .mat-select-panel')!;
- checkTriggerAlignedWithOption(4);
- });
+ fixture.whenStable().then(() => {
+ // Scroll should adjust by the difference between the bottom space available
+ // (56px from the bottom of the screen - 8px padding = 48px)
+ // and the height of the panel below the option (113px).
+ // 113px - 48px = 75px difference. Original scrollTop 88px - 75px = 23px
+ expect(scrollContainer.scrollTop)
+ .toEqual(idealScrollTop - expectedExtraScroll,
+ `Expected panel to adjust scroll position to fit in viewport.`);
+
+ checkTriggerAlignedWithOption(4);
+ });
+ });
+ }));
it('should fall back to "above" positioning if scroll adjustment will not help', () => {
// Push the select to a position with not enough space on the bottom to open
- select.style.bottom = '56px';
+ formField.style.bottom = '56px';
fixture.detectChanges();
// Select an option that cannot be scrolled any farther upward
@@ -1262,7 +1319,7 @@ describe('MdSelect', () => {
it('should fall back to "below" positioning if scroll adjustment will not help', () => {
// Push the select to a position with not enough space on the top to open
- select.style.top = '85px';
+ formField.style.top = '85px';
// Select an option that cannot be scrolled any farther downward
fixture.componentInstance.control.setValue('sushi-7');
@@ -1290,12 +1347,12 @@ describe('MdSelect', () => {
describe('limited space to open horizontally', () => {
beforeEach(() => {
- select.style.position = 'absolute';
- select.style.top = '200px';
+ formField.style.position = 'absolute';
+ formField.style.top = '200px';
});
it('should stay within the viewport when overflowing on the left in ltr', fakeAsync(() => {
- select.style.left = '-100px';
+ formField.style.left = '-100px';
trigger.click();
tick(400);
fixture.detectChanges();
@@ -1308,7 +1365,7 @@ describe('MdSelect', () => {
it('should stay within the viewport when overflowing on the left in rtl', fakeAsync(() => {
dir.value = 'rtl';
- select.style.left = '-100px';
+ formField.style.left = '-100px';
trigger.click();
tick(400);
fixture.detectChanges();
@@ -1320,7 +1377,7 @@ describe('MdSelect', () => {
}));
it('should stay within the viewport when overflowing on the right in ltr', fakeAsync(() => {
- select.style.right = '-100px';
+ formField.style.right = '-100px';
trigger.click();
tick(400);
fixture.detectChanges();
@@ -1335,7 +1392,7 @@ describe('MdSelect', () => {
it('should stay within the viewport when overflowing on the right in rtl', fakeAsync(() => {
dir.value = 'rtl';
- select.style.right = '-100px';
+ formField.style.right = '-100px';
trigger.click();
tick(400);
fixture.detectChanges();
@@ -1349,7 +1406,7 @@ describe('MdSelect', () => {
}));
it('should keep the position within the viewport on repeat openings', async(() => {
- select.style.left = '-100px';
+ formField.style.left = '-100px';
trigger.click();
fixture.detectChanges();
@@ -1369,7 +1426,6 @@ describe('MdSelect', () => {
`Expected select panel continue being inside the viewport.`);
});
}));
-
});
describe('when scrolled', () => {
@@ -1389,8 +1445,8 @@ describe('MdSelect', () => {
setScrollTop(0);
// Give the select enough horizontal space to open
- select.style.marginLeft = '20px';
- select.style.marginRight = '20px';
+ formField.style.marginLeft = '20px';
+ formField.style.marginRight = '20px';
});
it('should align the first option properly when scrolled', () => {
@@ -1493,14 +1549,12 @@ describe('MdSelect', () => {
expect(Math.floor(overlayTop))
.toEqual(Math.floor(triggerTop), `Expected trigger top to align with overlay top.`);
});
-
});
describe('x-axis positioning', () => {
-
beforeEach(() => {
- select.style.position = 'fixed';
- select.style.left = '30px';
+ formField.style.position = 'fixed';
+ formField.style.left = '30px';
});
it('should align the trigger and the selected option on the x-axis in ltr', fakeAsync(() => {
@@ -1544,11 +1598,12 @@ describe('MdSelect', () => {
beforeEach(() => {
multiFixture = TestBed.createComponent(MultiSelect);
multiFixture.detectChanges();
+ formField = multiFixture.debugElement.query(By.css('.mat-form-field')).nativeElement;
trigger = multiFixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
select = multiFixture.debugElement.query(By.css('md-select')).nativeElement;
- select.style.position = 'fixed';
- select.style.left = '60px';
+ formField.style.position = 'fixed';
+ formField.style.left = '60px';
});
it('should adjust for the checkbox in ltr', async(() => {
@@ -1560,9 +1615,9 @@ describe('MdSelect', () => {
const firstOptionLeft =
document.querySelector('.cdk-overlay-pane md-option')!.getBoundingClientRect().left;
- // 48px accounts for the checkbox size, margin and the panel's padding.
+ // 44px accounts for the checkbox size, margin and the panel's padding.
expect(Math.floor(firstOptionLeft))
- .toEqual(Math.floor(triggerLeft - 48),
+ .toEqual(Math.floor(triggerLeft - 44),
`Expected trigger label to align along x-axis, accounting for the checkbox.`);
});
}));
@@ -1577,9 +1632,9 @@ describe('MdSelect', () => {
const firstOptionRight =
document.querySelector('.cdk-overlay-pane md-option')!.getBoundingClientRect().right;
- // 48px accounts for the checkbox size, margin and the panel's padding.
+ // 44px accounts for the checkbox size, margin and the panel's padding.
expect(Math.floor(firstOptionRight))
- .toEqual(Math.floor(triggerRight + 48),
+ .toEqual(Math.floor(triggerRight + 44),
`Expected trigger label to align along x-axis, accounting for the checkbox.`);
}));
});
@@ -1590,11 +1645,12 @@ describe('MdSelect', () => {
beforeEach(() => {
groupFixture = TestBed.createComponent(SelectWithGroups);
groupFixture.detectChanges();
+ formField = groupFixture.debugElement.query(By.css('.mat-form-field')).nativeElement;
trigger = groupFixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
select = groupFixture.debugElement.query(By.css('md-select')).nativeElement;
- select.style.position = 'fixed';
- select.style.left = '60px';
+ formField.style.position = 'fixed';
+ formField.style.left = '60px';
});
it('should adjust for the group padding in ltr', fakeAsync(() => {
@@ -1648,26 +1704,42 @@ describe('MdSelect', () => {
expect(Math.floor(selectedOptionLeft)).toEqual(Math.floor(triggerLeft - 16));
}));
- it('should align the first option to the trigger, if nothing is selected', fakeAsync(() => {
- trigger.click();
- groupFixture.detectChanges();
+ it('should align the first option to the trigger, if nothing is selected', async(() => {
+ // Push down the form field so there is space for the item to completely align.
+ formField.style.top = '100px';
- const triggerTop = trigger.getBoundingClientRect().top;
+ const menuItemHeight = 48;
+ const triggerFontSize = 16;
+ const triggerLineHeightEm = 1.125;
+ const triggerHeight = triggerFontSize * triggerLineHeightEm;
- const option = overlayContainerElement.querySelector('.cdk-overlay-pane md-option');
- const optionTop = option ? option.getBoundingClientRect().top : 0;
+ trigger.click();
+ groupFixture.detectChanges();
- // Since the option is 18px higher than the trigger, it needs to be adjusted by 9px.
- expect(Math.floor(optionTop))
- .toBe(Math.floor(triggerTop - 9), 'Expected trigger to align with the first option.');
+ fixture.whenStable().then(() => {
+ const triggerTop = trigger.getBoundingClientRect().top;
+
+ const option = overlayContainerElement.querySelector('.cdk-overlay-pane md-option');
+ const optionTop = option ? option.getBoundingClientRect().top : 0;
+
+ // There appears to be a small rounding error on IE, so we verify that the value is close,
+ // not exact.
+ let platform = new Platform();
+ if (platform.TRIDENT) {
+ let difference =
+ Math.abs(optionTop + (menuItemHeight - triggerHeight) / 2 - triggerTop);
+ expect(difference)
+ .toBeLessThan(0.1, 'Expected trigger to align with the first option.');
+ } else {
+ expect(optionTop + (menuItemHeight - triggerHeight) / 2)
+ .toBe(triggerTop, 'Expected trigger to align with the first option.');
+ }
+ });
}));
-
});
-
});
describe('accessibility', () => {
-
describe('for select', () => {
let fixture: ComponentFixture;
let select: HTMLElement;
@@ -2124,13 +2196,10 @@ describe('MdSelect', () => {
});
}));
-
});
-
});
describe('special cases', () => {
-
it('should handle nesting in an ngIf', async(() => {
const fixture = TestBed.createComponent(NgIfSelect);
fixture.detectChanges();
@@ -2185,7 +2254,6 @@ describe('MdSelect', () => {
expect(() => fixture.detectChanges()).not.toThrow();
}));
-
it('should not throw when the triggerValue is accessed when there is no selected value', () => {
const fixture = TestBed.createComponent(BasicSelect);
fixture.detectChanges();
@@ -2229,25 +2297,26 @@ describe('MdSelect', () => {
describe('floatPlaceholder option', () => {
let fixture: ComponentFixture;
+ let formField: HTMLElement;
beforeEach(() => {
fixture = TestBed.createComponent(FloatPlaceholderSelect);
+ fixture.detectChanges();
+ formField = fixture.debugElement.query(By.css('.mat-form-field')).nativeElement;
});
it('should be able to disable the floating placeholder', () => {
- let placeholder = fixture.debugElement.query(By.css('.mat-select-placeholder')).nativeElement;
-
fixture.componentInstance.floatPlaceholder = 'never';
fixture.detectChanges();
- expect(placeholder.style.opacity).toBe('1');
- expect(fixture.componentInstance.select._getPlaceholderAnimationState()).toBeFalsy();
+ expect(formField.classList.contains('mat-form-field-can-float'))
+ .toBe(false, 'Floating placeholder should be disabled');
fixture.componentInstance.control.setValue('pizza-1');
fixture.detectChanges();
- expect(placeholder.style.opacity).toBe('0');
- expect(fixture.componentInstance.select._getPlaceholderAnimationState()).toBeFalsy();
+ expect(formField.classList.contains('mat-form-field-can-float'))
+ .toBe(false, 'Floating placeholder should be disabled');
});
it('should be able to always float the placeholder', () => {
@@ -2256,7 +2325,10 @@ describe('MdSelect', () => {
fixture.componentInstance.floatPlaceholder = 'always';
fixture.detectChanges();
- expect(fixture.componentInstance.select._getPlaceholderAnimationState()).toBe('floating-ltr');
+ expect(formField.classList.contains('mat-form-field-can-float'))
+ .toBe(true, 'Placeholder should be able to float');
+ expect(formField.classList.contains('mat-form-field-should-float'))
+ .toBe(true, 'Placeholder should be floating');
});
it ('should default to global floating placeholder type', () => {
@@ -2265,6 +2337,7 @@ describe('MdSelect', () => {
TestBed.resetTestingModule();
TestBed.configureTestingModule({
imports: [
+ MdFormFieldModule,
MdSelectModule,
ReactiveFormsModule,
FormsModule,
@@ -2279,8 +2352,12 @@ describe('MdSelect', () => {
fixture = TestBed.createComponent(FloatPlaceholderSelect);
fixture.componentInstance.floatPlaceholder = null;
fixture.detectChanges();
+ formField = fixture.debugElement.query(By.css('.mat-form-field')).nativeElement;
- expect(fixture.componentInstance.select.floatPlaceholder).toBe('always');
+ expect(formField.classList.contains('mat-form-field-can-float'))
+ .toBe(true, 'Placeholder should be able to float');
+ expect(formField.classList.contains('mat-form-field-should-float'))
+ .toBe(true, 'Placeholder should be floating');
});
});
@@ -2362,25 +2439,29 @@ describe('MdSelect', () => {
expect(testInstance.control.value).toEqual([]);
});
- it('should update the label', () => {
+ it('should update the label', async(() => {
trigger.click();
fixture.detectChanges();
- const options = overlayContainerElement.querySelectorAll('md-option') as
- NodeListOf;
+ fixture.whenStable().then(() => {
+ const options = overlayContainerElement.querySelectorAll('md-option') as
+ NodeListOf;
- options[0].click();
- options[2].click();
- options[5].click();
- fixture.detectChanges();
+ options[0].click();
+ options[2].click();
+ options[5].click();
+ fixture.detectChanges();
- expect(trigger.textContent).toContain('Steak, Tacos, Eggs');
+ expect(trigger.textContent).toContain('Steak, Tacos, Eggs');
- options[2].click();
- fixture.detectChanges();
+ options[2].click();
+ fixture.detectChanges();
- expect(trigger.textContent).toContain('Steak, Eggs');
- });
+ fixture.whenStable().then(() => {
+ expect(trigger.textContent).toContain('Steak, Eggs');
+ });
+ });
+ }));
it('should be able to set the selected value by taking an array', () => {
trigger.click();
@@ -2434,38 +2515,42 @@ describe('MdSelect', () => {
expect(testInstance.select.panelOpen).toBe(true);
});
- it('should sort the selected options based on their order in the panel', () => {
+ it('should sort the selected options based on their order in the panel', async(() => {
trigger.click();
fixture.detectChanges();
- const options = overlayContainerElement.querySelectorAll('md-option') as
- NodeListOf;
+ fixture.whenStable().then(() => {
+ const options = overlayContainerElement.querySelectorAll('md-option') as
+ NodeListOf;
- options[2].click();
- options[0].click();
- options[1].click();
- fixture.detectChanges();
+ options[2].click();
+ options[0].click();
+ options[1].click();
+ fixture.detectChanges();
- expect(trigger.textContent).toContain('Steak, Pizza, Tacos');
- expect(fixture.componentInstance.control.value).toEqual(['steak-0', 'pizza-1', 'tacos-2']);
- });
+ expect(trigger.textContent).toContain('Steak, Pizza, Tacos');
+ expect(fixture.componentInstance.control.value).toEqual(['steak-0', 'pizza-1', 'tacos-2']);
+ });
+ }));
- it('should sort the selected options in reverse in rtl', () => {
+ it('should sort the selected options in reverse in rtl', async(() => {
dir.value = 'rtl';
trigger.click();
fixture.detectChanges();
- const options = overlayContainerElement.querySelectorAll('md-option') as
- NodeListOf;
+ fixture.whenStable().then(() => {
+ const options = overlayContainerElement.querySelectorAll('md-option') as
+ NodeListOf;
- options[2].click();
- options[0].click();
- options[1].click();
- fixture.detectChanges();
+ options[2].click();
+ options[0].click();
+ options[1].click();
+ fixture.detectChanges();
- expect(trigger.textContent).toContain('Tacos, Pizza, Steak');
- expect(fixture.componentInstance.control.value).toEqual(['steak-0', 'pizza-1', 'tacos-2']);
- });
+ expect(trigger.textContent).toContain('Tacos, Pizza, Steak');
+ expect(fixture.componentInstance.control.value).toEqual(['steak-0', 'pizza-1', 'tacos-2']);
+ });
+ }));
it('should sort the values, that get set via the model, based on the panel order', () => {
trigger.click();
@@ -2533,40 +2618,6 @@ describe('MdSelect', () => {
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');
- });
-
it('should allow the user to customize the label', () => {
fixture.destroy();
@@ -2581,28 +2632,30 @@ describe('MdSelect', () => {
expect(label.textContent).toContain('azziP',
'Expected the displayed text to be "Pizza" in reverse.');
});
-
});
describe('reset values', () => {
let fixture: ComponentFixture;
let trigger: HTMLElement;
- let placeholder: HTMLElement;
+ let formField: HTMLElement;
let options: NodeListOf;
- beforeEach(() => {
+ beforeEach(async(() => {
fixture = TestBed.createComponent(ResetValuesSelect);
fixture.detectChanges();
trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
- placeholder = fixture.debugElement.query(By.css('.mat-select-placeholder')).nativeElement;
+ formField = fixture.debugElement.query(By.css('.mat-form-field')).nativeElement;
trigger.click();
fixture.detectChanges();
- options = overlayContainerElement.querySelectorAll('md-option') as NodeListOf;
- options[0].click();
- fixture.detectChanges();
- });
+ fixture.whenStable().then(() => {
+ options = overlayContainerElement.querySelectorAll('md-option') as NodeListOf;
+
+ options[0].click();
+ fixture.detectChanges();
+ });
+ }));
it('should reset when an option with an undefined value is selected', () => {
options[4].click();
@@ -2610,7 +2663,7 @@ describe('MdSelect', () => {
expect(fixture.componentInstance.control.value).toBeUndefined();
expect(fixture.componentInstance.select.selected).toBeFalsy();
- expect(placeholder.classList).not.toContain('mat-floating-placeholder');
+ expect(formField.classList).not.toContain('mat-form-field-should-float');
expect(trigger.textContent).not.toContain('Undefined');
});
@@ -2620,7 +2673,7 @@ describe('MdSelect', () => {
expect(fixture.componentInstance.control.value).toBeNull();
expect(fixture.componentInstance.select.selected).toBeFalsy();
- expect(placeholder.classList).not.toContain('mat-floating-placeholder');
+ expect(formField.classList).not.toContain('mat-form-field-should-float');
expect(trigger.textContent).not.toContain('Null');
});
@@ -2630,7 +2683,7 @@ describe('MdSelect', () => {
expect(fixture.componentInstance.control.value).toBeUndefined();
expect(fixture.componentInstance.select.selected).toBeFalsy();
- expect(placeholder.classList).not.toContain('mat-floating-placeholder');
+ expect(formField.classList).not.toContain('mat-form-field-should-float');
expect(trigger.textContent).not.toContain('None');
});
@@ -2640,19 +2693,19 @@ describe('MdSelect', () => {
expect(fixture.componentInstance.control.value).toBe(false);
expect(fixture.componentInstance.select.selected).toBeTruthy();
- expect(placeholder.classList).toContain('mat-floating-placeholder');
+ expect(formField.classList).toContain('mat-form-field-should-float');
expect(trigger.textContent).toContain('Falsy');
});
it('should not consider the reset values as selected when resetting the form control', () => {
- expect(placeholder.classList).toContain('mat-floating-placeholder');
+ expect(formField.classList).toContain('mat-form-field-should-float');
fixture.componentInstance.control.reset();
fixture.detectChanges();
expect(fixture.componentInstance.control.value).toBeNull();
expect(fixture.componentInstance.select.selected).toBeFalsy();
- expect(placeholder.classList).not.toContain('mat-floating-placeholder');
+ expect(formField.classList).not.toContain('mat-formf-field-should-float');
expect(trigger.textContent).not.toContain('Null');
expect(trigger.textContent).not.toContain('Undefined');
});
@@ -2727,7 +2780,6 @@ describe('MdSelect', () => {
expect(select.getAttribute('aria-invalid'))
.toBe('true', 'Expected aria-invalid to be set to true.');
});
-
});
describe('compareWith behavior', () => {
@@ -2796,24 +2848,23 @@ describe('MdSelect', () => {
fixture.detectChanges();
}).toThrowError(wrappedErrorMessage(getMdSelectNonFunctionValueError()));
});
-
});
-
});
-
});
@Component({
selector: 'basic-select',
template: `
-
-
- {{ food.viewValue }}
-
-
+
+
+
+ {{ food.viewValue }}
+
+
+
`
})
@@ -2845,9 +2896,11 @@ class BasicSelect {
@Component({
selector: 'ng-model-select',
template: `
-
- {{ food.viewValue }}
-
+
+
+ {{ food.viewValue }}
+
+
`
})
class NgModelSelect {
@@ -2865,14 +2918,18 @@ class NgModelSelect {
@Component({
selector: 'many-selects',
template: `
-
- one
- two
-
-
- three
- four
-
+
+
+ one
+ two
+
+
+
+
+ three
+ four
+
+
`
})
class ManySelects {}
@@ -2881,11 +2938,13 @@ class ManySelects {}
selector: 'ng-if-select',
template: `
-
-
- {{ food.viewValue }}
-
-
+
+
+
+ {{ food.viewValue }}
+
+
+
`,
})
@@ -2904,9 +2963,11 @@ class NgIfSelect {
@Component({
selector: 'select-with-change-event',
template: `
-
- {{ food }}
-
+
+
+ {{ food }}
+
+
`
})
class SelectWithChangeEvent {
@@ -2927,11 +2988,13 @@ class SelectWithChangeEvent {
@Component({
selector: 'select-init-without-options',
template: `
-
-
- {{ food.viewValue }}
-
-
+
+
+
+ {{ food.viewValue }}
+
+
+
`
})
class SelectInitWithoutOptions {
@@ -2952,9 +3015,7 @@ class SelectInitWithoutOptions {
@Component({
selector: 'custom-select-accessor',
- template: `
-
- `,
+ template: ` `,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: CustomSelectAccessor,
@@ -2971,10 +3032,7 @@ class CustomSelectAccessor implements ControlValueAccessor {
@Component({
selector: 'comp-with-custom-select',
- template: `
-
-
- `,
+ template: ` `,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: CustomSelectAccessor,
@@ -2989,7 +3047,9 @@ class CompWithCustomSelect {
@Component({
selector: 'select-infinite-loop',
template: `
-
+
+
+
`
})
@@ -3011,11 +3071,13 @@ export class ThrowsErrorOnInit implements OnInit {
selector: 'basic-select-on-push',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
-
-
- {{ food.viewValue }}
-
-
+
+
+
+ {{ food.viewValue }}
+
+
+
`
})
class BasicSelectOnPush {
@@ -3031,11 +3093,13 @@ class BasicSelectOnPush {
selector: 'basic-select-on-push-preselected',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
-
-
- {{ food.viewValue }}
-
-
+
+
+
+ {{ food.viewValue }}
+
+
+
`
})
class BasicSelectOnPushPreselected {
@@ -3050,12 +3114,13 @@ class BasicSelectOnPushPreselected {
@Component({
selector: 'floating-placeholder-select',
template: `
-
+
+
{{ food.viewValue }}
+
`,
})
class FloatPlaceholderSelect {
@@ -3073,9 +3138,11 @@ class FloatPlaceholderSelect {
@Component({
selector: 'multi-select',
template: `
-
- {{ food.viewValue }}
-
+
+
+ {{ food.viewValue }}
+
+
`
})
class MultiSelect {
@@ -3097,16 +3164,16 @@ class MultiSelect {
@Component({
selector: 'select-with-plain-tabindex',
- template: `
-
- `
+ template: ` `
})
class SelectWithPlainTabindex { }
@Component({
selector: 'select-early-sibling-access',
template: `
-
+
+
+
`
})
@@ -3115,9 +3182,11 @@ class SelectEarlyAccessSibling { }
@Component({
selector: 'basic-select-initially-hidden',
template: `
-
- There are no other options
-
+
+
+ There are no other options
+
+
`
})
class BasicSelectInitiallyHidden {
@@ -3127,9 +3196,11 @@ class BasicSelectInitiallyHidden {
@Component({
selector: 'basic-select-no-placeholder',
template: `
-
- There are no other options
-
+
+
+ There are no other options
+
+
`
})
class BasicSelectNoPlaceholder { }
@@ -3137,10 +3208,12 @@ class BasicSelectNoPlaceholder { }
@Component({
selector: 'basic-select-with-theming',
template: `
-
- Steak
- Pizza
-
+
+
+ Steak
+ Pizza
+
+
`
})
class BasicSelectWithTheming {
@@ -3152,13 +3225,14 @@ class BasicSelectWithTheming {
@Component({
selector: 'reset-values-select',
template: `
-
-
- {{ food.viewValue }}
-
-
- None
-
+
+
+
+ {{ food.viewValue }}
+
+ None
+
+
`
})
class ResetValuesSelect {
@@ -3177,9 +3251,11 @@ class ResetValuesSelect {
@Component({
template: `
-
- {{ food.viewValue }}
-
+
+
+ {{ food.viewValue }}
+
+
`
})
class FalsyValueSelect {
@@ -3195,17 +3271,17 @@ class FalsyValueSelect {
@Component({
selector: 'select-with-groups',
template: `
-
-
-
-
- {{ pokemon.viewValue }}
-
-
-
- Mr. Mime
-
+
+
+
+
+ {{ pokemon.viewValue }}
+
+
+ Mr. Mime
+
+
`
})
class SelectWithGroups {
@@ -3251,7 +3327,13 @@ class SelectWithGroups {
@Component({
- template: ``
+ template: `
+
+ `
})
class InvalidSelectInForm {
value: any;
@@ -3261,10 +3343,12 @@ class InvalidSelectInForm {
@Component({
template: `
`
})
@@ -3279,11 +3363,13 @@ class SelectInsideFormGroup {
@Component({
template: `
-
-
- {{ food.viewValue }}
-
-
+
+
+
+ {{ food.viewValue }}
+
+
+
`
})
class BasicSelectWithoutForms {
@@ -3299,11 +3385,13 @@ class BasicSelectWithoutForms {
@Component({
template: `
-
-
- {{ food.viewValue }}
-
-
+
+
+
+ {{ food.viewValue }}
+
+
+
`
})
class BasicSelectWithoutFormsPreselected {
@@ -3318,11 +3406,13 @@ class BasicSelectWithoutFormsPreselected {
@Component({
template: `
-
-
- {{ food.viewValue }}
-
-
+
+
+
+ {{ food.viewValue }}
+
+
+
`
})
class BasicSelectWithoutFormsMultiple {
@@ -3340,14 +3430,16 @@ class BasicSelectWithoutFormsMultiple {
@Component({
selector: 'select-with-custom-trigger',
template: `
-
-
- {{ select.selected?.viewValue.split('').reverse().join('') }}
-
-
- {{ food.viewValue }}
-
-
+
+
+
+ {{ select.selected?.viewValue.split('').reverse().join('') }}
+
+
+ {{ food.viewValue }}
+
+
+
`
})
class SelectWithCustomTrigger {
@@ -3362,10 +3454,12 @@ class SelectWithCustomTrigger {
@Component({
selector: 'ng-model-compare-with',
template: `
-
- {{ food.viewValue }}
-
+
+
+ {{ food.viewValue }}
+
+
`
})
class NgModelCompareWithSelect {
diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts
index 8601e3066dcb..1b164472f319 100644
--- a/src/lib/select/select.ts
+++ b/src/lib/select/select.ts
@@ -18,8 +18,7 @@ import {
ScrollStrategy,
ViewportRuler,
} from '@angular/cdk/overlay';
-import {Platform} from '@angular/cdk/platform';
-import {filter, startWith} from '@angular/cdk/rxjs';
+import {filter, first, startWith} from '@angular/cdk/rxjs';
import {
AfterContentInit,
Attribute,
@@ -35,6 +34,7 @@ import {
InjectionToken,
Input,
isDevMode,
+ NgZone,
OnDestroy,
OnInit,
Optional,
@@ -47,76 +47,56 @@ import {
} from '@angular/core';
import {ControlValueAccessor, FormGroupDirective, NgControl, NgForm} from '@angular/forms';
import {
- CanColor,
CanDisable,
- FloatPlaceholderType,
HasTabIndex,
- MD_PLACEHOLDER_GLOBAL_OPTIONS,
MdOptgroup,
MdOption,
MdOptionSelectionChange,
- mixinColor,
mixinDisabled,
mixinTabIndex,
- PlaceholderOptions,
} from '@angular/material/core';
+import {MdFormFieldControl} from '@angular/material/form-field';
import {Observable} from 'rxjs/Observable';
import {merge} from 'rxjs/observable/merge';
+import {Subject} from 'rxjs/Subject';
import {Subscription} from 'rxjs/Subscription';
-import {fadeInContent, transformPanel, transformPlaceholder} from './select-animations';
+import {fadeInContent, transformPanel} from './select-animations';
import {
getMdSelectDynamicMultipleError,
getMdSelectNonArrayValueError,
getMdSelectNonFunctionValueError,
} from './select-errors';
+
+let nextUniqueId = 0;
+
/**
* The following style constants are necessary to save here in order
* to properly calculate the alignment of the selected option over
* the trigger element.
*/
-/** The fixed height of every option element (option, group header etc.). */
-export const SELECT_ITEM_HEIGHT = 48;
-
/** The max height of the select's overlay panel */
export const SELECT_PANEL_MAX_HEIGHT = 256;
-/** The max number of options visible at once in the select panel. */
-export const SELECT_MAX_OPTIONS_DISPLAYED =
- Math.floor(SELECT_PANEL_MAX_HEIGHT / SELECT_ITEM_HEIGHT);
-
-/** The fixed height of the select's trigger element. */
-export const SELECT_TRIGGER_HEIGHT = 30;
-
-/**
- * Must adjust for the difference in height between the option and the trigger,
- * so the text will align on the y axis.
- */
-export const SELECT_OPTION_HEIGHT_ADJUSTMENT = (SELECT_ITEM_HEIGHT - SELECT_TRIGGER_HEIGHT) / 2;
-
/** The panel's padding on the x-axis */
export const SELECT_PANEL_PADDING_X = 16;
/** The panel's x axis padding if it is indented (e.g. there is an option group). */
export const SELECT_PANEL_INDENT_PADDING_X = SELECT_PANEL_PADDING_X * 2;
+/** The height of the select items in `em` units. */
+export const SELECT_ITEM_HEIGHT_EM = 3;
+
/**
* Distance between the panel edge and the option text in
* multi-selection mode.
*
- * (SELECT_PADDING * 1.75) + 20 = 48
- * The padding is multiplied by 1.75 because the checkbox's margin is half the padding, and
- * the browser adds ~4px, because we're using inline elements.
+ * (SELECT_PANEL_PADDING_X * 1.5) + 20 = 44
+ * The padding is multiplied by 1.5 because the checkbox's margin is half the padding.
* The checkbox width is 20px.
*/
-export const SELECT_MULTIPLE_PANEL_PADDING_X = SELECT_PANEL_PADDING_X * 1.75 + 20;
-
-/**
- * The panel's padding on the y-axis. This padding indicates there are more
- * options available if you scroll.
- */
-export const SELECT_PANEL_PADDING_Y = 16;
+export const SELECT_MULTIPLE_PANEL_PADDING_X = SELECT_PANEL_PADDING_X * 1.5 + 20;
/**
* The select panel will only "fit" inside the viewport if it is positioned at
@@ -124,13 +104,6 @@ export const SELECT_PANEL_PADDING_Y = 16;
*/
export const SELECT_PANEL_VIEWPORT_PADDING = 8;
-/**
- * Default minimum width of the trigger based on the CSS.
- * Used as a fallback for server-side rendering.
- * @docs-private
- */
-const SELECT_TRIGGER_MIN_WIDTH = 112;
-
/** Injection token that determines the scroll handling while a select is open. */
export const MD_SELECT_SCROLL_STRATEGY =
new InjectionToken<() => ScrollStrategy>('md-select-scroll-strategy');
@@ -158,8 +131,7 @@ export class MdSelectChange {
export class MdSelectBase {
constructor(public _renderer: Renderer2, public _elementRef: ElementRef) {}
}
-export const _MdSelectMixinBase =
- mixinTabIndex(mixinColor(mixinDisabled(MdSelectBase), 'primary'));
+export const _MdSelectMixinBase = mixinTabIndex(mixinDisabled(MdSelectBase));
/**
@@ -176,36 +148,39 @@ export class MdSelectTrigger {}
selector: 'md-select, mat-select',
templateUrl: 'select.html',
styleUrls: ['select.css'],
- inputs: ['color', 'disabled', 'tabIndex'],
+ inputs: ['disabled', 'tabIndex'],
encapsulation: ViewEncapsulation.None,
preserveWhitespaces: false,
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'role': 'listbox',
+ '[attr.id]': 'id',
'[attr.tabindex]': 'tabIndex',
'[attr.aria-label]': '_ariaLabel',
'[attr.aria-labelledby]': 'ariaLabelledby',
'[attr.aria-required]': 'required.toString()',
'[attr.aria-disabled]': 'disabled.toString()',
- '[attr.aria-invalid]': '_isErrorState()',
+ '[attr.aria-invalid]': 'errorState',
'[attr.aria-owns]': '_optionIds',
'[attr.aria-multiselectable]': 'multiple',
+ '[attr.aria-describedby]': '_ariaDescribedby || null',
'[class.mat-select-disabled]': 'disabled',
- '[class.mat-select-invalid]': '_isErrorState()',
+ '[class.mat-select-invalid]': 'errorState',
'[class.mat-select-required]': 'required',
'class': 'mat-select',
'(keydown)': '_handleClosedKeydown($event)',
+ '(focus)': '_onFocus()',
'(blur)': '_onBlur()',
},
animations: [
- transformPlaceholder,
transformPanel,
fadeInContent
],
+ providers: [{provide: MdFormFieldControl, useExisting: MdSelect}],
exportAs: 'mdSelect, matSelect',
})
export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, OnDestroy, OnInit,
- ControlValueAccessor, CanColor, CanDisable, HasTabIndex {
+ ControlValueAccessor, CanDisable, HasTabIndex, MdFormFieldControl {
/** Whether or not the overlay panel is open. */
private _panelOpen = false;
@@ -233,30 +208,24 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
/** Comparison function to specify which option is displayed. Defaults to object equality. */
private _compareWith = (o1: any, o2: any) => o1 === o2;
- /** Deals with the selection logic. */
- _selectionModel: SelectionModel;
+ /** Unique id for this input. */
+ private _uid = `mat-select-${nextUniqueId++}`;
- /** The animation state of the placeholder. */
- private _placeholderState = '';
+ /** The last measured value for the trigger's client bounding rect. */
+ _triggerRect: ClientRect;
- /** Deals with configuring placeholder options */
- private _placeholderOptions: PlaceholderOptions;
+ /** The aria-describedby attribute on the select for improved a11y. */
+ _ariaDescribedby: 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.
- */
- _triggerWidth: number;
+ /** The cached font-size of the trigger element. */
+ _triggerFontSize = 0;
+
+ /** Deals with the selection logic. */
+ _selectionModel: SelectionModel;
/** Manages keyboard events for options in the panel. */
_keyManager: FocusKeyManager;
- /**
- * The width of the selected option's value. Must be set programmatically
- * to ensure its overflow is clipped, as it's absolutely positioned.
- */
- _selectedValueWidth: number;
-
/** View -> model callback called when value changes */
_onChange: (value: any) => void = () => {};
@@ -303,6 +272,18 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
},
];
+ /**
+ * Stream that emits whenever the state of the select changes such that the wrapping `MdFormField`
+ * needs to run change detection.
+ */
+ stateChanges = new Subject();
+
+ /** Whether the select is focused. */
+ focused = false;
+
+ /** A name for this control that can be used by `md-form-field`. */
+ controlType = 'mat-select';
+
/** Trigger that opens the select. */
@ViewChild('trigger') trigger: ElementRef;
@@ -326,15 +307,16 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
get placeholder() { return this._placeholder; }
set placeholder(value: string) {
this._placeholder = value;
-
- // Must wait to record the trigger width to ensure placeholder width is included.
- Promise.resolve(null).then(() => this._setTriggerWidth());
+ this.stateChanges.next();
}
/** Whether the component is required. */
@Input()
get required() { return this._required; }
- set required(value: any) { this._required = coerceBooleanProperty(value); }
+ set required(value: any) {
+ this._required = coerceBooleanProperty(value);
+ this.stateChanges.next();
+ }
/** Whether the user should be allowed to select multiple options. */
@Input()
@@ -365,14 +347,6 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
}
}
- /** Whether to float the placeholder text. */
- @Input()
- get floatPlaceholder(): FloatPlaceholderType { return this._floatPlaceholder; }
- set floatPlaceholder(value: FloatPlaceholderType) {
- this._floatPlaceholder = value || this._placeholderOptions.float || 'auto';
- }
- private _floatPlaceholder: FloatPlaceholderType;
-
/** Value of the select control. */
@Input()
get value() { return this._value; }
@@ -397,6 +371,15 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
/** Input that can be used to specify the `aria-labelledby` attribute. */
@Input('aria-labelledby') ariaLabelledby: string = '';
+ /** Unique id of the element. */
+ @Input()
+ get id() { return this._id; }
+ set id(value: string) {
+ this._id = value || this._uid;
+ this.stateChanges.next();
+ }
+ private _id: string;
+
/** Combined stream of all of the child options' change events. */
get optionSelectionChanges(): Observable {
return merge(...this.options.map(option => option.onSelectionChange));
@@ -421,30 +404,31 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
constructor(
private _viewportRuler: ViewportRuler,
private _changeDetectorRef: ChangeDetectorRef,
- private _platform: Platform,
+ private _ngZone: NgZone,
renderer: Renderer2,
elementRef: ElementRef,
@Optional() private _dir: Directionality,
@Optional() private _parentForm: NgForm,
@Optional() private _parentFormGroup: FormGroupDirective,
- @Self() @Optional() public _control: NgControl,
+ @Self() @Optional() public ngControl: NgControl,
@Attribute('tabindex') tabIndex: string,
- @Optional() @Inject(MD_PLACEHOLDER_GLOBAL_OPTIONS) placeholderOptions: PlaceholderOptions,
@Inject(MD_SELECT_SCROLL_STRATEGY) private _scrollStrategyFactory) {
super(renderer, elementRef);
- if (this._control) {
- this._control.valueAccessor = this;
+ if (this.ngControl) {
+ this.ngControl.valueAccessor = this;
}
this.tabIndex = parseInt(tabIndex) || 0;
- this._placeholderOptions = placeholderOptions ? placeholderOptions : {};
- this.floatPlaceholder = this._placeholderOptions.float || 'auto';
+
+ // Force setter to be called in case id was not specified.
+ this.id = this.id;
}
ngOnInit() {
this._selectionModel = new SelectionModel(this.multiple, undefined, false);
+ this.stateChanges.next();
}
ngAfterContentInit() {
@@ -473,25 +457,28 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
return;
}
- if (!this._triggerWidth) {
- this._setTriggerWidth();
- }
+ this._triggerRect = this.trigger.nativeElement.getBoundingClientRect();
+ // Note: The computed font-size will be a string pixel value (e.g. "16px").
+ // `parseInt` ignores the trailing 'px' and converts this to a number.
+ this._triggerFontSize = parseInt(getComputedStyle(this.trigger.nativeElement)['font-size']);
this._calculateOverlayPosition();
- this._placeholderState = this._floatPlaceholderState();
this._panelOpen = true;
this._changeDetectorRef.markForCheck();
+
+ // Set the font size on the panel element once it exists.
+ first.call(this._ngZone.onStable).subscribe(() => {
+ if (this._triggerFontSize && this.overlayDir.overlayRef &&
+ this.overlayDir.overlayRef.overlayElement) {
+ this.overlayDir.overlayRef.overlayElement.style.fontSize = `${this._triggerFontSize}px`;
+ }
+ });
}
/** Closes the overlay panel and focuses the host element. */
close(): void {
if (this._panelOpen) {
this._panelOpen = false;
-
- if (this._selectionModel.isEmpty()) {
- this._placeholderState = '';
- }
-
this._changeDetectorRef.markForCheck();
this.focus();
}
@@ -540,6 +527,7 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
this._changeDetectorRef.markForCheck();
+ this.stateChanges.next();
}
/** Whether or not the overlay panel is open. */
@@ -577,17 +565,6 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
return this._dir ? this._dir.value === 'rtl' : false;
}
- /**
- * Sets the width of the trigger element. This is necessary to match
- * the overlay width to the trigger width.
- */
- private _setTriggerWidth(): void {
- this._triggerWidth = this._platform.isBrowser ? this._getTriggerRect().width :
- SELECT_TRIGGER_MIN_WIDTH;
-
- this._changeDetectorRef.markForCheck();
- }
-
/** Handles the keyboard interactions of a closed select. */
_handleClosedKeydown(event: KeyboardEvent): void {
if (!this.disabled) {
@@ -636,14 +613,23 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
this._changeDetectorRef.markForCheck();
}
+ _onFocus() {
+ if (!this.disabled) {
+ this.focused = true;
+ this.stateChanges.next();
+ }
+ }
+
/**
* Calls the touched callback only if the panel is closed. Otherwise, the trigger will
* "blur" to the panel when it opens, causing a false positive.
*/
_onBlur() {
if (!this.disabled && !this.panelOpen) {
+ this.focused = false;
this._onTouched();
this._changeDetectorRef.markForCheck();
+ this.stateChanges.next();
}
}
@@ -656,14 +642,14 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
}
/** Whether the select has a value. */
- _hasValue(): boolean {
- return this._selectionModel && this._selectionModel.hasValue();
+ get empty(): boolean {
+ return !this._selectionModel || this._selectionModel.isEmpty();
}
/** Whether the select is in an error state. */
- _isErrorState(): boolean {
- const isInvalid = this._control && this._control.invalid;
- const isTouched = this._control && this._control.touched;
+ get errorState(): boolean {
+ const isInvalid = this.ngControl && this.ngControl.invalid;
+ const isTouched = this.ngControl && this.ngControl.touched;
const isSubmitted = (this._parentFormGroup && this._parentFormGroup.submitted) ||
(this._parentForm && this._parentForm.submitted);
@@ -685,7 +671,7 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
// Defer setting the value in order to avoid the "Expression
// has changed after it was checked" errors from Angular.
Promise.resolve().then(() => {
- this._setSelectionByValue(this._control ? this._control.value : this._value);
+ this._setSelectionByValue(this.ngControl ? this.ngControl.value : this._value);
});
}
@@ -715,12 +701,6 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
}
}
- this._setValueWidth();
-
- if (this._selectionModel.isEmpty()) {
- this._placeholderState = '';
- }
-
this._changeDetectorRef.markForCheck();
}
@@ -745,6 +725,7 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
if (correspondingOption) {
isUserInput ? correspondingOption._selectViaInteraction() : correspondingOption.select();
this._selectionModel.select(correspondingOption);
+ this.stateChanges.next();
}
return correspondingOption;
@@ -762,10 +743,7 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
option.deselect();
}
});
- }
-
- private _getTriggerRect(): ClientRect {
- return this.trigger.nativeElement.getBoundingClientRect();
+ this.stateChanges.next();
}
/** Sets up a key manager to listen to keyboard events on the overlay panel. */
@@ -788,7 +766,6 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
this._optionSubscription = filter.call(this.optionSelectionChanges,
event => event.isUserInput).subscribe(event => {
this._onSelect(event.source);
- this._setValueWidth();
if (!this.multiple) {
this.close();
@@ -803,6 +780,7 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
// TODO(crisbeto): handle blank/null options inside multi-select.
if (this.multiple) {
this._selectionModel.toggle(option);
+ this.stateChanges.next();
wasSelected ? option.deselect() : option.select();
this._sortValues();
} else {
@@ -812,6 +790,7 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
this._propagateChanges(option.value);
} else {
this._selectionModel.select(option);
+ this.stateChanges.next();
}
}
@@ -833,6 +812,7 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
this._selectionModel.select(option);
}
});
+ this.stateChanges.next();
}
}
@@ -855,6 +835,7 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
this._onChange(valueToEmit);
this.change.emit(new MdSelectChange(this, valueToEmit));
this.valueChange.emit(valueToEmit);
+ this._changeDetectorRef.markForCheck();
}
/** Records option IDs to pass to the aria-owns property. */
@@ -880,15 +861,6 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
this.options.forEach(option => option.disableRipple = this.disableRipple);
}
}
- /**
- * Must set the width of the selected option's value programmatically
- * because it is absolutely positioned and otherwise will not clip
- * overflow. The selection arrow is 9px wide, add 4px of padding = 13
- */
- private _setValueWidth() {
- this._selectedValueWidth = this._triggerWidth - 13;
- this._changeDetectorRef.markForCheck();
- }
/**
* Focuses the selected item. If no option is selected, it will focus
@@ -916,34 +888,27 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
/** Calculates the scroll position and x- and y-offsets of the overlay panel. */
private _calculateOverlayPosition(): void {
+ const itemHeight = this._triggerFontSize * SELECT_ITEM_HEIGHT_EM;
+
const items = this._getItemCount();
- const panelHeight = Math.min(items * SELECT_ITEM_HEIGHT, SELECT_PANEL_MAX_HEIGHT);
- const scrollContainerHeight = items * SELECT_ITEM_HEIGHT;
+ const panelHeight = Math.min(items * itemHeight, SELECT_PANEL_MAX_HEIGHT);
+ const scrollContainerHeight = items * itemHeight;
// The farthest the panel can be scrolled before it hits the bottom
const maxScroll = scrollContainerHeight - panelHeight;
- if (this._hasValue()) {
- let selectedOptionOffset = this._getOptionIndex(this._selectionModel.selected[0])!;
+ // If no value is selected we open the popup to the first item.
+ let selectedOptionOffset =
+ this.empty ? 0 : this._getOptionIndex(this._selectionModel.selected[0])!;
- selectedOptionOffset += MdOption.countGroupLabelsBeforeOption(selectedOptionOffset,
- this.options, this.optionGroups);
+ selectedOptionOffset += MdOption.countGroupLabelsBeforeOption(selectedOptionOffset,
+ this.options, this.optionGroups);
- // We must maintain a scroll buffer so the selected option will be scrolled to the
- // center of the overlay panel rather than the top.
- const scrollBuffer = panelHeight / 2;
- this._scrollTop = this._calculateOverlayScroll(selectedOptionOffset, scrollBuffer, maxScroll);
- this._offsetY = this._calculateOverlayOffsetY(selectedOptionOffset, scrollBuffer, maxScroll);
- } else {
- // If no option is selected, the panel centers on the first option. In this case,
- // we must only adjust for the height difference between the option element
- // and the trigger element, then multiply it by -1 to ensure the panel moves
- // in the correct direction up the page.
- let groupLabels = MdOption.countGroupLabelsBeforeOption(0, this.options, this.optionGroups);
-
- this._offsetY = (SELECT_ITEM_HEIGHT - SELECT_TRIGGER_HEIGHT) / 2 * -1 -
- (groupLabels * SELECT_ITEM_HEIGHT);
- }
+ // We must maintain a scroll buffer so the selected option will be scrolled to the
+ // center of the overlay panel rather than the top.
+ const scrollBuffer = panelHeight / 2;
+ this._scrollTop = this._calculateOverlayScroll(selectedOptionOffset, scrollBuffer, maxScroll);
+ this._offsetY = this._calculateOverlayOffsetY(selectedOptionOffset, scrollBuffer, maxScroll);
this._checkOverlayWithinViewport(maxScroll);
}
@@ -957,8 +922,9 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
*/
_calculateOverlayScroll(selectedIndex: number, scrollBuffer: number,
maxScroll: number): number {
- const optionOffsetFromScrollTop = SELECT_ITEM_HEIGHT * selectedIndex;
- const halfOptionHeight = SELECT_ITEM_HEIGHT / 2;
+ const itemHeight = this._triggerFontSize * SELECT_ITEM_HEIGHT_EM;
+ const optionOffsetFromScrollTop = itemHeight * selectedIndex;
+ const halfOptionHeight = itemHeight / 2;
// Starts at the optionOffsetFromScrollTop, which scrolls the option to the top of the
// scroll container, then subtracts the scroll buffer to scroll the option down to
@@ -968,28 +934,6 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
return clampValue(0, optimalScrollPosition, maxScroll);
}
- /**
- * Figures out the appropriate animation state for the placeholder.
- */
- _getPlaceholderAnimationState(): string {
- if (this.floatPlaceholder === 'never') {
- return '';
- }
-
- if (this.floatPlaceholder === 'always') {
- return this._floatPlaceholderState();
- }
-
- return this._placeholderState;
- }
-
- /**
- * Determines the CSS `opacity` of the placeholder element.
- */
- _getPlaceholderOpacity(): string {
- return (this.floatPlaceholder !== 'never' || this._selectionModel.isEmpty()) ? '1' : '0';
- }
-
/** Returns the aria-label of the select component. */
get _ariaLabel(): string | null {
// If an ariaLabelledby value has been set, the select should not overwrite the
@@ -1050,31 +994,38 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
*/
private _calculateOverlayOffsetY(selectedIndex: number, scrollBuffer: number,
maxScroll: number): number {
+ const itemHeight = this._triggerFontSize * SELECT_ITEM_HEIGHT_EM;
+ const optionHeightAdjustment = (itemHeight - this._triggerRect.height) / 2;
+ const maxOptionsDisplayed = Math.floor(SELECT_PANEL_MAX_HEIGHT / itemHeight);
let optionOffsetFromPanelTop: number;
if (this._scrollTop === 0) {
- optionOffsetFromPanelTop = selectedIndex * SELECT_ITEM_HEIGHT;
+ optionOffsetFromPanelTop = selectedIndex * itemHeight;
} else if (this._scrollTop === maxScroll) {
- const firstDisplayedIndex = this._getItemCount() - SELECT_MAX_OPTIONS_DISPLAYED;
+ const firstDisplayedIndex = this._getItemCount() - maxOptionsDisplayed;
const selectedDisplayIndex = selectedIndex - firstDisplayedIndex;
+ // The first item is partially out of the viewport. Therefore we need to calculate what
+ // portion of it is shown in the viewport and account for it in our offset.
+ let partialItemHeight =
+ itemHeight - (this._getItemCount() * itemHeight - SELECT_PANEL_MAX_HEIGHT) % itemHeight;
+
// Because the panel height is longer than the height of the options alone,
// there is always extra padding at the top or bottom of the panel. When
// scrolled to the very bottom, this padding is at the top of the panel and
// must be added to the offset.
- optionOffsetFromPanelTop =
- selectedDisplayIndex * SELECT_ITEM_HEIGHT + SELECT_PANEL_PADDING_Y;
+ optionOffsetFromPanelTop = selectedDisplayIndex * itemHeight + partialItemHeight;
} else {
// If the option was scrolled to the middle of the panel using a scroll buffer,
// its offset will be the scroll buffer minus the half height that was added to
// center it.
- optionOffsetFromPanelTop = scrollBuffer - SELECT_ITEM_HEIGHT / 2;
+ optionOffsetFromPanelTop = scrollBuffer - itemHeight / 2;
}
// The final offset is the option's offset from the top, adjusted for the height
// difference, multiplied by -1 to ensure that the overlay moves in the correct
// direction up the page.
- return optionOffsetFromPanelTop * -1 - SELECT_OPTION_HEIGHT_ADJUSTMENT;
+ return optionOffsetFromPanelTop * -1 - optionHeightAdjustment;
}
/**
@@ -1084,17 +1035,17 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
* sets the offset back to 0 to allow the fallback position to take over.
*/
private _checkOverlayWithinViewport(maxScroll: number): void {
+ const itemHeight = this._triggerFontSize * SELECT_ITEM_HEIGHT_EM;
const viewportRect = this._viewportRuler.getViewportRect();
- const triggerRect = this._getTriggerRect();
- const topSpaceAvailable = triggerRect.top - SELECT_PANEL_VIEWPORT_PADDING;
+ const topSpaceAvailable = this._triggerRect.top - SELECT_PANEL_VIEWPORT_PADDING;
const bottomSpaceAvailable =
- viewportRect.height - triggerRect.bottom - SELECT_PANEL_VIEWPORT_PADDING;
+ viewportRect.height - this._triggerRect.bottom - SELECT_PANEL_VIEWPORT_PADDING;
const panelHeightTop = Math.abs(this._offsetY);
const totalPanelHeight =
- Math.min(this._getItemCount() * SELECT_ITEM_HEIGHT, SELECT_PANEL_MAX_HEIGHT);
- const panelHeightBottom = totalPanelHeight - panelHeightTop - triggerRect.height;
+ Math.min(this._getItemCount() * itemHeight, SELECT_PANEL_MAX_HEIGHT);
+ const panelHeightBottom = totalPanelHeight - panelHeightTop - this._triggerRect.height;
if (panelHeightBottom > bottomSpaceAvailable) {
this._adjustPanelUp(panelHeightBottom, bottomSpaceAvailable);
@@ -1107,7 +1058,8 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
/** Adjusts the overlay panel up to fit in the viewport. */
private _adjustPanelUp(panelHeightBottom: number, bottomSpaceAvailable: number) {
- const distanceBelowViewport = panelHeightBottom - bottomSpaceAvailable;
+ // Browsers ignore fractional scroll offsets, so we need to round.
+ const distanceBelowViewport = Math.round(panelHeightBottom - bottomSpaceAvailable);
// Scrolls the panel up by the distance it was extending past the boundary, then
// adjusts the offset by that amount to move the panel up into the viewport.
@@ -1128,7 +1080,8 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
/** Adjusts the overlay panel down to fit in the viewport. */
private _adjustPanelDown(panelHeightTop: number, topSpaceAvailable: number,
maxScroll: number) {
- const distanceAboveViewport = panelHeightTop - topSpaceAvailable;
+ // Browsers ignore fractional scroll offsets, so we need to round.
+ const distanceAboveViewport = Math.round(panelHeightTop - topSpaceAvailable);
// Scrolls the panel down by the distance it was extending past the boundary, then
// adjusts the offset by that amount to move the panel down into the viewport.
@@ -1149,16 +1102,12 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
/** Sets the transform origin point based on the selected option. */
private _getOriginBasedOnOption(): string {
- const originY =
- Math.abs(this._offsetY) - SELECT_OPTION_HEIGHT_ADJUSTMENT + SELECT_ITEM_HEIGHT / 2;
+ const itemHeight = this._triggerFontSize * SELECT_ITEM_HEIGHT_EM;
+ const optionHeightAdjustment = (itemHeight - this._triggerRect.height) / 2;
+ const originY = Math.abs(this._offsetY) - optionHeightAdjustment + itemHeight / 2;
return `50% ${originY}px 0px`;
}
- /** Figures out the floating placeholder state value. */
- private _floatPlaceholderState(): string {
- return this._isRtl() ? 'floating-rtl' : 'floating-ltr';
- }
-
/** Handles the user pressing the arrow keys on a closed select. */
private _handleArrowKey(event: KeyboardEvent): void {
if (this._multiple) {
@@ -1187,6 +1136,18 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
private _getItemCount(): number {
return this.options.length + this.optionGroups.length;
}
+
+ // Implemented as part of MdFormFieldControl.
+ setDescribedByIds(ids: string[]) { this._ariaDescribedby = ids.join(' '); }
+
+ // Implemented as part of MdFormFieldControl.
+ onContainerClick() {
+ this.focus();
+ this.open();
+ }
+
+ // Implemented as part of MdFormFieldControl.
+ get shouldPlaceholderFloat() { return this._panelOpen || !this.empty; }
}
/** Clamps a value n between min and max values. */
diff --git a/src/material-examples/select-form/select-form-example.html b/src/material-examples/select-form/select-form-example.html
index 6dea33cc5963..31252dd70a66 100644
--- a/src/material-examples/select-form/select-form-example.html
+++ b/src/material-examples/select-form/select-form-example.html
@@ -1,9 +1,11 @@
diff --git a/src/material-examples/select-overview/select-overview-example.html b/src/material-examples/select-overview/select-overview-example.html
index a4017ca5c242..bf3897383572 100644
--- a/src/material-examples/select-overview/select-overview-example.html
+++ b/src/material-examples/select-overview/select-overview-example.html
@@ -1,5 +1,7 @@
-
-
- {{ food.viewValue }}
-
-
+
+
+
+ {{ food.viewValue }}
+
+
+
diff --git a/src/material-examples/tooltip-position/tooltip-position-example.html b/src/material-examples/tooltip-position/tooltip-position-example.html
index 156a5bf68492..2187a60c71f4 100644
--- a/src/material-examples/tooltip-position/tooltip-position-example.html
+++ b/src/material-examples/tooltip-position/tooltip-position-example.html
@@ -1,11 +1,13 @@
Show tooltip
-
- Before
- After
- Above
- Below
- Left
- Right
-
+
+
+ Before
+ After
+ Above
+ Below
+ Left
+ Right
+
+
diff --git a/src/universal-app/kitchen-sink/kitchen-sink.html b/src/universal-app/kitchen-sink/kitchen-sink.html
index 4509dc2142cd..988a6d2cb34d 100644
--- a/src/universal-app/kitchen-sink/kitchen-sink.html
+++ b/src/universal-app/kitchen-sink/kitchen-sink.html
@@ -159,11 +159,13 @@ Standalone radios
Red
Select
-
- Glass
- Ceramic
- Steel
-
+
+
+ Glass
+ Ceramic
+ Steel
+
+
Sidenav