Skip to content

Commit

Permalink
fix(slider): correctly detect when sidenav align changes. (#1758)
Browse files Browse the repository at this point in the history
  • Loading branch information
mmalerba authored and jelbourn committed Nov 8, 2016
1 parent 2de461e commit 5ffdea6
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 44 deletions.
11 changes: 11 additions & 0 deletions src/demo-app/sidenav/sidenav-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,14 @@ <h2>Sidenav Already Opened</h2>
<button md-button (click)="start2.toggle()">Toggle Start Side Drawer</button>
</div>
</md-sidenav-layout>

<h2>Dynamic Alignment Sidenav</h2>

<md-sidenav-layout class="demo-sidenav-layout">
<md-sidenav #dynamicAlignSidenav mode="push" [align]="side">Drawer</md-sidenav>

<div class="demo-sidenav-content">
<button (click)="dynamicAlignSidenav.toggle()">Toggle sidenav</button>
<button (click)="side = (side == 'start') ? 'end' : 'start'">Change sides</button>
</div>
</md-sidenav-layout>
4 changes: 3 additions & 1 deletion src/demo-app/sidenav/sidenav-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ import {Component} from '@angular/core';
templateUrl: 'sidenav-demo.html',
styleUrls: ['sidenav-demo.css'],
})
export class SidenavDemo {}
export class SidenavDemo {
side = 'start';
}
9 changes: 1 addition & 8 deletions src/lib/sidenav/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,6 @@ MdSidenav is the side navigation component for Material 2. It is composed of two

The parent component. Contains the code necessary to coordinate one or two sidenav and the backdrop.

### Properties

| Name | Description |
| --- | --- |
| `start` | The start aligned `MdSidenav` instance, or `null` if none is specified. In LTR direction, this is the sidenav shown on the left side. In RTL direction, it will show on the right. There can only be one sidenav on either side. |
| `end` | The end aligned `MdSidenav` instance, or `null` if none is specified. This is the sidenav opposing the `start` sidenav. There can only be one sidenav on either side. |

## `<md-sidenav>`

The sidenav panel.
Expand All @@ -26,7 +19,7 @@ The sidenav panel.

| Name | Type | Description |
| --- | --- | --- |
| `align` | `"start"|"end"` | The alignment of this sidenav. In LTR direction, `"start"` will be shown on the left, `"end"` on the right. In RTL, it is reversed. `"start"` is used by default. An exception will be thrown if there are more than 1 sidenav on either side. |
| `align` | `"start"|"end"` | The alignment of this sidenav. In LTR direction, `"start"` will be shown on the left, `"end"` on the right. In RTL, it is reversed. `"start"` is used by default. If there is more than 1 sidenav on either side the layout will be considered invalid and none of the sidenavs will be visible or toggleable until the layout is valid again. |
| `mode` | `"over"|"push"|"side"` | The mode or styling of the sidenav, default being `"over"`. With `"over"` the sidenav will appear above the content, and a backdrop will be shown. With `"push"` the sidenav will push the content of the `<md-sidenav-layout>` 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. |

Expand Down
6 changes: 4 additions & 2 deletions src/lib/sidenav/sidenav.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,11 @@
}
&.md-sidenav-closing {
transform: translate3d($close, 0, 0);
will-change: transform;
}
&.md-sidenav-opening {
@include md-elevation(1);
visibility: visible;
transform: translate3d($open, 0, 0);
will-change: transform;
}
&.md-sidenav-opened {
@include md-elevation(1);
Expand Down Expand Up @@ -131,3 +129,7 @@ md-sidenav {
}
}
}

.md-sidenav-invalid {
display: none;
}
39 changes: 31 additions & 8 deletions src/lib/sidenav/sidenav.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe('MdSidenav', () => {
SidenavLayoutNoSidenavTestApp,
SidenavSetToOpenedFalse,
SidenavSetToOpenedTrue,
SidenavDynamicAlign,
],
});

Expand Down Expand Up @@ -193,14 +194,6 @@ describe('MdSidenav', () => {
tick();
}).not.toThrow();
}));

it('does throw when created with two sidenav on the same side', fakeAsync(() => {
expect(() => {
let fixture = TestBed.createComponent(SidenavLayoutTwoSidenavTestApp);
fixture.detectChanges();
tick();
}).toThrow();
}));
});

