diff --git a/src/demo-app/sidenav/sidenav-demo.html b/src/demo-app/sidenav/sidenav-demo.html index 83d58e7a47a4..6f885ab72e77 100644 --- a/src/demo-app/sidenav/sidenav-demo.html +++ b/src/demo-app/sidenav/sidenav-demo.html @@ -47,3 +47,14 @@

Sidenav Already Opened

+ +

Dynamic Alignment Sidenav

+ + + Drawer + +
+ + +
+
\ No newline at end of file diff --git a/src/demo-app/sidenav/sidenav-demo.ts b/src/demo-app/sidenav/sidenav-demo.ts index 6a689378c86b..3e653e115256 100644 --- a/src/demo-app/sidenav/sidenav-demo.ts +++ b/src/demo-app/sidenav/sidenav-demo.ts @@ -7,4 +7,6 @@ import {Component} from '@angular/core'; templateUrl: 'sidenav-demo.html', styleUrls: ['sidenav-demo.css'], }) -export class SidenavDemo {} +export class SidenavDemo { + side = 'start'; +} diff --git a/src/lib/sidenav/README.md b/src/lib/sidenav/README.md index 18a51b1af692..4393343800fa 100644 --- a/src/lib/sidenav/README.md +++ b/src/lib/sidenav/README.md @@ -11,13 +11,6 @@ MdSidenav is the side navigation component for Material 2. It is composed of two 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. @@ -26,7 +19,7 @@ The sidenav panel. | 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. | +| `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. If there is more than 1 sidenav on either side the layout will be considered invalid and none of the sidenavs will be visible or toggleable until the layout is valid again. | | `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. | diff --git a/src/lib/sidenav/sidenav.scss b/src/lib/sidenav/sidenav.scss index 3bfcdde54d61..c38d47a4a00c 100644 --- a/src/lib/sidenav/sidenav.scss +++ b/src/lib/sidenav/sidenav.scss @@ -18,13 +18,11 @@ } &.md-sidenav-closing { transform: translate3d($close, 0, 0); - will-change: transform; } &.md-sidenav-opening { @include md-elevation(1); visibility: visible; transform: translate3d($open, 0, 0); - will-change: transform; } &.md-sidenav-opened { @include md-elevation(1); @@ -131,3 +129,7 @@ md-sidenav { } } } + +.md-sidenav-invalid { + display: none; +} \ No newline at end of file diff --git a/src/lib/sidenav/sidenav.spec.ts b/src/lib/sidenav/sidenav.spec.ts index 0904aa9452d2..7949279ece82 100644 --- a/src/lib/sidenav/sidenav.spec.ts +++ b/src/lib/sidenav/sidenav.spec.ts @@ -25,6 +25,7 @@ describe('MdSidenav', () => { SidenavLayoutNoSidenavTestApp, SidenavSetToOpenedFalse, SidenavSetToOpenedTrue, + SidenavDynamicAlign, ], }); @@ -193,14 +194,6 @@ describe('MdSidenav', () => { tick(); }).not.toThrow(); })); - - it('does throw when created with two sidenav on the same side', fakeAsync(() => { - expect(() => { - let fixture = TestBed.createComponent(SidenavLayoutTwoSidenavTestApp); - fixture.detectChanges(); - tick(); - }).toThrow(); - })); }); describe('attributes', () => { @@ -238,6 +231,24 @@ describe('MdSidenav', () => { .toBe(false, 'Expected sidenav not to have a native align attribute.'); }); + it('should mark sidenavs invalid when multiple have same align', () => { + const fixture = TestBed.createComponent(SidenavDynamicAlign); + fixture.detectChanges(); + + const testComponent: SidenavDynamicAlign = fixture.debugElement.componentInstance; + const sidenavEl = fixture.debugElement.query(By.css('md-sidenav')).nativeElement; + expect(sidenavEl.classList).not.toContain('md-sidenav-invalid'); + + testComponent.sidenav1Align = 'end'; + fixture.detectChanges(); + + expect(sidenavEl.classList).toContain('md-sidenav-invalid'); + + testComponent.sidenav2Align = 'start'; + fixture.detectChanges(); + + expect(sidenavEl.classList).not.toContain('md-sidenav-invalid'); + }); }); }); @@ -314,3 +325,15 @@ class SidenavSetToOpenedFalse { } `, }) class SidenavSetToOpenedTrue { } + +@Component({ + template: ` + + + + `, +}) +class SidenavDynamicAlign { + sidenav1Align = 'start'; + sidenav2Align = 'end'; +} diff --git a/src/lib/sidenav/sidenav.ts b/src/lib/sidenav/sidenav.ts index 6674c1bf6471..e725e01f5ce3 100644 --- a/src/lib/sidenav/sidenav.ts +++ b/src/lib/sidenav/sidenav.ts @@ -5,7 +5,6 @@ import { Component, ContentChildren, ElementRef, - HostBinding, Input, Optional, Output, @@ -40,14 +39,51 @@ export class MdDuplicatedSidenavError extends MdError { host: { '(transitionend)': '_onTransitionEnd($event)', // must prevent the browser from aligning text based on value - '[attr.align]': 'null' + '[attr.align]': 'null', + '[class.md-sidenav-closed]': '_isClosed', + '[class.md-sidenav-closing]': '_isClosing', + '[class.md-sidenav-end]': '_isEnd', + '[class.md-sidenav-opened]': '_isOpened', + '[class.md-sidenav-opening]': '_isOpening', + '[class.md-sidenav-over]': '_modeOver', + '[class.md-sidenav-push]': '_modePush', + '[class.md-sidenav-side]': '_modeSide', + '[class.md-sidenav-invalid]': '!valid', }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, }) export class MdSidenav implements AfterContentInit { /** Alignment of the sidenav (direction neutral); whether 'start' or 'end'. */ - @Input() align: 'start' | 'end' = 'start'; + private _align: 'start' | 'end' = 'start'; + + /** Whether this md-sidenav is part of a valid md-sidenav-layout configuration. */ + get valid() { + return this._valid; + } + set valid(value) { + value = coerceBooleanProperty(value); + // When the drawers are not in a valid configuration we close them all until they are in a valid + // configuration again. + if (!value) { + this.close(); + } + this._valid = value; + } + private _valid = true; + + @Input() + get align() { + return this._align; + } + set align(value) { + // Make sure we have a valid value. + value = (value == 'end') ? 'end' : 'start'; + if (value != this._align) { + this._align = value; + this.onAlignChanged.emit(); + } + } /** Mode of the sidenav; whether 'over' or 'side'. */ @Input() mode: 'over' | 'push' | 'side' = 'over'; @@ -67,6 +103,9 @@ export class MdSidenav implements AfterContentInit { /** Event emitted when the sidenav is fully closed. */ @Output('close') onClose = new EventEmitter(); + /** Event emitted when the sidenav alignment changes. */ + @Output('align-changed') onAlignChanged = new EventEmitter(); + /** * @param _elementRef The DOM element reference. Used for transition and width calculation. * If not available we do not hook on transitions. @@ -113,6 +152,8 @@ export class MdSidenav implements AfterContentInit { * @param isOpen */ toggle(isOpen: boolean = !this.opened): Promise { + if (!this.valid) { return Promise.resolve(null); } + // Shortcut it if we're already opened. if (isOpen === this.opened) { if (!this._transition) { @@ -186,32 +227,31 @@ export class MdSidenav implements AfterContentInit { } } - @HostBinding('class.md-sidenav-closing') get _isClosing() { + get _isClosing() { return !this._opened && this._transition; } - @HostBinding('class.md-sidenav-opening') get _isOpening() { + get _isOpening() { return this._opened && this._transition; } - @HostBinding('class.md-sidenav-closed') get _isClosed() { + get _isClosed() { return !this._opened && !this._transition; } - @HostBinding('class.md-sidenav-opened') get _isOpened() { + get _isOpened() { return this._opened && !this._transition; } - @HostBinding('class.md-sidenav-end') get _isEnd() { + get _isEnd() { return this.align == 'end'; } - @HostBinding('class.md-sidenav-side') get _modeSide() { + get _modeSide() { return this.mode == 'side'; } - @HostBinding('class.md-sidenav-over') get _modeOver() { + get _modeOver() { return this.mode == 'over'; } - @HostBinding('class.md-sidenav-push') get _modePush() { + get _modePush() { return this.mode == 'push'; } - /** TODO: internal (needed by MdSidenavLayout). */ get _width() { if (this._elementRef.nativeElement) { return this._elementRef.nativeElement.offsetWidth; @@ -232,7 +272,7 @@ export class MdSidenav implements AfterContentInit { * component. * * This is the parent component to one or two s that validates the state internally - * and coordinate the backdrop and content styling. + * and coordinates the backdrop and content styling. */ @Component({ moduleId: module.id, @@ -275,48 +315,73 @@ export class MdSidenavLayout implements AfterContentInit { } } - /** TODO: internal */ ngAfterContentInit() { // On changes, assert on consistency. this._sidenavs.changes.subscribe(() => this._validateDrawers()); - this._sidenavs.forEach((sidenav: MdSidenav) => this._watchSidenavToggle(sidenav)); + this._sidenavs.forEach((sidenav: MdSidenav) => { + this._watchSidenavToggle(sidenav); + this._watchSidenavAlign(sidenav); + }); this._validateDrawers(); } - /* - * Subscribes to sidenav events in order to set a class on the main layout element when the sidenav - * is open and the backdrop is visible. This ensures any overflow on the layout element is properly - * hidden. - */ + /** + * Subscribes to sidenav events in order to set a class on the main layout element when the + * sidenav is open and the backdrop is visible. This ensures any overflow on the layout element is + * properly hidden. + */ private _watchSidenavToggle(sidenav: MdSidenav): void { if (!sidenav || sidenav.mode === 'side') { return; } sidenav.onOpen.subscribe(() => this._setLayoutClass(sidenav, true)); sidenav.onClose.subscribe(() => this._setLayoutClass(sidenav, false)); } - /* Toggles the 'md-sidenav-opened' class on the main 'md-sidenav-layout' element. */ + /** + * Subscribes to sidenav onAlignChanged event in order to re-validate drawers when the align + * changes. + */ + private _watchSidenavAlign(sidenav: MdSidenav): void { + if (!sidenav) { return; } + sidenav.onAlignChanged.subscribe(() => this._validateDrawers()); + } + + /** Toggles the 'md-sidenav-opened' class on the main 'md-sidenav-layout' element. */ private _setLayoutClass(sidenav: MdSidenav, bool: boolean): void { this._renderer.setElementClass(this._element.nativeElement, 'md-sidenav-opened', bool); } + /** Sets the valid state of the drawers. */ + private _setDrawersValid(valid: boolean) { + this._sidenavs.forEach((sidenav) => { + sidenav.valid = valid; + }); + if (!valid) { + this._start = this._end = this._left = this._right = null; + } + } + /** Validate the state of the sidenav children components. */ private _validateDrawers() { this._start = this._end = null; // Ensure that we have at most one start and one end sidenav. - this._sidenavs.forEach(sidenav => { + // NOTE: We must call toArray on _sidenavs even though it's iterable + // (see https://github.com/Microsoft/TypeScript/issues/3164). + for (let sidenav of this._sidenavs.toArray()) { if (sidenav.align == 'end') { if (this._end != null) { - throw new MdDuplicatedSidenavError('end'); + this._setDrawersValid(false); + return; } this._end = sidenav; } else { if (this._start != null) { - throw new MdDuplicatedSidenavError('start'); + this._setDrawersValid(false); + return; } this._start = sidenav; } - }); + } this._right = this._left = null; @@ -328,6 +393,8 @@ export class MdSidenavLayout implements AfterContentInit { this._left = this._end; this._right = this._start; } + + this._setDrawersValid(true); } _closeModalSidenav() {