diff --git a/beta/src/components/MDX/Sandpack/Error.tsx b/beta/src/components/MDX/Sandpack/ErrorMessage.tsx
similarity index 70%
rename from beta/src/components/MDX/Sandpack/Error.tsx
rename to beta/src/components/MDX/Sandpack/ErrorMessage.tsx
index 61344ebac83..7c67ee4617e 100644
--- a/beta/src/components/MDX/Sandpack/Error.tsx
+++ b/beta/src/components/MDX/Sandpack/ErrorMessage.tsx
@@ -10,13 +10,13 @@ interface ErrorType {
path?: string;
}
-export function Error({error}: {error: ErrorType}) {
+export function ErrorMessage({error, ...props}: {error: ErrorType}) {
const {message, title} = error;
return (
-
+
{title || 'Error'}
-
+
{message}
diff --git a/beta/src/components/MDX/Sandpack/LoadingOverlay.tsx b/beta/src/components/MDX/Sandpack/LoadingOverlay.tsx
new file mode 100644
index 00000000000..7c261866dce
--- /dev/null
+++ b/beta/src/components/MDX/Sandpack/LoadingOverlay.tsx
@@ -0,0 +1,142 @@
+import {useState} from 'react';
+
+import {
+ LoadingOverlayState,
+ OpenInCodeSandboxButton,
+ useSandpack,
+} from '@codesandbox/sandpack-react';
+import {useEffect} from 'react';
+
+const FADE_ANIMATION_DURATION = 200;
+
+export const LoadingOverlay = ({
+ clientId,
+ dependenciesLoading,
+ forceLoading,
+}: {
+ clientId: string;
+ dependenciesLoading: boolean;
+ forceLoading: boolean;
+} & React.HTMLAttributes
): JSX.Element | null => {
+ const loadingOverlayState = useLoadingOverlayState(
+ clientId,
+ dependenciesLoading,
+ forceLoading
+ );
+
+ if (loadingOverlayState === 'HIDDEN') {
+ return null;
+ }
+
+ if (loadingOverlayState === 'TIMEOUT') {
+ return (
+
+
+ Unable to establish connection with the sandpack bundler. Make sure
+ you are online or try again later. If the problem persists, please
+ report it via{' '}
+
+ email
+ {' '}
+ or submit an issue on{' '}
+
+ GitHub.
+
+
+
+ );
+ }
+
+ const stillLoading =
+ loadingOverlayState === 'LOADING' || loadingOverlayState === 'PRE_FADING';
+
+ return (
+
+ );
+};
+
+const useLoadingOverlayState = (
+ clientId: string,
+ dependenciesLoading: boolean,
+ forceLoading: boolean
+): LoadingOverlayState => {
+ const {sandpack, listen} = useSandpack();
+ const [state, setState] = useState('HIDDEN');
+
+ if (state !== 'LOADING' && forceLoading) {
+ setState('LOADING');
+ }
+
+ /**
+ * Sandpack listener
+ */
+ const sandpackIdle = sandpack.status === 'idle';
+ useEffect(() => {
+ const unsubscribe = listen((message) => {
+ if (message.type === 'done') {
+ setState((prev) => {
+ return prev === 'LOADING' ? 'PRE_FADING' : 'HIDDEN';
+ });
+ }
+ }, clientId);
+
+ return () => {
+ unsubscribe();
+ };
+ }, [listen, clientId, sandpackIdle]);
+
+ /**
+ * Fading transient state
+ */
+ useEffect(() => {
+ let fadeTimeout: ReturnType;
+
+ if (state === 'PRE_FADING' && !dependenciesLoading) {
+ setState('FADING');
+ } else if (state === 'FADING') {
+ fadeTimeout = setTimeout(
+ () => setState('HIDDEN'),
+ FADE_ANIMATION_DURATION
+ );
+ }
+
+ return () => {
+ clearTimeout(fadeTimeout);
+ };
+ }, [state, dependenciesLoading]);
+
+ if (sandpack.status === 'timeout') {
+ return 'TIMEOUT';
+ }
+
+ if (sandpack.status !== 'running') {
+ return 'HIDDEN';
+ }
+
+ return state;
+};
diff --git a/beta/src/components/MDX/Sandpack/NavigationBar.tsx b/beta/src/components/MDX/Sandpack/NavigationBar.tsx
index eedf9fc38cb..8c884a5d8a7 100644
--- a/beta/src/components/MDX/Sandpack/NavigationBar.tsx
+++ b/beta/src/components/MDX/Sandpack/NavigationBar.tsx
@@ -22,7 +22,6 @@ import {DownloadButton} from './DownloadButton';
import {IconChevron} from '../../Icon/IconChevron';
import {Listbox} from '@headlessui/react';
-// TODO: Replace with real useEvent.
export function useEvent(fn: any): any {
const ref = useRef(null);
useInsertionEffect(() => {
@@ -94,9 +93,20 @@ export function NavigationBar({providedFiles}: {providedFiles: Array}) {
}, [isMultiFile]);
const handleReset = () => {
- if (confirm('Reset all your edits too?')) {
+ /**
+ * resetAllFiles must come first, otherwise
+ * the previous content will appears for a second
+ * when the iframe loads.
+ *
+ * Plus, it should only prompts if there's any file changes
+ */
+ if (
+ sandpack.editorState === 'dirty' &&
+ confirm('Reset all your edits too?')
+ ) {
sandpack.resetAllFiles();
}
+
refresh();
};
diff --git a/beta/src/components/MDX/Sandpack/Preview.tsx b/beta/src/components/MDX/Sandpack/Preview.tsx
index 20c8311ee46..cec510b8177 100644
--- a/beta/src/components/MDX/Sandpack/Preview.tsx
+++ b/beta/src/components/MDX/Sandpack/Preview.tsx
@@ -3,26 +3,17 @@
*/
/* eslint-disable react-hooks/exhaustive-deps */
-import {useRef, useState, useEffect, useMemo} from 'react';
-import {
- useSandpack,
- LoadingOverlay,
- SandpackStack,
-} from '@codesandbox/sandpack-react';
+import {useRef, useState, useEffect, useMemo, useId} from 'react';
+import {useSandpack, SandpackStack} from '@codesandbox/sandpack-react';
import cn from 'classnames';
-import {Error} from './Error';
+import {ErrorMessage} from './ErrorMessage';
import {SandpackConsole} from './Console';
import type {LintDiagnostic} from './useSandpackLint';
-
-/**
- * TODO: can we use React.useId?
- */
-const generateRandomId = (): string =>
- Math.floor(Math.random() * 10000).toString();
+import {CSSProperties} from 'react';
+import {LoadingOverlay} from './LoadingOverlay';
type CustomPreviewProps = {
className?: string;
- customStyle?: Record;
isExpanded: boolean;
lintErrors: LintDiagnostic;
};
@@ -40,13 +31,13 @@ function useDebounced(value: any): any {
}
export function Preview({
- customStyle,
isExpanded,
className,
lintErrors,
}: CustomPreviewProps) {
const {sandpack, listen} = useSandpack();
- const [isReady, setIsReady] = useState(false);
+ const [bundlerIsReady, setBundlerIsReady] = useState(false);
+ const [showLoading, setShowLoading] = useState(false);
const [iframeComputedHeight, setComputedAutoHeight] = useState(
null
);
@@ -95,7 +86,7 @@ export function Preview({
// It changes too fast, causing flicker.
const error = useDebounced(rawError);
- const clientId = useRef(generateRandomId());
+ const clientId = useId();
const iframeRef = useRef(null);
// SandpackPreview immediately registers the custom screens/components so the bundler does not render any of them
@@ -104,46 +95,54 @@ export function Preview({
errorScreenRegisteredRef.current = true;
loadingScreenRegisteredRef.current = true;
+ const sandpackIdle = sandpack.status === 'idle';
+
useEffect(function createBundler() {
const iframeElement = iframeRef.current!;
- registerBundler(iframeElement, clientId.current);
+ registerBundler(iframeElement, clientId);
return () => {
- unregisterBundler(clientId.current);
+ unregisterBundler(clientId);
};
}, []);
useEffect(
function bundlerListener() {
- const unsubscribe = listen((message: any) => {
+ let timeout: ReturnType;
+
+ const unsubscribe = listen((message) => {
if (message.type === 'resize') {
setComputedAutoHeight(message.height);
} else if (message.type === 'start') {
if (message.firstLoad) {
- setIsReady(false);
+ setBundlerIsReady(false);
}
+
+ /**
+ * The spinner component transition might be longer than
+ * the bundler loading, so we only show the spinner if
+ * it takes more than 1s to load the bundler.
+ */
+ timeout = setTimeout(() => {
+ setShowLoading(true);
+ }, 500);
} else if (message.type === 'done') {
- setIsReady(true);
+ setBundlerIsReady(true);
+ setShowLoading(false);
+ clearTimeout(timeout);
}
- }, clientId.current);
+ }, clientId);
return () => {
- setIsReady(false);
+ clearTimeout(timeout);
+ setBundlerIsReady(false);
setComputedAutoHeight(null);
unsubscribe();
};
},
- [status === 'idle']
+ [sandpackIdle]
);
- const overrideStyle = error
- ? {
- // Don't collapse errors
- maxHeight: undefined,
- }
- : null;
- const hideContent = !isReady || error;
-
// WARNING:
// The layout and styling here is convoluted and really easy to break.
// If you make changes to it, you need to test different cases:
@@ -159,67 +158,68 @@ export function Preview({
// - It should work on mobile.
// The best way to test it is to actually go through some challenges.
+ const hideContent = error || !iframeComputedHeight || !bundlerIsReady;
+
+ const iframeWrapperPosition = (): CSSProperties => {
+ if (hideContent) {
+ return {position: 'relative'};
+ }
+
+ if (isExpanded) {
+ return {position: 'sticky', top: '2em'};
+ }
+
+ return {};
+ };
+
return (
-
+
-
diff --git a/beta/src/components/MDX/Sandpack/SandpackRoot.tsx b/beta/src/components/MDX/Sandpack/SandpackRoot.tsx
index 27ea59152d0..c0755c06518 100644
--- a/beta/src/components/MDX/Sandpack/SandpackRoot.tsx
+++ b/beta/src/components/MDX/Sandpack/SandpackRoot.tsx
@@ -87,7 +87,8 @@ function SandpackRoot(props: SandpackProps) {
autorun,
initMode: 'user-visible',
initModeObserverOptions: {rootMargin: '1400px 0px'},
- bundlerURL: 'https://ac83f2d6.sandpack-bundler.pages.dev',
+ bundlerURL:
+ 'https://71d9edc6.sandpack-bundler.pages.dev/?babel=minimal',
logLevel: SandpackLogLevel.None,
}}>