describe('attributes', () => {
Expand Down Expand Up @@ -238,6 +231,24 @@ describe('MdSidenav', () => {
.toBe(false, 'Expected sidenav not to have a native align attribute.');
});

it('should mark sidenavs invalid when multiple have same align', () => {
const fixture = TestBed.createComponent(SidenavDynamicAlign);
fixture.detectChanges();

const testComponent: SidenavDynamicAlign = fixture.debugElement.componentInstance;
const sidenavEl = fixture.debugElement.query(By.css('md-sidenav')).nativeElement;
expect(sidenavEl.classList).not.toContain('md-sidenav-invalid');

testComponent.sidenav1Align = 'end';
fixture.detectChanges();

expect(sidenavEl.classList).toContain('md-sidenav-invalid');

testComponent.sidenav2Align = 'start';
fixture.detectChanges();

expect(sidenavEl.classList).not.toContain('md-sidenav-invalid');
});
});

});
Expand Down Expand Up @@ -314,3 +325,15 @@ class SidenavSetToOpenedFalse { }
</md-sidenav-layout>`,
})
class SidenavSetToOpenedTrue { }

@Component({
template: `
<md-sidenav-layout>
<md-sidenav #sidenav1 [align]="sidenav1Align"></md-sidenav>
<md-sidenav #sidenav2 [align]="sidenav2Align"></md-sidenav>
</md-sidenav-layout>`,
})
class SidenavDynamicAlign {
sidenav1Align = 'start';
sidenav2Align = 'end';
}
117 changes: 92 additions & 25 deletions src/lib/sidenav/sidenav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
Component,
ContentChildren,
ElementRef,
HostBinding,
Input,
Optional,
Output,
Expand Down Expand Up @@ -40,14 +39,51 @@ export class MdDuplicatedSidenavError extends MdError {
host: {
'(transitionend)': '_onTransitionEnd($event)',
// must prevent the browser from aligning text based on value
'[attr.align]': 'null'
'[attr.align]': 'null',
'[class.md-sidenav-closed]': '_isClosed',
'[class.md-sidenav-closing]': '_isClosing',
'[class.md-sidenav-end]': '_isEnd',
'[class.md-sidenav-opened]': '_isOpened',
'[class.md-sidenav-opening]': '_isOpening',
'[class.md-sidenav-over]': '_modeOver',
'[class.md-sidenav-push]': '_modePush',
'[class.md-sidenav-side]': '_modeSide',
'[class.md-sidenav-invalid]': '!valid',
},
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
})
export class MdSidenav implements AfterContentInit {
/** Alignment of the sidenav (direction neutral); whether 'start' or 'end'. */
@Input() align: 'start' | 'end' = 'start';
private _align: 'start' | 'end' = 'start';

/** Whether this md-sidenav is part of a valid md-sidenav-layout configuration. */
get valid() {
return this._valid;
}
set valid(value) {
value = coerceBooleanProperty(value);
// When the drawers are not in a valid configuration we close them all until they are in a valid
// configuration again.
if (!value) {
this.close();
}
this._valid = value;
}
private _valid = true;

@Input()
get align() {
return this._align;
}
set align(value) {
// Make sure we have a valid value.
value = (value == 'end') ? 'end' : 'start';
if (value != this._align) {
this._align = value;
this.onAlignChanged.emit();
}
}

/** Mode of the sidenav; whether 'over' or 'side'. */
@Input() mode: 'over' | 'push' | 'side' = 'over';
Expand All @@ -67,6 +103,9 @@ export class MdSidenav implements AfterContentInit {
/** Event emitted when the sidenav is fully closed. */
@Output('close') onClose = new EventEmitter<void>();

/** Event emitted when the sidenav alignment changes. */
@Output('align-changed') onAlignChanged = new EventEmitter<void>();

/**
* @param _elementRef The DOM element reference. Used for transition and width calculation.
* If not available we do not hook on transitions.
Expand Down Expand Up @@ -113,6 +152,8 @@ export class MdSidenav implements AfterContentInit {
* @param isOpen
*/
toggle(isOpen: boolean = !this.opened): Promise<void> {
if (!this.valid) { return Promise.resolve(null); }

// Shortcut it if we're already opened.
if (isOpen === this.opened) {
if (!this._transition) {
Expand Down Expand Up @@ -186,32 +227,31 @@ export class MdSidenav implements AfterContentInit {
}
}

@HostBinding('class.md-sidenav-closing') get _isClosing() {
get _isClosing() {
return !this._opened && this._transition;
}
@HostBinding('class.md-sidenav-opening') get _isOpening() {
get _isOpening() {
return this._opened && this._transition;
}
@HostBinding('class.md-sidenav-closed') get _isClosed() {
get _isClosed() {
return !this._opened && !this._transition;
}
@HostBinding('class.md-sidenav-opened') get _isOpened() {
get _isOpened() {
return this._opened && !this._transition;
}
@HostBinding('class.md-sidenav-end') get _isEnd() {
get _isEnd() {
return this.align == 'end';
}
@HostBinding('class.md-sidenav-side') get _modeSide() {
get _modeSide() {
return this.mode == 'side';
}
@HostBinding('class.md-sidenav-over') get _modeOver() {
get _modeOver() {
return this.mode == 'over';
}
@HostBinding('class.md-sidenav-push') get _modePush() {
get _modePush() {
return this.mode == 'push';
}

/** TODO: internal (needed by MdSidenavLayout). */
get _width() {
if (this._elementRef.nativeElement) {
return this._elementRef.nativeElement.offsetWidth;
Expand All @@ -232,7 +272,7 @@ export class MdSidenav implements AfterContentInit {
* <md-sidenav-layout> component.
*
* This is the parent component to one or two <md-sidenav>s that validates the state internally
* and coordinate the backdrop and content styling.
* and coordinates the backdrop and content styling.
*/
@Component({
moduleId: module.id,
Expand Down Expand Up @@ -275,48 +315,73 @@ export class MdSidenavLayout implements AfterContentInit {
}
}

/** TODO: internal */
ngAfterContentInit() {
// On changes, assert on consistency.
this._sidenavs.changes.subscribe(() => this._validateDrawers());
this._sidenavs.forEach((sidenav: MdSidenav) => this._watchSidenavToggle(sidenav));
this._sidenavs.forEach((sidenav: MdSidenav) => {
this._watchSidenavToggle(sidenav);
this._watchSidenavAlign(sidenav);
});
this._validateDrawers();
}

/*
* Subscribes to sidenav events in order to set a class on the main layout element when the sidenav
* is open and the backdrop is visible. This ensures any overflow on the layout element is properly
* hidden.
*/
/**
* Subscribes to sidenav events in order to set a class on the main layout element when the
* sidenav is open and the backdrop is visible. This ensures any overflow on the layout element is
* properly hidden.
*/
private _watchSidenavToggle(sidenav: MdSidenav): void {
if (!sidenav || sidenav.mode === 'side') { return; }
sidenav.onOpen.subscribe(() => this._setLayoutClass(sidenav, true));
sidenav.onClose.subscribe(() => this._setLayoutClass(sidenav, false));
}

/* Toggles the 'md-sidenav-opened' class on the main 'md-sidenav-layout' element. */
/**
* Subscribes to sidenav onAlignChanged event in order to re-validate drawers when the align
* changes.
*/
private _watchSidenavAlign(sidenav: MdSidenav): void {
if (!sidenav) { return; }
sidenav.onAlignChanged.subscribe(() => this._validateDrawers());
}

/** Toggles the 'md-sidenav-opened' class on the main 'md-sidenav-layout' element. */
private _setLayoutClass(sidenav: MdSidenav, bool: boolean): void {
this._renderer.setElementClass(this._element.nativeElement, 'md-sidenav-opened', bool);
}

/** Sets the valid state of the drawers. */
private _setDrawersValid(valid: boolean) {
this._sidenavs.forEach((sidenav) => {
sidenav.valid = valid;
});
if (!valid) {
this._start = this._end = this._left = this._right = null;
}
}

/** Validate the state of the sidenav children components. */
private _validateDrawers() {
this._start = this._end = null;

// Ensure that we have at most one start and one end sidenav.
this._sidenavs.forEach(sidenav => {
// NOTE: We must call toArray on _sidenavs even though it's iterable
// (see https://github.com/Microsoft/TypeScript/issues/3164).
for (let sidenav of this._sidenavs.toArray()) {
if (sidenav.align == 'end') {
if (this._end != null) {
throw new MdDuplicatedSidenavError('end');
this._setDrawersValid(false);
return;
}
this._end = sidenav;
} else {
if (this._start != null) {
throw new MdDuplicatedSidenavError('start');
this._setDrawersValid(false);
return;
}
this._start = sidenav;
}
});
}

this._right = this._left = null;

Expand All @@ -328,6 +393,8 @@ export class MdSidenavLayout implements AfterContentInit {
this._left = this._end;
this._right = this._start;
}

this._setDrawersValid(true);
}

_closeModalSidenav() {
Expand Down

0 comments on commit 5ffdea6

Please sign in to comment.