diff --git a/package.json b/package.json index e12f2aa46..e47c10ff7 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "retext": "^7.0.1", "retext-smartypants": "^4.0.0", "rss": "^1.2.2", - "tailwindcss": "^3.0.22", + "tailwindcss": "^3.3.2", "typescript": "^4.0.2", "unist-util-visit": "^2.0.3", "webpack-bundle-analyzer": "^4.5.0" diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx index 8eead2302..e64b486d1 100644 --- a/src/components/Breadcrumbs.tsx +++ b/src/components/Breadcrumbs.tsx @@ -17,10 +17,10 @@ function Breadcrumbs({breadcrumbs}: {breadcrumbs: RouteItem[]}) { + className="text-link dark:text-link-dark text-sm tracking-wide font-bold uppercase me-1 hover:underline"> {crumb.title} - + diff --git a/src/components/Icon/IconArrow.tsx b/src/components/Icon/IconArrow.tsx index 53bde1326..714cccd82 100644 --- a/src/components/Icon/IconArrow.tsx +++ b/src/components/Icon/IconArrow.tsx @@ -7,7 +7,12 @@ import cn from 'classnames'; export const IconArrow = memo< JSX.IntrinsicElements['svg'] & { - displayDirection: 'left' | 'right' | 'up' | 'down'; + /** + * The direction the arrow should point. + * `start` and `end` are relative to the current locale. + * for example, in LTR, `start` is left and `end` is right. + */ + displayDirection: 'start' | 'end' | 'right' | 'left' | 'up' | 'down'; } >(function IconArrow({displayDirection, className, ...rest}) { return ( @@ -20,6 +25,7 @@ export const IconArrow = memo< {...rest} className={cn(className, { 'rotate-180': displayDirection === 'right', + 'rotate-180 rtl:rotate-0': displayDirection === 'end', })}> diff --git a/src/components/Icon/IconArrowSmall.tsx b/src/components/Icon/IconArrowSmall.tsx index cf85988d2..6653dc387 100644 --- a/src/components/Icon/IconArrowSmall.tsx +++ b/src/components/Icon/IconArrowSmall.tsx @@ -7,11 +7,17 @@ import cn from 'classnames'; export const IconArrowSmall = memo< JSX.IntrinsicElements['svg'] & { - displayDirection: 'left' | 'right' | 'up' | 'down'; + /** + * The direction the arrow should point. + * `start` and `end` are relative to the current locale. + * for example, in LTR, `start` is left and `end` is right. + */ + displayDirection: 'start' | 'end' | 'right' | 'left' | 'up' | 'down'; } >(function IconArrowSmall({displayDirection, className, ...rest}) { const classes = cn(className, { 'rotate-180': displayDirection === 'left', + 'rotate-180 rtl:rotate-0': displayDirection === 'start', 'rotate-90': displayDirection === 'down', }); return ( diff --git a/src/components/Icon/IconCanary.tsx b/src/components/Icon/IconCanary.tsx new file mode 100644 index 000000000..a7782b141 --- /dev/null +++ b/src/components/Icon/IconCanary.tsx @@ -0,0 +1,32 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + */ + +import {memo} from 'react'; + +export const IconCanary = memo( + function IconCanary({className, title}) { + return ( + + {title && {title}} + + + + + + + ); + } +); diff --git a/src/components/Icon/IconChevron.tsx b/src/components/Icon/IconChevron.tsx index 1184d77d2..4d40330ce 100644 --- a/src/components/Icon/IconChevron.tsx +++ b/src/components/Icon/IconChevron.tsx @@ -7,7 +7,12 @@ import cn from 'classnames'; export const IconChevron = memo< JSX.IntrinsicElements['svg'] & { - displayDirection: 'up' | 'down' | 'left' | 'right'; + /** + * The direction the arrow should point. + * `start` and `end` are relative to the current locale. + * for example, in LTR, `start` is left and `end` is right. + */ + displayDirection: 'start' | 'end' | 'right' | 'left' | 'up' | 'down'; } >(function IconChevron({className, displayDirection}) { const classes = cn( @@ -16,6 +21,8 @@ export const IconChevron = memo< 'rotate-90': displayDirection === 'left', 'rotate-180': displayDirection === 'up', '-rotate-90': displayDirection === 'right', + 'rotate-90 rtl:-rotate-90': displayDirection === 'start', + '-rotate-90 rtl:rotate-90': displayDirection === 'end', }, className ); diff --git a/src/components/Icon/IconNavArrow.tsx b/src/components/Icon/IconNavArrow.tsx index 93eed6e3c..f61175e9b 100644 --- a/src/components/Icon/IconNavArrow.tsx +++ b/src/components/Icon/IconNavArrow.tsx @@ -7,15 +7,22 @@ import cn from 'classnames'; export const IconNavArrow = memo< JSX.IntrinsicElements['svg'] & { - displayDirection: 'right' | 'down' | 'left'; + /** + * The direction the arrow should point. + * `start` and `end` are relative to the current locale. + * for example, in LTR, `start` is left and `end` is right. + */ + displayDirection: 'start' | 'end' | 'right' | 'left' | 'down'; } ->(function IconNavArrow({displayDirection = 'right', className}) { +>(function IconNavArrow({displayDirection = 'start', className}) { const classes = cn( 'duration-100 ease-in transition', { 'rotate-0': displayDirection === 'down', - '-rotate-90': displayDirection === 'right', 'rotate-90': displayDirection === 'left', + '-rotate-90': displayDirection === 'right', + 'rotate-90 rtl:-rotate-90': displayDirection === 'start', + '-rotate-90 rtl:rotate-90': displayDirection === 'end', }, className ); diff --git a/src/components/Layout/Feedback.tsx b/src/components/Layout/Feedback.tsx index 6bb8a4aac..2bf9afe57 100644 --- a/src/components/Layout/Feedback.tsx +++ b/src/components/Layout/Feedback.tsx @@ -62,13 +62,13 @@ function SendFeedback({onSubmit}: {onSubmit: () => void}) { const [isSubmitted, setIsSubmitted] = useState(false); return (
-

+

{isSubmitted ? 'Thank you for your feedback!' : 'Is this page useful?'}

{!isSubmitted && ( diff --git a/src/components/Layout/Sidebar/SidebarLink.tsx b/src/components/Layout/Sidebar/SidebarLink.tsx index 6222104c3..1b961e9c6 100644 --- a/src/components/Layout/Sidebar/SidebarLink.tsx +++ b/src/components/Layout/Sidebar/SidebarLink.tsx @@ -8,6 +8,7 @@ import {useRef, useEffect} from 'react'; import * as React from 'react'; import cn from 'classnames'; import {IconNavArrow} from 'components/Icon/IconNavArrow'; +import {IconCanary} from 'components/Icon/IconCanary'; import Link from 'next/link'; interface SidebarLinkProps { @@ -15,7 +16,7 @@ interface SidebarLinkProps { selected?: boolean; title: string; level: number; - wip: boolean | undefined; + canary?: boolean; icon?: React.ReactNode; isExpanded?: boolean; hideArrow?: boolean; @@ -26,7 +27,7 @@ export function SidebarLink({ href, selected = false, title, - wip, + canary, level, isExpanded, hideArrow, @@ -57,10 +58,10 @@ export function SidebarLink({ passHref aria-current={selected ? 'page' : undefined} className={cn( - 'p-2 pr-2 w-full rounded-none lg:rounded-r-2xl text-left hover:bg-gray-5 dark:hover:bg-gray-80 relative flex items-center justify-between', + 'p-2 pe-2 w-full rounded-none lg:rounded-s-2xl text-start hover:bg-gray-5 dark:hover:bg-gray-80 relative flex items-center justify-between', { - 'text-sm pl-6': level > 0, - 'pl-5': level < 2, + 'text-sm ps-6': level > 0, + 'ps-5': level < 2, 'text-base font-bold': level === 0, 'text-primary dark:text-primary-dark': level === 0 && !selected, 'text-base text-secondary dark:text-secondary-dark': @@ -72,19 +73,23 @@ export function SidebarLink({ } )}> {/* This here needs to be refactored ofc */} - - {title} - +
+ {title}{' '} + {canary && ( + + )} +
+ {isExpanded != null && !hideArrow && ( - + )} diff --git a/src/components/Layout/Sidebar/SidebarRouteTree.tsx b/src/components/Layout/Sidebar/SidebarRouteTree.tsx index fa6b0cb25..9a0dd23f5 100644 --- a/src/components/Layout/Sidebar/SidebarRouteTree.tsx +++ b/src/components/Layout/Sidebar/SidebarRouteTree.tsx @@ -82,7 +82,15 @@ export function SidebarRouteTree({
    {currentRoutes.map( ( - {path, title, routes, wip, heading, hasSectionHeader, sectionHeader}, + { + path, + title, + routes, + canary, + heading, + hasSectionHeader, + sectionHeader, + }, index ) => { const selected = slug === path; @@ -112,7 +120,7 @@ export function SidebarRouteTree({ selected={selected} level={level} title={title} - wip={wip} + canary={canary} isExpanded={isExpanded} hideArrow={isForceExpanded} /> @@ -136,7 +144,7 @@ export function SidebarRouteTree({ selected={selected} level={level} title={title} - wip={wip} + canary={canary} /> ); @@ -147,12 +155,12 @@ export function SidebarRouteTree({ {index !== 0 && (
  • )}

    {sectionHeader} diff --git a/src/components/Layout/SidebarNav/SidebarNav.tsx b/src/components/Layout/SidebarNav/SidebarNav.tsx index 525fddfb4..702ff5b5a 100644 --- a/src/components/Layout/SidebarNav/SidebarNav.tsx +++ b/src/components/Layout/SidebarNav/SidebarNav.tsx @@ -45,7 +45,7 @@ export default function SidebarNav({

-
+
diff --git a/src/components/MDX/CodeBlock/CodeBlock.tsx b/src/components/MDX/CodeBlock/CodeBlock.tsx index 3195bbe8f..21ab31e7c 100644 --- a/src/components/MDX/CodeBlock/CodeBlock.tsx +++ b/src/components/MDX/CodeBlock/CodeBlock.tsx @@ -202,6 +202,7 @@ const CodeBlock = function CodeBlock({ return (
-
+

{children}

diff --git a/src/components/MDX/ConsoleBlock.tsx b/src/components/MDX/ConsoleBlock.tsx index de6201b6a..5683d6dcf 100644 --- a/src/components/MDX/ConsoleBlock.tsx +++ b/src/components/MDX/ConsoleBlock.tsx @@ -38,7 +38,7 @@ function ConsoleBlock({level = 'error', children}: ConsoleBlockProps) { } return ( -
+
@@ -48,8 +48,8 @@ function ConsoleBlock({level = 'error', children}: ConsoleBlockProps) { Console
- - + +
diff --git a/src/components/MDX/ExpandableCallout.tsx b/src/components/MDX/ExpandableCallout.tsx index 1fb1ea0ce..c46898026 100644 --- a/src/components/MDX/ExpandableCallout.tsx +++ b/src/components/MDX/ExpandableCallout.tsx @@ -8,8 +8,9 @@ import cn from 'classnames'; import {IconNote} from '../Icon/IconNote'; import {IconWarning} from '../Icon/IconWarning'; import {IconPitfall} from '../Icon/IconPitfall'; +import {IconCanary} from '../Icon/IconCanary'; -type CalloutVariants = 'deprecated' | 'pitfall' | 'note' | 'wip'; +type CalloutVariants = 'deprecated' | 'pitfall' | 'note' | 'wip' | 'canary'; interface ExpandableCalloutProps { children: React.ReactNode; @@ -34,6 +35,15 @@ const variantMap = { overlayGradient: 'linear-gradient(rgba(245, 249, 248, 0), rgba(245, 249, 248, 1)', }, + canary: { + title: 'Canary', + Icon: IconCanary, + containerClasses: + 'bg-gray-5 dark:bg-gray-60 dark:bg-opacity-20 text-primary dark:text-primary-dark text-lg', + textColor: 'text-gray-60 dark:text-gray-30', + overlayGradient: + 'linear-gradient(rgba(245, 249, 248, 0), rgba(245, 249, 248, 1)', + }, pitfall: { title: 'Pitfall', Icon: IconPitfall, @@ -65,7 +75,7 @@ function ExpandableCallout({children, type = 'note'}: ExpandableCalloutProps) { )}>

{variant.title}

diff --git a/src/components/MDX/ExpandableExample.tsx b/src/components/MDX/ExpandableExample.tsx index 1ad1e0313..1e709e483 100644 --- a/src/components/MDX/ExpandableExample.tsx +++ b/src/components/MDX/ExpandableExample.tsx @@ -70,13 +70,13 @@ function ExpandableExample({children, excerpt, type}: ExpandableExampleProps) { })}> {isDeepDive && ( <> - + Deep Dive )} {isExample && ( <> - + Example )} @@ -98,7 +98,7 @@ function ExpandableExample({children, excerpt, type}: ExpandableExampleProps) { isExample, })} onClick={() => setIsExpanded((current) => !current)}> - + {isExpanded ? 'Hide Details' : 'Show Details'} diff --git a/src/components/MDX/Heading.tsx b/src/components/MDX/Heading.tsx index 707e5e3ca..50e209e74 100644 --- a/src/components/MDX/Heading.tsx +++ b/src/components/MDX/Heading.tsx @@ -39,7 +39,7 @@ const Heading = forwardRefWithAs(function Heading( height="1em" viewBox="0 0 13 13" xmlns="http://www.w3.org/2000/svg" - className="text-gray-70 ml-2 h-5 w-5"> + className="text-gray-70 ms-2 h-5 w-5"> diff --git a/src/components/MDX/InlineCode.tsx b/src/components/MDX/InlineCode.tsx index 4a87c2a53..d206e9888 100644 --- a/src/components/MDX/InlineCode.tsx +++ b/src/components/MDX/InlineCode.tsx @@ -13,6 +13,7 @@ function InlineCode({ }: JSX.IntrinsicElements['code'] & InlineCodeProps) { return ( in case of RTL languages to avoid like `()console.log` to be rendered as `console.log()` className={cn( 'inline text-code text-secondary dark:text-secondary-dark px-1 rounded-md no-underline', { diff --git a/src/components/MDX/MDXComponents.module.css b/src/components/MDX/MDXComponents.module.css index 9840e77ce..e3ed413e0 100644 --- a/src/components/MDX/MDXComponents.module.css +++ b/src/components/MDX/MDXComponents.module.css @@ -8,11 +8,11 @@ } .markdown ol { - @apply mb-4 ml-8 list-decimal; + @apply mb-4 ms-8 list-decimal; } .markdown ul { - @apply mb-4 ml-8 list-disc; + @apply mb-4 ms-8 list-disc; } .markdown h1 { @@ -30,7 +30,7 @@ } .markdown code { - @apply text-gray-70 bg-card dark:bg-card-dark p-1 rounded-lg no-underline; + @apply text-gray-70 bg-card dark:bg-card-dark p-1 rounded-lg no-underline; font-size: 90%; } diff --git a/src/components/MDX/MDXComponents.tsx b/src/components/MDX/MDXComponents.tsx index 8ddf371c9..a35a15147 100644 --- a/src/components/MDX/MDXComponents.tsx +++ b/src/components/MDX/MDXComponents.tsx @@ -62,13 +62,13 @@ const Strong = (strong: JSX.IntrinsicElements['strong']) => ( ); const OL = (p: JSX.IntrinsicElements['ol']) => ( -
    +
      ); const LI = (p: JSX.IntrinsicElements['li']) => (
    1. ); const UL = (p: JSX.IntrinsicElements['ul']) => ( -
        +
          ); const Divider = () => ( @@ -87,6 +87,10 @@ const Note = ({children}: {children: React.ReactNode}) => ( {children} ); +const Canary = ({children}: {children: React.ReactNode}) => ( + {children} +); + const Blockquote = ({ children, ...props @@ -123,7 +127,7 @@ function LearnMore({ href={path} type="primary"> Read More - + ) : null}
@@ -137,7 +141,7 @@ function ReadBlogPost({path}: {path: string}) { return ( Read Post - + ); } @@ -191,7 +195,7 @@ function AuthorCredit({ }) { return (
-

+

Illustrated by{' '} {authorLink ? ( @@ -403,7 +407,7 @@ export const MDXComponents = { return children; }, MaxWidth({children}: {children: any}) { - return

{children}
; + return
{children}
; }, Pitfall, Deprecated, @@ -416,6 +420,7 @@ export const MDXComponents = { Math, MathI, Note, + Canary, PackageImport, ReadBlogPost, Recap, diff --git a/src/components/MDX/Sandpack/Console.tsx b/src/components/MDX/Sandpack/Console.tsx index 23194c870..85daee1f8 100644 --- a/src/components/MDX/Sandpack/Console.tsx +++ b/src/components/MDX/Sandpack/Console.tsx @@ -162,7 +162,7 @@ export const SandpackConsole = ({visible}: {visible: boolean}) => { className="flex items-center p-1" onClick={() => setIsExpanded(!isExpanded)}> - Console ({logs.length}) + Console ({logs.length}) ); } diff --git a/src/components/MDX/Sandpack/NavigationBar.tsx b/src/components/MDX/Sandpack/NavigationBar.tsx index 94e2eb4b3..1392ea7dd 100644 --- a/src/components/MDX/Sandpack/NavigationBar.tsx +++ b/src/components/MDX/Sandpack/NavigationBar.tsx @@ -21,6 +21,7 @@ import {ResetButton} from './ResetButton'; import {DownloadButton} from './DownloadButton'; import {IconChevron} from '../../Icon/IconChevron'; import {Listbox} from '@headlessui/react'; +import {OpenInTypeScriptPlaygroundButton} from './OpenInTypeScriptPlayground'; export function useEvent(fn: any): any { const ref = useRef(null); @@ -137,7 +138,7 @@ export function NavigationBar({providedFiles}: {providedFiles: Array}) { // space that's taken by the (invisible) tab list.
{isMultiFile && showDropdown && ( - + {visibleFiles.map((filePath: string) => ( {({active}) => ( @@ -179,11 +180,16 @@ export function NavigationBar({providedFiles}: {providedFiles: Array}) {
+ {activeFile.endsWith('.tsx') && ( + + )}
); diff --git a/src/components/MDX/Sandpack/OpenInCodeSandboxButton.tsx b/src/components/MDX/Sandpack/OpenInCodeSandboxButton.tsx index 42a2d2743..f943ee6ff 100644 --- a/src/components/MDX/Sandpack/OpenInCodeSandboxButton.tsx +++ b/src/components/MDX/Sandpack/OpenInCodeSandboxButton.tsx @@ -8,10 +8,10 @@ import {IconNewPage} from '../../Icon/IconNewPage'; export const OpenInCodeSandboxButton = () => { return ( diff --git a/src/components/MDX/Sandpack/OpenInTypeScriptPlayground.tsx b/src/components/MDX/Sandpack/OpenInTypeScriptPlayground.tsx new file mode 100644 index 000000000..f4b7ba77d --- /dev/null +++ b/src/components/MDX/Sandpack/OpenInTypeScriptPlayground.tsx @@ -0,0 +1,26 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + */ + +import {IconNewPage} from '../../Icon/IconNewPage'; + +export const OpenInTypeScriptPlaygroundButton = (props: {content: string}) => { + const contentWithReactImport = `import * as React from 'react';\n\n${props.content}`; + return ( + + + TypeScript Playground + + ); +}; diff --git a/src/components/MDX/Sandpack/ResetButton.tsx b/src/components/MDX/Sandpack/ResetButton.tsx index 1ac413138..243ce2349 100644 --- a/src/components/MDX/Sandpack/ResetButton.tsx +++ b/src/components/MDX/Sandpack/ResetButton.tsx @@ -15,7 +15,7 @@ export function ResetButton({onReset}: ResetButtonProps) { onClick={onReset} title="Reset Sandbox" type="button"> - Reset + Reset ); } diff --git a/src/components/MDX/Sandpack/SandpackRoot.tsx b/src/components/MDX/Sandpack/SandpackRoot.tsx index 043daf9b1..f710f8fe0 100644 --- a/src/components/MDX/Sandpack/SandpackRoot.tsx +++ b/src/components/MDX/Sandpack/SandpackRoot.tsx @@ -62,7 +62,7 @@ code { } ul { - padding-left: 20px; + padding-inline-start: 20px; } `.trim(); @@ -78,7 +78,7 @@ function SandpackRoot(props: SandpackProps) { }; return ( -
+
(
-
+
{code}
diff --git a/src/components/MDX/TeamMember.tsx b/src/components/MDX/TeamMember.tsx index 0db067e10..da2dc4535 100644 --- a/src/components/MDX/TeamMember.tsx +++ b/src/components/MDX/TeamMember.tsx @@ -53,7 +53,7 @@ export function TeamMember({ className="block w-full sm:hidden flex-grow basis-2/5 rounded overflow-hidden relative"> {name}
-
+

{name}

@@ -61,23 +61,23 @@ export function TeamMember({ {children}
{twitter && ( -
+
- + {twitter}
)} {github && ( -
+
- {github} + {github}
)} @@ -86,7 +86,7 @@ export function TeamMember({ aria-label="Personal Site" href={`https://${personal}`} className="hover:text-primary dark:text-primary-dark flex flex-row items-center"> - {personal} + {personal} )}
diff --git a/src/components/MDX/TerminalBlock.tsx b/src/components/MDX/TerminalBlock.tsx index 9fb5ff35f..fc13af338 100644 --- a/src/components/MDX/TerminalBlock.tsx +++ b/src/components/MDX/TerminalBlock.tsx @@ -17,9 +17,9 @@ interface TerminalBlockProps { function LevelText({type}: {type: LogLevel}) { switch (type) { case 'warning': - return Warning: ; + return Warning: ; case 'error': - return Error: ; + return Error: ; default: return null; } @@ -55,22 +55,25 @@ function TerminalBlock({level = 'info', children}: TerminalBlockProps) {
- Terminal + Terminal
-
+
{message}
diff --git a/src/components/MDX/YouWillLearnCard.tsx b/src/components/MDX/YouWillLearnCard.tsx index 839876029..d46a70277 100644 --- a/src/components/MDX/YouWillLearnCard.tsx +++ b/src/components/MDX/YouWillLearnCard.tsx @@ -29,7 +29,7 @@ function YouWillLearnCard({title, path, children}: YouWillLearnCardProps) { size="md" label={title}> Read More - +
diff --git a/src/components/PageHeading.tsx b/src/components/PageHeading.tsx index a92cd8f60..b6437b46b 100644 --- a/src/components/PageHeading.tsx +++ b/src/components/PageHeading.tsx @@ -6,9 +6,12 @@ import Breadcrumbs from 'components/Breadcrumbs'; import Tag from 'components/Tag'; import {H1} from './MDX/Heading'; import type {RouteTag, RouteItem} from './Layout/getRouteMeta'; +import * as React from 'react'; +import {IconCanary} from './Icon/IconCanary'; interface PageHeadingProps { title: string; + canary?: boolean; status?: string; description?: string; tags?: RouteTag[]; @@ -18,20 +21,27 @@ interface PageHeadingProps { function PageHeading({ title, status, + canary, description, tags = [], breadcrumbs, }: PageHeadingProps) { return (
-
+
{breadcrumbs ? : null}

{title} + {canary && ( + + )} {status ? —{status} : ''}

{description && ( -

+

{description}

)} diff --git a/src/components/Seo.tsx b/src/components/Seo.tsx index e76df63e2..d4f037128 100644 --- a/src/components/Seo.tsx +++ b/src/components/Seo.tsx @@ -53,7 +53,7 @@ export const Seo = withRouter( const canonicalUrl = `https://${siteDomain}${ router.asPath.split(/[\?\#]/)[0] }`; - const pageTitle = isHomePage ? 'React' : title + ' – React'; + const pageTitle = isHomePage ? title : title + ' – React'; // Twitter's meta parser is not very good. const twitterTitle = pageTitle.replace(/[<>]/g, ''); return ( diff --git a/src/components/SocialBanner.tsx b/src/components/SocialBanner.tsx index 826119c14..e980b6f4d 100644 --- a/src/components/SocialBanner.tsx +++ b/src/components/SocialBanner.tsx @@ -37,7 +37,7 @@ export default function SocialBanner() { )}>
{bannerText}
🇺🇦
{bannerLinkText} diff --git a/src/components/Tag.tsx b/src/components/Tag.tsx index 7033e030a..2e63a81f6 100644 --- a/src/components/Tag.tsx +++ b/src/components/Tag.tsx @@ -37,7 +37,7 @@ interface TagProps { function Tag({text, variant, className}: TagProps) { const {name, classes} = variantMap[variant]; return ( - + -Эти компоненты сейчас находятся внутри **файла корневого компонента,** который в примере называется `App.js`. При использовании [Create React App](https://create-react-app.dev/) приложение находится в `src/App.js`. Однако, в зависимости от конфигурации проекта, корневой компонент может находиться в другом файле. У фреймворка с маршрутизацией на основе файлов, например Next.js, корневой компонент будет разным для каждой страницы. +Эти компоненты сейчас находятся внутри **файла корневого компонента,** который в примере называется `App.js`. Однако, в зависимости от конфигурации проекта, корневой компонент может находиться в другом файле. У фреймворка с маршрутизацией на основе файлов, например Next.js, корневой компонент будет разным для каждой страницы. ## Экспорт и импорт компонентов {/*exporting-and-importing-a-component*/} diff --git a/src/content/learn/tutorial-tic-tac-toe.md b/src/content/learn/tutorial-tic-tac-toe.md index 28d997b04..16e5f518d 100644 --- a/src/content/learn/tutorial-tic-tac-toe.md +++ b/src/content/learn/tutorial-tic-tac-toe.md @@ -1173,7 +1173,7 @@ Why didn't this problem happen earlier? When you were passing `onSquareClick={handleClick}`, you were passing the `handleClick` function down as a prop. You were not calling it! But now you are *calling* that function right away--notice the parentheses in `handleClick(0)`--and that's why it runs too early. You don't *want* to call `handleClick` until the user clicks! -You could fix by creating a function like `handleFirstSquareClick` that calls `handleClick(0)`, a function like `handleSecondSquareClick` that calls `handleClick(1)`, and so on. You would pass (rather than call) these functions down as props like `onSquareClick={handleFirstSquareClick}`. This would solve the infinite loop. +You could fix this by creating a function like `handleFirstSquareClick` that calls `handleClick(0)`, a function like `handleSecondSquareClick` that calls `handleClick(1)`, and so on. You would pass (rather than call) these functions down as props like `onSquareClick={handleFirstSquareClick}`. This would solve the infinite loop. However, defining nine different functions and giving each of them a name is too verbose. Instead, let's do this: diff --git a/src/content/learn/typescript.md b/src/content/learn/typescript.md new file mode 100644 index 000000000..d437096e1 --- /dev/null +++ b/src/content/learn/typescript.md @@ -0,0 +1,463 @@ +--- +title: Using TypeScript +re: https://github.com/reactjs/react.dev/issues/5960 +--- + + + +TypeScript is a popular way to add type definitions to JavaScript codebases. Out of the box, TypeScript [supports JSX](/learn/writing-markup-with-jsx) and you can get full React Web support by adding [`@types/react`](https://www.npmjs.com/package/@types/react) and [`@types/react-dom`](https://www.npmjs.com/package/@types/react-dom) to your project. + + + + + +* [TypeScript with React Components](/learn/typescript#typescript-with-react-components) +* [Examples of typing with hooks](/learn/typescript#example-hooks) +* [Common types from `@types/react`](/learn/typescript/#useful-types) +* [Further learning locations](/learn/typescript/#further-learning) + + + +## Installation {/*installation*/} + +All [production-grade React frameworks](https://react-dev-git-fork-orta-typescriptpage-fbopensource.vercel.app/learn/start-a-new-react-project#production-grade-react-frameworks) offer support for using TypeScript. Follow the framework specific guide for installation: + +- [Next.js](https://nextjs.org/docs/pages/building-your-application/configuring/typescript) +- [Remix](https://remix.run/docs/en/1.19.2/guides/typescript) +- [Gatsby](https://www.gatsbyjs.com/docs/how-to/custom-configuration/typescript/) +- [Expo](https://docs.expo.dev/guides/typescript/) + +### Adding TypeScript to an existing React project {/*adding-typescript-to-an-existing-react-project*/} + +To install the latest version of React's type definitions: + + +npm install @types/react @types/react-dom + + +The following compiler options need to be set in your `tsconfig.json`: + +1. `dom` must be included in [`lib`](https://www.typescriptlang.org/tsconfig/#lib) (Note: If no `lib` option is specified, `dom` is included by default). +1. [`jsx`](https://www.typescriptlang.org/tsconfig/#jsx) must be set to one of the valid options. `preserve` should suffice for most applications. + If you're publishing a library, consult the [`jsx` documentation](https://www.typescriptlang.org/tsconfig/#jsx) on what value to choose. + +## TypeScript with React Components {/*typescript-with-react-components*/} + + + +Every file containing JSX must use the `.tsx` file extension. This is a TypeScript-specific extension that tells TypeScript that this file contains JSX. + + + +Writing TypeScript with React is very similar to writing JavaScript with React. The key difference when working with a component is that you can provide types for your component's props. These types can be used for correctness checking and providing inline documentation in editors. + +Taking the [`MyButton` component](/learn#components) from the [Quick Start](/learn) guide, we can add a type describing the `title` for the button: + + + +```tsx App.tsx active +function MyButton({ title }: { title: string }) { + return ( + + ); +} + +export default function MyApp() { + return ( +
+

Welcome to my app

+ +
+ ); +} +``` + +```js App.js hidden +import AppTSX from "./App.tsx"; +export default App = AppTSX; +``` +
+ + + +These sandboxes can handle TypeScript code, but they do not run the type-checker. This means you can amend the TypeScript sandboxes to learn, but you won't get any type errors or warnings. To get type-checking, you can use the [TypeScript Playground](https://www.typescriptlang.org/play) or use a more fully-featured online sandbox. + + + +This inline syntax is the simplest way to provide types for a component, though once you start to have a few fields to describe it can become unwieldy. Instead, you can use an `interface` or `type` to describe the component's props: + + + +```tsx App.tsx active +interface MyButtonProps { + /** The text to display inside the button */ + title: string; + /** Whether the button can be interacted with */ + disabled: boolean; +} + +function MyButton({ title, disabled }: MyButtonProps) { + return ( + + ); +} + +export default function MyApp() { + return ( +
+

Welcome to my app

+ +
+ ); +} +``` + +```js App.js hidden +import AppTSX from "./App.tsx"; +export default App = AppTSX; +``` + +
+ +The type describing your component's props can be as simple or as complex as you need, though they should be an object type described with either a `type` or `interface`. You can learn about how TypeScript describes objects in [Object Types](https://www.typescriptlang.org/docs/handbook/2/objects.html) but you may also be interested in using [Union Types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types) to describe a prop that can be one of a few different types and the [Creating Types from Types](https://www.typescriptlang.org/docs/handbook/2/types-from-types.html) guide for more advanced use cases. + + +## Example Hooks {/*example-hooks*/} + +The type definitions from `@types/react` include types for the built-in hooks, so you can use them in your components without any additional setup. They are built to take into account the code you write in your component, so you will get [inferred types](https://www.typescriptlang.org/docs/handbook/type-inference.html) a lot of the time and ideally do not need to handle the minutiae of providing the types. + +However, we can look at a few examples of how to provide types for hooks. + +### `useState` {/*typing-usestate*/} + +The [`useState` hook](/reference/react/useState) will re-use the value passed in as the initial state to determine what the type of the value should be. For example: + +```ts +// Infer the type as "boolean" +const [enabled, setEnabled] = useState(false); +``` + +Will assign the type of `boolean` to `enabled`, and `setEnabled` will be a function accepting either a `boolean` argument, or a function that returns a `boolean`. If you want to explicitly provide a type for the state, you can do so by providing a type argument to the `useState` call: + +```ts +// Explicitly set the type to "boolean" +const [enabled, setEnabled] = useState(false); +``` + +This isn't very useful in this case, but a common case where you may want to provide a type is when you have a union type. For example, `status` here can be one of a few different strings: + +```ts +type Status = "idle" | "loading" | "success" | "error"; + +const [status, setStatus] = useState("idle"); +``` + +Or, as recommended in [Principles for structuring state](/learn/choosing-the-state-structure#principles-for-structuring-state), you can group related state as an object and describe the different possibilities via object types: + +```ts +type RequestState = + | { status: 'idle' } + | { status: 'loading' } + | { status: 'success', data: any } + | { status: 'error', error: Error }; + +const [requestState, setRequestState] = useState({ status: 'idle' }); +``` + +### `useReducer` {/*typing-usereducer*/} + +The [`useReducer` hook](/reference/react/useReducer) is a more complex hook that takes a reducer function and an initial state. The types for the reducer function are inferred from the initial state. You can optionally provide a type argument to the `useReducer` call to provide a type for the state, but it is often better to set the type on the initial state instead: + + + +```tsx App.tsx active +import {useReducer} from 'react'; + +interface State { + count: number +}; + +type CounterAction = + | { type: "reset" } + | { type: "setCount"; value: State["count"] } + +const initialState: State = { count: 0 }; + +function stateReducer(state: State, action: CounterAction): State { + switch (action.type) { + case "reset": + return initialState; + case "setCount": + return { ...state, count: action.value }; + default: + throw new Error("Unknown action"); + } +} + +export default function App() { + const [state, dispatch] = useReducer(stateReducer, initialState); + + const addFive = () => dispatch({ type: "setCount", value: state.count + 5 }); + const reset = () => dispatch({ type: "reset" }); + + return ( +
+

Welcome to my counter

+ +

Count: {state.count}

+ + +
+ ); +} + +``` + +```js App.js hidden +import AppTSX from "./App.tsx"; +export default App = AppTSX; +``` + +
+ + +We are using TypeScript in a few key places: + + - `interface State` describes the shape of the reducer's state. + - `type CounterAction` describes the different actions which can be dispatched to the reducer. + - `const initialState: State` provides a type for the initial state, and also the type which is used by `useReducer` by default. + - `stateReducer(state: State, action: CounterAction): State` sets the types for the reducer function's arguments and return value. + +A more explicit alternative to setting the type on `initialState` is to provide a type argument to `useReducer`: + +```ts +import { stateReducer, State } from './your-reducer-implementation'; + +const initialState = { count: 0 }; + +export default function App() { + const [state, dispatch] = useReducer(stateReducer, initialState); +} +``` + +### `useContext` {/*typing-usecontext*/} + +The [`useContext` hook](/reference/react/useContext) is a technique for passing data down the component tree without having to pass props through components. It is used by creating a provider component and often by creating a hook to consume the value in a child component. + +The type of the value provided by the context is inferred from the value passed to the `createContext` call: + + + +```tsx App.tsx active +import { createContext, useContext, useState } from 'react'; + +type Theme = "light" | "dark" | "system"; +const ThemeContext = createContext("system"); + +const useGetTheme = () => useContext(ThemeContext); + +export default function MyApp() { + const [theme, setTheme] = useState('light'); + + return ( + + + + ) +} + +function MyComponent() { + const theme = useGetTheme(); + + return ( +
+

Current theme: {theme}

+
+ ) +} +``` + +```js App.js hidden +import AppTSX from "./App.tsx"; +export default App = AppTSX; +``` + +
+ +This technique works when you have an default value which makes sense - but there are occasionally cases when you do not, and in those cases `null` can feel reasonable as a default value. However, to allow the type-system to understand your code, you need to explicitly set `ContextShape | null` on the `createContext`. + +This causes the issue that you need to eliminate the `| null` in the type for context consumers. Our recommendation is to have the hook do a runtime check for it's existence and throw an error when not present: + +```js {5, 16-20} +import { createContext, useContext, useState, useMemo } from 'react'; + +// This is a simpler example, but you can imagine a more complex object here +type ComplexObject = { + kind: string +}; + +// The context is created with `| null` in the type, to accurately reflect the default value. +const Context = createContext(null); + +// The `| null` will be removed via the check in the hook. +const useGetComplexObject = () => { + const object = useContext(Context); + if (!object) { throw new Error("useGetComplexObject must be used within a Provider") } + return object; +} + +export default function MyApp() { + const object = useMemo(() => ({ kind: "complex" }), []); + + return ( + + + + ) +} + +function MyComponent() { + const object = useGetComplexObject(); + + return ( +
+

Current object: {object.kind}

+
+ ) +} +``` + +### `useMemo` {/*typing-usememo*/} + +The [`useMemo`](/reference/react/useMemo) hooks will create/re-access a memorized value from a function call, re-running the function only when dependencies passed as the 2nd parameter are changed. The result of calling the hook is inferred from the return value from the function in the first parameter. You can be more explicit by providing a type argument to the hook. + +```ts +// The type of visibleTodos is inferred from the return value of filterTodos +const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]); +``` + + +### `useCallback` {/*typing-usecallback*/} + +The [`useCallback`](/reference/react/useCallback) provide a stable reference to a function as long as the dependencies passed into the second parameter are the same. Like `useMemo`, the function's type is inferred from the return value of the function in the first parameter, and you can be more explicit by providing a type argument to the hook. + + +```ts +const handleClick = useCallback(() => { + // ... +}, [todos]); +``` + +When working in TypeScript strict mode `useCallback` requires adding types for the parameters in your callback. This is because the type of the callback is inferred from the return value of the function, and without parameters the type cannot be fully understood. + +Depending on your code-style preferences, you could use the `*EventHandler` functions from the React types to provide the type for the event handler at the same time as defining the callback: + +```ts +import { useState, useCallback } from 'react'; + +export default function Form() { + const [value, setValue] = useState("Change me"); + + const handleChange = useCallback>((event) => { + setValue(event.currentTarget.value); + }, [setValue]) + + return ( + <> + +

Value: {value}

+ + ); +} +``` + +## Useful Types {/*useful-types*/} + +There is quite an expansive set of types which come from the `@types/react` package, it is worth a read when you feel comfortable with how React and TypeScript interact. You can find them [in React's folder in DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts). We will cover a few of the more common types here. + +### DOM Events {/*typing-dom-events*/} + +When working with DOM events in React, the type of the event can often be inferred from the event handler. However, when you want to extract a function to be passed to an event handler, you will need to explicitly set the type of the event. + + + +```tsx App.tsx active +import { useState } from 'react'; + +export default function Form() { + const [value, setValue] = useState("Change me"); + + function handleChange(event: React.ChangeEvent) { + setValue(event.currentTarget.value); + } + + return ( + <> + +

Value: {value}

+ + ); +} +``` + +```js App.js hidden +import AppTSX from "./App.tsx"; +export default App = AppTSX; +``` + +
+ +There are many types of events provided in the React types - the full list can be found [here](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/b580df54c0819ec9df62b0835a315dd48b8594a9/types/react/index.d.ts#L1247C1-L1373) which is based on the [most popular events from the DOM](https://developer.mozilla.org/en-US/docs/Web/Events). + +When determining the type you are looking for you can first look at the hover information for the event handler you are using, which will show the type of the event. + +If you need to use an event that is not included in this list, you can use the `React.SyntheticEvent` type, which is the base type for all events. + +### Children {/*typing-children*/} + +There are two common paths to describing the children of a component. The first is to use the `React.ReactNode` type, which is a union of all the possible types that can be passed as children in JSX: + +```ts +interface ModalRendererProps { + title: string; + children: React.ReactNode; +} +``` + +This is a very broad definition of children. The second is to use the `React.ReactElement` type, which is only JSX elements and not JavaScript primitives like strings or numbers: + +```ts +interface ModalRendererProps { + title: string; + children: React.ReactElement; +} +``` + +Note, that you cannot use TypeScript to describe that the children are a certain type of JSX elements, so you cannot use the type-system to describe a component which only accepts `
  • ` children. + +You can see all an example of both `React.ReactNode` and `React.ReactElement` with the type-checker in [this TypeScript playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgIilQ3wChSB6CxYmAOmXRgDkIATJOdNJMGAZzgwAFpxAR+8YADswAVwGkZMJFEzpOjDKw4AFHGEEBvUnDhphwADZsi0gFw0mDWjqQBuUgF9yaCNMlENzgAXjgACjADfkctFnYkfQhDAEpQgD44AB42YAA3dKMo5P46C2tbJGkvLIpcgt9-QLi3AEEwMFCItJDMrPTTbIQ3dKywdIB5aU4kKyQQKpha8drhhIGzLLWODbNs3b3s8YAxKBQAcwXpAThMaGWDvbH0gFloGbmrgQfBzYpd1YjQZbEYARkB6zMwO2SHSAAlZlYIBCdtCRkZpHIrFYahQYQD8UYYFA5EhcfjyGYqHAXnJAsIUHlOOUbHYhMIIHJzsI0Qk4P9SLUBuRqXEXEwAKKfRZcNA8PiCfxWACecAAUgBlAAacFm80W-CU11U6h4TgwUv11yShjgJjMLMqDnN9Dilq+nh8pD8AXgCHdMrCkWisVoAet0R6fXqhWKhjKllZVVxMcavpd4Zg7U6Qaj+2hmdG4zeRF10uu-Aeq0LBfLMEe-V+T2L7zLVu+FBWLdLeq+lc7DYFf39deFVOotMCACNOCh1dq219a+30uC8YWoZsRyuEdjkevR8uvoVMdjyTWt4WiSSydXD4NqZP4AymeZE072ZzuUeZQKheQgA). + +### Style Props {/*typing-style-props*/} + +When using inline styles in React, you can use `React.CSSProperties` to describe the object passed to the `style` prop. This type is a union of all the possible CSS properties, and is a good way to ensure you are passing valid CSS properties to the `style` prop, and to get auto-complete in your editor. + +```ts +interface MyComponentProps { + style: React.CSSProperties; +} +``` + +## Further learning {/*further-learning*/} + +This guide has covered the basics of using TypeScript with React, but there is a lot more to learn. +Individual API pages on the docs may contain more in-depth documentation on how to use them with TypeScript. + +We recommend the following resources: + + - [The TypeScript handbook](https://www.typescriptlang.org/docs/handbook/) is the official documentation for TypeScript, and covers most key language features. + + - [The TypeScript release notes](https://devblogs.microsoft.com/typescript/) covers a each new features in-depth. + + - [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app/) is a community-maintained cheatsheet for using TypeScript with React, covering a lot of useful edge cases and providing more breadth than this document. + + - [TypeScript Community Discord](discord.com/invite/typescript) is a great place to ask questions and get help with TypeScript and React issues. \ No newline at end of file diff --git a/src/content/learn/your-first-component.md b/src/content/learn/your-first-component.md index f4b58b216..55d57f8a5 100644 --- a/src/content/learn/your-first-component.md +++ b/src/content/learn/your-first-component.md @@ -211,7 +211,7 @@ function Profile() { #### Компоненты на все случаи жизни {/*components-all-the-way-down*/} -Ваше React-приложение начинается с "корневого" компонента. Обычно он создаётся автоматически при создании нового проекта. Например, если вы используете [CodeSandbox](https://codesandbox.io/) или [Create React App](https://create-react-app.dev/), корневой компонент определён в файле `src/App.js`. Если вы используете фреймворк [Next.js](https://nextjs.org/), корневой компонент определён в файле `pages/index.js`. В этих примерах вы экспортировали корневые компоненты. +Ваше React-приложение начинается с "корневого" компонента. Обычно он создаётся автоматически при создании нового проекта. Например, если вы используете [CodeSandbox](https://codesandbox.io/), корневой компонент определён в файле `src/App.js`. Если вы используете фреймворк [Next.js](https://nextjs.org/), корневой компонент определён в файле `pages/index.js`. В этих примерах вы экспортировали корневые компоненты. Большинство React-приложений используют компоненты повсюду. Это означает, что вы будете использовать компоненты не только для переиспользуемых элементов, таких как кнопки, но также для более крупных элементов: боковых панелей, списков и в конечном итоге, целых страниц! Компоненты — это удобный способ организации кода UI и разметки, даже если некоторые из них используются только один раз. diff --git a/src/content/reference/react-dom/components/common.md b/src/content/reference/react-dom/components/common.md index 04b0dc5e3..d3cc4d5b9 100644 --- a/src/content/reference/react-dom/components/common.md +++ b/src/content/reference/react-dom/components/common.md @@ -333,7 +333,7 @@ An event handler type for the [CSS animation](https://developer.mozilla.org/en-U * `e`: A [React event object](#react-event-object) with these extra [`AnimationEvent`](https://developer.mozilla.org/en-US/docs/Web/API/AnimationEvent) properties: * [`animationName`](https://developer.mozilla.org/en-US/docs/Web/API/AnimationEvent/animationName) * [`elapsedTime`](https://developer.mozilla.org/en-US/docs/Web/API/AnimationEvent/elapsedTime) - * [`pseudoElement`](https://developer.mozilla.org/en-US/docs/Web/API/AnimationEvent) + * [`pseudoElement`](https://developer.mozilla.org/en-US/docs/Web/API/AnimationEvent/pseudoElement) --- diff --git a/src/content/reference/react-dom/components/progress.md b/src/content/reference/react-dom/components/progress.md index b783a102d..9a8d60ab0 100644 --- a/src/content/reference/react-dom/components/progress.md +++ b/src/content/reference/react-dom/components/progress.md @@ -34,8 +34,8 @@ To display a progress indicator, render the [built-in browser ``](http Additionally, `` supports these props: -* [`max`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress#attr-max): A number. Specifies the maximum `value`. Defaults to `1`. -* [`value`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress#attr-value): A number between `0` and `max`, or `null` for indeterminate progress. Specifies how much was done. +* [`max`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress#max): A number. Specifies the maximum `value`. Defaults to `1`. +* [`value`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress#value): A number between `0` and `max`, or `null` for indeterminate progress. Specifies how much was done. --- diff --git a/src/content/reference/react-dom/components/select.md b/src/content/reference/react-dom/components/select.md index 93ff56ac5..46710908c 100644 --- a/src/content/reference/react-dom/components/select.md +++ b/src/content/reference/react-dom/components/select.md @@ -50,21 +50,21 @@ If your `` props are relevant both for uncontrolled and controlled select boxes: -* [`autoComplete`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select#attr-autocomplete): A string. Specifies one of the possible [autocomplete behaviors.](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete#values) -* [`autoFocus`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select#attr-autofocus): A boolean. If `true`, React will focus the element on mount. +* [`autoComplete`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select#autocomplete): A string. Specifies one of the possible [autocomplete behaviors.](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete#values) +* [`autoFocus`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select#autofocus): A boolean. If `true`, React will focus the element on mount. * `children`: `