Skip to content

Commit

Permalink
fix(drawer): allow for drawer container to auto-resize while open
Browse files Browse the repository at this point in the history
Adds the `autosize` input that allows users to opt-in to drawers that auto-resize, similarly to the behavior before angular#6189. The behavior is off by default, because it's unnecessary for most use cases and can cause performance issues.

Fixes angular#6743.
  • Loading branch information
crisbeto committed Nov 17, 2017
1 parent 541a95e commit 08fa7d0
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 10 deletions.
48 changes: 47 additions & 1 deletion src/lib/sidenav/drawer.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import {fakeAsync, async, tick, ComponentFixture, TestBed} from '@angular/core/testing';
import {
fakeAsync,
async,
tick,
ComponentFixture,
TestBed,
discardPeriodicTasks,
} from '@angular/core/testing';
import {Component, ElementRef, ViewChild} from '@angular/core';
import {By} from '@angular/platform-browser';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
Expand Down Expand Up @@ -422,6 +429,7 @@ describe('MatDrawerContainer', () => {
DrawerDelayed,
DrawerSetToOpenedTrue,
DrawerContainerStateChangesTestApp,
AutosizeDrawer,
],
});

Expand Down Expand Up @@ -523,6 +531,30 @@ describe('MatDrawerContainer', () => {
expect(container.classList).not.toContain('mat-drawer-transition');
}));

it('should recalculate the margin if a drawer changes size while open in autosize mode',
fakeAsync(() => {
const fixture = TestBed.createComponent(AutosizeDrawer);

fixture.detectChanges();
fixture.componentInstance.drawer.open();
fixture.detectChanges();
tick();
fixture.detectChanges();

const contentEl = fixture.debugElement.nativeElement.querySelector('.mat-drawer-content');
const initialMargin = parseInt(contentEl.style.marginLeft);

expect(initialMargin).toBeGreaterThan(0);

fixture.componentInstance.fillerWidth = 200;
fixture.detectChanges();
tick(10);
fixture.detectChanges();

expect(parseInt(contentEl.style.marginLeft)).toBeGreaterThan(initialMargin);
discardPeriodicTasks();
}));

});


Expand Down Expand Up @@ -676,3 +708,17 @@ class DrawerContainerStateChangesTestApp {
renderDrawer = true;
}


@Component({
template: `
<mat-drawer-container autosize>
<mat-drawer mode="push" [position]="drawer1Position">
Text
<div [style.width.px]="fillerWidth"></div>
</mat-drawer>
</mat-drawer-container>`,
})
class AutosizeDrawer {
@ViewChild(MatDrawer) drawer: MatDrawer;
fillerWidth = 0;
}
64 changes: 59 additions & 5 deletions src/lib/sidenav/drawer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ import {
QueryList,
Renderer2,
ViewEncapsulation,
InjectionToken,
} from '@angular/core';
import {DOCUMENT} from '@angular/platform-browser';
import {merge} from 'rxjs/observable/merge';
import {filter} from 'rxjs/operators/filter';
import {first} from 'rxjs/operators/first';
import {startWith} from 'rxjs/operators/startWith';
import {takeUntil} from 'rxjs/operators/takeUntil';
import {debounceTime} from 'rxjs/operators/debounceTime';
import {map} from 'rxjs/operators/map';
import {Subject} from 'rxjs/Subject';
import {Observable} from 'rxjs/Observable';
Expand All @@ -55,6 +57,9 @@ export class MatDrawerToggleResult {
constructor(public type: 'open' | 'close', public animationFinished: boolean) {}
}

/** Configures whether drawers should use auto sizing by default. */
export const MAT_DRAWER_DEFAULT_AUTOSIZE =
new InjectionToken<boolean>('MAT_DRAWER_DEFAULT_AUTOSIZE');

