Files
yoga/website/src/components/Playground.tsx
Nick Gerleman 206b95aba5 Yoga Docs: Rename website-next to website (#1613)
Summary:
Pull Request resolved: https://github.com/facebook/yoga/pull/1613

So that GitHub links to edit a page point to the right place.

This will fail in OSS build until I switch the directory used by Vercel instance, but I am waiting to do that until ready to land, since that would cause other in progress changes to fail when exported.

Reviewed By: joevilches

Differential Revision: D54837857

fbshipit-source-id: 9bec90232dbe3ec8638568685671185d597fcf2d
2024-03-13 17:25:39 -07:00

188 lines
5.3 KiB
TypeScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import React, {
CSSProperties,
Suspense,
lazy,
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';
import {usePrismTheme} from '@docusaurus/theme-common';
import clsx from 'clsx';
import {LiveProvider, LiveEditor, LivePreview, LiveError} from 'react-live';
import EditorToolbar from './EditorToolbar';
import type {FlexStyle} from './FlexStyle';
import type {StyleNode} from './YogaViewer';
import styles from './Playground.module.css';
import useIsBrowser from '@docusaurus/useIsBrowser';
export type Props = Readonly<{
code: string;
height?: CSSProperties['height'];
autoFocus?: boolean;
}>;
export default function Playground({code, height, autoFocus}: Props) {
const prismTheme = usePrismTheme();
const editorScrollRef = useRef<HTMLDivElement>(null);
const isBrowser = useIsBrowser();
const [liveCode, setLiveCode] = useState(code);
const [hasCodeChanged, setHasCodeChanged] = useState(false);
const [scrollbarWidth, setScrollbarWidth] = useState(0);
// Once react-live has hydrated the content-editable area, set focus to it
// if requested
useEffect(() => {
if (autoFocus && hasCodeChanged) {
const codeElem = editorScrollRef?.current?.querySelector('.prism-code');
const sel = window.getSelection();
if (codeElem?.clientHeight && sel != null) {
sel.selectAllChildren(codeElem);
sel.collapseToStart();
}
}
}, [autoFocus, hasCodeChanged]);
useLayoutEffect(() => {
// The toolbar is positioned relative to the outside of the scrolling
// container so it stays in the same place when scrolling, but this means
// it isn't automatically adjusted for scrollbar width. If code change
// causes overflow/scrollbar, adjust its position based on its width progrmatically.
if (editorScrollRef.current) {
setScrollbarWidth(
editorScrollRef.current.offsetWidth -
editorScrollRef.current.clientWidth,
);
}
}, [editorScrollRef, code]);
const heightStyle = height
? ({'--yg-playground-height': height} as React.CSSProperties)
: undefined;
const handleCodeChange = useCallback((code: string) => {
setHasCodeChanged(true);
setLiveCode(code);
}, []);
return (
<LiveProvider
code={liveCode}
theme={prismTheme}
scope={{Node: LiveNode, Layout: RootLiveNode}}>
<div className={styles.wrapper} style={heightStyle}>
<div className={clsx(styles.playgroundRow)}>
<div className={clsx(styles.editorColumn, 'playground-editor')}>
<div className={styles.editorScroll} ref={editorScrollRef}>
<EditorToolbar
code={liveCode}
className={styles.editorToolbar}
style={{paddingRight: scrollbarWidth + 'px'}}
/>
{isBrowser ? (
<LiveEditor
className={clsx(styles.playgroundEditor)}
onChange={handleCodeChange}
/>
) : (
<LiveEditorFallback code={liveCode} />
)}
</div>
</div>
<div className={clsx(styles.previewColumn)}>
<LivePreview className={clsx(styles.livePreview)} />
<LiveError className={clsx(styles.liveError)} />
</div>
</div>
</div>
</LiveProvider>
);
}
/**
* Provides a non-editable approximation of the LiveEditor result, without
* relying on prism rendering, for use during SSR.
* See https://github.com/facebook/docusaurus/issues/9629
*/
function LiveEditorFallback({code}: Readonly<{code: string}>) {
return (
<div className={clsx(styles.playgroundEditor)}>
<pre className={clsx('prism-code', styles.liveEditorFallback)}>
{code}
</pre>
</div>
);
}
type RootLiveNodeProps = Readonly<{
children: React.ReactNode;
config?: {useWebDefaults?: boolean};
}>;
function RootLiveNode({children, config}: RootLiveNodeProps) {
if (React.Children.count(children) !== 1) {
return null;
}
const child = React.Children.only(children);
if (!React.isValidElement(child) || child.type !== LiveNode) {
return null;
}
const styleNode = styleNodeFromLiveNode(child as unknown as LiveNode);
return (
<Suspense fallback={null}>
<LazyYogaViewer
rootNode={styleNode}
useWebDefaults={config?.useWebDefaults}
/>
</Suspense>
);
}
type LiveNodeProps = Readonly<{
children: React.ReactNode;
style: FlexStyle;
}>;
class LiveNode extends React.PureComponent<LiveNodeProps> {}
function styleNodeFromLiveNode(
liveNode: React.ElementRef<typeof LiveNode>,
): StyleNode {
const children: StyleNode[] = [];
React.Children.forEach(liveNode.props.children, child => {
if (React.isValidElement(child) && child.type === LiveNode) {
children.push(styleNodeFromLiveNode(child as unknown as LiveNode));
}
});
return {
style: liveNode.props.style,
children,
};
}
// Docusaurus SSR does not correctly support top-level await in the import
// chain
// 1. https://github.com/facebook/docusaurus/issues/7238
// 2. https://github.com/facebook/docusaurus/issues/9468
const LazyYogaViewer = lazy(() => import('./YogaViewer'));