beta/src/components/MDX/Sandpack/Preview.tsx (140 lines of code) (raw):

/* * Copyright (c) Facebook, Inc. and its affiliates. */ /* eslint-disable react-hooks/exhaustive-deps */ import * as React from 'react'; import {useSandpack, LoadingOverlay} from '@codesandbox/sandpack-react'; import cn from 'classnames'; import {Error} from './Error'; import {computeViewportSize, generateRandomId} from './utils'; type CustomPreviewProps = { className?: string; customStyle?: Record<string, unknown>; isExpanded: boolean; }; function useDebounced(value: any): any { const ref = React.useRef<any>(null); const [saved, setSaved] = React.useState(value); React.useEffect(() => { clearTimeout(ref.current); ref.current = setTimeout(() => { setSaved(value); }, 300); }, [value]); return saved; } export function Preview({ customStyle, isExpanded, className, }: CustomPreviewProps) { const {sandpack, listen} = useSandpack(); const [isReady, setIsReady] = React.useState(false); const [iframeComputedHeight, setComputedAutoHeight] = React.useState< number | null >(null); let { error: rawError, registerBundler, unregisterBundler, errorScreenRegisteredRef, openInCSBRegisteredRef, loadingScreenRegisteredRef, status, } = sandpack; if ( rawError && rawError.message === '_csbRefreshUtils.prelude is not a function' ) { // Work around a noisy internal error. rawError = null; } // It changes too fast, causing flicker. const error = useDebounced(rawError); const clientId = React.useRef<string>(generateRandomId()); const iframeRef = React.useRef<HTMLIFrameElement | null>(null); // SandpackPreview immediately registers the custom screens/components so the bundler does not render any of them // TODO: why are we doing this during render? openInCSBRegisteredRef.current = true; errorScreenRegisteredRef.current = true; loadingScreenRegisteredRef.current = true; React.useEffect(function createBundler() { const iframeElement = iframeRef.current!; registerBundler(iframeElement, clientId.current); return () => { unregisterBundler(clientId.current); }; }, []); React.useEffect( function bundlerListener() { const unsubscribe = listen((message: any) => { if (message.type === 'resize') { setComputedAutoHeight(message.height); } else if (message.type === 'start') { if (message.firstLoad) { setIsReady(false); } } else if (message.type === 'done') { setIsReady(true); } }, clientId.current); return () => { setIsReady(false); setComputedAutoHeight(null); unsubscribe(); }; }, [status === 'idle'] ); const viewportStyle = computeViewportSize('auto', 'portrait'); 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: // - Content -> (compile | runtime) error -> content editing flow should work. // - Errors should expand parent height rather than scroll. // - Long sandboxes should scroll unless "show more" is toggled. // - Expanded sandboxes ("show more") have sticky previews and errors. // - Sandboxes have autoheight based on content. // - That autoheight should be measured correctly! (Check some long ones.) // - You shouldn't see nested scrolls (that means autoheight is borked). // - Ideally you shouldn't see a blank preview tile while recompiling. // - Container shouldn't be horizontally scrollable (even while loading). // - It should work on mobile. // The best way to test it is to actually go through some challenges. return ( <div className={cn('sp-stack', className)} style={{ // TODO: clean up this mess. ...customStyle, ...viewportStyle, ...overrideStyle, }}> <div className={cn( 'p-0 sm:p-2 md:p-4 lg:p-8 md:bg-card md:dark:bg-wash-dark h-full relative md:rounded-b-lg lg:rounded-b-none', // Allow content to be scrolled if it's too high to fit. // Note we don't want this in the expanded state // because it breaks position: sticky (and isn't needed anyway). !isExpanded && (error || isReady) ? 'overflow-auto' : null )}> <div style={{ padding: 'initial', position: hideContent ? 'relative' : isExpanded ? 'sticky' : undefined, top: isExpanded ? '2rem' : undefined, }}> <iframe ref={iframeRef} className={cn( 'rounded-t-none bg-white md:shadow-md sm:rounded-lg w-full max-w-full', // We can't *actually* hide content because that would // break calculating the computed height in the iframe // (which we're using for autosizing). This is noticeable // if you make a compiler error and then fix it with code // that expands the content. You want to measure that. hideContent ? 'absolute opacity-0 pointer-events-none' : 'opacity-100' )} title="Sandbox Preview" style={{ height: iframeComputedHeight || '100%', zIndex: isExpanded ? 'initial' : -1, }} /> </div> {error && ( <div className={cn( 'p-2', // This isn't absolutely positioned so that // the errors can also expand the parent height. isExpanded ? 'sticky top-8' : null )}> <Error error={error} /> </div> )} <LoadingOverlay clientId={clientId.current} loading={!isReady && iframeComputedHeight === null} /> </div> </div> ); }