Skip to content

Commit c95706e

Browse files
JCU/fix(root): resolve the admin-sidebar gutter in CSS, not in an animation (#1496)
Backport of #1333, already on customer/TUL, customer/lindat and customer/mendelu. Written for this branch rather than cherry-picked. @slideSidebarPadding reads the gutter width from the browser-only CSS-variable store, so the server has nothing to substitute and renders style="padding-left: *", which is not valid CSS. For a logged-in user the browser then resolves the real width and the page moves right by the sidebar. That is visible on every reload, and all the more so now that the anti-flicker overlay holds the server's paint on screen while it happens. The gutter now comes from a ds-admin-sidebar-{hidden,unpinned,pinned} class whose padding-left reads --ds-admin-sidebar-fixed-element-width / --ds-admin-sidebar-total-width. CSS resolves those the same on the server and in the browser, so there is nothing left to shift and no width is hardcoded. The pin/unpin slide survives as transition: padding-left, gated behind ds-admin-sidebar-animate, which is added only after the first paint so the initial resolution never animates. Measured on the docker stack, logged in, reloading /home: outer-wrapper padding-left goes from "padding-left: *" (computed 0px, sidebar overlapping the content) to 55px, identical in the SSR snapshot and the live app, and the horizontal position of #main-content no longer moves at all. Refs: dataquest-dev/dspace-customers#717 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 2a8a531 commit c95706e

4 files changed

Lines changed: 61 additions & 9 deletions

File tree

src/app/root/root.component.html

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
{{ 'root.skip-to-content' | translate }}
33
</button>
44

5-
<div class="outer-wrapper" [class.d-none]="shouldShowFullscreenLoader" [ngClass]="browserOsClasses.asObservable() | async" [@slideSidebarPadding]="{
6-
value: ((isSidebarVisible$ | async) !== true ? 'hidden' : (slideSidebarOver$ | async) ? 'unpinned' : 'pinned'),
7-
params: { collapsedWidth: (collapsedSidebarWidth$ | async), expandedWidth: (expandedSidebarWidth$ | async) }
8-
}">
5+
<div class="outer-wrapper" [class.d-none]="shouldShowFullscreenLoader"
6+
[ngClass]="browserOsClasses.asObservable() | async"
7+
[class.ds-admin-sidebar-animate]="gutterTransitionEnabled"
8+
[class.ds-admin-sidebar-hidden]="(sidebarPaddingState$ | async) === 'hidden'"
9+
[class.ds-admin-sidebar-unpinned]="(sidebarPaddingState$ | async) === 'unpinned'"
10+
[class.ds-admin-sidebar-pinned]="(sidebarPaddingState$ | async) === 'pinned'">
911
<ds-admin-sidebar [expandedSidebarWidth$]="expandedSidebarWidth$" [collapsedSidebarWidth$]="collapsedSidebarWidth$"></ds-admin-sidebar>
1012
<div class="inner-wrapper">
1113
<ds-system-wide-alert-banner></ds-system-wide-alert-banner>

src/app/root/root.component.scss

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,27 @@
1414
top: 0;
1515
}
1616
}
17+
18+
// Admin-sidebar left gutter, driven by the ds-admin-sidebar-* class from sidebarPaddingState$ rather
19+
// than the @slideSidebarPadding animation. The animation read the width from a browser-only store, so
20+
// the server emitted `padding-left: *` and the page moved sideways once the browser resolved it.
21+
// Resolving from the custom properties in CSS renders the same on both sides, with no hardcoded px.
22+
.outer-wrapper {
23+
&.ds-admin-sidebar-hidden {
24+
padding-left: 0;
25+
}
26+
27+
&.ds-admin-sidebar-unpinned {
28+
padding-left: var(--ds-admin-sidebar-fixed-element-width);
29+
}
30+
31+
&.ds-admin-sidebar-pinned {
32+
padding-left: var(--ds-admin-sidebar-total-width);
33+
}
34+
35+
// Only genuine pin/unpin toggles slide: the class is added after the first paint, so the initial
36+
// gutter never animates behind the anti-flicker overlay.
37+
&.ds-admin-sidebar-animate {
38+
transition: padding-left 300ms ease-in-out;
39+
}
40+
}

