refactor(sidenav): split into drawer and sidenav (#6260)

* refactor(sidenav): split into drawer and sidenav

* s/side/position

* fix tests

* fix bad rebase
This commit is contained in:
mmalerba
2017-08-17 12:48:25 -07:00
committed by Andrew Seguin
parent 0f02a11bcd
commit 372436cb8a
13 changed files with 708 additions and 627 deletions
+5 -5
View File
@@ -15,7 +15,7 @@
<input #myinput>
</md-sidenav>
<md-sidenav #end align="end">
<md-sidenav #end position="end">
End Side Drawer
<br>
<button md-button (click)="end.close()">Close</button>
@@ -48,14 +48,14 @@
</div>
</md-sidenav-container>
<h2>Dynamic Alignment Sidenav</h2>
<h2>Dynamic Position Sidenav</h2>
<md-sidenav-container class="demo-sidenav-container">
<md-sidenav #dynamicAlignSidenav1 mode="side" [align]="invert ? 'end' : 'start'">Start</md-sidenav>
<md-sidenav #dynamicAlignSidenav2 mode="side" [align]="invert ? 'start' : 'end'">End</md-sidenav>
<md-sidenav #dynamicPosSidenav1 mode="side" [position]="invert ? 'end' : 'start'">Start</md-sidenav>
<md-sidenav #dynamicPosSidenav2 mode="side" [position]="invert ? 'start' : 'end'">End</md-sidenav>
<div class="demo-sidenav-content">
<button (click)="dynamicAlignSidenav1.toggle(); dynamicAlignSidenav2.toggle()">
<button (click)="dynamicPosSidenav1.toggle(); dynamicPosSidenav2.toggle()">
Toggle sidenavs
</button>
<button (click)="invert = !invert">Change sides</button>
+2 -2
View File
@@ -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 `<md-sidenav-container>` 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:
<br>
<button md-button #closeStartButton (click)="start.close()">Close</button>
</md-sidenav>
<md-sidenav #end align="end">
<md-sidenav #end position="end">
End Sidenav.
<button md-button (click)="end.close()">Close</button>
</md-sidenav>
+12 -12
View File
@@ -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;
}
}
+8
View File
@@ -0,0 +1,8 @@
<div class="mat-drawer-backdrop" (click)="_onBackdropClicked()"
[class.mat-drawer-shown]="_isShowingBackdrop()"></div>
<ng-content select="md-drawer, mat-drawer, md-sidenav, mat-sidenav"></ng-content>
<div class="mat-drawer-content" [ngStyle]="_styles" cdk-scrollable>
<ng-content></ng-content>
</div>
@@ -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;
}
}
@@ -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);
@@ -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<SidenavWitFocusableElements>;
let testComponent: SidenavWitFocusableElements;
let sidenav: MdSidenav;
let fixture: ComponentFixture<DrawerWitFocusableElements>;
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: `<md-sidenav-container></md-sidenav-container>`})
class SidenavContainerNoSidenavTestApp { }
/** Test component that contains an MdDrawerContainer but no MdDrawer. */
@Component({template: `<md-drawer-container></md-drawer-container>`})
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: `
<md-sidenav-container>
<md-sidenav align="start"> </md-sidenav>
<md-sidenav align="end"> </md-sidenav>
</md-sidenav-container>`,
<md-drawer-container>
<md-drawer position="start"></md-drawer>
<md-drawer position="end"></md-drawer>
</md-drawer-container>`,
})
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: `
<md-sidenav-container (backdropClick)="backdropClicked()">
<md-sidenav #sidenav align="start"
(open)="open()"
(close)="close()">
<button #sidenavButton>Content.</button>
</md-sidenav>
<button (click)="sidenav.open()" class="open" #openButton></button>
<button (click)="sidenav.close()" class="close" #closeButton></button>
</md-sidenav-container>`,
<md-drawer-container (backdropClick)="backdropClicked()">
<md-drawer #drawer position="start"
(open)="open()"
(close)="close()">
<button #drawerButton>Content.</button>
</md-drawer>
<button (click)="drawer.open()" class="open" #openButton></button>
<button (click)="drawer.close()" class="close" #closeButton></button>
</md-drawer-container>`,
})
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: `
<md-sidenav-container>
<md-sidenav #sidenav mode="side" opened="false">
Closed Sidenav.
</md-sidenav>
</md-sidenav-container>`,
<md-drawer-container>
<md-drawer #drawer mode="side" opened="false">
Closed Drawer.
</md-drawer>
</md-drawer-container>`,
})
class SidenavSetToOpenedFalse { }
class DrawerSetToOpenedFalse { }
@Component({
template: `
<md-sidenav-container>
<md-sidenav #sidenav mode="side" opened="true">
Closed Sidenav.
</md-sidenav>
</md-sidenav-container>`,
<md-drawer-container>
<md-drawer #drawer mode="side" opened="true">
Closed Drawer.
</md-drawer>
</md-drawer-container>`,
})
class SidenavSetToOpenedTrue { }
class DrawerSetToOpenedTrue { }
@Component({
template: `
<md-sidenav-container>
<md-sidenav #sidenav1 [align]="sidenav1Align"></md-sidenav>
<md-sidenav #sidenav2 [align]="sidenav2Align"></md-sidenav>
</md-sidenav-container>`,
<md-drawer-container>
<md-drawer #drawer1 [position]="drawer1Position"></md-drawer>
<md-drawer #drawer2 [position]="drawer2Position"></md-drawer>
</md-drawer-container>`,
})
class SidenavDynamicAlign {
sidenav1Align = 'start';
sidenav2Align = 'end';
class DrawerDynamicPosition {
drawer1Position = 'start';
drawer2Position = 'end';
}
@Component({
template: `
<md-sidenav-container>
<md-sidenav align="start" [mode]="mode">
<md-drawer-container>
<md-drawer position="start" [mode]="mode">
<a class="link1" href="#">link1</a>
</md-sidenav>
</md-drawer>
<a class="link2" href="#">link2</a>
</md-sidenav-container>`,
</md-drawer-container>`,
})
class SidenavWitFocusableElements {
class DrawerWitFocusableElements {
mode: string = 'over';
}
@Component({
template: `
<md-sidenav-container>
<md-sidenav *ngIf="showSidenav" #sidenav mode="side">Sidenav</md-sidenav>
</md-sidenav-container>
<md-drawer-container>
<md-drawer *ngIf="showDrawer" #drawer mode="side">Drawer</md-drawer>
</md-drawer-container>
`,
})
class SidenavDelayed {
@ViewChild(MdSidenav) sidenav: MdSidenav;
showSidenav = false;
class DrawerDelayed {
@ViewChild(MdDrawer) drawer: MdDrawer;
showDrawer = false;
}
+495
View File
@@ -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) {}
}
/**
* <md-drawer> 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<void>();
/** 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<MdDrawerToggleResult> | null;
/** Event emitted when the drawer is fully opened. */
@Output('open') onOpen = new EventEmitter<MdDrawerToggleResult | void>();
/** Event emitted when the drawer is fully closed. */
@Output('close') onClose = new EventEmitter<MdDrawerToggleResult | void>();
/** Event emitted when the drawer's position changes. */
@Output('positionChanged') onPositionChanged = new EventEmitter<void>();
/** @deprecated */
@Output('align-changed') onAlignChanged = new EventEmitter<void>();
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<MdDrawerToggleResult> {
return this.toggle(true);
}
/** Close the drawer. */
close(): Promise<MdDrawerToggleResult> {
return this.toggle(false);
}
/**
* Toggle this drawer.
* @param isOpen Whether the drawer should be open.
*/
toggle(isOpen: boolean = !this.opened): Promise<MdDrawerToggleResult> {
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;
}
}
/**
* <md-drawer-container> component.
*
* This is the parent component to one or two <md-drawer>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<MdDrawer>;
/** 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<void>();
/** 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)`
};
}
}
+4 -2
View File
@@ -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';
-8
View File
@@ -1,8 +0,0 @@
<div class="mat-sidenav-backdrop" (click)="_onBackdropClicked()"
[class.mat-sidenav-shown]="_isShowingBackdrop()"></div>
<ng-content select="md-sidenav, mat-sidenav"></ng-content>
<div class="mat-sidenav-content" [ngStyle]="_styles" cdk-scrollable>
<ng-content></ng-content>
</div>
+1 -1
View File
@@ -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.
+28 -444
View File
@@ -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) {}
}
/**
* <md-sidenav> 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<void>();
/** 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<MdSidenavToggleResult> | null;
/** Event emitted when the sidenav is fully opened. */
@Output('open') onOpen = new EventEmitter<MdSidenavToggleResult | void>();
/** Event emitted when the sidenav is fully closed. */
@Output('close') onClose = new EventEmitter<MdSidenavToggleResult | void>();
/** Event emitted when the sidenav alignment changes. */
@Output('align-changed') onAlignChanged = new EventEmitter<void>();
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<MdSidenavToggleResult> {
return this.toggle(true);
}
/** Close the sidenav. */
close(): Promise<MdSidenavToggleResult> {
return this.toggle(false);
}
/**
* Toggle this sidenav.
* @param isOpen Whether the sidenav should be open.
*/
toggle(isOpen: boolean = !this.opened): Promise<MdSidenavToggleResult> {
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;
}
}
/**
* <md-sidenav-container> component.
*
* This is the parent component to one or two <md-sidenav>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<MdSidenav>;
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<void>();
/** 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;
}