diff --git a/src/app/root/root.component.html b/src/app/root/root.component.html index 91cb236abed..b584cd95143 100644 --- a/src/app/root/root.component.html +++ b/src/app/root/root.component.html @@ -2,10 +2,12 @@ {{ 'root.skip-to-content' | translate }} -
+
diff --git a/src/app/root/root.component.scss b/src/app/root/root.component.scss index 9eb198417ad..2a6cdfb7848 100644 --- a/src/app/root/root.component.scss +++ b/src/app/root/root.component.scss @@ -14,3 +14,27 @@ top: 0; } } + +// Admin-sidebar left gutter, driven by the ds-admin-sidebar-* class from sidebarPaddingState$ rather +// than the @slideSidebarPadding animation. The animation read the width from a browser-only store, so +// the server emitted `padding-left: *` and the page moved sideways once the browser resolved it. +// Resolving from the custom properties in CSS renders the same on both sides, with no hardcoded px. +.outer-wrapper { + &.ds-admin-sidebar-hidden { + padding-left: 0; + } + + &.ds-admin-sidebar-unpinned { + padding-left: var(--ds-admin-sidebar-fixed-element-width); + } + + &.ds-admin-sidebar-pinned { + padding-left: var(--ds-admin-sidebar-total-width); + } + + // Only genuine pin/unpin toggles slide: the class is added after the first paint, so the initial + // gutter never animates behind the anti-flicker overlay. + &.ds-admin-sidebar-animate { + transition: padding-left 300ms ease-in-out; + } +} diff --git a/src/app/root/root.component.ts b/src/app/root/root.component.ts index 8ce800f5f5f..2dac12579f2 100644 --- a/src/app/root/root.component.ts +++ b/src/app/root/root.component.ts @@ -3,6 +3,7 @@ import { NgClass, } from '@angular/common'; import { + AfterViewInit, Component, Inject, Input, @@ -38,7 +39,6 @@ import { } from '../core/services/window.service'; import { ThemedFooterComponent } from '../footer/themed-footer.component'; import { ThemedHeaderNavbarWrapperComponent } from '../header-nav-wrapper/themed-header-navbar-wrapper.component'; -import { slideSidebarPadding } from '../shared/animations/slide'; import { HostWindowService } from '../shared/host-window.service'; import { LiveRegionComponent } from '../shared/live-region/live-region.component'; import { ThemedLoadingComponent } from '../shared/loading/themed-loading.component'; @@ -52,7 +52,6 @@ import { SystemWideAlertBannerComponent } from '../system-wide-alert/alert-banne selector: 'ds-base-root', templateUrl: './root.component.html', styleUrls: ['./root.component.scss'], - animations: [slideSidebarPadding], imports: [ AsyncPipe, LiveRegionComponent, @@ -68,12 +67,25 @@ import { SystemWideAlertBannerComponent } from '../system-wide-alert/alert-banne TranslateModule, ], }) -export class RootComponent implements OnInit { +export class RootComponent implements OnInit, AfterViewInit { theme: Observable = of({} as any); isSidebarVisible$: Observable; slideSidebarOver$: Observable; collapsedSidebarWidth$: Observable; expandedSidebarWidth$: Observable; + + /** + * Which admin-sidebar gutter the outer wrapper should carry: 'hidden', 'unpinned' or 'pinned'. + * The width itself comes from CSS (see root.component.scss), so the server and the browser + * resolve it the same way. + */ + sidebarPaddingState$: Observable; + + /** + * Lets the gutter transition run only after the first paint, so the initial resolution does not + * animate. Off on the server and on the first render; only pin/unpin toggles slide. + */ + gutterTransitionEnabled = false; notificationOptions: INotificationBoardOptions; models: any; @@ -129,11 +141,27 @@ export class RootComponent implements OnInit { startWith(true), ); + // A CSS class instead of the @slideSidebarPadding animation: that animation needed a concrete + // width from the browser-only CSS-variable store, so the server rendered `padding-left: *` and + // the page moved sideways once the browser resolved the real width. + this.sidebarPaddingState$ = combineLatestObservable([this.isSidebarVisible$, this.slideSidebarOver$]).pipe( + map(([visible, over]: [boolean, boolean]) => !visible ? 'hidden' : over ? 'unpinned' : 'pinned'), + ); + if (this.router.url === getPageInternalServerErrorRoute()) { this.shouldShowRouteLoader = false; } } + ngAfterViewInit(): void { + // Browser only; requestAnimationFrame does not exist under SSR. + if (typeof requestAnimationFrame === 'function') { + requestAnimationFrame(() => { + this.gutterTransitionEnabled = true; + }); + } + } + skipToMainContent() { const mainContent = document.getElementById('main-content'); if (mainContent) { diff --git a/src/themes/custom/app/root/root.component.ts b/src/themes/custom/app/root/root.component.ts index 2bdab293bf6..e3611b32a77 100644 --- a/src/themes/custom/app/root/root.component.ts +++ b/src/themes/custom/app/root/root.component.ts @@ -11,7 +11,6 @@ import { ThemedBreadcrumbsComponent } from '../../../../app/breadcrumbs/themed-b import { ThemedFooterComponent } from '../../../../app/footer/themed-footer.component'; import { ThemedHeaderNavbarWrapperComponent } from '../../../../app/header-nav-wrapper/themed-header-navbar-wrapper.component'; import { RootComponent as BaseComponent } from '../../../../app/root/root.component'; -import { slideSidebarPadding } from '../../../../app/shared/animations/slide'; import { LiveRegionComponent } from '../../../../app/shared/live-region/live-region.component'; import { ThemedLoadingComponent } from '../../../../app/shared/loading/themed-loading.component'; import { NotificationsBoardComponent } from '../../../../app/shared/notifications/notifications-board/notifications-board.component'; @@ -23,7 +22,6 @@ import { SystemWideAlertBannerComponent } from '../../../../app/system-wide-aler styleUrls: ['../../../../app/root/root.component.scss'], // templateUrl: './root.component.html', templateUrl: '../../../../app/root/root.component.html', - animations: [slideSidebarPadding], imports: [ AsyncPipe, LiveRegionComponent,