Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable lints for React Components #1515

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions website-next/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -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',
},
};
4 changes: 3 additions & 1 deletion website-next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
27 changes: 12 additions & 15 deletions website-next/src/components/EditorToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
() =>
navigator.clipboard.writeText(
window.location.origin +
`/playground?code=${encodeURIComponent(btoa(getCode()))}`,
),
[],
);
const handleShare = useCallback(() => {
navigator.clipboard.writeText(
window.location.origin +
`/playground?code=${encodeURIComponent(btoa(code))}`,
);
}, [code]);

return (
<div className={clsx(styles.toolbar, className)} style={style}>
Expand Down Expand Up @@ -71,7 +68,7 @@ function ToolbarButton({
copyTimeout.current = window.setTimeout(() => {
setIsSuccess(false);
}, 1000);
}, []);
}, [onClick]);

return (
<button
Expand Down
80 changes: 32 additions & 48 deletions website-next/src/components/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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'>) => {
useEffect(() => {
setIsLoaded(true);
}, []);

return <div {...props} className={styles.livePreviewWrapper} />;
},
[],
);

// Once react-live has hydrated the content-editable area, set focus to it
// if requested
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>
Expand All @@ -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}>
Expand All @@ -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,
};
}
Expand Down
4 changes: 2 additions & 2 deletions website-next/src/components/YogaViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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} />;
}
Expand Down
1 change: 0 additions & 1 deletion website-next/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
1 change: 0 additions & 1 deletion website-next/src/pages/playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Loading
Loading