Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 74 additions & 17 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,24 @@ export class AppComponent implements OnInit, AfterViewInit {
}

/**
* Drops the SSR mask overlay installed by the inline bootstrap script in src/index.html the
* moment the real CSR content is actually visible. We do NOT wait for ApplicationRef.isStable
* (which can be delayed many seconds by ongoing zone tasks, e.g. admin-only background HTTP
* polling, periodic timers, third-party AAI/discojuice scripts). Instead we react to the same
* condition root.component.html uses to swap the fullscreen loader for the real content:
* `!isAuthenticationBlocking && !isThemeLoading`. At that exact point the routed page is
* rendered, so removing the SSR snapshot does not produce flicker. One rAF delay lets the
* change-detection result commit to the DOM before the overlay fades.
* Drops the SSR mask overlay installed by the inline bootstrap script in src/index.html once the
* real CSR content has actually been painted. This trigger has to thread a needle between the two
* earlier approaches, each of which fixed one symptom and reintroduced the other:
*
* - PR #1288 waited for `ApplicationRef.isStable`. That guaranteed the content was painted (no
* flicker) but isStable is held hostage by ANY ongoing zone async — after an admin login the
* app keeps the zone busy (authz/widgets, periodic polling, AAI/discojuice scripts) so isStable
* fires many seconds late (or never, hitting the 15s fallback). The inert snapshot then masks
* the live, already-rendered page -> "looks rendered but not interactive" (issue #725).
* - PR #1317 switched to the loader-swap gate `!isAuthenticationBlocking && !isThemeLoading` plus
* a single requestAnimationFrame. That fires promptly (fixing #725) but the gate only un-hides
* `<router-outlet>`; Angular has NOT yet rendered the routed page at that instant and one rAF
* runs before the browser paints, so the snapshot is dropped over an empty <ds-app> for a frame
* or two -> the flicker came back.
*
* We keep #1317's decoupling from isStable (so background async can never delay us) but, after the
* gate opens, we wait across animation frames until the real <ds-app> is actually laid out before
* removing the snapshot. See {@link removeSsrOverlayAfterContentPainted}.
*/
private removeSsrOverlayWhenContentVisible(): void {
const w: Window | undefined = this._window?.nativeWindow;
Expand All @@ -116,18 +126,65 @@ export class AppComponent implements OnInit, AfterViewInit {
filter(([blocking, themeLoading]: [boolean, boolean]) => !blocking && !themeLoading),
first(),
).subscribe(() => {
const remove = () => {
if (typeof w.__dspaceRemoveSsrOverlay === 'function') {
w.__dspaceRemoveSsrOverlay();
}
};
if (typeof w.requestAnimationFrame === 'function') {
w.requestAnimationFrame(remove);
this.removeSsrOverlayAfterContentPainted(w);
});
});
}

/**
* Waits until the routed CSR view has been committed to the DOM and painted, then removes the SSR
* snapshot overlay. "Painted" is approximated by the real <ds-app> reaching a non-trivial height
* AND containing its `#main-content` host (i.e. it is no longer the empty shell the overlay script
* left behind). We poll this cheap layout signal once per animation frame, capped at MAX_FRAMES so
* that — unlike isStable in #1288 — nothing can hold the overlay open indefinitely; the 15s hard
* fallback in index.html stays as the catastrophic-error safety net.
*/
private removeSsrOverlayAfterContentPainted(w: Window): void {
const doc: Document = this.document;
const raf: ((cb: FrameRequestCallback) => number) | null =
typeof w.requestAnimationFrame === 'function' ? w.requestAnimationFrame.bind(w) : null;
const remove = () => {
if (typeof w.__dspaceRemoveSsrOverlay === 'function') {
w.__dspaceRemoveSsrOverlay();
}
};
const MAX_FRAMES = 180; // ~3s @60fps safety cap; the routed shell normally paints within a few frames
const MIN_CONTENT_HEIGHT = 200; // px: enough to prove the real <ds-app> is no longer the empty shell
let frames = 0;
const contentPainted = (): boolean => {
const app: Element | null = doc.querySelector('ds-app');
if (!app) {
return false;
}
let height = 0;
try {
height = app.getBoundingClientRect().height;
} catch (e) {
height = 0;
}
return height >= MIN_CONTENT_HEIGHT && app.querySelector('#main-content') !== null;
};
const tick = () => {
if (contentPainted() || ++frames >= MAX_FRAMES) {
// one more frame so the painted content is committed to screen before the snapshot fades
if (raf) {
raf(remove);
} else {
remove();
}
});
});
return;
}
if (raf) {
raf(tick);
} else {
setTimeout(tick, 16);
}
};
if (raf) {
raf(tick);
} else {
setTimeout(tick, 16);
}
}

ngOnInit() {
Expand Down
Loading