Skip to content

fix(drawer): allow for drawer container to auto-resize while open #8488

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
58 changes: 52 additions & 6 deletions src/lib/sidenav/drawer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ import {
Output,
QueryList,
ViewEncapsulation,
InjectionToken,
} from '@angular/core';
import {DOCUMENT} from '@angular/common';
import {merge} from 'rxjs/observable/merge';
import {filter} from 'rxjs/operators/filter';
import {take} from 'rxjs/operators/take';
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 @@ -54,6 +56,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 All @@ -74,7 +79,7 @@ export class MatDrawerContent implements AfterContentInit {
* drawer is open. We use margin rather than transform even for push mode because transform breaks
* fixed position elements inside of the transformed element.
*/
_margins: {left: number, right: number} = {left: 0, right: 0};
_margins: {left: number|null, right: number|null} = {left: null, right: null};

constructor(
private _changeDetectorRef: ChangeDetectorRef,
Expand Down Expand Up @@ -403,7 +408,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 @@ -412,6 +416,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 @@ -431,15 +448,23 @@ export class MatDrawerContainer implements AfterContentInit, OnDestroy {
/** Emits when the component is destroyed. */
private _destroyed = new Subject<void>();

_contentMargins = new Subject<{left: number, right: number}>();
/** Emits on every ngDoCheck. Used for debouncing reflows. */
private _doCheckSubject = new Subject<void>();

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

constructor(@Optional() private _dir: Directionality, private _element: ElementRef,
private _ngZone: NgZone, private _changeDetectorRef: ChangeDetectorRef) {
constructor(@Optional() private _dir: Directionality,
private _element: ElementRef,
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 @@ -460,9 +485,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 @@ -477,6 +508,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 @@ -572,6 +611,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 @@ -628,6 +673,7 @@ export class MatDrawerContainer implements AfterContentInit, OnDestroy {
}
}

this._contentMargins.next({left, right});
// Pull back into the NgZone since in some cases we could be outside.
this._ngZone.run(() => this._contentMargins.next({left, right}));
}
}
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 {}
14 changes: 12 additions & 2 deletions src/lib/sidenav/sidenav.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,16 @@ Custom handling for backdrop clicks can be done via the `(backdropClick)` output

<!-- example(sidenav-disable-close) -->

### 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) -->

### Setting the sidenav's size

The `<mat-sidenav>` and `<mat-drawer>` will, by default, fit the size of its content. The width can
Expand Down Expand Up @@ -158,7 +168,7 @@ the top or bottom.
A sidenav often needs to behave differently on a mobile vs a desktop display. On a desktop, it may
make sense to have just the content section scroll. However, on mobile you often want the body to be
the element that scrolls; this allows the address bar to auto-hide. The sidenav can be styled with
CSS to adjust to either type of device.
CSS to adjust to either type of device.

<!-- example(sidenav-responsive) -->

Expand All @@ -174,7 +184,7 @@ describes your sidenav, `role="region"` is recommended.

Similarly, the `<mat-sidenav-content>` should be given a role based on what it contains. If it
represents the primary content of the page, it may make sense to mark it `role="main"`. If no more
specific role makes sense, `role="region"` is again a good fallback.
specific role makes sense, `role="region"` is again a good fallback.

### Troubleshooting

Expand Down
8 changes: 8 additions & 0 deletions src/material-examples/example-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ import {SidenavOpenCloseExample} from './sidenav-open-close/sidenav-open-close-e
import {SidenavOverviewExample} from './sidenav-overview/sidenav-overview-example';
import {SidenavPositionExample} from './sidenav-position/sidenav-position-example';
import {SidenavResponsiveExample} from './sidenav-responsive/sidenav-responsive-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 @@ -674,6 +675,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 @@ -900,6 +907,7 @@ export const EXAMPLE_LIST = [
SidenavOverviewExample,
SidenavPositionExample,
SidenavResponsiveExample,
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;
}