+
+
This is the menu panel.
diff --git a/src/demo-app/slider/slider-demo.html b/src/demo-app/slider/slider-demo.html
index caa923afb276..8496236039b5 100644
--- a/src/demo-app/slider/slider-demo.html
+++ b/src/demo-app/slider/slider-demo.html
@@ -24,6 +24,10 @@
Slider with set tick interval
Slider with Thumb Label
+
Slider with one-way binding
+
+
+
Slider with two-way binding
diff --git a/src/demo-app/slider/slider-demo.ts b/src/demo-app/slider/slider-demo.ts
index f199cf26726e..2aba4410f874 100644
--- a/src/demo-app/slider/slider-demo.ts
+++ b/src/demo-app/slider/slider-demo.ts
@@ -8,4 +8,5 @@ import {Component} from '@angular/core';
})
export class SliderDemo {
demo: number;
+ val: number = 50;
}
diff --git a/src/e2e-app/checkbox/checkbox-e2e.html b/src/e2e-app/checkbox/checkbox-e2e.html
new file mode 100644
index 000000000000..5d78249c6d97
--- /dev/null
+++ b/src/e2e-app/checkbox/checkbox-e2e.html
@@ -0,0 +1 @@
+
Check this button
diff --git a/src/e2e-app/checkbox/checkbox-e2e.ts b/src/e2e-app/checkbox/checkbox-e2e.ts
new file mode 100644
index 000000000000..d919de8103be
--- /dev/null
+++ b/src/e2e-app/checkbox/checkbox-e2e.ts
@@ -0,0 +1,8 @@
+import {Component} from '@angular/core';
+
+@Component({
+ moduleId: module.id,
+ selector: 'checkbox-e2e',
+ templateUrl: 'checkbox-e2e.html',
+})
+export class SimpleCheckboxes {}
diff --git a/src/e2e-app/e2e-app-module.ts b/src/e2e-app/e2e-app-module.ts
index dc60972c9b8f..09be530b4614 100644
--- a/src/e2e-app/e2e-app-module.ts
+++ b/src/e2e-app/e2e-app-module.ts
@@ -1,10 +1,12 @@
import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {RouterModule} from '@angular/router';
+import {SimpleCheckboxes} from './checkbox/checkbox-e2e';
import {E2EApp, Home} from './e2e-app/e2e-app';
import {IconE2E} from './icon/icon-e2e';
import {ButtonE2E} from './button/button-e2e';
import {MenuE2E} from './menu/menu-e2e';
+import {SimpleRadioButtons} from './radio/radio-e2e';
import {BasicTabs} from './tabs/tabs-e2e';
import {MaterialModule} from '@angular/material';
import {E2E_APP_ROUTES} from './e2e-app/routes';
@@ -22,6 +24,8 @@ import {E2E_APP_ROUTES} from './e2e-app/routes';
ButtonE2E,
MenuE2E,
BasicTabs,
+ SimpleRadioButtons,
+ SimpleCheckboxes,
Home,
],
bootstrap: [E2EApp],
diff --git a/src/e2e-app/e2e-app/e2e-app.html b/src/e2e-app/e2e-app/e2e-app.html
index 8afb09392e21..084d2d854f5d 100644
--- a/src/e2e-app/e2e-app/e2e-app.html
+++ b/src/e2e-app/e2e-app/e2e-app.html
@@ -1,6 +1,8 @@
Button
+
Checkbox
Icon
Menu
+
Radios
Tabs
diff --git a/src/e2e-app/e2e-app/routes.ts b/src/e2e-app/e2e-app/routes.ts
index 78f805ea90d1..8ed58c0bae22 100644
--- a/src/e2e-app/e2e-app/routes.ts
+++ b/src/e2e-app/e2e-app/routes.ts
@@ -4,11 +4,16 @@ import {ButtonE2E} from '../button/button-e2e';
import {BasicTabs} from '../tabs/tabs-e2e';
import {IconE2E} from '../icon/icon-e2e';
import {MenuE2E} from '../menu/menu-e2e';
+import {SimpleRadioButtons} from '../radio/radio-e2e';
+import {SimpleCheckboxes} from '../checkbox/checkbox-e2e';
export const E2E_APP_ROUTES: Routes = [
{path: '', component: Home},
{path: 'button', component: ButtonE2E},
+ {path: 'checkbox', component: SimpleCheckboxes},
{path: 'menu', component: MenuE2E},
{path: 'icon', component: IconE2E},
+ {path: 'radio', component: SimpleRadioButtons},
{path: 'tabs', component: BasicTabs}
+
];
diff --git a/src/e2e-app/radio/radio-e2e.html b/src/e2e-app/radio/radio-e2e.html
new file mode 100644
index 000000000000..83d672df16cd
--- /dev/null
+++ b/src/e2e-app/radio/radio-e2e.html
@@ -0,0 +1,10 @@
+
+
+ Charmander
+ Squirtle
+ Bulbasaur
+
+
+
diff --git a/src/e2e-app/radio/radio-e2e.ts b/src/e2e-app/radio/radio-e2e.ts
new file mode 100644
index 000000000000..5df27b942fcc
--- /dev/null
+++ b/src/e2e-app/radio/radio-e2e.ts
@@ -0,0 +1,10 @@
+import {Component} from '@angular/core';
+
+@Component({
+ moduleId: module.id,
+ selector: 'radio-e2e',
+ templateUrl: 'radio-e2e.html',
+})
+export class SimpleRadioButtons {
+ isGroupDisabled: boolean = false;
+}
diff --git a/src/lib/button-toggle/button-toggle.ts b/src/lib/button-toggle/button-toggle.ts
index 1f4d3785ed0e..c1b922055da6 100644
--- a/src/lib/button-toggle/button-toggle.ts
+++ b/src/lib/button-toggle/button-toggle.ts
@@ -15,13 +15,9 @@ import {
forwardRef,
AfterViewInit
} from '@angular/core';
-import {
- NG_VALUE_ACCESSOR,
- ControlValueAccessor,
- FormsModule,
-} from '@angular/forms';
+import {NG_VALUE_ACCESSOR, ControlValueAccessor, FormsModule} from '@angular/forms';
import {Observable} from 'rxjs/Observable';
-import {BooleanFieldValue, MdUniqueSelectionDispatcher} from '../core';
+import {MdUniqueSelectionDispatcher, coerceBooleanProperty} from '../core';
export type ToggleType = 'checkbox' | 'radio';
@@ -102,13 +98,12 @@ export class MdButtonToggleGroup implements AfterViewInit, ControlValueAccessor
}
@Input()
- @BooleanFieldValue()
get disabled(): boolean {
return this._disabled;
}
set disabled(value) {
- this._disabled = (value != null && value !== false) ? true : null;
+ this._disabled = coerceBooleanProperty(value);
}
@Input()
diff --git a/src/lib/button/button.scss b/src/lib/button/button.scss
index 5f1a78b4755c..6ac83e421285 100644
--- a/src/lib/button/button.scss
+++ b/src/lib/button/button.scss
@@ -47,6 +47,13 @@
}
}
+// The text and icon should be vertical aligned inside a button
+[md-button], [md-raised-button] {
+ .md-button-wrapper > * {
+ vertical-align: middle;
+ }
+}
+
// The ripple container should match the bounds of the entire button.
.md-button-ripple {
position: absolute;
diff --git a/src/lib/button/button.ts b/src/lib/button/button.ts
index 03ac5438f1a3..9763d851a706 100644
--- a/src/lib/button/button.ts
+++ b/src/lib/button/button.ts
@@ -10,7 +10,7 @@ import {
ModuleWithProviders,
} from '@angular/core';
import {CommonModule} from '@angular/common';
-import {BooleanFieldValue, MdRippleModule} from '../core';
+import {MdRippleModule, coerceBooleanProperty} from '../core';
// TODO(jelbourn): Make the `isMouseDown` stuff done with one global listener.
// TODO(kara): Convert attribute selectors to classes when attr maps become available
@@ -41,7 +41,11 @@ export class MdButton {
_isMouseDown: boolean = false;
/** Whether the ripple effect on click should be disabled. */
- @Input() @BooleanFieldValue() disableRipple: boolean = false;
+ private _disableRipple: boolean = false;
+
+ @Input()
+ get disableRipple() { return this._disableRipple; }
+ set disableRipple(v) { this._disableRipple = coerceBooleanProperty(v); }
constructor(private _elementRef: ElementRef, private _renderer: Renderer) { }
diff --git a/src/lib/checkbox/README.md b/src/lib/checkbox/README.md
index 4efc10ccf5b4..a8cffb6ae668 100644
--- a/src/lib/checkbox/README.md
+++ b/src/lib/checkbox/README.md
@@ -77,3 +77,17 @@ checkbox if you do not wish to have any label text.
```html
```
+
+### Theming
+
+The color of a `md-checkbox` can be changed by using the `color` attribute.
+The value `accent` is default and will correspond to your theme accent color.
+Alternatively, you can specify `primary` or `warn`.
+
+Example:
+
+ ```html
+
+ I come after my label.
+
+ ```
diff --git a/src/lib/checkbox/_checkbox-theme.scss b/src/lib/checkbox/_checkbox-theme.scss
index ba66c87f06a2..4bde9b49407f 100644
--- a/src/lib/checkbox/_checkbox-theme.scss
+++ b/src/lib/checkbox/_checkbox-theme.scss
@@ -5,6 +5,7 @@
$is-dark-theme: map-get($theme, is-dark);
$primary: map-get($theme, primary);
$accent: map-get($theme, accent);
+ $warn: map-get($theme, warn);
$background: map-get($theme, background);
@@ -14,9 +15,6 @@
// The color of the checkbox's checkmark / mixedmark.
$checkbox-mark-color: md-color($background, background);
- // The color that is used as the checkbox background when it is checked.
- $checkbox-background-color: md-color($accent, 500);
-
// NOTE(traviskaufman): While the spec calls for translucent blacks/whites for disabled colors,
// this does not work well with elements layered on top of one another. To get around this we
// blend the colors together based on the base color and the theme background.
@@ -43,8 +41,16 @@
}
.md-checkbox-indeterminate, .md-checkbox-checked {
- .md-checkbox-background {
- background-color: $checkbox-background-color;
+ &.md-primary .md-checkbox-background {
+ background-color: md-color($primary, 500);
+ }
+
+ &.md-accent .md-checkbox-background {
+ background-color: md-color($accent, 500);
+ }
+
+ &.md-warn .md-checkbox-background {
+ background-color: md-color($warn, 500);
}
}
@@ -63,7 +69,17 @@
}
// TODO(jelbourn): remove style for temporary ripple once the real ripple is applied.
- .md-checkbox-focused .md-ink-ripple {
- background-color: md-color($accent, 0.26);
+ .md-checkbox-focused {
+ &.md-primary .md-ink-ripple {
+ background-color: md-color($primary, 0.26);
+ }
+
+ &.md-accent .md-ink-ripple {
+ background-color: md-color($accent, 0.26);
+ }
+
+ &.md-warn .md-ink-ripple {
+ background-color: md-color($warn, 0.26);
+ }
}
}
diff --git a/src/lib/checkbox/checkbox.scss b/src/lib/checkbox/checkbox.scss
index e634fe878cbb..d1135520dbcc 100644
--- a/src/lib/checkbox/checkbox.scss
+++ b/src/lib/checkbox/checkbox.scss
@@ -1,4 +1,5 @@
@import '../core/theming/theming';
+@import '../core/style/elevation';
@import '../core/style/variables';
@import '../core/ripple/ripple';
@@ -189,6 +190,10 @@ $_md-checkbox-indeterminate-checked-easing-function: cubic-bezier(0.14, 0, 0, 1)
md-checkbox {
cursor: pointer;
+
+ // Animation
+ transition: background $swift-ease-out-duration $swift-ease-out-timing-function,
+ md-elevation-transition-property-value();
}
.md-checkbox-layout {
diff --git a/src/lib/checkbox/checkbox.spec.ts b/src/lib/checkbox/checkbox.spec.ts
index e7af963aeb46..e0e0a251baeb 100644
--- a/src/lib/checkbox/checkbox.spec.ts
+++ b/src/lib/checkbox/checkbox.spec.ts
@@ -267,6 +267,36 @@ describe('MdCheckbox', () => {
expect(inputElement.required).toBe(false);
});
+ describe('color behaviour', () => {
+ it('should apply class based on color attribute', () => {
+ testComponent.checkboxColor = 'primary';
+ fixture.detectChanges();
+ expect(checkboxDebugElement.nativeElement.classList.contains('md-primary')).toBe(true);
+
+ testComponent.checkboxColor = 'accent';
+ fixture.detectChanges();
+ expect(checkboxDebugElement.nativeElement.classList.contains('md-accent')).toBe(true);
+ });
+
+ it('should should not clear previous defined classes', () => {
+ checkboxDebugElement.nativeElement.classList.add('custom-class');
+
+ testComponent.checkboxColor = 'primary';
+ fixture.detectChanges();
+
+ expect(checkboxDebugElement.nativeElement.classList.contains('md-primary')).toBe(true);
+ expect(checkboxDebugElement.nativeElement.classList.contains('custom-class')).toBe(true);
+
+ testComponent.checkboxColor = 'accent';
+ fixture.detectChanges();
+
+ expect(checkboxDebugElement.nativeElement.classList.contains('md-primary')).toBe(false);
+ expect(checkboxDebugElement.nativeElement.classList.contains('md-accent')).toBe(true);
+ expect(checkboxDebugElement.nativeElement.classList.contains('custom-class')).toBe(true);
+
+ });
+ });
+
describe('state transition css classes', () => {
it('should transition unchecked -> checked -> unchecked', () => {
testComponent.isChecked = true;
@@ -519,6 +549,7 @@ describe('MdCheckbox', () => {
[checked]="isChecked"
[indeterminate]="isIndeterminate"
[disabled]="isDisabled"
+ [color]="checkboxColor"
(change)="changeCount = changeCount + 1"
(click)="onCheckboxClick($event)"
(change)="onCheckboxChange($event)">
@@ -536,6 +567,7 @@ class SingleCheckbox {
parentElementKeyedUp: boolean = false;
lastKeydownEvent: Event = null;
changeCount: number = 0;
+ checkboxColor: string = 'primary';
onCheckboxClick(event: Event) {}
onCheckboxChange(event: MdCheckboxChange) {}
diff --git a/src/lib/checkbox/checkbox.ts b/src/lib/checkbox/checkbox.ts
index ab1b55fa6ba3..8d595def9bd4 100644
--- a/src/lib/checkbox/checkbox.ts
+++ b/src/lib/checkbox/checkbox.ts
@@ -12,7 +12,8 @@ import {
ModuleWithProviders,
} from '@angular/core';
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms';
-import {BooleanFieldValue} from '../core';
+import {coerceBooleanProperty} from '../core/coersion/boolean-property';
+
/**
* Monotonically increasing integer used to auto-generate unique ids for checkbox components.
@@ -93,8 +94,12 @@ export class MdCheckbox implements ControlValueAccessor {
return `input-${this.id}`;
}
+ private _required: boolean;
+
/** Whether the checkbox is required or not. */
- @Input() @BooleanFieldValue() required: boolean = false;
+ @Input()
+ get required(): boolean { return this._required; }
+ set required(value) { this._required = coerceBooleanProperty(value); }
/** Whether or not the checkbox should come before or after the label. */
@Input() align: 'start' | 'end' = 'start';
@@ -128,11 +133,15 @@ export class MdCheckbox implements ControlValueAccessor {
private _indeterminate: boolean = false;
+ private _color: string;
+
private _controlValueAccessorChangeFn: (value: any) => void = (value) => {};
hasFocus: boolean = false;
- constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
+ constructor(private _renderer: Renderer, private _elementRef: ElementRef) {
+ this.color = 'accent';
+ }
/**
* Whether the checkbox is checked. Note that setting `checked` will immediately set
@@ -174,6 +183,28 @@ export class MdCheckbox implements ControlValueAccessor {
}
}
+ /** Sets the color of the checkbox */
+ @Input()
+ get color(): string {
+ return this._color;
+ }
+
+ set color(value: string) {
+ this._updateColor(value);
+ }
+
+ _updateColor(newColor: string) {
+ this._setElementColor(this._color, false);
+ this._setElementColor(newColor, true);
+ this._color = newColor;
+ }
+
+ _setElementColor(color: string, isAdd: boolean) {
+ if (color != null && color != '') {
+ this._renderer.setElementClass(this._elementRef.nativeElement, `md-${color}`, isAdd);
+ }
+ }
+
/**
* Implemented as part of ControlValueAccessor.
* TODO: internal
diff --git a/src/lib/core/a11y/list-key-manager.spec.ts b/src/lib/core/a11y/list-key-manager.spec.ts
new file mode 100644
index 000000000000..e09dafeb99b5
--- /dev/null
+++ b/src/lib/core/a11y/list-key-manager.spec.ts
@@ -0,0 +1,109 @@
+import {QueryList} from '@angular/core';
+import {ListKeyManager, MdFocusable} from './list-key-manager';
+import {DOWN_ARROW, UP_ARROW, TAB} from '../keyboard/keycodes';
+
+class FakeFocusable {
+ disabled = false;
+ focus() {}
+}
+
+const DOWN_ARROW_EVENT = { keyCode: DOWN_ARROW } as KeyboardEvent;
+const UP_ARROW_EVENT = { keyCode: UP_ARROW } as KeyboardEvent;
+const TAB_EVENT = { keyCode: TAB } as KeyboardEvent;
+
+describe('ListKeyManager', () => {
+ let keyManager: ListKeyManager;
+ let itemList: QueryList
;
+ let items: MdFocusable[];
+
+ beforeEach(() => {
+ itemList = new QueryList();
+ items = [
+ new FakeFocusable(),
+ new FakeFocusable(),
+ new FakeFocusable()
+ ];
+
+ itemList.toArray = () => items;
+
+ keyManager = new ListKeyManager(itemList);
+
+ // first item is already focused
+ keyManager.focusedItemIndex = 0;
+
+ spyOn(items[0], 'focus');
+ spyOn(items[1], 'focus');
+ spyOn(items[2], 'focus');
+ });
+
+ it('should focus subsequent items when down arrow is pressed', () => {
+ keyManager.onKeydown(DOWN_ARROW_EVENT);
+
+ expect(items[0].focus).not.toHaveBeenCalled();
+ expect(items[1].focus).toHaveBeenCalledTimes(1);
+ expect(items[2].focus).not.toHaveBeenCalled();
+
+ keyManager.onKeydown(DOWN_ARROW_EVENT);
+ expect(items[0].focus).not.toHaveBeenCalled();
+ expect(items[1].focus).toHaveBeenCalledTimes(1);
+ expect(items[2].focus).toHaveBeenCalledTimes(1);
+ });
+
+ it('should focus previous items when up arrow is pressed', () => {
+ keyManager.onKeydown(DOWN_ARROW_EVENT);
+
+ expect(items[0].focus).not.toHaveBeenCalled();
+ expect(items[1].focus).toHaveBeenCalledTimes(1);
+
+ keyManager.onKeydown(UP_ARROW_EVENT);
+
+ expect(items[0].focus).toHaveBeenCalledTimes(1);
+ expect(items[1].focus).toHaveBeenCalledTimes(1);
+ });
+
+ it('should skip disabled items using arrow keys', () => {
+ items[1].disabled = true;
+
+ // down arrow should skip past disabled item from 0 to 2
+ keyManager.onKeydown(DOWN_ARROW_EVENT);
+ expect(items[0].focus).not.toHaveBeenCalled();
+ expect(items[1].focus).not.toHaveBeenCalled();
+ expect(items[2].focus).toHaveBeenCalledTimes(1);
+
+ // up arrow should skip past disabled item from 2 to 0
+ keyManager.onKeydown(UP_ARROW_EVENT);
+ expect(items[0].focus).toHaveBeenCalledTimes(1);
+ expect(items[1].focus).not.toHaveBeenCalled();
+ expect(items[2].focus).toHaveBeenCalledTimes(1);
+ });
+
+ it('should wrap back to menu when arrow keying past items', () => {
+ keyManager.onKeydown(DOWN_ARROW_EVENT);
+ keyManager.onKeydown(DOWN_ARROW_EVENT);
+
+ expect(items[0].focus).not.toHaveBeenCalled();
+ expect(items[1].focus).toHaveBeenCalledTimes(1);
+ expect(items[2].focus).toHaveBeenCalledTimes(1);
+
+ // this down arrow moves down past the end of the list
+ keyManager.onKeydown(DOWN_ARROW_EVENT);
+ expect(items[0].focus).toHaveBeenCalledTimes(1);
+ expect(items[1].focus).toHaveBeenCalledTimes(1);
+ expect(items[2].focus).toHaveBeenCalledTimes(1);
+
+ // this up arrow moves up past the beginning of the list
+ keyManager.onKeydown(UP_ARROW_EVENT);
+ expect(items[0].focus).toHaveBeenCalledTimes(1);
+ expect(items[1].focus).toHaveBeenCalledTimes(1);
+ expect(items[2].focus).toHaveBeenCalledTimes(2);
+ });
+
+ it('should emit tabOut when the tab key is pressed', () => {
+ let tabOutEmitted = false;
+ keyManager.tabOut.first().subscribe(() => tabOutEmitted = true);
+ keyManager.onKeydown(TAB_EVENT);
+
+ expect(tabOutEmitted).toBe(true);
+ });
+
+});
diff --git a/src/lib/core/a11y/list-key-manager.ts b/src/lib/core/a11y/list-key-manager.ts
new file mode 100644
index 000000000000..8eb080090836
--- /dev/null
+++ b/src/lib/core/a11y/list-key-manager.ts
@@ -0,0 +1,74 @@
+import {EventEmitter, Output, QueryList} from '@angular/core';
+import {UP_ARROW, DOWN_ARROW, TAB} from '../core';
+
+/**
+ * This is the interface for focusable items (used by the ListKeyManager).
+ * Each item must know how to focus itself and whether or not it is currently disabled.
+ */
+export interface MdFocusable {
+ focus(): void;
+ disabled: boolean;
+}
+
+/**
+ * This class manages keyboard events for selectable lists. If you pass it a query list
+ * of focusable items, it will focus the correct item when arrow events occur.
+ */
+export class ListKeyManager {
+ private _focusedItemIndex: number;
+
+ /**
+ * This event is emitted any time the TAB key is pressed, so components can react
+ * when focus is shifted off of the list.
+ */
+ @Output() tabOut: EventEmitter = new EventEmitter();
+
+ constructor(private _items: QueryList) {}
+
+ set focusedItemIndex(value: number) {
+ this._focusedItemIndex = value;
+ }
+
+ onKeydown(event: KeyboardEvent): void {
+ if (event.keyCode === DOWN_ARROW) {
+ this._focusNextItem();
+ } else if (event.keyCode === UP_ARROW) {
+ this._focusPreviousItem();
+ } else if (event.keyCode === TAB) {
+ this.tabOut.emit(null);
+ }
+ }
+
+ private _focusNextItem(): void {
+ const items = this._items.toArray();
+ this._updateFocusedItemIndex(1, items);
+ items[this._focusedItemIndex].focus();
+ }
+
+ private _focusPreviousItem(): void {
+ const items = this._items.toArray();
+ this._updateFocusedItemIndex(-1, items);
+ items[this._focusedItemIndex].focus();
+ }
+
+ /**
+ * This method sets focus to the correct item, given a list of items and the delta
+ * between the currently focused item and the new item to be focused. It will
+ * continue to move down the list until it finds an item that is not disabled, and it will wrap
+ * if it encounters either end of the list.
+ *
+ * @param delta the desired change in focus index
+ */
+ private _updateFocusedItemIndex(delta: number, items: MdFocusable[]) {
+ // when focus would leave menu, wrap to beginning or end
+ this._focusedItemIndex =
+ (this._focusedItemIndex + delta + items.length) % items.length;
+
+ // skip all disabled menu items recursively until an active one
+ // is reached or the menu closes for overreaching bounds
+ while (items[this._focusedItemIndex].disabled) {
+ this._updateFocusedItemIndex(delta, items);
+ }
+ }
+
+}
diff --git a/src/lib/core/annotations/field-value.spec.ts b/src/lib/core/annotations/field-value.spec.ts
deleted file mode 100644
index 55bcc5d25c0a..000000000000
--- a/src/lib/core/annotations/field-value.spec.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import {BooleanFieldValue} from './field-value';
-
-describe('BooleanFieldValue', () => {
- it('should work for null values', () => {
- let x = new BooleanFieldValueTest();
-
- x.field = null;
- expect(x.field).toBe(false);
-
- x.field = undefined;
- expect(x.field).toBe(false);
- });
-
- it('should work for string values', () => {
- let x = new BooleanFieldValueTest();
-
- (x).field = 'hello';
- expect(x.field).toBe(true);
-
- (x).field = 'true';
- expect(x.field).toBe(true);
-
- (x).field = '';
- expect(x.field).toBe(true);
-
- (x).field = 'false';
- expect(x.field).toBe(false);
- });
-});
-
-
-class BooleanFieldValueTest {
- @BooleanFieldValue() field: boolean;
-}
diff --git a/src/lib/core/annotations/field-value.ts b/src/lib/core/annotations/field-value.ts
deleted file mode 100644
index 46001c9812f6..000000000000
--- a/src/lib/core/annotations/field-value.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * Annotation Factory that allows HTML style boolean attributes. For example,
- * a field declared like this:
-
- * @Directive({ selector: 'component' }) class MyComponent {
- * @Input() @BooleanFieldValueFactory() myField: boolean;
- * }
- *
- * You could set it up this way:
- *
- * or:
- *
- * @deprecated
- */
-function booleanFieldValueFactory() {
- return function booleanFieldValueMetadata(target: any, key: string): void {
- const defaultValue = target[key];
- const localKey = `__md_private_symbol_${key}`;
- target[localKey] = defaultValue;
-
- Object.defineProperty(target, key, {
- get() { return (this)[localKey]; },
- set(value: boolean) {
- (this)[localKey] = value != null && `${value}` !== 'false';
- }
- });
- };
-}
-
-
-export { booleanFieldValueFactory as BooleanFieldValue };
diff --git a/src/lib/core/coersion/boolean-property.spec.ts b/src/lib/core/coersion/boolean-property.spec.ts
new file mode 100644
index 000000000000..b16c4ea75714
--- /dev/null
+++ b/src/lib/core/coersion/boolean-property.spec.ts
@@ -0,0 +1,48 @@
+import {coerceBooleanProperty} from './boolean-property';
+
+
+describe('coerceBooleanProperty', () => {
+ it('should coerce undefined to false', () => {
+ expect(coerceBooleanProperty(undefined)).toBe(false);
+ });
+
+ it('should coerce null to false', () => {
+ expect(coerceBooleanProperty(null)).toBe(false);
+ });
+
+ it('should coerce the empty string to true', () => {
+ expect(coerceBooleanProperty('')).toBe(true);
+ });
+
+ it('should coerce zero to true', () => {
+ expect(coerceBooleanProperty(0)).toBe(true);
+ });
+
+ it('should coerce the string "false" to false', () => {
+ expect(coerceBooleanProperty('false')).toBe(false);
+ });
+
+ it('should coerce the boolean false to false', () => {
+ expect(coerceBooleanProperty(false)).toBe(false);
+ });
+
+ it('should coerce the boolean true to true', () => {
+ expect(coerceBooleanProperty(true)).toBe(true);
+ });
+
+ it('should coerce the string "true" to true', () => {
+ expect(coerceBooleanProperty('true')).toBe(true);
+ });
+
+ it('should coerce an arbitrary string to true', () => {
+ expect(coerceBooleanProperty('pink')).toBe(true);
+ });
+
+ it('should coerce an object to true', () => {
+ expect(coerceBooleanProperty({})).toBe(true);
+ });
+
+ it('should coerce an array to true', () => {
+ expect(coerceBooleanProperty([])).toBe(true);
+ });
+});
diff --git a/src/lib/core/coersion/boolean-property.ts b/src/lib/core/coersion/boolean-property.ts
new file mode 100644
index 000000000000..eebe9f2f2d66
--- /dev/null
+++ b/src/lib/core/coersion/boolean-property.ts
@@ -0,0 +1,4 @@
+/** Coerces a data-bound value (typically a string) to a boolean. */
+export function coerceBooleanProperty(value: any): boolean {
+ return value != null && `${value}` !== 'false';
+}
diff --git a/src/lib/core/core.ts b/src/lib/core/core.ts
index 21bdb492d739..5802e3b40284 100644
--- a/src/lib/core/core.ts
+++ b/src/lib/core/core.ts
@@ -70,9 +70,6 @@ export {applyCssTransform} from './style/apply-transform';
// Error
export {MdError} from './errors/error';
-// Annotations.
-export {BooleanFieldValue} from './annotations/field-value';
-
// Misc
export {ComponentType} from './overlay/generic-component-type';
@@ -84,6 +81,9 @@ export * from './compatibility/style-compatibility';
// Animation
export * from './animation/animation';
+// Coersion
+export {coerceBooleanProperty} from './coersion/boolean-property';
+
@NgModule({
imports: [MdLineModule, RtlModule, MdRippleModule, PortalModule, OverlayModule, A11yModule],
diff --git a/src/lib/core/overlay/overlay-directives.spec.ts b/src/lib/core/overlay/overlay-directives.spec.ts
index 04b539d9550f..9fe8d4660ab3 100644
--- a/src/lib/core/overlay/overlay-directives.spec.ts
+++ b/src/lib/core/overlay/overlay-directives.spec.ts
@@ -27,38 +27,120 @@ describe('Overlay directives', () => {
fixture.detectChanges();
});
- it(`should create an overlay and attach the directive's template`, () => {
+ it(`should attach the overlay based on the open property`, () => {
+ fixture.componentInstance.isOpen = true;
+ fixture.detectChanges();
+
expect(overlayContainerElement.textContent).toContain('Menu content');
+
+ fixture.componentInstance.isOpen = false;
+ fixture.detectChanges();
+
+ expect(overlayContainerElement.textContent).toBe('');
});
it('should destroy the overlay when the directive is destroyed', () => {
+ fixture.componentInstance.isOpen = true;
+ fixture.detectChanges();
fixture.destroy();
expect(overlayContainerElement.textContent.trim()).toBe('');
});
it('should use a connected position strategy with a default set of positions', () => {
+ fixture.componentInstance.isOpen = true;
+ fixture.detectChanges();
+
let testComponent: ConnectedOverlayDirectiveTest =
fixture.debugElement.componentInstance;
let overlayDirective = testComponent.connectedOverlayDirective;
let strategy =
overlayDirective.overlayRef.getState().positionStrategy;
- expect(strategy) .toEqual(jasmine.any(ConnectedPositionStrategy));
+ expect(strategy).toEqual(jasmine.any(ConnectedPositionStrategy));
let positions = strategy.positions;
expect(positions.length).toBeGreaterThan(0);
});
+
+ describe('inputs', () => {
+
+ it('should set the width', () => {
+ fixture.componentInstance.width = 250;
+ fixture.componentInstance.isOpen = true;
+ fixture.detectChanges();
+
+ const pane = overlayContainerElement.children[0] as HTMLElement;
+ expect(pane.style.width).toEqual('250px');
+ });
+
+ it('should set the height', () => {
+ fixture.componentInstance.height = '100vh';
+ fixture.componentInstance.isOpen = true;
+ fixture.detectChanges();
+
+ const pane = overlayContainerElement.children[0] as HTMLElement;
+ expect(pane.style.height).toEqual('100vh');
+ });
+
+ it('should create the backdrop if designated', () => {
+ fixture.componentInstance.hasBackdrop = true;
+ fixture.componentInstance.isOpen = true;
+ fixture.detectChanges();
+
+ let backdrop = overlayContainerElement.querySelector('.md-overlay-backdrop');
+ expect(backdrop).toBeTruthy();
+ });
+
+ it('should not create the backdrop by default', () => {
+ fixture.componentInstance.isOpen = true;
+ fixture.detectChanges();
+
+ let backdrop = overlayContainerElement.querySelector('.md-overlay-backdrop');
+ expect(backdrop).toBeNull();
+ });
+
+ it('should set the custom backdrop class', () => {
+ fixture.componentInstance.hasBackdrop = true;
+ fixture.componentInstance.isOpen = true;
+ fixture.detectChanges();
+
+ const backdrop = overlayContainerElement.querySelector('.md-overlay-backdrop') as HTMLElement;
+ expect(backdrop.classList).toContain('md-test-class');
+ });
+
+ it('should emit backdropClick appropriately', () => {
+ fixture.componentInstance.hasBackdrop = true;
+ fixture.componentInstance.isOpen = true;
+ fixture.detectChanges();
+
+ const backdrop = overlayContainerElement.querySelector('.md-overlay-backdrop') as HTMLElement;
+ backdrop.click();
+ fixture.detectChanges();
+
+ expect(fixture.componentInstance.backdropClicked).toBe(true);
+ });
+
+ });
+
});
@Component({
template: `
-
+
Menu content
`,
})
class ConnectedOverlayDirectiveTest {
+ isOpen = false;
+ width: number | string;
+ height: number | string;
+ hasBackdrop: boolean;
+ backdropClicked = false;
+
@ViewChild(ConnectedOverlayDirective) connectedOverlayDirective: ConnectedOverlayDirective;
}
diff --git a/src/lib/core/overlay/overlay-directives.ts b/src/lib/core/overlay/overlay-directives.ts
index 53f082e27905..6661436ff05a 100644
--- a/src/lib/core/overlay/overlay-directives.ts
+++ b/src/lib/core/overlay/overlay-directives.ts
@@ -2,11 +2,13 @@ import {
NgModule,
ModuleWithProviders,
Directive,
+ EventEmitter,
TemplateRef,
ViewContainerRef,
- OnInit,
+ Optional,
Input,
OnDestroy,
+ Output,
ElementRef
} from '@angular/core';
import {Overlay, OVERLAY_PROVIDERS} from './overlay';
@@ -15,7 +17,9 @@ import {TemplatePortal} from '../portal/portal';
import {OverlayState} from './overlay-state';
import {ConnectionPositionPair} from './position/connected-position';
import {PortalModule} from '../portal/portal-directives';
-
+import {ConnectedPositionStrategy} from './position/connected-position-strategy';
+import {Subscription} from 'rxjs/Subscription';
+import {Dir, LayoutDirection} from '../rtl/dir';
/** Default set of positions for the overlay. Follows the behavior of a dropdown. */
let defaultPositionList = [
@@ -50,31 +54,68 @@ export class OverlayOrigin {
* Directive to facilitate declarative creation of an Overlay using a ConnectedPositionStrategy.
*/
@Directive({
- selector: '[connected-overlay]'
+ selector: '[connected-overlay]',
+ exportAs: 'connectedOverlay'
})
-export class ConnectedOverlayDirective implements OnInit, OnDestroy {
+export class ConnectedOverlayDirective implements OnDestroy {
private _overlayRef: OverlayRef;
private _templatePortal: TemplatePortal;
+ private _open = false;
+ private _hasBackdrop = false;
+ private _backdropSubscription: Subscription;
@Input() origin: OverlayOrigin;
@Input() positions: ConnectionPositionPair[];
+ /** The width of the overlay panel. */
+ @Input() width: number | string;
+
+ /** The height of the overlay panel. */
+ @Input() height: number | string;
+
+ /** The custom class to be set on the backdrop element. */
+ @Input() backdropClass: string;
+
+ /** Whether or not the overlay should attach a backdrop. */
+ @Input()
+ get hasBackdrop() {
+ return this._hasBackdrop;
+ }
+
+ // TODO: move the boolean coercion logic to a shared function in core
+ set hasBackdrop(value: any) {
+ this._hasBackdrop = value != null && `${value}` !== 'false';
+ }
+
+ @Input()
+ get open() {
+ return this._open;
+ }
+
+ set open(value: boolean) {
+ value ? this._attachOverlay() : this._detachOverlay();
+ this._open = value;
+ }
+
+ /** Event emitted when the backdrop is clicked. */
+ @Output() backdropClick: EventEmitter = new EventEmitter();
+
// TODO(jelbourn): inputs for size, scroll behavior, animation, etc.
constructor(
private _overlay: Overlay,
templateRef: TemplateRef,
- viewContainerRef: ViewContainerRef) {
+ viewContainerRef: ViewContainerRef,
+ @Optional() private _dir: Dir) {
this._templatePortal = new TemplatePortal(templateRef, viewContainerRef);
}
- get overlayRef() {
+ get overlayRef(): OverlayRef {
return this._overlayRef;
}
- /** TODO: internal */
- ngOnInit() {
- this._createOverlay();
+ get dir(): LayoutDirection {
+ return this._dir ? this._dir.value : 'ltr';
}
/** TODO: internal */
@@ -82,26 +123,87 @@ export class ConnectedOverlayDirective implements OnInit, OnDestroy {
this._destroyOverlay();
}
- /** Creates an overlay and attaches this directive's template to it. */
+ /** Creates an overlay */
private _createOverlay() {
if (!this.positions || !this.positions.length) {
this.positions = defaultPositionList;
}
+ this._overlayRef = this._overlay.create(this._buildConfig());
+ }
+
+ /** Builds the overlay config based on the directive's inputs */
+ private _buildConfig(): OverlayState {
let overlayConfig = new OverlayState();
- overlayConfig.positionStrategy =
- this._overlay.position().connectedTo(
- this.origin.elementRef,
- {originX: this.positions[0].overlayX, originY: this.positions[0].originY},
- {overlayX: this.positions[0].overlayX, overlayY: this.positions[0].overlayY});
- this._overlayRef = this._overlay.create(overlayConfig);
- this._overlayRef.attach(this._templatePortal);
+ if (this.width || this.width === 0) {
+ overlayConfig.width = this.width;
+ }
+
+ if (this.height || this.height === 0) {
+ overlayConfig.height = this.height;
+ }
+
+ overlayConfig.hasBackdrop = this.hasBackdrop;
+
+ if (this.backdropClass) {
+ overlayConfig.backdropClass = this.backdropClass;
+ }
+
+ overlayConfig.positionStrategy = this._getPosition();
+
+ overlayConfig.direction = this.dir;
+
+ return overlayConfig;
+ }
+
+ /** Returns the position of the overlay to be set on the overlay config */
+ private _getPosition(): ConnectedPositionStrategy {
+ return this._overlay.position().connectedTo(
+ this.origin.elementRef,
+ {originX: this.positions[0].overlayX, originY: this.positions[0].originY},
+ {overlayX: this.positions[0].overlayX, overlayY: this.positions[0].overlayY})
+ .setDirection(this.dir);
+ }
+
+ /** Attaches the overlay and subscribes to backdrop clicks if backdrop exists */
+ private _attachOverlay() {
+ if (!this._overlayRef) {
+ this._createOverlay();
+ }
+
+ if (!this._overlayRef.hasAttached()) {
+ this._overlayRef.attach(this._templatePortal);
+ }
+
+ if (this.hasBackdrop) {
+ this._backdropSubscription = this._overlayRef.backdropClick().subscribe(() => {
+ this.backdropClick.emit(null);
+ });
+ }
+ }
+
+ /** Detaches the overlay and unsubscribes to backdrop clicks if backdrop exists */
+ private _detachOverlay() {
+ if (this._overlayRef) {
+ this._overlayRef.detach();
+ }
+
+ if (this._backdropSubscription) {
+ this._backdropSubscription.unsubscribe();
+ this._backdropSubscription = null;
+ }
}
/** Destroys the overlay created by this directive. */
private _destroyOverlay() {
- this._overlayRef.dispose();
+ if (this._overlayRef) {
+ this._overlayRef.dispose();
+ }
+
+ if (this._backdropSubscription) {
+ this._backdropSubscription.unsubscribe();
+ }
}
}
diff --git a/src/lib/core/overlay/overlay-ref.ts b/src/lib/core/overlay/overlay-ref.ts
index aba1cac1e112..4ebaa2d2745e 100644
--- a/src/lib/core/overlay/overlay-ref.ts
+++ b/src/lib/core/overlay/overlay-ref.ts
@@ -23,6 +23,8 @@ export class OverlayRef implements PortalHost {
}
let attachResult = this._portalHost.attach(portal);
+ this.updateSize();
+ this.updateDirection();
this.updatePosition();
return attachResult;
@@ -58,6 +60,22 @@ export class OverlayRef implements PortalHost {
}
}
+ /** Updates the text direction of the overlay panel. **/
+ private updateDirection() {
+ this._pane.setAttribute('dir', this._state.direction);
+ }
+
+ /** Updates the size of the overlay based on the overlay config. */
+ updateSize() {
+ if (this._state.width || this._state.width === 0) {
+ this._pane.style.width = formatCssUnit(this._state.width);
+ }
+
+ if (this._state.height || this._state.height === 0) {
+ this._pane.style.height = formatCssUnit(this._state.height);
+ }
+ }
+
/** Attaches a backdrop for this overlay. */
private _attachBackdrop() {
this._backdropElement = document.createElement('div');
@@ -98,3 +116,7 @@ export class OverlayRef implements PortalHost {
}
}
}
+
+function formatCssUnit(value: number | string) {
+ return typeof value === 'string' ? value as string : `${value}px`;
+}
diff --git a/src/lib/core/overlay/overlay-state.ts b/src/lib/core/overlay/overlay-state.ts
index 95d9dcef3fbb..61b07fb05a16 100644
--- a/src/lib/core/overlay/overlay-state.ts
+++ b/src/lib/core/overlay/overlay-state.ts
@@ -1,4 +1,5 @@
import {PositionStrategy} from './position/position-strategy';
+import {LayoutDirection} from '../rtl/dir';
/**
@@ -12,10 +13,19 @@ export class OverlayState {
/** Whether the overlay has a backdrop. */
hasBackdrop: boolean = false;
+ /** Custom class to add to the backdrop **/
backdropClass: string = 'md-overlay-dark-backdrop';
+ /** The width of the overlay panel. If a number is provided, pixel units are assumed. **/
+ width: number | string;
+
+ /** The height of the overlay panel. If a number is provided, pixel units are assumed. **/
+ height: number | string;
+
+ /** The direction of the text in the overlay panel. */
+ direction: LayoutDirection = 'ltr';
+
// TODO(jelbourn): configuration still to add
- // - overlay size
// - focus trap
// - disable pointer events
// - z-index
diff --git a/src/lib/core/overlay/overlay.spec.ts b/src/lib/core/overlay/overlay.spec.ts
index 804ced72010c..b0b63402c4d5 100644
--- a/src/lib/core/overlay/overlay.spec.ts
+++ b/src/lib/core/overlay/overlay.spec.ts
@@ -82,6 +82,16 @@ describe('Overlay', () => {
expect(overlayContainerElement.textContent).toBe('');
});
+ it('should set the direction', () => {
+ const state = new OverlayState();
+ state.direction = 'rtl';
+
+ overlay.create(state).attach(componentPortal);
+
+ const pane = overlayContainerElement.children[0] as HTMLElement;
+ expect(pane.getAttribute('dir')).toEqual('rtl');
+ });
+
describe('positioning', () => {
let state: OverlayState;
@@ -98,6 +108,57 @@ describe('Overlay', () => {
});
});
+ describe('size', () => {
+ let state: OverlayState;
+
+ beforeEach(() => {
+ state = new OverlayState();
+ });
+
+ it('should apply the width set in the config', () => {
+ state.width = 500;
+
+ overlay.create(state).attach(componentPortal);
+ const pane = overlayContainerElement.children[0] as HTMLElement;
+ expect(pane.style.width).toEqual('500px');
+ });
+
+ it('should support using other units if a string width is provided', () => {
+ state.width = '200%';
+
+ overlay.create(state).attach(componentPortal);
+ const pane = overlayContainerElement.children[0] as HTMLElement;
+ expect(pane.style.width).toEqual('200%');
+ });
+
+ it('should apply the height set in the config', () => {
+ state.height = 500;
+
+ overlay.create(state).attach(componentPortal);
+ const pane = overlayContainerElement.children[0] as HTMLElement;
+ expect(pane.style.height).toEqual('500px');
+ });
+
+ it('should support using other units if a string height is provided', () => {
+ state.height = '100vh';
+
+ overlay.create(state).attach(componentPortal);
+ const pane = overlayContainerElement.children[0] as HTMLElement;
+ expect(pane.style.height).toEqual('100vh');
+ });
+
+ it('should support zero widths and heights', () => {
+ state.width = 0;
+ state.height = 0;
+
+ overlay.create(state).attach(componentPortal);
+ const pane = overlayContainerElement.children[0] as HTMLElement;
+ expect(pane.style.width).toEqual('0px');
+ expect(pane.style.height).toEqual('0px');
+ });
+
+ });
+
describe('backdrop', () => {
let config: OverlayState;
diff --git a/src/lib/core/overlay/position/connected-position-strategy.spec.ts b/src/lib/core/overlay/position/connected-position-strategy.spec.ts
index e5a47c22fb99..1ef65ee3d7cb 100644
--- a/src/lib/core/overlay/position/connected-position-strategy.spec.ts
+++ b/src/lib/core/overlay/position/connected-position-strategy.spec.ts
@@ -206,6 +206,23 @@ describe('ConnectedPositionStrategy', () => {
expect(overlayRect.top).toBe(originRect.bottom);
expect(overlayRect.right).toBe(originRect.left);
});
+
+ it('should position a panel properly when rtl', () => {
+ // must make the overlay longer than the origin to properly test attachment
+ overlayElement.style.width = `500px`;
+ originRect = originElement.getBoundingClientRect();
+ strategy = positionBuilder.connectedTo(
+ fakeElementRef,
+ {originX: 'start', originY: 'bottom'},
+ {overlayX: 'start', overlayY: 'top'})
+ .setDirection('rtl');
+
+ strategy.apply(overlayElement);
+
+ let overlayRect = overlayElement.getBoundingClientRect();
+ expect(overlayRect.top).toBe(originRect.bottom);
+ expect(overlayRect.right).toBe(originRect.right);
+ });
});
diff --git a/src/lib/core/overlay/position/connected-position-strategy.ts b/src/lib/core/overlay/position/connected-position-strategy.ts
index 89054ea84938..d46f1d48f353 100644
--- a/src/lib/core/overlay/position/connected-position-strategy.ts
+++ b/src/lib/core/overlay/position/connected-position-strategy.ts
@@ -17,9 +17,12 @@ import {
* of the overlay.
*/
export class ConnectedPositionStrategy implements PositionStrategy {
- // TODO(jelbourn): set RTL to the actual value from the app.
+ private _dir = 'ltr';
+
/** Whether the we're dealing with an RTL context */
- _isRtl: boolean = false;
+ get _isRtl() {
+ return this._dir === 'rtl';
+ }
/** Ordered list of preferred positions, from most to least desirable. */
_preferredPositions: ConnectionPositionPair[] = [];
@@ -85,6 +88,11 @@ export class ConnectedPositionStrategy implements PositionStrategy {
return this;
}
+ /** Sets the layout direction so the overlay's position can be adjusted to match. */
+ setDirection(dir: 'ltr' | 'rtl') {
+ this._dir = dir;
+ return this;
+ }
/**
* Gets the horizontal (x) "start" dimension based on whether the overlay is in an RTL context.
@@ -146,8 +154,10 @@ export class ConnectedPositionStrategy implements PositionStrategy {
let overlayStartX: number;
if (pos.overlayX == 'center') {
overlayStartX = -overlayRect.width / 2;
+ } else if (pos.overlayX === 'start') {
+ overlayStartX = this._isRtl ? -overlayRect.width : 0;
} else {
- overlayStartX = pos.overlayX == 'start' ? 0 : -overlayRect.width;
+ overlayStartX = this._isRtl ? 0 : -overlayRect.width;
}
let overlayStartY: number;
diff --git a/src/lib/core/style/_menu-common.scss b/src/lib/core/style/_menu-common.scss
new file mode 100644
index 000000000000..c010a34e0a65
--- /dev/null
+++ b/src/lib/core/style/_menu-common.scss
@@ -0,0 +1,45 @@
+@import './variables';
+@import './elevation';
+@import './list-common';
+
+/** The mixins below are shared between md-menu and md-select */
+
+// menu width must be a multiple of 56px
+$md-menu-overlay-min-width: 112px !default; // 56 * 2
+$md-menu-overlay-max-width: 280px !default; // 56 * 5
+
+$md-menu-item-height: 48px !default;
+$md-menu-font-size: 16px !default;
+$md-menu-side-padding: 16px !default;
+$md-menu-vertical-padding: 8px !default;
+
+@mixin md-menu-base() {
+ @include md-elevation(2);
+ min-width: $md-menu-overlay-min-width;
+ max-width: $md-menu-overlay-max-width;
+
+ overflow: auto;
+ -webkit-overflow-scrolling: touch; // for momentum scroll on mobile
+
+ padding-top: $md-menu-vertical-padding;
+ padding-bottom: $md-menu-vertical-padding;
+}
+
+@mixin md-menu-item-base() {
+ @include md-truncate-line();
+
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ height: $md-menu-item-height;
+ padding: 0 $md-menu-side-padding;
+
+ font-size: $md-menu-font-size;
+ font-family: $md-font-family;
+ text-align: start;
+ text-decoration: none; // necessary to reset anchor tags
+
+ &[disabled] {
+ cursor: default;
+ }
+}
diff --git a/src/lib/core/theming/_all-theme.scss b/src/lib/core/theming/_all-theme.scss
index a4a03a561dc5..084e2585350e 100644
--- a/src/lib/core/theming/_all-theme.scss
+++ b/src/lib/core/theming/_all-theme.scss
@@ -13,6 +13,7 @@
@import '../../progress-bar/progress-bar-theme';
@import '../../progress-circle/progress-circle-theme';
@import '../../radio/radio-theme';
+@import '../../select/select-theme';
@import '../../sidenav/sidenav-theme';
@import '../../slide-toggle/slide-toggle-theme';
@import '../../slider/slider-theme';
@@ -37,6 +38,7 @@
@include md-progress-bar-theme($theme);
@include md-progress-circle-theme($theme);
@include md-radio-theme($theme);
+ @include md-select-theme($theme);
@include md-sidenav-theme($theme);
@include md-slide-toggle-theme($theme);
@include md-slider-theme($theme);
diff --git a/src/lib/dialog/README.md b/src/lib/dialog/README.md
index e69de29bb2d1..8634a3b5b38e 100644
--- a/src/lib/dialog/README.md
+++ b/src/lib/dialog/README.md
@@ -0,0 +1,88 @@
+# MdDialog
+
+MdDialog is a service, which opens dialogs components in the view.
+
+### Methods
+
+| Name | Description |
+| --- | --- |
+| `open(component: ComponentType, config: MdDialogConfig): MdDialogRef` | Creates and opens a dialog matching material spec. |
+
+### Config
+
+| Key | Description |
+| --- | --- |
+| `viewContainerRef: ViewContainerRef` | The view container ref to attach the dialog to. |
+| `role: DialogRole = 'dialog'` | The ARIA role of the dialog element. Possible values are `dialog` and `alertdialog`. Defaults to `dialog`. |
+
+## MdDialogRef
+
+A reference to the dialog created by the MdDialog `open` method.
+
+### Methods
+
+| Name | Description |
+| --- | --- |
+| `close(dialogResult?: any)` | Closes the dialog, pushing a value to the afterClosed observable. |
+| `afterClosed(): Observable` | Returns an observable which will emit the dialog result, passed to the `close` method above. |
+
+## Example
+The service can be injected in a component.
+
+```ts
+@Component({
+ selector: 'pizza-component',
+ template: `
+
+ `
+})
+export class PizzaComponent {
+
+ dialogRef: MdDialogRef;
+
+ constructor(
+ public dialog: MdDialog,
+ public viewContainerRef: ViewContainerRef) { }
+
+ openDialog() {
+ let config = new MdDialogConfig();
+ config.viewContainerRef = this.viewContainerRef;
+
+ this.dialogRef = this.dialog.open(PizzaDialog, config);
+
+ this.dialogRef.afterClosed().subscribe(result => {
+ console.log('result: ' + result);
+ this.dialogRef = null;
+ });
+ }
+}
+
+@Component({
+ selector: 'pizza-dialog',
+ template: `
+
+
+ `
+})
+export class PizzaDialog {
+ constructor(public dialogRef: MdDialogRef) { }
+}
+```
+
+The dialog component should be declared in the list of entry components of the module:
+
+```ts
+@NgModule({
+ declarations: [
+ ...,
+ PizzaDialog
+ ],
+ entryComponents: [
+ ...,
+ PizzaDialog
+ ],
+ ...
+})
+export class AppModule { }
+
+```
diff --git a/src/lib/input/input.ts b/src/lib/input/input.ts
index 66c4fbe4bec4..4abbf4148b05 100644
--- a/src/lib/input/input.ts
+++ b/src/lib/input/input.ts
@@ -18,13 +18,9 @@ import {
ModuleWithProviders,
ViewEncapsulation,
} from '@angular/core';
-import {
- NG_VALUE_ACCESSOR,
- ControlValueAccessor,
- FormsModule,
-} from '@angular/forms';
+import {NG_VALUE_ACCESSOR, ControlValueAccessor, FormsModule} from '@angular/forms';
import {CommonModule} from '@angular/common';
-import {BooleanFieldValue, MdError} from '../core';
+import {MdError, coerceBooleanProperty} from '../core';
import {Observable} from 'rxjs/Observable';
@@ -118,9 +114,22 @@ export class MdInput implements ControlValueAccessor, AfterContentInit, OnChange
*/
@Input('aria-label') ariaLabel: string;
@Input('aria-labelledby') ariaLabelledBy: string;
- @Input('aria-disabled') @BooleanFieldValue() ariaDisabled: boolean;
- @Input('aria-required') @BooleanFieldValue() ariaRequired: boolean;
- @Input('aria-invalid') @BooleanFieldValue() ariaInvalid: boolean;
+
+ private _ariaDisabled: boolean;
+ private _ariaRequired: boolean;
+ private _ariaInvalid: boolean;
+
+ @Input('aria-disabled')
+ get ariaDisabled(): boolean { return this._ariaDisabled; }
+ set ariaDisabled(value) { this._ariaDisabled = coerceBooleanProperty(value); }
+
+ @Input('aria-required')
+ get ariaRequired(): boolean { return this._ariaRequired; }
+ set ariaRequired(value) { this._ariaRequired = coerceBooleanProperty(value); }
+
+ @Input('aria-invalid')
+ get ariaInvalid(): boolean { return this._ariaInvalid; }
+ set ariaInvalid(value) { this._ariaInvalid = coerceBooleanProperty(value); }
/**
* Content directives.
@@ -141,14 +150,11 @@ export class MdInput implements ControlValueAccessor, AfterContentInit, OnChange
*/
@Input() align: 'start' | 'end' = 'start';
@Input() dividerColor: 'primary' | 'accent' | 'warn' = 'primary';
- @Input() @BooleanFieldValue() floatingPlaceholder: boolean = true;
@Input() hintLabel: string = '';
@Input() autocomplete: string;
@Input() autocorrect: string;
@Input() autocapitalize: string;
- @Input() @BooleanFieldValue() autofocus: boolean = false;
- @Input() @BooleanFieldValue() disabled: boolean = false;
@Input() id: string = `md-input-${nextUniqueId++}`;
@Input() list: string = null;
@Input() max: string | number = null;
@@ -156,14 +162,43 @@ export class MdInput implements ControlValueAccessor, AfterContentInit, OnChange
@Input() min: string | number = null;
@Input() minlength: number = null;
@Input() placeholder: string = null;
- @Input() @BooleanFieldValue() readonly: boolean = false;
- @Input() @BooleanFieldValue() required: boolean = false;
- @Input() @BooleanFieldValue() spellcheck: boolean = false;
@Input() step: number = null;
@Input() tabindex: number = null;
@Input() type: string = 'text';
@Input() name: string = null;
+ private _floatingPlaceholder: boolean = false;
+ private _autofocus: boolean = false;
+ private _disabled: boolean = false;
+ private _readonly: boolean = false;
+ private _required: boolean = false;
+ private _spellcheck: boolean = false;
+
+ @Input()
+ get floatingPlaceholder(): boolean { return this._floatingPlaceholder; }
+ set floatingPlaceholder(value) { this._floatingPlaceholder = coerceBooleanProperty(value); }
+
+ @Input()
+ get autofocus(): boolean { return this._autofocus; }
+ set autofocus(value) { this._autofocus = coerceBooleanProperty(value); }
+
+ @Input()
+ get disabled(): boolean { return this._disabled; }
+ set disabled(value) { this._disabled = coerceBooleanProperty(value); }
+
+ @Input()
+ get readonly(): boolean { return this._readonly; }
+ set readonly(value) { this._readonly = coerceBooleanProperty(value); }
+
+ @Input()
+ get required(): boolean { return this._required; }
+ set required(value) { this._required = coerceBooleanProperty(value); }
+
+ @Input()
+ get spellcheck(): boolean { return this._spellcheck; }
+ set spellcheck(value) { this._spellcheck = coerceBooleanProperty(value); }
+
+
private _blurEmitter: EventEmitter = new EventEmitter();
private _focusEmitter: EventEmitter = new EventEmitter();
diff --git a/src/lib/menu/menu-directive.ts b/src/lib/menu/menu-directive.ts
index 2a3eeaa36b7c..b1bc3ab78321 100644
--- a/src/lib/menu/menu-directive.ts
+++ b/src/lib/menu/menu-directive.ts
@@ -15,7 +15,7 @@ import {
import {MenuPositionX, MenuPositionY} from './menu-positions';
import {MdMenuInvalidPositionX, MdMenuInvalidPositionY} from './menu-errors';
import {MdMenuItem} from './menu-item';
-import {UP_ARROW, DOWN_ARROW, TAB} from '../core';
+import {ListKeyManager} from '../core/a11y/list-key-manager';
@Component({
moduleId: module.id,
@@ -27,7 +27,7 @@ import {UP_ARROW, DOWN_ARROW, TAB} from '../core';
exportAs: 'mdMenu'
})
export class MdMenu {
- private _focusedItemIndex: number = 0;
+ private _keyManager: ListKeyManager;
// config object to be passed into the menu's ngClass
_classList: Object;
@@ -44,6 +44,11 @@ export class MdMenu {
if (posY) { this._setPositionY(posY); }
}
+ ngAfterContentInit() {
+ this._keyManager = new ListKeyManager(this.items);
+ this._keyManager.tabOut.subscribe(() => this._emitCloseEvent());
+ }
+
/**
* This method takes classes set on the host md-menu element and applies them on the
* menu template that displays in the overlay container. Otherwise, it's difficult
@@ -66,62 +71,18 @@ export class MdMenu {
* TODO: internal
*/
_focusFirstItem() {
+ // The menu always opens with the first item focused.
this.items.first.focus();
+ this._keyManager.focusedItemIndex = 0;
}
-
- // TODO(kara): update this when (keydown.downArrow) testability is fixed
- // TODO: internal
- _handleKeydown(event: KeyboardEvent): void {
- if (event.keyCode === DOWN_ARROW) {
- this._focusNextItem();
- } else if (event.keyCode === UP_ARROW) {
- this._focusPreviousItem();
- } else if (event.keyCode === TAB) {
- this._emitCloseEvent();
- }
- }
-
/**
* This emits a close event to which the trigger is subscribed. When emitted, the
* trigger will close the menu.
*/
private _emitCloseEvent(): void {
- this._focusedItemIndex = 0;
this.close.emit(null);
}
- private _focusNextItem(): void {
- this._updateFocusedItemIndex(1);
- this.items.toArray()[this._focusedItemIndex].focus();
- }
-
- private _focusPreviousItem(): void {
- this._updateFocusedItemIndex(-1);
- this.items.toArray()[this._focusedItemIndex].focus();
- }
-
- /**
- * This method sets focus to the correct menu item, given a list of menu items and the delta
- * between the currently focused menu item and the new menu item to be focused. It will
- * continue to move down the list until it finds an item that is not disabled, and it will wrap
- * if it encounters either end of the menu.
- *
- * @param delta the desired change in focus index
- * @param menuItems the menu items that should be focused
- * @private
- */
- private _updateFocusedItemIndex(delta: number, menuItems: MdMenuItem[] = this.items.toArray()) {
- // when focus would leave menu, wrap to beginning or end
- this._focusedItemIndex = (this._focusedItemIndex + delta + this.items.length)
- % this.items.length;
-
- // skip all disabled menu items recursively until an active one
- // is reached or the menu closes for overreaching bounds
- while (menuItems[this._focusedItemIndex].disabled) {
- this._updateFocusedItemIndex(delta, menuItems);
- }
- }
-
private _setPositionX(pos: MenuPositionX): void {
if ( pos !== 'before' && pos !== 'after') {
throw new MdMenuInvalidPositionX();
diff --git a/src/lib/menu/menu-item.ts b/src/lib/menu/menu-item.ts
index 2b70779ebbf6..06df0d60a9c1 100644
--- a/src/lib/menu/menu-item.ts
+++ b/src/lib/menu/menu-item.ts
@@ -1,4 +1,5 @@
import {Directive, ElementRef, Input, HostBinding, Renderer} from '@angular/core';
+import {MdFocusable} from '../core/a11y/list-key-manager';
/**
* This directive is intended to be used inside an md-menu tag.
@@ -13,7 +14,7 @@ import {Directive, ElementRef, Input, HostBinding, Renderer} from '@angular/core
},
exportAs: 'mdMenuItem'
})
-export class MdMenuItem {
+export class MdMenuItem implements MdFocusable {
_disabled: boolean;
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
diff --git a/src/lib/menu/menu.html b/src/lib/menu/menu.html
index 58721749878e..f23266c05da9 100644
--- a/src/lib/menu/menu.html
+++ b/src/lib/menu/menu.html
@@ -1,6 +1,6 @@
diff --git a/src/lib/menu/menu.scss b/src/lib/menu/menu.scss
index fafa964c6c22..061927531435 100644
--- a/src/lib/menu/menu.scss
+++ b/src/lib/menu/menu.scss
@@ -1,55 +1,22 @@
// TODO(kara): update vars for desktop when MD team responds
// TODO(kara): animation for menu opening
-@import '../core/style/variables';
-@import '../core/style/elevation';
+
@import '../core/style/button-common';
@import '../core/style/sidenav-common';
-@import '../core/style/list-common';
-
-
-// menu width must be a multiple of 56px
-$md-menu-overlay-min-width: 112px !default; // 56 * 2
-$md-menu-overlay-max-width: 280px !default; // 56 * 5
-
-$md-menu-item-height: 48px !default;
-$md-menu-font-size: 16px !default;
-$md-menu-side-padding: 16px !default;
-$md-menu-vertical-padding: 8px !default;
+@import '../core/style/menu-common';
.md-menu-panel {
- @include md-elevation(2);
- min-width: $md-menu-overlay-min-width;
- max-width: $md-menu-overlay-max-width;
+ @include md-menu-base();
// max height must be 100% of the viewport height + one row height
max-height: calc(100vh + 48px);
- overflow: auto;
- -webkit-overflow-scrolling: touch; // for momentum scroll on mobile
-
- padding-top: $md-menu-vertical-padding;
- padding-bottom: $md-menu-vertical-padding;
}
[md-menu-item] {
@include md-button-reset();
- @include md-truncate-line();
-
- display: flex;
- flex-direction: row;
- align-items: center;
- height: $md-menu-item-height;
- padding: 0 $md-menu-side-padding;
-
- font-size: $md-menu-font-size;
- font-family: $md-font-family;
- text-align: start;
- text-decoration: none; // necessary to reset anchor tags
-
- &[disabled] {
- cursor: default;
- }
+ @include md-menu-item-base();
}
button[md-menu-item] {
width: 100%;
-}
\ No newline at end of file
+}
diff --git a/src/lib/package.json b/src/lib/package.json
index 2714448bc147..dafec68919d1 100644
--- a/src/lib/package.json
+++ b/src/lib/package.json
@@ -22,7 +22,8 @@
"homepage": "https://github.com/angular/material2#readme",
"peerDependencies": {
"@angular/core": "^2.0.0",
- "@angular/common": "^2.0.0"
+ "@angular/common": "^2.0.0",
+ "@angular/http": "^2.0.0"
},
"dependencies": {
"@types/hammerjs": "^2.0.30"
diff --git a/src/lib/radio/radio.spec.ts b/src/lib/radio/radio.spec.ts
index af7303fa1939..7f15ba06feb6 100644
--- a/src/lib/radio/radio.spec.ts
+++ b/src/lib/radio/radio.spec.ts
@@ -1,4 +1,4 @@
-import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {async, ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing';
import {NgControl, FormsModule} from '@angular/forms';
import {Component, DebugElement} from '@angular/core';
import {By} from '@angular/platform-browser';
@@ -176,12 +176,12 @@ describe('MdRadio', () => {
expect(nativeRadioInput.classList).not.toContain('md-radio-focused');
- dispatchFocusChangeEvent('focus', nativeRadioInput);
+ dispatchEvent('focus', nativeRadioInput);
fixture.detectChanges();
expect(radioNativeElements[0].classList).toContain('md-radio-focused');
- dispatchFocusChangeEvent('blur', nativeRadioInput);
+ dispatchEvent('blur', nativeRadioInput);
fixture.detectChanges();
expect(radioNativeElements[0].classList).not.toContain('md-radio-focused');
@@ -223,7 +223,7 @@ describe('MdRadio', () => {
let groupDebugElement: DebugElement;
let groupNativeElement: HTMLElement;
let radioDebugElements: DebugElement[];
- let radioNativeElements: HTMLElement[];
+ let innerRadios: DebugElement[];
let radioLabelElements: HTMLLabelElement[];
let groupInstance: MdRadioGroup;
let radioInstances: MdRadioButton[];
@@ -242,8 +242,8 @@ describe('MdRadio', () => {
groupNgControl = groupDebugElement.injector.get(NgControl);
radioDebugElements = fixture.debugElement.queryAll(By.directive(MdRadioButton));
- radioNativeElements = radioDebugElements.map(debugEl => debugEl.nativeElement);
radioInstances = radioDebugElements.map(debugEl => debugEl.componentInstance);
+ innerRadios = fixture.debugElement.queryAll(By.css('input[type="radio"]'));
radioLabelElements = radioDebugElements
.map(debugEl => debugEl.query(By.css('label')).nativeElement);
@@ -280,16 +280,16 @@ describe('MdRadio', () => {
expect(groupNgControl.pristine).toBe(true);
expect(groupNgControl.touched).toBe(false);
- // After changing the value programmatically, the control should become dirty (not pristine),
+ // After changing the value programmatically, the control should stay pristine
// but remain untouched.
radioInstances[1].checked = true;
fixture.detectChanges();
expect(groupNgControl.valid).toBe(true);
- expect(groupNgControl.pristine).toBe(false);
+ expect(groupNgControl.pristine).toBe(true);
expect(groupNgControl.touched).toBe(false);
- // After a user interaction occurs (such as a click), the control should remain dirty and
+ // After a user interaction occurs (such as a click), the control should become dirty and
// now also be touched.
radioLabelElements[2].click();
fixture.detectChanges();
@@ -299,10 +299,18 @@ describe('MdRadio', () => {
expect(groupNgControl.touched).toBe(true);
});
- it('should update the ngModel value when selecting a radio button', () => {
- radioInstances[1].checked = true;
+ it('should write to the radio button based on ngModel', fakeAsync(() => {
+ testComponent.modelValue = 'chocolate';
+ fixture.detectChanges();
+ tick();
fixture.detectChanges();
+ expect(innerRadios[1].nativeElement.checked).toBe(true);
+ }));
+
+ it('should update the ngModel value when selecting a radio button', () => {
+ dispatchEvent('change', innerRadios[1].nativeElement);
+ fixture.detectChanges();
expect(testComponent.modelValue).toBe('chocolate');
});
@@ -310,16 +318,12 @@ describe('MdRadio', () => {
expect(testComponent.modelValue).toBeUndefined();
expect(testComponent.lastEvent).toBeUndefined();
- groupInstance.value = 'chocolate';
+ dispatchEvent('change', innerRadios[1].nativeElement);
fixture.detectChanges();
-
- expect(testComponent.modelValue).toBe('chocolate');
expect(testComponent.lastEvent.value).toBe('chocolate');
- groupInstance.value = 'vanilla';
+ dispatchEvent('change', innerRadios[0].nativeElement);
fixture.detectChanges();
-
- expect(testComponent.modelValue).toBe('vanilla');
expect(testComponent.lastEvent.value).toBe('vanilla');
});
});
@@ -484,14 +488,14 @@ class RadioGroupWithNgModel {
lastEvent: MdRadioChange;
}
-// TODO(jelbourn): remove eveything below when Angular supports faking events.
+// TODO(jelbourn): remove everything below when Angular supports faking events.
/**
- * Dispatches a focus change event from an element.
- * @param eventName Name of the event, either 'focus' or 'blur'.
+ * Dispatches an event from an element.
+ * @param eventName Name of the event
* @param element The element from which the event will be dispatched.
*/
-function dispatchFocusChangeEvent(eventName: string, element: HTMLElement): void {
+function dispatchEvent(eventName: string, element: HTMLElement): void {
let event = document.createEvent('Event');
event.initEvent(eventName, true, true);
element.dispatchEvent(event);
diff --git a/src/lib/radio/radio.ts b/src/lib/radio/radio.ts
index 125943f9589e..20d43c5f6671 100644
--- a/src/lib/radio/radio.ts
+++ b/src/lib/radio/radio.ts
@@ -78,7 +78,7 @@ export class MdRadioGroup implements AfterContentInit, ControlValueAccessor {
private _isInitialized: boolean = false;
/** The method to be called in order to update ngModel */
- private _controlValueAccessorChangeFn: (value: any) => void = (value) => {};
+ _controlValueAccessorChangeFn: (value: any) => void = (value) => {};
/** onTouch function registered via registerOnTouch (ControlValueAccessor). */
onTouched: () => any = () => {};
@@ -198,7 +198,6 @@ export class MdRadioGroup implements AfterContentInit, ControlValueAccessor {
let event = new MdRadioChange();
event.source = this._selected;
event.value = this._value;
- this._controlValueAccessorChangeFn(event.value);
this.change.emit(event);
}
@@ -405,6 +404,7 @@ export class MdRadioButton implements OnInit {
event.stopPropagation();
this.checked = true;
+ this.radioGroup._controlValueAccessorChangeFn(this.value);
this._emitChangeEvent();
if (this.radioGroup) {
diff --git a/src/lib/select/_select-theme.scss b/src/lib/select/_select-theme.scss
new file mode 100644
index 000000000000..78ac7a3f8cdf
--- /dev/null
+++ b/src/lib/select/_select-theme.scss
@@ -0,0 +1,6 @@
+@import '../core/theming/palette';
+@import '../core/theming/theming';
+
+@mixin md-select-theme($theme) {
+
+}
diff --git a/src/lib/select/index.ts b/src/lib/select/index.ts
index 9495a665b235..da05b1ee80ff 100644
--- a/src/lib/select/index.ts
+++ b/src/lib/select/index.ts
@@ -1,11 +1,12 @@
import {NgModule, ModuleWithProviders} from '@angular/core';
import {MdSelect} from './select';
+import {MdOption} from './option';
export * from './select';
@NgModule({
imports: [],
- exports: [MdSelect],
- declarations: [MdSelect],
+ exports: [MdSelect, MdOption],
+ declarations: [MdSelect, MdOption],
})
export class MdSelectModule {
static forRoot(): ModuleWithProviders {
diff --git a/src/lib/select/option.ts b/src/lib/select/option.ts
new file mode 100644
index 000000000000..ac716c040054
--- /dev/null
+++ b/src/lib/select/option.ts
@@ -0,0 +1,10 @@
+import {Component, ViewEncapsulation} from '@angular/core';
+
+@Component({
+ moduleId: module.id,
+ selector: 'md-option',
+ template: ``,
+ styleUrls: ['select.css'],
+ encapsulation: ViewEncapsulation.None
+})
+export class MdOption {}
diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts
index b38b95e1439b..7e5cb52374f2 100644
--- a/src/lib/select/select.ts
+++ b/src/lib/select/select.ts
@@ -1,10 +1,10 @@
import {Component, ViewEncapsulation} from '@angular/core';
@Component({
- moduleId: module.id,
- selector: 'md-select',
- templateUrl: 'select.html',
- styleUrls: ['select.css'],
- encapsulation: ViewEncapsulation.None
+ moduleId: module.id,
+ selector: 'md-select',
+ templateUrl: 'select.html',
+ styleUrls: ['select.css'],
+ encapsulation: ViewEncapsulation.None
})
export class MdSelect {}
diff --git a/src/lib/sidenav/sidenav.ts b/src/lib/sidenav/sidenav.ts
index 567c687d7f6e..ebba37cd54d7 100644
--- a/src/lib/sidenav/sidenav.ts
+++ b/src/lib/sidenav/sidenav.ts
@@ -16,7 +16,8 @@ import {
ViewEncapsulation,
} from '@angular/core';
import {CommonModule} from '@angular/common';
-import {Dir, MdError} from '../core';
+import {Dir, MdError, coerceBooleanProperty} from '../core';
+
/** Exception thrown when two MdSidenav are matching the same side. */
export class MdDuplicatedSidenavError extends MdError {
@@ -79,9 +80,7 @@ export class MdSidenav {
@Input()
get opened(): boolean { return this._opened; }
set opened(v: boolean) {
- // TODO(jelbourn): this coercion goes away when BooleanFieldValue is removed.
- let booleanValue = v != null && `${v}` !== 'false';
- this.toggle(booleanValue);
+ this.toggle(coerceBooleanProperty(v));
}
diff --git a/src/lib/slide-toggle/slide-toggle.ts b/src/lib/slide-toggle/slide-toggle.ts
index a8aaa1d07223..66632b784156 100644
--- a/src/lib/slide-toggle/slide-toggle.ts
+++ b/src/lib/slide-toggle/slide-toggle.ts
@@ -1,326 +1,329 @@
-import {
- Component,
- ElementRef,
- Renderer,
- forwardRef,
- ChangeDetectionStrategy,
- Input,
- Output,
- EventEmitter,
- AfterContentInit,
- NgModule,
- ModuleWithProviders,
- ViewEncapsulation,
-} from '@angular/core';
-import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
-import {
- FormsModule,
- ControlValueAccessor,
- NG_VALUE_ACCESSOR
-} from '@angular/forms';
-import {BooleanFieldValue, applyCssTransform} from '../core';
-import {Observable} from 'rxjs/Observable';
-import {MdGestureConfig} from '../core';
-
-
-export const MD_SLIDE_TOGGLE_VALUE_ACCESSOR: any = {
- provide: NG_VALUE_ACCESSOR,
- useExisting: forwardRef(() => MdSlideToggle),
- multi: true
-};
-
-// A simple change event emitted by the MdSlideToggle component.
-export class MdSlideToggleChange {
- source: MdSlideToggle;
- checked: boolean;
-}
-
-// Increasing integer for generating unique ids for slide-toggle components.
-let nextId = 0;
-
-@Component({
- moduleId: module.id,
- selector: 'md-slide-toggle',
- host: {
- '[class.md-checked]': 'checked',
- '[class.md-disabled]': 'disabled',
- // This md-slide-toggle prefix will change, once the temporary ripple is removed.
- '[class.md-slide-toggle-focused]': '_hasFocus',
- '(mousedown)': '_setMousedown()'
- },
- templateUrl: 'slide-toggle.html',
- styleUrls: ['slide-toggle.css'],
- providers: [MD_SLIDE_TOGGLE_VALUE_ACCESSOR],
- encapsulation: ViewEncapsulation.None,
- changeDetection: ChangeDetectionStrategy.OnPush
-})
-export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
-
- private onChange = (_: any) => {};
- private onTouched = () => {};
-
- // A unique id for the slide-toggle. By default the id is auto-generated.
- private _uniqueId = `md-slide-toggle-${++nextId}`;
- private _checked: boolean = false;
- private _color: string;
- private _isMousedown: boolean = false;
- private _slideRenderer: SlideToggleRenderer = null;
-
- // Needs to be public to support AOT compilation (as host binding).
- _hasFocus: boolean = false;
-
- @Input() @BooleanFieldValue() disabled: boolean = false;
- @Input() @BooleanFieldValue() required: boolean = false;
- @Input() name: string = null;
- @Input() id: string = this._uniqueId;
- @Input() tabIndex: number = 0;
- @Input() ariaLabel: string = null;
- @Input() ariaLabelledby: string = null;
-
- private _change: EventEmitter = new EventEmitter();
- @Output() change: Observable = this._change.asObservable();
-
- // Returns the unique id for the visual hidden input.
- getInputId = () => `${this.id || this._uniqueId}-input`;
-
- constructor(private _elementRef: ElementRef, private _renderer: Renderer) {}
-
- /** TODO: internal */
- ngAfterContentInit() {
- this._slideRenderer = new SlideToggleRenderer(this._elementRef);
- }
-
- /**
- * The onChangeEvent method will be also called on click.
- * This is because everything for the slide-toggle is wrapped inside of a label,
- * which triggers a onChange event on click.
- */
- _onChangeEvent(event: Event) {
- // We always have to stop propagation on the change event.
- // Otherwise the change event, from the input element, will bubble up and
- // emit its event object to the component's `change` output.
- event.stopPropagation();
-
- // Once a drag is currently in progress, we do not want to toggle the slide-toggle on a click.
- if (!this.disabled && !this._slideRenderer.isDragging()) {
- this.toggle();
-
- // Emit our custom change event if the native input emitted one.
- // It is important to only emit it, if the native input triggered one, because
- // we don't want to trigger a change event, when the `checked` variable changes for example.
- this._emitChangeEvent();
- }
- }
-
- _onInputClick(event: Event) {
- this.onTouched();
-
- // We have to stop propagation for click events on the visual hidden input element.
- // By default, when a user clicks on a label element, a generated click event will be
- // dispatched on the associated input element. Since we are using a label element as our
- // root container, the click event on the `slide-toggle` will be executed twice.
- // The real click event will bubble up, and the generated click event also tries to bubble up.
- // This will lead to multiple click events.
- // Preventing bubbling for the second event will solve that issue.
- event.stopPropagation();
- }
-
- _setMousedown() {
- // We only *show* the focus style when focus has come to the button via the keyboard.
- // The Material Design spec is silent on this topic, and without doing this, the
- // button continues to look :active after clicking.
- // @see http://marcysutton.com/button-focus-hell/
- this._isMousedown = true;
- setTimeout(() => this._isMousedown = false, 100);
- }
-
- _onInputFocus() {
- // Only show the focus / ripple indicator when the focus was not triggered by a mouse
- // interaction on the component.
- if (!this._isMousedown) {
- this._hasFocus = true;
- }
- }
-
- _onInputBlur() {
- this._hasFocus = false;
- this.onTouched();
- }
-
- /**
- * Implemented as part of ControlValueAccessor.
- * TODO: internal
- */
- writeValue(value: any): void {
- this.checked = value;
- }
-
- /**
- * Implemented as part of ControlValueAccessor.
- * TODO: internal
- */
- registerOnChange(fn: any): void {
- this.onChange = fn;
- }
-
- /**
- * Implemented as part of ControlValueAccessor.
- * TODO: internal
- */
- registerOnTouched(fn: any): void {
- this.onTouched = fn;
- }
-
- @Input()
- get checked() {
- return !!this._checked;
- }
-
- set checked(value) {
- if (this.checked !== !!value) {
- this._checked = value;
- this.onChange(this._checked);
- }
- }
-
- @Input()
- get color(): string {
- return this._color;
- }
-
- set color(value: string) {
- this._updateColor(value);
- }
-
- toggle() {
- this.checked = !this.checked;
- }
-
- private _updateColor(newColor: string) {
- this._setElementColor(this._color, false);
- this._setElementColor(newColor, true);
- this._color = newColor;
- }
-
- private _setElementColor(color: string, isAdd: boolean) {
- if (color != null && color != '') {
- this._renderer.setElementClass(this._elementRef.nativeElement, `md-${color}`, isAdd);
- }
- }
-
- /** Emits the change event to the `change` output EventEmitter */
- private _emitChangeEvent() {
- let event = new MdSlideToggleChange();
- event.source = this;
- event.checked = this.checked;
- this._change.emit(event);
- }
-
-
- /** TODO: internal */
- _onDragStart() {
- if (!this.disabled) {
- this._slideRenderer.startThumbDrag(this.checked);
- }
- }
-
- /** TODO: internal */
- _onDrag(event: HammerInput) {
- if (this._slideRenderer.isDragging()) {
- this._slideRenderer.updateThumbPosition(event.deltaX);
- }
- }
-
- /** TODO: internal */
- _onDragEnd() {
- if (!this._slideRenderer.isDragging()) {
- return;
- }
-
- // Notice that we have to stop outside of the current event handler,
- // because otherwise the click event will be fired and will reset the new checked variable.
- setTimeout(() => {
- this.checked = this._slideRenderer.stopThumbDrag();
- this._emitChangeEvent();
- }, 0);
- }
-
-}
-
-/**
- * Renderer for the Slide Toggle component, which separates DOM modification in its own class
- */
-class SlideToggleRenderer {
-
- private _thumbEl: HTMLElement;
- private _thumbBarEl: HTMLElement;
- private _thumbBarWidth: number;
- private _checked: boolean;
- private _percentage: number;
-
- constructor(private _elementRef: ElementRef) {
- this._thumbEl = _elementRef.nativeElement.querySelector('.md-slide-toggle-thumb-container');
- this._thumbBarEl = _elementRef.nativeElement.querySelector('.md-slide-toggle-bar');
- }
-
- /** Whether the slide-toggle is currently dragging. */
- isDragging(): boolean {
- return !!this._thumbBarWidth;
- }
-
-
- /** Initializes the drag of the slide-toggle. */
- startThumbDrag(checked: boolean) {
- if (!this.isDragging()) {
- this._thumbBarWidth = this._thumbBarEl.clientWidth - this._thumbEl.clientWidth;
- this._checked = checked;
- this._thumbEl.classList.add('md-dragging');
- }
- }
-
- /** Stops the current drag and returns the new checked value. */
- stopThumbDrag(): boolean {
- if (this.isDragging()) {
- this._thumbBarWidth = null;
- this._thumbEl.classList.remove('md-dragging');
-
- applyCssTransform(this._thumbEl, '');
-
- return this._percentage > 50;
- }
- }
-
- /** Updates the thumb containers position from the specified distance. */
- updateThumbPosition(distance: number) {
- this._percentage = this._getThumbPercentage(distance);
- applyCssTransform(this._thumbEl, `translate3d(${this._percentage}%, 0, 0)`);
- }
-
- /** Retrieves the percentage of thumb from the moved distance. */
- private _getThumbPercentage(distance: number) {
- let percentage = (distance / this._thumbBarWidth) * 100;
-
- // When the toggle was initially checked, then we have to start the drag at the end.
- if (this._checked) {
- percentage += 100;
- }
-
- return Math.max(0, Math.min(percentage, 100));
- }
-
-}
-
-
-@NgModule({
- imports: [FormsModule],
- exports: [MdSlideToggle],
- declarations: [MdSlideToggle],
-})
-export class MdSlideToggleModule {
- static forRoot(): ModuleWithProviders {
- return {
- ngModule: MdSlideToggleModule,
- providers: [{provide: HAMMER_GESTURE_CONFIG, useClass: MdGestureConfig}]
- };
- }
-}
+import {
+ Component,
+ ElementRef,
+ Renderer,
+ forwardRef,
+ ChangeDetectionStrategy,
+ Input,
+ Output,
+ EventEmitter,
+ AfterContentInit,
+ NgModule,
+ ModuleWithProviders,
+ ViewEncapsulation,
+} from '@angular/core';
+import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
+import {FormsModule, ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
+import {applyCssTransform, coerceBooleanProperty, MdGestureConfig} from '../core';
+import {Observable} from 'rxjs/Observable';
+
+
+export const MD_SLIDE_TOGGLE_VALUE_ACCESSOR: any = {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => MdSlideToggle),
+ multi: true
+};
+
+// A simple change event emitted by the MdSlideToggle component.
+export class MdSlideToggleChange {
+ source: MdSlideToggle;
+ checked: boolean;
+}
+
+// Increasing integer for generating unique ids for slide-toggle components.
+let nextId = 0;
+
+@Component({
+ moduleId: module.id,
+ selector: 'md-slide-toggle',
+ host: {
+ '[class.md-checked]': 'checked',
+ '[class.md-disabled]': 'disabled',
+ // This md-slide-toggle prefix will change, once the temporary ripple is removed.
+ '[class.md-slide-toggle-focused]': '_hasFocus',
+ '(mousedown)': '_setMousedown()'
+ },
+ templateUrl: 'slide-toggle.html',
+ styleUrls: ['slide-toggle.css'],
+ providers: [MD_SLIDE_TOGGLE_VALUE_ACCESSOR],
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
+
+ private onChange = (_: any) => {};
+ private onTouched = () => {};
+
+ // A unique id for the slide-toggle. By default the id is auto-generated.
+ private _uniqueId = `md-slide-toggle-${++nextId}`;
+ private _checked: boolean = false;
+ private _color: string;
+ private _isMousedown: boolean = false;
+ private _slideRenderer: SlideToggleRenderer = null;
+ private _disabled: boolean = false;
+ private _required: boolean = false;
+
+ // Needs to be public to support AOT compilation (as host binding).
+ _hasFocus: boolean = false;
+
+ @Input() name: string = null;
+ @Input() id: string = this._uniqueId;
+ @Input() tabIndex: number = 0;
+ @Input() ariaLabel: string = null;
+ @Input() ariaLabelledby: string = null;
+
+ @Input()
+ get disabled(): boolean { return this._disabled; }
+ set disabled(value) { this._disabled = coerceBooleanProperty(value); }
+
+ @Input()
+ get required(): boolean { return this._required; }
+ set required(value) { this._required = coerceBooleanProperty(value); }
+
+ private _change: EventEmitter = new EventEmitter();
+ @Output() change: Observable = this._change.asObservable();
+
+ // Returns the unique id for the visual hidden input.
+ getInputId = () => `${this.id || this._uniqueId}-input`;
+
+ constructor(private _elementRef: ElementRef, private _renderer: Renderer) {}
+
+ /** TODO: internal */
+ ngAfterContentInit() {
+ this._slideRenderer = new SlideToggleRenderer(this._elementRef);
+ }
+
+ /**
+ * The onChangeEvent method will be also called on click.
+ * This is because everything for the slide-toggle is wrapped inside of a label,
+ * which triggers a onChange event on click.
+ */
+ _onChangeEvent(event: Event) {
+ // We always have to stop propagation on the change event.
+ // Otherwise the change event, from the input element, will bubble up and
+ // emit its event object to the component's `change` output.
+ event.stopPropagation();
+
+ // Once a drag is currently in progress, we do not want to toggle the slide-toggle on a click.
+ if (!this.disabled && !this._slideRenderer.isDragging()) {
+ this.toggle();
+
+ // Emit our custom change event if the native input emitted one.
+ // It is important to only emit it, if the native input triggered one, because
+ // we don't want to trigger a change event, when the `checked` variable changes for example.
+ this._emitChangeEvent();
+ }
+ }
+
+ _onInputClick(event: Event) {
+ this.onTouched();
+
+ // We have to stop propagation for click events on the visual hidden input element.
+ // By default, when a user clicks on a label element, a generated click event will be
+ // dispatched on the associated input element. Since we are using a label element as our
+ // root container, the click event on the `slide-toggle` will be executed twice.
+ // The real click event will bubble up, and the generated click event also tries to bubble up.
+ // This will lead to multiple click events.
+ // Preventing bubbling for the second event will solve that issue.
+ event.stopPropagation();
+ }
+
+ _setMousedown() {
+ // We only *show* the focus style when focus has come to the button via the keyboard.
+ // The Material Design spec is silent on this topic, and without doing this, the
+ // button continues to look :active after clicking.
+ // @see http://marcysutton.com/button-focus-hell/
+ this._isMousedown = true;
+ setTimeout(() => this._isMousedown = false, 100);
+ }
+
+ _onInputFocus() {
+ // Only show the focus / ripple indicator when the focus was not triggered by a mouse
+ // interaction on the component.
+ if (!this._isMousedown) {
+ this._hasFocus = true;
+ }
+ }
+
+ _onInputBlur() {
+ this._hasFocus = false;
+ this.onTouched();
+ }
+
+ /**
+ * Implemented as part of ControlValueAccessor.
+ * TODO: internal
+ */
+ writeValue(value: any): void {
+ this.checked = value;
+ }
+
+ /**
+ * Implemented as part of ControlValueAccessor.
+ * TODO: internal
+ */
+ registerOnChange(fn: any): void {
+ this.onChange = fn;
+ }
+
+ /**
+ * Implemented as part of ControlValueAccessor.
+ * TODO: internal
+ */
+ registerOnTouched(fn: any): void {
+ this.onTouched = fn;
+ }
+
+ @Input()
+ get checked() {
+ return !!this._checked;
+ }
+
+ set checked(value) {
+ if (this.checked !== !!value) {
+ this._checked = value;
+ this.onChange(this._checked);
+ }
+ }
+
+ @Input()
+ get color(): string {
+ return this._color;
+ }
+
+ set color(value: string) {
+ this._updateColor(value);
+ }
+
+ toggle() {
+ this.checked = !this.checked;
+ }
+
+ private _updateColor(newColor: string) {
+ this._setElementColor(this._color, false);
+ this._setElementColor(newColor, true);
+ this._color = newColor;
+ }
+
+ private _setElementColor(color: string, isAdd: boolean) {
+ if (color != null && color != '') {
+ this._renderer.setElementClass(this._elementRef.nativeElement, `md-${color}`, isAdd);
+ }
+ }
+
+ /** Emits the change event to the `change` output EventEmitter */
+ private _emitChangeEvent() {
+ let event = new MdSlideToggleChange();
+ event.source = this;
+ event.checked = this.checked;
+ this._change.emit(event);
+ }
+
+
+ /** TODO: internal */
+ _onDragStart() {
+ if (!this.disabled) {
+ this._slideRenderer.startThumbDrag(this.checked);
+ }
+ }
+
+ /** TODO: internal */
+ _onDrag(event: HammerInput) {
+ if (this._slideRenderer.isDragging()) {
+ this._slideRenderer.updateThumbPosition(event.deltaX);
+ }
+ }
+
+ /** TODO: internal */
+ _onDragEnd() {
+ if (!this._slideRenderer.isDragging()) {
+ return;
+ }
+
+ // Notice that we have to stop outside of the current event handler,
+ // because otherwise the click event will be fired and will reset the new checked variable.
+ setTimeout(() => {
+ this.checked = this._slideRenderer.stopThumbDrag();
+ this._emitChangeEvent();
+ }, 0);
+ }
+
+}
+
+/**
+ * Renderer for the Slide Toggle component, which separates DOM modification in its own class
+ */
+class SlideToggleRenderer {
+
+ private _thumbEl: HTMLElement;
+ private _thumbBarEl: HTMLElement;
+ private _thumbBarWidth: number;
+ private _checked: boolean;
+ private _percentage: number;
+
+ constructor(private _elementRef: ElementRef) {
+ this._thumbEl = _elementRef.nativeElement.querySelector('.md-slide-toggle-thumb-container');
+ this._thumbBarEl = _elementRef.nativeElement.querySelector('.md-slide-toggle-bar');
+ }
+
+ /** Whether the slide-toggle is currently dragging. */
+ isDragging(): boolean {
+ return !!this._thumbBarWidth;
+ }
+
+
+ /** Initializes the drag of the slide-toggle. */
+ startThumbDrag(checked: boolean) {
+ if (!this.isDragging()) {
+ this._thumbBarWidth = this._thumbBarEl.clientWidth - this._thumbEl.clientWidth;
+ this._checked = checked;
+ this._thumbEl.classList.add('md-dragging');
+ }
+ }
+
+ /** Stops the current drag and returns the new checked value. */
+ stopThumbDrag(): boolean {
+ if (this.isDragging()) {
+ this._thumbBarWidth = null;
+ this._thumbEl.classList.remove('md-dragging');
+
+ applyCssTransform(this._thumbEl, '');
+
+ return this._percentage > 50;
+ }
+ }
+
+ /** Updates the thumb containers position from the specified distance. */
+ updateThumbPosition(distance: number) {
+ this._percentage = this._getThumbPercentage(distance);
+ applyCssTransform(this._thumbEl, `translate3d(${this._percentage}%, 0, 0)`);
+ }
+
+ /** Retrieves the percentage of thumb from the moved distance. */
+ private _getThumbPercentage(distance: number) {
+ let percentage = (distance / this._thumbBarWidth) * 100;
+
+ // When the toggle was initially checked, then we have to start the drag at the end.
+ if (this._checked) {
+ percentage += 100;
+ }
+
+ return Math.max(0, Math.min(percentage, 100));
+ }
+
+}
+
+
+@NgModule({
+ imports: [FormsModule],
+ exports: [MdSlideToggle],
+ declarations: [MdSlideToggle],
+})
+export class MdSlideToggleModule {
+ static forRoot(): ModuleWithProviders {
+ return {
+ ngModule: MdSlideToggleModule,
+ providers: [{provide: HAMMER_GESTURE_CONFIG, useClass: MdGestureConfig}]
+ };
+ }
+}
diff --git a/src/lib/slider/slider.spec.ts b/src/lib/slider/slider.spec.ts
index fd05c4a6fb43..0fb69433c4ee 100644
--- a/src/lib/slider/slider.spec.ts
+++ b/src/lib/slider/slider.spec.ts
@@ -22,7 +22,10 @@ describe('MdSlider', () => {
SliderWithAutoTickInterval,
SliderWithSetTickInterval,
SliderWithThumbLabel,
+ SliderWithOneWayBinding,
SliderWithTwoWayBinding,
+ SliderWithValueSmallerThanMin,
+ SliderWithValueGreaterThanMax
],
providers: [
{provide: HAMMER_GESTURE_CONFIG, useFactory: () => {
@@ -621,6 +624,130 @@ describe('MdSlider', () => {
// TODO: Add tests for ng-pristine, ng-touched, ng-invalid.
});
+
+ describe('slider with value property binding', () => {
+ let fixture: ComponentFixture;
+ let sliderDebugElement: DebugElement;
+ let sliderNativeElement: HTMLElement;
+ let sliderInstance: MdSlider;
+ let sliderTrackElement: HTMLElement;
+ let testComponent: SliderWithOneWayBinding;
+ let trackFillElement: HTMLElement;
+ let thumbElement: HTMLElement;
+ let sliderDimensions: ClientRect;
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SliderWithOneWayBinding);
+ fixture.detectChanges();
+
+ testComponent = fixture.debugElement.componentInstance;
+
+ sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider));
+ sliderNativeElement = sliderDebugElement.nativeElement;
+ sliderInstance = sliderDebugElement.injector.get(MdSlider);
+ sliderTrackElement = sliderNativeElement.querySelector('.md-slider-track');
+ trackFillElement = sliderNativeElement.querySelector('.md-slider-track-fill');
+ thumbElement = sliderNativeElement.querySelector('.md-slider-thumb-position');
+ sliderDimensions = sliderTrackElement.getBoundingClientRect();
+ });
+
+ it('should initialize based on bound value', () => {
+ let trackFillDimensions = trackFillElement.getBoundingClientRect();
+ let thumbDimensions = thumbElement.getBoundingClientRect();
+ let thumbPosition = thumbDimensions.left - trackFillDimensions.left;
+
+ expect(sliderInstance.value).toBe(50);
+ expect(thumbPosition).toBe(sliderDimensions.width / 2);
+ });
+
+ it('should update when bound value changes', () => {
+ testComponent.val = 75;
+ fixture.detectChanges();
+
+ let trackFillDimensions = trackFillElement.getBoundingClientRect();
+ let thumbDimensions = thumbElement.getBoundingClientRect();
+ let thumbPosition = thumbDimensions.left - trackFillDimensions.left;
+
+ expect(sliderInstance.value).toBe(75);
+ expect(thumbPosition).toBe(sliderDimensions.width * 3 / 4);
+ });
+ });
+
+ describe('slider with set min and max and a value smaller than min', () => {
+ let fixture: ComponentFixture;
+ let sliderDebugElement: DebugElement;
+ let sliderNativeElement: HTMLElement;
+ let sliderInstance: MdSlider;
+ let sliderTrackElement: HTMLElement;
+ let sliderDimensions: ClientRect;
+ let thumbElement: HTMLElement;
+ let thumbDimensions: ClientRect;
+
+ beforeEach(() => {
+
+ fixture = TestBed.createComponent(SliderWithValueSmallerThanMin);
+ fixture.detectChanges();
+
+ sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider));
+ sliderNativeElement = sliderDebugElement.nativeElement;
+ sliderInstance = sliderDebugElement.componentInstance;
+
+ sliderTrackElement = sliderNativeElement.querySelector('.md-slider-track');
+ sliderDimensions = sliderTrackElement.getBoundingClientRect();
+
+ thumbElement = sliderNativeElement.querySelector('.md-slider-thumb-position');
+ thumbDimensions = thumbElement.getBoundingClientRect();
+ });
+
+ it('should set the value smaller than the min value', () => {
+ expect(sliderInstance.value).toBe(3);
+ expect(sliderInstance.min).toBe(4);
+ expect(sliderInstance.max).toBe(6);
+ });
+
+ it('should place the thumb on the min value', () => {
+ thumbDimensions = thumbElement.getBoundingClientRect();
+ expect(thumbDimensions.left).toBe(sliderDimensions.left);
+ });
+ });
+
+ describe('slider with set min and max and a value greater than max', () => {
+ let fixture: ComponentFixture;
+ let sliderDebugElement: DebugElement;
+ let sliderNativeElement: HTMLElement;
+ let sliderInstance: MdSlider;
+ let sliderTrackElement: HTMLElement;
+ let sliderDimensions: ClientRect;
+ let thumbElement: HTMLElement;
+ let thumbDimensions: ClientRect;
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SliderWithValueGreaterThanMax);
+ fixture.detectChanges();
+
+ sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider));
+ sliderNativeElement = sliderDebugElement.nativeElement;
+ sliderInstance = sliderDebugElement.componentInstance;
+
+ sliderTrackElement = sliderNativeElement.querySelector('.md-slider-track');
+ sliderDimensions = sliderTrackElement.getBoundingClientRect();
+
+ thumbElement = sliderNativeElement.querySelector('.md-slider-thumb-position');
+ thumbDimensions = thumbElement.getBoundingClientRect();
+
+ });
+
+ it('should set the value greater than the max value', () => {
+ expect(sliderInstance.value).toBe(7);
+ expect(sliderInstance.min).toBe(4);
+ expect(sliderInstance.max).toBe(6);
+ });
+
+ it('should place the thumb on the max value', () => {
+ thumbDimensions = thumbElement.getBoundingClientRect();
+ expect(thumbDimensions.left).toBe(sliderDimensions.right);
+ });
+ });
});
// The transition has to be removed in order to test the updated positions without setTimeout.
@@ -671,6 +798,13 @@ class SliderWithSetTickInterval { }
})
class SliderWithThumbLabel { }
+@Component({
+ template: ``
+})
+class SliderWithOneWayBinding {
+ val = 50;
+}
+
@Component({
template: ``
})
@@ -678,6 +812,20 @@ class SliderWithTwoWayBinding {
control = new FormControl('');
}
+@Component({
+ template: ``,
+ styles: [noTransitionStyle],
+ encapsulation: ViewEncapsulation.None
+})
+class SliderWithValueSmallerThanMin { }
+
+@Component({
+ template: ``,
+ styles: [noTransitionStyle],
+ encapsulation: ViewEncapsulation.None
+})
+class SliderWithValueGreaterThanMax { }
+
/**
* Dispatches a click event from an element.
* Note: The mouse event truncates the position for the click.
diff --git a/src/lib/slider/slider.ts b/src/lib/slider/slider.ts
index 8c80ee4ad7ee..b7d2aa22017e 100644
--- a/src/lib/slider/slider.ts
+++ b/src/lib/slider/slider.ts
@@ -9,13 +9,9 @@ import {
AfterContentInit,
forwardRef,
} from '@angular/core';
-import {
- NG_VALUE_ACCESSOR,
- ControlValueAccessor,
- FormsModule,
-} from '@angular/forms';
+import {NG_VALUE_ACCESSOR, ControlValueAccessor, FormsModule} from '@angular/forms';
import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
-import {BooleanFieldValue, MdGestureConfig, applyCssTransform} from '../core';
+import {MdGestureConfig, applyCssTransform, coerceBooleanProperty} from '../core';
import {Input as HammerInput} from 'hammerjs';
/**
@@ -58,16 +54,20 @@ export class MdSlider implements AfterContentInit, ControlValueAccessor {
/** The dimensions of the slider. */
private _sliderDimensions: ClientRect = null;
+ private _disabled: boolean = false;
+
@Input()
- @BooleanFieldValue()
@HostBinding('class.md-slider-disabled')
@HostBinding('attr.aria-disabled')
- disabled: boolean = false;
+ get disabled(): boolean { return this._disabled; }
+ set disabled(value) { this._disabled = coerceBooleanProperty(value); }
/** Whether or not to show the thumb label. */
+ private _thumbLabel: boolean = false;
+
@Input('thumb-label')
- @BooleanFieldValue()
- thumbLabel: boolean = false;
+ get thumbLabel(): boolean { return this._thumbLabel; }
+ set thumbLabel(value) { this._thumbLabel = coerceBooleanProperty(value); }
/** The miniumum value that the slider can have. */
private _min: number = 0;
@@ -154,6 +154,7 @@ export class MdSlider implements AfterContentInit, ControlValueAccessor {
this._value = Number(v);
this._isInitialized = true;
this._controlValueAccessorChangeFn(this._value);
+ this.snapThumbToValue();
}
constructor(elementRef: ElementRef) {
@@ -265,7 +266,10 @@ export class MdSlider implements AfterContentInit, ControlValueAccessor {
*/
snapThumbToValue() {
this.updatePercentFromValue();
- this._renderer.updateThumbAndFillPosition(this._percent, this._sliderDimensions.width);
+ if (this._sliderDimensions) {
+ let renderedPercent = this.clamp(this._percent);
+ this._renderer.updateThumbAndFillPosition(renderedPercent, this._sliderDimensions.width);
+ }
}
/**
diff --git a/src/lib/snack-bar/snack-bar.spec.ts b/src/lib/snack-bar/snack-bar.spec.ts
index 65a9a11ef18e..8cac78af5546 100644
--- a/src/lib/snack-bar/snack-bar.spec.ts
+++ b/src/lib/snack-bar/snack-bar.spec.ts
@@ -45,6 +45,10 @@ describe('MdSnackBar', () => {
snackBar = sb;
}));
+ afterEach(() => {
+ overlayContainerElement.innerHTML = '';
+ });
+
beforeEach(() => {
viewContainerFixture = TestBed.createComponent(ComponentWithChildViewContainer);
diff --git a/src/lib/tabs/tabs.ts b/src/lib/tabs/tabs.ts
index ee77040a63d1..9ba8ef91019e 100644
--- a/src/lib/tabs/tabs.ts
+++ b/src/lib/tabs/tabs.ts
@@ -13,14 +13,14 @@ import {
ContentChildren
} from '@angular/core';
import {CommonModule} from '@angular/common';
-import {PortalModule} from '../core';
+import {PortalModule, RIGHT_ARROW, LEFT_ARROW, ENTER, coerceBooleanProperty} from '../core';
import {MdTabLabel} from './tab-label';
import {MdTabContent} from './tab-content';
import {MdTabLabelWrapper} from './tab-label-wrapper';
import {MdInkBar} from './ink-bar';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/map';
-import {RIGHT_ARROW, LEFT_ARROW, ENTER} from '../core';
+
/** Used to generate unique ID's for each tab component */
let nextId = 0;
@@ -38,11 +38,10 @@ export class MdTab {
@ContentChild(MdTabLabel) label: MdTabLabel;
@ContentChild(MdTabContent) content: MdTabContent;
- // TODO: Replace this when BooleanFieldValue is removed.
private _disabled = false;
@Input('disabled')
set disabled(value: boolean) {
- this._disabled = (value != null && `${value}` !== 'false');
+ this._disabled = coerceBooleanProperty(value);
}
get disabled(): boolean {
return this._disabled;
diff --git a/tools/gulp/tasks/components.ts b/tools/gulp/tasks/components.ts
index b9f3cbebbd4c..2bfceeb9d2c8 100644
--- a/tools/gulp/tasks/components.ts
+++ b/tools/gulp/tasks/components.ts
@@ -26,9 +26,9 @@ const tsconfigPath = path.relative(PROJECT_ROOT, path.join(componentsDir, 'tscon
/** [Watch task] Rebuilds (ESM output) whenever ts, scss, or html sources change. */
task(':watch:components', () => {
- watch(path.join(componentsDir, '**/*.ts'), [':build:components:ts']);
- watch(path.join(componentsDir, '**/*.scss'), [':build:components:scss']);
- watch(path.join(componentsDir, '**/*.html'), [':build:components:assets']);
+ watch(path.join(componentsDir, '**/*.ts'), [':build:components:rollup']);
+ watch(path.join(componentsDir, '**/*.scss'), [':build:components:rollup']);
+ watch(path.join(componentsDir, '**/*.html'), [':build:components:rollup']);
});
/** [Watch task] Rebuilds for tests (CJS output) whenever ts, scss, or html sources change. */