Skip to content

Commit e7b412a

Browse files
crisbetommalerba
authored andcommitted
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.
1 parent 8f7c7cf commit e7b412a

File tree

8 files changed

+173
-10
lines changed

8 files changed

+173
-10
lines changed

src/lib/sidenav/drawer.spec.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import {fakeAsync, async, tick, ComponentFixture, TestBed} from '@angular/core/testing';
1+
import {
2+
fakeAsync,
3+
async,
4+
tick,
5+
ComponentFixture,
6+
TestBed,
7+
discardPeriodicTasks,
8+
} from '@angular/core/testing';
29
import {Component, ElementRef, ViewChild} from '@angular/core';
310
import {By} from '@angular/platform-browser';
411
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
@@ -422,6 +429,7 @@ describe('MatDrawerContainer', () => {
422429
DrawerDelayed,
423430
DrawerSetToOpenedTrue,
424431
DrawerContainerStateChangesTestApp,
432+
AutosizeDrawer,
425433
],
426434
});
427435

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

534+
it('should recalculate the margin if a drawer changes size while open in autosize mode',
535+
fakeAsync(() => {
536+
const fixture = TestBed.createComponent(AutosizeDrawer);
537+
538+
fixture.detectChanges();
539+
fixture.componentInstance.drawer.open();
540+
fixture.detectChanges();
541+
tick();
542+
fixture.detectChanges();
543+
544+
const contentEl = fixture.debugElement.nativeElement.querySelector('.mat-drawer-content');
545+
const initialMargin = parseInt(contentEl.style.marginLeft);
546+
547+
expect(initialMargin).toBeGreaterThan(0);
548+
549+
fixture.componentInstance.fillerWidth = 200;
550+
fixture.detectChanges();
551+
tick(10);
552+
fixture.detectChanges();
553+
554+
expect(parseInt(contentEl.style.marginLeft)).toBeGreaterThan(initialMargin);
555+
discardPeriodicTasks();
556+
}));
557+
526558
});
527559

528560

@@ -676,3 +708,17 @@ class DrawerContainerStateChangesTestApp {
676708
renderDrawer = true;
677709
}
678710

711+
712+
@Component({
713+
template: `
714+
<mat-drawer-container autosize>
715+
<mat-drawer mode="push" [position]="drawer1Position">
716+
Text
717+
<div [style.width.px]="fillerWidth"></div>
718+
</mat-drawer>
719+
</mat-drawer-container>`,
720+
})
721+
class AutosizeDrawer {
722+
@ViewChild(MatDrawer) drawer: MatDrawer;
723+
fillerWidth = 0;
724+
}

src/lib/sidenav/drawer.ts

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@ import {
2828
Output,
2929
QueryList,
3030
ViewEncapsulation,
31+
InjectionToken,
3132
} from '@angular/core';
3233
import {DOCUMENT} from '@angular/common';
3334
import {merge} from 'rxjs/observable/merge';
3435
import {filter} from 'rxjs/operators/filter';
3536
import {take} from 'rxjs/operators/take';
3637
import {startWith} from 'rxjs/operators/startWith';
3738
import {takeUntil} from 'rxjs/operators/takeUntil';
39+
import {debounceTime} from 'rxjs/operators/debounceTime';
3840
import {map} from 'rxjs/operators/map';
3941
import {Subject} from 'rxjs/Subject';
4042
import {Observable} from 'rxjs/Observable';
@@ -54,6 +56,9 @@ export class MatDrawerToggleResult {
5456
constructor(public type: 'open' | 'close', public animationFinished: boolean) {}
5557
}
5658

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

