mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
mmalerba
parent
8f7c7cf482
commit
e7b412a6bf
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user