@Component({
moduleId: module.id,
Expand Down Expand Up @@ -404,7 +409,6 @@ export class MatDrawer implements AfterContentInit, OnDestroy {
})
export class MatDrawerContainer implements AfterContentInit, OnDestroy {
@ContentChildren(MatDrawer) _drawers: QueryList<MatDrawer>;

@ContentChild(MatDrawerContent) _content: MatDrawerContent;

/** The drawer child with the `start` position. */
Expand All @@ -413,6 +417,19 @@ export class MatDrawerContainer implements AfterContentInit, OnDestroy {
/** The drawer child with the `end` position. */
get end(): MatDrawer | null { return this._end; }

/**
* Whether to automatically resize the container whenever
* the size of any of its drawers changes.
*
* **Use at your own risk!** Enabling this option can cause layout thrashing by measuring
* the drawers on every change detection cycle. Can be configured globally via the
* `MAT_DRAWER_DEFAULT_AUTOSIZE` token.
*/
@Input()
get autosize(): boolean { return this._autosize; }
set autosize(value: boolean) { this._autosize = coerceBooleanProperty(value); }
private _autosize: boolean;

/** Event emitted when the drawer backdrop is clicked. */
@Output() backdropClick = new EventEmitter<void>();

Expand All @@ -432,16 +449,27 @@ export class MatDrawerContainer implements AfterContentInit, OnDestroy {
/** Emits when the component is destroyed. */
private _destroyed = new Subject<void>();

/** Cached margins used to verify that the values have changed. */
private _margins = {left: 0, right: 0};

/** Emits on every ngDoCheck. Used for debouncing reflows. */
private _doCheckSubject = new Subject<void>();

_contentMargins = new Subject<{left: number, right: number}>();

constructor(@Optional() private _dir: Directionality, private _element: ElementRef,
private _renderer: Renderer2, private _ngZone: NgZone,
private _changeDetectorRef: ChangeDetectorRef) {
constructor(@Optional() private _dir: Directionality,
private _element: ElementRef,
private _renderer: Renderer2,
private _ngZone: NgZone,
private _changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DRAWER_DEFAULT_AUTOSIZE) defaultAutosize = false) {
// 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.pipe(takeUntil(this._destroyed)).subscribe(() => this._validateDrawers());
}

this._autosize = defaultAutosize;
}

ngAfterContentInit() {
Expand All @@ -462,9 +490,15 @@ export class MatDrawerContainer implements AfterContentInit, OnDestroy {

this._changeDetectorRef.markForCheck();
});

this._doCheckSubject.pipe(
debounceTime(10), // Arbitrary debounce time, less than a frame at 60fps
takeUntil(this._destroyed)
).subscribe(() => this._updateContentMargins());
}

ngOnDestroy() {
this._doCheckSubject.complete();
this._destroyed.next();
this._destroyed.complete();
}
Expand All @@ -479,6 +513,14 @@ export class MatDrawerContainer implements AfterContentInit, OnDestroy {
this._drawers.forEach(drawer => drawer.close());
}

ngDoCheck() {
// If users opted into autosizing, do a check every change detection cycle.
if (this._autosize && this._isPushed()) {
// Run outside the NgZone, otherwise the debouncer will throw us into an infinite loop.
this._ngZone.runOutsideAngular(() => this._doCheckSubject.next());
}
}

/**
* 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
Expand Down Expand Up @@ -574,6 +616,12 @@ export class MatDrawerContainer implements AfterContentInit, OnDestroy {
}
}

/** Whether the container is being pushed to the side by one of the drawers. */
private _isPushed() {
return (this._isDrawerOpen(this._start) && this._start!.mode != 'over') ||
(this._isDrawerOpen(this._end) && this._end!.mode != 'over');
}

_onBackdropClicked() {
this.backdropClick.emit();
this._closeModalDrawer();
Expand Down Expand Up @@ -630,6 +678,12 @@ export class MatDrawerContainer implements AfterContentInit, OnDestroy {
}
}

this._contentMargins.next({left, right});
if (left !== this._margins.left || right !== this._margins.right) {
this._margins.left = left;
this._margins.right = right;

// Pull back into the NgZone since in some cases we could be outside.
this._ngZone.run(() => this._contentMargins.next(this._margins));
}
}
}
10 changes: 9 additions & 1 deletion src/lib/sidenav/sidenav-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
import {MatCommonModule} from '@angular/material/core';
import {ScrollDispatchModule} from '@angular/cdk/scrolling';
import {MatDrawer, MatDrawerContainer, MatDrawerContent} from './drawer';
import {MatSidenav, MatSidenavContainer, MatSidenavContent} from './sidenav';
import {
MatDrawer,
MatDrawerContainer,
MatDrawerContent,
MAT_DRAWER_DEFAULT_AUTOSIZE,
} from './drawer';


@NgModule({
Expand Down Expand Up @@ -41,5 +46,8 @@ import {MatSidenav, MatSidenavContainer, MatSidenavContent} from './sidenav';
MatSidenavContainer,
MatSidenavContent,
],
providers: [
{provide: MAT_DRAWER_DEFAULT_AUTOSIZE, useValue: false}
]
})
export class MatSidenavModule {}
16 changes: 13 additions & 3 deletions src/lib/sidenav/sidenav.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The sidenav and its associated content live inside of an `<mat-sidenav-container
</mat-sidenav-container>
```

