From 8947aec487c26ed129cb7dc5c1dac75f6d727076 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Thu, 16 Nov 2017 21:20:30 +0100 Subject: [PATCH] fix(drawer): allow for drawer container to auto-resize while open 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. --- src/lib/sidenav/drawer.spec.ts | 48 +++++++++++++- src/lib/sidenav/drawer.ts | 64 +++++++++++++++++-- src/lib/sidenav/sidenav-module.ts | 10 ++- src/lib/sidenav/sidenav.md | 16 ++++- src/material-examples/example-module.ts | 8 +++ .../sidenav-autosize-example.css | 16 +++++ .../sidenav-autosize-example.html | 16 +++++ .../sidenav-autosize-example.ts | 13 ++++ 8 files changed, 181 insertions(+), 10 deletions(-) create mode 100644 src/material-examples/sidenav-autosize/sidenav-autosize-example.css create mode 100644 src/material-examples/sidenav-autosize/sidenav-autosize-example.html create mode 100644 src/material-examples/sidenav-autosize/sidenav-autosize-example.ts diff --git a/src/lib/sidenav/drawer.spec.ts b/src/lib/sidenav/drawer.spec.ts index a6ac276b7227..49db2625de7c 100644 --- a/src/lib/sidenav/drawer.spec.ts +++ b/src/lib/sidenav/drawer.spec.ts @@ -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: ` + + + Text +
+
+
`, +}) +class AutosizeDrawer { + @ViewChild(MatDrawer) drawer: MatDrawer; + fillerWidth = 0; +} diff --git a/src/lib/sidenav/drawer.ts b/src/lib/sidenav/drawer.ts index 6a9b4410b062..5e17417614a3 100644 --- a/src/lib/sidenav/drawer.ts +++ b/src/lib/sidenav/drawer.ts @@ -29,6 +29,7 @@ import { QueryList, Renderer2, ViewEncapsulation, + InjectionToken, } from '@angular/core'; import {DOCUMENT} from '@angular/platform-browser'; import {merge} from 'rxjs/observable/merge'; @@ -36,6 +37,7 @@ 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'; @@ -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('MAT_DRAWER_DEFAULT_AUTOSIZE'); @Component({ moduleId: module.id, @@ -404,7 +409,6 @@ export class MatDrawer implements AfterContentInit, OnDestroy { }) export class MatDrawerContainer implements AfterContentInit, OnDestroy { @ContentChildren(MatDrawer) _drawers: QueryList; - @ContentChild(MatDrawerContent) _content: MatDrawerContent; /** The drawer child with the `start` position. */ @@ -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(); @@ -432,16 +449,27 @@ export class MatDrawerContainer implements AfterContentInit, OnDestroy { /** Emits when the component is destroyed. */ private _destroyed = new Subject(); + /** 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(); + _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() { @@ -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(); } @@ -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 @@ -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(); @@ -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)); + } } } diff --git a/src/lib/sidenav/sidenav-module.ts b/src/lib/sidenav/sidenav-module.ts index 49e81a3db878..a77029c3a47d 100644 --- a/src/lib/sidenav/sidenav-module.ts +++ b/src/lib/sidenav/sidenav-module.ts @@ -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 {} diff --git a/src/lib/sidenav/sidenav.md b/src/lib/sidenav/sidenav.md index cdbcaff0baa8..cade01836846 100644 --- a/src/lib/sidenav/sidenav.md +++ b/src/lib/sidenav/sidenav.md @@ -14,7 +14,7 @@ The sidenav and its associated content live inside of an ` ``` -A sidenav container may contain one or two `` elements. When there are two +A sidenav container may contain one or two `` elements. When there are two `` elements, each must be placed on a different side of the container. See the section on positioning below. @@ -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 @@ -82,3 +82,13 @@ is clicked or ESCAPE is pressed. To add custom logic on backdrop clic ``` + +### 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. + + diff --git a/src/material-examples/example-module.ts b/src/material-examples/example-module.ts index 1fd0719e1055..73d2a9fdd5ce 100644 --- a/src/material-examples/example-module.ts +++ b/src/material-examples/example-module.ts @@ -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'; @@ -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, @@ -845,6 +852,7 @@ export const EXAMPLE_LIST = [ SelectValueBindingExample, SidenavFabExample, SidenavOverviewExample, + SidenavAutosizeExample, SlideToggleConfigurableExample, SlideToggleFormsExample, SlideToggleOverviewExample, diff --git a/src/material-examples/sidenav-autosize/sidenav-autosize-example.css b/src/material-examples/sidenav-autosize/sidenav-autosize-example.css new file mode 100644 index 000000000000..8ab00588bb34 --- /dev/null +++ b/src/material-examples/sidenav-autosize/sidenav-autosize-example.css @@ -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; +} diff --git a/src/material-examples/sidenav-autosize/sidenav-autosize-example.html b/src/material-examples/sidenav-autosize/sidenav-autosize-example.html new file mode 100644 index 000000000000..9278475e5e78 --- /dev/null +++ b/src/material-examples/sidenav-autosize/sidenav-autosize-example.html @@ -0,0 +1,16 @@ + + +

Auto-resizing sidenav

+

Lorem, ipsum dolor sit amet consectetur.

+ +
+ +
+ +
+ +
diff --git a/src/material-examples/sidenav-autosize/sidenav-autosize-example.ts b/src/material-examples/sidenav-autosize/sidenav-autosize-example.ts new file mode 100644 index 000000000000..f7565c094e0e --- /dev/null +++ b/src/material-examples/sidenav-autosize/sidenav-autosize-example.ts @@ -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; +}