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';
|
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)
|
* Save scroll position to sessionStorage on scroll (debounced)
|
||||||
* Called from scroll event listener set up in _restore_scroll_on_refresh
|
* 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
|
* Initialize scroll restoration on page refresh
|
||||||
* Uses Performance API to detect reload navigation type
|
* Sets up scroll saving and queues restoration if this is a refresh
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
static _restore_scroll_on_refresh() {
|
static _restore_scroll_on_refresh() {
|
||||||
@@ -784,7 +791,7 @@ class Rsx {
|
|||||||
return;
|
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);
|
const stored = sessionStorage.getItem(Rsx._SCROLL_STORAGE_KEY);
|
||||||
if (!stored) {
|
if (!stored) {
|
||||||
return;
|
return;
|
||||||
@@ -796,24 +803,78 @@ class Rsx {
|
|||||||
|
|
||||||
// Only restore if URL matches
|
// Only restore if URL matches
|
||||||
if (scroll_data.url !== current_url) {
|
if (scroll_data.url !== current_url) {
|
||||||
|
sessionStorage.removeItem(Rsx._SCROLL_STORAGE_KEY);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore scroll position instantly
|
// Queue the scroll restoration
|
||||||
window.scrollTo({
|
Rsx._pending_scroll = { x: scroll_data.x, y: scroll_data.y };
|
||||||
left: scroll_data.x,
|
|
||||||
top: scroll_data.y,
|
|
||||||
behavior: 'instant'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear stored position after successful restore
|
// Clear stored position (we've loaded it into memory)
|
||||||
sessionStorage.removeItem(Rsx._SCROLL_STORAGE_KEY);
|
sessionStorage.removeItem(Rsx._SCROLL_STORAGE_KEY);
|
||||||
|
|
||||||
|
// Try first restoration attempt
|
||||||
|
Rsx.try_restore_scroll();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Invalid JSON or other error - ignore
|
// Invalid JSON or other error - ignore
|
||||||
sessionStorage.removeItem(Rsx._SCROLL_STORAGE_KEY);
|
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. */
|
/* Calling this stops the boot process. */
|
||||||
static async _rsx_core_boot_stop(reason) {
|
static async _rsx_core_boot_stop(reason) {
|
||||||
console.error(reason);
|
console.error(reason);
|
||||||
|
|||||||
@@ -573,6 +573,10 @@ class Spa {
|
|||||||
// Errors from previous page's pending requests should be ignored for 10 seconds
|
// Errors from previous page's pending requests should be ignored for 10 seconds
|
||||||
Spa._navigation_timestamp = Date.now();
|
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 {
|
try {
|
||||||
const opts = {
|
const opts = {
|
||||||
history: options.history || 'auto',
|
history: options.history || 'auto',
|
||||||
@@ -938,6 +942,12 @@ class Spa {
|
|||||||
if (is_last) {
|
if (is_last) {
|
||||||
// This is the action - set reference but don't wait
|
// This is the action - set reference but don't wait
|
||||||
Spa._action = component;
|
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 {
|
} else {
|
||||||
// This is a layout
|
// This is a layout
|
||||||
// Wait for render to complete (not full ready - we don't need child data to load)
|
// Wait for render to complete (not full ready - we don't need child data to load)
|
||||||
|
|||||||
Reference in New Issue
Block a user