A sidenav container may contain one or two `<mat-sidenav>` elements. When there are two
A sidenav container may contain one or two `<mat-sidenav>` elements. When there are two
`<mat-sidenav>` elements, each must be placed on a different side of the container.
See the section on positioning below.

Expand Down Expand Up @@ -68,8 +68,8 @@ html, body, material-app, mat-sidenav-container, .my-content {
```

### FABs inside sidenav
For a sidenav with a FAB (Floating Action Button) or other floating element, the recommended approach is to place the FAB
outside of the scrollable region and absolutely position it.
For a sidenav with a FAB (Floating Action Button) or other floating element, the recommended
approach is to place the FAB outside of the scrollable region and absolutely position it.


### Disabling closing of sidenav
Expand All @@ -82,3 +82,13 @@ is clicked or <kbd>ESCAPE</kbd> is pressed. To add custom logic on backdrop clic
<mat-sidenav disableClose (keydown)="customKeydownHandler($event)"></mat-sidenav>
</mat-sidenav-container>
```

### Resizing an open sidenav
By default, Material will only measure and resize the drawer container in a few key moments
(on open, on window resize, on mode change) in order to avoid layout thrashing, however there
are cases where this can be problematic. If your app requires for a drawer to change its width
while it is open, you can use the `autosize` option to tell Material to continue measuring it.
Note that you should use this option **at your own risk**, because it could cause performance
issues.

<!-- example(sidenav-autosize) -->
8 changes: 8 additions & 0 deletions src/material-examples/example-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ import {SelectResetExample} from './select-reset/select-reset-example';
import {SelectValueBindingExample} from './select-value-binding/select-value-binding-example';
import {SidenavFabExample} from './sidenav-fab/sidenav-fab-example';
import {SidenavOverviewExample} from './sidenav-overview/sidenav-overview-example';
import {SidenavAutosizeExample} from './sidenav-autosize/sidenav-autosize-example';
import {SlideToggleConfigurableExample} from './slide-toggle-configurable/slide-toggle-configurable-example';
import {SlideToggleFormsExample} from './slide-toggle-forms/slide-toggle-forms-example';
import {SlideToggleOverviewExample} from './slide-toggle-overview/slide-toggle-overview-example';
Expand Down Expand Up @@ -631,6 +632,12 @@ export const EXAMPLE_COMPONENTS = {
additionalFiles: null,
selectorName: null
},
'sidenav-autosize': {
title: 'Autosize sidenav',
component: SidenavAutosizeExample,
additionalFiles: null,
selectorName: null
},
'slide-toggle-configurable': {
title: 'Configurable slide-toggle',
component: SlideToggleConfigurableExample,
Expand Down Expand Up @@ -845,6 +852,7 @@ export const EXAMPLE_LIST = [
SelectValueBindingExample,
SidenavFabExample,
SidenavOverviewExample,
SidenavAutosizeExample,
SlideToggleConfigurableExample,
SlideToggleFormsExample,
SlideToggleOverviewExample,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.example-container {
width: 500px;
height: 300px;
border: 1px solid rgba(0, 0, 0, 0.5);
}

.example-sidenav-content {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
}

.example-sidenav {
padding: 20px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<mat-drawer-container class="example-container" autosize>
<mat-drawer #drawer class="example-sidenav" mode="side">
<p>Auto-resizing sidenav</p>
<p *ngIf="showFiller">Lorem, ipsum dolor sit amet consectetur.</p>
<button (click)="showFiller = !showFiller" mat-raised-button>
Toggle extra text
</button>
</mat-drawer>

<div class="example-sidenav-content">
<button type="button" mat-button (click)="drawer.toggle()">
Toggle sidenav
</button>
</div>

</mat-drawer-container>
13 changes: 13 additions & 0 deletions src/material-examples/sidenav-autosize/sidenav-autosize-example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {Component} from '@angular/core';

/**
* @title Autosize sidenav
*/
@Component({
selector: 'sidenav-autosize-example',
templateUrl: 'sidenav-autosize-example.html',
styleUrls: ['sidenav-autosize-example.css'],
})
export class SidenavAutosizeExample {
showFiller = false;
}

0 comments on commit 08fa7d0

Please sign in to comment.