diff --git a/addons/a11y/src/components/ColorBlindness.tsx b/addons/a11y/src/components/ColorBlindness.tsx index 50e6a35f14dc..9d643a797f4a 100644 --- a/addons/a11y/src/components/ColorBlindness.tsx +++ b/addons/a11y/src/components/ColorBlindness.tsx @@ -1,5 +1,5 @@ import { document } from 'global'; -import React, { ReactNode, useState } from 'react'; +import React, { FunctionComponent, ReactNode, useState } from 'react'; import memoize from 'memoizerific'; import { styled } from '@storybook/theming'; @@ -79,7 +79,7 @@ const getColorList = (active: string | null, set: (i: string | null) => void): L })), ]; -export const ColorBlindness: React.FC = () => { +export const ColorBlindness: FunctionComponent = () => { const [active, setActiveState] = useState(null); const setActive = (activeState: string | null): void => { diff --git a/addons/a11y/src/components/Report/HighlightToggle.tsx b/addons/a11y/src/components/Report/HighlightToggle.tsx index bb58a5afb289..5d075d8e075a 100644 --- a/addons/a11y/src/components/Report/HighlightToggle.tsx +++ b/addons/a11y/src/components/Report/HighlightToggle.tsx @@ -1,5 +1,5 @@ import { document } from 'global'; -import React, { Component } from 'react'; +import React, { Component, createRef } from 'react'; import { connect } from 'react-redux'; import { styled, themes, convert } from '@storybook/theming'; import memoize from 'memoizerific'; @@ -100,7 +100,7 @@ class HighlightToggle extends Component { elementsToHighlight: [], }; - private checkBoxRef = React.createRef(); + private checkBoxRef = createRef(); componentDidMount() { const { elementsToHighlight, highlightedElementsMap } = this.props; diff --git a/addons/actions/src/manager.tsx b/addons/actions/src/manager.tsx index d77272be077b..a7bfd9dbf596 100644 --- a/addons/actions/src/manager.tsx +++ b/addons/actions/src/manager.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import addons from '@storybook/addons'; import ActionLogger from './containers/ActionLogger'; import { ADDON_ID, PANEL_ID, PARAM_KEY } from './constants'; diff --git a/addons/contexts/src/manager/components/ToolBar.tsx b/addons/contexts/src/manager/components/ToolBar.tsx index 16a04113c843..863d36aa9903 100644 --- a/addons/contexts/src/manager/components/ToolBar.tsx +++ b/addons/contexts/src/manager/components/ToolBar.tsx @@ -1,4 +1,4 @@ -import React, { ComponentProps } from 'react'; +import React, { ComponentProps, memo } from 'react'; import { Separator } from '@storybook/components'; import { ToolBarControl } from './ToolBarControl'; import { ContextNode, FCNoChildren, SelectionState } from '../../shared/types.d'; @@ -9,7 +9,7 @@ type ToolBar = FCNoChildren<{ setSelected: ComponentProps['setSelected']; }>; -export const ToolBar: ToolBar = React.memo(({ nodes, state, setSelected }) => +export const ToolBar: ToolBar = memo(({ nodes, state, setSelected }) => nodes.length ? ( <> diff --git a/addons/contexts/src/manager/components/ToolBarControl.tsx b/addons/contexts/src/manager/components/ToolBarControl.tsx index 42c315ef470e..c6c42b761300 100644 --- a/addons/contexts/src/manager/components/ToolBarControl.tsx +++ b/addons/contexts/src/manager/components/ToolBarControl.tsx @@ -1,7 +1,7 @@ -import React from 'react'; +import React, { useState } from 'react'; import { ToolBarMenu } from './ToolBarMenu'; import { OPT_OUT } from '../../shared/constants'; -import { ContextNode, FCNoChildren, Omit } from '../../shared/types.d'; +import { ContextNode, FCNoChildren } from '../../shared/types.d'; type ToolBarControl = FCNoChildren< Omit< @@ -22,7 +22,7 @@ export const ToolBarControl: ToolBarControl = ({ selected, setSelected, }) => { - const [expanded, setExpanded] = React.useState(false); + const [expanded, setExpanded] = useState(false); const paramNames = params.map(({ name }) => name); const activeName = // validate the integrity of the selected name diff --git a/addons/contexts/src/manager/components/ToolBarMenuOptions.tsx b/addons/contexts/src/manager/components/ToolBarMenuOptions.tsx index d0929d7b3e2d..1e1f0f3ed378 100644 --- a/addons/contexts/src/manager/components/ToolBarMenuOptions.tsx +++ b/addons/contexts/src/manager/components/ToolBarMenuOptions.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import { TooltipLinkList } from '@storybook/components'; import { OPT_OUT } from '../../shared/constants'; import { FCNoChildren } from '../../shared/types.d'; diff --git a/addons/contexts/src/preview/frameworks/react.ts b/addons/contexts/src/preview/frameworks/react.ts index 2984951dc614..d2a0458bbccb 100644 --- a/addons/contexts/src/preview/frameworks/react.ts +++ b/addons/contexts/src/preview/frameworks/react.ts @@ -1,4 +1,4 @@ -import React from 'react'; +import { createElement, ReactElement } from 'react'; import { createAddonDecorator, Render } from '../../index'; import { ContextsPreviewAPI } from '../ContextsPreviewAPI'; @@ -6,9 +6,9 @@ import { ContextsPreviewAPI } from '../ContextsPreviewAPI'; * This is the framework specific bindings for React. * '@storybook/react' expects the returning object from a decorator to be a 'React Element' (vNode). */ -export const renderReact: Render = (contextNodes, propsMap, getStoryVNode) => { +export const renderReact: Render = (contextNodes, propsMap, getStoryVNode) => { const { getRendererFrom } = ContextsPreviewAPI(); - return getRendererFrom(React.createElement)(contextNodes, propsMap, getStoryVNode); + return getRendererFrom(createElement)(contextNodes, propsMap, getStoryVNode); }; export const withContexts = createAddonDecorator(renderReact); diff --git a/addons/contexts/src/shared/types.d.ts b/addons/contexts/src/shared/types.d.ts index e52027bea0a8..a60159fcb1a8 100644 --- a/addons/contexts/src/shared/types.d.ts +++ b/addons/contexts/src/shared/types.d.ts @@ -6,7 +6,6 @@ export { API as ManagerAPI } from '@storybook/api'; // helpers export declare type AnyFunctionReturns = (...arg: any[]) => T; export declare type FCNoChildren

= FunctionComponent<{ children?: never } & P>; -export declare type Omit = Pick>; export declare type GenericProp = null | { readonly [key: string]: unknown; }; diff --git a/addons/cssresources/src/register.tsx b/addons/cssresources/src/register.tsx index 4366ea61801a..8637d1ca5664 100644 --- a/addons/cssresources/src/register.tsx +++ b/addons/cssresources/src/register.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import { addons, types } from '@storybook/addons'; import { ADDON_ID, PANEL_ID, PARAM_KEY } from './constants'; diff --git a/addons/design-assets/src/register.tsx b/addons/design-assets/src/register.tsx index fb903ec514e5..61ae6a9c77ff 100644 --- a/addons/design-assets/src/register.tsx +++ b/addons/design-assets/src/register.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import { addons, types } from '@storybook/addons'; import { AddonPanel } from '@storybook/components'; diff --git a/addons/docs/src/blocks/Anchor.tsx b/addons/docs/src/blocks/Anchor.tsx index 6e7dd721541f..912f20258eeb 100644 --- a/addons/docs/src/blocks/Anchor.tsx +++ b/addons/docs/src/blocks/Anchor.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { FunctionComponent } from 'react'; export const anchorBlockIdFromId = (storyId: string) => `anchor--${storyId}`; @@ -6,6 +6,6 @@ export interface AnchorProps { storyId: string; } -export const Anchor: React.FC = ({ storyId, children }) => ( +export const Anchor: FunctionComponent = ({ storyId, children }) => (

{children}
); diff --git a/addons/docs/src/blocks/Description.tsx b/addons/docs/src/blocks/Description.tsx index c26ff425a5bf..7e569e393452 100644 --- a/addons/docs/src/blocks/Description.tsx +++ b/addons/docs/src/blocks/Description.tsx @@ -1,5 +1,5 @@ /* eslint-disable no-underscore-dangle */ -import React from 'react'; +import React, { FunctionComponent } from 'react'; import { Description, DescriptionProps as PureDescriptionProps } from '@storybook/components'; import { DocsContext, DocsContextProps } from './DocsContext'; import { Component, CURRENT_SELECTION } from './shared'; @@ -66,7 +66,7 @@ ${getDocgen(target) || ''} } }; -const DescriptionContainer: React.FunctionComponent = props => ( +const DescriptionContainer: FunctionComponent = props => ( {context => { const { markdown } = getDescriptionProps(props, context); diff --git a/addons/docs/src/blocks/DocsContainer.tsx b/addons/docs/src/blocks/DocsContainer.tsx index 03bb11b1913c..5d302106994d 100644 --- a/addons/docs/src/blocks/DocsContainer.tsx +++ b/addons/docs/src/blocks/DocsContainer.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { FunctionComponent, useEffect } from 'react'; import { document } from 'global'; import { MDXProvider } from '@mdx-js/react'; import { ThemeProvider, ensure as ensureTheme } from '@storybook/theming'; @@ -15,7 +15,7 @@ interface DocsContainerProps { interface CodeOrSourceProps { className?: string; } -export const CodeOrSource: React.FC = props => { +export const CodeOrSource: FunctionComponent = props => { const { className, children, ...rest } = props; // markdown-to-jsx does not add className to inline code if ( @@ -41,17 +41,14 @@ const defaultComponents = { code: CodeOrSource, }; -export const DocsContainer: React.FunctionComponent = ({ - context, - children, -}) => { +export const DocsContainer: FunctionComponent = ({ context, children }) => { const { id: storyId = null, parameters = {} } = context || {}; const options = parameters.options || {}; const theme = ensureTheme(options.theme); const { components: userComponents = null } = parameters.docs || {}; const components = { ...defaultComponents, ...userComponents }; - React.useEffect(() => { + useEffect(() => { let element = document.getElementById(anchorBlockIdFromId(storyId)); if (!element) { element = document.getElementById(storyBlockIdFromId(storyId)); diff --git a/addons/docs/src/blocks/DocsContext.ts b/addons/docs/src/blocks/DocsContext.ts index ac71a1d545b6..a900ed260773 100644 --- a/addons/docs/src/blocks/DocsContext.ts +++ b/addons/docs/src/blocks/DocsContext.ts @@ -1,4 +1,4 @@ -import React from 'react'; +import { Context, createContext } from 'react'; export interface DocsContextProps { id?: string; @@ -16,4 +16,4 @@ export interface DocsContextProps { forceRender?: () => void; } -export const DocsContext: React.Context = React.createContext({}); +export const DocsContext: Context = createContext({}); diff --git a/addons/docs/src/blocks/DocsPage.tsx b/addons/docs/src/blocks/DocsPage.tsx index 01946495e863..3f4f78f8ebf0 100644 --- a/addons/docs/src/blocks/DocsPage.tsx +++ b/addons/docs/src/blocks/DocsPage.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { FunctionComponent } from 'react'; import { parseKind } from '@storybook/router'; import { DocsPage as PureDocsPage, PropsTable, PropsTableProps } from '@storybook/components'; @@ -79,7 +79,7 @@ const defaultStoriesSlot: StoriesSlot = stories => { const StoriesHeading = H2; const StoryHeading = H3; -const DocsStory: React.FunctionComponent = ({ +const DocsStory: FunctionComponent = ({ id, name, expanded = true, @@ -97,7 +97,7 @@ const DocsStory: React.FunctionComponent = ({ ); -export const DocsPage: React.FunctionComponent = ({ +export const DocsPage: FunctionComponent = ({ titleSlot = defaultTitleSlot, subtitleSlot = defaultSubtitleSlot, descriptionSlot = defaultDescriptionSlot, diff --git a/addons/docs/src/blocks/Meta.tsx b/addons/docs/src/blocks/Meta.tsx index 2e4f022bf10f..49d43ae83b0a 100644 --- a/addons/docs/src/blocks/Meta.tsx +++ b/addons/docs/src/blocks/Meta.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import { FunctionComponent } from 'react'; type Decorator = (...args: any) => any; @@ -14,4 +14,4 @@ interface MetaProps { * and gets transformed into a default export underneath the hood. * It doesn't actually render anything. */ -export const Meta: React.FunctionComponent = props => null; +export const Meta: FunctionComponent = props => null; diff --git a/addons/docs/src/blocks/Preview.tsx b/addons/docs/src/blocks/Preview.tsx index 19276cccb3bb..1482955b446e 100644 --- a/addons/docs/src/blocks/Preview.tsx +++ b/addons/docs/src/blocks/Preview.tsx @@ -1,4 +1,4 @@ -import React, { ReactNodeArray } from 'react'; +import React, { FunctionComponent, ReactElement, ReactNode, ReactNodeArray } from 'react'; import { Preview as PurePreview, PreviewProps as PurePreviewProps } from '@storybook/components'; import { getSourceProps } from './Source'; import { DocsContext, DocsContextProps } from './DocsContext'; @@ -14,11 +14,7 @@ type PreviewProps = PurePreviewProps & { }; const getPreviewProps = ( - { - withSource = SourceState.CLOSED, - children, - ...props - }: PreviewProps & { children?: React.ReactNode }, + { withSource = SourceState.CLOSED, children, ...props }: PreviewProps & { children?: ReactNode }, { mdxStoryNameToId, storyStore }: DocsContextProps ): PurePreviewProps => { if (withSource === SourceState.NONE && !children) { @@ -26,8 +22,8 @@ const getPreviewProps = ( } const childArray: ReactNodeArray = Array.isArray(children) ? children : [children]; const stories = childArray.filter( - (c: React.ReactElement) => c.props && (c.props.id || c.props.name) - ) as React.ReactElement[]; + (c: ReactElement) => c.props && (c.props.id || c.props.name) + ) as ReactElement[]; const targetIds = stories.map(s => s.props.id || mdxStoryNameToId[s.props.name]); const sourceProps = getSourceProps({ ids: targetIds }, { storyStore }); return { @@ -37,7 +33,7 @@ const getPreviewProps = ( }; }; -export const Preview: React.FunctionComponent = props => ( +export const Preview: FunctionComponent = props => ( {context => { const previewProps = getPreviewProps(props, context); diff --git a/addons/docs/src/blocks/Props.tsx b/addons/docs/src/blocks/Props.tsx index b618fb150742..ab4769476adf 100644 --- a/addons/docs/src/blocks/Props.tsx +++ b/addons/docs/src/blocks/Props.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { FunctionComponent } from 'react'; import { PropsTable, PropsTableError, PropsTableProps, PropDef } from '@storybook/components'; import { DocsContext, DocsContextProps } from './DocsContext'; import { Component, CURRENT_SELECTION } from './shared'; @@ -44,7 +44,7 @@ export const getPropsTableProps = ( } }; -const PropsContainer: React.FunctionComponent = props => ( +const PropsContainer: FunctionComponent = props => ( {context => { const propsTableProps = getPropsTableProps(props, context); diff --git a/addons/docs/src/blocks/Source.tsx b/addons/docs/src/blocks/Source.tsx index 8ba904787f72..dbeecd969059 100644 --- a/addons/docs/src/blocks/Source.tsx +++ b/addons/docs/src/blocks/Source.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { FunctionComponent } from 'react'; import { Source, SourceProps as PureSourceProps, SourceError } from '@storybook/components'; import { DocsContext, DocsContextProps } from './DocsContext'; import { CURRENT_SELECTION } from './shared'; @@ -85,7 +85,7 @@ export const getSourceProps = ( * or the source for a story if `storyId` is provided, or * the source for the current story if nothing is provided. */ -const SourceContainer: React.FunctionComponent = props => ( +const SourceContainer: FunctionComponent = props => ( {context => { const sourceProps = getSourceProps(props, context); diff --git a/addons/docs/src/blocks/Story.tsx b/addons/docs/src/blocks/Story.tsx index 15f75073ceb6..f9a4902b08f4 100644 --- a/addons/docs/src/blocks/Story.tsx +++ b/addons/docs/src/blocks/Story.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { createElement, ElementType, FunctionComponent, ReactNode } from 'react'; import { MDXProvider } from '@mdx-js/react'; import { components as docsComponents } from '@storybook/components/html'; import { Story, StoryProps as PureStoryProps } from '@storybook/components'; @@ -8,9 +8,9 @@ import { DocsContext, DocsContextProps } from './DocsContext'; export const storyBlockIdFromId = (storyId: string) => `story--${storyId}`; -const resetComponents: Record = {}; +const resetComponents: Record = {}; Object.keys(docsComponents).forEach(key => { - resetComponents[key] = (props: any) => React.createElement(key, props); + resetComponents[key] = (props: any) => createElement(key, props); }); interface CommonProps { @@ -20,7 +20,7 @@ interface CommonProps { type StoryDefProps = { name: string; - children: React.ReactNode; + children: ReactNode; } & CommonProps; type StoryRefProps = { @@ -81,7 +81,7 @@ export const getStoryProps = ( }; }; -const StoryContainer: React.FunctionComponent = props => ( +const StoryContainer: FunctionComponent = props => ( {context => { const storyProps = getStoryProps(props, context); diff --git a/addons/docs/src/blocks/Wrapper.tsx b/addons/docs/src/blocks/Wrapper.tsx index a73402f89a57..3c4d80435c7a 100644 --- a/addons/docs/src/blocks/Wrapper.tsx +++ b/addons/docs/src/blocks/Wrapper.tsx @@ -1,9 +1,5 @@ -import React from 'react'; +import React, { FunctionComponent } from 'react'; -interface WrapperProps { - children: React.ReactNode; -} - -export const Wrapper: React.FunctionComponent = ({ children }) => ( +export const Wrapper: FunctionComponent = ({ children }) => (
{children}
); diff --git a/addons/events/src/components/Event.tsx b/addons/events/src/components/Event.tsx index 0367893283be..43759c0c3180 100644 --- a/addons/events/src/components/Event.tsx +++ b/addons/events/src/components/Event.tsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, { ChangeEvent, Component } from 'react'; import PropTypes from 'prop-types'; import { polyfill } from 'react-lifecycles-compat'; import isEqual from 'lodash/isEqual'; @@ -12,7 +12,7 @@ interface StyledTextareaProps { shown: boolean; failed: boolean; value?: string; - onChange?: (event: React.ChangeEvent) => void; + onChange?: (event: ChangeEvent) => void; } const StyledTextarea = styled(({ shown, failed, ...rest }: StyledTextareaProps) => ( @@ -143,7 +143,7 @@ class Item extends Component { prevPayload: null, }; - onChange = ({ target: { value } }: React.ChangeEvent) => { + onChange = ({ target: { value } }: ChangeEvent) => { const newState: Partial = { payloadString: value, }; diff --git a/addons/events/src/manager.tsx b/addons/events/src/manager.tsx index 8b98db2cb2b7..6cc77ebfdadc 100644 --- a/addons/events/src/manager.tsx +++ b/addons/events/src/manager.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import addons from '@storybook/addons'; import Panel from './components/Panel'; diff --git a/addons/graphql/src/preview.tsx b/addons/graphql/src/preview.tsx index 8718e4726436..526aeb7788e5 100644 --- a/addons/graphql/src/preview.tsx +++ b/addons/graphql/src/preview.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import GraphiQL from 'graphiql'; import 'graphiql/graphiql.css'; diff --git a/addons/info/src/__snapshots__/index.test.js.snap b/addons/info/src/__snapshots__/index.test.js.snap index 435e233d9162..a64408c42967 100644 --- a/addons/info/src/__snapshots__/index.test.js.snap +++ b/addons/info/src/__snapshots__/index.test.js.snap @@ -1334,11 +1334,9 @@ exports[`addon Info should render component description if story kind matches co > ) => - class TestProvider extends React.Component { +const provideTests = (Component: ComponentType) => + class TestProvider extends ReactComponent { state: HocState = {}; static defaultProps = { diff --git a/addons/jest/src/register.tsx b/addons/jest/src/register.tsx index a8d3b2c97bad..6537ad61373a 100644 --- a/addons/jest/src/register.tsx +++ b/addons/jest/src/register.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import addons from '@storybook/addons'; import { ADDON_ID, PANEL_ID, PARAM_KEY } from './shared'; diff --git a/addons/knobs/src/register.tsx b/addons/knobs/src/register.tsx index a6753c557a9d..8acbc9a5ea80 100644 --- a/addons/knobs/src/register.tsx +++ b/addons/knobs/src/register.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import addons from '@storybook/addons'; import Panel from './components/Panel'; import { ADDON_ID, PANEL_ID, PARAM_KEY } from './shared'; diff --git a/addons/links/src/react/components/link.tsx b/addons/links/src/react/components/link.tsx index 3d3b3d3c6fef..5627923cb24e 100644 --- a/addons/links/src/react/components/link.tsx +++ b/addons/links/src/react/components/link.tsx @@ -1,4 +1,4 @@ -import React, { PureComponent } from 'react'; +import React, { MouseEvent, PureComponent, ReactNode } from 'react'; import { navigate, hrefTo } from '../../preview'; @@ -8,10 +8,10 @@ import { navigate, hrefTo } from '../../preview'; // Cmd/Ctrl/Shift/Alt + Click should trigger default browser behaviour. Same applies to non-left clicks const LEFT_BUTTON = 0; -const isPlainLeftClick = (e: React.MouseEvent) => +const isPlainLeftClick = (e: MouseEvent) => e.button === LEFT_BUTTON && !e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey; -const cancelled = (e: React.MouseEvent, cb = (_e: any) => {}) => { +const cancelled = (e: MouseEvent, cb = (_e: any) => {}) => { if (isPlainLeftClick(e)) { e.preventDefault(); cb(e); @@ -21,7 +21,7 @@ const cancelled = (e: React.MouseEvent, cb = (_e: interface Props { kind: string; story: string; - children: React.ReactNode; + children: ReactNode; } interface State { diff --git a/addons/ondevice-actions/src/components/ActionLogger/Inspect.tsx b/addons/ondevice-actions/src/components/ActionLogger/Inspect.tsx index fb7ffbe513dd..158fbc2a0aee 100644 --- a/addons/ondevice-actions/src/components/ActionLogger/Inspect.tsx +++ b/addons/ondevice-actions/src/components/ActionLogger/Inspect.tsx @@ -1,6 +1,6 @@ /* eslint-disable react/no-array-index-key */ /* eslint-disable no-nested-ternary */ -import React from 'react'; +import React, { Component } from 'react'; import { Button, View, Text } from 'react-native'; const theme = { @@ -22,7 +22,7 @@ const theme = { ARROW_ANIMATION_DURATION: '0', }; -class Inspect extends React.Component<{ name?: string; value: any }, { expanded: boolean }> { +class Inspect extends Component<{ name?: string; value: any }, { expanded: boolean }> { state = { expanded: false }; render() { diff --git a/addons/ondevice-actions/src/components/ActionLogger/index.tsx b/addons/ondevice-actions/src/components/ActionLogger/index.tsx index efe84ca53e97..1081214c3bf3 100644 --- a/addons/ondevice-actions/src/components/ActionLogger/index.tsx +++ b/addons/ondevice-actions/src/components/ActionLogger/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { FunctionComponent } from 'react'; import { Button, View, Text, ScrollView } from 'react-native'; import { ActionDisplay } from '@storybook/addon-actions'; import Inspect from './Inspect'; @@ -8,7 +8,7 @@ interface ActionLoggerProps { onClear: () => void; } -export const ActionLogger = ({ actions, onClear }: ActionLoggerProps) => ( +export const ActionLogger: FunctionComponent = ({ actions, onClear }) => ( diff --git a/addons/ondevice-actions/src/index.tsx b/addons/ondevice-actions/src/index.tsx index 63b71d68a676..657f6d2dc38f 100644 --- a/addons/ondevice-actions/src/index.tsx +++ b/addons/ondevice-actions/src/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import addons from '@storybook/addons'; import { ADDON_ID, PANEL_ID, PARAM_KEY } from '@storybook/addon-actions'; import ActionLogger from './containers/ActionLogger'; diff --git a/addons/ondevice-backgrounds/src/container.tsx b/addons/ondevice-backgrounds/src/container.tsx index 6bea7b30fc0c..ba57b6d4fd5c 100644 --- a/addons/ondevice-backgrounds/src/container.tsx +++ b/addons/ondevice-backgrounds/src/container.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { Component } from 'react'; import { View } from 'react-native'; import Constants from './constants'; import { Channel } from './BackgroundPanel'; @@ -12,7 +12,7 @@ interface ContainerState { background: string; } -export default class Container extends React.Component { +export default class Container extends Component { constructor(props: ContainerProps) { super(props); this.state = { background: props.initialBackground || '' }; diff --git a/addons/ondevice-backgrounds/src/index.tsx b/addons/ondevice-backgrounds/src/index.tsx index f24ae28826a0..f9d1c8a0b848 100644 --- a/addons/ondevice-backgrounds/src/index.tsx +++ b/addons/ondevice-backgrounds/src/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import addons, { makeDecorator } from '@storybook/addons'; diff --git a/addons/ondevice-backgrounds/src/register.tsx b/addons/ondevice-backgrounds/src/register.tsx index f07741c2ffe5..39e1046517b8 100644 --- a/addons/ondevice-backgrounds/src/register.tsx +++ b/addons/ondevice-backgrounds/src/register.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import addons from '@storybook/addons'; import { ADDON_ID, PANEL_ID, PARAM_KEY } from './constants'; diff --git a/addons/ondevice-notes/src/components/Notes.tsx b/addons/ondevice-notes/src/components/Notes.tsx index adab2e02385a..beacd81be3f1 100644 --- a/addons/ondevice-notes/src/components/Notes.tsx +++ b/addons/ondevice-notes/src/components/Notes.tsx @@ -1,5 +1,5 @@ /* eslint-disable react/destructuring-assignment */ -import React from 'react'; +import React, { Component } from 'react'; import { View } from 'react-native'; import Markdown from 'react-native-simple-markdown'; import { AddonStore } from '@storybook/addons'; @@ -19,7 +19,7 @@ interface NotesState { selection: Selection; } -export class Notes extends React.Component { +export class Notes extends Component { componentDidMount() { this.props.channel.on(Events.SELECT_STORY, this.onStorySelected); } diff --git a/addons/ondevice-notes/src/register.tsx b/addons/ondevice-notes/src/register.tsx index e90460aba88f..fa663581ac21 100644 --- a/addons/ondevice-notes/src/register.tsx +++ b/addons/ondevice-notes/src/register.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import addons from '@storybook/addons'; import { Notes } from './components/Notes'; diff --git a/addons/viewport/src/Tool.tsx b/addons/viewport/src/Tool.tsx index 0ac5d94592fb..2813d119e7b5 100644 --- a/addons/viewport/src/Tool.tsx +++ b/addons/viewport/src/Tool.tsx @@ -1,5 +1,5 @@ /* eslint-disable no-fallthrough */ -import React, { Fragment, ReactNode, useEffect, useRef, FunctionComponent } from 'react'; +import React, { Fragment, ReactNode, useEffect, useRef, FunctionComponent, memo } from 'react'; import memoize from 'memoizerific'; import { styled, Global, Theme, withTheme } from '@storybook/theming'; @@ -122,7 +122,7 @@ const getStyles = ( return isRotated ? flip(result) : result; }; -export const ViewportTool: FunctionComponent<{}> = React.memo( +export const ViewportTool: FunctionComponent = memo( withTheme(({ theme }: { theme: Theme }) => { const { viewports, defaultViewport, disable } = useParameter( PARAM_KEY, diff --git a/addons/viewport/src/register.tsx b/addons/viewport/src/register.tsx index 16fa3aabf0f3..fab48d369678 100644 --- a/addons/viewport/src/register.tsx +++ b/addons/viewport/src/register.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import addons, { types } from '@storybook/addons'; import { ADDON_ID } from './constants'; diff --git a/app/react-native/src/preview/components/OnDeviceUI/addons/index.tsx b/app/react-native/src/preview/components/OnDeviceUI/addons/index.tsx index 304440423599..2487ada31cc6 100644 --- a/app/react-native/src/preview/components/OnDeviceUI/addons/index.tsx +++ b/app/react-native/src/preview/components/OnDeviceUI/addons/index.tsx @@ -5,18 +5,17 @@ import addons from '@storybook/addons'; import AddonsList from './list'; import AddonWrapper from './wrapper'; import { Label } from '../../Shared/text'; -import { EmotionProps } from '../../Shared/theme'; - -const NoAddonContainer = styled.View` - flex: 1; - align-items: center; - justify-content: center; -`; - -const Container = styled.View` - flex: 1; - background: ${(props: EmotionProps) => props.theme.backgroundColor}; -`; + +const NoAddonContainer = styled.View({ + flex: 1, + alignItems: 'center', + justifyContent: 'center', +}); + +const Container = styled.View(({ theme }) => ({ + flex: 1, + background: theme.backgroundColor, +})); export default class Addons extends PureComponent<{}, { addonSelected: string }> { panels = addons.getElements('panel'); diff --git a/app/react-native/src/preview/components/OnDeviceUI/addons/list.tsx b/app/react-native/src/preview/components/OnDeviceUI/addons/list.tsx index 71c0c0bdc5da..c9b4c2504e6a 100644 --- a/app/react-native/src/preview/components/OnDeviceUI/addons/list.tsx +++ b/app/react-native/src/preview/components/OnDeviceUI/addons/list.tsx @@ -3,13 +3,12 @@ import { ScrollView } from 'react-native'; import styled from '@emotion/native'; import { Collection } from '@storybook/addons'; import Button from '../navigation/button'; -import { EmotionProps } from '../../Shared/theme'; -const Container = styled.View` - flex-direction: row; - border-bottom-width: 1; - border-bottom-color: ${(props: EmotionProps) => props.theme.borderColor}; -`; +const Container = styled.View(({ theme }) => ({ + flexDirection: 'row', + borderBottomWidth: 1, + borderBottomColor: theme.borderColor, +})); export interface Props { panels: Collection; diff --git a/app/react-native/src/preview/components/OnDeviceUI/index.tsx b/app/react-native/src/preview/components/OnDeviceUI/index.tsx index 8b07f2248bde..93ea928fbcf3 100644 --- a/app/react-native/src/preview/components/OnDeviceUI/index.tsx +++ b/app/react-native/src/preview/components/OnDeviceUI/index.tsx @@ -4,7 +4,6 @@ import { KeyboardAvoidingView, Platform, Animated, - TouchableOpacity, TouchableOpacityProps, } from 'react-native'; import styled from '@emotion/native'; @@ -25,7 +24,6 @@ import { getAddonPanelPosition, getNavigatorPanelPosition, } from './animation'; -import { EmotionProps } from '../Shared/theme'; const ANIMATION_DURATION = 300; const IS_IOS = Platform.OS === 'ios'; @@ -46,17 +44,18 @@ interface OnDeviceUIState { previewHeight: number; } -type EmotionPreviewProps = EmotionProps & TouchableOpacityProps; - -const Preview: typeof TouchableOpacity = styled.TouchableOpacity` - flex: 1; - border-left-width: ${(props: EmotionPreviewProps) => (props.disabled ? '0' : '1')}; - border-top-width: ${(props: EmotionPreviewProps) => (props.disabled ? '0' : '1')}; - border-right-width: ${(props: EmotionPreviewProps) => (props.disabled ? '0' : '1')}; - border-bottom-width: ${(props: EmotionPreviewProps) => (props.disabled ? '0' : '1')}; - border-color: ${(props: EmotionPreviewProps) => - props.disabled ? 'transparent' : props.theme.previewBorderColor}; -`; +const Preview = styled.TouchableOpacity( + { + flex: 1, + }, + ({ disabled, theme }) => ({ + borderLeftWidth: disabled ? '0' : '1', + borderTopWidth: disabled ? '0' : '1', + borderRightWidth: disabled ? '0' : '1', + borderBottomWidth: disabled ? '0' : '1', + borderColor: disabled ? 'transparent' : theme.previewBorderColor, + }) +); export default class OnDeviceUI extends PureComponent { constructor(props: OnDeviceUIProps) { diff --git a/app/react-native/src/preview/components/OnDeviceUI/navigation/bar.tsx b/app/react-native/src/preview/components/OnDeviceUI/navigation/bar.tsx index 591946e3ac65..f3a31c522c3b 100644 --- a/app/react-native/src/preview/components/OnDeviceUI/navigation/bar.tsx +++ b/app/react-native/src/preview/components/OnDeviceUI/navigation/bar.tsx @@ -2,16 +2,15 @@ import React, { PureComponent } from 'react'; import styled from '@emotion/native'; import Button from './button'; import { NAVIGATOR, PREVIEW, ADDONS } from './constants'; -import { EmotionProps } from '../../Shared/theme'; -const Container = styled.View` - flex-direction: row; - padding-horizontal: 8; - background: ${(props: EmotionProps) => props.theme.backgroundColor}; - border-top-width: 1; - border-bottom-width: 1; - border-color: ${(props: EmotionProps) => props.theme.borderColor}; -`; +const Container = styled.View(({ theme }) => ({ + flexDirection: 'row', + paddingHorizontal: 8, + background: theme.backgroundColor, + borderTopWidth: 1, + borderBottomWidth: 1, + borderColor: theme.borderColor, +})); export interface Props { index: number; diff --git a/app/react-native/src/preview/components/OnDeviceUI/navigation/button.tsx b/app/react-native/src/preview/components/OnDeviceUI/navigation/button.tsx index 1ef97e1d658d..fbd26dd2f263 100644 --- a/app/react-native/src/preview/components/OnDeviceUI/navigation/button.tsx +++ b/app/react-native/src/preview/components/OnDeviceUI/navigation/button.tsx @@ -1,23 +1,18 @@ import React, { PureComponent } from 'react'; import { TouchableOpacity } from 'react-native'; import styled from '@emotion/native'; -import { EmotionProps } from '../../Shared/theme'; -type EmotionButtonProps = EmotionProps & { active: boolean }; +const ActiveBorder = styled.View<{ active: boolean }>(({ active, theme }) => ({ + background: active ? theme.borderColor : 'transparent', + height: 3, +})); -const ActiveBorder = styled.View` - background: ${(props: EmotionButtonProps) => - props.active ? props.theme.borderColor : 'transparent'}; - height: 3; -`; - -const ButtonText = styled.Text` - color: ${(props: EmotionButtonProps) => - props.active ? props.theme.buttonActiveTextColor : props.theme.buttonTextColor}; - padding-horizontal: 8; - padding-vertical: 10; - font-size: 11; -`; +const ButtonText = styled.Text<{ active: boolean }>(({ theme, active }) => ({ + color: active ? theme.buttonActiveTextColor : theme.buttonTextColor, + paddingHorizontal: 8, + paddingVertical: 10, + fontSize: 11, +})); interface Props { id: number | string; diff --git a/app/react-native/src/preview/components/OnDeviceUI/navigation/index.tsx b/app/react-native/src/preview/components/OnDeviceUI/navigation/index.tsx index 7e30ebe39825..c0615f04fc47 100644 --- a/app/react-native/src/preview/components/OnDeviceUI/navigation/index.tsx +++ b/app/react-native/src/preview/components/OnDeviceUI/navigation/index.tsx @@ -1,11 +1,11 @@ /* eslint-disable react/destructuring-assignment */ import React, { PureComponent } from 'react'; import { View, SafeAreaView, StyleSheet } from 'react-native'; -import GestureRecognizer, { GestureRecognizerConfig } from 'react-native-swipe-gestures'; +import GestureRecognizer from 'react-native-swipe-gestures'; import Bar from './bar'; import VisibilityButton from './visibility-button'; -const SWIPE_CONFIG: GestureRecognizerConfig = { +const SWIPE_CONFIG = { velocityThreshold: 0.2, directionalOffsetThreshold: 80, }; diff --git a/app/react-native/src/preview/components/OnDeviceUI/navigation/visibility-button.tsx b/app/react-native/src/preview/components/OnDeviceUI/navigation/visibility-button.tsx index 84c05add0990..41ce12531c41 100644 --- a/app/react-native/src/preview/components/OnDeviceUI/navigation/visibility-button.tsx +++ b/app/react-native/src/preview/components/OnDeviceUI/navigation/visibility-button.tsx @@ -1,24 +1,22 @@ import React, { PureComponent } from 'react'; -import { TouchableOpacity } from 'react-native'; import styled from '@emotion/native'; -import { EmotionProps } from '../../Shared/theme'; interface Props { onPress: () => void; } -const Touchable: typeof TouchableOpacity = styled.TouchableOpacity` - background: transparent; - position: absolute; - right: 8; - bottom: 12; - z-index: 100; -`; +const Touchable = styled.TouchableOpacity({ + background: 'transparent', + position: 'absolute', + right: '8', + bottom: '12', + zIndex: 100, +}); -const HideIcon = styled.Text` - font-size: 14; - color: ${(props: EmotionProps) => props.theme.buttonTextColor}; -`; +const HideIcon = styled.Text(({ theme }) => ({ + fontSize: 14, + color: theme.buttonTextColor, +})); export default class VisibilityButton extends PureComponent { render() { diff --git a/app/react-native/src/preview/components/OnDeviceUI/panel.tsx b/app/react-native/src/preview/components/OnDeviceUI/panel.tsx index f225ae56698b..ad545dfb00f0 100644 --- a/app/react-native/src/preview/components/OnDeviceUI/panel.tsx +++ b/app/react-native/src/preview/components/OnDeviceUI/panel.tsx @@ -1,11 +1,10 @@ import React, { PureComponent } from 'react'; import { StyleSheet, Animated } from 'react-native'; import styled from '@emotion/native'; -import { EmotionProps } from '../Shared/theme'; -const Container: typeof Animated.View = styled(Animated.View)` - background: ${(props: EmotionProps) => props.theme.backgroundColor}; -`; +const Container = styled(Animated.View)(({ theme }) => ({ + background: theme.backgroundColor, +})); interface Props { style: any[]; diff --git a/app/react-native/src/preview/components/Shared/text.ts b/app/react-native/src/preview/components/Shared/text.ts index a2cbf1ef161b..20db572032e9 100644 --- a/app/react-native/src/preview/components/Shared/text.ts +++ b/app/react-native/src/preview/components/Shared/text.ts @@ -1,19 +1,22 @@ import styled from '@emotion/native'; -import { EmotionProps } from './theme'; -export const Header = styled.Text` - font-size: 20; - color: ${(props: EmotionProps) => props.theme.headerTextColor}; - ${(props: any) => props.selected && 'font-weight: bold;'} -`; +export const Header = styled.Text<{ selected: boolean }>( + ({ theme }) => ({ + fontSize: 20, + color: theme.headerTextColor, + }), + ({ selected }) => (selected ? { fontWeight: 'bold' } : {}) +); -export const Name = styled.Text` - font-size: 16; - color: ${(props: EmotionProps) => props.theme.headerTextColor}; - ${(props: any) => props.selected && 'font-weight: bold;'} -`; +export const Name = styled.Text<{ selected: boolean }>( + ({ theme }) => ({ + fontSize: 16, + color: theme.headerTextColor, + }), + ({ selected }) => (selected ? { fontWeight: 'bold' } : {}) +); -export const Label = styled.Text` - font-size: 18; - color: ${(props: EmotionProps) => props.theme.labelColor}; -`; +export const Label = styled.Text(({ theme }) => ({ + fontSize: 18, + color: theme.labelColor, +})); diff --git a/app/react-native/src/preview/components/Shared/theme.ts b/app/react-native/src/preview/components/Shared/theme.ts index 1995c6ec0e7e..3c762a7c5b49 100644 --- a/app/react-native/src/preview/components/Shared/theme.ts +++ b/app/react-native/src/preview/components/Shared/theme.ts @@ -7,7 +7,3 @@ export const theme = { buttonTextColor: '#999999', buttonActiveTextColor: '#444444', }; - -export interface EmotionProps { - theme: typeof theme; -} diff --git a/app/react-native/src/preview/components/StoryListView/index.tsx b/app/react-native/src/preview/components/StoryListView/index.tsx index 1d386c7d655a..324023d18fae 100644 --- a/app/react-native/src/preview/components/StoryListView/index.tsx +++ b/app/react-native/src/preview/components/StoryListView/index.tsx @@ -1,38 +1,38 @@ -import React, { Component } from 'react'; -import { SectionList, TextInput, TouchableOpacity, View, SafeAreaView } from 'react-native'; +import React, { Component, FunctionComponent } from 'react'; +import { SafeAreaView } from 'react-native'; import styled from '@emotion/native'; import Events from '@storybook/core-events'; import addons from '@storybook/addons'; -import { EmotionProps } from '../Shared/theme'; import { Header, Name } from '../Shared/text'; -const SearchBar: typeof TextInput = styled.TextInput` - background: ${(props: EmotionProps) => props.theme.borderColor}; - color: ${(props: EmotionProps) => props.theme.buttonActiveTextColor}; - border-top-left-radius: 5; - border-top-right-radius: 5; - border-bottom-left-radius: 5; - border-bottom-right-radius: 5; - font-size: 16; - margin-horizontal: 5; - margin-vertical: 5; - padding-horizontal: 5; - padding-vertical: 5; -`; - -const HeaderContainer = styled.View` - padding-vertical: 5; -`; +const SearchBar = styled.TextInput( + { + borderTopLeftRadius: 5, + borderTopRightRadius: 5, + borderBottomLeftRadius: 5, + borderBottomRightRadius: 5, + fontSize: 16, + marginHorizontal: 5, + marginVertical: 5, + paddingHorizontal: 5, + paddingVertical: 5, + }, + ({ theme }) => ({ + background: theme.borderColor, + color: theme.buttonActiveTextColor, + }) +); + +const HeaderContainer = styled.View({ + paddingCertical: 5, +}); interface SectionProps { title: string; selected: boolean; } -const SectionHeader: React.FunctionComponent = ({ - title, - selected, -}: SectionProps) => ( +const SectionHeader: FunctionComponent = ({ title, selected }: SectionProps) => (
{title}
@@ -45,12 +45,12 @@ interface ListItemProps { onPress: () => void; } -const ItemTouchable: typeof TouchableOpacity = styled.TouchableOpacity` - padding-horizontal: 16; - padding-vertical: 5; -`; +const ItemTouchable = styled.TouchableOpacity({ + paddingHorizontal: 16, + paddingVertical: 5, +}); -const ListItem: React.FunctionComponent = ({ kind, title, selected, onPress }) => ( +const ListItem: FunctionComponent = ({ kind, title, selected, onPress }) => ( { constructor(props: Props) { super(props); diff --git a/app/react-native/src/preview/index.tsx b/app/react-native/src/preview/index.tsx index 8b2b4ec17d04..36b21ab35e2e 100644 --- a/app/react-native/src/preview/index.tsx +++ b/app/react-native/src/preview/index.tsx @@ -1,5 +1,5 @@ /* eslint-disable no-underscore-dangle */ -import React from 'react'; +import React, { PureComponent } from 'react'; import { AsyncStorage } from 'react-native'; import { ThemeProvider } from 'emotion-theming'; // @ts-ignore @@ -8,11 +8,10 @@ import addons from '@storybook/addons'; import Events from '@storybook/core-events'; import Channel from '@storybook/channels'; import createChannel from '@storybook/channel-websocket'; -// @ts-ignore remove when client-api is migrated to TS import { StoryStore, ClientApi } from '@storybook/client-api'; import OnDeviceUI from './components/OnDeviceUI'; import StoryView from './components/StoryView'; -import { theme, EmotionProps } from './components/Shared/theme'; +import { theme } from './components/Shared/theme'; const STORAGE_KEY = 'lastOpenedStory'; @@ -30,7 +29,7 @@ export type Params = { isUIHidden: boolean; shouldDisableKeyboardAvoidingView: boolean; keyboardAvoidingViewVerticalOffset: number; -} & EmotionProps; +} & { theme: typeof theme }; export default class Preview { _clientApi: ClientApi; @@ -119,7 +118,7 @@ export default class Preview { const appliedTheme = { ...theme, ...params.theme }; // react-native hot module loader must take in a Class - https://github.com/facebook/react-native/issues/10991 - return class StorybookRoot extends React.PureComponent { + return class StorybookRoot extends PureComponent { render() { if (onDeviceUI) { return ( diff --git a/app/react-native/src/typings.d.ts b/app/react-native/src/typings.d.ts index 27fc0f5a4240..4f33c1f29b7d 100644 --- a/app/react-native/src/typings.d.ts +++ b/app/react-native/src/typings.d.ts @@ -1,30 +1,89 @@ -declare module 'react-native-swipe-gestures' { - export type GestureRecognizerConfig = Partial<{ - velocityThreshold: number; - directionalOffsetThreshold: number; - gestureIsClickThreshold: number; - }>; +import React, { Component } from 'react'; +import css from '@emotion/css'; +import { + CreateStyled, + CreateStyledComponentExtrinsic, +} from '@emotion/styled-base'; +import ReactNative from 'react-native'; +import { theme } from './preview/components/Shared/theme' - export enum SwipeDirections { - SWIPE_UP = 'SWIPE_UP', - SWIPE_DOWN = 'SWIPE_DOWN', - SWIPE_LEFT = 'SWIPE_LEFT', - SWIPE_RIGHT = 'SWIPE_RIGHT', - } +// https://github.com/emotion-js/emotion/pull/1176/ +// meanwhile: https://github.com/emotion-js/emotion/issues/839#issuecomment-500195354 +declare module '@emotion/native' { + type StyledReactNativeComponents = + | 'ActivityIndicator' + | 'ActivityIndicatorIOS' + | 'ART' + | 'Button' + | 'DatePickerIOS' + | 'DrawerLayoutAndroid' + | 'Image' + | 'ImageBackground' + | 'ImageEditor' + | 'ImageStore' + | 'KeyboardAvoidingView' + | 'ListView' + | 'MapView' + | 'Modal' + | 'NavigatorIOS' + | 'Picker' + | 'PickerIOS' + | 'ProgressBarAndroid' + | 'ProgressViewIOS' + | 'ScrollView' + | 'SegmentedControlIOS' + | 'Slider' + | 'SliderIOS' + | 'SnapshotViewIOS' + | 'Switch' + | 'RecyclerViewBackedScrollView' + | 'RefreshControl' + | 'SafeAreaView' + | 'StatusBar' + | 'SwipeableListView' + | 'SwitchAndroid' + | 'SwitchIOS' + | 'TabBarIOS' + | 'Text' + | 'TextInput' + | 'ToastAndroid' + | 'ToolbarAndroid' + | 'Touchable' + | 'TouchableHighlight' + | 'TouchableNativeFeedback' + | 'TouchableOpacity' + | 'TouchableWithoutFeedback' + | 'View' + | 'ViewPagerAndroid' + | 'WebView' + | 'FlatList' + | 'SectionList' + | 'VirtualizedList'; - export interface Props { - onSwipe?: (swipeDirection: SwipeDirections) => void; - onSwipeLeft?: () => void; - onSwipeRight?: () => void; - onSwipeTop?: () => void; - onSwipeBottom?: () => void; - config: GestureRecognizerConfig; - } + type StyledComponentsForReactNative< + T extends keyof typeof ReactNative, + ExtraProps, + Theme + > = { + [K in T]: CreateStyledComponentExtrinsic< + typeof ReactNative[K], + ExtraProps, + Theme + >; + }; - declare class GestureRecognizer extends React.Component {} + type MyTheme = typeof theme; - export default GestureRecognizer; -} + export interface Styled + extends CreateStyled, + StyledComponentsForReactNative< + StyledReactNativeComponents, + ExtraProps, + Theme + > {} -// https://github.com/emotion-js/emotion/pull/1176/ -declare module '@emotion/native'; + export {css}; + + const styled: Styled; + export default styled; +} \ No newline at end of file diff --git a/app/react-native/tsconfig.json b/app/react-native/tsconfig.json index b2a74623bd83..6a4f8e20e1b7 100644 --- a/app/react-native/tsconfig.json +++ b/app/react-native/tsconfig.json @@ -2,7 +2,10 @@ "extends": "../../tsconfig.json", "compilerOptions": { "baseUrl": ".", - "rootDir": "./src" + "rootDir": "./src", + "paths": { + "@emotion/native": ["src/typings.d.ts"] + } }, "include": ["src/**/*"], "exclude": ["src/__tests__/**/*"] diff --git a/app/react/src/client/preview/render.tsx b/app/react/src/client/preview/render.tsx index e4d935246bfa..8f4d897f9d4a 100644 --- a/app/react/src/client/preview/render.tsx +++ b/app/react/src/client/preview/render.tsx @@ -1,20 +1,20 @@ import { document } from 'global'; -import React from 'react'; +import React, { Component, ReactElement, StrictMode } from 'react'; import ReactDOM from 'react-dom'; import { RenderMainArgs } from './types'; const rootEl = document ? document.getElementById('root') : null; -const render = (node: React.ReactElement, el: Element) => +const render = (node: ReactElement, el: Element) => new Promise(resolve => { ReactDOM.render( - process.env.STORYBOOK_EXAMPLE_APP ? {node} : node, + process.env.STORYBOOK_EXAMPLE_APP ? {node} : node, el, resolve ); }); -class ErrorBoundary extends React.Component<{ +class ErrorBoundary extends Component<{ showException: (err: Error) => void; showMain: () => void; }> { diff --git a/app/react/src/client/preview/types.ts b/app/react/src/client/preview/types.ts index 3c69ae2579d6..1b85b49c44ec 100644 --- a/app/react/src/client/preview/types.ts +++ b/app/react/src/client/preview/types.ts @@ -1,4 +1,4 @@ -import React from 'react'; +import { FunctionComponent, ReactElement } from 'react'; export interface ShowErrorArgs { title: string; @@ -6,7 +6,7 @@ export interface ShowErrorArgs { } export interface RenderMainArgs { - storyFn: React.FunctionComponent; + storyFn: FunctionComponent; selectedKind: string; selectedStory: string; showMain: () => void; @@ -15,7 +15,7 @@ export interface RenderMainArgs { forceRender: boolean; } -export type StoryFnReactReturnType = React.ReactElement; +export type StoryFnReactReturnType = ReactElement; export interface IStorybookStory { name: string; diff --git a/app/react/src/demo/Button.tsx b/app/react/src/demo/Button.tsx index 33811af2d287..6a203f4f0103 100644 --- a/app/react/src/demo/Button.tsx +++ b/app/react/src/demo/Button.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { FunctionComponent, HTMLAttributes } from 'react'; const styles = { border: '1px solid #eee', @@ -11,23 +10,14 @@ const styles = { margin: 10, }; -const Button = ({ - children, - onClick, -}: { - children: React.ReactChildren; - onClick: (event: React.MouseEvent) => void; -}) => ( +type Props = Pick, 'onClick'>; +const Button: FunctionComponent = ({ children, onClick }) => ( ); Button.displayName = 'Button'; -Button.propTypes = { - children: PropTypes.node.isRequired, - onClick: PropTypes.func, -}; Button.defaultProps = { onClick: () => {}, }; diff --git a/app/react/src/demo/Welcome.tsx b/app/react/src/demo/Welcome.tsx index 42ed1d0a9b4d..0af39fe3d4aa 100644 --- a/app/react/src/demo/Welcome.tsx +++ b/app/react/src/demo/Welcome.tsx @@ -1,7 +1,14 @@ -import React from 'react'; +import React, { + AnchorHTMLAttributes, + ButtonHTMLAttributes, + DetailedHTMLProps, + FunctionComponent, + HTMLAttributes, +} from 'react'; import PropTypes from 'prop-types'; -const Main = (props?: React.DetailedHTMLProps, HTMLElement>) => ( +type MainProps = Omit, HTMLElement>, 'style'>; +const Main: FunctionComponent = props => (
, /> ); -const Title = ({ - children, - ...props -}: { - children: string; - props?: React.DetailedHTMLProps, HTMLHeadingElement>; -}) =>

{children}

; +type TitleProps = DetailedHTMLProps, HTMLHeadingElement>; +const Title: FunctionComponent = ({ children, ...props }) => ( +

{children}

+); Title.propTypes = { children: PropTypes.node, }; @@ -27,9 +31,11 @@ Title.defaultProps = { children: undefined, }; -const Note = ( - props?: React.DetailedHTMLProps, HTMLParagraphElement> -) => ( +type NoteProps = Omit< + DetailedHTMLProps, HTMLParagraphElement>, + 'style' +>; +const Note: FunctionComponent = props => (

); -const InlineCode = ( - props?: React.DetailedHTMLProps, HTMLElement> -) => ( +type InlineCodeProps = Omit, HTMLElement>, 'style'>; +const InlineCode: FunctionComponent = props => ( ); -const Link = ({ - children, - href, - target, - rel, - ...props -}: { - children: string; +type LinkProps = Omit< + DetailedHTMLProps, HTMLAnchorElement>, + 'style' +> & { href: string; target: string; rel: string; - props?: React.DetailedHTMLProps, HTMLAnchorElement>; -}) => ( +}; +const Link: FunctionComponent = ({ children, href, target, rel, ...props }) => ( ) => void; - props?: React.DetailedHTMLProps, HTMLButtonElement>; -}) => ( +type NavButtonProps = Omit< + DetailedHTMLProps, HTMLButtonElement>, + 'style' | 'type' +> & {}; +const NavButton: FunctionComponent = ({ children, onClick, ...props }) => ( diff --git a/lib/api/src/context.ts b/lib/api/src/context.ts index 41b6225a9cde..faf94497fe32 100644 --- a/lib/api/src/context.ts +++ b/lib/api/src/context.ts @@ -1,4 +1,4 @@ -import React from 'react'; +import { createContext as ReactCreateContext } from 'react'; import { Combo } from './index'; -export const createContext = ({ api, state }: Combo) => React.createContext({ api, state }); +export const createContext = ({ api, state }: Combo) => ReactCreateContext({ api, state }); diff --git a/lib/cli/generators/REACT_NATIVE/index.js b/lib/cli/generators/REACT_NATIVE/index.js index f63d6a69b75b..c47d01ef8dd6 100644 --- a/lib/cli/generators/REACT_NATIVE/index.js +++ b/lib/cli/generators/REACT_NATIVE/index.js @@ -26,8 +26,8 @@ export default async (npmOptions, installServer, { storyFormat = 'csf' }) => { // Only notify about app name if running in React Native vanilla (Expo projects do not have ios directory) if (dirname) { - const projectName = - dirname && dirname.slice('ios/'.length, dirname.length - '.xcodeproj'.length - 1); + const projectName = dirname.slice('ios/'.length, dirname.length - '.xcodeproj'.length - 1); + if (projectName) { shell.sed('-i', '%APP_NAME%', projectName, 'storybook/storybook.js'); } else { diff --git a/lib/cli/lib/helpers.js b/lib/cli/lib/helpers.js index 6fe8f27f8a9f..6d2bb404237d 100644 --- a/lib/cli/lib/helpers.js +++ b/lib/cli/lib/helpers.js @@ -83,16 +83,15 @@ export function readFileAsJson(jsonPath, allowComments) { return JSON.parse(jsonContent); } -export function writeFileAsJson(jsonPath, content) { +export const writeFileAsJson = (jsonPath, content) => { const filePath = path.resolve(jsonPath); if (!fs.existsSync(filePath)) { return false; } - const jsonContent = fs.readFileSync(filePath, 'utf8'); fs.writeFileSync(filePath, `${JSON.stringify(content, null, 2)}\n`); return true; -} +}; export function writePackageJson(packageJson) { const content = `${JSON.stringify(packageJson, null, 2)}\n`; @@ -101,21 +100,17 @@ export function writePackageJson(packageJson) { fs.writeFileSync(packageJsonPath, content, 'utf8'); } -export function writeBabelRc(babelRc) { - const content = `${JSON.stringify(babelRc, null, 2)}\n`; - const babelRcPath = path.resolve('.babelrc'); - - fs.writeFileSync(babelRcPath, content, 'utf8'); -} - -export function commandLog(message) { +export const commandLog = message => { process.stdout.write(chalk.cyan(' • ') + message); - const done = (errorMessage, errorInfo) => { + + return (errorMessage, errorInfo) => { if (errorMessage) { process.stdout.write(`. ${chalk.red('✖')}\n`); logger.error(`\n ${chalk.red(errorMessage)}`); - if (!errorInfo) return; + if (!errorInfo) { + return; + } const newErrorInfo = errorInfo .split('\n') @@ -127,9 +122,7 @@ export function commandLog(message) { process.stdout.write(`. ${chalk.green('✓')}\n`); }; - - return done; -} +}; export function paddedLog(message) { const newMessage = message diff --git a/lib/components/src/ActionBar/ActionBar.tsx b/lib/components/src/ActionBar/ActionBar.tsx index d0e233d299f0..93ac23669cac 100644 --- a/lib/components/src/ActionBar/ActionBar.tsx +++ b/lib/components/src/ActionBar/ActionBar.tsx @@ -1,4 +1,4 @@ -import React, { FunctionComponent } from 'react'; +import React, { FunctionComponent, MouseEvent } from 'react'; import { styled } from '@storybook/theming'; @@ -55,7 +55,7 @@ ActionButton.displayName = 'ActionButton'; export interface ActionItem { title: string | JSX.Element; - onClick: (e: React.MouseEvent) => void; + onClick: (e: MouseEvent) => void; disabled?: boolean; } diff --git a/lib/components/src/blocks/ColorPalette.tsx b/lib/components/src/blocks/ColorPalette.tsx index ec9580e7663b..e4deaaef8fa2 100644 --- a/lib/components/src/blocks/ColorPalette.tsx +++ b/lib/components/src/blocks/ColorPalette.tsx @@ -1,16 +1,16 @@ -import React, { Fragment } from 'react'; +import React, { FunctionComponent } from 'react'; import { styled } from '@storybook/theming'; import { transparentize } from 'polished'; import { getBlockBackgroundStyle } from './BlockBackgroundStyles'; import { ResetWrapper } from '../typography/DocumentFormatting'; -const ItemTitle = styled.div<{}>(({ theme }) => ({ +const ItemTitle = styled.div(({ theme }) => ({ fontWeight: theme.typography.weight.bold, color: theme.color.defaultText, })); -const ItemSubtitle = styled.div<{}>(({ theme }) => ({ +const ItemSubtitle = styled.div(({ theme }) => ({ color: theme.base === 'light' ? transparentize(0.2, theme.color.defaultText) @@ -23,7 +23,7 @@ const ItemDescription = styled.div({ marginTop: 5, }); -const SwatchLabel = styled.div<{}>(({ theme }) => ({ +const SwatchLabel = styled.div(({ theme }) => ({ flex: 1, textAlign: 'center', fontFamily: theme.typography.fonts.mono, @@ -51,7 +51,7 @@ const Swatch = styled.div({ flex: 1, }); -const SwatchColors = styled.div<{}>(({ theme }) => ({ +const SwatchColors = styled.div(({ theme }) => ({ ...getBlockBackgroundStyle(theme), display: 'flex', flexDirection: 'row', @@ -87,7 +87,7 @@ const ListSwatches = styled.div({ flex: 1, }); -const ListHeading = styled.div<{}>(({ theme }) => ({ +const ListHeading = styled.div(({ theme }) => ({ display: 'flex', flexDirection: 'row', alignItems: 'center', @@ -99,7 +99,7 @@ const ListHeading = styled.div<{}>(({ theme }) => ({ : transparentize(0.6, theme.color.defaultText), })); -const List = styled.div<{}>(({ theme }) => ({ +const List = styled.div(({ theme }) => ({ fontSize: theme.typography.size.s2, lineHeight: `20px`, @@ -117,7 +117,7 @@ interface ColorProps { * A single color row your styleguide showing title, subtitle and one or more colors, used * as a child of `ColorPalette`. */ -export const ColorItem: React.FunctionComponent = ({ title, subtitle, colors }) => { +export const ColorItem: FunctionComponent = ({ title, subtitle, colors }) => { return ( @@ -155,7 +155,7 @@ export const ColorItem: React.FunctionComponent = ({ title, subtitle * Styleguide documentation for colors, including names, captions, and color swatches, * all specified as `ColorItem` children of this wrapper component. */ -export const ColorPalette: React.FunctionComponent = ({ children, ...props }) => ( +export const ColorPalette: FunctionComponent = ({ children, ...props }) => ( diff --git a/lib/components/src/blocks/Description.tsx b/lib/components/src/blocks/Description.tsx index 90d1c5994a88..e3951a538d06 100644 --- a/lib/components/src/blocks/Description.tsx +++ b/lib/components/src/blocks/Description.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { FunctionComponent } from 'react'; import Markdown from 'markdown-to-jsx'; import { ResetWrapper } from '../typography/DocumentFormatting'; import { components } from '../html'; @@ -11,7 +11,7 @@ export interface DescriptionProps { * A markdown description for a component, typically used to show the * components docgen docs. */ -export const Description: React.FunctionComponent = ({ markdown }) => ( +export const Description: FunctionComponent = ({ markdown }) => ( {markdown} diff --git a/lib/components/src/blocks/DocsPage.tsx b/lib/components/src/blocks/DocsPage.tsx index cfc789770ca7..0ca0c32fe454 100644 --- a/lib/components/src/blocks/DocsPage.tsx +++ b/lib/components/src/blocks/DocsPage.tsx @@ -1,38 +1,37 @@ -import React from 'react'; -import { styled } from '@storybook/theming'; +import React, { FunctionComponent } from 'react'; +import { styled, Theme } from '@storybook/theming'; import { transparentize } from 'polished'; import { withReset } from '../typography/withReset'; const breakpoint = 600; -const pageMargin = '5.55555'; export interface DocsPageProps { title: string; subtitle?: string; } -const Title = styled.h1<{}>(withReset, ({ theme }) => ({ +const Title = styled.h1<{}>(withReset, ({ theme }: { theme: Theme }) => ({ color: theme.color.defaultText, fontSize: theme.typography.size.m3, fontWeight: theme.typography.weight.black, lineHeight: '32px', - [`@media (min-width: ${breakpoint * 1}px)`]: { + [`@media (min-width: ${breakpoint}px)`]: { fontSize: theme.typography.size.l1, lineHeight: '36px', marginBottom: '.5rem', // 8px }, })); -const Subtitle = styled.h2<{}>(withReset, ({ theme }) => ({ +const Subtitle = styled.h2<{}>(withReset, ({ theme }: { theme: Theme }) => ({ fontWeight: theme.typography.weight.regular, fontSize: theme.typography.size.s3, lineHeight: '20px', borderBottom: 'none', marginBottom: '15px', - [`@media (min-width: ${breakpoint * 1}px)`]: { + [`@media (min-width: ${breakpoint}px)`]: { fontSize: theme.typography.size.m1, lineHeight: '28px', marginBottom: '24px', @@ -56,10 +55,10 @@ export const DocsWrapper = styled.div<{}>(({ theme }) => ({ minHeight: '100vh', padding: '4rem 20px', - [`@media (min-width: ${breakpoint * 1}px)`]: {}, + [`@media (min-width: ${breakpoint}px)`]: {}, })); -export const DocsPageWrapper: React.FunctionComponent = ({ children }) => ( +export const DocsPageWrapper: FunctionComponent = ({ children }) => ( {children} @@ -70,7 +69,7 @@ export const DocsPageWrapper: React.FunctionComponent = ({ children }) => ( * title & subtitle and a collection of blocks including `Description`, * and `Preview`s for each of the component's stories. */ -const DocsPage: React.FunctionComponent = ({ title, subtitle, children }) => ( +const DocsPage: FunctionComponent = ({ title, subtitle, children }) => ( <> {title && {title}} {subtitle && {subtitle}} diff --git a/lib/components/src/blocks/IFrame.tsx b/lib/components/src/blocks/IFrame.tsx index 66a0dc484b94..4f117c74758c 100644 --- a/lib/components/src/blocks/IFrame.tsx +++ b/lib/components/src/blocks/IFrame.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { Component } from 'react'; import window from 'global'; interface IFrameProps { @@ -18,7 +18,7 @@ interface BodyStyle { transformOrigin: string; } -export class IFrame extends React.Component { +export class IFrame extends Component { iframe: any = null; componentDidMount() { diff --git a/lib/components/src/blocks/IconGallery.tsx b/lib/components/src/blocks/IconGallery.tsx index 6d0e604265db..63edb9760162 100644 --- a/lib/components/src/blocks/IconGallery.tsx +++ b/lib/components/src/blocks/IconGallery.tsx @@ -1,17 +1,17 @@ -import React from 'react'; +import React, { FunctionComponent } from 'react'; import { styled } from '@storybook/theming'; import { ResetWrapper } from '../typography/DocumentFormatting'; import { getBlockBackgroundStyle } from './BlockBackgroundStyles'; -const ItemLabel = styled.div<{}>(({ theme }) => ({ +const ItemLabel = styled.div(({ theme }) => ({ fontFamily: theme.typography.fonts.base, fontSize: theme.typography.size.s2, marginLeft: 10, lineHeight: 1.2, })); -const ItemSpecimen = styled.div<{}>(({ theme }) => ({ +const ItemSpecimen = styled.div(({ theme }) => ({ ...getBlockBackgroundStyle(theme), overflow: 'hidden', height: 40, @@ -49,24 +49,20 @@ interface IconItemProps { /** * An individual icon with a caption and an example (passed as `children`). */ -export const IconItem: React.FunctionComponent = ({ name, children }) => { - return ( - - {children} - {name} - - ); -}; +export const IconItem: FunctionComponent = ({ name, children }) => ( + + {children} + {name} + +); /** * Show a grid of icons, as specified by `IconItem`. */ -export const IconGallery: React.FunctionComponent = ({ children, ...props }) => { - return ( - - - {children} - - - ); -}; +export const IconGallery: FunctionComponent = ({ children, ...props }) => ( + + + {children} + + +); diff --git a/lib/components/src/blocks/Preview.tsx b/lib/components/src/blocks/Preview.tsx index e91bd9c6ddf4..a8d5d23a47fe 100644 --- a/lib/components/src/blocks/Preview.tsx +++ b/lib/components/src/blocks/Preview.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { Children, FunctionComponent, ReactElement, ReactNode, useState } from 'react'; import { styled } from '@storybook/theming'; import { darken } from 'polished'; import { logger } from '@storybook/client-logger'; @@ -65,7 +65,7 @@ const PreviewContainer = styled.div({ }); interface SourceItem { - source?: React.ReactElement; + source?: ReactElement; actionItem: ActionItem; } @@ -99,9 +99,9 @@ const getSource = ( } } }; -function getStoryId(children: React.ReactNode) { - if (React.Children.count(children) === 1) { - const elt = children as React.ReactElement; +function getStoryId(children: ReactNode) { + if (Children.count(children) === 1) { + const elt = children as ReactElement; if (elt.props) { return elt.props.id; } @@ -114,7 +114,7 @@ function getStoryId(children: React.ReactNode) { * items. The preview also shows the source for the component * as a drop-down. */ -const Preview: React.FunctionComponent = ({ +const Preview: FunctionComponent = ({ isColumn, columns, children, @@ -123,9 +123,9 @@ const Preview: React.FunctionComponent = ({ isExpanded = false, ...props }) => { - const [expanded, setExpanded] = React.useState(isExpanded); + const [expanded, setExpanded] = useState(isExpanded); const { source, actionItem } = getSource(withSource, expanded, setExpanded); - const [scale, setScale] = React.useState(1); + const [scale, setScale] = useState(1); if (withToolbar && Array.isArray(children)) { logger.warn('Cannot use toolbar with multiple preview children, disabling'); diff --git a/lib/components/src/blocks/PropsTable/PropRow.tsx b/lib/components/src/blocks/PropsTable/PropRow.tsx index 7121f317b560..c88052055342 100644 --- a/lib/components/src/blocks/PropsTable/PropRow.tsx +++ b/lib/components/src/blocks/PropsTable/PropRow.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { FunctionComponent } from 'react'; import { styled } from '@storybook/theming'; import { transparentize } from 'polished'; import { PropDef } from './PropDef'; @@ -27,13 +27,13 @@ interface PropRowProps { const Name = styled.span({ fontWeight: 'bold' }); -const Required = styled.span<{}>(({ theme }) => ({ +const Required = styled.span(({ theme }) => ({ color: theme.color.negative, fontFamily: theme.typography.fonts.mono, cursor: 'help', })); -const StyledPropDef = styled.div<{}>(({ theme }) => ({ +const StyledPropDef = styled.div(({ theme }) => ({ color: theme.base === 'light' ? transparentize(0.4, theme.color.defaultText) @@ -75,15 +75,15 @@ const prettyPrint = (type: any): string => { } }; -export const PrettyPropType: React.FunctionComponent = ({ type }) => ( +export const PrettyPropType: FunctionComponent = ({ type }) => ( {prettyPrint(type)} ); -export const PrettyPropVal: React.FunctionComponent = ({ value }) => ( +export const PrettyPropVal: FunctionComponent = ({ value }) => ( {JSON.stringify(value)} ); -export const PropRow: React.FunctionComponent = ({ +export const PropRow: FunctionComponent = ({ row: { name, type, required, description, defaultValue }, }) => ( diff --git a/lib/components/src/blocks/PropsTable/PropsTable.tsx b/lib/components/src/blocks/PropsTable/PropsTable.tsx index e462a57de7f7..9aa8c48a9df1 100644 --- a/lib/components/src/blocks/PropsTable/PropsTable.tsx +++ b/lib/components/src/blocks/PropsTable/PropsTable.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { FunctionComponent } from 'react'; import { styled } from '@storybook/theming'; import { opacify, transparentize } from 'polished'; import { PropRow } from './PropRow'; @@ -136,7 +136,7 @@ export type PropsTableProps = PropsTableRowsProps | PropsTableErrorProps; * Display the props for a component as a props table. Each row is a collection of * PropDefs, usually derived from docgen info for the component. */ -const PropsTable: React.FunctionComponent = props => { +const PropsTable: FunctionComponent = props => { const { error } = props as PropsTableErrorProps; if (error) { return {error}; diff --git a/lib/components/src/blocks/Source.stories.tsx b/lib/components/src/blocks/Source.stories.tsx index bad3ea4acaeb..7e370689cb32 100644 --- a/lib/components/src/blocks/Source.stories.tsx +++ b/lib/components/src/blocks/Source.stories.tsx @@ -12,7 +12,6 @@ const jsxCode = ` `.trim(); -const jsxProps = {}; export const jsx = () => ; const cssCode = ` diff --git a/lib/components/src/blocks/Source.tsx b/lib/components/src/blocks/Source.tsx index 34faf42c8454..582801087ed6 100644 --- a/lib/components/src/blocks/Source.tsx +++ b/lib/components/src/blocks/Source.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { FunctionComponent } from 'react'; import { styled, ThemeProvider, convert, themes } from '@storybook/theming'; import { EmptyBlock } from './EmptyBlock'; @@ -42,7 +42,7 @@ export type SourceProps = SourceErrorProps & SourceCodeProps; /** * Syntax-highlighted source code for a component (or anything!) */ -const Source: React.FunctionComponent = props => { +const Source: FunctionComponent = props => { const { error } = props as SourceErrorProps; if (error) { return {error}; diff --git a/lib/components/src/blocks/Story.stories.tsx b/lib/components/src/blocks/Story.stories.tsx index 72518ace1c57..942a8ef1b6a0 100644 --- a/lib/components/src/blocks/Story.stories.tsx +++ b/lib/components/src/blocks/Story.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Story, StoryError } from './Story'; import { Button } from '../Button/Button'; @@ -10,7 +10,7 @@ export default { const buttonFn = () => ; const buttonHookFn = () => { - const [count, setCount] = React.useState(0); + const [count, setCount] = useState(0); return (