diff --git a/.eslintrc.js b/.eslintrc.js index ebd98d0a..28d15eaf 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,7 +1,3 @@ -// const error = 2; -// const warn = 1; -const ignore = 0; - module.exports = { root: true, extends: ['@storybook/eslint-config-storybook'], diff --git a/.github/workflows/type-checking.yml b/.github/workflows/type-checking.yml index def581b0..bc3e2c95 100755 --- a/.github/workflows/type-checking.yml +++ b/.github/workflows/type-checking.yml @@ -10,4 +10,4 @@ jobs: - run: | yarn - run: | - yarn types + yarn typescript:check diff --git a/package.json b/package.json index be40a6f6..ad60b745 100644 --- a/package.json +++ b/package.json @@ -17,14 +17,15 @@ ], "scripts": { "build": "babel src -d dist --extensions \".js,.jsx,.ts,.tsx\" --ignore \"**/*.test.js\" --ignore \"**/*.stories.js\"", - "types": "tsc --declaration --emitDeclarationOnly --outDir dist --declarationMap", "build-docs": "build-storybook --docs", "build-storybook": "build-storybook -s .storybook/static", "lint": "yarn lint:js && yarn lint:package", "lint:js": "cross-env NODE_ENV=production eslint --cache --cache-location=.cache/eslint --ext .js,.jsx,.html,.ts,.tsx,.mjs --report-unused-disable-directives", "lint:package": "sort-package-json", - "release": "dotenv yarn build & yarn types && auto shipit", - "storybook": "start-storybook -p 6006 -s .storybook/static" + "release": "dotenv yarn build & yarn typescript:generate && auto shipit", + "storybook": "start-storybook -p 6006 -s .storybook/static", + "typescript:check": "tsc --project ./tsconfig.json --noEmit", + "typescript:generate": "tsc --declaration --emitDeclarationOnly --outDir dist --declarationMap" }, "husky": { "hooks": { @@ -48,11 +49,13 @@ "dependencies": { "@types/pluralize": "^0.0.29", "@types/prismjs": "^1.16.6", + "@types/react-modal": "^3.12.1", + "@types/styled-components": "^5.1.0", + "@types/uuid": "^8.3.1", "copy-to-clipboard": "^3.3.1", "pluralize": "^8.0.0", "polished": "^3.6.4", "prismjs": "1.23.0", - "prop-types": "^15.5.4", "react-github-button": "^0.1.11", "react-modal": "^3.11.2", "react-popper-tooltip": "^2.11.1", @@ -77,7 +80,6 @@ "@storybook/linter-config": "^2.5.0", "@storybook/react": "^6.2.0", "@types/fs-extra": "^9.0.1", - "@types/styled-components": "^5.1.0", "auto": "^9.50.1", "babel-eslint": "^10.1.0", "babel-loader": "^8.1.0", @@ -89,17 +91,17 @@ "husky": "^4.2.5", "lint-staged": "^10.2.9", "node-fetch": "^2.6.0", - "prettier": "^2.0.5", - "react": "^16.13.1", - "react-dom": "^16.13.1", + "prettier": "^2.3.0", + "react": "17", + "react-dom": "17", "seedrandom": "^3.0.5", "sort-package-json": "^1.44.0", "ts-loader": "^7.0.5", "typescript": "^3.9.5" }, "peerDependencies": { - "react": "^15.0.0 || ^16.0.0", - "react-dom": "^15.0.0 || ^16.0.0" + "react": "^15.0.0 || ^16.0.0 || ^17.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0" }, "engines": { "node": ">=10", diff --git a/src/components/CodeSnippets.stories.js b/src/components/CodeSnippets.stories.tsx similarity index 95% rename from src/components/CodeSnippets.stories.js rename to src/components/CodeSnippets.stories.tsx index b167e797..7fd1adb3 100644 --- a/src/components/CodeSnippets.stories.js +++ b/src/components/CodeSnippets.stories.tsx @@ -1,8 +1,6 @@ import React from 'react'; import styled from 'styled-components'; -import { Button } from './Button'; import { CodeSnippets } from './CodeSnippets'; -import { Highlight } from './Highlight'; import { javascriptCodeWithWrappers, typescriptCodeWithWrappers } from './Highlight.stories'; import { color } from './shared/styles'; diff --git a/src/components/CodeSnippets.js b/src/components/CodeSnippets.tsx similarity index 81% rename from src/components/CodeSnippets.js rename to src/components/CodeSnippets.tsx index ca29fff6..d6a8b6bb 100644 --- a/src/components/CodeSnippets.js +++ b/src/components/CodeSnippets.tsx @@ -1,5 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react'; -import PropTypes from 'prop-types'; +import React, { ComponentProps, ComponentType, useRef, useState } from 'react'; import styled from 'styled-components'; import { Clipboard } from './clipboard/Clipboard'; @@ -53,9 +52,9 @@ const StyledClipboard = styled(Clipboard)` } `; -function Snippet({ snippet }) { +function Snippet({ snippet }: { snippet: SnippetType }) { const { PreSnippet: PreSnippetComponent, Snippet: SnippetComponent } = snippet; - const snippetRef = useRef(); + const snippetRef = useRef(); const getCopyContent = () => snippetRef.current && snippetRef.current.textContent; return ( @@ -71,13 +70,6 @@ function Snippet({ snippet }) { ); } -Snippet.propTypes = { - snippet: PropTypes.shape({ - Snippet: PropTypes.elementType.isRequired, - PreSnippet: PropTypes.elementType, - }).isRequired, -}; - const TabsWrapper = styled.div` background: ${color.lightest}; border-top-left-radius: ${spacing.borderRadius.small}px; @@ -97,7 +89,7 @@ const StyledTabs = styled(LinkTabs)` } `; -function SnippetList({ snippets }) { +function SnippetList({ snippets }: { snippets: SnippetType[] }) { const [activeSnippet, setActiveSnippet] = useState(snippets[0]); const tabItems = snippets.map((snippet, index) => { @@ -123,16 +115,10 @@ function SnippetList({ snippets }) { ); } -SnippetList.propTypes = { - snippets: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string.isRequired, - renderTabLabel: PropTypes.func.isRequired, - }).isRequired - ).isRequired, -}; - -export function CodeSnippets({ snippets, ...rest }) { +export function CodeSnippets({ + snippets, + ...rest +}: Props & ComponentProps & { children?: never }) { return ( @@ -146,6 +132,13 @@ export function CodeSnippets({ snippets, ...rest }) { ); } -CodeSnippets.propTypes = { - snippets: PropTypes.arrayOf(PropTypes.shape({})).isRequired, -}; +interface SnippetType { + Snippet: ComponentType; + PreSnippet?: ComponentType; + id: string; + renderTabLabel: (...a: any[]) => string; +} + +interface Props { + snippets: SnippetType[]; +} diff --git a/src/components/Highlight.tsx b/src/components/Highlight.tsx index cf0360cf..37364438 100644 --- a/src/components/Highlight.tsx +++ b/src/components/Highlight.tsx @@ -7,14 +7,23 @@ import { color } from './shared/styles'; if (typeof document !== 'undefined') { // @ts-ignore global.Prism = Prism; + // @ts-ignore require('prismjs/components/prism-bash'); + // @ts-ignore require('prismjs/components/prism-javascript'); + // @ts-ignore require('prismjs/components/prism-typescript'); + // @ts-ignore require('prismjs/components/prism-json'); + // @ts-ignore require('prismjs/components/prism-css'); + // @ts-ignore require('prismjs/components/prism-yaml'); + // @ts-ignore require('prismjs/components/prism-markdown'); + // @ts-ignore require('prismjs/components/prism-jsx'); + // @ts-ignore require('prismjs/components/prism-tsx'); } diff --git a/src/components/Icon.stories.js b/src/components/Icon.stories.tsx similarity index 89% rename from src/components/Icon.stories.js rename to src/components/Icon.stories.tsx index ac1e3b68..c2c5762c 100644 --- a/src/components/Icon.stories.js +++ b/src/components/Icon.stories.tsx @@ -9,7 +9,7 @@ const Meta = styled.div` font-size: 12px; `; -const Item = styled.li` +const Item = styled.li<{ minimal?: boolean }>` display: inline-flex; flex-direction: row; align-items: center; @@ -62,7 +62,7 @@ export const Labels = () => ( {Object.keys(icons).map((key) => ( - + {key} ))} @@ -74,7 +74,7 @@ export const NoLabels = () => ( {Object.keys(icons).map((key) => ( - + ))} diff --git a/src/components/Input.stories.js b/src/components/Input.stories.tsx similarity index 97% rename from src/components/Input.stories.js rename to src/components/Input.stories.tsx index eb0c68fc..4bec6e67 100644 --- a/src/components/Input.stories.js +++ b/src/components/Input.stories.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { action } from '@storybook/addon-actions'; import styled from 'styled-components'; @@ -148,17 +147,9 @@ const All = ({ appearance }) => ( ); -All.propTypes = { - appearance: PropTypes.string, -}; - -All.defaultProps = { - appearance: undefined, -}; - export const Default = () => ( - + ); diff --git a/src/components/Input.js b/src/components/Input.tsx similarity index 73% rename from src/components/Input.js rename to src/components/Input.tsx index 855d1d10..a1ef9c86 100644 --- a/src/components/Input.js +++ b/src/components/Input.tsx @@ -1,15 +1,24 @@ -import React, { useEffect, useCallback, useRef, useState, forwardRef } from 'react'; -import PropTypes from 'prop-types'; +import React, { + useEffect, + useCallback, + useRef, + useState, + forwardRef, + ReactNode, + FC, + ComponentProps, + MutableRefObject, +} from 'react'; import styled, { css } from 'styled-components'; import { color, typography, spacing } from './shared/styles'; import { jiggle } from './shared/animation'; import { Icon } from './Icon'; import { Link } from './Link'; -import WithTooltip, { validPlacements as validTooltipPlacements } from './tooltip/WithTooltip'; +import WithTooltip from './tooltip/WithTooltip'; import { TooltipMessage } from './tooltip/TooltipMessage'; // prettier-ignore -const Label = styled.label` +const Label = styled.label>` font-weight: ${props => props.appearance !== 'code' && typography.weight.bold}; font-family: ${props => props.appearance === 'code' && typography.type.code }; font-size: ${props => props.appearance === 'code' ? typography.size.s1 - 1 : typography.size.s2 }px; @@ -17,7 +26,7 @@ const Label = styled.label` `; // prettier-ignore -const LabelWrapper = styled.div` +const LabelWrapper = styled.div>` margin-bottom: 8px; ${props => props.hideLabel && css` @@ -55,7 +64,7 @@ const InputEl = styled.input` &:-webkit-autofill { -webkit-box-shadow: 0 0 0 3em ${color.lightest} inset; } `; -const getStackLevelStyling = (props) => { +const getStackLevelStyling = (props: Pick) => { const radius = 4; const stackLevelDefinedStyling = css` position: relative; @@ -97,7 +106,7 @@ const getStackLevelStyling = (props) => { }; // prettier-ignore -const InputWrapper = styled.div` +const InputWrapper = styled.div>` display: inline-block; position: relative; vertical-align: top; @@ -192,7 +201,7 @@ const InputWrapper = styled.div` `} `; // prettier-ignore -const InputContainer = styled.div` +const InputContainer = styled.div>` ${props => props.orientation === 'horizontal' && css` display: table-row; @@ -232,7 +241,11 @@ const Action = styled.div` z-index: 2; `; -const getErrorMessage = ({ error, value, lastErrorValue }) => { +const getErrorMessage = ({ + error, + value, + lastErrorValue, +}: Pick) => { let errorMessage = typeof error === 'function' ? error(value) : error; if (lastErrorValue) { if (value !== lastErrorValue) { @@ -242,25 +255,25 @@ const getErrorMessage = ({ error, value, lastErrorValue }) => { return errorMessage; }; -export const PureInput = forwardRef( +export const PureInput: FC> = forwardRef( ( { id, - value, + appearance = 'default', + className = null, + error = null, + errorTooltipPlacement = 'right', + hideLabel = false, + icon = null, label, - hideLabel, - orientation, - icon, - error, - appearance, - errorTooltipPlacement, - className, - lastErrorValue, - startingType, - type, - onActionClick, - stackLevel, - suppressErrorMessage, + lastErrorValue = null, + onActionClick = null, + orientation = 'vertical', + stackLevel = undefined, + startingType = 'text', + suppressErrorMessage = false, + type = 'text', + value = '', ...props }, ref @@ -339,84 +352,62 @@ export const PureInput = forwardRef( } ); -PureInput.propTypes = { - id: PropTypes.string.isRequired, - value: PropTypes.string, - appearance: PropTypes.oneOf(['default', 'pill', 'code']), - errorTooltipPlacement: PropTypes.oneOf(validTooltipPlacements), - stackLevel: PropTypes.oneOf(['top', 'middle', 'bottom']), - label: PropTypes.string.isRequired, - hideLabel: PropTypes.bool, - orientation: PropTypes.oneOf(['vertical', 'horizontal']), - icon: PropTypes.string, - error: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), - suppressErrorMessage: PropTypes.bool, - className: PropTypes.string, - lastErrorValue: PropTypes.string, - startingType: PropTypes.string, - type: PropTypes.string, - onActionClick: PropTypes.func, -}; +interface Props { + id: string; + value?: string; + appearance?: 'default' | 'pill' | 'code' | 'tertiary'; + errorTooltipPlacement?: ComponentProps['placement']; + stackLevel?: 'top' | 'middle' | 'bottom'; + label: string; + hideLabel?: boolean; + orientation?: 'vertical' | 'horizontal'; + icon?: ComponentProps['icon']; + error?: ReactNode | Function; + suppressErrorMessage?: boolean; + className?: string; + lastErrorValue?: string; + startingType?: string; + type?: string; + onActionClick?: Function; +} + +export const Input = forwardRef>( + ({ type: startingType, startFocused, ...rest }, ref) => { + const [type, setType] = useState(startingType); + const togglePasswordType = useCallback( + (event) => { + // Make sure this does not submit a form + event.preventDefault(); + event.stopPropagation(); + if (type === 'password') { + setType('text'); + return; + } + setType('password'); + }, + [type, setType] + ); -PureInput.defaultProps = { - value: '', - appearance: 'default', - errorTooltipPlacement: 'right', - stackLevel: undefined, - hideLabel: false, - orientation: 'vertical', - icon: null, - error: null, - suppressErrorMessage: false, - className: null, - lastErrorValue: null, - startingType: 'text', - type: 'text', - onActionClick: null, -}; + // Outside refs take precedence + const selfRef = useRef(); + const inputRef = (ref as MutableRefObject) || selfRef; + const didFocusOnStart = useRef(false); -export const Input = forwardRef(({ type: startingType, startFocused, ...rest }, ref) => { - const [type, setType] = useState(startingType); - const togglePasswordType = useCallback( - (event) => { - // Make sure this does not submit a form - event.preventDefault(); - event.stopPropagation(); - if (type === 'password') { - setType('text'); - return; + useEffect(() => { + if (inputRef && inputRef.current && startFocused && !didFocusOnStart.current) { + inputRef.current.focus(); + didFocusOnStart.current = true; } - setType('password'); - }, - [type, setType] - ); - // Outside refs take precedence - const inputRef = ref || useRef(); - const didFocusOnStart = useRef(false); - useEffect(() => { - if (inputRef && inputRef.current && startFocused && !didFocusOnStart.current) { - inputRef.current.focus(); - didFocusOnStart.current = true; - } - }, [inputRef, inputRef.current, didFocusOnStart, didFocusOnStart.current]); - - return ( - - ); -}); - -Input.propTypes = { - startFocused: PropTypes.bool, - type: PropTypes.string, -}; + }, [inputRef, inputRef.current, didFocusOnStart, didFocusOnStart.current]); -Input.defaultProps = { - startFocused: false, - type: 'text', -}; + return ( + + ); + } +); diff --git a/src/components/LinkTabs.stories.js b/src/components/LinkTabs.stories.tsx similarity index 53% rename from src/components/LinkTabs.stories.js rename to src/components/LinkTabs.stories.tsx index 407ba7f0..d36e44fd 100644 --- a/src/components/LinkTabs.stories.js +++ b/src/components/LinkTabs.stories.tsx @@ -4,9 +4,9 @@ import React from 'react'; import { LinkTabs } from './LinkTabs'; const items = [ - { label: 'Activity', title: 'View activity', href: '/activity' }, - { label: 'Components', title: 'View components', href: '/components', isActive: true }, - { label: 'Changeset', title: 'View UI changes', href: '/changes' }, + { key: '1', label: 'Activity', title: 'View activity', href: '/activity' }, + { key: '2', label: 'Components', title: 'View components', href: '/components', isActive: true }, + { key: '3', label: 'Changeset', title: 'View UI changes', href: '/changes' }, ]; export default { diff --git a/src/components/LinkTabs.js b/src/components/LinkTabs.tsx similarity index 78% rename from src/components/LinkTabs.js rename to src/components/LinkTabs.tsx index 81a0c3e9..3fcb1d12 100644 --- a/src/components/LinkTabs.js +++ b/src/components/LinkTabs.tsx @@ -1,5 +1,4 @@ -import PropTypes from 'prop-types'; -import React from 'react'; +import React, { ComponentProps, FC } from 'react'; import styled, { css } from 'styled-components'; import { Link } from './Link'; @@ -53,7 +52,21 @@ const Tab = styled(Link)` `}; `; -export const LinkTabs = ({ isLoading, items, ...props }) => ( +type ItemProps = { + key: string; + label: string; +} & ComponentProps; + +interface Props { + isLoading?: boolean; + items: ItemProps[]; +} + +export const LinkTabs: FC> = ({ + isLoading = false, + items = [], + ...props +}) => ( {items.map(({ key, label, ...item }) => (
  • @@ -64,13 +77,3 @@ export const LinkTabs = ({ isLoading, items, ...props }) => ( ))} ); - -LinkTabs.propTypes = { - isLoading: PropTypes.bool, - items: PropTypes.arrayOf(PropTypes.object), -}; - -LinkTabs.defaultProps = { - isLoading: false, - items: [], -}; diff --git a/src/components/Radio.tsx b/src/components/Radio.tsx index 2fd49e37..393ee25e 100644 --- a/src/components/Radio.tsx +++ b/src/components/Radio.tsx @@ -1,5 +1,4 @@ import React, { ComponentProps, FunctionComponent, ReactNode } from 'react'; -import PropTypes from 'prop-types'; import styled, { css } from 'styled-components'; import { rgba } from 'polished'; import { color, typography } from './shared/styles'; diff --git a/src/components/ShadowBoxCTA.stories.js b/src/components/ShadowBoxCTA.stories.tsx similarity index 100% rename from src/components/ShadowBoxCTA.stories.js rename to src/components/ShadowBoxCTA.stories.tsx diff --git a/src/components/ShadowBoxCTA.js b/src/components/ShadowBoxCTA.tsx similarity index 79% rename from src/components/ShadowBoxCTA.js rename to src/components/ShadowBoxCTA.tsx index 3f97f28e..4fe83600 100644 --- a/src/components/ShadowBoxCTA.js +++ b/src/components/ShadowBoxCTA.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { ComponentProps, FC, ReactNode } from 'react'; import styled from 'styled-components'; import { breakpoint, spacing, typography } from './shared/styles'; @@ -56,7 +55,18 @@ const Action = styled.div` } `; -export const ShadowBoxCTA = ({ action, headingText, messageText, ...rest }) => ( +interface Props { + headingText: ReactNode; + messageText?: ReactNode; + action: ReactNode; +} + +export const ShadowBoxCTA: FC> = ({ + action, + headingText, + messageText, + ...rest +}) => ( {headingText} @@ -66,13 +76,3 @@ export const ShadowBoxCTA = ({ action, headingText, messageText, ...rest }) => ( {action} ); - -ShadowBoxCTA.propTypes = { - headingText: PropTypes.node.isRequired, - messageText: PropTypes.node, - action: PropTypes.node.isRequired, -}; - -ShadowBoxCTA.defaultProps = { - messageText: undefined, -}; diff --git a/src/components/StoryLinkWrapper.js b/src/components/StoryLinkWrapper.js deleted file mode 100644 index 65112fa2..00000000 --- a/src/components/StoryLinkWrapper.js +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-disable import/no-extraneous-dependencies */ -// This is allows us to test whether the link works via the actions addon -import React from 'react'; -import PropTypes from 'prop-types'; -import { action } from '@storybook/addon-actions'; - -const fireClickAction = action('onLinkClick'); - -export function StoryLinkWrapper({ children, className, href, onClick, to, ...rest }) { - const modifiedOnClick = (event) => { - event.preventDefault(); - onClick(); - fireClickAction(href || to); - }; - - return ( - - {children} - - ); -} - -StoryLinkWrapper.propTypes = { - // eslint-disable-next-line react/forbid-prop-types - children: PropTypes.any.isRequired, - className: PropTypes.string, - href: PropTypes.string, - onClick: PropTypes.func, - to: PropTypes.string, -}; - -StoryLinkWrapper.defaultProps = { - className: '', - href: null, - onClick: () => {}, - to: null, -}; diff --git a/src/components/StoryLinkWrapper.tsx b/src/components/StoryLinkWrapper.tsx new file mode 100644 index 00000000..d0f2114e --- /dev/null +++ b/src/components/StoryLinkWrapper.tsx @@ -0,0 +1,30 @@ +/* eslint-disable import/no-extraneous-dependencies */ +// This is allows us to test whether the link works via the actions addon +import React, { ComponentProps, FC } from 'react'; +import { action } from '@storybook/addon-actions'; + +const fireClickAction = action('onLinkClick'); + +export const StoryLinkWrapper: FC> = ({ + children, + href, + onClick, + to, + ...rest +}) => { + const modifiedOnClick: React.DOMAttributes['onClick'] = (event) => { + event.preventDefault(); + onClick(event); + fireClickAction(href || to); + }; + + return ( + + {children} + + ); +}; + +interface Props { + to: string; +} diff --git a/src/components/Subheading.stories.js b/src/components/Subheading.stories.tsx similarity index 100% rename from src/components/Subheading.stories.js rename to src/components/Subheading.stories.tsx diff --git a/src/components/Subheading.js b/src/components/Subheading.tsx similarity index 63% rename from src/components/Subheading.js rename to src/components/Subheading.tsx index 2dc6c124..8365b22d 100644 --- a/src/components/Subheading.js +++ b/src/components/Subheading.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { ComponentProps, FC } from 'react'; import styled from 'styled-components'; import { typography } from './shared/styles'; @@ -9,4 +9,4 @@ const Heading = styled.span` font-size: ${typography.size.s2 - 1}px; `; -export const Subheading = (props) => ; +export const Subheading: FC> = (props) => ; diff --git a/src/components/Textarea.stories.js b/src/components/Textarea.stories.tsx similarity index 99% rename from src/components/Textarea.stories.js rename to src/components/Textarea.stories.tsx index e236cd4e..67135f26 100644 --- a/src/components/Textarea.stories.js +++ b/src/components/Textarea.stories.tsx @@ -23,6 +23,7 @@ export const Default = () => (