Fix playground handling of visible scrollbars (#1514)

Summary:
Pull Request resolved: https://github.com/facebook/yoga/pull/1514

On machines with scrollbars, we shouldn't show them unconditionally, and the toolbar should be in the area within them.

This also fixes a couple bugs:
1. Preview not rendering based on correct code when light/dark mode changes
2. Crash on start on mobile safari
3. Incorrect rendering of preview on mobile safari

This also fixes a bug where the playground re-rendering (e.g. on theme change) makes the preview snap back to the initial code passes.

https://yoga-website-next-git-fork-nickgerleman-exp-194d90-fbopensource.vercel.app/

Reviewed By: shwanton

Differential Revision: D52145666

fbshipit-source-id: 50184305987aab4cbcd066f37582997dfdc78c02
This commit is contained in:
Nick Gerleman
2023-12-13 20:05:29 -08:00
committed by Facebook GitHub Bot
parent 738d04fcb0
commit 43cb24fdce
5 changed files with 73 additions and 36 deletions

View File

@@ -8,9 +8,6 @@
.toolbar { .toolbar {
display: flex; display: flex;
column-gap: 8px; column-gap: 8px;
position: absolute;
top: 10px;
right: 10px;
} }
.toolbar button { .toolbar button {

View File

@@ -18,9 +18,15 @@ import styles from './EditorToolbar.module.css';
export type Props = Readonly<{ export type Props = Readonly<{
getCode: () => string; getCode: () => string;
className?: string;
style?: React.CSSProperties;
}>; }>;
export default function EditorToolbar({getCode}: Props): JSX.Element { export default function EditorToolbar({
getCode,
className,
style,
}: Props): JSX.Element {
const handleCopy = useCallback( const handleCopy = useCallback(
() => navigator.clipboard.writeText(getCode()), () => navigator.clipboard.writeText(getCode()),
[], [],
@@ -36,7 +42,7 @@ export default function EditorToolbar({getCode}: Props): JSX.Element {
); );
return ( return (
<div className={clsx(styles.toolbar)}> <div className={clsx(styles.toolbar, className)} style={style}>
<ToolbarButton Icon={CopyIcon} label="Copy" onClick={handleCopy} /> <ToolbarButton Icon={CopyIcon} label="Copy" onClick={handleCopy} />
<ToolbarButton Icon={LinkIcon} label="Share" onClick={handleShare} /> <ToolbarButton Icon={LinkIcon} label="Share" onClick={handleShare} />
</div> </div>

View File

@@ -26,30 +26,42 @@ html[data-theme='dark'] {
background-color: var(--yg-color-playground-background); background-color: var(--yg-color-playground-background);
} }
.editorColumn {
position: relative;
flex: 8;
min-width: 0;
}
.playgroundRow { .playgroundRow {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
column-gap: 16px; column-gap: 16px;
} }
.editorColumn {
flex: 8;
min-width: 0;
overflow-y: auto;
border: 1px solid var(--yg-color-editor-border);
border-radius: var(--ifm-pre-border-radius);
position: relative;
}
.editorScroll {
overflow-y: auto;
}
.editorToolbar {
position: absolute;
top: 10px;
right: 10px;
}
.playgroundEditor { .playgroundEditor {
font: var(--ifm-code-font-size) / var(--ifm-pre-line-height) font: var(--ifm-code-font-size) / var(--ifm-pre-line-height)
var(--ifm-font-family-monospace) !important; var(--ifm-font-family-monospace) !important;
direction: ltr; direction: ltr;
height: calc(var(--yg-playground-height, 400px) - 32px); height: calc(var(--yg-playground-height, 400px) - 32px);
overflow: scroll;
} }
.playgroundEditor :global(.prism-code) { .playgroundEditor :global(.prism-code) {
height: 100%;
box-shadow: var(--ifm-global-shadow-lw); box-shadow: var(--ifm-global-shadow-lw);
border: 1px solid var(--yg-color-editor-border); min-height: 100%;
border-radius: 0;
} }
.previewColumn { .previewColumn {
@@ -86,8 +98,7 @@ html[data-theme='dark'] {
} }
.playgroundEditor { .playgroundEditor {
height: max-content; height: unset;
overflow: visible;
} }
.playgroundRow { .playgroundRow {
@@ -97,12 +108,13 @@ html[data-theme='dark'] {
.editorColumn { .editorColumn {
padding: 0; padding: 0;
flex: 0 !important;
margin-bottom: 10px; margin-bottom: 10px;
flex: unset;
} }
.previewColumn { .previewColumn {
padding: 10px; padding: 10px;
width: 100%; width: 100%;
flex: unset;
} }
} }

View File

@@ -13,6 +13,7 @@ import React, {
lazy, lazy,
useCallback, useCallback,
useEffect, useEffect,
useLayoutEffect,
useRef, useRef,
useState, useState,
} from 'react'; } from 'react';
@@ -45,7 +46,11 @@ export type Props = Readonly<{
export default function Playground({code, height, autoFocus}: Props) { export default function Playground({code, height, autoFocus}: Props) {
const prismTheme = usePrismTheme(); const prismTheme = usePrismTheme();
const playgroundRef = useRef<HTMLDivElement>(null); const playgroundRef = useRef<HTMLDivElement>(null);
const editorScrollRef = useRef<HTMLDivElement>(null);
const [isLoaded, setIsLoaded] = useState(false); const [isLoaded, setIsLoaded] = useState(false);
const [liveCode, setLiveCode] = useState(code ?? defaultCode);
const [scrollbarWidth, setScrollbarWidth] = useState(0);
const LivePreviewWrapper = useCallback( const LivePreviewWrapper = useCallback(
(props: React.ComponentProps<'div'>) => { (props: React.ComponentProps<'div'>) => {
@@ -64,25 +69,38 @@ export default function Playground({code, height, autoFocus}: Props) {
if (isLoaded && autoFocus) { if (isLoaded && autoFocus) {
const codeElem = playgroundRef?.current?.querySelector('.prism-code'); const codeElem = playgroundRef?.current?.querySelector('.prism-code');
const sel = window.getSelection(); const sel = window.getSelection();
if (codeElem != null && sel != null) { if (codeElem?.clientHeight && sel != null) {
sel.selectAllChildren(codeElem); sel.selectAllChildren(codeElem);
sel.collapseToStart(); sel.collapseToStart();
} }
} }
}, [isLoaded, autoFocus]); }, [isLoaded, autoFocus]);
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 (editorScrollRef.current) {
setScrollbarWidth(
editorScrollRef.current.offsetWidth -
editorScrollRef.current.clientWidth,
);
}
});
const heightStyle = height const heightStyle = height
? ({'--yg-playground-height': height} as React.CSSProperties) ? ({'--yg-playground-height': height} as React.CSSProperties)
: undefined; : undefined;
const resolvedCode = code ?? defaultCode;
return ( return (
<LiveProvider code={resolvedCode} theme={prismTheme} scope={{Layout, Node}}> <LiveProvider code={liveCode} theme={prismTheme} scope={{Layout, Node}}>
<div className={styles.wrapper} ref={playgroundRef} style={heightStyle}> <div className={styles.wrapper} ref={playgroundRef} style={heightStyle}>
<div className={clsx(styles.playgroundRow, 'container')}> <div className={clsx(styles.playgroundRow, 'container')}>
<div className={clsx(styles.editorColumn)}> <div className={clsx(styles.editorColumn, 'playground-editor')}>
<div className={styles.editorScroll} ref={editorScrollRef}>
<EditorToolbar <EditorToolbar
className={styles.editorToolbar}
style={{paddingRight: scrollbarWidth + 'px'}}
getCode={useCallback( getCode={useCallback(
() => () =>
nullthrows( nullthrows(
@@ -92,7 +110,11 @@ export default function Playground({code, height, autoFocus}: Props) {
[], [],
)} )}
/> />
<LiveEditor className={clsx(styles.playgroundEditor)} /> <LiveEditor
className={clsx(styles.playgroundEditor)}
onChange={setLiveCode}
/>
</div>
</div> </div>
<div className={clsx(styles.previewColumn)}> <div className={clsx(styles.previewColumn)}>
<LivePreview <LivePreview

View File

@@ -24,7 +24,7 @@
display: none; display: none;
} }
.playgroundSection :global(.prism-code) { .playgroundSection :global(.playground-editor) {
display: none; display: none;
} }
} }