diff --git a/src/components/button/button.scss b/src/components/button/button.scss index cefb3353bdb9..5b47c0d4c258 100644 --- a/src/components/button/button.scss +++ b/src/components/button/button.scss @@ -89,7 +89,7 @@ $md-fab-mini-line-height: rem(4.00) !default; // Use a CSS class for focus style because we only want to render the focus style when // the focus originated from a keyboard event (see JS source for more details). &:hover, &.md-button-focus { - background: md-color($md-background, 500, 0.2); + background: md-color($md-background, background, 0.2); } &.md-primary { @@ -157,8 +157,8 @@ $md-fab-mini-line-height: rem(4.00) !default; [md-raised-button] { @include md-raised-button(); - color: md-color($md-background, default-contrast); - background-color: md-color($md-background, 50); + color: md-color($md-foreground, text); + background-color: md-color($md-background, background); } [md-fab] { diff --git a/src/components/card/card.scss b/src/components/card/card.scss index 74703cd0fba7..56c66df33c6f 100644 --- a/src/components/card/card.scss +++ b/src/components/card/card.scss @@ -15,7 +15,7 @@ md-card { border-radius: $md-card-border-radius; box-shadow: $md-shadow-bottom-z-1; font-family: $font-family; - background: md-color($md-background, 50); // TODO(kara): use updated background palette + background: md-color($md-background, card); } md-card:hover { diff --git a/src/components/sidenav/README.md b/src/components/sidenav/README.md new file mode 100644 index 000000000000..bc7fd8ccbda1 --- /dev/null +++ b/src/components/sidenav/README.md @@ -0,0 +1,83 @@ +# MdSidenav + +MdSidenav is the side navigation component for Material 2. It is composed of two components; `` and ``. + +## Screenshots + + + + +## `` + +The parent component. Contains the code necessary to coordinate one or two sidenav and the backdrop. + +### Properties + +| Name | Description | +| --- | --- | +| `start` | The start aligned `MdSidenav` instance, or `null` if none is specified. In LTR direction, this is the sidenav shown on the left side. In RTL direction, it will show on the right. There can only be one sidenav on either side. | +| `end` | The end aligned `MdSidenav` instance, or `null` if none is specified. This is the sidenav opposing the `start` sidenav. There can only be one sidenav on either side. | + +## `` + +The sidenav panel. + +### Bound Properties + +| Name | Type | Description | +| --- | --- | --- | +| `align` | `"start"|"end"` | The alignment of this sidenav. In LTR direction, `"start"` will be shown on the left, `"end"` on the right. In RTL, it is reversed. `"start"` is used by default. An exception will be thrown if there are more than 1 sidenav on either side. | +| `mode` | `"over"|"push"|"side"` | The mode or styling of the sidenav, default being `"over"`. With `"over"` the sidenav will appear above the content, and a backdrop will be shown. With `"push"` the sidenav will push the content of the `` to the side, and show a backdrop over it. `"side"` will resize the content and keep the sidenav opened. Clicking the backdrop will close sidenavs that do not have `mode="side"`. | +| `opened` | `boolean` | Whether or not the sidenav is opened. Use this binding to open/close the sidenav. | + +### Events + +| Name | Description | +| --- | --- | +| `open-start` | Emitted when the sidenav is starting opening. This should only be used to coordinate animations. | +| `close-start` | Emitted when the sidenav is starting closing. This should only be used to coordinate animations. | +| `open` | Emitted when the sidenav is done opening. Use this for, e.g., setting focus on controls or updating state. | +| `close` | Emitted when the sidenav is done closing. | + +### Methods + +| Signature | Description | +| --- | --- | +| `open(): Promise` | Open the sidenav. Equivalent to `opened = true`. Returns a promise that will resolve when the animation completes, or be rejected if the animation was cancelled. | +| `close(): Promise` | Close the sidenav. Equivalent to `opened = false`. Returns a promise that will resolve when the animation completes, or be rejected if the animation was cancelled. | +| `toggle(): Promise` | Toggle the sidenav. This is equivalent to `opened = !opened`. Returns a promise that will resolve when the animation completes, or be rejected if the animation was cancelled. | + +### Notes + +The `` will resize based on its content. You can also set its width in CSS, like so: + +```css +md-sidenav { + width: 200px; +} +``` + +Try to avoid percent based width as `resize` events are not (yet) supported. + +## Examples + +Here's a simple example of using the sidenav: + +```html + + + + Start Sidenav. +
+ Close +
+ + End Sidenav. + Close + + + My regular content. This will be moved into the proper DOM at runtime. +
+
+``` + diff --git a/src/components/sidenav/sidenav.html b/src/components/sidenav/sidenav.html new file mode 100644 index 000000000000..40f598703a3c --- /dev/null +++ b/src/components/sidenav/sidenav.html @@ -0,0 +1,11 @@ +
+ + + + + + diff --git a/src/components/sidenav/sidenav.scss b/src/components/sidenav/sidenav.scss new file mode 100644 index 000000000000..86cc986646d5 --- /dev/null +++ b/src/components/sidenav/sidenav.scss @@ -0,0 +1,143 @@ +@import "default-theme"; +@import "mixins"; +@import "variables"; +@import "shadows"; + + +// We use invert() here to have the darken the background color expected to be used. If the +// background is light, we use a dark backdrop. If the background is dark, we use a light backdrop. +$md-sidenav-backdrop-color: invert(md-color($md-background, card, 0.6)) !default; +$md-sidenav-background-color: md-color($md-background, dialog) !default; +$md-sidenav-push-background-color: md-color($md-background, dialog) !default; + + +/** + * Mixin to help with defining LTR/RTL `transform: translateX()` values. + * @param $open The translation value when the sidenav is opened. + * @param $close The translation value when the sidenav is closed. + */ +@mixin md-sidenav-transition($open, $close) { + transform: translateX($close); + + &.md-sidenav-closed { + // We use `visibility: hidden | visible` because `display: none` will not animate any + // transitions, while visibility will interpolate transitions properly. + // see https://developer.mozilla.org/en-US/docs/Web/CSS/visibility, the Interpolation + // section. + visibility: hidden; + } + &.md-sidenav-closing { + transform: translateX($close); + will-change: transform; + } + &.md-sidenav-opening { + visibility: visible; + transform: translateX($open); + will-change: transform; + box-shadow: $md-shadow-bottom-z-1; + } + &.md-sidenav-opened { + transform: translateX($open); + box-shadow: $md-shadow-bottom-z-1; + } +} + + +:host { + // We need a stacking context here so that the backdrop and drawers are clipped to the + // MdSidenavLayout. This creates a new z-index stack so we use low numbered z-indices. + // We create another stacking context in the `` and in each sidenav so that + // the application content does not get messed up with our own CSS. + @include md-stacking-context(); + + box-sizing: border-box; + + // Need this to take up space in the layout. + display: block; + + // Hide the sidenavs when they're closed. + overflow-x: hidden; + + transition: margin-left $swift-ease-out-duration $swift-ease-out-timing-function, + margin-right $swift-ease-out-duration $swift-ease-out-timing-function; + + & > .md-sidenav-backdrop { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + + display: block; + + // Because of the new stacking context, the z-index stack is new and we can use our own + // numbers. + z-index: 2; + + // We use `visibility: hidden | visible` because `display: none` will not animate any + // transitions, while visibility will interpolate transitions properly. + // see https://developer.mozilla.org/en-US/docs/Web/CSS/visibility, the Interpolation + // section. + visibility: hidden; + + &.md-sidenav-shown { + visibility: visible; + background-color: $md-sidenav-backdrop-color; + transition: background-color $swift-ease-out-duration $swift-ease-out-timing-function; + } + } + + & > md-content { + @include md-stacking-context(); + + display: block; + transition: margin-left $swift-ease-out-duration $swift-ease-out-timing-function, + margin-right $swift-ease-out-duration $swift-ease-out-timing-function, + left $swift-ease-out-duration $swift-ease-out-timing-function, + right $swift-ease-out-duration $swift-ease-out-timing-function; + } + + > md-sidenav { + @include md-stacking-context(); + + display: block; + position: fixed; + top: 0; + bottom: 0; + z-index: 3; + + background-color: $md-sidenav-background-color; + + transition: transform $swift-ease-out-duration $swift-ease-out-timing-function; + + @include md-sidenav-transition(0, -100%); + + &.md-sidenav-push { + background-color: $md-sidenav-push-background-color; + } + + &.md-sidenav-side { + z-index: 1; + } + + &.md-sidenav-end { + right: 0; + + @include md-sidenav-transition(0, 100%); + } + } +} + + +:host-context([dir="rtl"]) { + > md-sidenav { + @include md-sidenav-transition(0, 100%); + + &.md-sidenav-end { + left: 0; + right: auto; + + @include md-sidenav-transition(0, -100%); + } + } +} diff --git a/src/components/sidenav/sidenav.spec.ts b/src/components/sidenav/sidenav.spec.ts new file mode 100644 index 000000000000..4e94f8dfaa3a --- /dev/null +++ b/src/components/sidenav/sidenav.spec.ts @@ -0,0 +1,286 @@ +import { + it, + iit, + describe, + ddescribe, + expect, + inject, + injectAsync, + TestComponentBuilder, + beforeEachProviders, + beforeEach, +} from 'angular2/testing'; +import {provide, Component, DebugElement} from 'angular2/core'; +import {By} from 'angular2/platform/browser'; + +import {MdSidenav, MdSidenavLayout, MD_SIDENAV_DIRECTIVES} from './sidenav'; +import {AsyncTestFn, FunctionWithParamTokens} from 'angular2/testing'; +import {ComponentFixture} from "angular2/testing"; +import {EventEmitter} from "angular2/core"; +import {Predicate} from "angular2/src/facade/collection"; + + +function wait(msec: number) { + return new Promise(resolve => window.setTimeout(() => resolve(), msec)); +} + + +function waitOnEvent(fixture: ComponentFixture, + by: Predicate, + propertyName: string) { + fixture.detectChanges(); + + // Wait for the animation end. + return new Promise(resolve => { + const component: any = fixture.debugElement.query(by).componentInstance; + component[propertyName].subscribe(resolve); + }); +} + + +describe('MdSidenav', () => { + let builder: TestComponentBuilder; + + beforeEach(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { builder = tcb; })); + + describe('methods', () => { + it('should be able to open and close', (done: any) => { + let testComponent: BasicTestApp; + let fixture: ComponentFixture; + + return builder.createAsync(BasicTestApp) + .then((f) => { + fixture = f; + testComponent = fixture.debugElement.componentInstance; + + const openButtonElement = fixture.debugElement.query(By.css('.open')); + openButtonElement.nativeElement.click(); + }) + .then(() => wait(1)) + .then(() => { + expect(testComponent.openStartCount).toBe(1); + expect(testComponent.openCount).toBe(0); + }) + .then(() => waitOnEvent(fixture, By.directive(MdSidenav), 'onOpen')) + .then(() => { + expect(testComponent.openStartCount).toBe(1); + expect(testComponent.openCount).toBe(1); + expect(testComponent.closeStartCount).toBe(0); + expect(testComponent.closeCount).toBe(0); + + const sidenavElement = fixture.debugElement.query(By.css('md-sidenav')); + const sidenavBackdropElement = fixture.debugElement.query(By.css('md-sidenav-backdrop')); + expect(window.getComputedStyle(sidenavElement.nativeElement).visibility).toEqual('visible'); + expect(window.getComputedStyle(sidenavBackdropElement.nativeElement).visibility).toEqual('visible'); + + // Close it. + const closeButtonElement = fixture.debugElement.query(By.css('.close')); + closeButtonElement.nativeElement.click(); + }) + .then(() => wait(1)) + .then(() => { + expect(testComponent.openStartCount).toBe(1); + expect(testComponent.openCount).toBe(1); + expect(testComponent.closeStartCount).toBe(1); + expect(testComponent.closeCount).toBe(0); + }) + .then(() => waitOnEvent(fixture, By.directive(MdSidenav), 'onClose')) + .then(() => { + expect(testComponent.openStartCount).toBe(1); + expect(testComponent.openCount).toBe(1); + expect(testComponent.closeStartCount).toBe(1); + expect(testComponent.closeCount).toBe(1); + + const sidenavElement = fixture.debugElement.query(By.css('md-sidenav')); + const sidenavBackdropElement = fixture.debugElement.query(By.css('md-sidenav-backdrop')); + expect(window.getComputedStyle(sidenavElement.nativeElement).visibility).toEqual('hidden'); + expect(window.getComputedStyle(sidenavBackdropElement.nativeElement).visibility).toEqual('hidden'); + }) + .then(done, done.fail); + }, 8000); + + it('open() and close() return a promise that resolves after the animation ended', + (done: any) => { + let testComponent: BasicTestApp; + let fixture: ComponentFixture; + let sidenav: MdSidenav; + + let promise: Promise; + let called: boolean = false; + + return builder.createAsync(BasicTestApp) + .then((f) => { + fixture = f; + sidenav = fixture.debugElement.query(By.directive(MdSidenav)).componentInstance; + + promise = sidenav.open(); + promise.then(() => called = true); + }) + .then(() => wait(1)) + .then(() => fixture.detectChanges()) + .then(() => { + expect(called).toBe(false); + }) + .then(() => promise) + .then(() => expect(called).toBe(true)) + .then(() => { + // Close it now. + called = false; + promise = sidenav.close(); + promise.then(() => called = true); + }) + .then(() => wait(1)) + .then(() => fixture.detectChanges()) + .then(() => { + expect(called).toBe(false); + }) + .then(() => promise) + .then(() => expect(called).toBe(true)) + .then(done, done.fail); + }, 8000); + + it('open() twice returns the same promise', (done: any) => { + let testComponent: BasicTestApp; + let fixture: ComponentFixture; + let sidenav: MdSidenav; + + let promise: Promise; + let called: boolean = false; + + return builder.createAsync(BasicTestApp) + .then((f) => { + fixture = f; + sidenav = fixture.debugElement.query(By.directive(MdSidenav)).componentInstance; + + promise = sidenav.open(); + expect(sidenav.open()).toBe(promise); + }) + .then(() => wait(1)) + .then(() => { + fixture.detectChanges(); + return promise; + }) + .then(() => { + promise = sidenav.close(); + expect(sidenav.close()).toBe(promise); + }) + .then(done, done.fail); + }); + + it('open() then close() cancel animations when called too fast', + (done: any) => { + let testComponent: BasicTestApp; + let fixture: ComponentFixture; + let sidenav: MdSidenav; + + let openPromise: Promise; + let closePromise: Promise; + let openCalled: boolean = false; + let openCancelled: boolean = false; + let closeCalled: boolean = false; + + return builder.createAsync(BasicTestApp) + .then((f) => { + fixture = f; + sidenav = fixture.debugElement.query(By.directive(MdSidenav)).componentInstance; + + openPromise = sidenav.open().then(() => { openCalled = true; }, + () => { openCancelled = true; }); + }) + .then(() => wait(1)) + .then(() => fixture.detectChanges()) + // We need to wait for the browser to start the transition. + .then(() => wait(50)) + .then(() => { + closePromise = sidenav.close().then(() => { closeCalled = true; }, done.fail); + return wait(1); + }) + .then(() => { + fixture.detectChanges(); + return closePromise; + }) + .then(() => { + expect(openCalled).toBe(false); + expect(openCancelled).toBe(true); + expect(closeCalled).toBe(true); + }) + .then(done, done.fail); + }, 8000); + + it('close() then open() cancel animations when called too fast', + (done: any) => { + let testComponent: BasicTestApp; + let fixture: ComponentFixture; + let sidenav: MdSidenav; + + let openPromise: Promise; + let closePromise: Promise; + let closeCalled: boolean = false; + let closeCancelled: boolean = false; + let openCalled: boolean = false; + + return builder.createAsync(BasicTestApp) + .then((f) => { + fixture = f; + sidenav = fixture.debugElement.query(By.directive(MdSidenav)).componentInstance; + + /** First, open it. */ + openPromise = sidenav.open(); + }) + .then(() => wait(1)) + .then(() => { + fixture.detectChanges(); + return openPromise; + }) + .then(() => { + // Then close and check behavior. + closePromise = sidenav.close().then(() => { closeCalled = true; }, + () => { closeCancelled = true; }); + }) + .then(() => wait(1)) + .then(() => fixture.detectChanges()) + // We need to wait for the browser to start the transition. + .then(() => wait(50)) + .then(() => { + openPromise = sidenav.open().then(() => { openCalled = true; }, done.fail); + return wait(1); + }) + .then(() => { + fixture.detectChanges(); + return openPromise; + }) + .then(() => { + expect(closeCalled).toBe(false); + expect(closeCancelled).toBe(true); + expect(openCalled).toBe(true); + }) + .then(done, done.fail); + }, 8000); + }); +}); + + +/** Test component that contains an MdSidenavLayout and one MdSidenav. */ +@Component({ + selector: 'test-app', + directives: [MD_SIDENAV_DIRECTIVES], + template: ` + + + Content. + + + + + `, +}) +class BasicTestApp { + openStartCount: number = 0; + openCount: number = 0; + closeStartCount: number = 0; + closeCount: number = 0; +} diff --git a/src/components/sidenav/sidenav.ts b/src/components/sidenav/sidenav.ts new file mode 100644 index 000000000000..2aab62ef24ca --- /dev/null +++ b/src/components/sidenav/sidenav.ts @@ -0,0 +1,353 @@ +import { + AfterContentInit, + Component, + ContentChildren, + ElementRef, + EventEmitter, + Host, + HostBinding, + HostListener, + Input, + View, + ViewEncapsulation, + OnChanges, + Optional, + Output, + Query, + QueryList, + SimpleChange, + Type +} from 'angular2/core'; +import {BaseException} from 'angular2/src/facade/exceptions'; +import {CONST_EXPR} from 'angular2/src/facade/lang'; +import {Dir} from '../../directives/dir/dir'; +import {OneOf} from '../../core/annotations/one-of'; + + +/** + * Exception thrown when a MdSidenavLayout is missing both sidenavs. + */ +export class MdMissingSidenavException extends BaseException {} + +/** + * Exception thrown when two MdSidenav are matching the same side. + */ +export class MdDuplicatedSidenavException extends BaseException { + constructor(align: string) { + super(`A sidenav was already declared for 'align="${align}"'`); + } +} + + +/** + * component. + * + * This component corresponds to the drawer of the sidenav. + * + * Please refer to README.md for examples on how to use it. + */ +@Component({ + selector: 'md-sidenav', + template: '', +}) +export class MdSidenav { + /** Alignment of the sidenav (direction neutral); whether 'start' or 'end'. */ + @Input() @OneOf(['start', 'end']) align: string = 'start'; + + /** Mode of the sidenav; whether 'over' or 'side'. */ + @Input() @OneOf(['over', 'push', 'side']) mode: string = 'over'; + + /** Whether the sidenav is opened. */ + @Input('opened') private opened_: boolean; + + /** Event emitted when the sidenav is being opened. Use this to synchronize animations. */ + @Output('open-start') onOpenStart = new EventEmitter(); + + /** Event emitted when the sidenav is fully opened. */ + @Output('open') onOpen = new EventEmitter(); + + /** Event emitted when the sidenav is being closed. Use this to synchronize animations. */ + @Output('close-start') onCloseStart = new EventEmitter(); + + /** Event emitted when the sidenav is fully closed. */ + @Output('close') onClose = new EventEmitter(); + + + /** + * @param elementRef_ The DOM element reference. Used for transition and width calculation. + * If not available we do not hook on transitions. + */ + constructor(private elementRef_: ElementRef) {} + + /** + * Whether the sidenav is opened. We overload this because we trigger an event when it + * starts or end. + * @returns {boolean} + */ + get opened(): boolean { return this.opened_; } + set opened(v: boolean) { + this.toggle(v); + } + + + /** Open this sidenav, and return a Promise that will resolve when it's fully opened (or get + * rejected if it didn't). */ + open(): Promise { + return this.toggle(true); + } + + /** + * Close this sidenav, and return a Promise that will resolve when it's fully closed (or get + * rejected if it didn't). + */ + close(): Promise { + return this.toggle(false); + } + + /** + * Toggle this sidenav. This is equivalent to calling open() when it's already opened, or + * close() when it's closed. + * @param isOpen + * @returns {Promise} + */ + toggle(isOpen: boolean = !this.opened): Promise { + // Shortcut it if we're already opened. + if (isOpen === this.opened) { + if (!this.transition_) { + return Promise.resolve(); + } else { + return isOpen ? this.openPromise_ : this.closePromise_; + } + } + + this.opened_ = isOpen; + this.transition_ = true; + + if (isOpen) { + this.onOpenStart.emit(null); + } else { + this.onCloseStart.emit(null); + } + + const emitter = isOpen ? this.onOpen : this.onClose; + const other = isOpen ? this.onClose : this.onOpen; + + if (isOpen) { + if (!this.openPromise_) { + this.openPromise_ = new Promise((resolve, reject) => { + this.openPromiseResolve_ = resolve; + this.openPromiseReject_ = reject; + }); + } + return this.openPromise_; + } else { + if (!this.closePromise_) { + this.closePromise_ = new Promise((resolve, reject) => { + this.closePromiseResolve_ = resolve; + this.closePromiseReject_ = reject; + }); + } + return this.closePromise_; + } + } + + + /** + * When transition has finished, set the internal state for classes and emit the proper event. + * The event passed is actually of type TransitionEvent, but that type is not available in + * Android so we use any. + * @param e The event. + * @private + */ + @HostListener('transitionend', ['$event']) private onTransitionEnd_(e: any) { + if (e.target == this.elementRef_.nativeElement + // Simpler version to check for prefixes. + && e.propertyName.endsWith('transform')) { + this.transition_ = false; + if (this.opened_) { + if (this.openPromise_) { + this.openPromiseResolve_(); + } + if (this.closePromise_) { + this.closePromiseReject_(); + } + + this.onOpen.emit(null); + } else { + if (this.closePromise_) { + this.closePromiseResolve_(); + } + if (this.openPromise_) { + this.openPromiseReject_(); + } + + this.onClose.emit(null); + } + + this.openPromise_ = null; + this.closePromise_ = null; + } + } + + @HostBinding('class.md-sidenav-closing') private get isClosing_() { + return !this.opened_ && this.transition_; + } + @HostBinding('class.md-sidenav-opening') private get isOpening_() { + return this.opened_ && this.transition_; + } + @HostBinding('class.md-sidenav-closed') private get isClosed_() { + return !this.opened_ && !this.transition_; + } + @HostBinding('class.md-sidenav-opened') private get isOpened_() { + return this.opened_ && !this.transition_; + } + @HostBinding('class.md-sidenav-end') private get isEnd_() { + return this.align == 'end'; + } + @HostBinding('class.md-sidenav-side') private get modeSide_() { + return this.mode == 'side'; + } + @HostBinding('class.md-sidenav-over') private get modeOver_() { + return this.mode == 'over'; + } + @HostBinding('class.md-sidenav-push') private get modePush_() { + return this.mode == 'push'; + } + + /** + * This is public because we need it from MdSidenavLayout, but it's undocumented and should + * not be used outside. + * @returns {number} + * @private + */ + public get width_() { + if (this.elementRef_.nativeElement) { + return this.elementRef_.nativeElement.offsetWidth; + } + return 0; + } + + private transition_: boolean = false; + private openPromise_: Promise; + private openPromiseResolve_: () => void; + private openPromiseReject_: () => void; + private closePromise_: Promise; + private closePromiseResolve_: () => void; + private closePromiseReject_: () => void; +} + + + +/** + * component. + */ +@Component({ + selector: 'md-sidenav-layout', + directives: [MdSidenav], + templateUrl: './components/sidenav/sidenav.html', + styleUrls: ['./components/sidenav/sidenav.css'], +}) +export class MdSidenavLayout implements AfterContentInit { + @ContentChildren(MdSidenav) private drawers_: QueryList; + + get start() { return this.start_; } + get end() { return this.end_; } + + constructor(@Optional() @Host() private dir_: Dir) { + if (dir_) { + dir_.dirChange.subscribe(() => this.validateDrawers_()); + } + } + + ngAfterContentInit() { + // On changes, assert on consistency. + this.drawers_.changes.subscribe(() => this.validateDrawers_()); + this.validateDrawers_(); + } + + + + private start_: MdSidenav; + private end_: MdSidenav; + private right_: MdSidenav; + private left_: MdSidenav; + + private validateDrawers_() { + this.start_ = this.end_ = null; + if (this.drawers_.length === 0) { + throw new MdMissingSidenavException(); + } + + this.drawers_.toArray().forEach(drawer => { + if (drawer.align == 'end') { + if (this.end_) { + throw new MdDuplicatedSidenavException('end'); + } + this.end_ = drawer; + } else { + if (this.start_) { + throw new MdDuplicatedSidenavException('start'); + } + this.start_ = drawer; + } + }); + + this.right_ = this.left_ = null; + this.left_ = this.start_; + this.right_ = this.end_; + + // Detect if we're LTR or RTL. + if (!this.dir_ || this.dir_.dir == 'ltr') { + this.left_ = this.start_; + this.right_ = this.end_; + } else { + this.left_ = this.end_; + this.right_ = this.start_; + } + } + + private onBackdropClick_() { + if (this.start_ && this.start_.mode != 'side') { + this.start_.close(); + } + if (this.end_ && this.end_.mode != 'side') { + this.end_.close(); + } + } + + private get showBackdrop_() { + return (this.start_ && this.start_.mode != 'side' && this.start_.opened) + || (this.end_ && this.end_.mode != 'side' && this.end_.opened); + } + + /** + * Return the width of the sidenav, if it's in the proper mode and opened. + * @param MdSidenav + * @private + */ + private widthFor_(sidenav: MdSidenav, mode: string): number { + if (sidenav && sidenav.mode == mode && sidenav.opened) { + return sidenav.width_; + } + return 0; + } + + private get marginLeft_() { + return this.widthFor_(this.left_, 'side'); + } + + private get marginRight_() { + return this.widthFor_(this.right_, 'side'); + } + + private get positionLeft_() { + return this.widthFor_(this.left_, 'push'); + } + + private get positionRight_() { + return this.widthFor_(this.right_, 'push'); + } +} + + +export const MD_SIDENAV_DIRECTIVES: Type[] = CONST_EXPR([MdSidenavLayout, MdSidenav]); diff --git a/src/core/annotations/one-of.dart b/src/core/annotations/one-of.dart new file mode 100644 index 000000000000..fa3c5ef13ed9 --- /dev/null +++ b/src/core/annotations/one-of.dart @@ -0,0 +1,7 @@ +/** + * Annotation for a @OneOf([]) property. For now, this only works in TypeScript. + * TODO(hansl): Implement this properly in Dart. + */ +class OneOf { + const OneOf(List values); +} diff --git a/src/core/annotations/one-of.ts b/src/core/annotations/one-of.ts new file mode 100644 index 000000000000..1e1982d0b638 --- /dev/null +++ b/src/core/annotations/one-of.ts @@ -0,0 +1,34 @@ +import {isPresent} from 'angular2/src/facade/lang'; + + +declare var Symbol: any; + + +/** + * Annotation Factory that only allows a set of values to be set for a property. + * @param values The list of allowed values. The first value listed will be used as a default if + * the set value isn't part of the list of accepted values. + */ +function OneOfFactory(values: any[]) { + return function OneOfMetadata(target: any, key: string): void { + const defaultValue = values[0]; + + // Use a fallback if Symbol isn't available. + const localKey = isPresent(Symbol) ? Symbol(key) : `@@$${key}_`; + target[localKey] = defaultValue; + + Object.defineProperty(target, key, { + get() { return this[localKey]; }, + set(v) { + if (values.indexOf(v) == -1) { + this[localKey] = defaultValue; + } else { + this[localKey] = v; + } + } + }); + } +} + + +export const OneOf = OneOfFactory; diff --git a/src/core/style/_default-theme.scss b/src/core/style/_default-theme.scss index 72da087ea22b..a99ee6c19b8d 100644 --- a/src/core/style/_default-theme.scss +++ b/src/core/style/_default-theme.scss @@ -8,6 +8,6 @@ $md-is-dark-theme: false; $md-primary: md-palette($md-indigo, 500, 100, 700, $md-contrast-palettes); $md-accent: md-palette($md-red, A200, A100, A400, $md-contrast-palettes); -$md-background: md-palette($md-grey, 500, 300, 600, $md-contrast-palettes); $md-warn: md-palette($md-red, 500, 300, 800, $md-contrast-palettes); $md-foreground: if($md-is-dark-theme, $md-dark-theme-foreground, $md-light-theme-foreground); +$md-background: if($md-is-dark-theme, $md-dark-theme-background, $md-light-theme-background); diff --git a/src/core/style/_mixins.scss b/src/core/style/_mixins.scss new file mode 100644 index 000000000000..e809739c037f --- /dev/null +++ b/src/core/style/_mixins.scss @@ -0,0 +1,11 @@ + +/** + * Mixin that creates a new stacking context. + * see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context + */ +@mixin md-stacking-context() { + position: relative; + + // Use a transform to create a new stacking context. + transform: translate3D(0, 0, 0); +} diff --git a/src/core/style/_variables.scss b/src/core/style/_variables.scss index e54e70361ec2..70f1400d2f27 100644 --- a/src/core/style/_variables.scss +++ b/src/core/style/_variables.scss @@ -10,6 +10,7 @@ $md-xsmall: "max-width: 600px"; // z-index master list $z-index-fab: 20 !default; +$z-index-drawer: 100 !default; // Easing Curves // TODO(jelbourn): all of these need to be revisited diff --git a/src/demo-app/demo-app.html b/src/demo-app/demo-app.html index af29b899c403..d8ea94f2c169 100644 --- a/src/demo-app/demo-app.html +++ b/src/demo-app/demo-app.html @@ -1,57 +1,103 @@ -

- material2 Works! - - - -

-
- - - - Card with title + + +
+

+ material2 Works! + + + +

+ +
+ + + + Card with title + Subtitle + + + + + Subtitle - - - - - - Subtitle - Card with title - -

This is supporting text.

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

-
- - - - -
- - - - Content Title - -

Here is some content

-
-
- - - - - Header title - Header subtitle - - - -

Here is some content

-
-
- - - Easily customizable - - - - - -
\ No newline at end of file + Card with title + +

This is supporting text.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

+
+ + + + + + + + + Content Title + +

Here is some content

+
+
+ + + + + Header title + Header subtitle + + + +

Here is some content

+
+
+ + + Easily customizable + + + + + +
+ + + + Start Side Drawer +
+ +
+ +
+ +
Mode: {{start.mode}}
+
+ +
+ + + End Side Drawer +
+ +
+ +
+

My Content

+ +
+
Sidenav
+ + +
+ + + + +
+
+ +

Content after Sidenav

+
+ diff --git a/src/demo-app/demo-app.scss b/src/demo-app/demo-app.scss index c3fe5a4f3ad5..4db785456d72 100644 --- a/src/demo-app/demo-app.scss +++ b/src/demo-app/demo-app.scss @@ -20,3 +20,16 @@ md-card { img { background-color: gray; } + +md-sidenav-layout { + border: 3px solid black; + + md-sidenav { + padding: 10px; + } +} + +div.content { + padding: 10px; +} + diff --git a/src/demo-app/demo-app.ts b/src/demo-app/demo-app.ts index 0a08ca975715..5b5fe34de85f 100644 --- a/src/demo-app/demo-app.ts +++ b/src/demo-app/demo-app.ts @@ -1,13 +1,16 @@ import {Component} from 'angular2/core'; import {MdButton} from '../components/button/button'; import {MD_CARD_DIRECTIVES} from '../components/card/card'; +import {MD_SIDENAV_DIRECTIVES} from '../components/sidenav/sidenav'; +import {Dir} from '../directives/dir/dir'; + @Component({ selector: 'demo-app', providers: [], templateUrl: 'demo-app/demo-app.html', styleUrls: ['demo-app/demo-app.css'], - directives: [MdButton, MD_CARD_DIRECTIVES], + directives: [MdButton, MD_CARD_DIRECTIVES, Dir, MdButton, MD_SIDENAV_DIRECTIVES], pipes: [] }) export class DemoApp { } diff --git a/src/directives/dir/dir.ts b/src/directives/dir/dir.ts new file mode 100644 index 000000000000..15b0644f65eb --- /dev/null +++ b/src/directives/dir/dir.ts @@ -0,0 +1,31 @@ +import {Directive, HostBinding, EventEmitter, Output, Input} from 'angular2/core'; +import {OneOf} from "../../core/annotations/one-of"; + + +/** + * Directive to listen to changes of direction of part of the DOM. + * + * Applications should use this directive instead of the native attribute so that Material + * components can listen on changes of direction. + */ +@Directive({ + selector: '[dir]', + exportAs: '$implicit' +}) +export class Dir { + @Input('dir') @OneOf(['ltr', 'rtl']) private dir_: string = 'ltr'; + + @Output() dirChange = new EventEmitter(); + + @HostBinding('attr.dir') + get dir(): string { + return this.dir_; + } + set dir(v: string) { + const old = this.dir_; + this.dir_ = v; + if (old != this.dir_) { + this.dirChange.emit(null); + } + } +} diff --git a/src/index.html b/src/index.html index c89b6cfc4383..bd042e267ab1 100644 --- a/src/index.html +++ b/src/index.html @@ -12,6 +12,7 @@ Loading... + @@ -26,7 +27,15 @@ 'components': { format: 'register', defaultExtension: 'js' - } + }, + 'directives': { + format: 'register', + defaultExtension: 'js' + }, + 'core': { + format: 'register', + defaultExtension: 'js' + }, } }); System.import('main.js').then(null, console.error.bind(console)); diff --git a/test/karma-test-shim.js b/test/karma-test-shim.js index 6cca7c7f829f..5e556f7d47c0 100644 --- a/test/karma-test-shim.js +++ b/test/karma-test-shim.js @@ -11,13 +11,14 @@ __karma__.loaded = function() {}; */ function getPathsMap(dir) { return Object.keys(window.__karma__.files) - .filter(isComponentsFile) + .filter(not(isSpecFile)) + .filter(function(x) { return new RegExp('^/base/dist/' + dir + '/.*\\.js$').test(x); }) .reduce(function(pathsMapping, appPath) { var pathToReplace = new RegExp('^/base/dist/' + dir + '/'); var moduleName = appPath.replace(pathToReplace, './').replace(/\.js$/, ''); pathsMapping[moduleName] = appPath + '?' + window.__karma__.files[appPath]; - return pathsMapping; - }, {}); + return pathsMapping; + }, {}); } System.config({ @@ -26,7 +27,17 @@ System.config({ defaultExtension: false, format: 'register', map: getPathsMap('components') - } + }, + 'base/dist/core': { + defaultExtension: false, + format: 'register', + map: getPathsMap('core') + }, + 'base/dist/directives': { + defaultExtension: false, + format: 'register', + map: getPathsMap('directives') + }, } }); @@ -36,7 +47,7 @@ System.import('angular2/platform/browser').then(function(browser_adapter) { }).then(function() { return Promise.all( Object.keys(window.__karma__.files) - .filter(onlySpecFiles) + .filter(isSpecFile) .map(function(moduleName) { return System.import(moduleName); })); @@ -46,10 +57,12 @@ System.import('angular2/platform/browser').then(function(browser_adapter) { __karma__.error(error.stack || error); }); -function isComponentsFile(filePath) { - return /^\/base\/dist\/components\/(?!spec)([a-z0-9-_\/]+)\.js$/.test(filePath); +function isSpecFile(path) { + return /\.spec\.js$/.test(path); } -function onlySpecFiles(path) { - return /\.spec\.js$/.test(path); +function not(fn) { + return function() { + return !fn.apply(this, arguments); + }; } diff --git a/test/karma.config.ts b/test/karma.config.ts index 003eb653f2d9..9dc60e814c75 100644 --- a/test/karma.config.ts +++ b/test/karma.config.ts @@ -46,6 +46,8 @@ export function config(config) { // required for component assests fetched by Angular's compiler '/demo-app/': '/base/dist/demo-app/', '/components/': '/base/dist/components/', + '/core/': '/base/dist/core/', + '/directives/': '/base/dist/directives/', }, customLaunchers: customLaunchers,