import React, { useEffect, useRef, useState } from 'react'; import './code-preview.css'; import 'tippy.js/dist/tippy.css'; import { ControlButton } from '../ControlButton'; import { OutputTargetButton } from '../OutputTargetButton'; import { ReportIssueButton } from '../ReportIssueButton'; import { ToggleSourceCodeButton } from '../ToggleSourceCodeButton'; import { StackblitzButton } from '../StackblitzButton'; import { CopyCodeButton } from '../CopyCodeButton'; import { PreviewFrame } from '../PreviewFrame'; import { FrameSize } from '../../utils/frame-sizes'; interface CodePreviewProps { /** * The code snippets to be displayed in the code preview. */ code: { [key: string]: () => {} }; title?: string; description?: string; source?: string; output?: { outputs: { name: string; value?: string; }[]; defaultOutput: string; }; viewport?: { viewports: { name: string; src: (baseUrl: string) => string; }[]; defaultViewport: string; }; controls?: { reportIssue?: { url: string; }; stackblitz?: | { tooltip?: string; } | boolean; }; /** * The size of the code preview frame. Default is `sm`. */ size?: FrameSize | string; isDarkMode?: boolean; onOpenOutputTarget?: (outputTarget: string, codeBlock: string) => void; } export const CodePreview = ({ code, source, viewport, size, output, controls, onOpenOutputTarget, isDarkMode, }: CodePreviewProps) => { const codeRef = useRef(null); const [outputTarget, setOutputTarget] = useState( output?.defaultOutput ?? Object.keys(code)[0] ); const [codeExpanded, setCodeExpanded] = useState(false); const [codeSnippets, setCodeSnippets] = useState({} as any); const [selectedViewport, setSelectedViewport] = useState( viewport?.defaultViewport ?? null ); function copySourceCode() { const copyButton = codeRef.current!.querySelector('button'); copyButton?.click(); } function openEditor() { // codeSnippets are React components, so we need to get their rendered text // using outerText will preserve line breaks for formatting in Stackblitz editor const codeEl: any = codeRef.current?.querySelector('code'); const codeBlock = codeEl!.outerText; if (onOpenOutputTarget !== undefined) { onOpenOutputTarget(outputTarget, codeBlock); } } useEffect(() => { const codeSnippets: any = {}; Object.keys(code).forEach(key => { // Instantiates the React component from the MDX content. codeSnippets[key] = (code as any)[key]({}); }); setCodeSnippets(codeSnippets); }, [code]); let stackBlitzTooltip; if (controls?.stackblitz && typeof controls.stackblitz !== 'boolean') { stackBlitzTooltip = controls.stackblitz.tooltip; } return (
{Object.keys(code).map(key => { const target = output?.outputs.find( o => o.value === key || o.name === key )!; const targetValue = target.value || target.name; return ( { setCodeExpanded(true); setOutputTarget(targetValue); }} /> ); })}
{viewport?.viewports.map(({ name }) => ( setSelectedViewport(name)} label={name} /> ))}
{controls?.reportIssue && ( )} {controls?.stackblitz && ( )}
{/* We render an iframe for each viewport. When the selected viewport changes, we hide one frame and show the other. This is done to avoid flickering and doing unnecessary reloads when switching viewports. */} {source && viewport?.viewports.map(({ src, name }) => ( ))}
{outputTarget && codeSnippets[outputTarget]}
); };