Skip to content

Commit 7076f96

Browse files
committed
fix(material/sidenav): enable hydration
Fixes that the sidenav was disabling hydration. This is problematic, because it essentially disables hydration for the entire app that is using the sidenav. The hydration errors had a couple of root causes: 1. Focus trap anchors throw off hydration. 2. We were moving the end sidenav manually so the visual order matches the DOM order for accessibility. Now this happens only on the client. Fixes #27848.
1 parent a75bb7a commit 7076f96

File tree

3 files changed

+26
-20
lines changed

3 files changed

+26
-20
lines changed

src/material/sidenav/drawer.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,6 @@ export function MAT_DRAWER_DEFAULT_AUTOSIZE_FACTORY(): boolean {
101101
'class': 'mat-drawer-content',
102102
'[style.margin-left.px]': '_container._contentMargins.left',
103103
'[style.margin-right.px]': '_container._contentMargins.right',
104-
'ngSkipHydration': '',
105104
},
106105
changeDetection: ChangeDetectionStrategy.OnPush,
107106
encapsulation: ViewEncapsulation.None,
@@ -151,13 +150,12 @@ export class MatDrawerContent extends CdkScrollable implements AfterContentInit
151150
'[@transform]': '_animationState',
152151
'(@transform.start)': '_animationStarted.next($event)',
153152
'(@transform.done)': '_animationEnd.next($event)',
154-
'ngSkipHydration': '',
155153
},
156154
changeDetection: ChangeDetectionStrategy.OnPush,
157155
encapsulation: ViewEncapsulation.None,
158156
})
159157
export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy {
160-
private _focusTrap: FocusTrap;
158+
private _focusTrap: FocusTrap | null = null;
161159
private _elementFocusedBeforeDrawerWasOpened: HTMLElement | null = null;
162160

163161
/** Whether the drawer is initialized. Used for disabling the initial animation. */
@@ -477,14 +475,19 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
477475

478476
ngAfterViewInit() {
479477
this._isAttached = true;
480-
this._focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement);
481-
this._updateFocusTrapState();
482478

483479
// Only update the DOM position when the sidenav is positioned at
484480
// the end since we project the sidenav before the content by default.
485481
if (this._position === 'end') {
486482
this._updatePositionInParent('end');
487483
}
484+
485+
// Needs to happen after the position is updated
486+
// so the focus trap anchors are in the right place.
487+
if (this._platform.isBrowser) {
488+
this._focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement);
489+
this._updateFocusTrapState();
490+
}
488491
}
489492

490493
ngAfterContentChecked() {
@@ -498,10 +501,7 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
498501
}
499502

500503
ngOnDestroy() {
501-
if (this._focusTrap) {
502-
this._focusTrap.destroy();
503-
}
504-
504+
this._focusTrap?.destroy();
505505
this._anchor?.remove();
506506
this._anchor = null;
507507
this._animationStarted.complete();
@@ -607,7 +607,12 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
607607
* matches the tab order. We also need to be able to move it back to `start` if the sidenav
608608
* started off as `end` and was changed to `start`.
609609
*/
610-
private _updatePositionInParent(newPosition: 'start' | 'end') {
610+
private _updatePositionInParent(newPosition: 'start' | 'end'): void {
611+
// Don't move the DOM node around on the server, because it can throw off hydration.
612+
if (!this._platform.isBrowser) {
613+
return;
614+
}
615+
611616
const element = this._elementRef.nativeElement;
612617
const parent = element.parentNode!;
613618

@@ -638,7 +643,6 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
638643
host: {
639644
'class': 'mat-drawer-container',
640645
'[class.mat-drawer-container-explicit-backdrop]': '_backdropOverride',
641-
'ngSkipHydration': '',
642646
},
643647
changeDetection: ChangeDetectionStrategy.OnPush,
644648
encapsulation: ViewEncapsulation.None,

src/material/sidenav/sidenav.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ import {ScrollDispatcher, CdkScrollable} from '@angular/cdk/scrolling';
3737
'class': 'mat-drawer-content mat-sidenav-content',
3838
'[style.margin-left.px]': '_container._contentMargins.left',
3939
'[style.margin-right.px]': '_container._contentMargins.right',
40-
'ngSkipHydration': '',
4140
},
4241
changeDetection: ChangeDetectionStrategy.OnPush,
4342
encapsulation: ViewEncapsulation.None,
@@ -78,7 +77,6 @@ export class MatSidenavContent extends MatDrawerContent {
7877
'[class.mat-sidenav-fixed]': 'fixedInViewport',
7978
'[style.top.px]': 'fixedInViewport ? fixedTopGap : null',
8079
'[style.bottom.px]': 'fixedInViewport ? fixedBottomGap : null',
81-
'ngSkipHydration': '',
8280
},
8381
changeDetection: ChangeDetectionStrategy.OnPush,
8482
encapsulation: ViewEncapsulation.None,
@@ -129,7 +127,6 @@ export class MatSidenav extends MatDrawer {
129127
host: {
130128
'class': 'mat-drawer-container mat-sidenav-container',
131129
'[class.mat-drawer-container-explicit-backdrop]': '_backdropOverride',
132-
'ngSkipHydration': '',
133130
},
134131
changeDetection: ChangeDetectionStrategy.OnPush,
135132
encapsulation: ViewEncapsulation.None,

src/universal-app/kitchen-sink/kitchen-sink.html

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -257,18 +257,23 @@ <h2>Select</h2>
257257
<h2>Sidenav</h2>
258258
<mat-sidenav-container>
259259
<!--
260-
TODO: add sample with multiple sidenavs which can be problematic for hydration.
261-
Currently blocked on https://github.com/angular/angular/issues/53246
260+
Place the end sidenav first to test the behavior where we manually change the DOM order.
262261
-->
262+
<mat-sidenav #endSidenav position="end" mode="push">
263+
End sidenav
264+
<button (click)="endSidenav.toggle()">Close</button>
265+
</mat-sidenav>
266+
263267
<!--
264268
Only attempt to capture focus when automated, otherwise it makes the page jump around.
265269
-->
266-
<mat-sidenav #sidenav opened [autoFocus]="isAutomated ? 'first-tabbable' : false">
267-
On the side
268-
<button (click)="sidenav.toggle()">Button for testing focus trapping</button>
270+
<mat-sidenav #startSidenav opened [autoFocus]="isAutomated ? 'first-tabbable' : false">
271+
Start sidenav
272+
<button (click)="startSidenav.toggle()">Button for testing focus trapping</button>
269273
</mat-sidenav>
270274
Main content
271-
<button (click)="sidenav.toggle()">Click me</button>
275+
<button (click)="startSidenav.toggle()">Toggle start</button>
276+
<button (click)="endSidenav.toggle()">Toggle end</button>
272277
</mat-sidenav-container>
273278

274279
<h2>Slide-toggle</h2>

0 commit comments

Comments
 (0)