mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
@@ -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>
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)`
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user