diff --git a/.gitignore b/.gitignore index 25dd68a..659c828 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ lib coverage .yalc yalc.lock +.env diff --git a/jest.config.js b/jest.config.js index cc6302f..5496bc3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,5 +1,5 @@ module.exports = { - setupFilesAfterEnv: ['/test/setup.js'], + setupFilesAfterEnv: ['/src/test/setup.ts'], moduleFileExtensions: ['ts', 'tsx', 'js'], testMatch: ['/src/**/*.spec.(ts|js|tsx|jsx)'], moduleNameMapper: { diff --git a/package.json b/package.json index 35401d7..8dec0e6 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "molekule", "description": "React component library built on top of styled-components and styled-system", - "version": "5.3.9", + "version": "6.0.0", "main": "lib/index.js", - "types": "lib/main.d.ts", + "types": "lib/index.d.ts", "files": [ "lib" ], @@ -17,7 +17,7 @@ "scripts": { "clean": "rimraf lib", "watch": "yarn rollup --watch", - "build": "npm run clean && babel src --out-dir lib --extensions '.ts,.tsx,.js,.jsx'", + "build": "npm run clean && tsc --importHelpers --noEmit false --project tsconfig.build.json", "docs": "start-storybook -p 1234", "dev": "start-storybook -p 1234", "docs:build": "rimraf dist && build-storybook -o dist", @@ -26,6 +26,7 @@ "preview": "nodemon -w src -x 'yalc publish --push --changed'", "rollup": "rollup -c", "test": "jest", + "types": "tsc -w", "lint": "eslint src", "release": "npm run build && standard-version", "chromatic": "chromatic --exit-zero-on-changes --auto-accept-changes main" @@ -33,6 +34,7 @@ "dependencies": { "@styled-system/prop-types": "^5.1.2", "@testing-library/react": "^9.1.3", + "@types/react-transition-group": "^4.4.0", "chromatic": "^4.0.3", "cleave.js": "~1.6.0", "libphonenumber-js": "^1.7.52", @@ -48,6 +50,7 @@ "react-transition-group": "^2.5.3", "styled-components": "^4.0.0", "styled-system": "^5.1.2", + "tslib": "^2.0.1", "typescript": "^3.9.4" }, "peerDependencies": { @@ -74,7 +77,11 @@ "@storybook/react": "5.3.18", "@types/jest": "^25.2.2", "@types/lodash": "^4.14.150", + "@types/react-portal": "^4.0.2", + "@types/react-transition-group": "^4.4.0", "@types/styled-components": "^5.1.0", + "@types/styled-system": "^5.1.10", + "@types/yup": "^0.29.4", "babel-jest": "^24.1.0", "babel-loader": "^8.0.6", "babel-preset-react-app": "^9.1.2", diff --git a/src/@types/react-animations/index.d.ts b/src/@types/react-animations/index.d.ts new file mode 100644 index 0000000..fc02dc6 --- /dev/null +++ b/src/@types/react-animations/index.d.ts @@ -0,0 +1 @@ +declare module 'react-animations' {} diff --git a/src/@types/styled.d.ts b/src/@types/styled.d.ts index 05a203f..81f0271 100644 --- a/src/@types/styled.d.ts +++ b/src/@types/styled.d.ts @@ -1,25 +1,8 @@ // import original module declarations import 'styled-components'; -import { ThemeColors, ThemeBreakpoints, ThemeTypography, ThemeSizes, ThemeVariants } from 'src/types'; +import { Theme } from 'types'; // and extend them! declare module 'styled-components' { - export interface DefaultTheme extends Theme { - classPrefix: string; - space: number[]; - gridWidth: number; - gridGutter: number; - gridColumns: number; - radii: number[]; - radius: number; - shadow: { - soft: string; - hard: string; - }; - colors: ThemeColors; - breakpoints: ThemeBreakpoints; - typography: ThemeTypography; - sizes: ThemeSizes; - variants: ThemeVariants; - } + export interface DefaultTheme extends Theme {} } diff --git a/src/Accordion/Accordion.js b/src/Accordion/Accordion.js deleted file mode 100644 index 2e769f8..0000000 --- a/src/Accordion/Accordion.js +++ /dev/null @@ -1,138 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { css } from 'styled-components'; -import Collapse from '../Collapse'; -import { createComponent } from '../utils'; -import Box from '../Box'; -import Icon from '../Icon'; -import Flex from '../Flex'; - -const AccordionItemProps = { - title: PropTypes.oneOfType([PropTypes.element, PropTypes.string]), - content: PropTypes.oneOfType([PropTypes.element, PropTypes.string]), - renderHeader: PropTypes.func, - renderContent: PropTypes.func, -}; - -const AccordionContainer = createComponent({ - name: 'Accordion', -}); - -const AccordionItemContainer = createComponent({ - name: 'AccordionItem', -}); - -const AccordionHeader = createComponent({ - name: 'AccordionItemHeader', - tag: 'header', - style: css` - padding: 0.75rem 1rem; - cursor: pointer; - `, -}); - -const AccordionTitle = createComponent({ - name: 'AccordionItemTitle', - tag: 'span', - style: css``, -}); - -const AccordionIcon = createComponent({ - name: 'AccordionItemIcon', - as: Icon, - style: ({ isOpen }) => css` - transition: 175ms transform; - transform: rotate(${isOpen ? 90 : 0}deg); - `, -}); - -const AccordionContent = createComponent({ - name: 'AccordionItemContent', -}); - -const AccordionItem = ({ isOpen, title, content, contentContainerStyle, renderHeader, renderContent, onToggle }) => ( - - - renderHeader ? ( - renderHeader({ isOpen, title, onToggle }) - ) : ( - - - - {title} - - - - - ) - }> - {renderContent ? ( - renderContent({ isOpen, content }) - ) : ( - {content} - )} - - -); - -AccordionItem.propTypes = AccordionItemProps; - -export default class Accordion extends Component { - static propTypes = { - /** - * An array of AccordionItems - */ - items: PropTypes.arrayOf(PropTypes.shape(AccordionItemProps)).isRequired, - - /** - * Only one accordion cell open at a time - */ - solo: PropTypes.bool, - - /** - * Style passed to content container box - */ - contentContainerStyle: PropTypes.shape(), - }; - - static Item = AccordionItem; - - state = { - openList: [], - }; - - handleItemToggle = idx => { - const { solo } = this.props; - this.setState(({ openList }) => { - if (openList.indexOf(idx) >= 0) { - return { openList: openList.filter(i => i !== idx) }; - } - - return { - openList: solo ? [idx] : [...openList, idx], - }; - }); - }; - - render() { - const { items, children, contentContainerStyle } = this.props; - const { openList } = this.state; - - return ( - - {children || - items.map((item, i) => ( - = 0} - {...item} - onToggle={() => this.handleItemToggle(i)} - /> - ))} - - ); - } -} diff --git a/src/Accordion/Accordion.stories.js b/src/Accordion/Accordion.stories.tsx similarity index 100% rename from src/Accordion/Accordion.stories.js rename to src/Accordion/Accordion.stories.tsx diff --git a/src/Accordion/Accordion.tsx b/src/Accordion/Accordion.tsx new file mode 100644 index 0000000..063b54e --- /dev/null +++ b/src/Accordion/Accordion.tsx @@ -0,0 +1,133 @@ +import React, { useState, useCallback } from 'react'; +import { css } from 'styled-components'; +import Collapse from '../Collapse'; +import { createComponent } from '../utils'; +import { Box } from '../Box'; +import { Icon } from '../Icon'; +import { Flex } from '../Flex'; + +const AccordionContainer = createComponent({ + name: 'Accordion', +}); + +const AccordionItemContainer = createComponent({ + name: 'AccordionItem', +}); + +const AccordionHeader = createComponent({ + name: 'AccordionItemHeader', + tag: 'header', + style: css` + padding: 0.75rem 1rem; + cursor: pointer; + `, +}); + +const AccordionTitle = createComponent({ + name: 'AccordionItemTitle', + tag: 'span', + style: css``, +}); + +const AccordionIcon = createComponent<{ isOpen?: boolean }, typeof Icon>({ + name: 'AccordionItemIcon', + as: Icon, + style: ({ isOpen }) => css` + transition: 175ms transform; + transform: rotate(${isOpen ? 90 : 0}deg); + `, +}); + +const AccordionContent = createComponent({ + name: 'AccordionItemContent', +}); + +interface AccordionItemProps extends Partial> { + title: string; + isOpen?: boolean; + content?: React.ReactNode | string; + onToggle?: () => void; + renderHeader?: (p: Pick) => React.ReactNode; + renderContent?: (p: Pick) => React.ReactNode; +} + +const AccordionItem: React.FC = ({ + isOpen, + title, + content, + contentContainerStyle, + renderHeader, + renderContent, + onToggle, +}) => ( + + + renderHeader ? ( + renderHeader({ isOpen, title, onToggle }) + ) : ( + + + + {title} + + + + + ) + }> + {renderContent ? ( + renderContent({ isOpen, content }) + ) : ( + {content} + )} + + +); + +export interface AccordionProps { + items: AccordionItemProps[]; + solo?: boolean; + contentContainerStyle?: React.CSSProperties; +} + +export interface AccordionStaticMembers { + Item: typeof AccordionItem; +} + +const Accordion: React.FC & AccordionStaticMembers = ({ + items, + solo, + contentContainerStyle, + children, +}) => { + const [openList, setOpenList] = useState([]); + + const handleItemToggle = useCallback( + (idx: number) => { + if (openList.indexOf(idx) >= 0) setOpenList(openList.filter(i => i !== idx)); + else setOpenList(solo ? [idx] : [...openList, idx]); + }, + [solo, openList] + ); + + return ( + + {children || + items.map((item, i) => ( + = 0} + onToggle={() => handleItemToggle(i)} + /> + ))} + + ); +}; + +Accordion.Item = AccordionItem; + +export default Accordion; diff --git a/src/Accordion/index.js b/src/Accordion/index.ts similarity index 100% rename from src/Accordion/index.js rename to src/Accordion/index.ts diff --git a/src/Alert/Alert.stories.js b/src/Alert/Alert.stories.tsx similarity index 81% rename from src/Alert/Alert.stories.js rename to src/Alert/Alert.stories.tsx index 2fb1927..520f81d 100644 --- a/src/Alert/Alert.stories.js +++ b/src/Alert/Alert.stories.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import Alert from './Alert'; +// eslint-disable-next-line import/no-extraneous-dependencies import { text } from '@storybook/addon-knobs'; +import { Alert } from './Alert'; export default { title: 'Components|Alert', @@ -17,7 +18,7 @@ export const Basic = () => { This is a generic informational message. - Oops! Something wen't wrong. + Oops! Something went wrong. Caution! There be dragons. diff --git a/src/Alert/Alert.js b/src/Alert/Alert.tsx similarity index 55% rename from src/Alert/Alert.js rename to src/Alert/Alert.tsx index c5202c3..bf631bd 100644 --- a/src/Alert/Alert.js +++ b/src/Alert/Alert.tsx @@ -1,21 +1,25 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { css } from 'styled-components'; -import { space } from 'styled-system'; -import propTypes from '@styled-system/prop-types'; +import { space, SpaceProps } from 'styled-system'; import { getComponentVariant, createComponent } from '../utils'; -const StyledAlert = createComponent({ +export interface AlertProps extends SpaceProps { + variant?: string; + children?: string | React.ReactNode; +} + +/** Alerts are typically used to display meaningful copy to users - typically notifying the user of an important message. */ +export const Alert = createComponent({ name: 'Alert', style: ({ variant, theme }) => { - const variantStyles = getComponentVariant(theme, 'Alert', variant); + const variantStyles = variant ? getComponentVariant(theme, 'Alert', variant) : ''; return css` padding: 1rem; margin-bottom: 1rem; border: 0; font-size: 14px; - font-family: ${theme.typography.fontFamily || 'inherit'}; + font-family: ${theme.typography.bodyFontFamily || 'inherit'}; border-radius: ${theme.radius}px; a { @@ -24,21 +28,11 @@ const StyledAlert = createComponent({ } ${variantStyles} - ${space}; + ${space} `; }, }); -/** Alerts are typically used to display meaningful copy to users - typically notifying the user of an important message. */ -const Alert = React.forwardRef((props, ref) => ); - -Alert.propTypes = { - variant: PropTypes.string, - ...propTypes.space, -}; - Alert.defaultProps = { variant: 'primary', }; - -export default Alert; diff --git a/src/Alert/index.js b/src/Alert/index.js deleted file mode 100644 index becaea7..0000000 --- a/src/Alert/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Alert'; diff --git a/src/Alert/index.ts b/src/Alert/index.ts new file mode 100644 index 0000000..79e3b15 --- /dev/null +++ b/src/Alert/index.ts @@ -0,0 +1 @@ +export * from './Alert'; diff --git a/src/Avatar/Avatar.stories.js b/src/Avatar/Avatar.stories.tsx similarity index 100% rename from src/Avatar/Avatar.stories.js rename to src/Avatar/Avatar.stories.tsx diff --git a/src/Avatar/Avatar.js b/src/Avatar/Avatar.tsx similarity index 67% rename from src/Avatar/Avatar.js rename to src/Avatar/Avatar.tsx index 701f01d..4772e41 100644 --- a/src/Avatar/Avatar.js +++ b/src/Avatar/Avatar.tsx @@ -1,7 +1,6 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { css } from 'styled-components'; -import Box from '../Box'; +import { Box, BoxProps } from '../Box'; import { createComponent } from '../utils'; // from https://flatuicolors.com/ @@ -22,18 +21,50 @@ const getInitials = (name = '') => .map(w => w[0]) .join(''); -const AvatarContainer = createComponent({ +export interface AvatarProps { + /** + * We'll take the first letter of the first two words to create the initials + */ + name?: string; + + /** + * The size of the Avatar + */ + size?: number; + + /** + * The image source of the Avatar + */ + src?: string; + + /** + * Border radius of the Avatar + */ + borderRadius?: string | number; + + /** + * Colors of the initials + */ + color?: string; + + /** + * Background color when initials are used + */ + backgroundColor?: string; +} + +const AvatarContainer = createComponent({ name: 'Avatar', as: Box, style: ({ size, borderRadius, color, backgroundColor, src, theme }) => css` height: ${size}px; width: ${size}px; border-radius: ${borderRadius}; - background: ${theme.colors[backgroundColor] || backgroundColor}; + background: ${theme.colors[backgroundColor || ''] || backgroundColor}; color: ${color}; text-align: center; line-height: ${size}px; - font-size: ${size * 0.5}px; + font-size: ${(size || 0) * 0.5}px; ${src && { backgroundImage: `url(${src})`, @@ -43,43 +74,27 @@ const AvatarContainer = createComponent({ `, }); -const Avatar = ({ name = '', src, backgroundColor, ...props }) => ( +const Avatar: React.FC = ({ + name = '', + src, + backgroundColor, + size = 25, + borderRadius = '100%', + color = 'white', + ...props +}) => ( + aria-label={name} + {...props}> {src ? null : getInitials(name)} ); -Avatar.propTypes = { - /** - * We'll take the first letter of the first two words to create the initials - */ - name: PropTypes.string, - - /** - * The size of the Avatar - */ - size: PropTypes.number, - - /** - * Border radius of the Avatar - */ - borderRadius: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - - /** - * Colors of the initials - */ - color: PropTypes.string, - - /** - * Background color when initials are used - */ - backgroundColor: PropTypes.string, -}; - Avatar.defaultProps = { size: 25, borderRadius: '100%', diff --git a/src/Avatar/index.js b/src/Avatar/index.ts similarity index 100% rename from src/Avatar/index.js rename to src/Avatar/index.ts diff --git a/src/Badge/Badge.js b/src/Badge/Badge.js deleted file mode 100644 index c916ad8..0000000 --- a/src/Badge/Badge.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { css } from 'styled-components'; -import { space } from 'styled-system'; -import propTypes from '@styled-system/prop-types'; -import { getComponentVariant, createComponent, getComponentSize } from '../utils'; - -const StyledBadge = createComponent({ - name: 'Badge', - tag: 'span', - style: ({ variant, theme, size }) => { - const variantStyles = getComponentVariant(theme, 'Badge', variant); - const sizeStyles = getComponentSize(theme, 'Badge', size); - - return css` - font-family: inherit; - font-weight: bold; - - ${variantStyles}; - ${sizeStyles}; - ${space}; - `; - }, -}); - -const Badge = React.forwardRef((props, ref) => ); - -Badge.propTypes = { - variant: PropTypes.string, - size: PropTypes.string, - ...propTypes.space, -}; - -Badge.defaultProps = { - variant: 'info', - size: 'md', -}; - -export default Badge; diff --git a/src/Badge/Badge.spec.js b/src/Badge/Badge.spec.tsx similarity index 67% rename from src/Badge/Badge.spec.js rename to src/Badge/Badge.spec.tsx index a097a31..6353050 100644 --- a/src/Badge/Badge.spec.js +++ b/src/Badge/Badge.spec.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { renderWithTheme } from '../../test/utils'; -import Badge from './Badge'; +import { renderWithTheme } from '../test/utils'; +import { Badge } from './Badge'; test('Badge', () => { const { asFragment } = renderWithTheme(I'm a badge!); diff --git a/src/Badge/Badge.stories.js b/src/Badge/Badge.stories.tsx similarity index 94% rename from src/Badge/Badge.stories.js rename to src/Badge/Badge.stories.tsx index 966f4a5..1ea1cc8 100644 --- a/src/Badge/Badge.stories.js +++ b/src/Badge/Badge.stories.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import Badge from './Badge'; +import { Badge } from './Badge'; export default { title: 'Components|Badge', diff --git a/src/Badge/Badge.tsx b/src/Badge/Badge.tsx new file mode 100644 index 0000000..1b730b4 --- /dev/null +++ b/src/Badge/Badge.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { css } from 'styled-components'; +import { space, SpaceProps } from 'styled-system'; +import { getComponentVariant, createComponent, getComponentSize } from '../utils'; + +export interface BadgeProps extends SpaceProps { + variant?: string; + size?: string; + children?: React.ReactNode; +} + +export const Badge = createComponent({ + name: 'Badge', + tag: 'span', + style: ({ variant, theme, size }) => { + const variantStyles = variant ? getComponentVariant(theme, 'Badge', variant) : ''; + const sizeStyles = getComponentSize(theme, 'Badge', size); + + return css` + font-family: inherit; + font-weight: bold; + + ${variantStyles} + ${sizeStyles} + ${space} + `; + }, +}); + +Badge.defaultProps = { + variant: 'info', + size: 'md', +}; diff --git a/src/Badge/__snapshots__/Badge.spec.js.snap b/src/Badge/__snapshots__/Badge.spec.tsx.snap similarity index 100% rename from src/Badge/__snapshots__/Badge.spec.js.snap rename to src/Badge/__snapshots__/Badge.spec.tsx.snap diff --git a/src/Badge/index.js b/src/Badge/index.js deleted file mode 100644 index 0979058..0000000 --- a/src/Badge/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import Badge from './Badge'; - -export default Badge; diff --git a/src/Badge/index.ts b/src/Badge/index.ts new file mode 100644 index 0000000..9c8edca --- /dev/null +++ b/src/Badge/index.ts @@ -0,0 +1 @@ +export * from './Badge'; diff --git a/src/Box/Box.js b/src/Box/Box.js deleted file mode 100644 index c679edc..0000000 --- a/src/Box/Box.js +++ /dev/null @@ -1,28 +0,0 @@ -import { compose, space, color, typography, layout, flexbox, position, background } from 'styled-system'; -import propTypes from '@styled-system/prop-types'; -import { createComponent } from '../utils'; - -const Box = createComponent({ - name: 'Box', - style: compose( - space, - color, - typography, - layout, - flexbox, - position, - background - ), -}); - -Box.propTypes = { - ...propTypes.space, - ...propTypes.color, - ...propTypes.typography, - ...propTypes.layout, - ...propTypes.flexbox, - ...propTypes.position, - ...propTypes.background, -}; - -export default Box; diff --git a/src/Box/Box.stories.tsx b/src/Box/Box.stories.tsx new file mode 100644 index 0000000..60cd233 --- /dev/null +++ b/src/Box/Box.stories.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { Box } from '.'; + +export default { + title: 'Components|Box', + component: Box, +}; + +export const Basic = () => ( + <> + + I take up the remaining space + + + I am a Box + + {}}> + I am Box + + {}}> + I am Box as div + + +); diff --git a/src/Box/Box.ts b/src/Box/Box.ts new file mode 100644 index 0000000..e1ca117 --- /dev/null +++ b/src/Box/Box.ts @@ -0,0 +1,41 @@ +import { + compose, + space, + color, + typography, + layout, + flexbox, + position, + background, + SpaceProps, + ColorProps, + TypographyProps, + LayoutProps, + FlexboxProps, + PositionProps, + BackgroundProps, +} from 'styled-system'; +import { createComponent } from '../utils'; + +export interface BoxProps + extends SpaceProps, + ColorProps, + TypographyProps, + LayoutProps, + FlexboxProps, + PositionProps, + BackgroundProps { + color?: + | string + | (string & (string | number | symbol | null)[]) + | (string & { + [x: string]: string | number | symbol | undefined; + [x: number]: string | number | symbol | undefined; + }) + | undefined; // typing issue +} + +export const Box = createComponent({ + name: 'Box', + style: compose(space, color, typography, layout, flexbox, position, background), +}); diff --git a/src/Box/index.js b/src/Box/index.js deleted file mode 100644 index a3e93ac..0000000 --- a/src/Box/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Box'; diff --git a/src/Box/index.ts b/src/Box/index.ts new file mode 100644 index 0000000..305f81d --- /dev/null +++ b/src/Box/index.ts @@ -0,0 +1 @@ +export * from './Box'; diff --git a/src/Button/Button.stories.js b/src/Button/Button.stories.tsx similarity index 86% rename from src/Button/Button.stories.js rename to src/Button/Button.stories.tsx index 93f9561..f8c62d6 100644 --- a/src/Button/Button.stories.js +++ b/src/Button/Button.stories.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import Button from './Button'; +import { Button } from './Button'; export default { title: 'Components|Button', @@ -12,6 +12,9 @@ export const All = () => ( + ); @@ -63,3 +66,9 @@ export const Loading = () => ( ); export const FullWidth = () => ; + +export const AsAnchor = () => ( + + + +); diff --git a/src/Button/Button.js b/src/Button/Button.tsx similarity index 75% rename from src/Button/Button.js rename to src/Button/Button.tsx index 74fd9ad..93c2591 100644 --- a/src/Button/Button.js +++ b/src/Button/Button.tsx @@ -1,10 +1,9 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { FC } from 'react'; import { css, keyframes } from 'styled-components'; -import { space } from 'styled-system'; +import { space, SpaceProps } from 'styled-system'; import { getComponentVariant, getComponentSize, createComponent } from '../utils'; -import Flex from '../Flex'; -import Icon from '../Icon'; +import { Flex } from '../Flex'; +import { Icon, IconProps } from '../Icon'; const spinKeyframes = keyframes` from { @@ -15,7 +14,7 @@ const spinKeyframes = keyframes` transform: rotate(360deg); }`; -const loadingCss = (height, color) => css` +const loadingCss = (height: number, color: string) => css` color: transparent !important; pointer-events: none; position: relative; @@ -40,7 +39,28 @@ const loadingCss = (height, color) => css` } `; -const StyledButton = createComponent({ +export interface ButtonProps extends SpaceProps { + variant?: string; + size?: string; + outline?: boolean; + block?: boolean; + colorFocus?: string; + disabled?: boolean; + loading?: boolean; + borderRadius?: number; + leftIcon?: string; + leftIconProps?: Omit; + rightIcon?: string; + rightIconProps?: Omit; + children?: React.ReactNode; +} + +interface StyledButtonProps extends Partial { + hasText?: boolean; + isLoading?: boolean; +} + +const StyledButton = createComponent({ name: 'Button', tag: 'button', style: ({ @@ -56,7 +76,7 @@ const StyledButton = createComponent({ borderRadius = theme.radius, colorFocus = theme.colors.colorFocus, }) => { - const variantStyles = getComponentVariant(theme, 'Button', variant); + const variantStyles = variant ? getComponentVariant(theme, 'Button', variant) : ''; const sizeStyles = getComponentSize(theme, 'Button', size); return css` @@ -129,10 +149,15 @@ const StyledButton = createComponent({ }, }); -const renderIcon = (icon, props) => ; +const renderIcon = (icon: IconProps['name'], props?: Omit) => ; + +export interface ButtonStaticMembers { + Group: any; +} /** Custom button styles for actions in forms, dialogs, and more with support for multiple sizes, states, and more. We include several predefined button styles, each serving its own semantic purpose, with a few extras thrown in for more control. */ -const Button = React.forwardRef( +// have to use & ...Props and FC<...> because typing is missing, dont know why +const ButtonForward: FC & ButtonProps> = React.forwardRef( ({ children, leftIcon, leftIconProps, rightIcon, rightIconProps, colorFocus, loading, ...rest }, ref) => ( { +const verticalCss = ({ breakpoints, vertical, borderRadius }: any) => { const maybeNumber = parseInt(vertical, 10); const fallback = breakpoints[vertical] || breakpoints.sm; const breakpoint = Number.isInteger(maybeNumber) ? `${maybeNumber}px` : fallback; @@ -194,7 +208,13 @@ const verticalCss = ({ breakpoints, vertical, borderRadius }) => { `; }; -Button.Group = createComponent({ +interface ButtonGroupProps { + vertical?: boolean; + borderRadius?: number; + connected?: boolean; +} + +Button.Group = createComponent({ name: 'ButtonGroup', as: Flex, style: ({ vertical = false, theme: { radius, breakpoints }, borderRadius = radius || 2, connected = false }) => css` @@ -222,5 +242,3 @@ Button.Group = createComponent({ ${vertical && verticalCss({ breakpoints, vertical, borderRadius })}; `, }); - -export default Button; diff --git a/src/Button/index.js b/src/Button/index.js deleted file mode 100644 index 803f51f..0000000 --- a/src/Button/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import Button from './Button'; - -export default Button; diff --git a/src/Button/index.ts b/src/Button/index.ts new file mode 100644 index 0000000..8b166a8 --- /dev/null +++ b/src/Button/index.ts @@ -0,0 +1 @@ +export * from './Button'; diff --git a/src/Card/Card.spec.js b/src/Card/Card.spec.tsx similarity index 85% rename from src/Card/Card.spec.js rename to src/Card/Card.spec.tsx index 0b2c506..3166faa 100644 --- a/src/Card/Card.spec.js +++ b/src/Card/Card.spec.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { renderWithTheme } from '../../test/utils'; +import { renderWithTheme } from '../test/utils'; import Card from './Card'; test('Card', () => { diff --git a/src/Card/Card.stories.js b/src/Card/Card.stories.tsx similarity index 100% rename from src/Card/Card.stories.js rename to src/Card/Card.stories.tsx diff --git a/src/Card/Card.js b/src/Card/Card.tsx similarity index 59% rename from src/Card/Card.js rename to src/Card/Card.tsx index 1901177..9931a56 100644 --- a/src/Card/Card.js +++ b/src/Card/Card.tsx @@ -1,10 +1,14 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { css } from 'styled-components'; -import Box from '../Box'; +import { Box, BoxProps } from '../Box'; import { themeGet, createComponent } from '../utils'; -const StyledCard = createComponent({ +export interface CardProps { + shadow?: boolean; + children?: React.ReactNode; +} + +const StyledCard = createComponent({ name: 'Card', as: Box, style: ({ shadow, theme }) => css` @@ -16,18 +20,20 @@ const StyledCard = createComponent({ `, }); -/** Cards provide a flexible way to encapsulate content with multiple variants and options. */ -const Card = React.forwardRef((props, ref) => ); +export interface CardStaticMembers { + Footer: any; + Body: any; + Header: any; +} -Card.propTypes = { - shadow: PropTypes.bool, -}; +/** Cards provide a flexible way to encapsulate content with multiple variants and options. */ +const Card = StyledCard as typeof StyledCard & CardStaticMembers; Card.defaultProps = { shadow: false, }; -Card.Footer = createComponent({ +Card.Footer = createComponent({ name: 'CardFooter', as: Box, style: css` @@ -35,26 +41,30 @@ Card.Footer = createComponent({ `, }); -Card.Body = createComponent({ +const cardFooterClass = Card.Footer.toString(); + +Card.Body = createComponent({ name: 'CardBody', as: Box, style: () => css` padding: 16px; - & + ${Card.Footer} { + & + ${cardFooterClass} { padding-top: 0px; } `, }); -Card.Header = createComponent({ +const cardBodyClass = Card.Body.toString(); + +Card.Header = createComponent({ name: 'CardHeader', as: Box, style: css` padding: 16px; font-weight: 700; - & + ${Card.Body} { + & + ${cardBodyClass} { padding-top: 0px; } `, diff --git a/src/Card/__snapshots__/Card.spec.js.snap b/src/Card/__snapshots__/Card.spec.tsx.snap similarity index 100% rename from src/Card/__snapshots__/Card.spec.js.snap rename to src/Card/__snapshots__/Card.spec.tsx.snap diff --git a/src/Card/index.js b/src/Card/index.js deleted file mode 100644 index b7d457f..0000000 --- a/src/Card/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import Card from './Card'; - -export default Card; diff --git a/src/Card/index.ts b/src/Card/index.ts new file mode 100644 index 0000000..c68311d --- /dev/null +++ b/src/Card/index.ts @@ -0,0 +1 @@ +export { default } from './Card'; diff --git a/src/Collapse/Collapse.stories.js b/src/Collapse/Collapse.stories.tsx similarity index 78% rename from src/Collapse/Collapse.stories.js rename to src/Collapse/Collapse.stories.tsx index 784c5fc..44fcbc5 100644 --- a/src/Collapse/Collapse.stories.js +++ b/src/Collapse/Collapse.stories.tsx @@ -1,6 +1,7 @@ +/* eslint-disable react/no-this-in-sfc */ import React from 'react'; -import Button from '../Button'; -import Box from '../Box'; +import { Button } from '../Button'; +import { Box } from '../Box'; import Collapse from './Collapse'; export default { @@ -11,8 +12,8 @@ export default { }; export const Basic = () => { - class Example extends React.Component { - constructor(props) { + class Example extends React.Component { + constructor(props: any) { super(props); this.state = { @@ -33,7 +34,7 @@ export const Basic = () => { - I'm in a collapsible element! + I'm in a collapsible element! @@ -48,7 +49,7 @@ export const CustomTrigger = () => ( <> }> - I'm in a collapsible element! + I'm in a collapsible element! diff --git a/src/Collapse/Collapse.js b/src/Collapse/Collapse.tsx similarity index 62% rename from src/Collapse/Collapse.js rename to src/Collapse/Collapse.tsx index c2636a8..78bccdd 100644 --- a/src/Collapse/Collapse.js +++ b/src/Collapse/Collapse.tsx @@ -1,10 +1,10 @@ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { css } from 'styled-components'; import { Transition } from 'react-transition-group'; +import { TransitionStatus } from 'react-transition-group/Transition'; import { createComponent } from '../utils'; -const getTransitionStyle = (state, duration) => { +const getTransitionStyle = (state: TransitionStatus, duration: number) => { switch (state) { case 'exited': return css` @@ -23,7 +23,7 @@ const getTransitionStyle = (state, duration) => { } }; -const Container = createComponent({ +const Container = createComponent<{ state: TransitionStatus; height: number; duration: number }>({ name: 'Collapse', style: ({ duration, height, state }) => css` position: relative; @@ -37,19 +37,26 @@ const Trigger = createComponent({ name: 'CollapseTrigger', }); -/** Collapse is used to show and hide content. Use a button, anchor, or other clickable elements as triggers. */ -export default class Collapse extends Component { - static propTypes = { - isOpen: PropTypes.bool, - duration: PropTypes.number, - onEnter: PropTypes.func, - onEntering: PropTypes.func, - onEntered: PropTypes.func, - onExit: PropTypes.func, - onExiting: PropTypes.func, - onExited: PropTypes.func, - }; +interface CollapseProps { + isOpen?: boolean; + duration?: number; + onEnter?: (node: HTMLElement, isAppearing: boolean) => void; + onEntering?: (node: HTMLElement, isAppearing: boolean) => void; + onEntered?: (node: HTMLElement, isAppearing: boolean) => void; + onExit?: (node: HTMLElement) => void; + onExiting?: (node: HTMLElement) => void; + onExited?: (node: HTMLElement) => void; + trigger?: React.ReactNode; + renderTrigger?: (p: { toggle: () => void }) => React.ReactNode; +} +interface CollapseState { + isOpen: boolean; + height: number; +} + +/** Collapse is used to show and hide content. Use a button, anchor, or other clickable elements as triggers. */ +export default class Collapse extends Component { static defaultProps = { duration: 175, onEnter: () => {}, @@ -60,7 +67,7 @@ export default class Collapse extends Component { onExited: () => {}, }; - static getDerivedStateFromProps(props, state) { + static getDerivedStateFromProps(props: CollapseProps, state: CollapseState) { if (props.isOpen !== undefined && props.isOpen !== state.isOpen) { return { ...state, @@ -76,34 +83,33 @@ export default class Collapse extends Component { height: 0, }; - onEnter = (node, isAppearing) => { - this.props.onEnter(node, isAppearing); + onEnter = (node: HTMLElement, isAppearing: boolean) => { + this.props.onEnter!(node, isAppearing); }; - onEntering = (node, isAppearing) => { + onEntering = (node: HTMLElement, isAppearing: boolean) => { this.setState({ height: node.scrollHeight }); - this.props.onEntering(node, isAppearing); + this.props.onEntering!(node, isAppearing); }; - onEntered = (node, isAppearing) => { + onEntered = (node: HTMLElement, isAppearing: boolean) => { this.setState({ height: node.scrollHeight }); - this.props.onEntered(node, isAppearing); + this.props.onEntered!(node, isAppearing); }; - onExit = node => { + onExit = (node: HTMLElement) => { this.setState({ height: node.scrollHeight }); - this.props.onExit(node); + this.props.onExit!(node); }; - onExiting = node => { + onExiting = (node: HTMLElement) => { // Taken from: https://github.com/reactstrap/reactstrap/blob/master/src/Collapse.js#L80 - const _unused = node.offsetHeight; // eslint-disable-line this.setState({ height: 0 }); - this.props.onExiting(node); + this.props.onExiting!(node); }; - onExited = node => { - this.props.onExited(node); + onExited = (node: HTMLElement) => { + this.props.onExited!(node); }; toggle = () => { @@ -117,7 +123,8 @@ export default class Collapse extends Component { if (renderTrigger) { return renderTrigger({ toggle: this.toggle }); - } else if (trigger) { + } + if (trigger) { return {trigger}; } @@ -135,7 +142,7 @@ export default class Collapse extends Component { {state => ( - + {children} )} diff --git a/src/Collapse/index.js b/src/Collapse/index.ts similarity index 100% rename from src/Collapse/index.js rename to src/Collapse/index.ts diff --git a/src/Dropdown/Dropdown.spec.js b/src/Dropdown/Dropdown.spec.tsx similarity index 91% rename from src/Dropdown/Dropdown.spec.js rename to src/Dropdown/Dropdown.spec.tsx index 37e2d87..2ea3869 100644 --- a/src/Dropdown/Dropdown.spec.js +++ b/src/Dropdown/Dropdown.spec.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { renderWithTheme, fireEvent, wait } from '../../test/utils'; +import { renderWithTheme, fireEvent, wait } from '../test/utils'; import Dropdown from './Dropdown'; jest.mock('popper.js', () => { @@ -18,11 +18,11 @@ jest.mock('popper.js', () => { }); describe('', () => { - let renderUtils; + let renderUtils: any; const renderDropdown = (props = {}) => { const wrapper = document.createElement('div'); - wrapper.setAttribute('tabindex', 1); + wrapper.setAttribute('tabindex', '1'); const utils = renderWithTheme( Trigger}> Title @@ -42,11 +42,13 @@ describe('', () => { const assertDropdownOpen = (utils = renderUtils) => wait(() => { + // @ts-ignore expect(utils.queryByText('Title')).toBeInTheDocument(); }); const assertDropdownClosed = (utils = renderUtils) => wait(() => { + // @ts-ignore expect(utils.queryByText('Title')).not.toBeInTheDocument(); }); @@ -90,7 +92,7 @@ describe('', () => { await openDropdown(); const itemOne = renderUtils.getByTestId('item-one'); const itemTwo = renderUtils.getByTestId('item-two'); - const isFocused = node => node === document.activeElement; + const isFocused = (node: any) => node === document.activeElement; // First focusable element in tree should be selected on first arrow down fireEvent.keyDown(document.body, { key: 'ArrowDown' }); @@ -105,21 +107,24 @@ describe('', () => { }); describe('prop: width', () => { - test('wdith defaults to auto', async () => { + test('width defaults to auto', async () => { const utils = renderDropdown(); await openDropdown(utils); + // @ts-ignore expect(utils.getByTestId('dropdown-menu')).toHaveStyleRule('width', 'auto'); }); test('supports string widths', async () => { const utils = renderDropdown({ width: '2rem' }); await openDropdown(utils); + // @ts-ignore expect(utils.getByTestId('dropdown-menu')).toHaveStyleRule('width', '2rem'); }); test('supports number widths', async () => { const utils = renderDropdown({ width: 200 }); await openDropdown(utils); + // @ts-ignore expect(utils.getByTestId('dropdown-menu')).toHaveStyleRule('width', '200px'); }); }); diff --git a/src/Dropdown/Dropdown.stories.js b/src/Dropdown/Dropdown.stories.tsx similarity index 91% rename from src/Dropdown/Dropdown.stories.js rename to src/Dropdown/Dropdown.stories.tsx index b8c5462..60a8861 100644 --- a/src/Dropdown/Dropdown.stories.js +++ b/src/Dropdown/Dropdown.stories.tsx @@ -1,8 +1,8 @@ import React, { useState } from 'react'; -import Dropdown, { PLACEMENT_TRANSITION_ORIGINS } from './Dropdown'; -import Flex from '../Flex'; +import Dropdown, { PLACEMENT_TRANSITION_ORIGINS, Placement } from './Dropdown'; +import { Flex } from '../Flex'; import RadioGroup from '../Form/RadioGroup'; -import Button from '../Button'; +import { Button } from '../Button'; export default { title: 'Components|Dropdown', @@ -21,13 +21,13 @@ function BasicExample() { value: p, label: p, }))} - onChange={(_, val) => setPlacement(val)} + onChange={(_: any, val: any) => setPlacement(val)} /> diff --git a/src/Dropdown/Dropdown.js b/src/Dropdown/Dropdown.tsx similarity index 77% rename from src/Dropdown/Dropdown.js rename to src/Dropdown/Dropdown.tsx index 0fb37bd..dc457aa 100644 --- a/src/Dropdown/Dropdown.js +++ b/src/Dropdown/Dropdown.tsx @@ -1,18 +1,32 @@ import React, { useRef, useState, useEffect, useContext } from 'react'; -import PropTypes from 'prop-types'; import { FocusOn } from 'react-focus-on'; import { Transition } from 'react-transition-group'; +import { TransitionStatus } from 'react-transition-group/Transition'; import { Manager, Reference, Popper } from 'react-popper'; import { css, keyframes } from 'styled-components'; -import Box from '../Box'; +import { Box } from '../Box'; import Portal from '../Portal'; -import Icon from '../Icon'; +import { Icon } from '../Icon'; import { useKeyPress } from '../hooks'; import { createComponent, findNextFocusableElement, findPreviousFocusableElement } from '../utils'; const DropdownContext = React.createContext({}); -export const PLACEMENT_TRANSITION_ORIGINS = { +export type Placement = + | 'top-start' + | 'top' + | 'top-end' + | 'right' + | 'right-start' + | 'right-end' + | 'bottom-start' + | 'bottom' + | 'bottom-end' + | 'left-start' + | 'left' + | 'left-end'; + +export const PLACEMENT_TRANSITION_ORIGINS: Record = { 'top-start': '0 100%', top: '50% 100%', 'top-end': '100% 100%', @@ -38,23 +52,45 @@ const Trigger = createComponent({ `, }); +export interface DropdownProps { + placement?: Placement; + trigger: any; + render?: (p?: any) => React.ReactNode; + autoclose?: boolean; + offset?: string; + boundariesElement?: Element | 'viewport' | 'scrollParent' | 'window'; + positionFixed?: boolean; + width?: number | string; + zIndex?: number; + transitionDuration?: number; + transitionTimingFunction?: string; + portalNode?: any; + styles?: any; +} + +export interface DropdownStaticMembers { + Divider: any; + Title: any; + Item: any; +} + /** Easily display contextual overlays using custom trigger elements. Dropdowns positioning system uses [Popper.js](https://github.com/FezVrasta/popper.js). Refer to their documentation for placement and option overrides. */ -export default function Dropdown({ - autoclose, - placement, - positionFixed, - boundariesElement, - offset, +const Dropdown: React.FC & DropdownStaticMembers = ({ + autoclose = true, + placement = 'bottom-start', + positionFixed = false, + boundariesElement = 'viewport', + offset = '0, 10', + portalNode, + styles = {}, trigger, render, children, - portalNode, - styles = {}, ...menuProps -}) { - const popperRef = useRef(); - const menuRef = useRef(); - const triggerRef = useRef(); +}) => { + const popperRef = useRef(); + const menuRef = useRef(); + const triggerRef = useRef(); const [isOpen, setOpen] = useState(false); @@ -62,7 +98,7 @@ export default function Dropdown({ const close = () => setOpen(false); const toggle = () => (isOpen ? close() : open()); - const handleTrigger = e => { + const handleTrigger = (e: any) => { // Allow all clicks and, for non-button elements, Enter and Space to toggle Dropdown // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/button_role#Required_JavaScript_Features if (e.type === 'click' || (e.type === 'keypress' && (e.which === 13 || e.which === 32))) { @@ -76,23 +112,24 @@ export default function Dropdown({ useEffect(() => { if (isOpen) { setTimeout(() => { - if (menuRef.current) menuRef.current.focus(); + if (menuRef?.current) menuRef.current.focus(); }); } }, [isOpen]); useKeyPress('Escape', () => { if (isOpen) { - if (triggerRef.current) triggerRef.current.focus(); + if (triggerRef?.current) triggerRef.current.focus(); close(); } }); - useKeyPress(ARROW_KEYS, event => { + useKeyPress(ARROW_KEYS, (event: any) => { if (isOpen && menuRef.current) { event.preventDefault(); const focusArgs = [menuRef.current, document.activeElement]; - const nextFocusable = + const nextFocusable: any = + // @ts-ignore event.key === 'ArrowUp' ? findPreviousFocusableElement(...focusArgs) : findNextFocusableElement(...focusArgs); if (nextFocusable) { @@ -101,7 +138,7 @@ export default function Dropdown({ } }); - const handleMenuBlur = e => { + const handleMenuBlur = (e: any) => { if (autoclose && menuRef.current && !menuRef.current.contains(e.relatedTarget)) { close(); } @@ -159,6 +196,7 @@ export default function Dropdown({ { menuRef.current = menuInner; + // @ts-ignore ref(menuInner); }} style={style} @@ -183,34 +221,9 @@ export default function Dropdown({ ); -} - -Dropdown.propTypes = { - placement: PropTypes.oneOf(Object.keys(PLACEMENT_TRANSITION_ORIGINS)), - trigger: PropTypes.element.isRequired, - render: PropTypes.func, - autoclose: PropTypes.bool, - offset: PropTypes.string, - boundariesElement: PropTypes.string, - positionFixed: PropTypes.bool, - width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - zIndex: PropTypes.number, - transitionDuration: PropTypes.number, - transitionTimingFunction: PropTypes.string, - portalNode: PropTypes.instanceOf(typeof Element !== 'undefined' ? Element : Object), }; -Dropdown.defaultProps = { - placement: 'bottom-start', - autoclose: true, - offset: '0, 10', - positionFixed: false, - boundariesElement: 'viewport', - width: 'auto', - zIndex: 10, - transitionDuration: 225, - transitionTimingFunction: 'cubic-bezier(0.25, 0.1, 0.17, 1.2)', -}; +export default Dropdown; const scaleIn = keyframes` from { @@ -223,7 +236,16 @@ const scaleIn = keyframes` } `; -const DropdownMenu = createComponent({ +interface DropdownMenuProps { + width: number | string; + placement: Placement; + zIndex: number; + transitionState: TransitionStatus; + transitionDuration: number; + transitionTimingFunction: string; +} + +const DropdownMenu = createComponent>({ name: 'DropdownMenu', props: () => ({ tabIndex: 0, @@ -238,7 +260,7 @@ const DropdownMenu = createComponent({ width: ${typeof width === 'string' ? width : `${width}px`}; opacity: 0.75; transform: scale(0.75); - transform-origin: ${PLACEMENT_TRANSITION_ORIGINS[placement]}; + transform-origin: ${placement ? PLACEMENT_TRANSITION_ORIGINS[placement] : 'top'}; padding: 8px; ${(transitionState === 'entering' || transitionState === 'entered') && @@ -270,7 +292,15 @@ Dropdown.Title = createComponent({ `, }); -const StyledDropdownItem = createComponent({ +interface StyledDropdownItemProps { + disabled: boolean; + color: string; + icon: any; + iconProps: any; + selected: boolean; +} + +const StyledDropdownItem = createComponent>({ name: 'DropdownItem', props: ({ disabled }) => ({ tabIndex: disabled ? -1 : 0, @@ -332,7 +362,23 @@ const StyledIcon = createComponent({ `, }); -Dropdown.Item = ({ closeOnClick = true, onClick, children, icon, iconProps = {}, ...props }) => { +interface DropdownItemProps { + closeOnClick?: boolean; + onClick?: () => void; + children?: React.ReactNode; + icon?: any; + iconProps?: any; +} + +const DropdownItem: React.FC = ({ + closeOnClick = true, + onClick, + children, + icon, + iconProps = {}, + ...props +}) => { + // @ts-ignore const { close } = useContext(DropdownContext); const handleClick = () => { if (closeOnClick) { @@ -349,3 +395,17 @@ Dropdown.Item = ({ closeOnClick = true, onClick, children, icon, iconProps = {}, ); }; + +Dropdown.Item = DropdownItem; + +Dropdown.defaultProps = { + placement: 'bottom-start', + autoclose: true, + offset: '0, 10', + positionFixed: false, + boundariesElement: 'viewport', + width: 'auto', + zIndex: 10, + transitionDuration: 225, + transitionTimingFunction: 'cubic-bezier(0.25, 0.1, 0.17, 1.2)', +}; diff --git a/src/Dropdown/__snapshots__/Dropdown.spec.js.snap b/src/Dropdown/__snapshots__/Dropdown.spec.tsx.snap similarity index 100% rename from src/Dropdown/__snapshots__/Dropdown.spec.js.snap rename to src/Dropdown/__snapshots__/Dropdown.spec.tsx.snap diff --git a/src/Dropdown/index.js b/src/Dropdown/index.ts similarity index 100% rename from src/Dropdown/index.js rename to src/Dropdown/index.ts diff --git a/src/Flex/Flex.stories.js b/src/Flex/Flex.stories.tsx similarity index 72% rename from src/Flex/Flex.stories.js rename to src/Flex/Flex.stories.tsx index a132a8e..e62b4b6 100644 --- a/src/Flex/Flex.stories.js +++ b/src/Flex/Flex.stories.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import Box from '../Box'; -import Flex from './Flex'; +import { Box } from '../Box'; +import { Flex } from './Flex'; export default { title: 'Components|Flex', @@ -13,7 +13,7 @@ export const Basic = () => ( I take up the remaining space - + {}} m={2} p={2} style={{ backgroundColor: 'gainsboro' }}> I am a Box diff --git a/src/Flex/Flex.js b/src/Flex/Flex.tsx similarity index 63% rename from src/Flex/Flex.js rename to src/Flex/Flex.tsx index 1667b98..b6145ea 100644 --- a/src/Flex/Flex.js +++ b/src/Flex/Flex.tsx @@ -1,9 +1,12 @@ -import React from 'react'; import { css } from 'styled-components'; -import Box from '../Box'; +import { Box, BoxProps } from '../Box'; import { createComponent } from '../utils'; -const StyledFlex = createComponent({ +export interface FlexProps extends BoxProps {} + +/** Quickly manage the layout, alignment, and sizing of grid columns, navigation, components, and more with a full suite of responsive flexbox utilities. For more complex implementations, custom CSS may be necessary. + */ +export const Flex = createComponent({ name: 'Flex', as: Box, style: () => css` @@ -11,14 +14,4 @@ const StyledFlex = createComponent({ `, }); -/** Quickly manage the layout, alignment, and sizing of grid columns, navigation, components, and more with a full suite of responsive flexbox utilities. For more complex implementations, custom CSS may be necessary. - */ -const Flex = React.forwardRef((props, ref) => ); - Flex.displayName = 'Flex'; - -Flex.propTypes = { - ...Box.propTypes, -}; - -export default Flex; diff --git a/src/Flex/index.js b/src/Flex/index.js deleted file mode 100644 index b86d29f..0000000 --- a/src/Flex/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import Flex from './Flex'; - -export default Flex; diff --git a/src/Flex/index.ts b/src/Flex/index.ts new file mode 100644 index 0000000..23f5ac9 --- /dev/null +++ b/src/Flex/index.ts @@ -0,0 +1,5 @@ +import { Flex } from './Flex'; + +export * from './Flex'; + +export default Flex; diff --git a/src/Form/Checkbox.stories.js b/src/Form/Checkbox.stories.tsx similarity index 92% rename from src/Form/Checkbox.stories.js rename to src/Form/Checkbox.stories.tsx index c978389..81c7a80 100644 --- a/src/Form/Checkbox.stories.js +++ b/src/Form/Checkbox.stories.tsx @@ -1,6 +1,7 @@ import React from 'react'; +// eslint-disable-next-line import/no-extraneous-dependencies import { boolean } from '@storybook/addon-knobs'; -import Flex from '../Flex'; +import { Flex } from '../Flex'; import { Checkbox } from './Checkbox'; export default { diff --git a/src/Form/Checkbox.js b/src/Form/Checkbox.tsx similarity index 83% rename from src/Form/Checkbox.js rename to src/Form/Checkbox.tsx index 0526240..8fba9bb 100644 --- a/src/Form/Checkbox.js +++ b/src/Form/Checkbox.tsx @@ -1,14 +1,13 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { css } from 'styled-components'; -import Icon from '../Icon'; -import FormError from '../Form/FormError'; +import { Icon } from '../Icon'; +import { FormError } from '../Form/FormError'; import { createEasyInput } from './EasyInput'; import { getComponentSize, createComponent } from '../utils'; const transitionTiming = '250ms cubic-bezier(0.4, 0, 0.2, 1)'; -const HiddenInput = createComponent({ +const HiddenInput = createComponent({ name: 'CheckboxInput', tag: 'input', style: css` @@ -25,7 +24,7 @@ const HiddenInput = createComponent({ `, }); -const CheckIcon = createComponent({ +const CheckIcon = createComponent({ name: 'CheckIcon', as: Icon, style: ({ theme, iconSize }) => { @@ -43,9 +42,9 @@ const CheckIcon = createComponent({ }, }); -const CheckboxShape = createComponent({ +const CheckboxShape = createComponent({ name: 'CheckboxShape', - as: 'div', + tag: 'div', style: ({ theme, isRadio, isFocused, size }) => { const checkboxSizeStyles = getComponentSize(theme, 'Checkbox', size); const radioSizeStyles = getComponentSize(theme, 'Radio', size); @@ -108,9 +107,9 @@ const CheckboxShape = createComponent({ }, }); -const CheckboxLabel = createComponent({ +const CheckboxLabel = createComponent({ name: 'CheckboxLabel', - as: 'span', + tag: 'span', style: ({ theme, size }) => { const sizeStyles = getComponentSize(theme, 'CheckboxLabel', size); @@ -122,7 +121,7 @@ const CheckboxLabel = createComponent({ }, }); -const CheckboxContainer = createComponent({ +const CheckboxContainer = createComponent({ name: 'CheckboxContainer', tag: 'label', style: ({ theme, isChecked, isDisabled, isHorizontal, size, color }) => { @@ -193,26 +192,34 @@ const CheckboxContainer = createComponent({ }, }); -export class Checkbox extends React.Component { - static propTypes = { - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - label: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.bool]), - valueTrue: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.bool]), - valueFalse: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.bool]), - onChange: PropTypes.func, - size: PropTypes.string, - horizontal: PropTypes.bool, - disabled: PropTypes.bool, - styles: PropTypes.shape(), - colorOn: PropTypes.string, - colorOff: PropTypes.string, - ariaLabel: PropTypes.string, - checkIconColor: PropTypes.string, - checkIcon: PropTypes.string, - }; +export interface CheckboxProps { + id?: string; + name?: string; + label?: string | React.ReactElement; + value?: number | string | boolean; + valueTrue?: number | string | boolean; + valueFalse?: number | string | boolean; + onChange?: (n?: string, v?: number | string | boolean) => void; + size?: string; + horizontal?: boolean; + disabled?: boolean; + styles?: any; + colorOn?: string; + colorOff?: string; + ariaLabel?: string; + checkIconColor?: string; + checkIcon?: string; + error?: string; + isRadio?: boolean; +} + +interface CheckboxState { + isFocused?: boolean; + isActive?: boolean; + value?: CheckboxProps['value']; +} +export class Checkbox extends React.Component { static defaultProps = { size: 'md', valueTrue: true, @@ -228,7 +235,7 @@ export class Checkbox extends React.Component { checkIcon: 'check', }; - static getDerivedStateFromProps(props, state) { + static getDerivedStateFromProps(props: CheckboxProps, state: CheckboxState) { if (props.value !== undefined && props.value !== state.value) { return { value: props.value, @@ -240,6 +247,8 @@ export class Checkbox extends React.Component { state = { value: this.props.value, + isFocused: false, + isActive: false, }; get checked() { @@ -256,7 +265,8 @@ export class Checkbox extends React.Component { value: newValue, }, () => { - onChange(this.props.name, newValue); + // eslint-disable-next-line no-unused-expressions + onChange?.(this.props.name, newValue); } ); }; diff --git a/src/Form/CheckboxGroup.stories.js b/src/Form/CheckboxGroup.stories.tsx similarity index 100% rename from src/Form/CheckboxGroup.stories.js rename to src/Form/CheckboxGroup.stories.tsx diff --git a/src/Form/CheckboxGroup.js b/src/Form/CheckboxGroup.tsx similarity index 72% rename from src/Form/CheckboxGroup.js rename to src/Form/CheckboxGroup.tsx index 975cedc..facd8bf 100644 --- a/src/Form/CheckboxGroup.js +++ b/src/Form/CheckboxGroup.tsx @@ -1,28 +1,29 @@ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import Box from '../Box'; +import { Box } from '../Box'; import Checkbox from './Checkbox'; -import FormError from './FormError'; +import { FormError } from './FormError'; import { createEasyInput } from './EasyInput'; import GroupContainer from './GroupContainer'; -export class CheckboxGroup extends Component { - static propTypes = { - name: PropTypes.string.isRequired, - horizontal: PropTypes.bool, - value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), - choices: PropTypes.arrayOf( - PropTypes.shape({ - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - label: PropTypes.string, - disabled: PropTypes.bool, - }) - ).isRequired, - onChange: PropTypes.func, - colorOn: PropTypes.string, - colorOff: PropTypes.string, - }; +interface CheckboxGroupProps { + id?: string; + name?: string; + horizontal?: boolean; + value?: string[] | number[] | string | number; + choices: { + value: string | number; + label: string; + disabled?: boolean; + exclusive?: boolean; + id?: any; + }[]; + onChange?: (name?: string, newSelected?: any) => void; + colorOn?: string; + colorOff?: string; + error?: string; +} +export class CheckboxGroup extends Component { static defaultProps = { horizontal: false, onChange() {}, @@ -34,9 +35,9 @@ export class CheckboxGroup extends Component { selected: Array.isArray(this.props.value) ? [...this.props.value] : [], }; - createChangeHandler = choice => (name, value) => { + createChangeHandler = (choice: any) => (_name: any, value: any) => { const { selected } = this.state; - let newSelected; + let newSelected: any; if (!selected.includes(value)) { if (choice.exclusive) { newSelected = [value]; @@ -55,7 +56,8 @@ export class CheckboxGroup extends Component { selected: newSelected, }, () => { - this.props.onChange(this.props.name, newSelected); + // eslint-disable-next-line no-unused-expressions + this.props.onChange?.(this.props.name, newSelected); } ); }; diff --git a/src/Form/DateInput.spec.js b/src/Form/DateInput.spec.tsx similarity index 88% rename from src/Form/DateInput.spec.js rename to src/Form/DateInput.spec.tsx index b5b3e52..3facf34 100644 --- a/src/Form/DateInput.spec.js +++ b/src/Form/DateInput.spec.tsx @@ -1,18 +1,18 @@ import React from 'react'; -import { renderWithTheme, fireEvent, act } from '../../test/utils'; -import DateInput, { getRawMaxLength } from './DateInput'; -import ThemeProvider from '../ThemeProvider'; +import { renderWithTheme, fireEvent, act } from '../test/utils'; +import DateInput, { getRawMaxLength, DateInputProps } from './DateInput'; +import { ThemeProvider } from '../ThemeProvider'; describe('', () => { - const renderInput = props => { + const renderInput = (props?: DateInputProps) => { const utils = renderWithTheme(); return { ...utils, input: utils.getByPlaceholderText('Input'), - }; + } as any; }; - const updateInputValue = (input, value) => { + const updateInputValue = (input: any, value: any) => { act(() => { fireEvent.change(input, { target: { value } }); }); diff --git a/src/Form/DateInput.stories.js b/src/Form/DateInput.stories.tsx similarity index 100% rename from src/Form/DateInput.stories.js rename to src/Form/DateInput.stories.tsx diff --git a/src/Form/DateInput.js b/src/Form/DateInput.tsx similarity index 76% rename from src/Form/DateInput.js rename to src/Form/DateInput.tsx index 9fae59f..e7dd4f2 100644 --- a/src/Form/DateInput.js +++ b/src/Form/DateInput.tsx @@ -1,17 +1,16 @@ import React, { useState, useEffect, useRef } from 'react'; -import PropTypes from 'prop-types'; import DateFormatter from 'cleave.js/src/shortcuts/DateFormatter'; -import Input from './Input'; +import { Input, InputProps } from './Input'; import { createEasyInput } from './EasyInput'; import { getNextCursorPosition } from '../utils'; -export const getRawMaxLength = pattern => { +export const getRawMaxLength = (pattern: any) => { const formatter = new DateFormatter(pattern, '1900-01-01', '2099-12-31'); - const blocks = formatter.getBlocks(); + const blocks: any[] = formatter.getBlocks(); return blocks.reduce((sum, block) => sum + block, 0); }; -const formatDate = (pattern, delimiter, dateString = '') => { +const formatDate = (pattern: any, delimiter: any, dateString = '') => { const formatter = new DateFormatter(pattern, '1900-01-01', '2099-12-31'); // Process our date string, bounding values between 1 and 31, and prepending 0s for @@ -19,7 +18,7 @@ const formatDate = (pattern, delimiter, dateString = '') => { let tmpDate = formatter.getValidatedDate(`${dateString}`); // Blocks look something like [2, 2, 4], telling us how long each chunk should be - return formatter.getBlocks().reduce((str, blockLength, index, blockArr) => { + return (formatter.getBlocks() as any[]).reduce((str, blockLength, index, blockArr) => { const block = tmpDate.substring(0, blockLength); if (!block) { return str; @@ -34,7 +33,13 @@ const formatDate = (pattern, delimiter, dateString = '') => { }, ''); }; -export function DateInput({ +export interface DateInputProps extends InputProps { + initialValue?: string; + pattern?: string[]; + delimiter?: string; +} + +export const DateInput: React.FC = ({ delimiter, pattern, forwardedRef, @@ -43,9 +48,10 @@ export function DateInput({ onKeyDown, onChange, ...inputProps -}) { - const format = value => formatDate(pattern, delimiter, value); +}) => { + const format = (value: any) => formatDate(pattern, delimiter, value); const [currentValue, setValue] = useState(initialValue || format(propValue)); + // eslint-disable-next-line react-hooks/rules-of-hooks const inputRef = forwardedRef || useRef(); const previousValue = useRef(propValue); @@ -54,9 +60,10 @@ export function DateInput({ setValue(format(propValue)); } previousValue.current = propValue; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [propValue]); - const handleKeyDown = event => { + const handleKeyDown = (event: any) => { const isLetterLike = /^\w{1}$/.test(event.key); if (isLetterLike && currentValue.replace(/\D/g, '').length >= getRawMaxLength(pattern)) { event.preventDefault(); @@ -68,7 +75,7 @@ export function DateInput({ } }; - const handleChange = (name, newValue, event) => { + const handleChange = (name: any, newValue: any, event: any) => { const nextValue = newValue.length < currentValue.length ? newValue.trim() : format(newValue); const nextCursorPosition = getNextCursorPosition(event.target.selectionStart, currentValue, nextValue); @@ -91,13 +98,6 @@ export function DateInput({ {...inputProps} /> ); -} - -DateInput.propTypes = { - ...Input.propTypes, - initialValue: PropTypes.string, - pattern: PropTypes.arrayOf(PropTypes.string), - delimiter: PropTypes.string, }; DateInput.defaultProps = { diff --git a/src/Form/EasyInput.js b/src/Form/EasyInput.tsx similarity index 58% rename from src/Form/EasyInput.js rename to src/Form/EasyInput.tsx index 5d4c749..02b52df 100644 --- a/src/Form/EasyInput.js +++ b/src/Form/EasyInput.tsx @@ -6,16 +6,22 @@ import { Context } from './Formbot'; * each subscribed component and there's currently no way to bail out of renders if the * values we care about haven't changed. Below is a sufficient workaround below and redux maintainers are discussing here: https://github.com/facebook/react/issues/14110 */ -const PureInput = React.memo(({ Component, ...props }) => ); +const PureInput = React.memo(({ Component, ...props }: any) => ); -function EasyInput({ name, Component, shouldRenderError = true, ...props }) { - const state = useContext(Context); +export interface EasyInputProps { + name?: string; + Component?: any; + shouldRenderError?: boolean; +} + +function EasyInput({ name, Component, shouldRenderError = true, ...props }: T) { + const state: any = useContext(Context); if (!state) { return ; } - const value = state.values[name]; + const value = name && state.values[name]; const defaultValue = Component.defaultProps && Component.defaultProps.defaultValue !== undefined ? Component.defaultProps.defaultValue @@ -25,7 +31,7 @@ function EasyInput({ name, Component, shouldRenderError = true, ...props }) { - forwardRef((props, ref) => ); // eslint-disable-line - -export default EasyInput; +export function createEasyInput(Component: any) { + return forwardRef((props: any, ref) => { + return Component={Component} forwardedRef={ref} {...props} />; + }); +} diff --git a/src/Form/Field.js b/src/Form/Field.tsx similarity index 70% rename from src/Form/Field.js rename to src/Form/Field.tsx index 43b004f..510f0aa 100644 --- a/src/Form/Field.js +++ b/src/Form/Field.tsx @@ -1,7 +1,11 @@ import { css } from 'styled-components'; import { createComponent } from '../utils'; -const Field = createComponent({ +interface FieldProps { + styles?: any; +} + +export const Field = createComponent({ name: 'Field', style: css` position: relative; @@ -12,5 +16,3 @@ const Field = createComponent({ } `, }); - -export default Field; diff --git a/src/Form/Fieldset.stories.js b/src/Form/Fieldset.stories.tsx similarity index 78% rename from src/Form/Fieldset.stories.js rename to src/Form/Fieldset.stories.tsx index e58861a..61e4cac 100644 --- a/src/Form/Fieldset.stories.js +++ b/src/Form/Fieldset.stories.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import Fieldset from './Fieldset'; -import Input from './Input'; +import { Fieldset } from './Fieldset'; +import { Input } from './Input'; export default { title: 'Components|Forms/Fieldset', diff --git a/src/Form/Fieldset.js b/src/Form/Fieldset.tsx similarity index 59% rename from src/Form/Fieldset.js rename to src/Form/Fieldset.tsx index 4168812..aa735f8 100644 --- a/src/Form/Fieldset.js +++ b/src/Form/Fieldset.tsx @@ -1,12 +1,17 @@ import React from 'react'; -import PropTypes from 'prop-types'; import styled, { css } from 'styled-components'; import { createComponent } from '../utils'; -const Legend = createComponent({ +export interface FieldsetProps { + legend?: string | JSX.Element; + sublegend?: string | JSX.Element; + children?: any; +} + +const Legend = createComponent({ name: 'Legend', tag: 'legend', - style: ({ theme, color = theme.colors.greyDarkest }) => css` + style: ({ theme, color = theme.colors.greyDarkest }: any) => css` font-weight: 700; margin-bottom: 8px; font-size: 18px; @@ -24,16 +29,10 @@ const Container = styled.fieldset` } `; -const Fieldset = ({ legend, children }) => ( +export const Fieldset = ({ legend, children }: FieldsetProps) => ( {legend && {legend}} {children} ); - -Fieldset.propTypes = { - legend: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), -}; - -export default Fieldset; diff --git a/src/Form/Form.js b/src/Form/Form.tsx similarity index 63% rename from src/Form/Form.js rename to src/Form/Form.tsx index 66d728e..f32d559 100644 --- a/src/Form/Form.js +++ b/src/Form/Form.tsx @@ -1,14 +1,12 @@ import React, { useContext } from 'react'; import { Context } from './Formbot'; -function Form({ children, ...props }) { - const state = useContext(Context); +export function Form({ children, ...props }: any) { + const state = useContext(Context) as any; return (
{children}
- ) + ); } - -export default Form; diff --git a/src/Form/FormError.js b/src/Form/FormError.tsx similarity index 81% rename from src/Form/FormError.js rename to src/Form/FormError.tsx index cb5cc6a..8a04544 100644 --- a/src/Form/FormError.js +++ b/src/Form/FormError.tsx @@ -5,7 +5,12 @@ import get from 'lodash/get'; import { createComponent } from '../utils'; import { Context as FormbotContext } from './Formbot'; -export const StyledFormError = createComponent({ +interface FormErrorProps { + name?: string; + children?: any; +} + +export const StyledFormError = createComponent({ name: 'FormError', tag: 'span', style: css` @@ -17,7 +22,7 @@ export const StyledFormError = createComponent({ `, }); -const FormError = ({ name, children }) => { +export const FormError = ({ name = '', children }: FormErrorProps) => { const context = useContext(FormbotContext); const hasNameOnly = !!name && typeof children !== 'function'; const hasRenderProp = !!name && typeof children === 'function'; @@ -33,5 +38,3 @@ const FormError = ({ name, children }) => { {children} ); }; - -export default FormError; diff --git a/src/Form/FormGroup.js b/src/Form/FormGroup.js deleted file mode 100644 index 286a6a9..0000000 --- a/src/Form/FormGroup.js +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; -import Box from '../Box'; - -const FormGroup = ({ children, padding = 3 }) => {children}; - -export default FormGroup; diff --git a/src/Form/FormGroup.tsx b/src/Form/FormGroup.tsx new file mode 100644 index 0000000..42ae100 --- /dev/null +++ b/src/Form/FormGroup.tsx @@ -0,0 +1,4 @@ +import React from 'react'; +import { Box } from '../Box'; + +export const FormGroup = ({ children, padding = 3 }: any) => {children}; diff --git a/src/Form/Formbot.example.js b/src/Form/Formbot.example.js index 9644314..4444266 100644 --- a/src/Form/Formbot.example.js +++ b/src/Form/Formbot.example.js @@ -1,16 +1,16 @@ import React, { useContext, createRef } from 'react'; -import Input from './Input'; +import { Input } from './Input'; import Select from './Select'; import Formbot, { Context } from './Formbot'; import Form from './Form'; -import Button from '../Button'; +import { Button } from '../Button'; import Fieldset from './Fieldset'; import CheckboxGroup from './CheckboxGroup'; import RadioGroup from './RadioGroup'; import Switch from './Switch'; import PhoneInput from './PhoneInput'; import DateInput from './DateInput'; -import FormError from './FormError'; +import { FormError } from './FormError'; const selectValues = [{ id: 1, value: 'male', label: 'Male' }, { id: 1, value: 'female', label: 'Female' }]; diff --git a/src/Form/Formbot.stories.js b/src/Form/Formbot.stories.tsx similarity index 74% rename from src/Form/Formbot.stories.js rename to src/Form/Formbot.stories.tsx index 99c719d..008452a 100644 --- a/src/Form/Formbot.stories.js +++ b/src/Form/Formbot.stories.tsx @@ -1,18 +1,21 @@ import React, { useContext, createRef } from 'react'; +// eslint-disable-next-line import/no-extraneous-dependencies import * as yup from 'yup'; -import Formbot, { Context } from './Formbot'; -import Form from './Form'; -import Field from './Field'; -import Fieldset from './Fieldset'; -import FormError from './FormError'; +import { Formbot, Context } from './Formbot'; +import { Form } from './Form'; +import { Field } from './Field'; +import { Fieldset } from './Fieldset'; +import { FormError } from './FormError'; import PhoneInput from './PhoneInput'; import DateInput from './DateInput'; import Select from './Select'; import CheckboxGroup from './CheckboxGroup'; import RadioGroup from './RadioGroup'; import Switch from './Switch'; -import Button from '../Button'; -import Input from './Input'; +import { Button } from '../Button'; +import { Input } from './Input'; + +const ButtonAsAny = Button as any; export default { title: 'Components|Forms/Formbot', @@ -36,38 +39,42 @@ export const Basic = () => ( - + Sign In ); +const randomCallback = async (_value: any) => { + const callback = (resolve: any) => { + setTimeout(() => { + resolve(false); + }, 2000); + }; + + return new Promise(callback); +}; + export const AsyncValidation = () => ( - new Promise(resolve => { - setTimeout(() => { - resolve(false); - }, 2000); - }) - ), + random: yup.string().test('valid', 'This test was delayed by 2 seconds.', randomCallback as any), }}>
- + Sign In
); export const FullExample = () => { - const selectValues = [{ id: 1, value: 'male', label: 'Male' }, { id: 1, value: 'female', label: 'Female' }]; + const selectValues = [ + { id: 1, value: 'male', label: 'Male' }, + { id: 1, value: 'female', label: 'Female' }, + ]; const checkboxValues = [ { @@ -97,12 +104,12 @@ export const FullExample = () => { }, ]; const Values = () => { - const state = useContext(Context); + const state: any = useContext(Context); return
{JSON.stringify(state.values, null, 2)}
; }; class FormbotExample extends React.Component { - nameRef = createRef(); + nameRef: any = createRef(); componentDidMount() { this.nameRef.current.focus(); @@ -112,7 +119,7 @@ export const FullExample = () => { return ( { + name: (val: any) => { if (val !== 'Bob') { throw new Error('Your name must be Bob'); } @@ -142,7 +149,7 @@ export const FullExample = () => { - {error => Hi, I am a custom error: {error}} + {(error: any) => Hi, I am a custom error: {error}} @@ -160,9 +167,9 @@ export const FullExample = () => { - + diff --git a/src/Form/Formbot.js b/src/Form/Formbot.tsx similarity index 84% rename from src/Form/Formbot.js rename to src/Form/Formbot.tsx index 36b2da2..1db7e1d 100644 --- a/src/Form/Formbot.js +++ b/src/Form/Formbot.tsx @@ -1,38 +1,37 @@ import React from 'react'; -import PropTypes from 'prop-types'; export const Context = React.createContext(null); const VALIDATIONS = { - required: (val, isRequired) => { + required: (val: any, isRequired: boolean) => { if (!isRequired) return; if (!val || (typeof val === 'string' && val === '')) { throw new Error('This field is required'); } }, - minLength: (val, minLength) => { + minLength: (val: any, minLength: number) => { if (!val || `${val}`.length < minLength) { throw new Error(`This field must be at least ${minLength} characters`); } }, - maxLength: (val, maxLength) => { + maxLength: (val: any, maxLength: number) => { if (val && `${val}`.length > maxLength) { throw new Error(`This field cannot be more than ${maxLength} characters`); } }, }; -export default class Formbot extends React.Component { - static propTypes = { - initialValues: PropTypes.shape(), - validations: PropTypes.shape(), - validationSchema: PropTypes.shape(), - onFocus: PropTypes.func, - onChange: PropTypes.func, - onBlur: PropTypes.func, - onSubmit: PropTypes.func, - }; +export interface FormbotProps { + initialValues: any; + validations: any; + validationSchema: any; + onFocus: any; + onChange: any; + onBlur: any; + onSubmit: any; +} +export class Formbot extends React.Component { static defaultProps = { initialValues: {}, validations: {}, @@ -77,7 +76,7 @@ export default class Formbot extends React.Component { setValues = (values = {}) => new Promise(resolve => { this.setState( - state => ({ + (state: any) => ({ values: { ...state.values, ...values, @@ -87,9 +86,9 @@ export default class Formbot extends React.Component { ); }); - setErrors = (errors = {}, cb) => + setErrors = (errors = {}, cb: any) => this.setState( - state => ({ + (state: any) => ({ errors: { ...state.errors, ...errors, @@ -98,10 +97,10 @@ export default class Formbot extends React.Component { cb ); - updateField = (field, updates = {}) => + updateField = (field: any, updates = {}) => new Promise(resolve => { this.setState( - state => ({ + (state: any) => ({ fields: { ...state.fields, [field]: { @@ -122,7 +121,7 @@ export default class Formbot extends React.Component { }); }; - validateField(field) { + validateField(field: any) { return new Promise(resolve => { const fieldState = this.state.fields[field] || {}; if (fieldState.validated) { @@ -144,7 +143,7 @@ export default class Formbot extends React.Component { const fieldValue = this.state.values[field]; - const setFieldValidated = message => { + const setFieldValidated = (message?: any) => { this.updateField(field, { validated: true }).then(() => { this.setErrors({ [field]: message }, resolve); }); @@ -154,7 +153,8 @@ export default class Formbot extends React.Component { .then(() => { if (hasSchema && typeof validation.validate === 'function') { return validation.validate(fieldValue, validationOpts); - } else if (typeof validation === 'function') { + } + if (typeof validation === 'function') { validation(fieldValue); } else { Object.keys(validation).forEach(method => { @@ -187,15 +187,15 @@ export default class Formbot extends React.Component { ); } - onFocus = field => { + onFocus = (field: any) => { this.updateField(field, { focused: true }).then(() => { this.props.onFocus(field); }); }; - onChange = (field, value) => { + onChange = (field: any, value: any) => { this.setState( - state => ({ + (state: any) => ({ values: { ...state.values, [field]: value, @@ -209,7 +209,7 @@ export default class Formbot extends React.Component { ); }; - onBlur = field => { + onBlur = (field: any) => { this.updateField(field, { blurred: true }) .then(() => this.validateField(field)) .then(() => { @@ -217,7 +217,7 @@ export default class Formbot extends React.Component { }); }; - onSubmit = event => { + onSubmit = (event: any) => { event.preventDefault(); this.validateAllFields().then(() => { @@ -246,7 +246,7 @@ export default class Formbot extends React.Component { const { children } = this.props; return ( - + {typeof children === 'function' ? children(this.getContext()) : children} ); diff --git a/src/Form/GroupContainer.js b/src/Form/GroupContainer.ts similarity index 65% rename from src/Form/GroupContainer.js rename to src/Form/GroupContainer.ts index 435593d..969c0f2 100644 --- a/src/Form/GroupContainer.js +++ b/src/Form/GroupContainer.ts @@ -1,7 +1,11 @@ import styled, { css } from 'styled-components'; -import Flex from '../Flex'; +import { Flex } from '../Flex'; -const GroupContainer = styled(Flex)( +interface GroupContainerProps { + horizontal?: boolean; +} + +const GroupContainer = styled(Flex)( ({ theme, horizontal }) => css` flex-direction: column; diff --git a/src/Form/Input.stories.js b/src/Form/Input.stories.tsx similarity index 80% rename from src/Form/Input.stories.js rename to src/Form/Input.stories.tsx index a64d42c..95bfae0 100644 --- a/src/Form/Input.stories.js +++ b/src/Form/Input.stories.tsx @@ -1,9 +1,12 @@ import React from 'react'; -import { object, select, boolean } from '@storybook/addon-knobs/react'; -import Formbot from './Formbot'; -import Button from '../Button'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { object, select, boolean } from '@storybook/addon-knobs'; +import { Formbot } from './Formbot'; +import { Button } from '../Button'; import { Input } from './Input'; +const ButtonAsAny = Button as any; + export default { title: 'Components|Forms/Input', component: Input, @@ -14,22 +17,21 @@ const defaultInputProps = { placeholder: 'Start typing here...', }; -export const Basic = () => ; +export const Basic = () => ; export const Controlled = () => { function ControlledInput() { return ( { + value: (value: any) => { if (value !== 'secretpassword') { throw new Error('Try typing "secretpassword"'); } }, }}> - {({ values, errors, onChange, onSubmit }) => ( + {({ values, errors, onChange, onSubmit }: any) => (
{ error={errors.value} onChange={onChange} /> - +
)}
@@ -65,7 +67,7 @@ export const FloatingLabel = () => ; export const Disabled = () => ; -export const Error = () => ; +export const ErrorComponent = () => ; export const Styles = () => ( ({ name: 'InputContainer', style: css` position: relative; @@ -19,7 +22,39 @@ const InputContainer = createComponent({ `, }); -const StyledInput = createComponent({ +export interface InputProps { + value?: any; + type?: string; + disabled?: boolean; + placeholder?: string; + multiline?: boolean; + label?: any; + autoFocus?: boolean; + transformOnBlur?: any; + onFocus?: any; + onBlur?: any; + onChange?: any; + onKeyDown?: any; + minRows?: number; + rows?: number; + maxRows?: number; + rowHeight?: number; + lineHeight?: number; + autogrow?: boolean; + size?: string; + floating?: boolean; + forwardedRef?: any; + leftIcon?: string; + leftIconProps?: any; + rightIcon?: string; + rightIconProps?: any; + style?: any; + styles?: any; + id?: any; + error?: any; +} + +const StyledInput = createComponent({ name: 'Input', tag: 'input', style: ({ @@ -31,7 +66,7 @@ const StyledInput = createComponent({ rightIcon, leftIconProps, rightIconProps, - }) => css` + }: any) => css` border: 1px solid ${theme.colors.greyLight}; height: 48px; display: block; @@ -93,7 +128,7 @@ const StyledInput = createComponent({ }); const StyledIcon = styled(Icon)` - ${({ theme, isDisabled }) => css` + ${({ theme, isDisabled }: any) => css` position: absolute; top: 50%; transform: translateY(-50%); @@ -109,7 +144,7 @@ const StyledIcon = styled(Icon)` const LeftIcon = createComponent({ name: 'InputLeftIcon', as: StyledIcon, - style: ({ isFocused, isFloating, isFloatable, theme }) => css` + style: ({ isFocused, isFloating, isFloatable, theme }: any) => css` left: 8px; ${isFloatable && @@ -142,7 +177,7 @@ const RightIcon = createComponent({ const StyledTextArea = createComponent({ name: 'TextArea', as: StyledInput.withComponent('textarea'), - style: ({ isFloatable, isFloating }) => css` + style: ({ isFloatable, isFloating }: any) => css` ${isFloatable && isFloating && css` @@ -163,43 +198,10 @@ const AutogrowShadow = createComponent({ }), }); -const validateValueProp = (props, propName, componentName) => { - if (props.type === 'number' && typeof props[propName] !== 'number') { - return new Error(`Invalid prop ${propName} supplied to ${componentName} with type="number", expected Number`); - } - if (typeof props[propName] !== 'string' && props.type !== 'number') { - return new Error(`Invalid prop ${propName} supplied to ${componentName}, expected String`); - } - return null; -}; - -export class Input extends Component { - static propTypes = { - value: validateValueProp, - type: PropTypes.string, - disabled: PropTypes.bool, - placeholder: PropTypes.string, - multiline: PropTypes.bool, - label: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), - onFocus: PropTypes.func, - onBlur: PropTypes.func, - onChange: PropTypes.func, - minRows: PropTypes.number, - rows: PropTypes.number, - maxRows: PropTypes.number, - rowHeight: PropTypes.number, - lineHeight: PropTypes.number, - autogrow: PropTypes.bool, - size: PropTypes.string, - floating: PropTypes.bool, - forwardedRef: PropTypes.oneOfType([PropTypes.shape(), PropTypes.func]), - leftIcon: PropTypes.string, - leftIconProps: PropTypes.shape(), - rightIcon: PropTypes.string, - rightIconProps: PropTypes.shape(), - }; +export class InputClass extends Component { + autogrowShadowNode: any; - static defaultProps = { + static defaultProps: InputProps = { type: 'text', multiline: false, minRows: 2, @@ -218,7 +220,7 @@ export class Input extends Component { rightIconProps: {}, }; - static getDerivedStateFromProps(props, state) { + static getDerivedStateFromProps(props: InputProps, state: any) { if (props.value !== undefined && props.value !== state.value) { return { value: props.value, @@ -227,7 +229,7 @@ export class Input extends Component { return null; } - state = { + state: any = { focused: false, }; @@ -238,19 +240,19 @@ export class Input extends Component { } componentDidMount() { - if (this.props.autofocus && this.ref.current) { + if (this.props.autoFocus && this.ref.current) { this.ref.current.focus(); } if (this.props.multiline) { /* eslint-disable-next-line react/no-did-mount-set-state */ this.setState({ - height: this.props.rows * this.props.rowHeight * this.props.lineHeight, + height: (this.props.rows || 0) * (this.props.rowHeight || 0) * (this.props.lineHeight || 0), }); } } - componentDidUpdate(oldProps, oldState) { + componentDidUpdate(_oldProps: InputProps, oldState: any) { if (oldState.value !== this.state.value) { this.autogrow(); } @@ -262,12 +264,12 @@ export class Input extends Component { } } - onFocus = e => { + onFocus = (e: any) => { this.setState({ focused: true }); this.props.onFocus(e.target.name); }; - onBlur = e => { + onBlur = (e: any) => { this.setState({ focused: false }); const { onBlur, onChange, transformOnBlur } = this.props; @@ -283,12 +285,12 @@ export class Input extends Component { } }; - onChange = e => { + onChange = (e: any) => { this.setState({ value: e.target.value }); this.props.onChange(e.target.name, e.target.value, e); }; - handleAutogrowRef = node => { + handleAutogrowRef = (node: any) => { this.autogrowShadowNode = node; this.autogrow(); }; @@ -298,7 +300,7 @@ export class Input extends Component { return; } - const { minRows, maxRows, rowHeight, lineHeight } = this.props; + const { minRows = 0, maxRows = 0, rowHeight = 0, lineHeight = 0 } = this.props; const minHeight = minRows * rowHeight * lineHeight; const maxHeight = maxRows * rowHeight * lineHeight; @@ -340,7 +342,7 @@ export class Input extends Component { label, multiline, autogrow, - autofocus, + autoFocus, id, error, floating, @@ -380,6 +382,8 @@ export class Input extends Component { disabled, }; + const { styles } = rest as any; + const statusProps = { isFloatable: floating, isFloating, @@ -390,35 +394,31 @@ export class Input extends Component { const Label = label ? ( - {leftIcon && } + {leftIcon && } {label} ) : null; return ( - + {!floating && Label} - + {floating && Label} - {leftIcon && !floating && ( - - )} + {leftIcon && !floating && } - {rightIcon && ( - - )} + {rightIcon && } - {multiline ? : } + {multiline ? : } - {!focused && error ? {error} : null} + {!focused && error ? {error} : null} {autogrow && } @@ -426,4 +426,4 @@ export class Input extends Component { } } -export default createEasyInput(Input); +export const Input = createEasyInput & EasyInputProps>(InputClass); diff --git a/src/Form/Label.js b/src/Form/Label.tsx similarity index 81% rename from src/Form/Label.js rename to src/Form/Label.tsx index de502b9..9ba6af3 100644 --- a/src/Form/Label.js +++ b/src/Form/Label.tsx @@ -1,7 +1,19 @@ import { css } from 'styled-components'; import { themeGet, createComponent } from '../utils'; -const Label = createComponent({ +interface LabelProps { + htmlFor?: any; + styles?: any; + multiline?: boolean; + isFloatable?: boolean; + isFloating?: boolean; + isFocused?: boolean; + isDisabled?: boolean; + error?: any; + size?: any; +} + +const Label = createComponent({ name: 'Label', tag: 'label', style: ({ isFloatable, isFloating, isFocused, isDisabled, multiline, theme }) => { diff --git a/src/Form/PhoneInput.spec.js b/src/Form/PhoneInput.spec.tsx similarity index 90% rename from src/Form/PhoneInput.spec.js rename to src/Form/PhoneInput.spec.tsx index 6818bdb..e3158dc 100644 --- a/src/Form/PhoneInput.spec.js +++ b/src/Form/PhoneInput.spec.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { renderWithTheme, fireEvent, act } from '../../test/utils'; +import { renderWithTheme, fireEvent, act } from '../test/utils'; import PhoneInput from './PhoneInput'; -import ThemeProvider from '../ThemeProvider'; +import { ThemeProvider } from '../ThemeProvider'; describe('', () => { - const renderInput = props => { + const renderInput = (props?: any): any => { const utils = renderWithTheme(); return { ...utils, @@ -12,7 +12,7 @@ describe('', () => { }; }; - const updateInputValue = (input, value) => { + const updateInputValue = (input: any, value: any) => { act(() => { fireEvent.change(input, { target: { value } }); }); diff --git a/src/Form/PhoneInput.stories.js b/src/Form/PhoneInput.stories.tsx similarity index 100% rename from src/Form/PhoneInput.stories.js rename to src/Form/PhoneInput.stories.tsx diff --git a/src/Form/PhoneInput.js b/src/Form/PhoneInput.tsx similarity index 73% rename from src/Form/PhoneInput.js rename to src/Form/PhoneInput.tsx index a441427..7737b09 100644 --- a/src/Form/PhoneInput.js +++ b/src/Form/PhoneInput.tsx @@ -1,40 +1,53 @@ -import React, { useState, useEffect, useRef } from 'react'; -import PropTypes from 'prop-types'; -import { AsYouType, isSupportedCountry, getCountryCallingCode, parseDigits } from 'libphonenumber-js/min'; +import React, { useState, useEffect, useRef, FC } from 'react'; +import { AsYouType, isSupportedCountry, getCountryCallingCode, parseDigits, CountryCode } from 'libphonenumber-js/min'; +// @ts-ignore import examplePhoneNumbers from 'libphonenumber-js/examples.mobile.json'; -import Input from './Input'; +import { Input, InputProps } from './Input'; import { createEasyInput } from './EasyInput'; import { getNextCursorPosition } from '../utils'; -export const getRawMaxLength = (countryCode, value) => { +export const getRawMaxLength = (countryCode: any, value: any) => { const countryCallingCode = getCountryCallingCode(countryCode); const beginsWithCountryCode = parseDigits(value).substr(0, countryCallingCode.length) === countryCallingCode; const examplePhoneNumber = examplePhoneNumbers[countryCode]; return beginsWithCountryCode ? countryCallingCode.length + examplePhoneNumber.length : examplePhoneNumber.length; }; -export function PhoneInput({ countryCode, forwardedRef, value: propValue, onKeyDown, onChange, ...inputProps }) { - const countryCodeSupported = isSupportedCountry(countryCode); +export interface PhoneInputProps extends InputProps { + countryCode?: CountryCode; +} + +export const PhoneInput: FC = ({ + countryCode, + forwardedRef, + value: propValue, + onKeyDown, + onChange, + ...inputProps +}) => { + const countryCodeSupported = countryCode && isSupportedCountry(countryCode); if (!countryCodeSupported) { throw new Error(`${countryCode} is not supported`); } - const format = value => { + const format = (value: any) => { const phoneString = value || ''; const parsed = parseDigits(phoneString).substr(0, getRawMaxLength(countryCode, phoneString)); return new AsYouType(countryCode).input(parsed); }; const [currentValue, setValue] = useState(format(propValue)); - const inputRef = forwardedRef || useRef(); + const ref = useRef(); + const inputRef = forwardedRef || ref; useEffect(() => { if (propValue !== currentValue) { setValue(format(propValue)); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [propValue]); - const handleKeyDown = event => { + const handleKeyDown = (event: any) => { const isLetterLike = /^\w{1}$/.test(event.key); if (isLetterLike) { const cursorPos = event.target.selectionStart; @@ -53,7 +66,7 @@ export function PhoneInput({ countryCode, forwardedRef, value: propValue, onKeyD } }; - const handleChange = (name, newValue, event) => { + const handleChange = (name: any, newValue: any, event: any) => { const nextValue = newValue.length < currentValue.length ? newValue.trim() : format(newValue); const nextCursorPosition = getNextCursorPosition(event.target.selectionStart, currentValue, nextValue); @@ -77,11 +90,6 @@ export function PhoneInput({ countryCode, forwardedRef, value: propValue, onKeyD {...inputProps} /> ); -} - -PhoneInput.propTypes = { - ...Input.propTypes, - countryCode: PropTypes.string, }; PhoneInput.defaultProps = { diff --git a/src/Form/RadioGroup.stories.js b/src/Form/RadioGroup.stories.tsx similarity index 97% rename from src/Form/RadioGroup.stories.js rename to src/Form/RadioGroup.stories.tsx index a166928..5f958cc 100644 --- a/src/Form/RadioGroup.stories.js +++ b/src/Form/RadioGroup.stories.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { boolean } from '@storybook/addon-knobs'; import { RadioGroup } from './RadioGroup'; -import Box from '../Box'; +import { Box } from '../Box'; export default { title: 'Components|Forms/RadioGroup', diff --git a/src/Form/RadioGroup.js b/src/Form/RadioGroup.tsx similarity index 69% rename from src/Form/RadioGroup.js rename to src/Form/RadioGroup.tsx index 76eda3d..073c99b 100644 --- a/src/Form/RadioGroup.js +++ b/src/Form/RadioGroup.tsx @@ -1,39 +1,45 @@ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import Box from '../Box'; +import { Box, BoxProps } from '../Box'; import Checkbox from './Checkbox'; import Label from './Label'; -import FormError from './FormError'; +import { FormError } from './FormError'; import { createEasyInput } from './EasyInput'; import GroupContainer from './GroupContainer'; import { createComponent } from '../utils'; -const StyledRadioGroup = createComponent({ +interface RadioGroupChoice { + id: string | number; + value: string | number; + label?: string; + disabled?: boolean; +} + +interface RadioGroupProps extends BoxProps { + name?: string; + onChange?: any; + value?: string | number; + colorOn?: string; + colorOff?: string; + fontSize?: number; + iconSize?: number; + choices: RadioGroupChoice[]; + styles?: any; + iconOn?: string; + iconOff?: string; + error?: any; + label?: string; + horizontal?: any; + colorFocus?: any; + id?: string; + disabled?: boolean; +} + +const StyledRadioGroup = createComponent({ name: 'RadioGroup', as: Box, }); -export class RadioGroup extends Component { - static propTypes = { - name: PropTypes.string, - onChange: PropTypes.func, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - colorOn: PropTypes.string, - colorOff: PropTypes.string, - fontSize: PropTypes.number, - iconSize: PropTypes.number, - choices: PropTypes.arrayOf( - PropTypes.shape({ - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - label: PropTypes.string, - disabled: PropTypes.bool, - }) - ), - styles: PropTypes.shape(), - iconOn: PropTypes.string, - iconOff: PropTypes.string, - }; - +export class RadioGroup extends Component { static defaultProps = { choices: [], onChange() {}, @@ -42,7 +48,7 @@ export class RadioGroup extends Component { iconOff: 'radiobox-blank', }; - static getDerivedStateFromProps(props, state) { + static getDerivedStateFromProps(props: any, state: any) { if (props.value !== undefined && props.value !== state.value) { return { value: props.value, @@ -56,7 +62,7 @@ export class RadioGroup extends Component { value: this.props.value || null, }; - handleChange = (field, value) => { + handleChange = (_field: any, value: any) => { // Bail out if value is the same if (this.state.value === value) return; diff --git a/src/Form/Select.stories.js b/src/Form/Select.stories.tsx similarity index 75% rename from src/Form/Select.stories.js rename to src/Form/Select.stories.tsx index 7e915e9..3d0680a 100644 --- a/src/Form/Select.stories.js +++ b/src/Form/Select.stories.tsx @@ -9,7 +9,10 @@ const defaultProps = { id: 'select', name: 'select', label: 'Example', - options: [{ id: 1, value: 'male', label: 'Male' }, { id: 1, value: 'female', label: 'Female' }], + options: [ + { id: 1, value: 'male', label: 'Male' }, + { id: 1, value: 'female', label: 'Female' }, + ], }; export const Basic = () => + diff --git a/src/Modal/Modal.example.js b/src/Modal/Modal.example.tsx similarity index 86% rename from src/Modal/Modal.example.js rename to src/Modal/Modal.example.tsx index bf78a58..883d9ee 100644 --- a/src/Modal/Modal.example.js +++ b/src/Modal/Modal.example.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import Modal from './Modal'; -import Button from '../Button'; -import Input from '../Form/Input'; +import { Modal } from './Modal'; +import { Button } from '../Button'; +import { Input } from '../Form/Input'; -export default class ModalDemo extends React.Component { +export default class ModalDemo extends React.Component { state = { isModalOpen: false, isModalTwoOpen: false, @@ -21,6 +21,7 @@ export default class ModalDemo extends React.Component { this.toggle(); setTimeout(() => { + // eslint-disable-next-line no-alert alert('Oh no! It has been canceled.'); }, 500); }; @@ -36,7 +37,7 @@ export default class ModalDemo extends React.Component { <> {body} - + diff --git a/src/Modal/Modal.noAutoFocusEx.js b/src/Modal/Modal.noAutoFocusEx.tsx similarity index 92% rename from src/Modal/Modal.noAutoFocusEx.js rename to src/Modal/Modal.noAutoFocusEx.tsx index df56002..d2b0d93 100644 --- a/src/Modal/Modal.noAutoFocusEx.js +++ b/src/Modal/Modal.noAutoFocusEx.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import Modal from './Modal'; -import Button from '../Button'; -import Input from '../Form/Input'; +import { Modal } from './Modal'; +import { Button } from '../Button'; +import { Input } from '../Form/Input'; -export default class ModalNoAutoFocusExample extends React.Component { +export default class ModalNoAutoFocusExample extends React.Component { state = { isModalOpen: false, isModalTwoOpen: false, @@ -21,6 +21,7 @@ export default class ModalNoAutoFocusExample extends React.Component { this.toggle(); setTimeout(() => { + // eslint-disable-next-line no-alert alert('Oh no! It has been canceled.'); }, 500); }; diff --git a/src/Modal/Modal.stories.js b/src/Modal/Modal.stories.tsx similarity index 87% rename from src/Modal/Modal.stories.js rename to src/Modal/Modal.stories.tsx index 46626d9..76d4e95 100644 --- a/src/Modal/Modal.stories.js +++ b/src/Modal/Modal.stories.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import Modal from './Modal'; +import { Modal } from './Modal'; import ModalExample from './Modal.example'; import ModalDropdownExample from './Modal.dropdownExample'; import ModalNoAutoFocusExample from './Modal.noAutoFocusEx'; @@ -31,3 +31,5 @@ export const LongContent = () => ( export const DropdownTrigger = () => ; export const NoAutoFocus = () => ; + +export const TitleElement = () => Hello world} />; diff --git a/src/Modal/Modal.js b/src/Modal/Modal.tsx similarity index 75% rename from src/Modal/Modal.js rename to src/Modal/Modal.tsx index 955ec38..669bae8 100644 --- a/src/Modal/Modal.js +++ b/src/Modal/Modal.tsx @@ -1,18 +1,30 @@ -import React, { createContext, useContext, useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; +import React, { createContext, useContext, useEffect, useState, FC } from 'react'; import { keyframes, css } from 'styled-components'; import * as animations from 'react-animations'; import { Transition } from 'react-transition-group'; import { FocusOn } from 'react-focus-on'; import Portal from '../Portal'; -import Flex from '../Flex'; -import Icon from '../Icon'; -import Button from '../Button'; +import { Flex } from '../Flex'; +import { Icon } from '../Icon'; import { createComponent, themeGet } from '../utils'; const ModalContext = createContext({}); -const getAnimation = name => keyframes`${animations[name]}`; +const getAnimation = (name: any) => keyframes`${animations[name]}`; + +export interface ModalProps { + open?: boolean; + showClose?: boolean; + closeOnBackdropClick?: boolean; + closeOnEscape?: boolean; + minWidth?: number; + maxWidth?: number; + animationIn?: string; + animationOut?: string; + animationDuration?: number; + onClose?: any; + title?: string | JSX.Element; +} const ModalContainer = createComponent({ name: 'ModalContainer', @@ -31,7 +43,7 @@ const ModalContainer = createComponent({ justify-content: center; `, }); -const Backdrop = createComponent({ +const Backdrop = createComponent<{ transitionState: any }>({ name: 'ModalBackdrop', style: ({ transitionState }) => css` position: fixed; @@ -57,7 +69,17 @@ const Backdrop = createComponent({ `, }); -const ModalContent = createComponent({ +interface ModalContentProps { + transitionState?: any; + closeOnBackdropClick?: boolean; + closeOnEscape?: boolean; + minWidth?: number; + maxWidth?: number; + animationIn?: string; + animationOut?: string; +} + +const ModalContent = createComponent({ name: 'ModalContent', style: ({ minWidth, maxWidth, transitionState, animationIn, animationOut, theme }) => css` position: relative; @@ -84,16 +106,24 @@ const ModalContent = createComponent({ }); /** Modals are a great way to add dialogs to your site for lightboxes, user notifications, or completely custom content. */ -export function Modal({ children, title, animationDuration, showClose, onClose, open, ...props }) { +export const Modal: FC & { Title?: any; Header?: any; Body?: any; Footer?: any } = ({ + children, + title, + animationDuration, + showClose, + onClose, + open, + ...props +}) => { const [isOpen, setOpen] = useState(open); - const modalRef = React.useRef(null); + const modalRef = React.useRef(null); const handleClose = () => { setOpen(false); onClose(); }; - const handleContentClick = event => event.stopPropagation(); + const handleContentClick = (event: any) => event.stopPropagation(); const handleBackdropClick = () => { if (!props.closeOnBackdropClick) return; @@ -111,12 +141,19 @@ export function Modal({ children, title, animationDuration, showClose, onClose, if (open !== isOpen) { setOpen(open); } - }, [open]); + }, [open, isOpen]); return ( - + {}}> {state => ( @@ -137,19 +174,6 @@ export function Modal({ children, title, animationDuration, showClose, onClose, ); -} - -Modal.propTypes = { - open: PropTypes.bool, - showClose: PropTypes.bool, - closeOnBackdropClick: PropTypes.bool, - closeOnEscape: PropTypes.bool, - minWidth: PropTypes.number, - maxWidth: PropTypes.number, - animationIn: PropTypes.string, - animationOut: PropTypes.string, - animationDuration: PropTypes.number, - onClose: PropTypes.func, }; Modal.defaultProps = { @@ -191,10 +215,11 @@ const ModalHeaderInner = createComponent({ `, }); -Modal.Header = ({ title, children, showClose = true }) => { - const { handleClose } = useContext(ModalContext); +Modal.Header = ({ title, children, showClose = true }: any) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { handleClose } = useContext(ModalContext); - const handleKeyDown = ({ keyCode }) => { + const handleKeyDown = ({ keyCode }: any) => { if (keyCode === 13) { handleClose(); } @@ -214,7 +239,7 @@ Modal.Header = ({ title, children, showClose = true }) => { onKeyDown={handleKeyDown} aria-label="Close Modal" role="button" - tabIndex="-1" + tabIndex={-1} name="close" color="greyDarkest" size={24} @@ -243,5 +268,3 @@ Modal.Footer = createComponent({ border-bottom-right-radius: ${themeGet('radius')}px; `, }); - -export default Modal; diff --git a/src/Modal/index.js b/src/Modal/index.js deleted file mode 100644 index 8144af5..0000000 --- a/src/Modal/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import Modal from './Modal'; - -export default Modal; diff --git a/src/Modal/index.ts b/src/Modal/index.ts new file mode 100644 index 0000000..cb89ee1 --- /dev/null +++ b/src/Modal/index.ts @@ -0,0 +1 @@ +export * from './Modal'; diff --git a/src/Placeholder/Placeholder.stories.js b/src/Placeholder/Placeholder.stories.tsx similarity index 91% rename from src/Placeholder/Placeholder.stories.js rename to src/Placeholder/Placeholder.stories.tsx index 9400bd3..5afc090 100644 --- a/src/Placeholder/Placeholder.stories.js +++ b/src/Placeholder/Placeholder.stories.tsx @@ -10,7 +10,7 @@ export const Basic = () => ; export const WithDelay = () => ( - Hey, I'm loaded asynchronously. + Hey, I'm loaded asynchronously. ); diff --git a/src/Placeholder/Placeholder.js b/src/Placeholder/Placeholder.tsx similarity index 78% rename from src/Placeholder/Placeholder.js rename to src/Placeholder/Placeholder.tsx index 638d29c..0749941 100644 --- a/src/Placeholder/Placeholder.js +++ b/src/Placeholder/Placeholder.tsx @@ -1,12 +1,11 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { css } from 'styled-components'; -import Box from '../Box'; -import Button from '../Button'; +import { Box } from '../Box'; +import { Button } from '../Button'; import Spinner from '../Spinner'; import { createComponent } from '../utils'; -const Container = createComponent({ +const Container = createComponent({ name: 'Placeholder', style: css` display: flex; @@ -19,22 +18,24 @@ const Container = createComponent({ `, }); -/** Placeholder shows a spinner after a specified delay while content is loaded asynchronously. */ -export default class Placeholder extends React.Component { - static propTypes = { - loading: PropTypes.bool, - error: PropTypes.string, - delay: PropTypes.number, - renderLoading: PropTypes.func, - renderError: PropTypes.func, - onReload: PropTypes.func, - }; +interface PlaceholderProps { + loading?: boolean; + error?: string; + delay?: number; + renderLoading?: any; + renderError?: any; + onReload?: any; +} +/** Placeholder shows a spinner after a specified delay while content is loaded asynchronously. */ +export default class Placeholder extends React.Component { static defaultProps = { loading: false, delay: 250, }; + delayTimer: number; + state = { delayed: false, }; @@ -48,7 +49,7 @@ export default class Placeholder extends React.Component { } runDelay() { - if (this.props.delay <= 0) return; + if (this.props.delay !== undefined && this.props.delay <= 0) return; this.setState({ delayed: true }, () => { this.delayTimer = setTimeout(() => { diff --git a/src/Placeholder/index.js b/src/Placeholder/index.ts similarity index 100% rename from src/Placeholder/index.js rename to src/Placeholder/index.ts diff --git a/src/Portal/Portal.js b/src/Portal/Portal.js deleted file mode 100644 index e69de29..0000000 diff --git a/src/Portal/index.js b/src/Portal/index.ts similarity index 100% rename from src/Portal/index.js rename to src/Portal/index.ts diff --git a/src/Spinner/Spinner.js b/src/Spinner/Spinner.tsx similarity index 84% rename from src/Spinner/Spinner.js rename to src/Spinner/Spinner.tsx index 76d04bf..ed96ff5 100644 --- a/src/Spinner/Spinner.js +++ b/src/Spinner/Spinner.tsx @@ -7,7 +7,11 @@ const spin = keyframes` from { transform: rotate(360deg); } `; -const Spinner = createComponent({ +export interface SpinnerProps { + size?: number; +} + +const Spinner = createComponent({ name: 'Spinner', style: ({ size = 15 }) => css` height: ${size}px; diff --git a/src/Spinner/index.js b/src/Spinner/index.ts similarity index 100% rename from src/Spinner/index.js rename to src/Spinner/index.ts diff --git a/src/Tabs/Tabs.spec.js b/src/Tabs/Tabs.spec.tsx similarity index 80% rename from src/Tabs/Tabs.spec.js rename to src/Tabs/Tabs.spec.tsx index 6072882..cc6ff77 100644 --- a/src/Tabs/Tabs.spec.js +++ b/src/Tabs/Tabs.spec.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { renderWithTheme } from '../../test/utils'; +import { renderWithTheme } from '../test/utils'; import Tabs from './Tabs'; -const TabContent = ({ body }) =>
This is the content for Tab {body}
; +const TabContent = ({ body }: any) =>
This is the content for Tab {body}
; const onActiveSpy = jest.fn(); const TABS = [ { diff --git a/src/Tabs/Tabs.stories.js b/src/Tabs/Tabs.stories.tsx similarity index 100% rename from src/Tabs/Tabs.stories.js rename to src/Tabs/Tabs.stories.tsx diff --git a/src/Tabs/Tabs.js b/src/Tabs/Tabs.tsx similarity index 88% rename from src/Tabs/Tabs.js rename to src/Tabs/Tabs.tsx index 5493559..4537ff9 100644 --- a/src/Tabs/Tabs.js +++ b/src/Tabs/Tabs.tsx @@ -1,9 +1,8 @@ import React, { Component } from 'react'; import { css } from 'styled-components'; -import PropTypes from 'prop-types'; import { createComponent } from '../utils'; -const TabsProvider = createComponent({ +const TabsProvider = createComponent({ name: 'Tabs', style: ({ vertical }) => css` display: flex; @@ -11,7 +10,7 @@ const TabsProvider = createComponent({ `, }); -const TabList = createComponent({ +const TabList = createComponent({ name: 'TabList', tag: 'ul', style: ({ vertical, theme }) => css` @@ -27,7 +26,7 @@ const TabList = createComponent({ `}; `, }); -const TabListItem = createComponent({ +const TabListItem = createComponent({ name: 'TabListItem', tag: 'li', style: () => css` @@ -37,7 +36,7 @@ const TabListItem = createComponent({ `, }); -const Tab = createComponent({ +const Tab = createComponent({ name: 'Tab', tag: 'button', style: ({ vertical, disabled }) => css` @@ -64,7 +63,7 @@ const Tab = createComponent({ `, }); -const TabTitle = createComponent({ +const TabTitle = createComponent({ name: 'TabTitle', tag: 'span', style: ({ active, vertical, theme }) => css` @@ -88,14 +87,14 @@ const TabContent = createComponent({ `, }); -class Tabs extends Component { - static propTypes = { - tabs: PropTypes.arrayOf(PropTypes.shape()).isRequired, - active: PropTypes.number, - vertical: PropTypes.bool, - onChange: PropTypes.func, - }; +export interface TabsProps { + tabs: any[]; + active?: number; + vertical?: boolean; + onChange?: any; +} +class Tabs extends Component { static defaultProps = { vertical: false, }; @@ -106,13 +105,14 @@ class Tabs extends Component { componentDidUpdate() { if (this.props.active && this.props.active !== this.state.active) { + // eslint-disable-next-line react/no-did-update-set-state this.setState({ active: this.props.active, }); } } - handleTabClick = selected => { + handleTabClick = (selected: any) => { if (this.props.onChange) { this.props.onChange(selected); } diff --git a/src/Tabs/__snapshots__/Tabs.spec.js.snap b/src/Tabs/__snapshots__/Tabs.spec.tsx.snap similarity index 100% rename from src/Tabs/__snapshots__/Tabs.spec.js.snap rename to src/Tabs/__snapshots__/Tabs.spec.tsx.snap diff --git a/src/Tabs/index.js b/src/Tabs/index.ts similarity index 100% rename from src/Tabs/index.js rename to src/Tabs/index.ts diff --git a/src/Text/Text.js b/src/Text/Text.js deleted file mode 100644 index 4f1ef3d..0000000 --- a/src/Text/Text.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import { compose, space, color, typography } from 'styled-system'; -import propTypes from '@styled-system/prop-types'; -import { createComponent } from '../utils'; - -const StyledText = createComponent({ - name: 'Text', - tag: 'span', - style: compose( - space, - color, - typography - ), -}); - -const Text = React.forwardRef((props, ref) => ); - -Text.propTypes = { - ...propTypes.typography, - ...propTypes.color, - ...propTypes.space, -}; - -export default Text; diff --git a/src/Text/Text.spec.js b/src/Text/Text.spec.tsx similarity index 86% rename from src/Text/Text.spec.js rename to src/Text/Text.spec.tsx index 8ae522c..4cfa525 100644 --- a/src/Text/Text.spec.js +++ b/src/Text/Text.spec.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { renderWithTheme } from '../../test/utils'; -import Text from './Text'; +import { renderWithTheme } from '../test/utils'; +import { Text } from './Text'; describe('Text', () => { test('default to ', () => { diff --git a/src/Text/Text.stories.js b/src/Text/Text.stories.tsx similarity index 91% rename from src/Text/Text.stories.js rename to src/Text/Text.stories.tsx index 5e94288..4f9c80e 100644 --- a/src/Text/Text.stories.js +++ b/src/Text/Text.stories.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import Text from './Text'; +import { Text } from './Text'; export default { title: 'Components|Text', diff --git a/src/Text/Text.tsx b/src/Text/Text.tsx new file mode 100644 index 0000000..9cbbe6d --- /dev/null +++ b/src/Text/Text.tsx @@ -0,0 +1,10 @@ +import { compose, space, color, typography, SpaceProps, ColorProps, TypographyProps } from 'styled-system'; +import { createComponent } from '../utils'; + +export interface TextProps extends SpaceProps, ColorProps, TypographyProps {} + +export const Text = createComponent({ + name: 'Text', + tag: 'span', + style: compose(space, color, typography), +}); diff --git a/src/Text/__snapshots__/Text.spec.js.snap b/src/Text/__snapshots__/Text.spec.tsx.snap similarity index 100% rename from src/Text/__snapshots__/Text.spec.js.snap rename to src/Text/__snapshots__/Text.spec.tsx.snap diff --git a/src/Text/index.js b/src/Text/index.js deleted file mode 100644 index ddd9726..0000000 --- a/src/Text/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import Text from './Text'; - -export default Text; diff --git a/src/Text/index.ts b/src/Text/index.ts new file mode 100644 index 0000000..92e95d7 --- /dev/null +++ b/src/Text/index.ts @@ -0,0 +1,3 @@ +import { Text } from './Text'; + +export default Text; diff --git a/src/ThemeProvider/ThemeProvider.js b/src/ThemeProvider/ThemeProvider.js deleted file mode 100644 index d349033..0000000 --- a/src/ThemeProvider/ThemeProvider.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { ThemeProvider as SCThemeProvider } from 'styled-components'; -import merge from 'lodash/merge'; -import { createTheme } from '../createTheme'; - -const ThemeProvider = props => { - const theme = merge(createTheme(props.theme), props.theme); - - return {props.children}; -}; - -export default ThemeProvider; diff --git a/src/ThemeProvider/ThemeProvider.tsx b/src/ThemeProvider/ThemeProvider.tsx new file mode 100644 index 0000000..03631e2 --- /dev/null +++ b/src/ThemeProvider/ThemeProvider.tsx @@ -0,0 +1,14 @@ +import React, { FC } from 'react'; +import { ThemeProvider as SCThemeProvider, DefaultTheme } from 'styled-components'; +import merge from 'lodash/merge'; +import { createTheme } from '../createTheme'; + +interface ThemeProviderProps { + theme?: Partial; +} + +export const ThemeProvider: FC = props => { + const theme = merge(createTheme(props.theme), props.theme); + + return {props.children}; +}; diff --git a/src/ThemeProvider/index.js b/src/ThemeProvider/index.js deleted file mode 100644 index c64af38..0000000 --- a/src/ThemeProvider/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import ThemeProvider from './ThemeProvider'; - -export default ThemeProvider; diff --git a/src/ThemeProvider/index.ts b/src/ThemeProvider/index.ts new file mode 100644 index 0000000..8abd195 --- /dev/null +++ b/src/ThemeProvider/index.ts @@ -0,0 +1 @@ +export * from './ThemeProvider'; diff --git a/src/Toast/Toast.stories.js b/src/Toast/Toast.stories.tsx similarity index 97% rename from src/Toast/Toast.stories.js rename to src/Toast/Toast.stories.tsx index 1a69823..8fabf45 100644 --- a/src/Toast/Toast.stories.js +++ b/src/Toast/Toast.stories.tsx @@ -1,7 +1,7 @@ import React from 'react'; import ToastContainer from './ToastContainer'; import toast from './toast'; -import Button from '../Button'; +import { Button } from '../Button'; export default { title: 'Components|Toast', diff --git a/src/Toast/ToastContainer.js b/src/Toast/ToastContainer.tsx similarity index 79% rename from src/Toast/ToastContainer.js rename to src/Toast/ToastContainer.tsx index 38f75ff..29ab790 100644 --- a/src/Toast/ToastContainer.js +++ b/src/Toast/ToastContainer.tsx @@ -1,14 +1,13 @@ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { Transition, TransitionGroup } from 'react-transition-group'; import * as animations from 'react-animations'; import { css, keyframes } from 'styled-components'; import Portal from '../Portal'; -import Flex from '../Flex'; -import Box from '../Box'; -import Icon from '../Icon'; +import { Flex } from '../Flex'; +import { Box } from '../Box'; +import { Icon } from '../Icon'; import { emitter } from './toast'; -import { createComponent, themeGet } from '../utils'; +import { createComponent } from '../utils'; import { Types, Events, Positions, PositionConfigs } from './config'; const VariantToColorMap = { @@ -18,7 +17,7 @@ const VariantToColorMap = { info: 'blue', }; -const getTransitionStyle = (state, position, duration) => { +const getTransitionStyle = (state: any, position: any, duration: any) => { const { animationIn, animationOut } = PositionConfigs[position]; switch (state) { @@ -37,12 +36,12 @@ const getTransitionStyle = (state, position, duration) => { } }; -const ToastPortal = createComponent({ +const ToastPortal = createComponent({ name: 'ToastPortal', style: ({ position }) => PositionConfigs[position].wrapperStyle, }); -const Toast = createComponent({ +const Toast = createComponent({ name: 'Toast', style: ({ state, type, position, animationDuration, theme }) => css` padding: 0.75rem 1rem; @@ -69,24 +68,25 @@ const Toast = createComponent({ `, }); +interface ToastContainerProps { + type?: string; + position?: string; + timeout?: number; + animationDuration?: number; + autoClose?: boolean; + closeOnClick?: boolean; + showClose?: boolean; +} + /** Toast positions will default to `top-center`. To change the positioning, you can pass the `position` prop to the `` to be used as the default. You can also pass the position to each individual toast you're rendering, which will override the default.rendering. */ -export default class ToastContainer extends Component { +export default class ToastContainer extends Component { counter = 0; + state = { toasts: [], }; - static propTypes = { - type: PropTypes.string, - position: PropTypes.string, - timeout: PropTypes.number, - animationDuration: PropTypes.number, - autoClose: PropTypes.bool, - closeOnClick: PropTypes.bool, - showClose: PropTypes.bool, - }; - static defaultProps = { type: Types.INFO, position: Positions.TOP_CENTER, @@ -115,7 +115,7 @@ export default class ToastContainer extends Component { }; this.setState( - state => ({ + (state: any) => ({ ...state, toasts: [...state.toasts, toast], }), @@ -123,19 +123,19 @@ export default class ToastContainer extends Component { if (toast.autoClose) { setTimeout(() => { this.remove(id); - }, toast.timeout + toast.animationDuration); + }, (toast.timeout || 0) + (toast.animationDuration || 0)); } } ); }; - remove = id => { - this.setState(state => ({ - toasts: state.toasts.filter(t => t.id !== id), + remove = (id: any) => { + this.setState((state: any) => ({ + toasts: state.toasts.filter((t: any) => t.id !== id), })); }; - handleToastClick = toast => { + handleToastClick = (toast: any) => { if (toast.closeOnClick) { this.remove(toast.id); } @@ -151,8 +151,8 @@ export default class ToastContainer extends Component { {toasts - .filter(t => t.position === p) - .map(toast => ( + .filter((t: any) => t.position === p) + .map((toast: any) => ( {state => ( this.handleToastClick(toast)}> diff --git a/src/Toast/config.js b/src/Toast/config.ts similarity index 100% rename from src/Toast/config.js rename to src/Toast/config.ts diff --git a/src/Toast/index.js b/src/Toast/index.ts similarity index 100% rename from src/Toast/index.js rename to src/Toast/index.ts diff --git a/src/Toast/toast.js b/src/Toast/toast.tsx similarity index 59% rename from src/Toast/toast.js rename to src/Toast/toast.tsx index 47f63a1..9553167 100644 --- a/src/Toast/toast.js +++ b/src/Toast/toast.tsx @@ -1,9 +1,9 @@ import EventEmitter from 'mitt'; import { Events } from './config'; -export const emitter = new EventEmitter(); +export const emitter = new (EventEmitter as any)(); -const toast = (options = {}) => { +const toast = (options: any = {}) => { if (!options.message) { throw new Error('Molekule: Toast requires a message'); } @@ -11,19 +11,19 @@ const toast = (options = {}) => { emitter.emit(Events.ADD, options); }; -toast.success = (message, options = {}) => { +toast.success = (message: string, options = {}) => { toast({ message, type: 'success', ...options }); }; -toast.error = (message, options = {}) => { +toast.error = (message: string, options = {}) => { toast({ message, type: 'error', ...options }); }; -toast.warn = (message, options = {}) => { +toast.warn = (message: string, options = {}) => { toast({ message, type: 'warn', ...options }); }; -toast.info = (message, options = {}) => { +toast.info = (message: string, options = {}) => { toast({ message, type: 'info', ...options }); }; diff --git a/src/createTheme.spec.ts b/src/createTheme.spec.ts index aaeb1c2..345a7fe 100644 --- a/src/createTheme.spec.ts +++ b/src/createTheme.spec.ts @@ -7,7 +7,7 @@ describe('createTheme', () => { const theme = createTheme(); expect(theme.colors.blue).toEqual(defaultThemeColors.blue); - expect(theme.breakpoints.lg).toEqual(defaultBreakpoints.lg); + expect(theme.breakpoints.lg).toEqual(defaultBreakpoints[3]); }); it('overwrite', () => { @@ -20,6 +20,6 @@ describe('createTheme', () => { expect(theme.colors.primaryDark).toEqual('foo'); expect(theme.variants.Badge.primary.color).toEqual('foo'); - expect(theme.breakpoints.lg).toEqual(defaultBreakpoints.lg); + expect(theme.breakpoints.lg).toEqual(defaultBreakpoints[3]); }); }); diff --git a/src/createTheme.ts b/src/createTheme.ts index dd34588..631ea35 100644 --- a/src/createTheme.ts +++ b/src/createTheme.ts @@ -1,12 +1,13 @@ -import { DefaultTheme } from 'styled-components'; +import { Theme, ThemeBreakpoints } from 'types'; import { merge } from 'lodash'; +import { defaultBreakpoints } from './defaultBreakpoints'; import { defaultThemeColors } from './defaultThemeColors'; import { BadgeVariants } from './types/BadgeVariants'; import { defaultBadgeVariants } from './defaultBadgeVariants'; import { defaultTheme } from './defaultTheme'; import { ButtonVariants } from './types'; -export const createTheme = (customTheme?: Partial): DefaultTheme => { +export const createTheme = (customTheme?: Partial): Theme => { const colors = { ...defaultThemeColors, ...customTheme?.colors, @@ -141,13 +142,19 @@ export const createTheme = (customTheme?: Partial): DefaultTheme = }, }); + const breakpoints = (customTheme?.breakpoints || defaultBreakpoints) as string[] & ThemeBreakpoints; + /* eslint-disable prefer-destructuring */ + breakpoints.xs = breakpoints[0]; + breakpoints.sm = breakpoints[1]; + breakpoints.md = breakpoints[2]; + breakpoints.lg = breakpoints[3]; + breakpoints.xl = breakpoints[4]; + /* eslint-enable prefer-destructuring */ + return { ...defaultTheme, colors, - breakpoints: { - ...defaultTheme.breakpoints, - ...customTheme?.breakpoints, - }, + breakpoints, typography: { ...defaultTheme.typography, color: colors.black, diff --git a/src/defaultBreakpoints.ts b/src/defaultBreakpoints.ts index 3640e59..1bdc520 100644 --- a/src/defaultBreakpoints.ts +++ b/src/defaultBreakpoints.ts @@ -1,9 +1 @@ -// Note - Typing is generated by this constant - -export const defaultBreakpoints = { - xs: '400px', - sm: '600px', - md: '900px', - lg: '1200px', - xl: '1500px', -} as const; +export const defaultBreakpoints = ['400px', '600px', '900px', '1200px', '1500px'] as const; diff --git a/src/defaultTheme.ts b/src/defaultTheme.ts index 9115750..34e5933 100644 --- a/src/defaultTheme.ts +++ b/src/defaultTheme.ts @@ -1,15 +1,23 @@ -import { DefaultTheme } from 'styled-components'; +import { ThemeBreakpoints, Theme } from 'types'; import { defaultThemeColors } from './defaultThemeColors'; import { defaultBreakpoints } from './defaultBreakpoints'; import { defaultThemeSizes } from './defaultThemeSizes'; import { defaultThemeVariants } from './defaultThemeVariants'; -export const defaultTheme: DefaultTheme = { +const breakpoints = [...defaultBreakpoints] as string[] & ThemeBreakpoints; +/* eslint-disable prefer-destructuring */ +breakpoints.xs = breakpoints[0]; +breakpoints.sm = breakpoints[1]; +breakpoints.md = breakpoints[2]; +breakpoints.lg = breakpoints[3]; +breakpoints.xl = breakpoints[4]; + +export const defaultTheme: Theme = { classPrefix: 're', colors: defaultThemeColors, space: [0, 4, 8, 16, 24, 32, 64, 126, 256], - breakpoints: defaultBreakpoints, + breakpoints, gridWidth: 1200, gridGutter: 16, gridColumns: 12, diff --git a/src/hooks/index.js b/src/hooks/index.js deleted file mode 100644 index b89c046..0000000 --- a/src/hooks/index.js +++ /dev/null @@ -1 +0,0 @@ -export useKeyPress from './useKeyPress'; diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..483f578 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1 @@ +export * from './useKeyPress'; diff --git a/src/hooks/useKeyPress.js b/src/hooks/useKeyPress.ts similarity index 55% rename from src/hooks/useKeyPress.js rename to src/hooks/useKeyPress.ts index 0d41726..e1dbe6c 100644 --- a/src/hooks/useKeyPress.js +++ b/src/hooks/useKeyPress.ts @@ -1,10 +1,14 @@ import { useCallback, useEffect } from 'react'; -export default function useKeyPress(targetKeys, callback, keyEvent = 'keydown') { - const keys = [].concat(targetKeys); - +export const useKeyPress = ( + targetKeys: string | string[], + callback: (event: KeyboardEvent) => void, + keyEvent = 'keydown' +) => { const pressHandler = useCallback( - event => { + (event: KeyboardEvent) => { + const keys = Array.isArray(targetKeys) ? targetKeys : [targetKeys]; + if (keys.indexOf(event.key) >= 0) { callback(event); } @@ -15,5 +19,5 @@ export default function useKeyPress(targetKeys, callback, keyEvent = 'keydown') useEffect(() => { window.addEventListener(keyEvent, pressHandler); return () => window.removeEventListener(keyEvent, pressHandler); - }, [targetKeys, callback, keyEvent]); -} + }, [pressHandler, keyEvent]); +}; diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 83380ad..0000000 --- a/src/index.js +++ /dev/null @@ -1,43 +0,0 @@ -export Accordion from './Accordion'; -export Alert from './Alert'; -export Avatar from './Avatar'; -export Badge from './Badge'; -export Box from './Box'; -export Button from './Button'; -export Card from './Card'; -export Checkbox from './Form/Checkbox'; -export CheckboxGroup from './Form/CheckboxGroup'; -export Collapse from './Collapse'; -export Column from './Grid/Column'; -export Container from './Grid/Container'; -export DateInput from './Form/DateInput'; -export Dropdown from './Dropdown'; -export Field from './Form/Field'; -export Fieldset from './Form/Fieldset'; -export Flex from './Flex'; -export Form from './Form/Form'; -export Formbot from './Form/Formbot'; -export FormError from './Form/FormError'; -export FormGroup from './Form/FormGroup'; -export Icon from './Icon'; -export Input from './Form/Input'; -export Label from './Form/Label'; -export Linkify from './Linkify'; -export Modal from './Modal'; -export OrderedList from './Lists/OrderedList'; -export PhoneInput from './Form/PhoneInput'; -export Placeholder from './Placeholder'; -export Portal from './Portal'; -export RadioGroup from './Form/RadioGroup'; -export Row from './Grid/Row'; -export Select from './Form/Select'; -export Spinner from './Spinner'; -export Switch from './Form/Switch'; -export Tabs from './Tabs'; -export Text from './Text'; -export ThemeProvider from './ThemeProvider'; -export * from './Toast'; - -export { createTheme } from './createTheme'; -export * from './utils'; -export { default as styled, css, keyframes, createGlobalStyle } from 'styled-components'; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..d7b5a5d --- /dev/null +++ b/src/index.ts @@ -0,0 +1,41 @@ +export { default as Accordion } from './Accordion'; +export * from './Alert'; +export { default as Avatar } from './Avatar'; +export * from './Badge'; +export * from './Box'; +export * from './Button'; +export { default as Card } from './Card'; +export { default as Checkbox } from './Form/Checkbox'; +export { default as CheckboxGroup } from './Form/CheckboxGroup'; +export { default as Collapse } from './Collapse'; +export * from './Grid'; +export { default as DateInput } from './Form/DateInput'; +export { default as Dropdown } from './Dropdown'; +export * from './Form/Field'; +export * from './Form/Fieldset'; +export { default as Flex } from './Flex'; +export * from './Form/Form'; +export * from './Form/Formbot'; +export { FormError } from './Form/FormError'; +export * from './Form/FormGroup'; +export * from './Icon'; +export { Input } from './Form/Input'; +export { default as Label } from './Form/Label'; +export * from './Linkify'; +export * from './Modal'; +export * from './Lists/OrderedList'; +export { default as PhoneInput } from './Form/PhoneInput'; +export { default as Placeholder } from './Placeholder'; +export { default as Portal } from './Portal'; +export { default as RadioGroup } from './Form/RadioGroup'; +export { default as Select } from './Form/Select'; +export { default as Spinner } from './Spinner'; +export { default as Switch } from './Form/Switch'; +export { default as Tabs } from './Tabs'; +export { default as Text } from './Text'; +export * from './ThemeProvider'; +export * from './Toast'; + +export { createTheme } from './createTheme'; +export * from './utils'; +export { default as styled, css, keyframes, createGlobalStyle } from 'styled-components'; diff --git a/test/setup.js b/src/test/setup.ts similarity index 62% rename from test/setup.js rename to src/test/setup.ts index d3cf7e0..9a59f0b 100644 --- a/test/setup.js +++ b/src/test/setup.ts @@ -1,3 +1,4 @@ +/* eslint-disable import/no-extraneous-dependencies */ import '@babel/polyfill'; import 'jest-styled-components'; import 'jest-dom/extend-expect'; diff --git a/test/utils.js b/src/test/utils.tsx similarity index 62% rename from test/utils.js rename to src/test/utils.tsx index 9518f0b..c0ed87e 100644 --- a/test/utils.js +++ b/src/test/utils.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { render } from '@testing-library/react'; -import ThemeProvider from '../src/ThemeProvider'; +import { ThemeProvider } from '../ThemeProvider'; -export function renderWithTheme(component, options = {}) { +export function renderWithTheme(component: any, options = {}) { return render({component}, options); } diff --git a/src/types/DateFormatter.d.ts b/src/types/DateFormatter.d.ts new file mode 100644 index 0000000..23394ba --- /dev/null +++ b/src/types/DateFormatter.d.ts @@ -0,0 +1 @@ +declare module 'cleave.js/src/shortcuts/DateFormatter'; diff --git a/src/types/Theme.ts b/src/types/Theme.ts new file mode 100644 index 0000000..077b2cf --- /dev/null +++ b/src/types/Theme.ts @@ -0,0 +1,20 @@ +import { ThemeColors, ThemeBreakpoints, ThemeTypography, ThemeSizes, ThemeVariants } from 'types'; + +export interface Theme { + classPrefix: string; + space: number[]; + gridWidth: number; + gridGutter: number; + gridColumns: number; + radii: number[]; + radius: number; + shadow: { + soft: string; + hard: string; + }; + colors: ThemeColors; + breakpoints: string[] & ThemeBreakpoints; + typography: ThemeTypography; + sizes: ThemeSizes; + variants: ThemeVariants; +} diff --git a/src/types/ThemeBreakpoints.ts b/src/types/ThemeBreakpoints.ts index 0a397fd..557d8bc 100644 --- a/src/types/ThemeBreakpoints.ts +++ b/src/types/ThemeBreakpoints.ts @@ -1,7 +1,7 @@ -import { defaultBreakpoints } from '../defaultBreakpoints'; - -type Transform = { - [P in keyof T]: string; -}; - -export interface ThemeBreakpoints extends Transform {} +export interface ThemeBreakpoints { + xs: string; + sm: string; + md: string; + lg: string; + xl: string; +} diff --git a/src/types/ThemeColors.ts b/src/types/ThemeColors.ts index a24b500..fb3a43b 100644 --- a/src/types/ThemeColors.ts +++ b/src/types/ThemeColors.ts @@ -1,4 +1,4 @@ -import { defaultThemeColors } from 'src/defaultThemeColors'; +import { defaultThemeColors } from 'defaultThemeColors'; type Transform = { [P in keyof T]: string; diff --git a/src/types/ThemeSize.ts b/src/types/ThemeSize.ts index 12dfa83..cd355c7 100644 --- a/src/types/ThemeSize.ts +++ b/src/types/ThemeSize.ts @@ -1,8 +1,7 @@ -import { defaultBreakpoints } from 'src/defaultBreakpoints'; import { ExtendedCSSProperties } from './ExtendedCSSProperties'; -type Transform = { - [P in keyof T]: ExtendedCSSProperties; -}; - -export interface ThemeSize extends Partial> {} +export interface ThemeSize { + sm?: ExtendedCSSProperties; + md?: ExtendedCSSProperties; + lg?: ExtendedCSSProperties; +} diff --git a/src/types/index.ts b/src/types/index.ts index 749e940..5c799a3 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -5,3 +5,4 @@ export * from './ThemeSizes'; export * from './ThemeTypography'; export * from './BadgeVariants'; export * from './ButtonVariants'; +export * from './Theme'; diff --git a/src/utils.ts b/src/utils.ts index 5619bd6..46fc925 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,6 @@ import kebabCase from 'lodash/kebabCase'; import get from 'lodash/get'; -import styled from 'styled-components'; +import styled, { CSSProp, StyledProps } from 'styled-components'; export const themeGet = (lookup: any, fallback?: any) => ({ theme }: any = {}) => get(theme, lookup, fallback); @@ -13,7 +13,7 @@ export const getComponentVariant = (theme: any, componentName: string, variant: return config; }; -export const getComponentSize = (theme: any, componentName: string, size: string) => +export const getComponentSize = (theme: any, componentName?: string, size?: string) => themeGet(`sizes.${componentName}.${size}`, {})({ theme }); export const getComponentStyle = (componentName: string) => themeGet(`styles.${componentName}`, {}); @@ -34,21 +34,24 @@ const getComponentClassName = ( name: string ) => `${className || ''} ${classPrefix}-${name} ${variant ? `${classPrefix}-${name}-${variant}` : ''}`.trim(); -interface CreateComponentProps { +interface CreateComponentProps { name: string; - tag?: keyof JSX.IntrinsicElements; - as?: React.ComponentType; - style?: any; - props?: (props: any) => any; + tag?: keyof JSX.IntrinsicElements | React.JSXElementConstructor; + as?: React.ComponentType | keyof JSX.IntrinsicElements | React.JSXElementConstructor; + style?: ((props: StyledProps) => CSSProp) | CSSProp; + props?: (props: T) => any; } -export const createComponent = >({ +export const createComponent = < + T extends object, + O extends keyof JSX.IntrinsicElements | React.ComponentType = 'div' +>({ name, tag = 'div', as, style, props: getBaseProps = () => ({}), -}: CreateComponentProps) => { +}: CreateComponentProps) => { const component = styled((as || tag) as any); return component.attrs((props: any) => { diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..929bea6 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "jsx": "react", + }, + "exclude": [ + "node_modules", + "**/*.spec.ts", + "**/*.stories.tsx", + ] +} diff --git a/tsconfig.json b/tsconfig.json index df4d147..cfe6a04 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,12 +2,13 @@ "compilerOptions": { "module": "esnext", "target": "es5", + "outDir": "./lib", "lib": [ "esnext", "dom.iterable", "dom" ], - "baseUrl": ".", + "baseUrl": "src", "declaration": true, "declarationDir": "./lib", "sourceMap": true, diff --git a/yarn.lock b/yarn.lock index 994c74a..176acac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2597,6 +2597,13 @@ dependencies: "@types/react" "*" +"@types/react-portal@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/react-portal/-/react-portal-4.0.2.tgz#57a7f4c8ad48097c5a2d0cbbd09187831b91afdf" + integrity sha512-8tOaQHURcZ9j5lg9laFRu5/7+ol71WvVs10VXuIp7IuoIwR2iXQB8+BOEASMRgc/+L1omgANCy+WyXDTmc1/iQ== + dependencies: + "@types/react" "*" + "@types/react-syntax-highlighter@11.0.4": version "11.0.4" resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.4.tgz#d86d17697db62f98046874f62fdb3e53a0bbc4cd" @@ -2611,6 +2618,13 @@ dependencies: "@types/react" "*" +"@types/react-transition-group@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.0.tgz#882839db465df1320e4753e6e9f70ca7e9b4d46d" + integrity sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w== + dependencies: + "@types/react" "*" + "@types/react@*": version "16.9.35" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.35.tgz#a0830d172e8aadd9bd41709ba2281a3124bbd368" @@ -2639,6 +2653,13 @@ "@types/react-native" "*" csstype "^2.2.0" +"@types/styled-system@^5.1.10": + version "5.1.10" + resolved "https://registry.yarnpkg.com/@types/styled-system/-/styled-system-5.1.10.tgz#dcf5690dd837ca49b8de1f23cb99d510c7f4ecb3" + integrity sha512-OmVjC9OzyUckAgdavJBc+t5oCJrNXTlzWl9vo2x47leqpX1REq2qJC49SEtzbu1OnWSzcD68Uq3Aj8TeX+Kvtg== + dependencies: + csstype "^3.0.2" + "@types/tapable@*", "@types/tapable@^1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.5.tgz#9adbc12950582aa65ead76bffdf39fe0c27a3c02" @@ -2724,6 +2745,11 @@ dependencies: "@types/yargs-parser" "*" +"@types/yup@^0.29.4": + version "0.29.4" + resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.29.4.tgz#f7b1f9978180d5155663c1cd0ecdc41a72c23d81" + integrity sha512-OQ7gZRQb7eSbGu5h57tbK67sgX8UH5wbuqPORTFBG7qiBtOkEf1dXAr0QULyHIeRwaGLPYxPXiQru+40ClR6ng== + "@typescript-eslint/experimental-utils@2.32.0": version "2.32.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.32.0.tgz#bee7fbe1d21d13a273066d70abc82549d0b7943e" @@ -4806,10 +4832,6 @@ compare-func@^1.3.1: array-ify "^1.0.0" dot-prop "^3.0.0" -compare-versions@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62" - integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA== compare-func@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/compare-func/-/compare-func-2.0.0.tgz#fb65e75edbddfd2e568554e8b5b05fff7a51fcb3" @@ -4818,6 +4840,11 @@ compare-func@^2.0.0: array-ify "^1.0.0" dot-prop "^5.1.0" +compare-versions@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62" + integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA== + component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -5417,6 +5444,11 @@ csstype@^2.2.0, csstype@^2.5.7, csstype@^2.6.7: resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.10.tgz#e63af50e66d7c266edb6b32909cfd0aabe03928b" integrity sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w== +csstype@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.2.tgz#ee5ff8f208c8cd613b389f7b222c9801ca62b3f7" + integrity sha512-ofovWglpqoqbfLNOTBNZLSbMuGrblAf1efvvArGKOZMBrIoJeu5UsAipQolkijtyQx5MtAzT/J9IHj/CEY1mJw== + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -14325,6 +14357,11 @@ tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.2.tgz#9c79d83272c9a7aaf166f73915c9667ecdde3cc9" integrity sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg== +tslib@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e" + integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ== + tsutils@^3.17.1: version "3.17.1" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"