Skip to content

Commit 5ffdea6

Browse files
mmalerbajelbourn
authored andcommitted
fix(slider): correctly detect when sidenav align changes. (#1758)
1 parent 2de461e commit 5ffdea6

File tree

6 files changed

+142
-44
lines changed

6 files changed

+142
-44
lines changed

src/demo-app/sidenav/sidenav-demo.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,14 @@ <h2>Sidenav Already Opened</h2>
4747
<button md-button (click)="start2.toggle()">Toggle Start Side Drawer</button>
4848
</div>
4949
</md-sidenav-layout>
50+
51+
<h2>Dynamic Alignment Sidenav</h2>
52+
53+
<md-sidenav-layout class="demo-sidenav-layout">
54+
<md-sidenav #dynamicAlignSidenav mode="push" [align]="side">Drawer</md-sidenav>
55+
56+
<div class="demo-sidenav-content">
57+
<button (click)="dynamicAlignSidenav.toggle()">Toggle sidenav</button>
58+
<button (click)="side = (side == 'start') ? 'end' : 'start'">Change sides</button>
59+
</div>
60+
</md-sidenav-layout>

src/demo-app/sidenav/sidenav-demo.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ import {Component} from '@angular/core';
77
templateUrl: 'sidenav-demo.html',
88
styleUrls: ['sidenav-demo.css'],
99
})
10-
export class SidenavDemo {}
10+
export class SidenavDemo {
11+
side = 'start';
12+
}

src/lib/sidenav/README.md

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,6 @@ MdSidenav is the side navigation component for Material 2. It is composed of two
1111

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

14-
### Properties
15-
16-
| Name | Description |
17-
| --- | --- |
18-
| `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. |
19-
| `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. |
20-
2114
## `<md-sidenav>`
2215

2316
The sidenav panel.
@@ -26,7 +19,7 @@ The sidenav panel.
2619

2720
| Name | Type | Description |
2821
| --- | --- | --- |
29-
| `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. |
22+
| `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. |
3023
| `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"`. |
3124
| `opened` | `boolean` | Whether or not the sidenav is opened. Use this binding to open/close the sidenav. |
3225

src/lib/sidenav/sidenav.scss

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,11 @@
1818
}
1919
&.md-sidenav-closing {
2020
transform: translate3d($close, 0, 0);
21-
will-change: transform;
2221
}
2322
&.md-sidenav-opening {
2423
@include md-elevation(1);
2524
visibility: visible;
2625
transform: translate3d($open, 0, 0);
27-
will-change: transform;
2826
}
2927
&.md-sidenav-opened {
3028
@include md-elevation(1);
@@ -131,3 +129,7 @@ md-sidenav {
131129
}
132130
}
133131
}
132+
133+
.md-sidenav-invalid {
134+
display: none;
135+
}

src/lib/sidenav/sidenav.spec.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ describe('MdSidenav', () => {
2525
SidenavLayoutNoSidenavTestApp,
2626
SidenavSetToOpenedFalse,
2727
SidenavSetToOpenedTrue,
28+
SidenavDynamicAlign,
2829
],
2930
});
3031

@@ -193,14 +194,6 @@ describe('MdSidenav', () => {
193194
tick();
194195
}).not.toThrow();
195196
}));
196-
197-
it('does throw when created with two sidenav on the same side', fakeAsync(() => {
198-
expect(() => {
199-
let fixture = TestBed.createComponent(SidenavLayoutTwoSidenavTestApp);
200-
fixture.detectChanges();
201-
tick();
202-
}).toThrow();
203-
}));
204197
});
205198

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

234+
it('should mark sidenavs invalid when multiple have same align', () => {
235+
const fixture = TestBed.createComponent(SidenavDynamicAlign);
236+
fixture.detectChanges();
237+
238+
const testComponent: SidenavDynamicAlign = fixture.debugElement.componentInstance;
239+
const sidenavEl = fixture.debugElement.query(By.css('md-sidenav')).nativeElement;
240+
expect(sidenavEl.classList).not.toContain('md-sidenav-invalid');
241+
242+
testComponent.sidenav1Align = 'end';
243+
fixture.detectChanges();
244+
245+
expect(sidenavEl.classList).toContain('md-sidenav-invalid');
246+
247+
testComponent.sidenav2Align = 'start';
248+
fixture.detectChanges();
249+
250+
expect(sidenavEl.classList).not.toContain('md-sidenav-invalid');
251+
});
241252
});
242253