5863
@Component({
5964
moduleId: module.id,
@@ -74,7 +79,7 @@ export class MatDrawerContent implements AfterContentInit {
7479
* drawer is open. We use margin rather than transform even for push mode because transform breaks
7580
* fixed position elements inside of the transformed element.
7681
*/
77-
_margins: {left: number, right: number} = {left: 0, right: 0};
82+
_margins: {left: number|null, right: number|null} = {left: null, right: null};
7883

7984
constructor(
8085
private _changeDetectorRef: ChangeDetectorRef,
@@ -403,7 +408,6 @@ export class MatDrawer implements AfterContentInit, OnDestroy {
403408
})
404409
export class MatDrawerContainer implements AfterContentInit, OnDestroy {
405410
@ContentChildren(MatDrawer) _drawers: QueryList<MatDrawer>;
406-
407411
@ContentChild(MatDrawerContent) _content: MatDrawerContent;
408412

409413
/** The drawer child with the `start` position. */
@@ -412,6 +416,19 @@ export class MatDrawerContainer implements AfterContentInit, OnDestroy {
412416
/** The drawer child with the `end` position. */
413417
get end(): MatDrawer | null { return this._end; }
414418

419+
/**
420+
* Whether to automatically resize the container whenever
421+
* the size of any of its drawers changes.
422+
*
423+
* **Use at your own risk!** Enabling this option can cause layout thrashing by measuring
424+
* the drawers on every change detection cycle. Can be configured globally via the
425+
* `MAT_DRAWER_DEFAULT_AUTOSIZE` token.
426+
*/
427+
@Input()
428+
get autosize(): boolean { return this._autosize; }
429+
set autosize(value: boolean) { this._autosize = coerceBooleanProperty(value); }
430+
private _autosize: boolean;
431+
415432
/** Event emitted when the drawer backdrop is clicked. */
416433
@Output() backdropClick = new EventEmitter<void>();
417434

@@ -431,15 +448,23 @@ export class MatDrawerContainer implements AfterContentInit, OnDestroy {
431448
/** Emits when the component is destroyed. */
432449
private _destroyed = new Subject<void>();
433450

434-
_contentMargins = new Subject<{left: number, right: number}>();
451+
/** Emits on every ngDoCheck. Used for debouncing reflows. */
452+
private _doCheckSubject = new Subject<void>();
453+
454+
_contentMargins = new Subject<{left: number|null, right: number|null}>();
435455

436-
constructor(@Optional() private _dir: Directionality, private _element: ElementRef,
437-
private _ngZone: NgZone, private _changeDetectorRef: ChangeDetectorRef) {
456+
constructor(@Optional() private _dir: Directionality,
457+
private _element: ElementRef,
458+
private _ngZone: NgZone,
459+
private _changeDetectorRef: ChangeDetectorRef,
460+
@Inject(MAT_DRAWER_DEFAULT_AUTOSIZE) defaultAutosize = false) {
438461
// If a `Dir` directive exists up the tree, listen direction changes and update the left/right
439462
// properties to point to the proper start/end.
440463
if (_dir != null) {
441464
_dir.change.pipe(takeUntil(this._destroyed)).subscribe(() => this._validateDrawers());
442465
}
466+
467+
this._autosize = defaultAutosize;
443468
}
444469

445470
ngAfterContentInit() {
@@ -460,9 +485,15 @@ export class MatDrawerContainer implements AfterContentInit, OnDestroy {
460485

461486
this._changeDetectorRef.markForCheck();
462487
});
488+
489+
this._doCheckSubject.pipe(
490+
debounceTime(10), // Arbitrary debounce time, less than a frame at 60fps
491+
takeUntil(this._destroyed)
492+
).subscribe(() => this._updateContentMargins());
463493
}
464494

465495
ngOnDestroy() {
496+
this._doCheckSubject.complete();
466497
this._destroyed.next();
467498
this._destroyed.complete();
468499
}
@@ -477,6 +508,14 @@ export class MatDrawerContainer implements AfterContentInit, OnDestroy {
477508
this._drawers.forEach(drawer => drawer.close());
478509
}
479510

511+
ngDoCheck() {
512+
// If users opted into autosizing, do a check every change detection cycle.
513+
if (this._autosize && this._isPushed()) {
514+
// Run outside the NgZone, otherwise the debouncer will throw us into an infinite loop.
515+
this._ngZone.runOutsideAngular(() => this._doCheckSubject.next());
516+
}
517+
}
518+
480519
/**
481520
* Subscribes to drawer events in order to set a class on the main container element when the
482521
* 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 {
572611
}
573612
}
574613

614+
/** Whether the container is being pushed to the side by one of the drawers. */
615+
private _isPushed() {
616+
return (this._isDrawerOpen(this._start) && this._start!.mode != 'over') ||
617+
(this._isDrawerOpen(this._end) && this._end!.mode != 'over');
618+
}
619+
575620
_onBackdropClicked() {
576621
this.backdropClick.emit();
577622
this._closeModalDrawer();
@@ -628,6 +673,7 @@ export class MatDrawerContainer implements AfterContentInit, OnDestroy {
628673
}
629674
}
630675

631-
this._contentMargins.next({left, right});
676+
// Pull back into the NgZone since in some cases we could be outside.
677+
this._ngZone.run(() => this._contentMargins.next({left, right}));
632678
}
633679
}

src/lib/sidenav/sidenav-module.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,13 @@ import {CommonModule} from '@angular/common';
1212
import {NgModule} from '@angular/core';
1313
import {MatCommonModule} from '@angular/material/core';
1414
import {ScrollDispatchModule} from '@angular/cdk/scrolling';
15-
import {MatDrawer, MatDrawerContainer, MatDrawerContent} from './drawer';
1615
import {MatSidenav, MatSidenavContainer, MatSidenavContent} from './sidenav';
16+
import {
17+
MatDrawer,
18+
MatDrawerContainer,
19+
MatDrawerContent,
20+
MAT_DRAWER_DEFAULT_AUTOSIZE,
21+
} from './drawer';
1722

1823

1924
@NgModule({
@@ -41,5 +46,8 @@ import {MatSidenav, MatSidenavContainer, MatSidenavContent} from './sidenav';
4146
MatSidenavContainer,
4247
MatSidenavContent,
4348
],
49+
providers: [
50+
{provide: MAT_DRAWER_DEFAULT_AUTOSIZE, useValue: false}
51+
]
4452
})
4553
export class MatSidenavModule {}

src/lib/sidenav/sidenav.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,16 @@ Custom handling for backdrop clicks can be done via the `(backdropClick)` output
131131

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

134+
### Resizing an open sidenav
135+
By default, Material will only measure and resize the drawer container in a few key moments
136+
(on open, on window resize, on mode change) in order to avoid layout thrashing, however there
137+
are cases where this can be problematic. If your app requires for a drawer to change its width
138+
while it is open, you can use the `autosize` option to tell Material to continue measuring it.
139+
Note that you should use this option **at your own risk**, because it could cause performance
140+
issues.
141+
142+
<!-- example(sidenav-autosize) -->
143+
134144
### Setting the sidenav's size
135145

136146
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.
158168
A sidenav often needs to behave differently on a mobile vs a desktop display. On a desktop, it may
159169
make sense to have just the content section scroll. However, on mobile you often want the body to be
160170
the element that scrolls; this allows the address bar to auto-hide. The sidenav can be styled with
161-
CSS to adjust to either type of device.
171+
CSS to adjust to either type of device.
162172

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

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

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

179189
### Troubleshooting
180190

src/material-examples/example-module.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ import {SidenavOpenCloseExample} from './sidenav-open-close/sidenav-open-close-e
104104
import {SidenavOverviewExample} from './sidenav-overview/sidenav-overview-example';
105105
import {SidenavPositionExample} from './sidenav-position/sidenav-position-example';
106106
import {SidenavResponsiveExample} from './sidenav-responsive/sidenav-responsive-example';
107+
import {SidenavAutosizeExample} from './sidenav-autosize/sidenav-autosize-example';
107108
import {SlideToggleConfigurableExample} from './slide-toggle-configurable/slide-toggle-configurable-example';
108109
import {SlideToggleFormsExample} from './slide-toggle-forms/slide-toggle-forms-example';
109110
import {SlideToggleOverviewExample} from './slide-toggle-overview/slide-toggle-overview-example';
@@ -674,6 +675,12 @@ export const EXAMPLE_COMPONENTS = {
674675
additionalFiles: null,
675676
selectorName: null
676677
},
678+
'sidenav-autosize': {
679+
title: 'Autosize sidenav',
680+
component: SidenavAutosizeExample,
681+
additionalFiles: null,
682+
selectorName: null
683+
},
677684
'slide-toggle-configurable': {
678685
title: 'Configurable slide-toggle',
679686
component: SlideToggleConfigurableExample,
@@ -900,6 +907,7 @@ export const EXAMPLE_LIST = [
900907
SidenavOverviewExample,
901908
SidenavPositionExample,
902909
SidenavResponsiveExample,
910+
SidenavAutosizeExample,
903911
SlideToggleConfigurableExample,
904912
SlideToggleFormsExample,
905913
SlideToggleOverviewExample,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
.example-container {
2+
width: 500px;
3+
height: 300px;
4+
border: 1px solid rgba(0, 0, 0, 0.5);
5+
}
6+
7+
.example-sidenav-content {
8+
display: flex;
9+
height: 100%;
10+
align-items: center;
11+
justify-content: center;
12+
}
13+
14+
.example-sidenav {
15+
padding: 20px;
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<mat-drawer-container class="example-container" autosize>
2+
<mat-drawer #drawer class="example-sidenav" mode="side">
3+
<p>Auto-resizing sidenav</p>
4+
<p *ngIf="showFiller">Lorem, ipsum dolor sit amet consectetur.</p>
5+
<button (click)="showFiller = !showFiller" mat-raised-button>
6+
Toggle extra text
7+
</button>
8+
</mat-drawer>
9+
10+
<div class="example-sidenav-content">
11+
<button type="button" mat-button (click)="drawer.toggle()">
12+
Toggle sidenav
13+
</button>
14+
</div>
15+
16+
</mat-drawer-container>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {Component} from '@angular/core';
2+
3+
/**
4+
* @title Autosize sidenav
5+
*/
6+
@Component({
7+
selector: 'sidenav-autosize-example',
8+
templateUrl: 'sidenav-autosize-example.html',
9+
styleUrls: ['sidenav-autosize-example.css'],
10+
})
11+
export class SidenavAutosizeExample {
12+
showFiller = false;
13+
}

0 commit comments

Comments
 (0)