From c1e4cb61e988c89e019bb57bd5424370eef0e826 Mon Sep 17 00:00:00 2001 From: jnealey88 <129418666+jnealey88@users.noreply.github.com> Date: Tue, 26 Aug 2025 14:10:58 -0700 Subject: [PATCH 1/3] Add browser refresh confirmation for temporary sites (fixes #81) Implements a beforeunload event handler that warns users when they attempt to leave or refresh the page while using a temporary Playground site. This prevents accidental data loss by showing a browser confirmation dialog. Changes: - Add browserConfirmationMiddleware to handle beforeunload events - Check if the active site is temporary (storage === 'none') - Show confirmation only for temporary sites, not saved ones - Register middleware in the Redux store --- .../website/src/lib/state/redux/slice-ui.ts | 27 +++++++++++++++++++ .../website/src/lib/state/redux/store.ts | 4 ++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/playground/website/src/lib/state/redux/slice-ui.ts b/packages/playground/website/src/lib/state/redux/slice-ui.ts index 9b1f089d16..c0899bd8cf 100644 --- a/packages/playground/website/src/lib/state/redux/slice-ui.ts +++ b/packages/playground/website/src/lib/state/redux/slice-ui.ts @@ -127,6 +127,33 @@ export const listenToOnlineOfflineEventsMiddleware: Middleware = return next(action); }; +let browserConfirmationRanOnce = false; +export const browserConfirmationMiddleware: Middleware = + (store) => (next) => (action) => { + if (!browserConfirmationRanOnce) { + browserConfirmationRanOnce = true; + if (typeof window !== 'undefined') { + window.addEventListener('beforeunload', (e) => { + const state = store.getState() as any; + // Access the active site directly from state structure + const activeSiteSlug = state.ui?.activeSite?.slug; + const activeSite = activeSiteSlug ? state.sites?.entities?.[activeSiteSlug] : undefined; + + // Only show confirmation for temporary sites (storage === 'none') + if (activeSite && activeSite.metadata?.storage === 'none') { + const message = 'Your changes will be lost. This is a temporary Playground site. Are you sure you want to leave?'; + e.preventDefault(); + // Modern browsers require setting returnValue + e.returnValue = message; + // Some older browsers use the return value + return message; + } + }); + } + } + return next(action); + }; + export const { setActiveModal, setActiveSiteError, diff --git a/packages/playground/website/src/lib/state/redux/store.ts b/packages/playground/website/src/lib/state/redux/store.ts index 3ba04ed195..ac62e0e741 100644 --- a/packages/playground/website/src/lib/state/redux/store.ts +++ b/packages/playground/website/src/lib/state/redux/store.ts @@ -3,6 +3,7 @@ import type { SiteError } from './slice-ui'; import uiReducer, { __internal_uiSlice, listenToOnlineOfflineEventsMiddleware, + browserConfirmationMiddleware, } from './slice-ui'; import type { SiteInfo } from './slice-sites'; import sitesReducer, { @@ -61,7 +62,8 @@ const store = configureStore({ }, middleware: (getDefaultMiddleware) => ignoreSerializableCheck(getDefaultMiddleware).concat( - listenToOnlineOfflineEventsMiddleware + listenToOnlineOfflineEventsMiddleware, + browserConfirmationMiddleware ), }); From 766bf59f425410118577aa3b43ae7f6c665929f0 Mon Sep 17 00:00:00 2001 From: jnealey88 <129418666+jnealey88@users.noreply.github.com> Date: Tue, 26 Aug 2025 14:46:51 -0700 Subject: [PATCH 2/3] Simplify beforeunload handler Remove unnecessary comments and custom message strings. The handler now simply triggers the browser's confirmation dialog for temporary sites. --- .../website/src/lib/state/redux/slice-ui.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/playground/website/src/lib/state/redux/slice-ui.ts b/packages/playground/website/src/lib/state/redux/slice-ui.ts index c0899bd8cf..df12b11c8c 100644 --- a/packages/playground/website/src/lib/state/redux/slice-ui.ts +++ b/packages/playground/website/src/lib/state/redux/slice-ui.ts @@ -137,16 +137,15 @@ export const browserConfirmationMiddleware: Middleware = const state = store.getState() as any; // Access the active site directly from state structure const activeSiteSlug = state.ui?.activeSite?.slug; - const activeSite = activeSiteSlug ? state.sites?.entities?.[activeSiteSlug] : undefined; - + const activeSite = activeSiteSlug + ? state.sites?.entities?.[activeSiteSlug] + : undefined; + // Only show confirmation for temporary sites (storage === 'none') if (activeSite && activeSite.metadata?.storage === 'none') { - const message = 'Your changes will be lost. This is a temporary Playground site. Are you sure you want to leave?'; e.preventDefault(); - // Modern browsers require setting returnValue - e.returnValue = message; - // Some older browsers use the return value - return message; + e.returnValue = ''; + return ''; } }); } From f1fa479679f72b7e6320de6a338b9f94cd4a8d0d Mon Sep 17 00:00:00 2001 From: jnealey88 <129418666+jnealey88@users.noreply.github.com> Date: Thu, 28 Aug 2025 08:26:13 -0700 Subject: [PATCH 3/3] Improve browser refresh confirmation based on feedback - Check ALL temporary sites, not just the active one - Track user interactions to avoid warning on quick open/close - Skip our confirmation if WordPress editor already has unsaved changes - Only warn users who have actually interacted with the site This addresses concerns about: - Multiple sites being open (temporary in background) - Double dialogs with WordPress editor - Harassing users who quickly open/close without editing --- .../website/src/lib/state/redux/slice-ui.ts | 56 ++++++++++++++++--- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/packages/playground/website/src/lib/state/redux/slice-ui.ts b/packages/playground/website/src/lib/state/redux/slice-ui.ts index df12b11c8c..e248469263 100644 --- a/packages/playground/website/src/lib/state/redux/slice-ui.ts +++ b/packages/playground/website/src/lib/state/redux/slice-ui.ts @@ -128,21 +128,61 @@ export const listenToOnlineOfflineEventsMiddleware: Middleware = }; let browserConfirmationRanOnce = false; +let hasUserInteracted = false; + export const browserConfirmationMiddleware: Middleware = (store) => (next) => (action) => { if (!browserConfirmationRanOnce) { browserConfirmationRanOnce = true; if (typeof window !== 'undefined') { + // Track user interactions to avoid warning on quick open/close + const interactionEvents = [ + 'click', + 'keypress', + 'input', + 'change', + ]; + interactionEvents.forEach((event) => { + window.addEventListener( + event, + () => { + hasUserInteracted = true; + }, + { once: true, capture: true } + ); + }); + window.addEventListener('beforeunload', (e) => { + // Don't warn if user hasn't interacted with the site + if (!hasUserInteracted) { + return; + } + + // Check if WordPress editor already has unsaved changes + // If it does, let WordPress handle the confirmation + try { + const wpWindow = window as any; + if ( + wpWindow.wp?.data + ?.select?.('core/editor') + ?.isEditedPostDirty?.() + ) { + // WordPress will show its own dialog + return; + } + } catch { + // WordPress editor not available, continue with our check + } + const state = store.getState() as any; - // Access the active site directly from state structure - const activeSiteSlug = state.ui?.activeSite?.slug; - const activeSite = activeSiteSlug - ? state.sites?.entities?.[activeSiteSlug] - : undefined; - - // Only show confirmation for temporary sites (storage === 'none') - if (activeSite && activeSite.metadata?.storage === 'none') { + + // Check ALL temporary sites, not just the active one + const temporarySites = Object.values( + state.sites?.entities || {} + ).filter((site: any) => site?.metadata?.storage === 'none'); + + // Only show confirmation if there are temporary sites + if (temporarySites.length > 0) { e.preventDefault(); e.returnValue = ''; return '';