diff --git a/packages/overlays/package.json b/packages/overlays/package.json index 5c61ab020..96b2c045a 100644 --- a/packages/overlays/package.json +++ b/packages/overlays/package.json @@ -102,7 +102,7 @@ "root": true }, "dependencies": { - "@sanity/visual-editing": "1.8.20" + "@sanity/visual-editing": "workspace:*" }, "devDependencies": { "@repo/package.config": "workspace:*", diff --git a/packages/svelte-loader/package.json b/packages/svelte-loader/package.json index 7395b24b0..9a9e63203 100644 --- a/packages/svelte-loader/package.json +++ b/packages/svelte-loader/package.json @@ -98,7 +98,7 @@ "@sanity/client": "^6.18.2", "@sanity/pkg-utils": "6.8.15", "@sanity/preview-url-secret": "^1.6.13", - "@sanity/visual-editing": "1.8.20", + "@sanity/visual-editing": "workspace:*", "@sveltejs/kit": "^2.5.8", "@typescript-eslint/eslint-plugin": "^7.9.0", "@typescript-eslint/parser": "^7.9.0", diff --git a/packages/visual-editing/src/ui/enableVisualEditing.tsx b/packages/visual-editing/src/ui/enableVisualEditing.tsx index ed9f50998..ed34f3a08 100644 --- a/packages/visual-editing/src/ui/enableVisualEditing.tsx +++ b/packages/visual-editing/src/ui/enableVisualEditing.tsx @@ -1,12 +1,5 @@ -import type {Root} from 'react-dom/client' - -import {OVERLAY_ID} from '../constants' import type {DisableVisualEditing, VisualEditingOptions} from '../types' -let node: HTMLElement | null = null -let root: Root | null = null -let cleanup: number | null = null - /** * Enables Visual Editing overlay in a page with sourcemap encoding. * @@ -14,45 +7,26 @@ let cleanup: number | null = null * @public */ export function enableVisualEditing(options: VisualEditingOptions = {}): DisableVisualEditing { - if (cleanup) clearTimeout(cleanup) const controller = new AbortController() - - // Lazy load everything needed to render the app - Promise.all([import('react-dom/client'), import('./VisualEditing')]).then( - ([reactClient, {VisualEditing}]) => { - if (controller.signal.aborted) return - - if (!node) { - node = document.createElement('div') - node.id = OVERLAY_ID - document.body.appendChild(node) - } - - if (!root) { - const {createRoot} = 'default' in reactClient ? reactClient.default : reactClient - root = createRoot(node) - } - - root.render( - <> - - , - ) - }, - ) + // Lazy load everything, react, the app, all of it + import('./renderVisualEditing').then(({renderVisualEditing}) => { + const {signal} = controller + /** + * Due to lazy loading it's possible for the following to happen, and is a consequence of dynamic ESM imports not being cancellable natively: + * 1. Userland calls `const disableVisualEditing = enableVisualEditing()` and the dynamic ESM import is started. + * 2. The dynamic import uses the network, and it takes a while to load. + * 3. The user navigates to a different page in the app that doesn't need Visual Editing, for example a login page. + * 4. Since the app is no longer in a state where Visual Editing is needed, the user calls `disableVisualEditing()`. + * 5. The dynamic import eventually resolves and this function is called. + * When this happens we want to skip calling `renderVisualEditing` since we know it's no longer needed. + */ + if (signal.aborted) return + + // Hand off to the render function with the signal, which will be subscribed to for detecting when to cancel the rendering if needed and unmount the app. + renderVisualEditing(signal, options) + }) return () => { controller.abort() - // Handle React StrictMode, delay cleanup for a second in case it's a rerender - cleanup = window.setTimeout(() => { - if (root) { - root.unmount() - root = null - } - if (node) { - document.body.removeChild(node) - node = null - } - }, 1000) } } diff --git a/packages/visual-editing/src/ui/renderVisualEditing.tsx b/packages/visual-editing/src/ui/renderVisualEditing.tsx new file mode 100644 index 000000000..000c5b17a --- /dev/null +++ b/packages/visual-editing/src/ui/renderVisualEditing.tsx @@ -0,0 +1,52 @@ +/** + * The purpose of this file is to contain the logic for rendering the + * component in a way that is easy to lazy load for the `enableVisualEditing` function. + */ + +import {createRoot, type Root} from 'react-dom/client' + +import {OVERLAY_ID} from '../constants' +import type {VisualEditingOptions} from '../types' +import {VisualEditing} from './VisualEditing' + +let node: HTMLElement | null = null +let root: Root | null = null +let cleanup: ReturnType | null = null + +export function renderVisualEditing( + signal: AbortSignal, + {history, refresh, zIndex}: VisualEditingOptions, +): void { + // Cancel pending cleanups, this is useful to avoid overlays blinking as the parent app transition between URLs, or hot module reload is happening + if (cleanup) clearTimeout(cleanup) + // Setup a cleanup function listener right away, as the signal might abort in-between the next steps + signal.addEventListener('abort', () => { + // Handle React StrictMode, delay cleanup for a second in case it's a rerender + cleanup = setTimeout(() => { + if (root) { + root.unmount() + root = null + } + if (node) { + document.body.removeChild(node) + node = null + } + }, 1000) + }) + + if (!node) { + // eslint-disable-next-line no-warning-comments + // @TODO use 'sanity-visual-editing' instead of 'div' + node = document.createElement('div') + // eslint-disable-next-line no-warning-comments + // @TODO after the element is `sanity-visual-editing` instead of `div`, stop setting this ID + node.id = OVERLAY_ID + document.body.appendChild(node) + } + + if (!root) { + root = createRoot(node) + } + + root.render() +}