Skip to content

Commit 1c685f4

Browse files
committed
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.
1 parent 541a95e commit 1c685f4

File tree

4 files changed

+132
-10
lines changed

4 files changed

+132
-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: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@ import {
2929
QueryList,
3030
Renderer2,
3131
ViewEncapsulation,
32+
InjectionToken,
3233
} from '@angular/core';
3334
import {DOCUMENT} from '@angular/platform-browser';
3435
import {merge} from 'rxjs/observable/merge';
3536
import {filter} from 'rxjs/operators/filter';
3637
import {first} from 'rxjs/operators/first';
3738
import {startWith} from 'rxjs/operators/startWith';
3839
import {takeUntil} from 'rxjs/operators/takeUntil';
40+
import {debounceTime} from 'rxjs/operators/debounceTime';
3941
import {map} from 'rxjs/operators/map';
4042
import {Subject} from 'rxjs/Subject';
4143
import {Observable} from 'rxjs/Observable';
@@ -55,6 +57,9 @@ export class MatDrawerToggleResult {
5557
constructor(public type: 'open' | 'close', public animationFinished: boolean) {}
5658
}
5759

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

5964
@Component({
6065
moduleId: module.id,
@@ -404,7 +409,6 @@ export class MatDrawer implements AfterContentInit, OnDestroy {
404409
})
405410
export class MatDrawerContainer implements AfterContentInit, OnDestroy {
406411
@ContentChildren(MatDrawer) _drawers: QueryList<MatDrawer>;
407-
408412
@ContentChild(MatDrawerContent) _content: MatDrawerContent;
409413

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

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

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

452+
/** Cached margins used to verify that the values have changed. */
453+
private _margins = {left: 0, right: 0};
454+
455+
/** Emits on every ngDoCheck. Used for debouncing reflows. */
456+
private _doCheckSubject = new Subject<void>();
457+
435458
_contentMargins = new Subject<{left: number, right: number}>();
436459

437-
constructor(@Optional() private _dir: Directionality, private _element: ElementRef,
438-
private _renderer: Renderer2, private _ngZone: NgZone,
439-
private _changeDetectorRef: ChangeDetectorRef) {
460+
constructor(@Optional() private _dir: Directionality,
461+
private _element: ElementRef,
462+
private _renderer: Renderer2,
463+
private _ngZone: NgZone,
464+
private _changeDetectorRef: ChangeDetectorRef,
465+
@Inject(MAT_DRAWER_DEFAULT_AUTOSIZE) defaultAutosize = false) {
440466
// If a `Dir` directive exists up the tree, listen direction changes and update the left/right
441467
// properties to point to the proper start/end.
442468
if (_dir != null) {
443469
_dir.change.pipe(takeUntil(this._destroyed)).subscribe(() => this._validateDrawers());
444470
}
471+
472+
this._autosize = defaultAutosize;
445473
}
446474

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

463491
this._changeDetectorRef.markForCheck();
464492
});
493+
494+
this._doCheckSubject.pipe(
495+
debounceTime(10), // Arbitrary debounce time, less than a frame at 60fps
496+
takeUntil(this._destroyed)
497+
).subscribe(() => this._updateContentMargins());
465498
}
466499

467500
ngOnDestroy() {
501+
this._doCheckSubject.complete();
468502
this._destroyed.next();
469503
this._destroyed.complete();
470504
}
@@ -479,6 +513,14 @@ export class MatDrawerContainer implements AfterContentInit, OnDestroy {
479513
this._drawers.forEach(drawer => drawer.close());
480514
}
481515

516+
ngDoCheck() {
517+
// If users opted into autosizing, do a check every change detection cycle.
518+
if (this._autosize && this._isPushed()) {
519+
// Run outside the NgZone, otherwise the debouncer will throw us into an infinite loop.
520+
this._ngZone.runOutsideAngular(() => this._doCheckSubject.next());
521+
}
522+
}
523+
482524
/**
483525
* Subscribes to drawer events in order to set a class on the main container element when the
484526
* 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 {
574616
}
575617
}
576618

619+
/** Whether the container is being pushed to the side by one of the drawers. */
620+
private _isPushed() {
621+
return (this._isDrawerOpen(this._start) && this._start!.mode != 'over') ||
622+
(this._isDrawerOpen(this._end) && this._end!.mode != 'over');
623+
}
624+
577625
_onBackdropClicked() {
578626
this.backdropClick.emit();
579627
this._closeModalDrawer();
@@ -630,6 +678,12 @@ export class MatDrawerContainer implements AfterContentInit, OnDestroy {
630678
}
631679
}
632680

633-
this._contentMargins.next({left, right});
681+
if (left !== this._margins.left || right !== this._margins.right) {
682+
this._margins.left = left;
683+
this._margins.right = right;
684+
685+
// Pull back into the NgZone since in some cases we could be outside.
686+
this._ngZone.run(() => this._contentMargins.next(this._margins));
687+
}
634688
}
635689
}

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: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ The sidenav and its associated content live inside of an `<mat-sidenav-container
1414
</mat-sidenav-container>
1515
```
1616

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

@@ -68,8 +68,8 @@ html, body, material-app, mat-sidenav-container, .my-content {
6868
```
6969

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

7474

7575
### Disabling closing of sidenav
@@ -82,3 +82,17 @@ is clicked or <kbd>ESCAPE</kbd> is pressed. To add custom logic on backdrop clic
8282
<mat-sidenav disableClose (keydown)="customKeydownHandler($event)"></mat-sidenav>
8383
</mat-sidenav-container>
8484
```
85+
86+
### Resizing an open sidenav
87+
By default, Material will only measure and resize the drawer container in a few key moments
88+
(on open, on window resize, on mode change) in order to avoid layout thrashing, however there
89+
are cases where this can be problematic. If your app requires for a drawer to change its width
90+
while it is open, you can use the `autosize` option to tell Material to continue measuring it.
91+
Note that you should use this option **at your own risk**, because it could cause performance
92+
issues.
93+
94+
```html
95+
<mat-sidenav-container [autosize]="true">
96+
<mat-sidenav></mat-sidenav>
97+
</mat-sidenav-container>
98+
```

0 commit comments

Comments
 (0)