Enable lints for React Components (#1515)

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

The out-of-the-box docusaurus template doesn't enable linting for React components. This enables those, fixes the errors, and does dome cleanup around the area (e.g. autofocus is a lot more sane).

Reviewed By: vincentriemer

Differential Revision: D52156109

fbshipit-source-id: f32fede3ec4f8a42ecb7f9d77caa2a30581f35ee
This commit is contained in:
Nick Gerleman
2023-12-15 22:46:58 -08:00
committed by Facebook GitHub Bot
parent bac80cafba
commit baf95897cb
8 changed files with 864 additions and 183 deletions

View File

@@ -0,0 +1,26 @@
/**
* 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
*/
module.exports = {
root: false,
extends: [
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
],
settings: {
react: {
version: 'detect',
},
},
rules: {
'react/prop-types': 'off',
'react/no-unstable-nested-components': 'error',
},
};

View File

@@ -31,7 +31,9 @@
"devDependencies": { "devDependencies": {
"@docusaurus/module-type-aliases": "3.0.0", "@docusaurus/module-type-aliases": "3.0.0",
"@docusaurus/tsconfig": "3.0.0", "@docusaurus/tsconfig": "3.0.0",
"@docusaurus/types": "3.0.0" "@docusaurus/types": "3.0.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0"
}, },
"browserslist": "> 0.5%, last 2 versions, Firefox ESR, not dead", "browserslist": "> 0.5%, last 2 versions, Firefox ESR, not dead",
"engines": { "engines": {

View File

@@ -17,29 +17,26 @@ import SuccessIcon from '@theme/Icon/Success';
import styles from './EditorToolbar.module.css'; import styles from './EditorToolbar.module.css';
export type Props = Readonly<{ export type Props = Readonly<{
getCode: () => string; code: string;
className?: string; className?: string;
style?: React.CSSProperties; style?: React.CSSProperties;
}>; }>;
export default function EditorToolbar({ export default function EditorToolbar({
getCode, code,
className, className,
style, style,
}: Props): JSX.Element { }: Props): JSX.Element {
const handleCopy = useCallback( const handleCopy = useCallback(() => {
() => navigator.clipboard.writeText(getCode()), navigator.clipboard.writeText(code);
[], }, [code]);
);
const handleShare = useCallback( const handleShare = useCallback(() => {
() => navigator.clipboard.writeText(
navigator.clipboard.writeText( window.location.origin +
window.location.origin + `/playground?code=${encodeURIComponent(btoa(code))}`,
`/playground?code=${encodeURIComponent(btoa(getCode()))}`, );
), }, [code]);
[],
);
return ( return (
<div className={clsx(styles.toolbar, className)} style={style}> <div className={clsx(styles.toolbar, className)} style={style}>
@@ -71,7 +68,7 @@ function ToolbarButton({
copyTimeout.current = window.setTimeout(() => { copyTimeout.current = window.setTimeout(() => {
setIsSuccess(false); setIsSuccess(false);
}, 1000); }, 1000);
}, []); }, [onClick]);
return ( return (
<button <button

View File

@@ -20,7 +20,6 @@ import React, {
import {usePrismTheme} from '@docusaurus/theme-common'; import {usePrismTheme} from '@docusaurus/theme-common';
import clsx from 'clsx'; import clsx from 'clsx';
import nullthrows from 'nullthrows';
import {LiveProvider, LiveEditor, LivePreview, LiveError} from 'react-live'; import {LiveProvider, LiveEditor, LivePreview, LiveError} from 'react-live';
import EditorToolbar from './EditorToolbar'; import EditorToolbar from './EditorToolbar';
@@ -45,82 +44,67 @@ 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 editorScrollRef = useRef<HTMLDivElement>(null); const editorScrollRef = useRef<HTMLDivElement>(null);
const [isLoaded, setIsLoaded] = useState(false);
const [liveCode, setLiveCode] = useState(code ?? defaultCode); const [liveCode, setLiveCode] = useState(code ?? defaultCode);
const [hasCodeChanged, setHasCodeChanged] = useState(false);
const [scrollbarWidth, setScrollbarWidth] = useState(0); const [scrollbarWidth, setScrollbarWidth] = useState(0);
const LivePreviewWrapper = useCallback( // Once react-live has hydrated the content-editable area, set focus to it
(props: React.ComponentProps<'div'>) => { // if requested
useEffect(() => {
setIsLoaded(true);
}, []);
return <div {...props} className={styles.livePreviewWrapper} />;
},
[],
);
useEffect(() => { useEffect(() => {
// TODO: This is hacky and relies on being called after some operation if (autoFocus && hasCodeChanged) {
// "react-live" does which itself can manipulate global focus const codeElem = editorScrollRef?.current?.querySelector('.prism-code');
if (isLoaded && autoFocus) {
const codeElem = playgroundRef?.current?.querySelector('.prism-code');
const sel = window.getSelection(); const sel = window.getSelection();
if (codeElem?.clientHeight && sel != null) { if (codeElem?.clientHeight && sel != null) {
sel.selectAllChildren(codeElem); sel.selectAllChildren(codeElem);
sel.collapseToStart(); sel.collapseToStart();
} }
} }
}, [isLoaded, autoFocus]); }, [autoFocus, hasCodeChanged]);
useLayoutEffect(() => { useLayoutEffect(() => {
// The toolbar is positioned relative to the outside of the scrolling // The toolbar is positioned relative to the outside of the scrolling
// container so it stays in the same place when scrolling, but this means // container so it stays in the same place when scrolling, but this means
// it isn't automatically adjusted for scrollbar width // 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) { if (editorScrollRef.current) {
setScrollbarWidth( setScrollbarWidth(
editorScrollRef.current.offsetWidth - editorScrollRef.current.offsetWidth -
editorScrollRef.current.clientWidth, editorScrollRef.current.clientWidth,
); );
} }
}); }, [editorScrollRef, code]);
const heightStyle = height const heightStyle = height
? ({'--yg-playground-height': height} as React.CSSProperties) ? ({'--yg-playground-height': height} as React.CSSProperties)
: undefined; : undefined;
return ( return (
<LiveProvider code={liveCode} theme={prismTheme} scope={{Layout, Node}}> <LiveProvider
<div className={styles.wrapper} ref={playgroundRef} style={heightStyle}> code={liveCode}
theme={prismTheme}
scope={{Node: LiveNode, Layout: RootLiveNode}}>
<div className={styles.wrapper} style={heightStyle}>
<div className={clsx(styles.playgroundRow, 'container')}> <div className={clsx(styles.playgroundRow, 'container')}>
<div className={clsx(styles.editorColumn, 'playground-editor')}> <div className={clsx(styles.editorColumn, 'playground-editor')}>
<div className={styles.editorScroll} ref={editorScrollRef}> <div className={styles.editorScroll} ref={editorScrollRef}>
<EditorToolbar <EditorToolbar
code={liveCode}
className={styles.editorToolbar} className={styles.editorToolbar}
style={{paddingRight: scrollbarWidth + 'px'}} style={{paddingRight: scrollbarWidth + 'px'}}
getCode={useCallback(
() =>
nullthrows(
playgroundRef.current?.querySelector('.prism-code')
?.textContent,
),
[],
)}
/> />
<LiveEditor <LiveEditor
className={clsx(styles.playgroundEditor)} className={clsx(styles.playgroundEditor)}
onChange={setLiveCode} onChange={useCallback((code: string) => {
setHasCodeChanged(true);
setLiveCode(code);
}, [])}
/> />
</div> </div>
</div> </div>
<div className={clsx(styles.previewColumn)}> <div className={clsx(styles.previewColumn)}>
<LivePreview <LivePreview className={clsx(styles.livePreview)} />
className={clsx(styles.livePreview)}
Component={LivePreviewWrapper}
/>
<LiveError className={clsx(styles.liveError)} /> <LiveError className={clsx(styles.liveError)} />
</div> </div>
</div> </div>
@@ -129,22 +113,22 @@ export default function Playground({code, height, autoFocus}: Props) {
); );
} }
type LayoutProps = Readonly<{ type RootLiveNodeProps = Readonly<{
children: React.ReactNode; children: React.ReactNode;
config?: {useWebDefaults?: boolean}; config?: {useWebDefaults?: boolean};
}>; }>;
function Layout({children, config}: LayoutProps) { function RootLiveNode({children, config}: RootLiveNodeProps) {
if (React.Children.count(children) !== 1) { if (React.Children.count(children) !== 1) {
return null; return null;
} }
const child = React.Children.only(children); const child = React.Children.only(children);
if (!React.isValidElement(child) || child.type !== Node) { if (!React.isValidElement(child) || child.type !== LiveNode) {
return null; return null;
} }
const styleNode = styleNodeFromYogaNode(child as unknown as Node); const styleNode = styleNodeFromLiveNode(child as unknown as LiveNode);
return ( return (
<Suspense fallback={null}> <Suspense fallback={null}>
@@ -156,26 +140,26 @@ function Layout({children, config}: LayoutProps) {
); );
} }
type NodeProps = Readonly<{ type LiveNodeProps = Readonly<{
children: React.ReactNode; children: React.ReactNode;
style: FlexStyle; style: FlexStyle;
}>; }>;
class Node extends React.PureComponent<NodeProps> {} class LiveNode extends React.PureComponent<LiveNodeProps> {}
function styleNodeFromYogaNode( function styleNodeFromLiveNode(
yogaNode: React.ElementRef<typeof Node>, liveNode: React.ElementRef<typeof LiveNode>,
): StyleNode { ): StyleNode {
const children: StyleNode[] = []; const children: StyleNode[] = [];
React.Children.forEach(yogaNode.props.children, child => { React.Children.forEach(liveNode.props.children, child => {
if (React.isValidElement(child) && child.type === Node) { if (React.isValidElement(child) && child.type === LiveNode) {
children.push(styleNodeFromYogaNode(child as unknown as Node)); children.push(styleNodeFromLiveNode(child as unknown as LiveNode));
} }
}); });
return { return {
style: yogaNode.props.style, style: liveNode.props.style,
children, children,
}; };
} }

View File

@@ -7,7 +7,7 @@
* @format * @format
*/ */
import React, {useMemo} from 'react'; import {useMemo} from 'react';
import Yoga, {Direction, Overflow, Node as YogaNode} from 'yoga-layout'; import Yoga, {Direction, Overflow, Node as YogaNode} from 'yoga-layout';
import {FlexStyle, applyStyle} from './FlexStyle'; import {FlexStyle, applyStyle} from './FlexStyle';
import LayoutBox from './LayoutBox'; import LayoutBox from './LayoutBox';
@@ -36,7 +36,7 @@ export default function YogaViewer({
}: Props) { }: Props) {
const layout = useMemo( const layout = useMemo(
() => layoutStyleTree(rootNode, width, height, {useWebDefaults}), () => layoutStyleTree(rootNode, width, height, {useWebDefaults}),
[rootNode, width, height], [rootNode, width, height, useWebDefaults],
); );
return <LayoutBox metrics={layout} depth={0} className={className} />; return <LayoutBox metrics={layout} depth={0} className={className} />;
} }

View File

@@ -5,7 +5,6 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import React from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import Link from '@docusaurus/Link'; import Link from '@docusaurus/Link';
import Layout from '@theme/Layout'; import Layout from '@theme/Layout';

View File

@@ -5,7 +5,6 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import React from 'react';
import Layout from '@theme/Layout'; import Layout from '@theme/Layout';
import {useLocation} from '@docusaurus/router'; import {useLocation} from '@docusaurus/router';

904
yarn.lock

File diff suppressed because it is too large Load Diff