Add scroll restoration on browser refresh for SPA pages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2026-01-28 20:27:28 +00:00
parent 90efeab7ea
commit d5572e6081
2 changed files with 81 additions and 10 deletions

View File

@@ -742,6 +742,13 @@ class Rsx {
*/
static _SCROLL_STORAGE_KEY = 'rsx_scroll_pos';
/**
* Pending scroll restoration state
* Set when a refresh is detected, cleared when restoration succeeds
* @private
*/
static _pending_scroll = null;
/**
* Save scroll position to sessionStorage on scroll (debounced)
* Called from scroll event listener set up in _restore_scroll_on_refresh
@@ -765,8 +772,8 @@ class Rsx {
}
/**
* Restore scroll position if this is a page refresh
* Uses Performance API to detect reload navigation type
* Initialize scroll restoration on page refresh
* Sets up scroll saving and queues restoration if this is a refresh
* @private
*/
static _restore_scroll_on_refresh() {
@@ -784,7 +791,7 @@ class Rsx {
return;
}
// This is a refresh - try to restore scroll position
// This is a refresh - load stored scroll position
const stored = sessionStorage.getItem(Rsx._SCROLL_STORAGE_KEY);
if (!stored) {
return;
@@ -796,24 +803,78 @@ class Rsx {
// Only restore if URL matches
if (scroll_data.url !== current_url) {
sessionStorage.removeItem(Rsx._SCROLL_STORAGE_KEY);
return;
}
// Restore scroll position instantly
window.scrollTo({
left: scroll_data.x,
top: scroll_data.y,
behavior: 'instant'
});
// Queue the scroll restoration
Rsx._pending_scroll = { x: scroll_data.x, y: scroll_data.y };
// Clear stored position after successful restore
// Clear stored position (we've loaded it into memory)
sessionStorage.removeItem(Rsx._SCROLL_STORAGE_KEY);
// Try first restoration attempt
Rsx.try_restore_scroll();
} catch (e) {
// Invalid JSON or other error - ignore
sessionStorage.removeItem(Rsx._SCROLL_STORAGE_KEY);
}
}
/**
* Attempt to restore scroll position if pending
*
* Can be called multiple times during page load. Checks if the page is tall
* enough to scroll to the target position. Once successful, subsequent calls
* are no-ops until the next page refresh.
*
* Call this after content that may increase page height has loaded
* (e.g., after SPA action on_ready completes).
*
* @returns {boolean} True if scroll was restored, false if pending/skipped
*/
static try_restore_scroll() {
// No pending restoration
if (!Rsx._pending_scroll) {
return false;
}
const target = Rsx._pending_scroll;
// Check if page is tall enough to scroll to target position
// Page needs to be at least (target.y + viewport height) tall
const page_height = document.documentElement.scrollHeight;
const viewport_height = window.innerHeight;
const min_height_needed = target.y + viewport_height;
if (page_height < min_height_needed) {
// Page not tall enough yet - keep pending for retry
console_debug('Spa', `Scroll restore waiting: page ${page_height}px < needed ${min_height_needed}px`);
return false;
}
// Page is tall enough - restore scroll position
window.scrollTo({
left: target.x,
top: target.y,
behavior: 'instant'
});
// Mark as complete - clear pending state
Rsx._pending_scroll = null;
console_debug('Spa', `Scroll restored to (${target.x}, ${target.y})`);
return true;
}
/**
* Reset pending scroll state
* Called by SPA dispatch to clear any pending restoration from previous navigation
*/
static reset_pending_scroll() {
Rsx._pending_scroll = null;
}
/* Calling this stops the boot process. */
static async _rsx_core_boot_stop(reason) {
console.error(reason);

View File

@@ -573,6 +573,10 @@ class Spa {
// Errors from previous page's pending requests should be ignored for 10 seconds
Spa._navigation_timestamp = Date.now();
// Reset any pending scroll restoration from previous navigation
// (Browser refresh scroll is handled separately by Rsx._restore_scroll_on_refresh)
Rsx.reset_pending_scroll();
try {
const opts = {
history: options.history || 'auto',
@@ -938,6 +942,12 @@ class Spa {
if (is_last) {
// This is the action - set reference but don't wait
Spa._action = component;
// After action is fully ready (on_load + on_ready complete), retry scroll restoration
// This handles cases where on_load fetches data that increases page height
component.ready().then(() => {
Rsx.try_restore_scroll();
});
} else {
// This is a layout
// Wait for render to complete (not full ready - we don't need child data to load)