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:
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user