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:
committed by
Facebook GitHub Bot
parent
bac80cafba
commit
baf95897cb
26
website-next/.eslintrc.cjs
Normal file
26
website-next/.eslintrc.cjs
Normal 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',
|
||||
},
|
||||
};
|
@@ -31,7 +31,9 @@
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "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",
|
||||
"engines": {
|
||||
|
@@ -17,29 +17,26 @@ import SuccessIcon from '@theme/Icon/Success';
|
||||
import styles from './EditorToolbar.module.css';
|
||||
|
||||
export type Props = Readonly<{
|
||||
getCode: () => string;
|
||||
code: string;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}>;
|
||||
|
||||
export default function EditorToolbar({
|
||||
getCode,
|
||||
code,
|
||||
className,
|
||||
style,
|
||||
}: Props): JSX.Element {
|
||||
const handleCopy = useCallback(
|
||||
() => navigator.clipboard.writeText(getCode()),
|
||||
[],
|
||||
);
|
||||
const handleCopy = useCallback(() => {
|
||||
navigator.clipboard.writeText(code);
|
||||
}, [code]);
|
||||
|
||||
const handleShare = useCallback(
|
||||
() =>
|
||||
const handleShare = useCallback(() => {
|
||||
navigator.clipboard.writeText(
|
||||
window.location.origin +
|
||||
`/playground?code=${encodeURIComponent(btoa(getCode()))}`,
|
||||
),
|
||||
[],
|
||||
`/playground?code=${encodeURIComponent(btoa(code))}`,
|
||||
);
|
||||
}, [code]);
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.toolbar, className)} style={style}>
|
||||
@@ -71,7 +68,7 @@ function ToolbarButton({
|
||||
copyTimeout.current = window.setTimeout(() => {
|
||||
setIsSuccess(false);
|
||||
}, 1000);
|
||||
}, []);
|
||||
}, [onClick]);
|
||||
|
||||
return (
|
||||
<button
|
||||
|
@@ -20,7 +20,6 @@ import React, {
|
||||
|
||||
import {usePrismTheme} from '@docusaurus/theme-common';
|
||||
import clsx from 'clsx';
|
||||
import nullthrows from 'nullthrows';
|
||||
import {LiveProvider, LiveEditor, LivePreview, LiveError} from 'react-live';
|
||||
import EditorToolbar from './EditorToolbar';
|
||||
|
||||
@@ -45,82 +44,67 @@ export type Props = Readonly<{
|
||||
|
||||
export default function Playground({code, height, autoFocus}: Props) {
|
||||
const prismTheme = usePrismTheme();
|
||||
const playgroundRef = useRef<HTMLDivElement>(null);
|
||||
const editorScrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const [liveCode, setLiveCode] = useState(code ?? defaultCode);
|
||||
const [hasCodeChanged, setHasCodeChanged] = useState(false);
|
||||
const [scrollbarWidth, setScrollbarWidth] = useState(0);
|
||||
|
||||
const LivePreviewWrapper = useCallback(
|
||||
(props: React.ComponentProps<'div'>) => {
|
||||
// Once react-live has hydrated the content-editable area, set focus to it
|
||||
// if requested
|
||||
useEffect(() => {
|
||||
setIsLoaded(true);
|
||||
}, []);
|
||||
|
||||
return <div {...props} className={styles.livePreviewWrapper} />;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: This is hacky and relies on being called after some operation
|
||||
// "react-live" does which itself can manipulate global focus
|
||||
if (isLoaded && autoFocus) {
|
||||
const codeElem = playgroundRef?.current?.querySelector('.prism-code');
|
||||
if (autoFocus && hasCodeChanged) {
|
||||
const codeElem = editorScrollRef?.current?.querySelector('.prism-code');
|
||||
const sel = window.getSelection();
|
||||
if (codeElem?.clientHeight && sel != null) {
|
||||
sel.selectAllChildren(codeElem);
|
||||
sel.collapseToStart();
|
||||
}
|
||||
}
|
||||
}, [isLoaded, autoFocus]);
|
||||
}, [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
|
||||
// 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;
|
||||
|
||||
return (
|
||||
<LiveProvider code={liveCode} theme={prismTheme} scope={{Layout, Node}}>
|
||||
<div className={styles.wrapper} ref={playgroundRef} style={heightStyle}>
|
||||
<LiveProvider
|
||||
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.editorColumn, 'playground-editor')}>
|
||||
<div className={styles.editorScroll} ref={editorScrollRef}>
|
||||
<EditorToolbar
|
||||
code={liveCode}
|
||||
className={styles.editorToolbar}
|
||||
style={{paddingRight: scrollbarWidth + 'px'}}
|
||||
getCode={useCallback(
|
||||
() =>
|
||||
nullthrows(
|
||||
playgroundRef.current?.querySelector('.prism-code')
|
||||
?.textContent,
|
||||
),
|
||||
[],
|
||||
)}
|
||||
/>
|
||||
<LiveEditor
|
||||
className={clsx(styles.playgroundEditor)}
|
||||
onChange={setLiveCode}
|
||||
onChange={useCallback((code: string) => {
|
||||
setHasCodeChanged(true);
|
||||
setLiveCode(code);
|
||||
}, [])}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx(styles.previewColumn)}>
|
||||
<LivePreview
|
||||
className={clsx(styles.livePreview)}
|
||||
Component={LivePreviewWrapper}
|
||||
/>
|
||||
<LivePreview className={clsx(styles.livePreview)} />
|
||||
<LiveError className={clsx(styles.liveError)} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -129,22 +113,22 @@ export default function Playground({code, height, autoFocus}: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
type LayoutProps = Readonly<{
|
||||
type RootLiveNodeProps = Readonly<{
|
||||
children: React.ReactNode;
|
||||
config?: {useWebDefaults?: boolean};
|
||||
}>;
|
||||
|
||||
function Layout({children, config}: LayoutProps) {
|
||||
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 !== Node) {
|
||||
if (!React.isValidElement(child) || child.type !== LiveNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const styleNode = styleNodeFromYogaNode(child as unknown as Node);
|
||||
const styleNode = styleNodeFromLiveNode(child as unknown as LiveNode);
|
||||
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
@@ -156,26 +140,26 @@ function Layout({children, config}: LayoutProps) {
|
||||
);
|
||||
}
|
||||
|
||||
type NodeProps = Readonly<{
|
||||
type LiveNodeProps = Readonly<{
|
||||
children: React.ReactNode;
|
||||
style: FlexStyle;
|
||||
}>;
|
||||
|
||||
class Node extends React.PureComponent<NodeProps> {}
|
||||
class LiveNode extends React.PureComponent<LiveNodeProps> {}
|
||||
|
||||
function styleNodeFromYogaNode(
|
||||
yogaNode: React.ElementRef<typeof Node>,
|
||||
function styleNodeFromLiveNode(
|
||||
liveNode: React.ElementRef<typeof LiveNode>,
|
||||
): StyleNode {
|
||||
const children: StyleNode[] = [];
|
||||
|
||||
React.Children.forEach(yogaNode.props.children, child => {
|
||||
if (React.isValidElement(child) && child.type === Node) {
|
||||
children.push(styleNodeFromYogaNode(child as unknown as Node));
|
||||
React.Children.forEach(liveNode.props.children, child => {
|
||||
if (React.isValidElement(child) && child.type === LiveNode) {
|
||||
children.push(styleNodeFromLiveNode(child as unknown as LiveNode));
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
style: yogaNode.props.style,
|
||||
style: liveNode.props.style,
|
||||
children,
|
||||
};
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {useMemo} from 'react';
|
||||
import Yoga, {Direction, Overflow, Node as YogaNode} from 'yoga-layout';
|
||||
import {FlexStyle, applyStyle} from './FlexStyle';
|
||||
import LayoutBox from './LayoutBox';
|
||||
@@ -36,7 +36,7 @@ export default function YogaViewer({
|
||||
}: Props) {
|
||||
const layout = useMemo(
|
||||
() => layoutStyleTree(rootNode, width, height, {useWebDefaults}),
|
||||
[rootNode, width, height],
|
||||
[rootNode, width, height, useWebDefaults],
|
||||
);
|
||||
return <LayoutBox metrics={layout} depth={0} className={className} />;
|
||||
}
|
||||
|
@@ -5,7 +5,6 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import Link from '@docusaurus/Link';
|
||||
import Layout from '@theme/Layout';
|
||||
|
@@ -5,7 +5,6 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Layout from '@theme/Layout';
|
||||
import {useLocation} from '@docusaurus/router';
|
||||
|
||||
|
Reference in New Issue
Block a user