src/app/root/root.component.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
NgClass,
44
} from '@angular/common';
55
import {
6+
AfterViewInit,
67
Component,
78
Inject,
89
Input,
@@ -38,7 +39,6 @@ import {
3839
} from '../core/services/window.service';
3940
import { ThemedFooterComponent } from '../footer/themed-footer.component';
4041
import { ThemedHeaderNavbarWrapperComponent } from '../header-nav-wrapper/themed-header-navbar-wrapper.component';
41-
import { slideSidebarPadding } from '../shared/animations/slide';
4242
import { HostWindowService } from '../shared/host-window.service';
4343
import { LiveRegionComponent } from '../shared/live-region/live-region.component';
4444
import { ThemedLoadingComponent } from '../shared/loading/themed-loading.component';
@@ -52,7 +52,6 @@ import { SystemWideAlertBannerComponent } from '../system-wide-alert/alert-banne
5252
selector: 'ds-base-root',
5353
templateUrl: './root.component.html',
5454
styleUrls: ['./root.component.scss'],
55-
animations: [slideSidebarPadding],
5655
imports: [
5756
AsyncPipe,
5857
LiveRegionComponent,
@@ -68,12 +67,25 @@ import { SystemWideAlertBannerComponent } from '../system-wide-alert/alert-banne
6867
TranslateModule,
6968
],
7069
})
71-
export class RootComponent implements OnInit {
70+
export class RootComponent implements OnInit, AfterViewInit {
7271
theme: Observable<ThemeConfig> = of({} as any);
7372
isSidebarVisible$: Observable<boolean>;
7473
slideSidebarOver$: Observable<boolean>;
7574
collapsedSidebarWidth$: Observable<string>;
7675
expandedSidebarWidth$: Observable<string>;
76+
77+
/**
78+
* Which admin-sidebar gutter the outer wrapper should carry: 'hidden', 'unpinned' or 'pinned'.
79+
* The width itself comes from CSS (see root.component.scss), so the server and the browser
80+
* resolve it the same way.
81+
*/
82+
sidebarPaddingState$: Observable<string>;
83+
84+
/**
85+
* Lets the gutter transition run only after the first paint, so the initial resolution does not
86+
* animate. Off on the server and on the first render; only pin/unpin toggles slide.
87+
*/
88+
gutterTransitionEnabled = false;
7789
notificationOptions: INotificationBoardOptions;
7890
models: any;
7991

@@ -129,11 +141,27 @@ export class RootComponent implements OnInit {
129141
startWith(true),
130142
);
131143

144+
// A CSS class instead of the @slideSidebarPadding animation: that animation needed a concrete
145+
// width from the browser-only CSS-variable store, so the server rendered `padding-left: *` and
146+
// the page moved sideways once the browser resolved the real width.
147+
this.sidebarPaddingState$ = combineLatestObservable([this.isSidebarVisible$, this.slideSidebarOver$]).pipe(
148+
map(([visible, over]: [boolean, boolean]) => !visible ? 'hidden' : over ? 'unpinned' : 'pinned'),
149+
);
150+
132151
if (this.router.url === getPageInternalServerErrorRoute()) {
133152
this.shouldShowRouteLoader = false;
134153
}
135154
}
136155

156+
ngAfterViewInit(): void {
157+
// Browser only; requestAnimationFrame does not exist under SSR.
158+
if (typeof requestAnimationFrame === 'function') {
159+
requestAnimationFrame(() => {
160+
this.gutterTransitionEnabled = true;
161+
});
162+
}
163+
}
164+
137165
skipToMainContent() {
138166
const mainContent = document.getElementById('main-content');
139167
if (mainContent) {

src/themes/custom/app/root/root.component.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { ThemedBreadcrumbsComponent } from '../../../../app/breadcrumbs/themed-b
1111
import { ThemedFooterComponent } from '../../../../app/footer/themed-footer.component';
1212
import { ThemedHeaderNavbarWrapperComponent } from '../../../../app/header-nav-wrapper/themed-header-navbar-wrapper.component';
1313
import { RootComponent as BaseComponent } from '../../../../app/root/root.component';
14-
import { slideSidebarPadding } from '../../../../app/shared/animations/slide';
1514
import { LiveRegionComponent } from '../../../../app/shared/live-region/live-region.component';
1615
import { ThemedLoadingComponent } from '../../../../app/shared/loading/themed-loading.component';
1716
import { NotificationsBoardComponent } from '../../../../app/shared/notifications/notifications-board/notifications-board.component';
@@ -23,7 +22,6 @@ import { SystemWideAlertBannerComponent } from '../../../../app/system-wide-aler
2322
styleUrls: ['../../../../app/root/root.component.scss'],
2423
// templateUrl: './root.component.html',
2524
templateUrl: '../../../../app/root/root.component.html',
26-
animations: [slideSidebarPadding],
2725
imports: [
2826
AsyncPipe,
2927
LiveRegionComponent,

0 commit comments

Comments
 (0)