diff --git a/e2e/components/sidenav-e2e.spec.ts b/e2e/components/sidenav-e2e.spec.ts index 998d732e5d88..f14688f3803b 100644 --- a/e2e/components/sidenav-e2e.spec.ts +++ b/e2e/components/sidenav-e2e.spec.ts @@ -1,26 +1,27 @@ -import {browser, by, element, ExpectedConditions} from 'protractor'; +import {browser, by, element, ElementFinder} from 'protractor'; describe('sidenav', () => { describe('opening and closing', () => { - beforeEach(() => browser.get('/sidenav')); - - let input = element(by.tagName('md-sidenav')); + let sidenav: ElementFinder; + beforeEach(() => { + browser.get('/sidenav'); + sidenav = element(by.tagName('md-sidenav')); + }); it('should be closed', () => { - expect(input.isDisplayed()).toBeFalsy(); + expect(sidenav.isDisplayed()).toBeFalsy(); }); it('should open', () => { element(by.buttonText('Open sidenav')).click(); - expect(input.isDisplayed()).toBeTruthy(); + expect(sidenav.isDisplayed()).toBeTruthy(); }); it('should close again', () => { element(by.buttonText('Open sidenav')).click(); element(by.buttonText('Open sidenav')).click(); - browser.wait(ExpectedConditions.presenceOf(element(by.className('mat-sidenav-closed'))), 999); - expect(input.isDisplayed()).toBeFalsy(); + expect(sidenav.isDisplayed()).toBeFalsy(); }); }); }); diff --git a/src/lib/sidenav/sidenav-transitions.scss b/src/lib/sidenav/sidenav-transitions.scss index d9222e5da47a..cbbbd9e0c472 100644 --- a/src/lib/sidenav/sidenav-transitions.scss +++ b/src/lib/sidenav/sidenav-transitions.scss @@ -2,10 +2,6 @@ @import '../core/style/variables'; .mat-sidenav-transition { - .mat-sidenav { - transition: transform $swift-ease-out-duration $swift-ease-out-timing-function; - } - .mat-sidenav-content { transition: { duration: $swift-ease-out-duration; diff --git a/src/lib/sidenav/sidenav.scss b/src/lib/sidenav/sidenav.scss index 4f70dff0c874..4a3083d678df 100644 --- a/src/lib/sidenav/sidenav.scss +++ b/src/lib/sidenav/sidenav.scss @@ -3,26 +3,6 @@ @import '../core/style/layout-common'; @import '../core/a11y/a11y'; - -// Mixin to help with defining LTR/RTL 'transform: translate3d()' values. -// @param $open The translation value when the sidenav is opened. -// @param $close The translation value when the sidenav is closed. -@mixin mat-sidenav-transition($open, $close) { - transform: translate3d($close, 0, 0); - - &.mat-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; - } - - &.mat-sidenav-opening, &.mat-sidenav-opened { - transform: translate3d($open, 0, 0); - } -} - // 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 mat-sidenav-stacking-context() { @@ -103,8 +83,7 @@ box-sizing: border-box; height: 100%; overflow-y: auto; // TODO(kara): revisit scrolling behavior for sidenavs - - @include mat-sidenav-transition(0, -100%); + transform: translate3d(-100%, 0, 0); &.mat-sidenav-side { z-index: 1; @@ -112,18 +91,16 @@ &.mat-sidenav-end { right: 0; - - @include mat-sidenav-transition(0, 100%); + transform: translate3d(100%, 0, 0); } [dir='rtl'] & { - @include mat-sidenav-transition(0, 100%); + transform: translate3d(100%, 0, 0); &.mat-sidenav-end { left: 0; right: auto; - - @include mat-sidenav-transition(0, -100%); + transform: translate3d(-100%, 0, 0); } } diff --git a/src/lib/sidenav/sidenav.spec.ts b/src/lib/sidenav/sidenav.spec.ts index 659be9585193..55e8f31dd6ca 100644 --- a/src/lib/sidenav/sidenav.spec.ts +++ b/src/lib/sidenav/sidenav.spec.ts @@ -1,25 +1,18 @@ import {fakeAsync, async, tick, ComponentFixture, TestBed} from '@angular/core/testing'; import {Component, ElementRef, ViewChild} from '@angular/core'; import {By} from '@angular/platform-browser'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {MdSidenav, MdSidenavModule, MdSidenavContainer} from './index'; import {A11yModule} from '../core/a11y/index'; import {PlatformModule} from '../core/platform/index'; import {ESCAPE} from '../core/keyboard/keycodes'; - -function endSidenavTransition(fixture: ComponentFixture) { - let sidenav: any = fixture.debugElement.query(By.directive(MdSidenav)).componentInstance; - sidenav._onTransitionEnd( { - target: (sidenav)._elementRef.nativeElement, - propertyName: 'transform' - }); - fixture.detectChanges(); -} +import {dispatchKeyboardEvent} from '@angular/cdk/testing'; describe('MdSidenav', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [MdSidenavModule, A11yModule, PlatformModule], + imports: [MdSidenavModule, A11yModule, PlatformModule, NoopAnimationsModule], declarations: [ BasicTestApp, SidenavContainerNoSidenavTestApp, @@ -37,138 +30,39 @@ describe('MdSidenav', () => { it('should be able to open and close', fakeAsync(() => { let fixture = TestBed.createComponent(BasicTestApp); - let testComponent: BasicTestApp = fixture.debugElement.componentInstance; - let openButtonElement = fixture.debugElement.query(By.css('.open')); - openButtonElement.nativeElement.click(); fixture.detectChanges(); tick(); - expect(testComponent.openStartCount).toBe(1); + let testComponent: BasicTestApp = fixture.debugElement.componentInstance; + let sidenav = fixture.debugElement.query(By.directive(MdSidenav)); + let sidenavBackdropElement = fixture.debugElement.query(By.css('.mat-sidenav-backdrop')); + + fixture.debugElement.query(By.css('.open')).nativeElement.click(); + fixture.detectChanges(); + expect(testComponent.openCount).toBe(0); + expect(testComponent.closeCount).toBe(0); - endSidenavTransition(fixture); tick(); - expect(testComponent.openStartCount).toBe(1); + expect(sidenav.componentInstance._isAnimating).toBe(false); expect(testComponent.openCount).toBe(1); - expect(testComponent.closeStartCount).toBe(0); expect(testComponent.closeCount).toBe(0); + expect(getComputedStyle(sidenav.nativeElement).visibility).toBe('visible'); + expect(getComputedStyle(sidenavBackdropElement.nativeElement).visibility).toBe('visible'); - let sidenavElement = fixture.debugElement.query(By.css('md-sidenav')); - let sidenavBackdropElement = fixture.debugElement.query(By.css('.mat-sidenav-backdrop')); - expect(getComputedStyle(sidenavElement.nativeElement).visibility).toEqual('visible'); - expect(getComputedStyle(sidenavBackdropElement.nativeElement).visibility) - .toEqual('visible'); - - // Close it. - let closeButtonElement = fixture.debugElement.query(By.css('.close')); - closeButtonElement.nativeElement.click(); + fixture.debugElement.query(By.css('.close')).nativeElement.click(); fixture.detectChanges(); - tick(); - expect(testComponent.openStartCount).toBe(1); expect(testComponent.openCount).toBe(1); - expect(testComponent.closeStartCount).toBe(1); expect(testComponent.closeCount).toBe(0); - endSidenavTransition(fixture); tick(); - expect(testComponent.openStartCount).toBe(1); expect(testComponent.openCount).toBe(1); - expect(testComponent.closeStartCount).toBe(1); expect(testComponent.closeCount).toBe(1); - - expect(getComputedStyle(sidenavElement.nativeElement).visibility).toEqual('hidden'); - expect(getComputedStyle(sidenavBackdropElement.nativeElement).visibility).toEqual('hidden'); - })); - - it('open/close() return a promise that resolves after animation end', fakeAsync(() => { - let fixture = TestBed.createComponent(BasicTestApp); - let sidenav: MdSidenav = fixture.debugElement - .query(By.directive(MdSidenav)).componentInstance; - let called = false; - - sidenav.open().then(() => { - called = true; - }); - - expect(called).toBe(false); - endSidenavTransition(fixture); - tick(); - expect(called).toBe(true); - - called = false; - sidenav.close().then(() => { - called = true; - }); - - expect(called).toBe(false); - endSidenavTransition(fixture); - tick(); - expect(called).toBe(true); - - })); - - it('open/close() twice returns the same promise', fakeAsync(() => { - let fixture = TestBed.createComponent(BasicTestApp); - let sidenav: MdSidenav = fixture.debugElement - .query(By.directive(MdSidenav)).componentInstance; - - let promise = sidenav.open(); - expect(sidenav.open()).toBe(promise); - fixture.detectChanges(); - tick(); - - promise = sidenav.close(); - expect(sidenav.close()).toBe(promise); - tick(); - })); - - it('open() then close() cancel animations when called too fast', fakeAsync(() => { - let fixture = TestBed.createComponent(BasicTestApp); - let sidenav: MdSidenav = fixture.debugElement - .query(By.directive(MdSidenav)).componentInstance; - - sidenav.open().then(openResult => { - expect(openResult.type).toBe('open'); - expect(openResult.animationFinished).toBe(false); - }); - - // We do not call transition end, close directly. - sidenav.close().then(closeResult => { - expect(closeResult.type).toBe('close'); - expect(closeResult.animationFinished).toBe(true); - }); - - endSidenavTransition(fixture); - tick(); - })); - - it('close() then open() cancel animations when called too fast', fakeAsync(() => { - let fixture = TestBed.createComponent(BasicTestApp); - let sidenav: MdSidenav = fixture.debugElement - .query(By.directive(MdSidenav)).componentInstance; - - // First, open the sidenav completely. - sidenav.open(); - endSidenavTransition(fixture); - tick(); - - // Then close and check behavior. - sidenav.close().then(closeResult => { - expect(closeResult.type).toBe('close'); - expect(closeResult.animationFinished).toBe(false); - }); - - // We do not call transition end, open directly. - sidenav.open().then(openResult => { - expect(openResult.type).toBe('open'); - expect(openResult.animationFinished).toBe(true); - }); - - endSidenavTransition(fixture); - tick(); + expect(getComputedStyle(sidenav.nativeElement).visibility).toBe('hidden'); + expect(getComputedStyle(sidenavBackdropElement.nativeElement).visibility).toBe('hidden'); })); it('does not throw when created without a sidenav', fakeAsync(() => { @@ -181,93 +75,67 @@ describe('MdSidenav', () => { it('should emit the backdropClick event when the backdrop is clicked', fakeAsync(() => { let fixture = TestBed.createComponent(BasicTestApp); - let testComponent: BasicTestApp = fixture.debugElement.componentInstance; - let openButtonElement = fixture.debugElement.query(By.css('.open')); - openButtonElement.nativeElement.click(); - fixture.detectChanges(); - tick(); + let openButtonElement = fixture.debugElement.query(By.css('.open')).nativeElement; - endSidenavTransition(fixture); + openButtonElement.click(); + fixture.detectChanges(); tick(); expect(testComponent.backdropClickedCount).toBe(0); - let sidenavBackdropElement = fixture.debugElement.query(By.css('.mat-sidenav-backdrop')); - sidenavBackdropElement.nativeElement.click(); + fixture.debugElement.query(By.css('.mat-sidenav-backdrop')).nativeElement.click(); fixture.detectChanges(); tick(); expect(testComponent.backdropClickedCount).toBe(1); - endSidenavTransition(fixture); - tick(); - - openButtonElement.nativeElement.click(); + openButtonElement.click(); fixture.detectChanges(); tick(); - endSidenavTransition(fixture); - tick(); - - let closeButtonElement = fixture.debugElement.query(By.css('.close')); - closeButtonElement.nativeElement.click(); + fixture.debugElement.query(By.css('.close')).nativeElement.click(); fixture.detectChanges(); tick(); - endSidenavTransition(fixture); - tick(); - expect(testComponent.backdropClickedCount).toBe(1); })); it('should close when pressing escape', fakeAsync(() => { let fixture = TestBed.createComponent(BasicTestApp); - let testComponent: BasicTestApp = fixture.debugElement.componentInstance; - let sidenav: MdSidenav = fixture.debugElement - .query(By.directive(MdSidenav)).componentInstance; - - sidenav.open(); fixture.detectChanges(); - endSidenavTransition(fixture); tick(); - expect(testComponent.openCount).toBe(1); - expect(testComponent.closeCount).toBe(0); + let testComponent: BasicTestApp = fixture.debugElement.componentInstance; + let sidenav = fixture.debugElement.query(By.directive(MdSidenav)); + + sidenav.componentInstance.open(); + fixture.detectChanges(); + tick(); - // Simulate pressing the escape key. - sidenav.handleKeydown({ - keyCode: ESCAPE, - stopPropagation: () => {} - } as KeyboardEvent); + expect(testComponent.openCount).toBe(1, 'Expected one open event.'); + expect(testComponent.closeCount).toBe(0, 'Expected no close events.'); + dispatchKeyboardEvent(sidenav.nativeElement, 'keydown', ESCAPE); fixture.detectChanges(); - endSidenavTransition(fixture); tick(); - expect(testComponent.closeCount).toBe(1); + expect(testComponent.closeCount).toBe(1, 'Expected one close event.'); })); it('should not close by pressing escape when disableClose is set', fakeAsync(() => { let fixture = TestBed.createComponent(BasicTestApp); let testComponent = fixture.debugElement.componentInstance; - let sidenav = fixture.debugElement.query(By.directive(MdSidenav)).componentInstance; - - sidenav.disableClose = true; - sidenav.open(); + let sidenav = fixture.debugElement.query(By.directive(MdSidenav)); + sidenav.componentInstance.disableClose = true; + sidenav.componentInstance.open(); fixture.detectChanges(); - endSidenavTransition(fixture); tick(); - sidenav.handleKeydown({ - keyCode: ESCAPE, - stopPropagation: () => {} - }); - + dispatchKeyboardEvent(sidenav.nativeElement, 'keydown', ESCAPE); fixture.detectChanges(); - endSidenavTransition(fixture); tick(); expect(testComponent.closeCount).toBe(0); @@ -280,42 +148,34 @@ describe('MdSidenav', () => { sidenav.disableClose = true; sidenav.open(); - fixture.detectChanges(); - endSidenavTransition(fixture); tick(); - let backdropEl = fixture.debugElement.query(By.css('.mat-sidenav-backdrop')).nativeElement; - backdropEl.click(); + fixture.debugElement.query(By.css('.mat-sidenav-backdrop')).nativeElement.click(); fixture.detectChanges(); tick(); - fixture.detectChanges(); - endSidenavTransition(fixture); - tick(); - expect(testComponent.closeCount).toBe(0); })); it('should restore focus on close if focus is inside sidenav', fakeAsync(() => { let fixture = TestBed.createComponent(BasicTestApp); - let sidenav: MdSidenav = fixture.debugElement - .query(By.directive(MdSidenav)).componentInstance; + + fixture.detectChanges(); + tick(); + + let sidenav = fixture.debugElement.query(By.directive(MdSidenav)).componentInstance; let openButton = fixture.componentInstance.openButton.nativeElement; let sidenavButton = fixture.componentInstance.sidenavButton.nativeElement; openButton.focus(); sidenav.open(); - fixture.detectChanges(); - endSidenavTransition(fixture); tick(); sidenavButton.focus(); sidenav.close(); - fixture.detectChanges(); - endSidenavTransition(fixture); tick(); expect(document.activeElement) @@ -333,14 +193,11 @@ describe('MdSidenav', () => { sidenav.open(); fixture.detectChanges(); - endSidenavTransition(fixture); tick(); closeButton.focus(); sidenav.close(); - fixture.detectChanges(); - endSidenavTransition(fixture); tick(); expect(document.activeElement) @@ -351,26 +208,22 @@ describe('MdSidenav', () => { describe('attributes', () => { it('should correctly parse opened="false"', () => { let fixture = TestBed.createComponent(SidenavSetToOpenedFalse); + fixture.detectChanges(); - let sidenavEl = fixture.debugElement.query(By.css('md-sidenav')).nativeElement; + let sidenav = fixture.debugElement.query(By.directive(MdSidenav)).componentInstance; - expect(sidenavEl.classList).toContain('mat-sidenav-closed'); - expect(sidenavEl.classList).not.toContain('mat-sidenav-opened'); + expect((sidenav as MdSidenav).opened).toBe(false); }); it('should correctly parse opened="true"', () => { let fixture = TestBed.createComponent(SidenavSetToOpenedTrue); - fixture.detectChanges(); - endSidenavTransition(fixture); - let sidenavEl = fixture.debugElement.query(By.css('md-sidenav')).nativeElement; - let testComponent = fixture.debugElement.query(By.css('md-sidenav')).componentInstance; + fixture.detectChanges(); - expect(sidenavEl.classList).not.toContain('mat-sidenav-closed'); - expect(sidenavEl.classList).toContain('mat-sidenav-opened'); + let sidenav = fixture.debugElement.query(By.directive(MdSidenav)).componentInstance; - expect((testComponent as any)._toggleAnimationPromise).toBeNull(); + expect((sidenav as MdSidenav).opened).toBe(true); }); it('should remove align attr from DOM', () => { @@ -382,15 +235,16 @@ describe('MdSidenav', () => { .toBe(false, 'Expected sidenav not to have a native align attribute.'); }); - it('should throw when multiple sidenavs have the same align', () => { + it('should throw when multiple sidenavs have the same align', fakeAsync(() => { const fixture = TestBed.createComponent(SidenavDynamicAlign); fixture.detectChanges(); + tick(); const testComponent: SidenavDynamicAlign = fixture.debugElement.componentInstance; testComponent.sidenav1Align = 'end'; expect(() => fixture.detectChanges()).toThrow(); - }); + })); it('should not throw when sidenavs swap sides', () => { const fixture = TestBed.createComponent(SidenavDynamicAlign); @@ -425,7 +279,7 @@ describe('MdSidenav', () => { lastFocusableElement.focus(); sidenav.open(); - endSidenavTransition(fixture); + fixture.detectChanges(); tick(); expect(document.activeElement).toBe(firstFocusableElement); @@ -436,7 +290,7 @@ describe('MdSidenav', () => { lastFocusableElement.focus(); sidenav.open(); - endSidenavTransition(fixture); + fixture.detectChanges(); tick(); expect(document.activeElement).toBe(firstFocusableElement); @@ -447,7 +301,7 @@ describe('MdSidenav', () => { lastFocusableElement.focus(); sidenav.open(); - endSidenavTransition(fixture); + fixture.detectChanges(); tick(); expect(document.activeElement).toBe(lastFocusableElement); @@ -458,38 +312,36 @@ describe('MdSidenav', () => { describe('MdSidenavContainer', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [MdSidenavModule, A11yModule, PlatformModule], - declarations: [ - SidenavContainerTwoSidenavTestApp - ], + imports: [MdSidenavModule, A11yModule, PlatformModule, NoopAnimationsModule], + declarations: [SidenavContainerTwoSidenavTestApp], }); TestBed.compileComponents(); })); - describe('methods', () => { - it('should be able to open and close', async(() => { - const fixture = TestBed.createComponent(SidenavContainerTwoSidenavTestApp); + it('should be able to open and close all sidenavs', fakeAsync(() => { + const fixture = TestBed.createComponent(SidenavContainerTwoSidenavTestApp); - fixture.detectChanges(); + fixture.detectChanges(); - const testComponent: SidenavContainerTwoSidenavTestApp = - fixture.debugElement.componentInstance; - const sidenavs = fixture.debugElement.queryAll(By.directive(MdSidenav)); + const testComponent: SidenavContainerTwoSidenavTestApp = + fixture.debugElement.componentInstance; + const sidenavs = fixture.debugElement.queryAll(By.directive(MdSidenav)); - expect(sidenavs.every(sidenav => sidenav.componentInstance.opened)).toBeFalsy(); + expect(sidenavs.every(sidenav => sidenav.componentInstance.opened)).toBe(false); - return testComponent.sidenavContainer.open() - .then(() => { - expect(sidenavs.every(sidenav => sidenav.componentInstance.opened)).toBeTruthy(); + testComponent.sidenavContainer.open(); + fixture.detectChanges(); + tick(); - return testComponent.sidenavContainer.close(); - }) - .then(() => { - expect(sidenavs.every(sidenav => sidenav.componentInstance.opened)).toBeTruthy(); - }); - })); - }); + expect(sidenavs.every(sidenav => sidenav.componentInstance.opened)).toBe(true); + + testComponent.sidenavContainer.close(); + fixture.detectChanges(); + tick(); + + expect(sidenavs.every(sidenav => sidenav.componentInstance.opened)).toBe(false); + })); }); @@ -515,9 +367,7 @@ class SidenavContainerTwoSidenavTestApp { template: ` @@ -526,9 +376,7 @@ class SidenavContainerTwoSidenavTestApp { `, }) class BasicTestApp { - openStartCount: number = 0; openCount: number = 0; - closeStartCount: number = 0; closeCount: number = 0; backdropClickedCount: number = 0; @@ -536,18 +384,10 @@ class BasicTestApp { @ViewChild('openButton') openButton: ElementRef; @ViewChild('closeButton') closeButton: ElementRef; - openStart() { - this.openStartCount++; - } - open() { this.openCount++; } - closeStart() { - this.closeStartCount++; - } - close() { this.closeCount++; } diff --git a/src/lib/sidenav/sidenav.ts b/src/lib/sidenav/sidenav.ts index 329a5b066776..884985a3d536 100644 --- a/src/lib/sidenav/sidenav.ts +++ b/src/lib/sidenav/sidenav.ts @@ -24,12 +24,12 @@ import { Inject, ChangeDetectorRef, } from '@angular/core'; +import {animate, state, style, transition, trigger, AnimationEvent} from '@angular/animations'; import {Directionality, coerceBooleanProperty} from '../core'; import {FocusTrapFactory, FocusTrap} from '../core/a11y/focus-trap'; import {ESCAPE} from '../core/keyboard/keycodes'; import {first} from '../core/rxjs/index'; import {DOCUMENT} from '@angular/platform-browser'; -import {merge} from 'rxjs/observable/merge'; /** Throws an exception when two MdSidenav are matching the same side. */ @@ -38,12 +38,14 @@ export function throwMdDuplicatedSidenavError(align: string) { } -/** Sidenav toggle promise result. */ +/** + * Sidenav toggle promise result. + * @deprecated + */ export class MdSidenavToggleResult { constructor(public type: 'open' | 'close', public animationFinished: boolean) {} } - /** * component. * @@ -55,27 +57,42 @@ export class MdSidenavToggleResult { moduleId: module.id, selector: 'md-sidenav, mat-sidenav', templateUrl: 'sidenav.html', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + animations: [ + trigger('transform', [ + state('open, open-instant', style({ + transform: 'translate3d(0, 0, 0)', + visibility: 'visible', + })), + state('void', style({ + visibility: 'hidden', + })), + transition('void => open-instant', animate('0ms')), + transition('void <=> open', animate('400ms cubic-bezier(0.25, 0.8, 0.25, 1)')) + ]) + ], host: { 'class': 'mat-sidenav', - '(transitionend)': '_onTransitionEnd($event)', + '[@transform]': '_getAnimationState()', + '(@transform.start)': '_onAnimationStart()', + '(@transform.done)': '_onAnimationEnd($event)', '(keydown)': 'handleKeydown($event)', // must prevent the browser from aligning text based on value '[attr.align]': 'null', - '[class.mat-sidenav-closed]': '_isClosed', - '[class.mat-sidenav-closing]': '_isClosing', - '[class.mat-sidenav-end]': '_isEnd', - '[class.mat-sidenav-opened]': '_isOpened', - '[class.mat-sidenav-opening]': '_isOpening', - '[class.mat-sidenav-over]': '_modeOver', - '[class.mat-sidenav-push]': '_modePush', - '[class.mat-sidenav-side]': '_modeSide', + '[class.mat-sidenav-end]': 'align === "end"', + '[class.mat-sidenav-over]': 'mode === "over"', + '[class.mat-sidenav-push]': 'mode === "push"', + '[class.mat-sidenav-side]': 'mode === "side"', 'tabIndex': '-1' }, - changeDetection: ChangeDetectionStrategy.OnPush, - encapsulation: ViewEncapsulation.None, }) export class MdSidenav implements AfterContentInit, OnDestroy { private _focusTrap: FocusTrap; + private _elementFocusedBeforeSidenavWasOpened: HTMLElement | null = null; + + /** Whether the sidenav is initialized. Used for disabling the initial animation. */ + private _enableAnimations = false; /** Alignment of the sidenav (direction neutral); whether 'start' or 'end'. */ private _align: 'start' | 'end' = 'start'; @@ -85,7 +102,7 @@ export class MdSidenav implements AfterContentInit, OnDestroy { get align() { return this._align; } set align(value) { // Make sure we have a valid value. - value = (value == 'end') ? 'end' : 'start'; + value = value === 'end' ? 'end' : 'start'; if (value != this._align) { this._align = value; this.onAlignChanged.emit(); @@ -102,41 +119,35 @@ export class MdSidenav implements AfterContentInit, OnDestroy { private _disableClose: boolean = false; /** Whether the sidenav is opened. */ - _opened: boolean = false; + private _opened: boolean = false; - /** Event emitted when the sidenav is being opened. Use this to synchronize animations. */ - @Output('open-start') onOpenStart = new EventEmitter(); + /** Emits whenever the sidenav has started animating. */ + _animationStarted = new EventEmitter(); - /** Event emitted when the sidenav is fully opened. */ - @Output('open') onOpen = new EventEmitter(); + /** Whether the sidenav is animating. Used to prevent overlapping animations. */ + _isAnimating = false; + + /** + * Promise that resolves when the open/close animation completes. It is here for backwards + * compatibility and should be removed next time we do sidenav breaking changes. + * @deprecated + */ + private _currentTogglePromise: Promise | null; - /** 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 opened. */ + @Output('open') onOpen = new EventEmitter(); /** Event emitted when the sidenav is fully closed. */ - @Output('close') onClose = new EventEmitter(); + @Output('close') onClose = new EventEmitter(); /** Event emitted when the sidenav alignment changes. */ @Output('align-changed') onAlignChanged = new EventEmitter(); - /** The current toggle animation promise. `null` if no animation is in progress. */ - private _toggleAnimationPromise: Promise | null = null; - - /** - * The current toggle animation promise resolution function. - * `null` if no animation is in progress. - */ - private _resolveToggleAnimationPromise: ((animationFinished: boolean) => void) | null = null; - get isFocusTrapEnabled() { // The focus trap is only enabled when the sidenav is open in any mode other than side. return this.opened && this.mode !== 'side'; } - /** - * @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, private _focusTrapFactory: FocusTrapFactory, @Optional() @Inject(DOCUMENT) private _doc: any) { @@ -173,13 +184,7 @@ export class MdSidenav implements AfterContentInit, OnDestroy { ngAfterContentInit() { this._focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement); this._focusTrap.enabled = this.isFocusTrapEnabled; - - // This can happen when the sidenav is set to opened in - // the template and the transition hasn't ended. - if (this._toggleAnimationPromise && this._resolveToggleAnimationPromise) { - this._resolveToggleAnimationPromise(true); - this._toggleAnimationPromise = this._resolveToggleAnimationPromise = null; - } + Promise.resolve().then(() => this._enableAnimations = true); } ngOnDestroy() { @@ -199,53 +204,35 @@ export class MdSidenav implements AfterContentInit, OnDestroy { } - /** Open this sidenav, and return a Promise that will resolve when it's fully opened (or get - * rejected if it didn't). */ + /** Open the sidenav. */ 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 the sidenav. */ 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. + * Toggle this sidenav. * @param isOpen Whether the sidenav should be open. - * @returns Resolves with the result of whether the sidenav was opened or closed. */ toggle(isOpen: boolean = !this.opened): Promise { - // Shortcut it if we're already opened. - if (isOpen === this.opened) { - return this._toggleAnimationPromise || - Promise.resolve(new MdSidenavToggleResult(isOpen ? 'open' : 'close', true)); - } - - this._opened = isOpen; - - if (this._focusTrap) { - this._focusTrap.enabled = this.isFocusTrapEnabled; - } - - if (isOpen) { - this.onOpenStart.emit(); - } else { - this.onCloseStart.emit(); + if (!this._isAnimating) { + this._opened = isOpen; + this._currentTogglePromise = new Promise(resolve => { + first.call(isOpen ? this.onOpen : this.onClose).subscribe(resolve); + }); + + if (this._focusTrap) { + this._focusTrap.enabled = this.isFocusTrapEnabled; + } } - if (this._toggleAnimationPromise && this._resolveToggleAnimationPromise) { - this._resolveToggleAnimationPromise(false); - } - this._toggleAnimationPromise = new Promise(resolve => { - this._resolveToggleAnimationPromise = animationFinished => - resolve(new MdSidenavToggleResult(isOpen ? 'open' : 'close', animationFinished)); - }); - return this._toggleAnimationPromise; + // TODO(crisbeto): This promise is here backwards-compatibility. + // It should be removed next time we do breaking changes in the sidenav. + return this._currentTogglePromise!; } /** @@ -260,60 +247,37 @@ export class MdSidenav implements AfterContentInit, OnDestroy { } /** - * 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. + * Figures out the state of the sidenav animation. */ - _onTransitionEnd(transitionEvent: TransitionEvent) { - if (transitionEvent.target == this._elementRef.nativeElement - // Simpler version to check for prefixes. - && transitionEvent.propertyName.endsWith('transform')) { - if (this._opened) { - this.onOpen.emit(); - } else { - this.onClose.emit(); - } - - if (this._toggleAnimationPromise && this._resolveToggleAnimationPromise) { - this._resolveToggleAnimationPromise(true); - this._toggleAnimationPromise = this._resolveToggleAnimationPromise = null; - } + _getAnimationState(): 'open-instant' | 'open' | 'void' { + if (this.opened) { + return this._enableAnimations ? 'open' : 'open-instant'; } - } - get _isClosing() { - return !this._opened && !!this._toggleAnimationPromise; - } - get _isOpening() { - return this._opened && !!this._toggleAnimationPromise; - } - get _isClosed() { - return !this._opened && !this._toggleAnimationPromise; + return 'void'; } - get _isOpened() { - return this._opened && !this._toggleAnimationPromise; - } - get _isEnd() { - return this.align == 'end'; - } - get _modeSide() { - return this.mode == 'side'; - } - get _modeOver() { - return this.mode == 'over'; - } - get _modePush() { - return this.mode == 'push'; + + _onAnimationStart() { + this._isAnimating = true; + this._animationStarted.emit(); } - get _width() { - if (this._elementRef.nativeElement) { - return this._elementRef.nativeElement.offsetWidth; + _onAnimationEnd(event: AnimationEvent) { + const {fromState, toState} = event; + + if (toState === 'open' && fromState === 'void') { + this.onOpen.emit(new MdSidenavToggleResult('open', true)); + } else if (toState === 'void' && fromState === 'open') { + this.onClose.emit(new MdSidenavToggleResult('close', true)); } - return 0; + + this._isAnimating = false; + this._currentTogglePromise = null; } - private _elementFocusedBeforeSidenavWasOpened: HTMLElement | null = null; + get _width() { + return this._elementRef.nativeElement ? (this._elementRef.nativeElement.offsetWidth || 0) : 0; + } } /** @@ -382,17 +346,13 @@ export class MdSidenavContainer implements AfterContentInit { } /** Calls `open` of both start and end sidenavs */ - public open() { - return Promise.all([this._start, this._end] - .filter(sidenav => sidenav) - .map(sidenav => sidenav!.open())); + open(): void { + this._sidenavs.forEach(sidenav => sidenav.open()); } /** Calls `close` of both start and end sidenavs */ - public close() { - return Promise.all([this._start, this._end] - .filter(sidenav => sidenav) - .map(sidenav => sidenav!.close())); + close(): void { + this._sidenavs.forEach(sidenav => sidenav.close()); } /** @@ -401,7 +361,7 @@ export class MdSidenavContainer implements AfterContentInit { * is properly hidden. */ private _watchSidenavToggle(sidenav: MdSidenav): void { - merge(sidenav.onOpenStart, sidenav.onCloseStart).subscribe(() => { + sidenav._animationStarted.subscribe(() => { // Set the transition class on the container so that the animations occur. This should not // be set initially because animations should only be triggered via a change in state. this._renderer.addClass(this._element.nativeElement, 'mat-sidenav-transition'); @@ -442,9 +402,7 @@ export class MdSidenavContainer implements AfterContentInit { this._start = this._end = null; // Ensure that we have at most one start and one end 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()) { + this._sidenavs.forEach(sidenav => { if (sidenav.align == 'end') { if (this._end != null) { throwMdDuplicatedSidenavError('end'); @@ -456,7 +414,7 @@ export class MdSidenavContainer implements AfterContentInit { } this._start = sidenav; } - } + }); this._right = this._left = null;