diff --git a/src/material/sidenav/drawer.html b/src/material/sidenav/drawer.html
index 9e7cb52395ba..5743adea2ce9 100644
--- a/src/material/sidenav/drawer.html
+++ b/src/material/sidenav/drawer.html
@@ -1,3 +1,3 @@
-
+
diff --git a/src/material/sidenav/drawer.spec.ts b/src/material/sidenav/drawer.spec.ts
index 5417122de04e..8348b357a352 100644
--- a/src/material/sidenav/drawer.spec.ts
+++ b/src/material/sidenav/drawer.spec.ts
@@ -661,6 +661,144 @@ describe('MatDrawer', () => {
expect(scrollable).toBeTruthy();
expect(scrollable.getElementRef().nativeElement).toBe(content.nativeElement);
});
+
+ describe('DOM position', () => {
+ it('should project start drawer before the content', () => {
+ const fixture = TestBed.createComponent(BasicTestApp);
+ fixture.componentInstance.position = 'start';
+ fixture.detectChanges();
+
+ const allNodes = getDrawerNodesArray(fixture);
+ const drawerIndex = allNodes.indexOf(fixture.nativeElement.querySelector('.mat-drawer'));
+ const contentIndex = allNodes.indexOf(
+ fixture.nativeElement.querySelector('.mat-drawer-content'),
+ );
+
+ expect(drawerIndex)
+ .withContext('Expected drawer to be inside the container')
+ .toBeGreaterThan(-1);
+ expect(contentIndex)
+ .withContext('Expected content to be inside the container')
+ .toBeGreaterThan(-1);
+ expect(drawerIndex)
+ .withContext('Expected drawer to be before the content')
+ .toBeLessThan(contentIndex);
+ });
+
+ it('should project end drawer after the content', () => {
+ const fixture = TestBed.createComponent(BasicTestApp);
+ fixture.componentInstance.position = 'end';
+ fixture.detectChanges();
+
+ const allNodes = getDrawerNodesArray(fixture);
+ const drawerIndex = allNodes.indexOf(fixture.nativeElement.querySelector('.mat-drawer'));
+ const contentIndex = allNodes.indexOf(
+ fixture.nativeElement.querySelector('.mat-drawer-content'),
+ );
+
+ expect(drawerIndex)
+ .withContext('Expected drawer to be inside the container')
+ .toBeGreaterThan(-1);
+ expect(contentIndex)
+ .withContext('Expected content to be inside the container')
+ .toBeGreaterThan(-1);
+ expect(drawerIndex)
+ .withContext('Expected drawer to be after the content')
+ .toBeGreaterThan(contentIndex);
+ });
+
+ it(
+ 'should move the drawer before/after the content when its position changes after being ' +
+ 'initialized at `start`',
+ () => {
+ const fixture = TestBed.createComponent(BasicTestApp);
+ fixture.componentInstance.position = 'start';
+ fixture.detectChanges();
+
+ const drawer = fixture.nativeElement.querySelector('.mat-drawer');
+ const content = fixture.nativeElement.querySelector('.mat-drawer-content');
+
+ let allNodes = getDrawerNodesArray(fixture);
+ const startDrawerIndex = allNodes.indexOf(drawer);
+ const startContentIndex = allNodes.indexOf(content);
+
+ expect(startDrawerIndex)
+ .withContext('Expected drawer to be inside the container')
+ .toBeGreaterThan(-1);
+ expect(startContentIndex)
+ .withContext('Expected content to be inside the container')
+ .toBeGreaterThan(-1);
+ expect(startDrawerIndex)
+ .withContext('Expected drawer to be before the content on init')
+ .toBeLessThan(startContentIndex);
+
+ fixture.componentInstance.position = 'end';
+ fixture.detectChanges();
+ allNodes = getDrawerNodesArray(fixture);
+
+ expect(allNodes.indexOf(drawer))
+ .withContext('Expected drawer to be after content when position changes to `end`')
+ .toBeGreaterThan(allNodes.indexOf(content));
+
+ fixture.componentInstance.position = 'start';
+ fixture.detectChanges();
+ allNodes = getDrawerNodesArray(fixture);
+
+ expect(allNodes.indexOf(drawer))
+ .withContext('Expected drawer to be before content when position changes back to `start`')
+ .toBeLessThan(allNodes.indexOf(content));
+ },
+ );
+
+ it(
+ 'should move the drawer before/after the content when its position changes after being ' +
+ 'initialized at `end`',
+ () => {
+ const fixture = TestBed.createComponent(BasicTestApp);
+ fixture.componentInstance.position = 'end';
+ fixture.detectChanges();
+
+ const drawer = fixture.nativeElement.querySelector('.mat-drawer');
+ const content = fixture.nativeElement.querySelector('.mat-drawer-content');
+
+ let allNodes = getDrawerNodesArray(fixture);
+ const startDrawerIndex = allNodes.indexOf(drawer);
+ const startContentIndex = allNodes.indexOf(content);
+
+ expect(startDrawerIndex).toBeGreaterThan(-1, 'Expected drawer to be inside the container');
+ expect(startContentIndex).toBeGreaterThan(
+ -1,
+ 'Expected content to be inside the container',
+ );
+ expect(startDrawerIndex).toBeGreaterThan(
+ startContentIndex,
+ 'Expected drawer to be after the content on init',
+ );
+
+ fixture.componentInstance.position = 'start';
+ fixture.detectChanges();
+ allNodes = getDrawerNodesArray(fixture);
+
+ expect(allNodes.indexOf(drawer)).toBeLessThan(
+ allNodes.indexOf(content),
+ 'Expected drawer to be before content when position changes to `start`',
+ );
+
+ fixture.componentInstance.position = 'end';
+ fixture.detectChanges();
+ allNodes = getDrawerNodesArray(fixture);
+
+ expect(allNodes.indexOf(drawer)).toBeGreaterThan(
+ allNodes.indexOf(content),
+ 'Expected drawer to be after content when position changes back to `end`',
+ );
+ },
+ );
+
+ function getDrawerNodesArray(fixture: ComponentFixture
): HTMLElement[] {
+ return Array.from(fixture.nativeElement.querySelector('.mat-drawer-container').childNodes);
+ }
+ });
});
describe('MatDrawerContainer', () => {
@@ -949,6 +1087,41 @@ describe('MatDrawerContainer', () => {
expect(spy).toHaveBeenCalled();
subscription.unsubscribe();
}));
+
+ it('should position the drawers before/after the content in the DOM based on their position', fakeAsync(() => {
+ const fixture = TestBed.createComponent(DrawerContainerTwoDrawerTestApp);
+ fixture.detectChanges();
+
+ const drawerDebugElements = fixture.debugElement.queryAll(By.directive(MatDrawer));
+ const [start, end] = drawerDebugElements.map(el => el.componentInstance);
+ const [startNode, endNode] = drawerDebugElements.map(el => el.nativeElement);
+ const contentNode = fixture.nativeElement.querySelector('.mat-drawer-content');
+ const allNodes: HTMLElement[] = Array.from(
+ fixture.nativeElement.querySelector('.mat-drawer-container').childNodes,
+ );
+ const startIndex = allNodes.indexOf(startNode);
+ const endIndex = allNodes.indexOf(endNode);
+ const contentIndex = allNodes.indexOf(contentNode);
+
+ expect(start.position).toBe('start');
+ expect(end.position).toBe('end');
+ expect(contentIndex)
+ .withContext('Expected content to be inside the container')
+ .toBeGreaterThan(-1);
+ expect(startIndex)
+ .withContext('Expected start drawer to be inside the container')
+ .toBeGreaterThan(-1);
+ expect(endIndex)
+ .withContext('Expected end drawer to be inside the container')
+ .toBeGreaterThan(-1);
+
+ expect(startIndex)
+ .withContext('Expected start drawer to be before content')
+ .toBeLessThan(contentIndex);
+ expect(endIndex)
+ .withContext('Expected end drawer to be after content')
+ .toBeGreaterThan(contentIndex);
+ }));
});
/** Test component that contains an MatDrawerContainer but no MatDrawer. */
@@ -971,7 +1144,7 @@ class DrawerContainerTwoDrawerTestApp {
@Component({
template: `
- ;
diff --git a/src/material/sidenav/drawer.ts b/src/material/sidenav/drawer.ts
index 1fc21f92193c..068c16a8d5eb 100644
--- a/src/material/sidenav/drawer.ts
+++ b/src/material/sidenav/drawer.ts
@@ -22,6 +22,7 @@ import {DOCUMENT} from '@angular/common';
import {
AfterContentChecked,
AfterContentInit,
+ AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
@@ -153,13 +154,19 @@ export class MatDrawerContent extends CdkScrollable implements AfterContentInit
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
})
-export class MatDrawer implements AfterContentInit, AfterContentChecked, OnDestroy {
+export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy {
private _focusTrap: FocusTrap;
private _elementFocusedBeforeDrawerWasOpened: HTMLElement | null = null;
/** Whether the drawer is initialized. Used for disabling the initial animation. */
private _enableAnimations = false;
+ /** Whether the view of the component has been attached. */
+ private _isAttached: boolean;
+
+ /** Anchor node used to restore the drawer to its initial position. */
+ private _anchor: Comment | null;
+
/** The side that the drawer is attached to. */
@Input()
get position(): 'start' | 'end' {
@@ -168,7 +175,12 @@ export class MatDrawer implements AfterContentInit, AfterContentChecked, OnDestr
set position(value: 'start' | 'end') {
// Make sure we have a valid value.
value = value === 'end' ? 'end' : 'start';
- if (value != this._position) {
+ if (value !== this._position) {
+ // Static inputs in Ivy are set before the element is in the DOM.
+ if (this._isAttached) {
+ this._updatePositionInParent(value);
+ }
+
this._position = value;
this.onPositionChanged.emit();
}
@@ -293,6 +305,9 @@ export class MatDrawer implements AfterContentInit, AfterContentChecked, OnDestr
// tslint:disable-next-line:no-output-on-prefix
@Output('positionChanged') readonly onPositionChanged = new EventEmitter();
+ /** Reference to the inner element that contains all the content. */
+ @ViewChild('content') _content: ElementRef;
+
/**
* An observable that emits when the drawer mode changes. This is used by the drawer container to
* to know when to when the mode changes so it can adapt the margins on the content.
@@ -448,13 +463,20 @@ export class MatDrawer implements AfterContentInit, AfterContentChecked, OnDestr
/** Whether focus is currently within the drawer. */
private _isFocusWithinDrawer(): boolean {
- const activeEl = this._doc?.activeElement;
+ const activeEl = this._doc.activeElement;
return !!activeEl && this._elementRef.nativeElement.contains(activeEl);
}
- ngAfterContentInit() {
+ ngAfterViewInit() {
+ this._isAttached = true;
this._focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement);
this._updateFocusTrapState();
+
+ // Only update the DOM position when the sidenav is positioned at
+ // the end since we project the sidenav before the content by default.
+ if (this._position === 'end') {
+ this._updatePositionInParent('end');
+ }
}
ngAfterContentChecked() {
@@ -472,6 +494,8 @@ export class MatDrawer implements AfterContentInit, AfterContentChecked, OnDestr
this._focusTrap.destroy();
}
+ this._anchor?.remove();
+ this._anchor = null;
this._animationStarted.complete();
this._animationEnd.complete();
this._modeChanged.complete();
@@ -567,6 +591,28 @@ export class MatDrawer implements AfterContentInit, AfterContentChecked, OnDestr
this._focusTrap.enabled = this.opened && this.mode !== 'side';
}
}
+
+ /**
+ * Updates the position of the drawer in the DOM. We need to move the element around ourselves
+ * when it's in the `end` position so that it comes after the content and the visual order
+ * matches the tab order. We also need to be able to move it back to `start` if the sidenav
+ * started off as `end` and was changed to `start`.
+ */
+ private _updatePositionInParent(newPosition: 'start' | 'end') {
+ const element = this._elementRef.nativeElement;
+ const parent = element.parentNode!;
+
+ if (newPosition === 'end') {
+ if (!this._anchor) {
+ this._anchor = this._doc.createComment('mat-drawer-anchor')!;
+ parent.insertBefore(this._anchor!, element);
+ }
+
+ parent.appendChild(element);
+ } else if (this._anchor) {
+ this._anchor.parentNode!.insertBefore(element, this._anchor);
+ }
+ }
}
/**
diff --git a/tools/public_api_guard/material/sidenav.md b/tools/public_api_guard/material/sidenav.md
index 85762e2a76ef..8e4cc8e41f00 100644
--- a/tools/public_api_guard/material/sidenav.md
+++ b/tools/public_api_guard/material/sidenav.md
@@ -6,6 +6,7 @@
import { AfterContentChecked } from '@angular/core';
import { AfterContentInit } from '@angular/core';
+import { AfterViewInit } from '@angular/core';
import { AnimationEvent as AnimationEvent_2 } from '@angular/animations';
import { AnimationTriggerMetadata } from '@angular/animations';
import { BooleanInput } from '@angular/cdk/coercion';
@@ -48,7 +49,7 @@ export const MAT_DRAWER_DEFAULT_AUTOSIZE: InjectionToken;
export function MAT_DRAWER_DEFAULT_AUTOSIZE_FACTORY(): boolean;
// @public
-export class MatDrawer implements AfterContentInit, AfterContentChecked, OnDestroy {
+export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy {
constructor(_elementRef: ElementRef, _focusTrapFactory: FocusTrapFactory, _focusMonitor: FocusMonitor, _platform: Platform, _ngZone: NgZone, _interactivityChecker: InteractivityChecker, _doc: any, _container?: MatDrawerContainer | undefined);
readonly _animationEnd: Subject;
readonly _animationStarted: Subject;
@@ -61,6 +62,7 @@ export class MatDrawer implements AfterContentInit, AfterContentChecked, OnDestr
_closeViaBackdropClick(): Promise;
// (undocumented)
_container?: MatDrawerContainer | undefined;
+ _content: ElementRef;
get disableClose(): boolean;
set disableClose(value: BooleanInput);
// (undocumented)
@@ -71,7 +73,7 @@ export class MatDrawer implements AfterContentInit, AfterContentChecked, OnDestr
// (undocumented)
ngAfterContentChecked(): void;
// (undocumented)
- ngAfterContentInit(): void;
+ ngAfterViewInit(): void;
// (undocumented)
ngOnDestroy(): void;
readonly onPositionChanged: EventEmitter;