243254
});
@@ -314,3 +325,15 @@ class SidenavSetToOpenedFalse { }
314325
</md-sidenav-layout>`,
315326
})
316327
class SidenavSetToOpenedTrue { }
328+
329+
@Component({
330+
template: `
331+
<md-sidenav-layout>
332+
<md-sidenav #sidenav1 [align]="sidenav1Align"></md-sidenav>
333+
<md-sidenav #sidenav2 [align]="sidenav2Align"></md-sidenav>
334+
</md-sidenav-layout>`,
335+
})
336+
class SidenavDynamicAlign {
337+
sidenav1Align = 'start';
338+
sidenav2Align = 'end';
339+
}

src/lib/sidenav/sidenav.ts

Lines changed: 92 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
Component,
66
ContentChildren,
77
ElementRef,
8-
HostBinding,
98
Input,
109
Optional,
1110
Output,
@@ -40,14 +39,51 @@ export class MdDuplicatedSidenavError extends MdError {
4039
host: {
4140
'(transitionend)': '_onTransitionEnd($event)',
4241
// must prevent the browser from aligning text based on value
43-
'[attr.align]': 'null'
42+
'[attr.align]': 'null',
43+
'[class.md-sidenav-closed]': '_isClosed',
44+
'[class.md-sidenav-closing]': '_isClosing',
45+
'[class.md-sidenav-end]': '_isEnd',
46+
'[class.md-sidenav-opened]': '_isOpened',
47+
'[class.md-sidenav-opening]': '_isOpening',
48+
'[class.md-sidenav-over]': '_modeOver',
49+
'[class.md-sidenav-push]': '_modePush',
50+
'[class.md-sidenav-side]': '_modeSide',
51+
'[class.md-sidenav-invalid]': '!valid',
4452
},
4553
changeDetection: ChangeDetectionStrategy.OnPush,
4654
encapsulation: ViewEncapsulation.None,
4755
})
4856
export class MdSidenav implements AfterContentInit {
4957
/** Alignment of the sidenav (direction neutral); whether 'start' or 'end'. */
50-
@Input() align: 'start' | 'end' = 'start';
58+
private _align: 'start' | 'end' = 'start';
59+
60+
/** Whether this md-sidenav is part of a valid md-sidenav-layout configuration. */
61+
get valid() {
62+
return this._valid;
63+
}
64+
set valid(value) {
65+
value = coerceBooleanProperty(value);
66+
// When the drawers are not in a valid configuration we close them all until they are in a valid
67+
// configuration again.
68+
if (!value) {
69+
this.close();
70+
}
71+
this._valid = value;
72+
}
73+
private _valid = true;
74+
75+
@Input()
76+
get align() {
77+
return this._align;
78+
}
79+
set align(value) {
80+
// Make sure we have a valid value.
81+
value = (value == 'end') ? 'end' : 'start';
82+
if (value != this._align) {
83+
this._align = value;
84+
this.onAlignChanged.emit();
85+
}
86+
}
5187

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

106+
/** Event emitted when the sidenav alignment changes. */
107+
@Output('align-changed') onAlignChanged = new EventEmitter<void>();
108+
70109
/**
71110
* @param _elementRef The DOM element reference. Used for transition and width calculation.
72111
* If not available we do not hook on transitions.
@@ -113,6 +152,8 @@ export class MdSidenav implements AfterContentInit {
113152
* @param isOpen
114153
*/
115154
toggle(isOpen: boolean = !this.opened): Promise<void> {
155+
if (!this.valid) { return Promise.resolve(null); }
156+
116157
// Shortcut it if we're already opened.
117158
if (isOpen === this.opened) {
118159
if (!this._transition) {
@@ -186,32 +227,31 @@ export class MdSidenav implements AfterContentInit {
186227
}
187228
}
188229

189-
@HostBinding('class.md-sidenav-closing') get _isClosing() {
230+
get _isClosing() {
190231
return !this._opened && this._transition;
191232
}
192-
@HostBinding('class.md-sidenav-opening') get _isOpening() {
233+
get _isOpening() {
193234
return this._opened && this._transition;
194235
}
195-
@HostBinding('class.md-sidenav-closed') get _isClosed() {
236+
get _isClosed() {
196237
return !this._opened && !this._transition;
197238
}
198-
@HostBinding('class.md-sidenav-opened') get _isOpened() {
239+
get _isOpened() {
199240
return this._opened && !this._transition;
200241
}
201-
@HostBinding('class.md-sidenav-end') get _isEnd() {
242+
get _isEnd() {
202243
return this.align == 'end';
203244
}
204-
@HostBinding('class.md-sidenav-side') get _modeSide() {
245+
get _modeSide() {
205246
return this.mode == 'side';
206247
}
207-
@HostBinding('class.md-sidenav-over') get _modeOver() {
248+
get _modeOver() {
208249
return this.mode == 'over';
209250
}
210-
@HostBinding('class.md-sidenav-push') get _modePush() {
251+
get _modePush() {
211252
return this.mode == 'push';
212253
}
213254

214-
/** TODO: internal (needed by MdSidenavLayout). */
215255
get _width() {
216256
if (this._elementRef.nativeElement) {
217257
return this._elementRef.nativeElement.offsetWidth;
@@ -232,7 +272,7 @@ export class MdSidenav implements AfterContentInit {
232272
* <md-sidenav-layout> component.
233273
*
234274
* This is the parent component to one or two <md-sidenav>s that validates the state internally
235-
* and coordinate the backdrop and content styling.
275+
* and coordinates the backdrop and content styling.
236276
*/
237277
@Component({
238278
moduleId: module.id,
@@ -275,48 +315,73 @@ export class MdSidenavLayout implements AfterContentInit {
275315
}
276316
}
277317

278-
/** TODO: internal */
279318
ngAfterContentInit() {
280319
// On changes, assert on consistency.
281320
this._sidenavs.changes.subscribe(() => this._validateDrawers());
282-
this._sidenavs.forEach((sidenav: MdSidenav) => this._watchSidenavToggle(sidenav));
321+
this._sidenavs.forEach((sidenav: MdSidenav) => {
322+
this._watchSidenavToggle(sidenav);
323+
this._watchSidenavAlign(sidenav);
324+
});
283325
this._validateDrawers();
284326
}
285327

286-
/*
287-
* Subscribes to sidenav events in order to set a class on the main layout element when the sidenav
288-
* is open and the backdrop is visible. This ensures any overflow on the layout element is properly
289-
* hidden.
290-
*/
328+
/**
329+
* Subscribes to sidenav events in order to set a class on the main layout element when the
330+
* sidenav is open and the backdrop is visible. This ensures any overflow on the layout element is
331+
* properly hidden.
332+
*/
291333
private _watchSidenavToggle(sidenav: MdSidenav): void {
292334
if (!sidenav || sidenav.mode === 'side') { return; }
293335
sidenav.onOpen.subscribe(() => this._setLayoutClass(sidenav, true));
294336
sidenav.onClose.subscribe(() => this._setLayoutClass(sidenav, false));
295337
}
296338

297-
/* Toggles the 'md-sidenav-opened' class on the main 'md-sidenav-layout' element. */
339+
/**
340+
* Subscribes to sidenav onAlignChanged event in order to re-validate drawers when the align
341+
* changes.
342+
*/
343+
private _watchSidenavAlign(sidenav: MdSidenav): void {
344+
if (!sidenav) { return; }
345+
sidenav.onAlignChanged.subscribe(() => this._validateDrawers());
346+
}
347+
348+
/** Toggles the 'md-sidenav-opened' class on the main 'md-sidenav-layout' element. */
298349
private _setLayoutClass(sidenav: MdSidenav, bool: boolean): void {
299350
this._renderer.setElementClass(this._element.nativeElement, 'md-sidenav-opened', bool);
300351
}
301352

353+
/** Sets the valid state of the drawers. */
354+
private _setDrawersValid(valid: boolean) {
355+
this._sidenavs.forEach((sidenav) => {
356+
sidenav.valid = valid;
357+
});
358+
if (!valid) {
359+
this._start = this._end = this._left = this._right = null;
360+
}
361+
}
362+
302363
/** Validate the state of the sidenav children components. */
303364
private _validateDrawers() {
304365
this._start = this._end = null;
305366

306367
// Ensure that we have at most one start and one end sidenav.
307-
this._sidenavs.forEach(sidenav => {
368+
// NOTE: We must call toArray on _sidenavs even though it's iterable
369+
// (see https://github.com/Microsoft/TypeScript/issues/3164).
370+
for (let sidenav of this._sidenavs.toArray()) {
308371
if (sidenav.align == 'end') {
309372
if (this._end != null) {
310-
throw new MdDuplicatedSidenavError('end');
373+
this._setDrawersValid(false);
374+
return;
311375
}
312376
this._end = sidenav;
313377
} else {
314378
if (this._start != null) {
315-
throw new MdDuplicatedSidenavError('start');
379+
this._setDrawersValid(false);
380+
return;
316381
}
317382
this._start = sidenav;
318383
}
319-
});
384+
}
320385

321386
this._right = this._left = null;
322387

@@ -328,6 +393,8 @@ export class MdSidenavLayout implements AfterContentInit {
328393
this._left = this._end;
329394
this._right = this._start;
330395
}
396+
397+
this._setDrawersValid(true);
331398
}
332399

333400
_closeModalSidenav() {

0 commit comments

Comments
 (0)