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

Adds the `autosize` input that allows users to opt-in to drawers that auto-resize, similarly to the behavior before #6189. The behavior is off by default, because it's unnecessary for most use cases and can cause performance issues.

Fixes #6743.
This commit is contained in:
Kristiyan Kostadinov
2017-12-05 06:29:35 +01:00
committed by mmalerba
parent 8f7c7cf482
commit e7b412a6bf
8 changed files with 173 additions and 10 deletions
+47 -1
View File
@@ -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';
@@ -422,6 +429,7 @@ describe('MatDrawerContainer', () => {
DrawerDelayed,
DrawerSetToOpenedTrue,
DrawerContainerStateChangesTestApp,
AutosizeDrawer,
],
});
@@ -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();
}));
});
@@ -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;
}
+52 -6
View File
@@ -28,6 +28,7 @@ import {
Output,
QueryList,
ViewEncapsulation,
InjectionToken,
} from '@angular/core';
import {DOCUMENT} from '@angular/common';
import {merge} from 'rxjs/observable/merge';
@@ -35,6 +36,7 @@ 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';
@@ -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,
@@ -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,
@@ -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. */
@@ -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>();
@@ -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>();
constructor(@Optional() private _dir: Directionality, private _element: ElementRef,
private _ngZone: NgZone, private _changeDetectorRef: ChangeDetectorRef) {
_contentMargins = new Subject<{left: number|null, right: number|null}>();
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() {
@@ -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();
}
@@ -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
@@ -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();
@@ -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}));
}
}
+9 -1
View File
@@ -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({
@@ -41,5 +46,8 @@ import {MatSidenav, MatSidenavContainer, MatSidenavContent} from './sidenav';
MatSidenavContainer,
MatSidenavContent,
],
providers: [
{provide: MAT_DRAWER_DEFAULT_AUTOSIZE, useValue: false}
]
})
export class MatSidenavModule {}
+12 -2
View File
@@ -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
@@ -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) -->
@@ -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
+8
View File
@@ -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';
@@ -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,
@@ -900,6 +907,7 @@ export const EXAMPLE_LIST = [
SidenavOverviewExample,
SidenavPositionExample,
SidenavResponsiveExample,
SidenavAutosizeExample,
SlideToggleConfigurableExample,
SlideToggleFormsExample,
SlideToggleOverviewExample,
@@ -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;
}
@@ -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>
@@ -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;
}