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": {
|
"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": {
|
||||||
|
@@ -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(getCode()))}`,
|
`/playground?code=${encodeURIComponent(btoa(code))}`,
|
||||||
),
|
|
||||||
[],
|
|
||||||
);
|
);
|
||||||
|
}, [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
|
||||||
|
@@ -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(() => {
|
useEffect(() => {
|
||||||
setIsLoaded(true);
|
if (autoFocus && hasCodeChanged) {
|
||||||
}, []);
|
const codeElem = editorScrollRef?.current?.querySelector('.prism-code');
|
||||||
|
|
||||||
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');
|
|
||||||
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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -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} />;
|
||||||
}
|
}
|
||||||
|
@@ -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';
|
||||||
|
@@ -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';
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user