From 0342487b3b7d54f87d976c2601294f3c4328fe8e Mon Sep 17 00:00:00 2001 From: amandaesmith3 Date: Tue, 10 Oct 2023 09:41:50 -0500 Subject: [PATCH 1/6] fix(playground): wait until playground is in view before loading stored framework/mode --- src/components/global/Playground/index.tsx | 112 +++++---------------- 1 file changed, 26 insertions(+), 86 deletions(-) diff --git a/src/components/global/Playground/index.tsx b/src/components/global/Playground/index.tsx index 2f46fe23049..bdded2eb54e 100644 --- a/src/components/global/Playground/index.tsx +++ b/src/components/global/Playground/index.tsx @@ -13,7 +13,6 @@ import TabItem from '@theme/TabItem'; import { IconHtml, IconTs, IconVue, IconDefault, IconCss, IconDots } from './icons'; -import { useScrollPositionBlocker } from '@docusaurus/theme-common'; import useIsBrowser from '@docusaurus/useIsBrowser'; const ControlButton = forwardRef( @@ -183,8 +182,6 @@ export default function Playground({ const frameMD = useRef(null); const consoleBodyRef = useRef(null); - const { blockElementScrollPositionUntilNextRender } = useScrollPositionBlocker(); - const getDefaultMode = () => { /** * If a custom mode was specified, use that. @@ -258,16 +255,6 @@ export default function Playground({ if (isBrowser) { localStorage.setItem(MODE_STORAGE_KEY, mode); - - /** - * Tell other playgrounds on the page that the mode has - * updated, so they can sync up. - */ - window.dispatchEvent( - new CustomEvent(MODE_UPDATED_EVENT, { - detail: mode, - }) - ); } }; @@ -276,26 +263,6 @@ export default function Playground({ if (isBrowser) { localStorage.setItem(USAGE_TARGET_STORAGE_KEY, target); - - /** - * This prevents the scroll position from jumping around if - * there is a playground above this one with code that changes - * in length between frameworks. - * - * Note that we don't need this when changing the mode because - * the two mode iframes are always the same height. - */ - blockElementScrollPositionUntilNextRender(tab); - - /** - * Tell other playgrounds on the page that the framework - * has updated, so they can sync up. - */ - window.dispatchEvent( - new CustomEvent(USAGE_TARGET_UPDATED_EVENT, { - detail: target, - }) - ); } }; @@ -401,25 +368,39 @@ export default function Playground({ }); /** - * By default, we do not render the iframe content - * as it could cause delays on page load. Instead - * we wait for even 1 pixel of the playground to - * scroll into view (intersect with the viewport) - * before loading the iframes. + * By default, we do not render the iframe content as it could + * cause delays on page load. We also do not immediately switch + * the framework/mode when it gets changed through another + * playground on the page, as switching them for every playground + * at once can cause memory-related crashes on some devices. + * + * Instead, we wait for even 1 pixel of the playground to scroll + * into view (intersect with the viewport) before loading the + * iframes or auto-switching the framework/mode. */ useEffect(() => { const io = new IntersectionObserver( (entries: IntersectionObserverEntry[]) => { const ev = entries[0]; - if (!ev.isIntersecting || renderIframes) return; + if (!ev.isIntersecting) return; - setRenderIframes(true); + /** + * Load the stored mode and/or usage target, if present + * from previously being toggled. + */ + if (isBrowser) { + const storedMode = localStorage.getItem(MODE_STORAGE_KEY); + if (storedMode) setIonicMode(storedMode); + const storedUsageTarget = localStorage.getItem(USAGE_TARGET_STORAGE_KEY); + if (storedUsageTarget) setUsageTarget(storedUsageTarget); + } /** - * Once the playground is loaded, it is never "unloaded" - * so we can safely disconnect the observer. + * If the iframes weren't already loaded, load them now. */ - io.disconnect(); + if (!renderIframes) { + setRenderIframes(true); + } }, { threshold: 0 } ); @@ -427,47 +408,6 @@ export default function Playground({ io.observe(hostRef.current!); }); - /** - * Sometimes, the app isn't fully hydrated on the first render, - * causing isBrowser to be set to false even if running the app - * in a browser (vs. SSR). isBrowser is then updated on the next - * render cycle. - * - * This useEffect contains code that can only run in the browser, - * and also needs to run on that first go-around. Note that - * isBrowser will never be set from true back to false, so the - * code within the if(isBrowser) check will only run once. - */ - useEffect(() => { - if (isBrowser) { - /** - * Load the stored mode and/or usage target, if present - * from previously being toggled. - */ - const storedMode = localStorage.getItem(MODE_STORAGE_KEY); - if (storedMode) setIonicMode(storedMode); - const storedUsageTarget = localStorage.getItem(USAGE_TARGET_STORAGE_KEY); - if (storedUsageTarget) setUsageTarget(storedUsageTarget); - - /** - * Listen for any playground on the page to have its mode or framework - * updated so this playground can switch to the same setting. - */ - window.addEventListener(MODE_UPDATED_EVENT, (e: CustomEvent) => { - const mode = e.detail; - if (Object.values(Mode).includes(mode)) { - setIonicMode(mode); // don't use setAndSave to avoid infinite loop - } - }); - window.addEventListener(USAGE_TARGET_UPDATED_EVENT, (e: CustomEvent) => { - const usageTarget = e.detail; - if (Object.values(UsageTarget).includes(usageTarget)) { - setUsageTarget(usageTarget); // don't use setAndSave to avoid infinite loop - } - }); - } - }, [isBrowser]); - const isIOS = ionicMode === Mode.iOS; const isMD = ionicMode === Mode.MD; @@ -897,5 +837,5 @@ const isFrameReady = (frame: HTMLIFrameElement) => { const USAGE_TARGET_STORAGE_KEY = 'playground_usage_target'; const MODE_STORAGE_KEY = 'playground_mode'; -const USAGE_TARGET_UPDATED_EVENT = 'playground-usage-target-updated'; -const MODE_UPDATED_EVENT = 'playground-event-updated'; +// const USAGE_TARGET_UPDATED_EVENT = 'playground-usage-target-updated'; +// const MODE_UPDATED_EVENT = 'playground-event-updated'; From 1db91c00ce1bc3fa3cd64dc7b0958e88fdceccfe Mon Sep 17 00:00:00 2001 From: amandaesmith3 Date: Tue, 10 Oct 2023 09:46:49 -0500 Subject: [PATCH 2/6] remove unneeded event keys --- src/components/global/Playground/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/global/Playground/index.tsx b/src/components/global/Playground/index.tsx index bdded2eb54e..f663353f609 100644 --- a/src/components/global/Playground/index.tsx +++ b/src/components/global/Playground/index.tsx @@ -837,5 +837,3 @@ const isFrameReady = (frame: HTMLIFrameElement) => { const USAGE_TARGET_STORAGE_KEY = 'playground_usage_target'; const MODE_STORAGE_KEY = 'playground_mode'; -// const USAGE_TARGET_UPDATED_EVENT = 'playground-usage-target-updated'; -// const MODE_UPDATED_EVENT = 'playground-event-updated'; From 2604d619c575e83a5f67ec3334e802f4887200ee Mon Sep 17 00:00:00 2001 From: amandaesmith3 Date: Tue, 10 Oct 2023 09:47:40 -0500 Subject: [PATCH 3/6] lint --- src/components/global/Playground/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/global/Playground/index.tsx b/src/components/global/Playground/index.tsx index f663353f609..1e1c7640272 100644 --- a/src/components/global/Playground/index.tsx +++ b/src/components/global/Playground/index.tsx @@ -373,7 +373,7 @@ export default function Playground({ * the framework/mode when it gets changed through another * playground on the page, as switching them for every playground * at once can cause memory-related crashes on some devices. - * + * * Instead, we wait for even 1 pixel of the playground to scroll * into view (intersect with the viewport) before loading the * iframes or auto-switching the framework/mode. From 6d79a3fc8b641a951200272acc3eae558447c5a9 Mon Sep 17 00:00:00 2001 From: amandaesmith3 Date: Tue, 10 Oct 2023 11:58:57 -0500 Subject: [PATCH 4/6] re-add event listening to handle playgrounds already on the screen --- src/components/global/Playground/index.tsx | 88 ++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/src/components/global/Playground/index.tsx b/src/components/global/Playground/index.tsx index 1e1c7640272..d72e7e8b9a3 100644 --- a/src/components/global/Playground/index.tsx +++ b/src/components/global/Playground/index.tsx @@ -13,6 +13,7 @@ import TabItem from '@theme/TabItem'; import { IconHtml, IconTs, IconVue, IconDefault, IconCss, IconDots } from './icons'; +import { useScrollPositionBlocker } from '@docusaurus/theme-common'; import useIsBrowser from '@docusaurus/useIsBrowser'; const ControlButton = forwardRef( @@ -182,6 +183,8 @@ export default function Playground({ const frameMD = useRef(null); const consoleBodyRef = useRef(null); + const { blockElementScrollPositionUntilNextRender } = useScrollPositionBlocker(); + const getDefaultMode = () => { /** * If a custom mode was specified, use that. @@ -250,11 +253,27 @@ export default function Playground({ */ const [resetCount, setResetCount] = useState(0); + /** + * Keeps track of whether any amount of this playground is + * currently on the screen. + */ + const [isInView, setIsInView] = useState(false); + const setAndSaveMode = (mode: Mode) => { setIonicMode(mode); if (isBrowser) { localStorage.setItem(MODE_STORAGE_KEY, mode); + + /** + * Tell other playgrounds on the page that the mode has + * updated, so they can sync up if they're in view. + */ + window.dispatchEvent( + new CustomEvent(MODE_UPDATED_EVENT, { + detail: mode, + }) + ); } }; @@ -263,6 +282,26 @@ export default function Playground({ if (isBrowser) { localStorage.setItem(USAGE_TARGET_STORAGE_KEY, target); + + /** + * This prevents the scroll position from jumping around if + * there is a playground above this one with code that changes + * in length between frameworks. + * + * Note that we don't need this when changing the mode because + * the two mode iframes are always the same height. + */ + blockElementScrollPositionUntilNextRender(tab); + + /** + * Tell other playgrounds on the page that the framework + * has updated, so they can sync up if they're in view. + */ + window.dispatchEvent( + new CustomEvent(USAGE_TARGET_UPDATED_EVENT, { + detail: target, + }) + ); } }; @@ -382,6 +421,7 @@ export default function Playground({ const io = new IntersectionObserver( (entries: IntersectionObserverEntry[]) => { const ev = entries[0]; + setIsInView(ev.isIntersecting); if (!ev.isIntersecting) return; /** @@ -408,6 +448,52 @@ export default function Playground({ io.observe(hostRef.current!); }); + const handleModeUpdated = (e: CustomEvent) => { + const mode = e.detail; + if (Object.values(Mode).includes(mode)) { + setIonicMode(mode); // don't use setAndSave to avoid infinite loop + } + }; + + const handleUsageTargetUpdated = (e: CustomEvent) => { + const usageTarget = e.detail; + if (Object.values(UsageTarget).includes(usageTarget)) { + setUsageTarget(usageTarget); // don't use setAndSave to avoid infinite loop + } + }; + + /** + * When this playground is in view, listen for any other playgrounds + * on the page to switch their framework or mode, so this one can + * sync up to the same setting. This is needed because if the + * playground is already in view, the IntersectionObserver doesn't + * fire until the playground is scrolled off and back on the screen. + * + * Sometimes, the app isn't fully hydrated on the first render, + * causing isBrowser to be set to false even if running the app + * in a browser (vs. SSR). isBrowser is then updated on the next + * render cycle. This means we need to re-run this hook when + * isBrowser changes to handle playgrounds that were in view + * from the start of the page load. + * + * We also re-run when isInView changes because the event callbacks + * would otherwise capture a stale state value. Since we need to + * listen for these events only when the playground is in view, + * we check the state before adding the listeners at all, rather + * than within the callbacks. + */ + useEffect(() => { + if (isBrowser && isInView) { + window.addEventListener(MODE_UPDATED_EVENT, handleModeUpdated); + window.addEventListener(USAGE_TARGET_UPDATED_EVENT, handleUsageTargetUpdated); + } + + return () => { + window.removeEventListener(MODE_UPDATED_EVENT, handleModeUpdated); + window.removeEventListener(USAGE_TARGET_UPDATED_EVENT, handleUsageTargetUpdated); + } + }, [isBrowser, isInView]); + const isIOS = ionicMode === Mode.iOS; const isMD = ionicMode === Mode.MD; @@ -837,3 +923,5 @@ const isFrameReady = (frame: HTMLIFrameElement) => { const USAGE_TARGET_STORAGE_KEY = 'playground_usage_target'; const MODE_STORAGE_KEY = 'playground_mode'; +const USAGE_TARGET_UPDATED_EVENT = 'playground-usage-target-updated'; +const MODE_UPDATED_EVENT = 'playground-event-updated'; From 3c0c4bcfc356f26a98a4e1a8b4446adf6d031beb Mon Sep 17 00:00:00 2001 From: amandaesmith3 Date: Tue, 10 Oct 2023 12:00:22 -0500 Subject: [PATCH 5/6] lint --- src/components/global/Playground/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/global/Playground/index.tsx b/src/components/global/Playground/index.tsx index d72e7e8b9a3..9c8fab4cac6 100644 --- a/src/components/global/Playground/index.tsx +++ b/src/components/global/Playground/index.tsx @@ -468,14 +468,14 @@ export default function Playground({ * sync up to the same setting. This is needed because if the * playground is already in view, the IntersectionObserver doesn't * fire until the playground is scrolled off and back on the screen. - * + * * Sometimes, the app isn't fully hydrated on the first render, * causing isBrowser to be set to false even if running the app * in a browser (vs. SSR). isBrowser is then updated on the next * render cycle. This means we need to re-run this hook when * isBrowser changes to handle playgrounds that were in view * from the start of the page load. - * + * * We also re-run when isInView changes because the event callbacks * would otherwise capture a stale state value. Since we need to * listen for these events only when the playground is in view, @@ -491,7 +491,7 @@ export default function Playground({ return () => { window.removeEventListener(MODE_UPDATED_EVENT, handleModeUpdated); window.removeEventListener(USAGE_TARGET_UPDATED_EVENT, handleUsageTargetUpdated); - } + }; }, [isBrowser, isInView]); const isIOS = ionicMode === Mode.iOS; From 271ecc6f626b9c42c1bb91dbe27ba23051044562 Mon Sep 17 00:00:00 2001 From: amandaesmith3 Date: Tue, 10 Oct 2023 12:05:34 -0500 Subject: [PATCH 6/6] make comment less confusing --- src/components/global/Playground/index.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/global/Playground/index.tsx b/src/components/global/Playground/index.tsx index 9c8fab4cac6..803a58cec8a 100644 --- a/src/components/global/Playground/index.tsx +++ b/src/components/global/Playground/index.tsx @@ -476,11 +476,11 @@ export default function Playground({ * isBrowser changes to handle playgrounds that were in view * from the start of the page load. * - * We also re-run when isInView changes because the event callbacks - * would otherwise capture a stale state value. Since we need to - * listen for these events only when the playground is in view, - * we check the state before adding the listeners at all, rather - * than within the callbacks. + * We also re-run when isInView changes because otherwise, a stale + * state value would be captured. Since we need to listen for these + * events only when the playground is in view, we check the state + * before adding the listeners at all, rather than within the + * callbacks. */ useEffect(() => { if (isBrowser && isInView) {