Skip to content

Commit 7a91d46

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 a0eb8b0 commit 7a91d46

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,
@@ -152,15 +151,14 @@ export class MatDrawerContent extends CdkScrollable implements AfterContentInit
152151
'[@transform]': '_animationState',
153152
'(@transform.start)': '_animationStarted.next($event)',
154153
'(@transform.done)': '_animationEnd.next($event)',
155-
'ngSkipHydration': '',
156154
},
157155
changeDetection: ChangeDetectionStrategy.OnPush,
158156
encapsulation: ViewEncapsulation.None,
159157
standalone: true,
160158
imports: [CdkScrollable],
161159
})
162160
export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy {
163-
private _focusTrap: FocusTrap;
161+
private _focusTrap: FocusTrap | null = null;
164162
private _elementFocusedBeforeDrawerWasOpened: HTMLElement | null = null;
165163

166164
/** Whether the drawer is initialized. Used for disabling the initial animation. */
@@ -480,14 +478,19 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
480478

481479
ngAfterViewInit() {
482480
this._isAttached = true;
483-
this._focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement);
484-
this._updateFocusTrapState();
485481

486482
// Only update the DOM position when the sidenav is positioned at
487483
// the end since we project the sidenav before the content by default.
488484
if (this._position === 'end') {
489485
this._updatePositionInParent('end');
490486
}
487+
488+
// Needs to happen after the position is updated
489+
// so the focus trap anchors are in the right place.
490+
if (this._platform.isBrowser) {
491+
this._focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement);
492+
this._updateFocusTrapState();
493+
}
491494
}
492495

493496
ngAfterContentChecked() {
@@ -501,10 +504,7 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
501504
}
502505

503506
ngOnDestroy() {
504-
if (this._focusTrap) {
505-
this._focusTrap.destroy();
506-
}
507-
507+
this._focusTrap?.destroy();
508508
this._anchor?.remove();
509509
this._anchor = null;
510510
this._animationStarted.complete();
@@ -610,7 +610,12 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
610610
* matches the tab order. We also need to be able to move it back to `start` if the sidenav
611611
* started off as `end` and was changed to `start`.
612612
*/
613-
private _updatePositionInParent(newPosition: 'start' | 'end') {
613+
private _updatePositionInParent(newPosition: 'start' | 'end'): void {
614+
// Don't move the DOM node around on the server, because it can throw off hydration.
615+
if (!this._platform.isBrowser) {
616+
return;
617+
}
618+
614619
const element = this._elementRef.nativeElement;
615620
const parent = element.parentNode!;
616621

@@ -641,7 +646,6 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
641646
host: {
642647
'class': 'mat-drawer-container',
643648
'[class.mat-drawer-container-explicit-backdrop]': '_backdropOverride',
644-
'ngSkipHydration': '',
645649
},
646650
changeDetection: ChangeDetectionStrategy.OnPush,
647651
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,
@@ -79,7 +78,6 @@ export class MatSidenavContent extends MatDrawerContent {
7978
'[class.mat-sidenav-fixed]': 'fixedInViewport',
8079
'[style.top.px]': 'fixedInViewport ? fixedTopGap : null',
8180
'[style.bottom.px]': 'fixedInViewport ? fixedBottomGap : null',
82-
'ngSkipHydration': '',
8381
},
8482
changeDetection: ChangeDetectionStrategy.OnPush,
8583
encapsulation: ViewEncapsulation.None,
@@ -132,7 +130,6 @@ export class MatSidenav extends MatDrawer {
132130
host: {
133131
'class': 'mat-drawer-container mat-sidenav-container',
134132
'[class.mat-drawer-container-explicit-backdrop]': '_backdropOverride',
135-
'ngSkipHydration': '',
136133
},
137134
changeDetection: ChangeDetectionStrategy.OnPush,
138135
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)