From 372436cb8a4505f193cd0ad6c459b0733dc926ca Mon Sep 17 00:00:00 2001 From: mmalerba Date: Thu, 17 Aug 2017 12:48:25 -0700 Subject: [PATCH] refactor(sidenav): split into drawer and sidenav (#6260) * refactor(sidenav): split into drawer and sidenav * s/side/position * fix tests * fix bad rebase --- src/demo-app/sidenav/sidenav-demo.html | 10 +- src/lib/sidenav/README.md | 4 +- src/lib/sidenav/_sidenav-theme.scss | 24 +- src/lib/sidenav/drawer-container.html | 8 + ...ansitions.scss => drawer-transitions.scss} | 6 +- src/lib/sidenav/{sidenav.html => drawer.html} | 0 src/lib/sidenav/{sidenav.scss => drawer.scss} | 38 +- .../{sidenav.spec.ts => drawer.spec.ts} | 262 ++++----- src/lib/sidenav/drawer.ts | 495 ++++++++++++++++++ src/lib/sidenav/index.ts | 6 +- src/lib/sidenav/sidenav-container.html | 8 - src/lib/sidenav/sidenav.md | 2 +- src/lib/sidenav/sidenav.ts | 472 +---------------- 13 files changed, 708 insertions(+), 627 deletions(-) create mode 100644 src/lib/sidenav/drawer-container.html rename src/lib/sidenav/{sidenav-transitions.scss => drawer-transitions.scss} (80%) rename src/lib/sidenav/{sidenav.html => drawer.html} (100%) rename src/lib/sidenav/{sidenav.scss => drawer.scss} (75%) rename src/lib/sidenav/{sidenav.spec.ts => drawer.spec.ts} (54%) create mode 100644 src/lib/sidenav/drawer.ts delete mode 100644 src/lib/sidenav/sidenav-container.html diff --git a/src/demo-app/sidenav/sidenav-demo.html b/src/demo-app/sidenav/sidenav-demo.html index 10da18a0fa3..bdddd69c381 100644 --- a/src/demo-app/sidenav/sidenav-demo.html +++ b/src/demo-app/sidenav/sidenav-demo.html @@ -15,7 +15,7 @@ - + End Side Drawer
@@ -48,14 +48,14 @@ -

Dynamic Alignment Sidenav

+

Dynamic Position Sidenav

- Start - End + Start + End
- diff --git a/src/lib/sidenav/README.md b/src/lib/sidenav/README.md index 850d6328a92..6c4e057b526 100644 --- a/src/lib/sidenav/README.md +++ b/src/lib/sidenav/README.md @@ -23,7 +23,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. If there is more than 1 sidenav on either side the container will be considered invalid and none of the sidenavs will be visible or toggleable until the container is valid again. | +| `position` | `"start"\|"end"` | The position 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 container will be considered invalid and none of the sidenavs will be visible or toggleable until the container 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. | @@ -68,7 +68,7 @@ Here's a simple example of using the sidenav:
- + End Sidenav. diff --git a/src/lib/sidenav/_sidenav-theme.scss b/src/lib/sidenav/_sidenav-theme.scss index 4955c78f194..380604bb0ae 100644 --- a/src/lib/sidenav/_sidenav-theme.scss +++ b/src/lib/sidenav/_sidenav-theme.scss @@ -12,27 +12,27 @@ // 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. - $sidenav-backdrop-color: invert(mat-color($background, card, 0.6)) !default; - $sidenav-background-color: mat-color($background, dialog) !default; - $sidenav-container-background-color: mat-color($background, background) !default; - $sidenav-push-background-color: mat-color($background, dialog) !default; + $drawer-backdrop-color: invert(mat-color($background, card, 0.6)) !default; + $drawer-background-color: mat-color($background, dialog) !default; + $drawer-container-background-color: mat-color($background, background) !default; + $drawer-push-background-color: mat-color($background, dialog) !default; - .mat-sidenav-container { - background-color: $sidenav-container-background-color; + .mat-drawer-container { + background-color: $drawer-container-background-color; color: mat-color($foreground, text); } - .mat-sidenav { - background-color: $sidenav-background-color; + .mat-drawer { + background-color: $drawer-background-color; color: mat-color($foreground, text); - &.mat-sidenav-push { - background-color: $sidenav-push-background-color; + &.mat-drawer-push { + background-color: $drawer-push-background-color; } } - .mat-sidenav-backdrop.mat-sidenav-shown { - background-color: $sidenav-backdrop-color; + .mat-drawer-backdrop.mat-drawer-shown { + background-color: $drawer-backdrop-color; } } diff --git a/src/lib/sidenav/drawer-container.html b/src/lib/sidenav/drawer-container.html new file mode 100644 index 00000000000..f305c0545a8 --- /dev/null +++ b/src/lib/sidenav/drawer-container.html @@ -0,0 +1,8 @@ +
+ + + +
+ +
diff --git a/src/lib/sidenav/sidenav-transitions.scss b/src/lib/sidenav/drawer-transitions.scss similarity index 80% rename from src/lib/sidenav/sidenav-transitions.scss rename to src/lib/sidenav/drawer-transitions.scss index cbbbd9e0c47..f14d83784f2 100644 --- a/src/lib/sidenav/sidenav-transitions.scss +++ b/src/lib/sidenav/drawer-transitions.scss @@ -1,8 +1,8 @@ // Separate transitions to be able to disable them in unit tests by omitting this file. @import '../core/style/variables'; -.mat-sidenav-transition { - .mat-sidenav-content { +.mat-drawer-transition { + .mat-drawer-content { transition: { duration: $swift-ease-out-duration; timing-function: $swift-ease-out-timing-function; @@ -10,7 +10,7 @@ } } - .mat-sidenav-backdrop.mat-sidenav-shown { + .mat-drawer-backdrop.mat-drawer-shown { transition: background-color $swift-ease-out-duration $swift-ease-out-timing-function; } } diff --git a/src/lib/sidenav/sidenav.html b/src/lib/sidenav/drawer.html similarity index 100% rename from src/lib/sidenav/sidenav.html rename to src/lib/sidenav/drawer.html diff --git a/src/lib/sidenav/sidenav.scss b/src/lib/sidenav/drawer.scss similarity index 75% rename from src/lib/sidenav/sidenav.scss rename to src/lib/sidenav/drawer.scss index 23d300baed5..26800af1f25 100644 --- a/src/lib/sidenav/sidenav.scss +++ b/src/lib/sidenav/drawer.scss @@ -7,19 +7,19 @@ // 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 // stylelint-enable -@mixin mat-sidenav-stacking-context() { +@mixin mat-drawer-stacking-context() { position: relative; // Use a transform to create a new stacking context. transform: translate3d(0, 0, 0); } -.mat-sidenav-container { +.mat-drawer-container { // We need a stacking context here so that the backdrop and drawers are clipped to the - // MdSidenavContainer. This creates a new z-index stack so we use low numbered z-indices. - // We create another stacking context in the '.mat-sidenav-content' and in each sidenav so that + // MdDrawerContainer. This creates a new z-index stack so we use low numbered z-indices. + // We create another stacking context in the '.mat-drawer-content' and in each drawer so that // the application content does not get messed up with our own CSS. - @include mat-sidenav-stacking-context(); + @include mat-drawer-stacking-context(); box-sizing: border-box; -webkit-overflow-scrolling: touch; @@ -27,20 +27,20 @@ // Need this to take up space in the layout. display: block; - // Hide the sidenavs when they're closed. + // Hide the drawers when they're closed. overflow: hidden; // TODO(hansl): Update this with a more robust solution. &[fullscreen] { @include mat-fill(); - &.mat-sidenav-opened { + &.mat-drawer-opened { overflow: hidden; } } } -.mat-sidenav-backdrop { +.mat-drawer-backdrop { @include mat-fill(); display: block; @@ -55,7 +55,7 @@ // section. visibility: hidden; - &.mat-sidenav-shown { + &.mat-drawer-shown { visibility: visible; } @@ -64,16 +64,16 @@ } } -.mat-sidenav-content { - @include mat-sidenav-stacking-context(); +.mat-drawer-content { + @include mat-drawer-stacking-context(); display: block; height: 100%; overflow: auto; } -.mat-sidenav { - @include mat-sidenav-stacking-context(); +.mat-drawer { + @include mat-drawer-stacking-context(); display: block; position: absolute; @@ -84,14 +84,14 @@ outline: 0; box-sizing: border-box; height: 100%; - overflow-y: auto; // TODO(kara): revisit scrolling behavior for sidenavs + overflow-y: auto; // TODO(kara): revisit scrolling behavior for drawers transform: translate3d(-100%, 0, 0); - &.mat-sidenav-side { + &.mat-drawer-side { z-index: 1; } - &.mat-sidenav-end { + &.mat-drawer-end { right: 0; transform: translate3d(100%, 0, 0); } @@ -99,15 +99,15 @@ [dir='rtl'] & { transform: translate3d(100%, 0, 0); - &.mat-sidenav-end { + &.mat-drawer-end { left: 0; right: auto; transform: translate3d(-100%, 0, 0); } } - &.mat-sidenav-opening, &.mat-sidenav-opened { - &:not(.mat-sidenav-side) { + &.mat-drawer-opening, &.mat-drawer-opened { + &:not(.mat-drawer-side) { // The elevation of z-16 is noted in the design specifications. // See https://material.io/guidelines/patterns/navigation-drawer.html# @include mat-elevation(16); diff --git a/src/lib/sidenav/sidenav.spec.ts b/src/lib/sidenav/drawer.spec.ts similarity index 54% rename from src/lib/sidenav/sidenav.spec.ts rename to src/lib/sidenav/drawer.spec.ts index b0c3d316a19..6d0a5171a06 100644 --- a/src/lib/sidenav/sidenav.spec.ts +++ b/src/lib/sidenav/drawer.spec.ts @@ -2,24 +2,24 @@ import {fakeAsync, async, tick, ComponentFixture, TestBed} from '@angular/core/t import {Component, ElementRef, ViewChild} from '@angular/core'; import {By} from '@angular/platform-browser'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; +import {MdDrawer, MdSidenavModule, MdDrawerContainer} from './index'; import {A11yModule} from '@angular/cdk/a11y'; import {PlatformModule} from '@angular/cdk/platform'; import {ESCAPE} from '@angular/cdk/keycodes'; import {dispatchKeyboardEvent} from '@angular/cdk/testing'; -import {MdSidenav, MdSidenavModule, MdSidenavContainer} from './index'; -describe('MdSidenav', () => { +describe('MdDrawer', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [MdSidenavModule, A11yModule, PlatformModule, NoopAnimationsModule], declarations: [ BasicTestApp, - SidenavContainerNoSidenavTestApp, - SidenavSetToOpenedFalse, - SidenavSetToOpenedTrue, - SidenavDynamicAlign, - SidenavWitFocusableElements, + DrawerContainerNoDrawerTestApp, + DrawerSetToOpenedFalse, + DrawerSetToOpenedTrue, + DrawerDynamicPosition, + DrawerWitFocusableElements, ], }); @@ -33,8 +33,8 @@ describe('MdSidenav', () => { fixture.detectChanges(); let testComponent: BasicTestApp = fixture.debugElement.componentInstance; - let sidenav = fixture.debugElement.query(By.directive(MdSidenav)); - let sidenavBackdropElement = fixture.debugElement.query(By.css('.mat-sidenav-backdrop')); + let drawer = fixture.debugElement.query(By.directive(MdDrawer)); + let drawerBackdropElement = fixture.debugElement.query(By.css('.mat-drawer-backdrop')); fixture.debugElement.query(By.css('.open')).nativeElement.click(); fixture.detectChanges(); @@ -45,11 +45,11 @@ describe('MdSidenav', () => { tick(); fixture.detectChanges(); - expect(sidenav.componentInstance._isAnimating).toBe(false); + expect(drawer.componentInstance._isAnimating).toBe(false); expect(testComponent.openCount).toBe(1); expect(testComponent.closeCount).toBe(0); - expect(getComputedStyle(sidenav.nativeElement).visibility).toBe('visible'); - expect(getComputedStyle(sidenavBackdropElement.nativeElement).visibility).toBe('visible'); + expect(getComputedStyle(drawer.nativeElement).visibility).toBe('visible'); + expect(getComputedStyle(drawerBackdropElement.nativeElement).visibility).toBe('visible'); fixture.debugElement.query(By.css('.close')).nativeElement.click(); fixture.detectChanges(); @@ -62,11 +62,11 @@ describe('MdSidenav', () => { expect(testComponent.openCount).toBe(1); expect(testComponent.closeCount).toBe(1); - expect(getComputedStyle(sidenav.nativeElement).visibility).toBe('hidden'); - expect(getComputedStyle(sidenavBackdropElement.nativeElement).visibility).toBe('hidden'); + expect(getComputedStyle(drawer.nativeElement).visibility).toBe('hidden'); + expect(getComputedStyle(drawerBackdropElement.nativeElement).visibility).toBe('hidden'); })); - it('does not throw when created without a sidenav', fakeAsync(() => { + it('does not throw when created without a drawer', fakeAsync(() => { expect(() => { let fixture = TestBed.createComponent(BasicTestApp); fixture.detectChanges(); @@ -85,7 +85,7 @@ describe('MdSidenav', () => { expect(testComponent.backdropClickedCount).toBe(0); - fixture.debugElement.query(By.css('.mat-sidenav-backdrop')).nativeElement.click(); + fixture.debugElement.query(By.css('.mat-drawer-backdrop')).nativeElement.click(); fixture.detectChanges(); tick(); @@ -108,16 +108,16 @@ describe('MdSidenav', () => { fixture.detectChanges(); let testComponent: BasicTestApp = fixture.debugElement.componentInstance; - let sidenav = fixture.debugElement.query(By.directive(MdSidenav)); + let drawer = fixture.debugElement.query(By.directive(MdDrawer)); - sidenav.componentInstance.open(); + drawer.componentInstance.open(); fixture.detectChanges(); tick(); expect(testComponent.openCount).toBe(1, 'Expected one open event.'); expect(testComponent.closeCount).toBe(0, 'Expected no close events.'); - dispatchKeyboardEvent(sidenav.nativeElement, 'keydown', ESCAPE); + dispatchKeyboardEvent(drawer.nativeElement, 'keydown', ESCAPE); fixture.detectChanges(); tick(); @@ -127,14 +127,14 @@ describe('MdSidenav', () => { 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)); + let drawer = fixture.debugElement.query(By.directive(MdDrawer)); - sidenav.componentInstance.disableClose = true; - sidenav.componentInstance.open(); + drawer.componentInstance.disableClose = true; + drawer.componentInstance.open(); fixture.detectChanges(); tick(); - dispatchKeyboardEvent(sidenav.nativeElement, 'keydown', ESCAPE); + dispatchKeyboardEvent(drawer.nativeElement, 'keydown', ESCAPE); fixture.detectChanges(); tick(); @@ -144,36 +144,36 @@ describe('MdSidenav', () => { it('should not close by clicking on the backdrop when disableClose is set', fakeAsync(() => { let fixture = TestBed.createComponent(BasicTestApp); let testComponent = fixture.debugElement.componentInstance; - let sidenav = fixture.debugElement.query(By.directive(MdSidenav)).componentInstance; + let drawer = fixture.debugElement.query(By.directive(MdDrawer)).componentInstance; - sidenav.disableClose = true; - sidenav.open(); + drawer.disableClose = true; + drawer.open(); fixture.detectChanges(); tick(); - fixture.debugElement.query(By.css('.mat-sidenav-backdrop')).nativeElement.click(); + fixture.debugElement.query(By.css('.mat-drawer-backdrop')).nativeElement.click(); fixture.detectChanges(); tick(); expect(testComponent.closeCount).toBe(0); })); - it('should restore focus on close if focus is inside sidenav', fakeAsync(() => { + it('should restore focus on close if focus is inside drawer', fakeAsync(() => { let fixture = TestBed.createComponent(BasicTestApp); fixture.detectChanges(); - let sidenav = fixture.debugElement.query(By.directive(MdSidenav)).componentInstance; + let drawer = fixture.debugElement.query(By.directive(MdDrawer)).componentInstance; let openButton = fixture.componentInstance.openButton.nativeElement; - let sidenavButton = fixture.componentInstance.sidenavButton.nativeElement; + let drawerButton = fixture.componentInstance.drawerButton.nativeElement; openButton.focus(); - sidenav.open(); + drawer.open(); fixture.detectChanges(); tick(); - sidenavButton.focus(); + drawerButton.focus(); - sidenav.close(); + drawer.close(); fixture.detectChanges(); tick(); @@ -181,21 +181,21 @@ describe('MdSidenav', () => { .toBe(openButton, 'Expected focus to be restored to the open button on close.'); })); - it('should not restore focus on close if focus is outside sidenav', fakeAsync(() => { + it('should not restore focus on close if focus is outside drawer', fakeAsync(() => { let fixture = TestBed.createComponent(BasicTestApp); - let sidenav: MdSidenav = fixture.debugElement - .query(By.directive(MdSidenav)).componentInstance; + let drawer: MdDrawer = fixture.debugElement + .query(By.directive(MdDrawer)).componentInstance; let openButton = fixture.componentInstance.openButton.nativeElement; let closeButton = fixture.componentInstance.closeButton.nativeElement; openButton.focus(); - sidenav.open(); + drawer.open(); fixture.detectChanges(); tick(); closeButton.focus(); - sidenav.close(); + drawer.close(); fixture.detectChanges(); tick(); @@ -206,68 +206,68 @@ describe('MdSidenav', () => { describe('attributes', () => { it('should correctly parse opened="false"', () => { - let fixture = TestBed.createComponent(SidenavSetToOpenedFalse); + let fixture = TestBed.createComponent(DrawerSetToOpenedFalse); fixture.detectChanges(); - let sidenav = fixture.debugElement.query(By.directive(MdSidenav)).componentInstance; + let drawer = fixture.debugElement.query(By.directive(MdDrawer)).componentInstance; - expect((sidenav as MdSidenav).opened).toBe(false); + expect((drawer as MdDrawer).opened).toBe(false); }); it('should correctly parse opened="true"', () => { - let fixture = TestBed.createComponent(SidenavSetToOpenedTrue); + let fixture = TestBed.createComponent(DrawerSetToOpenedTrue); fixture.detectChanges(); - let sidenav = fixture.debugElement.query(By.directive(MdSidenav)).componentInstance; + let drawer = fixture.debugElement.query(By.directive(MdDrawer)).componentInstance; - expect((sidenav as MdSidenav).opened).toBe(true); + expect((drawer as MdDrawer).opened).toBe(true); }); it('should remove align attr from DOM', () => { const fixture = TestBed.createComponent(BasicTestApp); fixture.detectChanges(); - const sidenavEl = fixture.debugElement.query(By.css('md-sidenav')).nativeElement; - expect(sidenavEl.hasAttribute('align')) - .toBe(false, 'Expected sidenav not to have a native align attribute.'); + const drawerEl = fixture.debugElement.query(By.css('md-drawer')).nativeElement; + expect(drawerEl.hasAttribute('align')) + .toBe(false, 'Expected drawer not to have a native align attribute.'); }); - it('should throw when multiple sidenavs have the same align', fakeAsync(() => { - const fixture = TestBed.createComponent(SidenavDynamicAlign); + it('should throw when multiple drawers have the same position', fakeAsync(() => { + const fixture = TestBed.createComponent(DrawerDynamicPosition); fixture.detectChanges(); tick(); - const testComponent: SidenavDynamicAlign = fixture.debugElement.componentInstance; - testComponent.sidenav1Align = 'end'; + const testComponent: DrawerDynamicPosition = fixture.debugElement.componentInstance; + testComponent.drawer1Position = 'end'; expect(() => fixture.detectChanges()).toThrow(); })); - it('should not throw when sidenavs swap sides', () => { - const fixture = TestBed.createComponent(SidenavDynamicAlign); + it('should not throw when drawers swap positions', () => { + const fixture = TestBed.createComponent(DrawerDynamicPosition); fixture.detectChanges(); - const testComponent: SidenavDynamicAlign = fixture.debugElement.componentInstance; - testComponent.sidenav1Align = 'end'; - testComponent.sidenav2Align = 'start'; + const testComponent: DrawerDynamicPosition = fixture.debugElement.componentInstance; + testComponent.drawer1Position = 'end'; + testComponent.drawer2Position = 'start'; expect(() => fixture.detectChanges()).not.toThrow(); }); }); describe('focus trapping behavior', () => { - let fixture: ComponentFixture; - let testComponent: SidenavWitFocusableElements; - let sidenav: MdSidenav; + let fixture: ComponentFixture; + let testComponent: DrawerWitFocusableElements; + let drawer: MdDrawer; let firstFocusableElement: HTMLElement; let lastFocusableElement: HTMLElement; beforeEach(() => { - fixture = TestBed.createComponent(SidenavWitFocusableElements); + fixture = TestBed.createComponent(DrawerWitFocusableElements); testComponent = fixture.debugElement.componentInstance; - sidenav = fixture.debugElement.query(By.directive(MdSidenav)).componentInstance; + drawer = fixture.debugElement.query(By.directive(MdDrawer)).componentInstance; firstFocusableElement = fixture.debugElement.query(By.css('.link1')).nativeElement; lastFocusableElement = fixture.debugElement.query(By.css('.link1')).nativeElement; lastFocusableElement.focus(); @@ -277,7 +277,7 @@ describe('MdSidenav', () => { testComponent.mode = 'over'; lastFocusableElement.focus(); - sidenav.open(); + drawer.open(); fixture.detectChanges(); tick(); @@ -288,7 +288,7 @@ describe('MdSidenav', () => { testComponent.mode = 'push'; lastFocusableElement.focus(); - sidenav.open(); + drawer.open(); fixture.detectChanges(); tick(); @@ -299,7 +299,7 @@ describe('MdSidenav', () => { testComponent.mode = 'side'; lastFocusableElement.focus(); - sidenav.open(); + drawer.open(); fixture.detectChanges(); tick(); @@ -308,53 +308,53 @@ describe('MdSidenav', () => { }); }); -describe('MdSidenavContainer', () => { +describe('MdDrawerContainer', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [MdSidenavModule, A11yModule, PlatformModule, NoopAnimationsModule], - declarations: [SidenavContainerTwoSidenavTestApp, SidenavDelayed], + declarations: [DrawerContainerTwoDrawerTestApp, DrawerDelayed], }); TestBed.compileComponents(); })); - it('should be able to open and close all sidenavs', fakeAsync(() => { - const fixture = TestBed.createComponent(SidenavContainerTwoSidenavTestApp); + it('should be able to open and close all drawers', fakeAsync(() => { + const fixture = TestBed.createComponent(DrawerContainerTwoDrawerTestApp); fixture.detectChanges(); - const testComponent: SidenavContainerTwoSidenavTestApp = + const testComponent: DrawerContainerTwoDrawerTestApp = fixture.debugElement.componentInstance; - const sidenavs = fixture.debugElement.queryAll(By.directive(MdSidenav)); + const drawers = fixture.debugElement.queryAll(By.directive(MdDrawer)); - expect(sidenavs.every(sidenav => sidenav.componentInstance.opened)).toBe(false); + expect(drawers.every(drawer => drawer.componentInstance.opened)).toBe(false); - testComponent.sidenavContainer.open(); + testComponent.drawerContainer.open(); fixture.detectChanges(); tick(); - expect(sidenavs.every(sidenav => sidenav.componentInstance.opened)).toBe(true); + expect(drawers.every(drawer => drawer.componentInstance.opened)).toBe(true); - testComponent.sidenavContainer.close(); + testComponent.drawerContainer.close(); fixture.detectChanges(); tick(); - expect(sidenavs.every(sidenav => sidenav.componentInstance.opened)).toBe(false); + expect(drawers.every(drawer => drawer.componentInstance.opened)).toBe(false); })); - it('should animate the content when a sidenav is added at a later point', fakeAsync(() => { - const fixture = TestBed.createComponent(SidenavDelayed); + it('should animate the content when a drawer is added at a later point', fakeAsync(() => { + const fixture = TestBed.createComponent(DrawerDelayed); fixture.detectChanges(); - const contentElement = fixture.debugElement.nativeElement.querySelector('.mat-sidenav-content'); + const contentElement = fixture.debugElement.nativeElement.querySelector('.mat-drawer-content'); expect(parseInt(contentElement.style.marginLeft)).toBeFalsy(); - fixture.componentInstance.showSidenav = true; + fixture.componentInstance.showDrawer = true; fixture.detectChanges(); - fixture.componentInstance.sidenav.open(); + fixture.componentInstance.drawer.open(); fixture.detectChanges(); tick(); fixture.detectChanges(); @@ -364,41 +364,41 @@ describe('MdSidenavContainer', () => { }); -/** Test component that contains an MdSidenavContainer but no MdSidenav. */ -@Component({template: ``}) -class SidenavContainerNoSidenavTestApp { } +/** Test component that contains an MdDrawerContainer but no MdDrawer. */ +@Component({template: ``}) +class DrawerContainerNoDrawerTestApp { } -/** Test component that contains an MdSidenavContainer and 2 MdSidenav on the same side. */ +/** Test component that contains an MdDrawerContainer and 2 MdDrawer in the same position. */ @Component({ template: ` - - - - `, + + + + `, }) -class SidenavContainerTwoSidenavTestApp { - @ViewChild(MdSidenavContainer) sidenavContainer: MdSidenavContainer; +class DrawerContainerTwoDrawerTestApp { + @ViewChild(MdDrawerContainer) drawerContainer: MdDrawerContainer; } -/** Test component that contains an MdSidenavContainer and one MdSidenav. */ +/** Test component that contains an MdDrawerContainer and one MdDrawer. */ @Component({ template: ` - - - - - - - `, + + + + + + + `, }) class BasicTestApp { openCount: number = 0; closeCount: number = 0; backdropClickedCount: number = 0; - @ViewChild('sidenavButton') sidenavButton: ElementRef; + @ViewChild('drawerButton') drawerButton: ElementRef; @ViewChild('openButton') openButton: ElementRef; @ViewChild('closeButton') closeButton: ElementRef; @@ -417,58 +417,58 @@ class BasicTestApp { @Component({ template: ` - - - Closed Sidenav. - - `, + + + Closed Drawer. + + `, }) -class SidenavSetToOpenedFalse { } +class DrawerSetToOpenedFalse { } @Component({ template: ` - - - Closed Sidenav. - - `, + + + Closed Drawer. + + `, }) -class SidenavSetToOpenedTrue { } +class DrawerSetToOpenedTrue { } @Component({ template: ` - - - - `, + + + + `, }) -class SidenavDynamicAlign { - sidenav1Align = 'start'; - sidenav2Align = 'end'; +class DrawerDynamicPosition { + drawer1Position = 'start'; + drawer2Position = 'end'; } @Component({ template: ` - - + + link1 - + link2 - `, + `, }) -class SidenavWitFocusableElements { +class DrawerWitFocusableElements { mode: string = 'over'; } @Component({ template: ` - - Sidenav - + + Drawer + `, }) -class SidenavDelayed { - @ViewChild(MdSidenav) sidenav: MdSidenav; - showSidenav = false; +class DrawerDelayed { + @ViewChild(MdDrawer) drawer: MdDrawer; + showDrawer = false; } diff --git a/src/lib/sidenav/drawer.ts b/src/lib/sidenav/drawer.ts new file mode 100644 index 00000000000..4a00b86a035 --- /dev/null +++ b/src/lib/sidenav/drawer.ts @@ -0,0 +1,495 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { + AfterContentInit, + Component, + ContentChildren, + ElementRef, + Input, + Optional, + Output, + QueryList, + ChangeDetectionStrategy, + EventEmitter, + Renderer2, + ViewEncapsulation, + NgZone, + OnDestroy, + 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, takeUntil, startWith} from '../core/rxjs/index'; +import {DOCUMENT} from '@angular/platform-browser'; +import {merge} from 'rxjs/observable/merge'; + + +/** Throws an exception when two MdDrawer are matching the same position. */ +export function throwMdDuplicatedDrawerError(position: string) { + throw Error(`A drawer was already declared for 'position="${position}"'`); +} + + +/** + * Drawer toggle promise result. + * @deprecated + */ +export class MdDrawerToggleResult { + constructor(public type: 'open' | 'close', public animationFinished: boolean) {} +} + +/** + * component. + * + * This component corresponds to a drawer that can be opened on the drawer container. + * + * Please refer to README.md for examples on how to use it. + */ +@Component({ + moduleId: module.id, + selector: 'md-drawer, mat-drawer', + templateUrl: 'drawer.html', + 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, open-instant => void', + animate('400ms cubic-bezier(0.25, 0.8, 0.25, 1)')) + ]) + ], + host: { + 'class': 'mat-drawer', + '[@transform]': '_animationState', + '(@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-drawer-end]': 'position === "end"', + '[class.mat-drawer-over]': 'mode === "over"', + '[class.mat-drawer-push]': 'mode === "push"', + '[class.mat-drawer-side]': 'mode === "side"', + 'tabIndex': '-1', + }, + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, +}) +export class MdDrawer implements AfterContentInit, OnDestroy { + private _focusTrap: FocusTrap; + private _elementFocusedBeforeDrawerWasOpened: HTMLElement | null = null; + + /** Whether the drawer is initialized. Used for disabling the initial animation. */ + private _enableAnimations = false; + + /** The side that the drawer is attached to. */ + @Input() + get position() { return this._position; } + set position(value) { + // Make sure we have a valid value. + value = value === 'end' ? 'end' : 'start'; + if (value != this._position) { + this._position = value; + this.onAlignChanged.emit(); + this.onPositionChanged.emit(); + } + } + private _position: 'start' | 'end' = 'start'; + + /** @deprecated */ + @Input() + get align() { return this.position; } + set align(value) { this.position = value; } + + /** Mode of the drawer; one of 'over', 'push' or 'side'. */ + @Input() mode: 'over' | 'push' | 'side' = 'over'; + + /** Whether the drawer can be closed with the escape key or not. */ + @Input() + get disableClose(): boolean { return this._disableClose; } + set disableClose(value: boolean) { this._disableClose = coerceBooleanProperty(value); } + private _disableClose: boolean = false; + + /** Whether the drawer is opened. */ + private _opened: boolean = false; + + /** Emits whenever the drawer has started animating. */ + _animationStarted = new EventEmitter(); + + /** Whether the drawer is animating. Used to prevent overlapping animations. */ + _isAnimating = false; + + /** Current state of the sidenav animation. */ + _animationState: 'open-instant' | 'open' | 'void' = 'void'; + + /** + * Promise that resolves when the open/close animation completes. It is here for backwards + * compatibility and should be removed next time we do drawer breaking changes. + * @deprecated + */ + private _currentTogglePromise: Promise | null; + + /** Event emitted when the drawer is fully opened. */ + @Output('open') onOpen = new EventEmitter(); + + /** Event emitted when the drawer is fully closed. */ + @Output('close') onClose = new EventEmitter(); + + /** Event emitted when the drawer's position changes. */ + @Output('positionChanged') onPositionChanged = new EventEmitter(); + + /** @deprecated */ + @Output('align-changed') onAlignChanged = new EventEmitter(); + + get isFocusTrapEnabled() { + // The focus trap is only enabled when the drawer is open in any mode other than side. + return this.opened && this.mode !== 'side'; + } + + constructor(private _elementRef: ElementRef, + private _focusTrapFactory: FocusTrapFactory, + @Optional() @Inject(DOCUMENT) private _doc: any) { + this.onOpen.subscribe(() => { + if (this._doc) { + this._elementFocusedBeforeDrawerWasOpened = this._doc.activeElement as HTMLElement; + } + + if (this.isFocusTrapEnabled && this._focusTrap) { + this._focusTrap.focusInitialElementWhenReady(); + } + }); + + this.onClose.subscribe(() => this._restoreFocus()); + } + + /** + * If focus is currently inside the drawer, restores it to where it was before the drawer + * opened. + */ + private _restoreFocus() { + let activeEl = this._doc && this._doc.activeElement; + if (activeEl && this._elementRef.nativeElement.contains(activeEl)) { + if (this._elementFocusedBeforeDrawerWasOpened instanceof HTMLElement) { + this._elementFocusedBeforeDrawerWasOpened.focus(); + } else { + this._elementRef.nativeElement.blur(); + } + } + + this._elementFocusedBeforeDrawerWasOpened = null; + } + + ngAfterContentInit() { + this._focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement); + this._focusTrap.enabled = this.isFocusTrapEnabled; + this._enableAnimations = true; + } + + ngOnDestroy() { + if (this._focusTrap) { + this._focusTrap.destroy(); + } + } + + /** + * Whether the drawer is opened. We overload this because we trigger an event when it + * starts or end. + */ + @Input() + get opened(): boolean { return this._opened; } + set opened(v: boolean) { + this.toggle(coerceBooleanProperty(v)); + } + + + /** Open the drawer. */ + open(): Promise { + return this.toggle(true); + } + + /** Close the drawer. */ + close(): Promise { + return this.toggle(false); + } + + /** + * Toggle this drawer. + * @param isOpen Whether the drawer should be open. + */ + toggle(isOpen: boolean = !this.opened): Promise { + if (!this._isAnimating) { + this._opened = isOpen; + + if (isOpen) { + this._animationState = this._enableAnimations ? 'open' : 'open-instant'; + } else { + this._animationState = 'void'; + } + + this._currentTogglePromise = new Promise(resolve => { + first.call(isOpen ? this.onOpen : this.onClose).subscribe(resolve); + }); + + if (this._focusTrap) { + this._focusTrap.enabled = this.isFocusTrapEnabled; + } + } + + // TODO(crisbeto): This promise is here for backwards-compatibility. + // It should be removed next time we do breaking changes in the drawer. + return this._currentTogglePromise!; + } + + /** + * Handles the keyboard events. + * @docs-private + */ + handleKeydown(event: KeyboardEvent) { + if (event.keyCode === ESCAPE && !this.disableClose) { + this.close(); + event.stopPropagation(); + } + } + + _onAnimationStart() { + this._isAnimating = true; + this._animationStarted.emit(); + } + + _onAnimationEnd(event: AnimationEvent) { + const {fromState, toState} = event; + + if (toState === 'open' && fromState === 'void') { + this.onOpen.emit(new MdDrawerToggleResult('open', true)); + } else if (toState === 'void' && fromState === 'open') { + this.onClose.emit(new MdDrawerToggleResult('close', true)); + } + + // Note: as of Angular 4.3, the animations module seems to fire the `start` callback before + // the end if animations are disabled. Make this call async to ensure that it still fires + // at the appropriate time. + Promise.resolve().then(() => { + this._isAnimating = false; + this._currentTogglePromise = null; + }); + } + + get _width() { + return this._elementRef.nativeElement ? (this._elementRef.nativeElement.offsetWidth || 0) : 0; + } +} + +/** + * component. + * + * This is the parent component to one or two s that validates the state internally + * and coordinates the backdrop and content styling. + */ +@Component({ + moduleId: module.id, + selector: 'md-drawer-container, mat-drawer-container', + templateUrl: 'drawer-container.html', + styleUrls: [ + 'drawer.css', + 'drawer-transitions.css', + ], + host: { + 'class': 'mat-drawer-container', + }, + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, +}) +export class MdDrawerContainer implements AfterContentInit { + @ContentChildren(MdDrawer) _drawers: QueryList; + + /** The drawer child with the `start` position. */ + get start() { return this._start; } + + /** The drawer child with the `end` position. */ + get end() { return this._end; } + + /** Event emitted when the drawer backdrop is clicked. */ + @Output() backdropClick = new EventEmitter(); + + /** The drawer at the start/end position, independent of direction. */ + private _start: MdDrawer | null; + private _end: MdDrawer | null; + + /** + * The drawer at the left/right. When direction changes, these will change as well. + * They're used as aliases for the above to set the left/right style properly. + * In LTR, _left == _start and _right == _end. + * In RTL, _left == _end and _right == _start. + */ + private _left: MdDrawer | null; + private _right: MdDrawer | null; + + /** Inline styles to be applied to the container. */ + _styles: { marginLeft: string; marginRight: string; transform: string; }; + + constructor(@Optional() private _dir: Directionality, private _element: ElementRef, + private _renderer: Renderer2, private _ngZone: NgZone, + private _changeDetectorRef: ChangeDetectorRef) { + // If a `Dir` directive exists up the tree, listen direction changes and update the left/right + // properties to point to the proper start/end. + if (_dir != null) { + _dir.change.subscribe(() => this._validateDrawers()); + } + } + + ngAfterContentInit() { + startWith.call(this._drawers.changes, null).subscribe(() => { + this._validateDrawers(); + this._drawers.forEach((drawer: MdDrawer) => { + this._watchDrawerToggle(drawer); + this._watchDrawerPosition(drawer); + }); + }); + } + + /** Calls `open` of both start and end drawers */ + open(): void { + this._drawers.forEach(drawer => drawer.open()); + } + + /** Calls `close` of both start and end drawers */ + close(): void { + this._drawers.forEach(drawer => drawer.close()); + } + + /** + * Subscribes to drawer events in order to set a class on the main container element when the + * drawer is open and the backdrop is visible. This ensures any overflow on the container element + * is properly hidden. + */ + private _watchDrawerToggle(drawer: MdDrawer): void { + takeUntil.call(drawer._animationStarted, this._drawers.changes).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-drawer-transition'); + this._updateStyles(); + this._changeDetectorRef.markForCheck(); + }); + + if (drawer.mode !== 'side') { + takeUntil.call(merge(drawer.onOpen, drawer.onClose), this._drawers.changes).subscribe(() => + this._setContainerClass(drawer.opened)); + } + } + + /** + * Subscribes to drawer onPositionChanged event in order to re-validate drawers when the position + * changes. + */ + private _watchDrawerPosition(drawer: MdDrawer): void { + if (!drawer) { + return; + } + // NOTE: We need to wait for the microtask queue to be empty before validating, + // since both drawers may be swapping positions at the same time. + takeUntil.call(drawer.onPositionChanged, this._drawers.changes).subscribe(() => + first.call(this._ngZone.onMicrotaskEmpty).subscribe(() => this._validateDrawers())); + } + + /** Toggles the 'mat-drawer-opened' class on the main 'md-drawer-container' element. */ + private _setContainerClass(isAdd: boolean): void { + if (isAdd) { + this._renderer.addClass(this._element.nativeElement, 'mat-drawer-opened'); + } else { + this._renderer.removeClass(this._element.nativeElement, 'mat-drawer-opened'); + } + } + + /** Validate the state of the drawer children components. */ + private _validateDrawers() { + this._start = this._end = null; + + // Ensure that we have at most one start and one end drawer. + this._drawers.forEach(drawer => { + if (drawer.position == 'end') { + if (this._end != null) { + throwMdDuplicatedDrawerError('end'); + } + this._end = drawer; + } else { + if (this._start != null) { + throwMdDuplicatedDrawerError('start'); + } + this._start = drawer; + } + }); + + this._right = this._left = null; + + // Detect if we're LTR or RTL. + if (this._dir == null || this._dir.value == 'ltr') { + this._left = this._start; + this._right = this._end; + } else { + this._left = this._end; + this._right = this._start; + } + } + + _onBackdropClicked() { + this.backdropClick.emit(); + this._closeModalDrawer(); + } + + _closeModalDrawer() { + // Close all open drawers where closing is not disabled and the mode is not `side`. + [this._start, this._end] + .filter(drawer => drawer && !drawer.disableClose && drawer.mode !== 'side') + .forEach(drawer => drawer!.close()); + } + + _isShowingBackdrop(): boolean { + return (this._isDrawerOpen(this._start) && this._start!.mode != 'side') + || (this._isDrawerOpen(this._end) && this._end!.mode != 'side'); + } + + private _isDrawerOpen(drawer: MdDrawer | null): boolean { + return drawer != null && drawer.opened; + } + + /** + * Return the width of the drawer, if it's in the proper mode and opened. + * This may relayout the view, so do not call this often. + * @param drawer + * @param mode + */ + private _getDrawerEffectiveWidth(drawer: MdDrawer, mode: string): number { + return (this._isDrawerOpen(drawer) && drawer.mode == mode) ? drawer._width : 0; + } + + /** + * Recalculates and updates the inline styles. Note that this + * should be used sparingly, because it causes a reflow. + */ + private _updateStyles() { + const marginLeft = this._left ? this._getDrawerEffectiveWidth(this._left, 'side') : 0; + const marginRight = this._right ? this._getDrawerEffectiveWidth(this._right, 'side') : 0; + const leftWidth = this._left ? this._getDrawerEffectiveWidth(this._left, 'push') : 0; + const rightWidth = this._right ? this._getDrawerEffectiveWidth(this._right, 'push') : 0; + + this._styles = { + marginLeft: `${marginLeft}px`, + marginRight: `${marginRight}px`, + transform: `translate3d(${leftWidth - rightWidth}px, 0, 0)` + }; + } +} diff --git a/src/lib/sidenav/index.ts b/src/lib/sidenav/index.ts index c0f6e8351a5..dca718a9fca 100644 --- a/src/lib/sidenav/index.ts +++ b/src/lib/sidenav/index.ts @@ -11,15 +11,17 @@ import {CommonModule} from '@angular/common'; import {A11yModule} from '@angular/cdk/a11y'; import {OverlayModule} from '@angular/cdk/overlay'; import {MdCommonModule} from '../core'; +import {MdDrawer, MdDrawerContainer} from './drawer'; import {MdSidenav, MdSidenavContainer} from './sidenav'; @NgModule({ imports: [CommonModule, MdCommonModule, A11yModule, OverlayModule], - exports: [MdSidenavContainer, MdSidenav, MdCommonModule], - declarations: [MdSidenavContainer, MdSidenav], + exports: [MdDrawerContainer, MdDrawer, MdSidenavContainer, MdSidenav, MdCommonModule], + declarations: [MdDrawerContainer, MdDrawer, MdSidenavContainer, MdSidenav], }) export class MdSidenavModule {} +export * from './drawer'; export * from './sidenav'; diff --git a/src/lib/sidenav/sidenav-container.html b/src/lib/sidenav/sidenav-container.html deleted file mode 100644 index 11c19f8c0c2..00000000000 --- a/src/lib/sidenav/sidenav-container.html +++ /dev/null @@ -1,8 +0,0 @@ -
- - - -
- -
diff --git a/src/lib/sidenav/sidenav.md b/src/lib/sidenav/sidenav.md index 4c29b328132..e58f54c3fae 100644 --- a/src/lib/sidenav/sidenav.md +++ b/src/lib/sidenav/sidenav.md @@ -31,7 +31,7 @@ Using the `side` mode on mobile devices can affect the performance and is also n [Material Design specification](https://material.io/guidelines/patterns/navigation-drawer.html#navigation-drawer-behavior). ### Positioning the sidenav -The `align` property determines whether the sidenav appears at the `"start"` or `"end"` of the +The `position` property determines whether the sidenav appears at the `"start"` or `"end"` of the container. This is affected by the current text direction ("ltr" or "rtl"). By default, the sidenav appears at the start of the container. diff --git a/src/lib/sidenav/sidenav.ts b/src/lib/sidenav/sidenav.ts index b2b995674b0..05f65c0e3af 100644 --- a/src/lib/sidenav/sidenav.ts +++ b/src/lib/sidenav/sidenav.ts @@ -7,60 +7,19 @@ */ import { - AfterContentInit, + ChangeDetectionStrategy, Component, ContentChildren, - ElementRef, - Input, - Optional, - Output, - QueryList, - ChangeDetectionStrategy, - EventEmitter, - Renderer2, - ViewEncapsulation, - NgZone, - OnDestroy, - Inject, - ChangeDetectorRef, + ViewEncapsulation } from '@angular/core'; -import {animate, state, style, transition, trigger, AnimationEvent} from '@angular/animations'; -import {coerceBooleanProperty} from '@angular/cdk/coercion'; -import {Directionality} from '@angular/cdk/bidi'; -import {ESCAPE} from '@angular/cdk/keycodes'; -import {first, takeUntil, startWith} from '@angular/cdk/rxjs'; -import {FocusTrapFactory, FocusTrap} from '@angular/cdk/a11y'; -import {DOCUMENT} from '@angular/platform-browser'; -import {merge} from 'rxjs/observable/merge'; +import {MdDrawer, MdDrawerContainer} from './drawer'; +import {animate, state, style, transition, trigger} from '@angular/animations'; -/** Throws an exception when two MdSidenav are matching the same side. */ -export function throwMdDuplicatedSidenavError(align: string) { - throw Error(`A sidenav was already declared for 'align="${align}"'`); -} - - -/** - * Sidenav toggle promise result. - * @deprecated - */ -export class MdSidenavToggleResult { - constructor(public type: 'open' | 'close', public animationFinished: boolean) {} -} - -/** - * component. - * - * This component corresponds to the drawer of the sidenav. - * - * Please refer to README.md for examples on how to use it. - */ @Component({ moduleId: module.id, selector: 'md-sidenav, mat-sidenav', - templateUrl: 'sidenav.html', - changeDetection: ChangeDetectionStrategy.OnPush, - encapsulation: ViewEncapsulation.None, + templateUrl: 'drawer.html', animations: [ trigger('transform', [ state('open, open-instant', style({ @@ -76,414 +35,39 @@ export class MdSidenavToggleResult { ]) ], host: { - 'class': 'mat-sidenav', + 'class': 'mat-drawer mat-sidenav', '[@transform]': '_animationState', '(@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-end]': 'align === "end"', - '[class.mat-sidenav-over]': 'mode === "over"', - '[class.mat-sidenav-push]': 'mode === "push"', - '[class.mat-sidenav-side]': 'mode === "side"', - 'tabIndex': '-1' - }, -}) -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'; - - /** Direction which the sidenav is aligned in. */ - @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; one of 'over', 'push' or 'side'. */ - @Input() mode: 'over' | 'push' | 'side' = 'over'; - - /** Whether the sidenav can be closed with the escape key or not. */ - @Input() - get disableClose(): boolean { return this._disableClose; } - set disableClose(value: boolean) { this._disableClose = coerceBooleanProperty(value); } - private _disableClose: boolean = false; - - /** Whether the sidenav is opened. */ - private _opened: boolean = false; - - /** Emits whenever the sidenav has started animating. */ - _animationStarted = new EventEmitter(); - - /** Whether the sidenav is animating. Used to prevent overlapping animations. */ - _isAnimating = false; - - /** Current state of the sidenav animation. */ - _animationState: 'open-instant' | 'open' | 'void' = 'void'; - - /** - * 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 fully opened. */ - @Output('open') onOpen = new EventEmitter(); - - /** 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(); - - 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'; - } - - constructor(private _elementRef: ElementRef, - private _focusTrapFactory: FocusTrapFactory, - @Optional() @Inject(DOCUMENT) private _doc: any) { - this.onOpen.subscribe(() => { - if (this._doc) { - this._elementFocusedBeforeSidenavWasOpened = this._doc.activeElement as HTMLElement; - } - - if (this.isFocusTrapEnabled && this._focusTrap) { - this._focusTrap.focusInitialElementWhenReady(); - } - }); - - this.onClose.subscribe(() => this._restoreFocus()); - } - - /** - * If focus is currently inside the sidenav, restores it to where it was before the sidenav - * opened. - */ - private _restoreFocus() { - let activeEl = this._doc && this._doc.activeElement; - if (activeEl && this._elementRef.nativeElement.contains(activeEl)) { - if (this._elementFocusedBeforeSidenavWasOpened instanceof HTMLElement) { - this._elementFocusedBeforeSidenavWasOpened.focus(); - } else { - this._elementRef.nativeElement.blur(); - } - } - - this._elementFocusedBeforeSidenavWasOpened = null; - } - - ngAfterContentInit() { - this._focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement); - this._focusTrap.enabled = this.isFocusTrapEnabled; - this._enableAnimations = true; - } - - ngOnDestroy() { - if (this._focusTrap) { - this._focusTrap.destroy(); - } - } - - /** - * Whether the sidenav is opened. We overload this because we trigger an event when it - * starts or end. - */ - @Input() - get opened(): boolean { return this._opened; } - set opened(v: boolean) { - this.toggle(coerceBooleanProperty(v)); - } - - - /** Open the sidenav. */ - open(): Promise { - return this.toggle(true); - } - - /** Close the sidenav. */ - close(): Promise { - return this.toggle(false); - } - - /** - * Toggle this sidenav. - * @param isOpen Whether the sidenav should be open. - */ - toggle(isOpen: boolean = !this.opened): Promise { - if (!this._isAnimating) { - this._opened = isOpen; - - if (isOpen) { - this._animationState = this._enableAnimations ? 'open' : 'open-instant'; - } else { - this._animationState = 'void'; - } - - this._currentTogglePromise = new Promise(resolve => { - first.call(isOpen ? this.onOpen : this.onClose).subscribe(resolve); - }); - - if (this._focusTrap) { - this._focusTrap.enabled = this.isFocusTrapEnabled; - } - } - - // TODO(crisbeto): This promise is here for backwards-compatibility. - // It should be removed next time we do breaking changes in the sidenav. - return this._currentTogglePromise!; - } - - /** - * Handles the keyboard events. - * @docs-private - */ - handleKeydown(event: KeyboardEvent) { - if (event.keyCode === ESCAPE && !this.disableClose) { - this.close(); - event.stopPropagation(); - } - } - - _onAnimationStart() { - this._isAnimating = true; - this._animationStarted.emit(); - } - - _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)); - } - - // Note: as of Angular 4.3, the animations module seems to fire the `start` callback before - // the end if animations are disabled. Make this call async to ensure that it still fires - // at the appropriate time. - Promise.resolve().then(() => { - this._isAnimating = false; - this._currentTogglePromise = null; - }); - } - - get _width() { - return this._elementRef.nativeElement ? (this._elementRef.nativeElement.offsetWidth || 0) : 0; - } -} - -/** - * component. - * - * This is the parent component to one or two s that validates the state internally - * and coordinates the backdrop and content styling. - */ -@Component({ - moduleId: module.id, - selector: 'md-sidenav-container, mat-sidenav-container', - templateUrl: 'sidenav-container.html', - styleUrls: [ - 'sidenav.css', - 'sidenav-transitions.css', - ], - host: { - 'class': 'mat-sidenav-container', + '[class.mat-drawer-end]': 'position === "end"', + '[class.mat-drawer-over]': 'mode === "over"', + '[class.mat-drawer-push]': 'mode === "push"', + '[class.mat-drawer-side]': 'mode === "side"', + 'tabIndex': '-1', }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, }) -export class MdSidenavContainer implements AfterContentInit { - @ContentChildren(MdSidenav) _sidenavs: QueryList; +export class MdSidenav extends MdDrawer {} - /** The sidenav child with the `start` alignment. */ - get start() { return this._start; } - /** The sidenav child with the `end` alignment. */ - get end() { return this._end; } - - /** Event emitted when the sidenav backdrop is clicked. */ - @Output() backdropClick = new EventEmitter(); - - /** The sidenav at the start/end alignment, independent of direction. */ - private _start: MdSidenav | null; - private _end: MdSidenav | null; - - /** - * The sidenav at the left/right. When direction changes, these will change as well. - * They're used as aliases for the above to set the left/right style properly. - * In LTR, _left == _start and _right == _end. - * In RTL, _left == _end and _right == _start. - */ - private _left: MdSidenav | null; - private _right: MdSidenav | null; - - /** Inline styles to be applied to the container. */ - _styles: { marginLeft: string; marginRight: string; transform: string; }; - - constructor(@Optional() private _dir: Directionality, private _element: ElementRef, - private _renderer: Renderer2, private _ngZone: NgZone, - private _changeDetectorRef: ChangeDetectorRef) { - // If a `Dir` directive exists up the tree, listen direction changes and update the left/right - // properties to point to the proper start/end. - if (_dir != null) { - _dir.change.subscribe(() => this._validateDrawers()); - } - } - - ngAfterContentInit() { - startWith.call(this._sidenavs.changes, null).subscribe(() => { - this._validateDrawers(); - this._sidenavs.forEach((sidenav: MdSidenav) => { - this._watchSidenavToggle(sidenav); - this._watchSidenavAlign(sidenav); - }); - }); - } - - /** Calls `open` of both start and end sidenavs */ - open(): void { - this._sidenavs.forEach(sidenav => sidenav.open()); - } - - /** Calls `close` of both start and end sidenavs */ - close(): void { - this._sidenavs.forEach(sidenav => sidenav.close()); - } - - /** - * Subscribes to sidenav events in order to set a class on the main container element when the - * sidenav is open and the backdrop is visible. This ensures any overflow on the container element - * is properly hidden. - */ - private _watchSidenavToggle(sidenav: MdSidenav): void { - takeUntil.call(sidenav._animationStarted, this._sidenavs.changes).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'); - this._updateStyles(); - this._changeDetectorRef.markForCheck(); - }); - - if (sidenav.mode !== 'side') { - takeUntil.call(merge(sidenav.onOpen, sidenav.onClose), this._sidenavs.changes).subscribe(() => - this._setContainerClass(sidenav.opened)); - } - } - - /** - * Subscribes to sidenav onAlignChanged event in order to re-validate drawers when the align - * changes. - */ - private _watchSidenavAlign(sidenav: MdSidenav): void { - if (!sidenav) { - return; - } - // NOTE: We need to wait for the microtask queue to be empty before validating, - // since both drawers may be swapping sides at the same time. - takeUntil.call(sidenav.onAlignChanged, this._sidenavs.changes).subscribe(() => - first.call(this._ngZone.onMicrotaskEmpty).subscribe(() => this._validateDrawers())); - } - - /** Toggles the 'mat-sidenav-opened' class on the main 'md-sidenav-container' element. */ - private _setContainerClass(isAdd: boolean): void { - if (isAdd) { - this._renderer.addClass(this._element.nativeElement, 'mat-sidenav-opened'); - } else { - this._renderer.removeClass(this._element.nativeElement, 'mat-sidenav-opened'); - } - } - - /** 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 => { - if (sidenav.align == 'end') { - if (this._end != null) { - throwMdDuplicatedSidenavError('end'); - } - this._end = sidenav; - } else { - if (this._start != null) { - throwMdDuplicatedSidenavError('start'); - } - this._start = sidenav; - } - }); - - this._right = this._left = null; - - // Detect if we're LTR or RTL. - if (this._dir == null || this._dir.value == 'ltr') { - this._left = this._start; - this._right = this._end; - } else { - this._left = this._end; - this._right = this._start; - } - } - - _onBackdropClicked() { - this.backdropClick.emit(); - this._closeModalSidenav(); - } - - _closeModalSidenav() { - // Close all open sidenav's where closing is not disabled and the mode is not `side`. - [this._start, this._end] - .filter(sidenav => sidenav && !sidenav.disableClose && sidenav.mode !== 'side') - .forEach(sidenav => sidenav!.close()); - } - - _isShowingBackdrop(): boolean { - return (this._isSidenavOpen(this._start) && this._start!.mode != 'side') - || (this._isSidenavOpen(this._end) && this._end!.mode != 'side'); - } - - private _isSidenavOpen(side: MdSidenav | null): boolean { - return side != null && side.opened; - } - - /** - * Return the width of the sidenav, if it's in the proper mode and opened. - * This may relayout the view, so do not call this often. - * @param sidenav - * @param mode - */ - private _getSidenavEffectiveWidth(sidenav: MdSidenav, mode: string): number { - return (this._isSidenavOpen(sidenav) && sidenav.mode == mode) ? sidenav._width : 0; - } - - /** - * Recalculates and updates the inline styles. Note that this - * should be used sparingly, because it causes a reflow. - */ - private _updateStyles() { - const marginLeft = this._left ? this._getSidenavEffectiveWidth(this._left, 'side') : 0; - const marginRight = this._right ? this._getSidenavEffectiveWidth(this._right, 'side') : 0; - const leftWidth = this._left ? this._getSidenavEffectiveWidth(this._left, 'push') : 0; - const rightWidth = this._right ? this._getSidenavEffectiveWidth(this._right, 'push') : 0; - - this._styles = { - marginLeft: `${marginLeft}px`, - marginRight: `${marginRight}px`, - transform: `translate3d(${leftWidth - rightWidth}px, 0, 0)` - }; - } +@Component({ + moduleId: module.id, + selector: 'md-sidenav-container, mat-sidenav-container', + templateUrl: 'drawer-container.html', + styleUrls: [ + 'drawer.css', + 'drawer-transitions.css', + ], + host: { + 'class': 'mat-drawer-container mat-sidenav-container', + }, + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, +}) +export class MdSidenavContainer extends MdDrawerContainer { + @ContentChildren(MdSidenav) _drawers; }