feature(devui): convert to TypeScript (#629)

* stash

* and those

* stash

* stash

* stash

* stash

* tests

* fix errors

* revert

* stash

* fix lint

* prettier
This commit is contained in:
Nathan Bierema 2020-09-09 10:35:22 -04:00 committed by GitHub
parent f4405ac0f0
commit 727d753081
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
167 changed files with 1563 additions and 852 deletions

View File

@ -7,6 +7,3 @@ build
coverage
node_modules
__snapshots__
# Ignore until TS linting is configured in redux-devtools package
packages/redux-devtools/index.d.ts

View File

@ -31,6 +31,11 @@
"raw-loader": "^4.0.1",
"rimraf": "^3.0.2",
"style-loader": "^1.2.1",
"stylelint": "^13.6.1",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^20.0.0",
"stylelint-config-styled-components": "^0.1.1",
"stylelint-processor-styled-components": "^1.10.0",
"ts-jest": "^26.2.0",
"ts-node": "^9.0.0",
"typescript": "^3.9.7",
@ -40,14 +45,11 @@
},
"scripts": {
"lerna": "lerna",
"build": "lerna run prepare --since master --stream --sort -- --scripts-prepend-node-path",
"build:all": "lerna run build",
"publish": "lerna publish",
"canary": "lerna publish --canary preminor --npm-tag alpha",
"next": "lerna publish --bump prerelease --npm-tag next",
"lint": "eslint \"**/*.{js,jsx,ts,tsx}\" --cache",
"lint:fix": "eslint \"**/*.{js,jsx,ts,tsx}\" --fix --cache",
"lint:all": "eslint \"**/*.{js,jsx,ts,tsx}\"",
"lint:all": "lerna run lint",
"prettify": "prettier --write .",
"prettier:check": "prettier --check .",
"test": "jest --onlyChanged",

View File

@ -7,10 +7,6 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
**Note:** Version bump only for package d3-state-visualizer
## 1.3.3 (2020-08-14)
**Note:** Version bump only for package d3-state-visualizer

View File

@ -7,10 +7,6 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
**Note:** Version bump only for package d3-state-visualizer-tree-example
## 0.0.1 (2020-08-14)
**Note:** Version bump only for package d3-state-visualizer-tree-example

View File

@ -1,7 +1,8 @@
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-export-default-from"
]
"presets": [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript"
],
"plugins": ["@babel/plugin-proposal-class-properties"]
}

View File

@ -0,0 +1 @@
lib

View File

@ -0,0 +1,29 @@
module.exports = {
extends: '../../.eslintrc',
overrides: [
{
files: ['*.ts', '*.tsx'],
extends: '../../eslintrc.ts.react.base.json',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
},
{
files: ['tests/*.ts', 'tests/*.tsx'],
extends: '../../eslintrc.ts.react.jest.base.json',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tests/tsconfig.json'],
},
},
{
files: ['.storybook/tests/*.ts', '.storybook/tests/*.tsx'],
extends: '../../eslintrc.ts.react.jest.base.json',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./.storybook/tsconfig.json'],
},
},
],
};

View File

@ -1,7 +1,7 @@
import React from 'react';
import { Container } from '../src';
import { listSchemes, listThemes } from '../src/utils/theme';
import '../src/presets.js';
import '../src/presets';
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },

View File

@ -0,0 +1,4 @@
{
"extends": "../../../tsconfig.react.base.json",
"include": ["../src", "."]
}

View File

@ -7,10 +7,6 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
**Note:** Version bump only for package devui
# [1.0.0-5](https://github.com/reduxjs/redux-devtools/compare/devui@1.0.0-4...devui@1.0.0-5) (2020-08-14)
**Note:** Version bump only for package devui

View File

@ -0,0 +1,4 @@
module.exports = {
preset: 'ts-jest',
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
};

View File

@ -2,79 +2,69 @@
"name": "devui",
"version": "1.0.0-6",
"description": "Reusable React components for building DevTools monitors and apps.",
"homepage": "https://github.com/reduxjs/redux-devtools/tree/master/packages/devui",
"bugs": {
"url": "https://github.com/reduxjs/redux-devtools/issues"
},
"license": "MIT",
"author": "Mihail Diordiev <zalmoxisus@gmail.com> (https://github.com/zalmoxisus)",
"files": [
"lib",
"fonts"
],
"main": "lib/index.js",
"types": "lib/index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/reduxjs/redux-devtools.git"
},
"author": "Mihail Diordiev <zalmoxisus@gmail.com> (https://github.com/zalmoxisus)",
"license": "MIT",
"scripts": {
"start": "npm run storybook",
"build": "rimraf ./lib && babel ./src --out-dir ./lib --ignore tests,stories",
"lint:css": "stylelint \"./src/**/*.js\"",
"test:update": "npm run jest -- -u",
"test": "jest --no-cache",
"storybook": "start-storybook -p 6006 -s ./fonts",
"build-storybook": "build-storybook -s ./fonts",
"prepare": "npm run build",
"prepublishOnly": "npm run test && npm run build"
},
"bugs": {
"url": "https://github.com/reduxjs/redux-devtools/issues"
},
"homepage": "https://github.com/reduxjs/redux-devtools",
"devDependencies": {
"@babel/cli": "^7.10.5",
"@babel/core": "^7.11.1",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-proposal-decorators": "^7.10.5",
"@babel/plugin-proposal-export-default-from": "^7.10.4",
"@babel/plugin-transform-runtime": "^7.11.0",
"@babel/preset-env": "^7.11.0",
"@babel/preset-react": "^7.10.4",
"@storybook/addon-essentials": "^6.0.21",
"@storybook/react": "^6.0.21",
"babel-loader": "^8.1.0",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.3",
"enzyme-to-json": "^3.5.0",
"jest": "^26.2.2",
"jsdom": "^16.4.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-is": "^16.13.1",
"react-test-renderer": "^16.13.1",
"rimraf": "^3.0.2",
"stylelint": "^13.6.1",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^20.0.0",
"stylelint-config-styled-components": "^0.1.1",
"stylelint-processor-styled-components": "^1.10.0"
},
"peerDependencies": {
"react": "^16.3.0"
"build": "npm run build:types && npm run build:js",
"build:types": "tsc --emitDeclarationOnly",
"build:js": "babel src --out-dir lib --extensions \".ts,.tsx\" --source-maps inline",
"clean": "rimraf lib",
"test": "jest",
"lint": "eslint . --ext .ts,.tsx",
"lint:fix": "eslint . --ext .ts,.tsx --fix",
"lint:css": "stylelint \"./src/**/*.js\"",
"type-check": "tsc --noEmit",
"type-check:watch": "npm run type-check -- --watch",
"preversion": "npm run type-check && npm run lint && npm run test",
"prepublishOnly": "npm run clean && npm run build"
},
"dependencies": {
"base16": "^1.0.0",
"codemirror": "^5.56.0",
"color": "^3.1.2",
"hoist-non-react-statics": "^3.3.2",
"prop-types": "^15.7.2",
"react-icons": "^3.10.0",
"react-is": "^16.13.1",
"react-jsonschema-form": "^1.8.1",
"react-select": "^3.1.0",
"redux-devtools-themes": "^1.0.0",
"simple-element-resize-detector": "^1.3.0",
"styled-components": "^5.1.1"
},
"jest": {
"setupFilesAfterEnv": [
"<rootDir>/tests/setup.js"
]
"devDependencies": {
"@storybook/addon-essentials": "^6.0.21",
"@storybook/react": "^6.0.21",
"@types/codemirror": "^0.0.97",
"@types/enzyme": "^3.10.5",
"@types/enzyme-adapter-react-16": "^1.0.6",
"@types/react-jsonschema-form": "^1.7.4",
"@types/react-select": "^3.0.19",
"csstype": "^3.0.2",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.3",
"enzyme-to-json": "^3.5.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-is": "^16.13.1"
},
"main": "lib/index.js"
"peerDependencies": {
"react": "^16.3.0"
}
}

View File

@ -1,92 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import createStyledComponent from '../utils/createStyledComponent';
import * as styles from './styles';
import { commonStyle, tooltipStyle } from './styles/common';
const ButtonWrapper = createStyledComponent(styles, 'button');
const TooltipWrapper = createStyledComponent(tooltipStyle);
const CommonWrapper = createStyledComponent(commonStyle);
export default class Button extends Component {
shouldComponentUpdate(nextProps) {
return (
nextProps.children !== this.props.children ||
nextProps.disabled !== this.props.disabled ||
nextProps.mark !== this.props.mark ||
nextProps.size !== this.props.size ||
nextProps.primary !== this.props.primary ||
nextProps.tooltipPosition !== this.props.tooltipPosition ||
nextProps.title !== this.props.title
);
}
onMouseUp = (e) => {
e.target.blur();
};
render() {
const button = (
<ButtonWrapper
theme={this.props.theme}
aria-label={this.props.title}
primary={this.props.primary}
disabled={this.props.disabled}
onMouseUp={this.onMouseUp}
onClick={this.props.onClick}
type={this.props.type}
>
{this.props.children}
</ButtonWrapper>
);
const Wrapper = this.props.title ? TooltipWrapper : CommonWrapper;
return (
<Wrapper
theme={this.props.theme}
tooltipTitle={this.props.title}
tooltipPosition={this.props.tooltipPosition}
size={this.props.size}
mark={this.props.mark}
>
{button}
</Wrapper>
);
}
}
Button.propTypes = {
children: PropTypes.any.isRequired,
title: PropTypes.string,
tooltipPosition: PropTypes.oneOf([
'top',
'bottom',
'left',
'right',
'bottom-left',
'bottom-right',
'top-left',
'top-right',
]),
onClick: PropTypes.func,
type: PropTypes.string,
disabled: PropTypes.bool,
primary: PropTypes.bool,
size: PropTypes.oneOf(['big', 'normal', 'small']),
mark: PropTypes.oneOf([
false,
'base08',
'base09',
'base0A',
'base0B',
'base0C',
'base0D',
'base0E',
'base0F',
]),
theme: PropTypes.object,
};
Button.defaultProps = {
tooltipPosition: 'top',
};

View File

@ -1,7 +1,9 @@
import React from 'react';
import styled from 'styled-components';
import { MdFiberManualRecord } from 'react-icons/md';
import { Story } from '@storybook/react';
import Button from './';
import { ButtonProps } from './Button';
export default {
title: 'Button',
@ -16,7 +18,7 @@ const Container = styled.div`
align-items: center;
`;
const Template = (args) => (
const Template: Story<ButtonProps> = (args) => (
<Container>
<Button {...args} />
</Container>
@ -25,6 +27,7 @@ const Template = (args) => (
export const Default = Template.bind({});
Default.args = {
title: 'Hello Tooltip! \\a And from new line hello!',
tooltipPosition: 'top',
primary: true,
size: 'normal',
disabled: false,
@ -41,6 +44,7 @@ export const Mark = Template.bind({});
Mark.args = {
mark: 'base08',
title: 'Hello Tooltip',
tooltipPosition: 'top',
size: 'normal',
disabled: false,
children: <MdFiberManualRecord />,

View File

@ -0,0 +1,128 @@
import React, { Component, ReactNode } from 'react';
import PropTypes from 'prop-types';
import createStyledComponent from '../utils/createStyledComponent';
import * as styles from './styles';
import { commonStyle, tooltipStyle } from './styles/common';
import { Theme } from '../themes/default';
const ButtonWrapper = createStyledComponent(styles, 'button');
const TooltipWrapper = createStyledComponent(tooltipStyle);
const CommonWrapper = createStyledComponent(commonStyle);
export type TooltipPosition =
| 'top'
| 'bottom'
| 'left'
| 'right'
| 'bottom-left'
| 'bottom-right'
| 'top-left'
| 'top-right';
export type Size = 'big' | 'normal' | 'small';
export type Mark =
| 'base08'
| 'base09'
| 'base0A'
| 'base0B'
| 'base0C'
| 'base0D'
| 'base0E'
| 'base0F';
export interface ButtonProps {
children: ReactNode;
title?: string;
tooltipPosition: TooltipPosition;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
type?: 'button' | 'reset' | 'submit';
disabled?: boolean;
primary?: boolean;
size?: Size;
mark?: Mark | false;
theme?: Theme;
}
export default class Button extends Component<ButtonProps> {
shouldComponentUpdate(nextProps: ButtonProps) {
return (
nextProps.children !== this.props.children ||
nextProps.disabled !== this.props.disabled ||
nextProps.mark !== this.props.mark ||
nextProps.size !== this.props.size ||
nextProps.primary !== this.props.primary ||
nextProps.tooltipPosition !== this.props.tooltipPosition ||
nextProps.title !== this.props.title
);
}
onMouseUp: React.MouseEventHandler<HTMLButtonElement> = (e) => {
e.currentTarget.blur();
};
render() {
const button = (
<ButtonWrapper
theme={this.props.theme}
aria-label={this.props.title}
primary={this.props.primary}
disabled={this.props.disabled}
onMouseUp={this.onMouseUp}
onClick={this.props.onClick}
type={this.props.type}
>
{this.props.children}
</ButtonWrapper>
);
const Wrapper = this.props.title ? TooltipWrapper : CommonWrapper;
return (
<Wrapper
theme={this.props.theme}
tooltipTitle={this.props.title}
tooltipPosition={this.props.tooltipPosition}
size={this.props.size}
mark={this.props.mark}
>
{button}
</Wrapper>
);
}
static propTypes = {
children: PropTypes.any.isRequired,
title: PropTypes.string,
tooltipPosition: PropTypes.oneOf([
'top',
'bottom',
'left',
'right',
'bottom-left',
'bottom-right',
'top-left',
'top-right',
]),
onClick: PropTypes.func,
type: PropTypes.string,
disabled: PropTypes.bool,
primary: PropTypes.bool,
size: PropTypes.oneOf(['big', 'normal', 'small']),
mark: PropTypes.oneOf([
false,
'base08',
'base09',
'base0A',
'base0B',
'base0C',
'base0D',
'base0E',
'base0F',
]),
theme: PropTypes.object,
};
static defaultProps = {
tooltipPosition: 'top',
};
}

View File

@ -1,8 +1,10 @@
import { css } from 'styled-components';
import { css, ThemedStyledProps } from 'styled-components';
import { fadeIn } from '../../utils/animations';
import colorEffect from '../../utils/color';
import { Mark, Size, TooltipPosition } from '../Button';
import { Theme } from '../../themes/default';
const both = (tooltipPosition) => {
const both = (tooltipPosition: TooltipPosition) => {
switch (tooltipPosition) {
case 'bottom':
return `
@ -46,7 +48,7 @@ const both = (tooltipPosition) => {
}
};
const before = (tooltipPosition) => {
const before = (tooltipPosition: TooltipPosition) => {
switch (tooltipPosition) {
case 'bottom-left':
return `
@ -69,7 +71,7 @@ const before = (tooltipPosition) => {
}
};
const after = (tooltipPosition, color) => {
const after = (tooltipPosition: TooltipPosition, color: string) => {
switch (tooltipPosition) {
case 'bottom':
return `
@ -110,13 +112,13 @@ const after = (tooltipPosition, color) => {
}
};
const getDirection = (tooltipPosition) => {
const getDirection = (tooltipPosition: TooltipPosition) => {
return tooltipPosition.indexOf('-') > 0
? tooltipPosition.substring(0, tooltipPosition.indexOf('-'))
: tooltipPosition;
};
const getSize = (size) => {
const getSize = (size: Size | undefined) => {
switch (size) {
case 'big':
return 'min-height: 34px; padding: 2px 12px;';
@ -127,7 +129,16 @@ const getSize = (size) => {
}
};
export const commonStyle = ({ theme, mark, size }) => css`
interface CommonStyleProps {
size: Size | undefined;
mark: Mark | false | undefined;
}
export const commonStyle = ({
theme,
mark,
size,
}: ThemedStyledProps<CommonStyleProps, Theme>) => css`
display: inline-block;
position: relative;
flex-shrink: 0;
@ -164,13 +175,20 @@ export const commonStyle = ({ theme, mark, size }) => css`
}
`;
interface TooltipStyleProps {
tooltipTitle: string | undefined;
tooltipPosition: TooltipPosition;
size: Size | undefined;
mark: Mark | false | undefined;
}
export const tooltipStyle = ({
theme,
tooltipTitle,
tooltipPosition,
mark,
size,
}) => css`
}: ThemedStyledProps<TooltipStyleProps, Theme>) => css`
${commonStyle({ theme, mark, size })}
&:before {

View File

@ -1,6 +1,16 @@
import { css } from 'styled-components';
import { css, ThemedStyledProps } from 'styled-components';
import { Theme } from '../../themes/default';
export const style = ({ theme, primary, disabled }) => css`
export interface StyleProps {
primary: boolean | undefined;
disabled: boolean | undefined;
}
export const style = ({
theme,
primary,
disabled,
}: ThemedStyledProps<StyleProps, Theme>) => css`
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
outline: none;

View File

@ -1,7 +1,13 @@
import { css } from 'styled-components';
import { css, ThemedStyledProps } from 'styled-components';
import { ripple } from '../../utils/animations';
import { StyleProps } from './default';
import { Theme } from '../../themes/default';
export const style = ({ theme, primary, disabled }) => css`
export const style = ({
theme,
primary,
disabled,
}: ThemedStyledProps<StyleProps, Theme>) => css`
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
outline: none;

View File

@ -1,10 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ThemeProvider } from 'styled-components';
import { getTheme } from '../utils/theme';
import { getTheme, ThemeData } from '../utils/theme';
import { MainContainerWrapper, ContainerWrapper } from './styles';
import { Theme } from '../themes/default';
const Container = ({ themeData, className, theme, children }) => {
interface Props {
children?: React.ReactNode;
themeData?: ThemeData;
theme?: Theme;
className?: string;
}
const Container: React.FunctionComponent<Props> = ({
themeData,
className,
theme,
children,
}) => {
if (!themeData) {
return (
<ContainerWrapper className={className} theme={theme}>
@ -24,8 +37,8 @@ const Container = ({ themeData, className, theme, children }) => {
Container.propTypes = {
children: PropTypes.node,
themeData: PropTypes.object,
theme: PropTypes.object,
themeData: PropTypes.any,
theme: PropTypes.any,
className: PropTypes.string,
};

View File

@ -1,7 +1,9 @@
import React from 'react';
import styled from 'styled-components';
import { Story } from '@storybook/react';
import ContextMenu from './';
import { items } from './data';
import { ContextMenuProps } from './ContextMenu';
export default {
title: 'ContextMenu',
@ -16,7 +18,7 @@ const Container = styled.div`
align-items: center;
`;
const Template = (args) => (
const Template: Story<ContextMenuProps> = (args) => (
<Container>
<ContextMenu {...args} />
</Container>

View File

@ -5,13 +5,34 @@ import styles from './styles/index';
const ContextMenuWrapper = createStyledComponent(styles);
export default class ContextMenu extends Component {
constructor(props) {
type ReactButtonElement = React.ReactElement<
JSX.IntrinsicElements['button'],
'button'
>;
type Item = { name: string; value?: string } | ReactButtonElement;
function isReactButtonElement(item: Item): item is ReactButtonElement {
return (item as ReactButtonElement).type === 'button';
}
export interface ContextMenuProps {
items: Item[];
onClick: (value: string) => void;
x: number;
y: number;
visible?: boolean;
}
export default class ContextMenu extends Component<ContextMenuProps> {
constructor(props: ContextMenuProps) {
super(props);
this.updateItems(props.items);
}
UNSAFE_componentWillReceiveProps(nextProps) {
menu?: HTMLDivElement | null;
items?: React.ReactNode[];
UNSAFE_componentWillReceiveProps(nextProps: ContextMenuProps) {
if (
nextProps.items !== this.props.items ||
nextProps.visible !== this.props.visible
@ -24,25 +45,25 @@ export default class ContextMenu extends Component {
this.amendPosition();
}
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps: ContextMenuProps) {
if (prevProps.x !== this.props.x || prevProps.y !== this.props.y) {
this.amendPosition();
}
}
onMouseUp = (e) => {
e.target.blur();
onMouseUp: React.MouseEventHandler<HTMLButtonElement> = (e) => {
e.currentTarget.blur();
};
onClick = (e) => {
this.props.onClick(e.target.value);
onClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
this.props.onClick(e.currentTarget.value);
};
amendPosition() {
const { x, y } = this.props;
const { scrollTop, scrollLeft } = document.documentElement;
const { innerWidth, innerHeight } = window;
const rect = this.menu.getBoundingClientRect();
const rect = this.menu!.getBoundingClientRect();
let left = x + scrollLeft;
let top = y + scrollTop;
@ -59,14 +80,14 @@ export default class ContextMenu extends Component {
left = rect.width < innerWidth ? (innerWidth - rect.width) / 2 : 0;
}
this.menu.style.top = `${top}px`;
this.menu.style.left = `${left}px`;
this.menu!.style.top = `${top}px`;
this.menu!.style.left = `${left}px`;
}
updateItems(items) {
updateItems(items: Item[]) {
this.items = items.map((item) => {
if (isReactButtonElement(item)) return item;
const value = item.value || item.name;
if (item.type === 'button') return item;
return (
<button
key={value}
@ -80,7 +101,7 @@ export default class ContextMenu extends Component {
});
}
menuRef = (c) => {
menuRef: React.RefCallback<HTMLDivElement> = (c) => {
this.menu = c;
};
@ -96,12 +117,12 @@ export default class ContextMenu extends Component {
</ContextMenuWrapper>
);
}
}
ContextMenu.propTypes = {
static propTypes = {
items: PropTypes.array.isRequired,
onClick: PropTypes.func.isRequired,
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
visible: PropTypes.bool,
};
}

View File

@ -1 +0,0 @@
export default from './ContextMenu';

View File

@ -0,0 +1 @@
export { default } from './ContextMenu';

View File

@ -1,6 +1,18 @@
import { css } from 'styled-components';
import { css, ThemedStyledProps } from 'styled-components';
import { Theme } from '../../themes/default';
export default ({ theme, left, top, visible }) => css`
interface StyleProps {
left: number;
top: number;
visible: boolean | undefined;
}
export default ({
theme,
left,
top,
visible,
}: ThemedStyledProps<StyleProps, Theme>) => css`
${visible
? `
visibility: visible;

View File

@ -1,13 +1,18 @@
import React from 'react';
import { Story } from '@storybook/react';
import Dialog from './';
import { schema, uiSchema, formData } from '../Form/schema';
import { DialogProps } from './Dialog';
import { Props as FormProps } from '../Form/Form';
export default {
title: 'Dialog',
component: Dialog,
};
const Template = (args) => <Dialog {...args} />;
const Template: Story<DialogProps | (DialogProps & FormProps<unknown>)> = (
args
) => <Dialog {...args} />;
export const Default = Template.bind({});
Default.args = {

View File

@ -4,20 +4,60 @@ import createStyledComponent from '../utils/createStyledComponent';
import * as styles from './styles';
import Button from '../Button';
import Form from '../Form';
import { Theme } from '../themes/default';
import { Props as FormProps } from '../Form/Form';
const DialogWrapper = createStyledComponent(styles);
export default class Dialog extends (PureComponent || Component) {
export interface DialogProps {
open?: boolean;
title?: string;
children?: React.ReactNode;
actions?: React.ReactNode[];
submitText?: string;
fullWidth?: boolean;
noHeader?: boolean;
noFooter?: boolean;
modal?: boolean;
onDismiss: (
e: React.MouseEvent<HTMLButtonElement | HTMLDivElement> | false
) => void;
onSubmit: () => void;
theme?: Theme;
}
type Rest<P> = Omit<
DialogProps & FormProps<P>,
| 'modal'
| 'open'
| 'fullWidth'
| 'title'
| 'children'
| 'actions'
| 'noHeader'
| 'noFooter'
| 'submitText'
| 'onDismiss'
>;
function isForm<P>(rest?: FormProps<P>): rest is FormProps<P> {
return (rest as FormProps<P>).schema !== undefined;
}
export default class Dialog<P> extends (PureComponent || Component)<
DialogProps | (DialogProps & FormProps<P>)
> {
submitButton?: HTMLInputElement | null;
onSubmit = () => {
if (this.submitButton) this.submitButton.click();
else this.props.onSubmit();
};
getFormButtonRef = (node) => {
getFormButtonRef: React.RefCallback<HTMLInputElement> = (node) => {
this.submitButton = node;
};
onKeyDown = (e) => {
onKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (e) => {
if (e.keyCode === 27 /* esc */) {
e.preventDefault();
this.props.onDismiss(false);
@ -38,7 +78,7 @@ export default class Dialog extends (PureComponent || Component) {
onDismiss,
...rest
} = this.props;
const schema = rest.schema;
const schema = (rest as Rest<P>).schema;
return (
<DialogWrapper
@ -47,7 +87,7 @@ export default class Dialog extends (PureComponent || Component) {
onKeyDown={this.onKeyDown}
theme={rest.theme}
>
<div onClick={!modal && onDismiss} />
<div onClick={!modal ? onDismiss : undefined} />
<div>
{!noHeader && (
<div className="mc-dialog--header">
@ -57,8 +97,8 @@ export default class Dialog extends (PureComponent || Component) {
)}
<div className="mc-dialog--body">
{children}
{schema && (
<Form {...rest}>
{isForm(rest as FormProps<P>) && (
<Form {...(rest as FormProps<P>)}>
{!noFooter && (
<input
type="submit"
@ -97,9 +137,8 @@ export default class Dialog extends (PureComponent || Component) {
</DialogWrapper>
);
}
}
Dialog.propTypes = {
static propTypes = {
open: PropTypes.bool,
title: PropTypes.string,
children: PropTypes.any,
@ -113,3 +152,4 @@ Dialog.propTypes = {
onSubmit: PropTypes.func,
theme: PropTypes.object,
};
}

View File

@ -1,6 +1,16 @@
import { css } from 'styled-components';
import { css, ThemedStyledProps } from 'styled-components';
import { Theme } from '../../themes/default';
export const style = ({ theme, open, fullWidth }) => css`
export interface StyleProps {
open: boolean | undefined;
fullWidth: boolean | undefined;
}
export const style = ({
theme,
open,
fullWidth,
}: ThemedStyledProps<StyleProps, Theme>) => css`
position: fixed;
top: 0px;
right: 0px;

View File

@ -1,6 +1,12 @@
import { css } from 'styled-components';
import { css, ThemedStyledProps } from 'styled-components';
import { StyleProps } from './default';
import { Theme } from '../../themes/default';
export const style = ({ theme, open, fullWidth }) => css`
export const style = ({
theme,
open,
fullWidth,
}: ThemedStyledProps<StyleProps, Theme>) => css`
position: fixed;
top: 0px;
right: 0px;

View File

@ -1,91 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import CodeMirror from 'codemirror';
import { defaultStyle, themedStyle } from './styles';
const EditorContainer = styled.div('', ({ theme }) =>
theme.scheme === 'default' && theme.light ? defaultStyle : themedStyle(theme)
);
/**
* Based on [CodeMirror](http://codemirror.net/).
*/
export default class Editor extends Component {
componentDidMount() {
this.cm = CodeMirror(this.node, {
value: this.props.value,
mode: this.props.mode,
lineNumbers: this.props.lineNumbers,
lineWrapping: this.props.lineWrapping,
readOnly: this.props.readOnly,
autofocus: this.props.autofocus,
foldGutter: this.props.foldGutter,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
});
if (this.props.onChange) {
this.cm.on('change', (doc, change) => {
this.props.onChange(doc.getValue(), change);
});
}
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.value !== this.cm.getValue()) {
this.cm.setValue(nextProps.value);
}
if (nextProps.readOnly !== this.props.readOnly) {
this.cm.setOption('readOnly', nextProps.readOnly);
}
if (nextProps.lineNumbers !== this.props.lineNumbers) {
this.cm.setOption('lineNumbers', nextProps.lineNumbers);
}
if (nextProps.lineWrapping !== this.props.lineWrapping) {
this.cm.setOption('lineWrapping', nextProps.lineWrapping);
}
if (nextProps.foldGutter !== this.props.foldGutter) {
this.cm.setOption('foldGutter', nextProps.foldGutter);
}
}
shouldComponentUpdate() {
return false;
}
componentWillUnmount() {
const node = this.node;
node.removeChild(node.children[0]);
this.cm = null;
}
getRef = (node) => {
this.node = node;
};
render() {
return <EditorContainer ref={this.getRef} theme={this.props.theme} />;
}
}
Editor.propTypes = {
value: PropTypes.string,
mode: PropTypes.string,
lineNumbers: PropTypes.bool,
lineWrapping: PropTypes.bool,
readOnly: PropTypes.bool,
theme: PropTypes.object,
foldGutter: PropTypes.bool,
autofocus: PropTypes.bool,
onChange: PropTypes.func,
};
Editor.defaultProps = {
value: '',
mode: 'javascript',
lineNumbers: true,
lineWrapping: false,
readOnly: false,
foldGutter: true,
autofocus: false,
};

View File

@ -1,6 +1,8 @@
import React from 'react';
import { Story } from '@storybook/react';
import Editor from './';
import { default as WithTabsComponent } from './WithTabs';
import { default as WithTabsComponent, WithTabsProps } from './WithTabs';
import { EditorProps } from './Editor';
const value = `
var themes = [];
@ -15,7 +17,7 @@ export default {
component: Editor,
};
const Template = (args) => <Editor {...args} />;
const Template: Story<EditorProps> = (args) => <Editor {...args} />;
export const Default = Template.bind({});
Default.args = {
@ -33,12 +35,14 @@ Default.argTypes = {
onChange: { control: { disable: true } },
};
const WithTabsTemplate = (args) => <WithTabsComponent {...args} />;
const WithTabsTemplate: Story<WithTabsProps> = (args) => (
<WithTabsComponent {...args} />
);
export const WithTabs = WithTabsTemplate.bind({});
WithTabs.args = {
lineNumbers: true,
align: 'left',
position: 'left',
};
WithTabs.argTypes = {
value: { control: { disable: true } },

View File

@ -0,0 +1,111 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import CodeMirror from 'codemirror';
import { defaultStyle, themedStyle } from './styles';
import { Theme } from '../themes/default';
const EditorContainer = styled.div(
('' as unknown) as TemplateStringsArray,
({ theme }: { theme: Theme }) =>
theme.scheme === 'default' && theme.light
? defaultStyle
: themedStyle(theme)
);
export interface EditorProps {
value: string;
mode: string;
lineNumbers: boolean;
lineWrapping: boolean;
readOnly: boolean;
theme?: Theme;
foldGutter: boolean;
autofocus: boolean;
onChange: (value: string, change: CodeMirror.EditorChangeLinkedList) => void;
}
/**
* Based on [CodeMirror](http://codemirror.net/).
*/
export default class Editor extends Component<EditorProps> {
cm?: CodeMirror.Editor | null;
node?: HTMLDivElement | null;
componentDidMount() {
this.cm = CodeMirror(this.node!, {
value: this.props.value,
mode: this.props.mode,
lineNumbers: this.props.lineNumbers,
lineWrapping: this.props.lineWrapping,
readOnly: this.props.readOnly,
autofocus: this.props.autofocus,
foldGutter: this.props.foldGutter,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
});
if (this.props.onChange) {
this.cm.on('change', (doc, change) => {
this.props.onChange(doc.getValue(), change);
});
}
}
UNSAFE_componentWillReceiveProps(nextProps: EditorProps) {
if (nextProps.value !== this.cm!.getValue()) {
this.cm!.setValue(nextProps.value);
}
if (nextProps.readOnly !== this.props.readOnly) {
this.cm!.setOption('readOnly', nextProps.readOnly);
}
if (nextProps.lineNumbers !== this.props.lineNumbers) {
this.cm!.setOption('lineNumbers', nextProps.lineNumbers);
}
if (nextProps.lineWrapping !== this.props.lineWrapping) {
this.cm!.setOption('lineWrapping', nextProps.lineWrapping);
}
if (nextProps.foldGutter !== this.props.foldGutter) {
this.cm!.setOption('foldGutter', nextProps.foldGutter);
}
}
shouldComponentUpdate() {
return false;
}
componentWillUnmount() {
const node = this.node!;
node.removeChild(node.children[0]);
this.cm = null;
}
getRef: React.RefCallback<HTMLDivElement> = (node) => {
this.node = node;
};
render() {
return <EditorContainer ref={this.getRef} theme={this.props.theme} />;
}
static propTypes = {
value: PropTypes.string,
mode: PropTypes.string,
lineNumbers: PropTypes.bool,
lineWrapping: PropTypes.bool,
readOnly: PropTypes.bool,
theme: PropTypes.object,
foldGutter: PropTypes.bool,
autofocus: PropTypes.bool,
onChange: PropTypes.func,
};
static defaultProps = {
value: '',
mode: 'javascript',
lineNumbers: true,
lineWrapping: false,
readOnly: false,
foldGutter: true,
autofocus: false,
};
}

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React, { Component, ComponentType } from 'react';
import Editor from './';
import Tabs from '../Tabs';
@ -10,25 +10,35 @@ const value2 = `
const func2 = () => {}
`;
export interface WithTabsProps {
position: 'left' | 'right' | 'center';
lineNumbers: boolean;
}
interface TabProps {
value: string;
lineNumbers: boolean;
}
/* eslint-disable react/prop-types */
export default class WithTabs extends Component {
export default class WithTabs extends Component<WithTabsProps> {
state = {
selected: 'Function 1',
};
render() {
const { align, lineNumbers } = this.props;
const { position, lineNumbers } = this.props;
return (
<Tabs
<Tabs<TabProps>
tabs={[
{
name: 'Function 1',
component: Editor,
component: (Editor as unknown) as ComponentType<TabProps>,
selector: () => ({ value: value1, lineNumbers }),
},
{
name: 'Function 2',
component: Editor,
component: (Editor as unknown) as ComponentType<TabProps>,
selector: () => ({ value: value2, lineNumbers }),
},
]}
@ -36,7 +46,7 @@ export default class WithTabs extends Component {
onClick={(selected) => {
this.setState({ selected });
}}
align={align}
position={position}
/>
);
}

View File

@ -1,4 +1,5 @@
import { css } from 'styled-components';
import { Theme } from '../../themes/default';
export const defaultStyle = `
height: 100%;
@ -10,7 +11,7 @@ export const defaultStyle = `
}
`;
export const themedStyle = (theme) => css`
export const themedStyle = (theme: Theme) => css`
height: 100%;
> div {

View File

@ -1,13 +1,15 @@
import React from 'react';
import { Story } from '@storybook/react';
import Form from './';
import { schema, uiSchema, formData } from './schema';
import { Props as FormProps } from './Form';
export default {
title: 'Form',
component: Form,
};
const Template = (args) => <Form {...args} />;
const Template: Story<FormProps<unknown>> = (args) => <Form {...args} />;
export const Default = Template.bind({});
Default.args = {

View File

@ -1,17 +1,26 @@
import React, { PureComponent, Component } from 'react';
import PropTypes from 'prop-types';
import JSONSchemaForm from 'react-jsonschema-form';
import JSONSchemaForm, { FormProps } from 'react-jsonschema-form';
import createStyledComponent from '../utils/createStyledComponent';
import styles from './styles';
import Button from '../Button';
import customWidgets from './widgets';
import { Theme } from '../themes/default';
const FormContainer = createStyledComponent(styles, JSONSchemaForm);
export interface Props<T> extends FormProps<T> {
children?: React.ReactNode;
submitText?: string;
primaryButton?: boolean;
noSubmit?: boolean;
theme?: Theme;
}
/**
* Wrapper around [`react-jsonschema-form`](https://github.com/mozilla-services/react-jsonschema-form) with custom widgets.
*/
export default class Form extends (PureComponent || Component) {
export default class Form<T> extends (PureComponent || Component)<Props<T>> {
render() {
const {
widgets,
@ -22,7 +31,10 @@ export default class Form extends (PureComponent || Component) {
...rest
} = this.props;
return (
<FormContainer {...rest} widgets={{ ...customWidgets, ...widgets }}>
<FormContainer
{...(rest as Props<unknown>)}
widgets={{ ...customWidgets, ...widgets }}
>
{noSubmit ? (
<noscript />
) : (
@ -40,9 +52,8 @@ export default class Form extends (PureComponent || Component) {
</FormContainer>
);
}
}
Form.propTypes = {
static propTypes = {
children: PropTypes.any,
submitText: PropTypes.string,
primaryButton: PropTypes.bool,
@ -54,3 +65,4 @@ Form.propTypes = {
PropTypes.oneOfType([PropTypes.func, PropTypes.object])
),
};
}

View File

@ -1,89 +0,0 @@
module.exports = {
schema: {
title: 'Example form',
description: 'A simple form example.',
type: 'object',
required: ['name'],
properties: {
name: {
type: 'string',
title: 'Full name',
},
age: {
type: 'integer',
title: 'Age',
},
bio: {
type: 'string',
title: 'Bio',
},
password: {
type: 'string',
title: 'Password',
minLength: 3,
},
multipleChoicesList: {
type: 'array',
title: 'A multiple choices list',
items: {
type: 'string',
enum: ['foo', 'bar', 'fuzz'],
},
uniqueItems: true,
},
numberEnum: {
type: 'number',
title: 'Number enum',
enum: [1, 2, 3],
},
numberEnumRadio: {
type: 'number',
title: 'Number enum',
enum: [1, 2, 3],
},
integerRange: {
title: 'Integer range',
type: 'integer',
minimum: 42,
maximum: 100,
},
},
},
uiSchema: {
name: {
'ui:autofocus': true,
},
age: {
'ui:widget': 'updown',
},
bio: {
'ui:widget': 'textarea',
},
password: {
'ui:widget': 'password',
'ui:help': 'Hint: Make it strong!',
},
date: {
'ui:widget': 'alt-datetime',
},
multipleChoicesList: {
'ui:widget': 'checkboxes',
},
numberEnumRadio: {
'ui:widget': 'radio',
'ui:options': {
inline: true,
},
},
integerRange: {
'ui:widget': 'range',
},
},
formData: {
name: 'Chuck Norris',
age: 75,
bio: 'Roundhouse kicking asses since 1940',
password: 'noneed',
integerRange: 52,
},
};

View File

@ -0,0 +1,91 @@
import { JSONSchema6 } from 'json-schema';
export const schema: JSONSchema6 = {
title: 'Example form',
description: 'A simple form example.',
type: 'object',
required: ['name'],
properties: {
name: {
type: 'string',
title: 'Full name',
},
age: {
type: 'integer',
title: 'Age',
},
bio: {
type: 'string',
title: 'Bio',
},
password: {
type: 'string',
title: 'Password',
minLength: 3,
},
multipleChoicesList: {
type: 'array',
title: 'A multiple choices list',
items: {
type: 'string',
enum: ['foo', 'bar', 'fuzz'],
},
uniqueItems: true,
},
numberEnum: {
type: 'number',
title: 'Number enum',
enum: [1, 2, 3],
},
numberEnumRadio: {
type: 'number',
title: 'Number enum',
enum: [1, 2, 3],
},
integerRange: {
title: 'Integer range',
type: 'integer',
minimum: 42,
maximum: 100,
},
},
};
export const uiSchema = {
name: {
'ui:autofocus': true,
},
age: {
'ui:widget': 'updown',
},
bio: {
'ui:widget': 'textarea',
},
password: {
'ui:widget': 'password',
'ui:help': 'Hint: Make it strong!',
},
date: {
'ui:widget': 'alt-datetime',
},
multipleChoicesList: {
'ui:widget': 'checkboxes',
},
numberEnumRadio: {
'ui:widget': 'radio',
'ui:options': {
inline: true,
},
},
integerRange: {
'ui:widget': 'range',
},
};
export const formData = {
name: 'Chuck Norris',
age: 75,
bio: 'Roundhouse kicking asses since 1940',
password: 'noneed',
integerRange: 52,
};

View File

@ -1,6 +1,8 @@
import { css } from 'styled-components';
import { css, ThemedStyledProps } from 'styled-components';
import { Theme } from '../../themes/default';
export default ({ theme }) => css`
// eslint-disable-next-line @typescript-eslint/ban-types
export default ({ theme }: ThemedStyledProps<{}, Theme>) => css`
padding: 10px;
line-height: 1.846;
font-size: 14px;

View File

@ -1,31 +0,0 @@
import React from 'react';
import Select from '../Select';
import Slider from '../Slider';
/* eslint-disable react/prop-types */
const SelectWidget = ({ options, multi, ...rest }) => (
<Select options={options.enumOptions} multiple={multi} {...rest} />
);
const RangeWidget = ({
schema,
readonly,
autofocus,
label, // eslint-disable-line
options, // eslint-disable-line
formContext, // eslint-disable-line
registry, // eslint-disable-line
...rest
}) => (
<Slider
{...rest}
autoFocus={autofocus}
readOnly={readonly}
min={schema.minimum}
max={schema.maximum}
step={schema.multipleOf}
withValue
/>
);
export default { SelectWidget, RangeWidget };

View File

@ -0,0 +1,30 @@
import React from 'react';
import { FieldProps, Widget, WidgetProps } from 'react-jsonschema-form';
import Select from '../Select';
import Slider from '../Slider';
/* eslint-disable react/prop-types */
const SelectWidget: Widget = ({ options, ...rest }) => (
<Select options={options.enumOptions} {...rest} />
);
const RangeWidget: Widget = (({
schema,
disabled,
label, // eslint-disable-line
options, // eslint-disable-line
formContext, // eslint-disable-line
registry, // eslint-disable-line
...rest
}: WidgetProps & { registry: FieldProps['registry'] }) =>
(
<Slider
{...rest}
disabled={disabled}
min={schema.minimum}
max={schema.maximum}
withValue
/>
) as unknown) as Widget;
export default { SelectWidget, RangeWidget };

View File

@ -1,6 +1,8 @@
import React from 'react';
import styled from 'styled-components';
import { Story } from '@storybook/react';
import Notification from './';
import { NotificationProps } from './Notification';
const Container = styled.div`
display: flex;
@ -15,7 +17,7 @@ export default {
component: Notification,
};
const Template = (args) => (
const Template: Story<NotificationProps> = (args) => (
<Container>
<Notification {...args} />
</Container>

View File

@ -6,11 +6,21 @@ import { MdError } from 'react-icons/md';
import { MdCheckCircle } from 'react-icons/md';
import createStyledComponent from '../utils/createStyledComponent';
import styles from './styles';
import { Theme } from '../themes/default';
const NotificationWrapper = createStyledComponent(styles);
export default class Notification extends Component {
shouldComponentUpdate(nextProps) {
export type Type = 'info' | 'success' | 'warning' | 'error';
export interface NotificationProps {
children?: React.ReactNode;
type: Type;
onClose?: React.MouseEventHandler<HTMLButtonElement>;
theme?: Theme;
}
export default class Notification extends Component<NotificationProps> {
shouldComponentUpdate(nextProps: NotificationProps) {
return (
nextProps.children !== this.props.children ||
nextProps.type !== this.props.type
@ -43,15 +53,15 @@ export default class Notification extends Component {
</NotificationWrapper>
);
}
}
Notification.propTypes = {
static propTypes = {
children: PropTypes.any.isRequired,
type: PropTypes.oneOf(['info', 'success', 'warning', 'error']),
onClose: PropTypes.func,
theme: PropTypes.object,
};
Notification.defaultProps = {
static defaultProps = {
type: 'info',
};
}

View File

@ -1,6 +1,8 @@
import { css } from 'styled-components';
import { css, ThemedStyledProps } from 'styled-components';
import { Theme } from '../../themes/default';
import { Type } from '../Notification';
const getBackground = (theme, type) => {
const getBackground = (theme: Theme, type: Type) => {
switch (type) {
case 'success':
return `background-color: ${theme.base0B};`;
@ -13,7 +15,11 @@ const getBackground = (theme, type) => {
}
};
export default ({ theme, type }) => css`
interface StyleProps {
type: Type;
}
export default ({ theme, type }: ThemedStyledProps<StyleProps, Theme>) => css`
display: flex;
align-items: flex-start;
flex-shrink: 0;

View File

@ -1,6 +1,8 @@
import React from 'react';
import styled from 'styled-components';
import { Story } from '@storybook/react';
import SegmentedControl from './';
import { SegmentedControlProps } from './SegmentedControl';
const Container = styled.div`
display: flex;
@ -15,7 +17,7 @@ export default {
component: SegmentedControl,
};
const Template = (args) => (
const Template: Story<Omit<SegmentedControlProps, 'values'>> = (args) => (
<Container>
<SegmentedControl values={['Button1', 'Button2', 'Button3']} {...args} />
</Container>

View File

@ -2,23 +2,32 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import createStyledComponent from '../utils/createStyledComponent';
import styles from './styles';
import { Theme } from '../themes/default';
const SegmentedWrapper = createStyledComponent(styles);
export default class SegmentedControl extends Component {
shouldComponentUpdate(nextProps) {
export interface SegmentedControlProps {
values: string[];
selected?: string;
onClick: (value: string) => void;
disabled?: boolean;
theme?: Theme;
}
export default class SegmentedControl extends Component<SegmentedControlProps> {
shouldComponentUpdate(nextProps: SegmentedControlProps) {
return (
nextProps.disabled !== this.props.disabled ||
nextProps.selected !== this.props.selected
);
}
onClick = (e) => {
this.props.onClick(e.target.value);
onClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
this.props.onClick(e.currentTarget.value);
};
onMouseUp = (e) => {
e.target.blur();
onMouseUp: React.MouseEventHandler<HTMLButtonElement> = (e) => {
e.currentTarget.blur();
};
render() {
@ -39,12 +48,12 @@ export default class SegmentedControl extends Component {
</SegmentedWrapper>
);
}
}
SegmentedControl.propTypes = {
static propTypes = {
values: PropTypes.array.isRequired,
selected: PropTypes.string,
onClick: PropTypes.func,
disabled: PropTypes.bool,
theme: PropTypes.object,
};
}

View File

@ -1,7 +1,15 @@
import { css } from 'styled-components';
import { css, ThemedStyledProps } from 'styled-components';
import color from '../../utils/color';
import { Theme } from '../../themes/default';
export default ({ theme, disabled }) => css`
interface StyleProps {
disabled: boolean | undefined;
}
export default ({
theme,
disabled,
}: ThemedStyledProps<StyleProps, Theme>) => css`
display: flex;
flex-shrink: 0;

View File

@ -2,6 +2,8 @@ import React from 'react';
import styled from 'styled-components';
import Select from './';
import { options } from './options';
import { Story } from '@storybook/react';
import { SelectProps } from './Select';
const Container = styled.div`
display: flex;
@ -20,7 +22,10 @@ export default {
component: Select,
};
const Template = ({ value, ...args }) => (
type TemplateArgs = Omit<SelectProps, 'value'> & { value: string };
// eslint-disable-next-line react/prop-types
const Template: Story<TemplateArgs> = ({ value, ...args }) => (
<Container>
<Select
options={options}

View File

@ -1,12 +1,17 @@
import React, { PureComponent, Component } from 'react';
import PropTypes from 'prop-types';
import ReactSelect from 'react-select';
import ReactSelect, { Props as ReactSelectProps } from 'react-select';
import createThemedComponent from '../utils/createThemedComponent';
import { Theme } from '../themes/default';
export interface SelectProps extends Omit<ReactSelectProps, 'theme'> {
theme: Theme;
}
/**
* Wrapper around [React Select](https://github.com/JedWatson/react-select).
*/
class Select extends (PureComponent || Component) {
export class Select extends (PureComponent || Component)<SelectProps> {
render() {
return (
<ReactSelect
@ -37,9 +42,8 @@ class Select extends (PureComponent || Component) {
/>
);
}
}
Select.propTypes = {
static propTypes = {
isClearable: PropTypes.bool, // should it be possible to reset value
isDisabled: PropTypes.bool, // whether the Select is disabled or not
isLoading: PropTypes.bool, // whether the Select is loading externally or not
@ -49,5 +53,6 @@ Select.propTypes = {
value: PropTypes.any, // initial field value
menuPlacement: PropTypes.oneOf(['auto', 'bottom', 'top']), // value to control the opening direction
};
}
export default createThemedComponent(Select);

View File

@ -1 +0,0 @@
export default from './Select';

View File

@ -0,0 +1 @@
export { default } from './Select';

View File

@ -1,6 +1,8 @@
import React from 'react';
import styled from 'styled-components';
import { Story } from '@storybook/react';
import Slider from './';
import { SliderProps } from './Slider';
const Container = styled.div`
display: flex;
@ -15,7 +17,7 @@ export default {
component: Slider,
};
const Template = (args) => (
const Template: Story<SliderProps> = (args) => (
<Container>
<Slider {...args} />
</Container>

View File

@ -3,12 +3,25 @@ import PropTypes from 'prop-types';
import createStyledComponent from '../utils/createStyledComponent';
import * as styles from './styles';
import { containerStyle } from './styles/common';
import { Theme } from '../themes/default';
const SliderWrapper = createStyledComponent(styles);
const ContainerWithValue = createStyledComponent(containerStyle);
export default class Slider extends Component {
shouldComponentUpdate(nextProps) {
export interface SliderProps {
value: number;
min: number;
max: number;
label?: string;
sublabel?: string;
withValue?: boolean;
disabled?: boolean;
onChange: (value: number) => void;
theme?: Theme;
}
export default class Slider extends Component<SliderProps> {
shouldComponentUpdate(nextProps: SliderProps) {
return (
nextProps.label !== this.props.label ||
nextProps.value !== this.props.value ||
@ -19,7 +32,7 @@ export default class Slider extends Component {
);
}
onChange = (e) => {
onChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
this.props.onChange(parseFloat(e.target.value));
};
@ -53,9 +66,8 @@ export default class Slider extends Component {
</SliderWrapper>
);
}
}
Slider.propTypes = {
static propTypes = {
value: PropTypes.number,
min: PropTypes.number,
max: PropTypes.number,
@ -67,4 +79,5 @@ Slider.propTypes = {
theme: PropTypes.object,
};
Slider.defaultProps = { value: 0, min: 0, max: 100 };
static defaultProps = { value: 0, min: 0, max: 100 };
}

View File

@ -1 +0,0 @@
export default from './Slider';

View File

@ -0,0 +1 @@
export { default } from './Slider';

View File

@ -1,6 +1,8 @@
import { css } from 'styled-components';
import { css, ThemedStyledProps } from 'styled-components';
import { Theme } from '../../themes/default';
export const containerStyle = ({ theme }) => css`
// eslint-disable-next-line @typescript-eslint/ban-types
export const containerStyle = ({ theme }: ThemedStyledProps<{}, Theme>) => css`
display: flex;
align-items: center;

View File

@ -6,10 +6,22 @@ Based on:
http://codepen.io/thebabydino/pen/YPOPxr
*/
import { css } from 'styled-components';
import { css, ThemedStyledProps } from 'styled-components';
import { prefixSelectors } from '../../utils/autoPrefix';
import { Theme } from '../../themes/default';
export const style = ({ theme, percent, disabled, withLabel }) => css`
export interface StyleProps {
percent: number;
disabled: boolean;
withLabel: boolean;
}
export const style = ({
theme,
percent,
disabled,
withLabel,
}: ThemedStyledProps<StyleProps, Theme>) => css`
display: block;
width: 100%;
position: relative;

View File

@ -1,9 +1,16 @@
import { css } from 'styled-components';
import { css, ThemedStyledProps } from 'styled-components';
import { prefixSelectors } from '../../utils/autoPrefix';
import color from '../../utils/color';
import { animationCurve } from '../../utils/animations';
import { StyleProps } from './default';
import { Theme } from '../../themes/default';
export const style = ({ theme, percent, disabled, withLabel }) => css`
export const style = ({
theme,
percent,
disabled,
withLabel,
}: ThemedStyledProps<StyleProps, Theme>) => css`
display: block;
width: 100%;
position: relative;

View File

@ -1,7 +1,9 @@
import React from 'react';
import styled from 'styled-components';
import { Story } from '@storybook/react';
import Tabs from './';
import { tabs, simple10Tabs } from './data';
import { TabsProps } from './Tabs';
const Container = styled.div`
display: flex;
@ -16,7 +18,7 @@ export default {
component: Tabs,
};
const Template = (args) => (
const Template: Story<TabsProps<unknown>> = (args) => (
<Container>
<Tabs {...args} />
</Container>
@ -35,7 +37,9 @@ Default.argTypes = {
onClick: { control: { disable: true } },
};
export const WithContent = Template.bind({});
export const WithContent = (Template as Story<
TabsProps<{ selected: string }>
>).bind({});
WithContent.args = {
tabs,
selected: 'Tab2',

View File

@ -1,29 +1,44 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import TabsHeader from './TabsHeader';
import TabsHeader, { ReactButtonElement, Tab } from './TabsHeader';
import { TabsContainer } from './styles/common';
export default class Tabs extends Component {
constructor(props) {
export type Position = 'left' | 'right' | 'center';
export interface TabsProps<P> {
tabs: Tab<P>[];
selected?: string;
main?: boolean;
onClick: (value: string) => void;
collapsible?: boolean;
position: Position;
}
export default class Tabs<P> extends Component<TabsProps<P>> {
constructor(props: TabsProps<P>) {
super(props);
this.updateTabs(props);
}
UNSAFE_componentWillReceiveProps(nextProps) {
tabsHeader?: ReactButtonElement[];
SelectedComponent?: React.ComponentType<P>;
selector?: () => P;
UNSAFE_componentWillReceiveProps(nextProps: TabsProps<P>) {
if (nextProps.selected !== this.props.selected) {
this.updateTabs(nextProps);
}
}
onMouseUp = (e) => {
e.target.blur();
onMouseUp: React.MouseEventHandler<HTMLButtonElement> = (e) => {
e.currentTarget.blur();
};
onClick = (e) => {
this.props.onClick(e.target.value);
onClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
this.props.onClick(e.currentTarget.value);
};
updateTabs(props) {
updateTabs(props: TabsProps<P>) {
const tabs = props.tabs;
const selected = props.selected;
@ -34,7 +49,7 @@ export default class Tabs extends Component {
isSelected = true;
if (tab.component) {
this.SelectedComponent = tab.component;
if (tab.selector) this.selector = () => tab.selector(tab);
if (tab.selector) this.selector = () => tab.selector!(tab);
}
}
return (
@ -54,7 +69,7 @@ export default class Tabs extends Component {
render() {
const tabsHeader = (
<TabsHeader
tabs={this.tabsHeader}
tabs={this.tabsHeader!}
items={this.props.tabs}
main={this.props.main}
collapsible={this.props.collapsible}
@ -76,14 +91,13 @@ export default class Tabs extends Component {
<TabsContainer position={this.props.position}>
{tabsHeader}
<div>
<this.SelectedComponent {...(this.selector && this.selector())} />
<this.SelectedComponent {...(this.selector! && this.selector!())} />
</div>
</TabsContainer>
);
}
}
Tabs.propTypes = {
static propTypes = {
tabs: PropTypes.array.isRequired,
selected: PropTypes.string,
main: PropTypes.bool,
@ -92,4 +106,5 @@ Tabs.propTypes = {
position: PropTypes.oneOf(['left', 'right', 'center']),
};
Tabs.defaultProps = { position: 'left' };
static defaultProps = { position: 'left' };
}

View File

@ -8,20 +8,50 @@ import * as styles from './styles';
const TabsWrapper = createStyledComponent(styles);
export default class TabsHeader extends Component {
constructor(props) {
super(props);
this.state = {
visibleTabs: props.tabs.slice(),
export type ReactButtonElement = React.ReactElement<
JSX.IntrinsicElements['button'],
'button'
>;
export interface Tab<P> {
name: string;
value?: string;
component?: React.ComponentType<P>;
selector?: (tab: this) => P;
}
interface Props<P> {
tabs: ReactButtonElement[];
items: Tab<P>[];
main: boolean | undefined;
onClick: (value: string) => void;
position: 'left' | 'right' | 'center';
collapsible: boolean | undefined;
selected: string | undefined;
}
interface State {
visibleTabs: ReactButtonElement[];
hiddenTabs: ReactButtonElement[];
subMenuOpened: boolean;
contextMenu: { top: number; left: number } | undefined;
}
export default class TabsHeader<P> extends Component<Props<P>, State> {
state: State = {
visibleTabs: this.props.tabs.slice(),
hiddenTabs: [],
subMenuOpened: false,
contextMenu: undefined,
};
this.iconWidth = 0;
this.hiddenTabsWidth = [];
}
UNSAFE_componentWillReceiveProps(nextProps) {
iconWidth = 0;
hiddenTabsWidth: number[] = [];
tabsWrapperRef?: HTMLDivElement | null;
tabsRef?: HTMLDivElement | null;
resizeDetector?: HTMLIFrameElement;
UNSAFE_componentWillReceiveProps(nextProps: Props<P>) {
if (
nextProps.tabs !== this.props.tabs ||
nextProps.selected !== this.props.selected ||
@ -38,7 +68,7 @@ export default class TabsHeader extends Component {
}
}
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps: Props<P>) {
const { collapsible } = this.props;
if (!collapsible) {
if (prevProps.collapsible !== collapsible) this.disableResizeEvents();
@ -47,8 +77,11 @@ export default class TabsHeader extends Component {
let shouldCollapse = false;
if (this.iconWidth === 0) {
const tabButtons = this.tabsRef.children;
if (this.tabsRef.children[tabButtons.length - 1].value === 'expandIcon') {
const tabButtons = this.tabsRef!.children;
if (
(this.tabsRef!.children[tabButtons.length - 1] as HTMLButtonElement)
.value === 'expandIcon'
) {
this.iconWidth = tabButtons[
tabButtons.length - 1
].getBoundingClientRect().width;
@ -75,12 +108,12 @@ export default class TabsHeader extends Component {
}
enableResizeEvents() {
this.resizeDetector = observeResize(this.tabsWrapperRef, this.collapse);
this.resizeDetector = observeResize(this.tabsWrapperRef!, this.collapse);
window.addEventListener('mousedown', this.hideSubmenu);
}
disableResizeEvents() {
this.resizeDetector.remove();
this.resizeDetector!.remove();
window.removeEventListener('mousedown', this.hideSubmenu);
}
@ -90,13 +123,13 @@ export default class TabsHeader extends Component {
const { selected, tabs } = this.props;
const tabsWrapperRef = this.tabsWrapperRef;
const tabsRef = this.tabsRef;
const tabButtons = this.tabsRef.children;
const tabButtons = this.tabsRef!.children;
const visibleTabs = this.state.visibleTabs;
const hiddenTabs = this.state.hiddenTabs;
let tabsWrapperRight = tabsWrapperRef.getBoundingClientRect().right;
let tabsWrapperRight = tabsWrapperRef!.getBoundingClientRect().right;
if (!tabsWrapperRight) return; // tabs are hidden
const tabsRefRight = tabsRef.getBoundingClientRect().right;
const tabsRefRight = tabsRef!.getBoundingClientRect().right;
let i = visibleTabs.length - 1;
let hiddenTab;
@ -104,16 +137,16 @@ export default class TabsHeader extends Component {
if (
this.props.position === 'right' &&
hiddenTabs.length > 0 &&
tabsRef.getBoundingClientRect().width + this.hiddenTabsWidth[0] <
tabsWrapperRef.getBoundingClientRect().width
tabsRef!.getBoundingClientRect().width + this.hiddenTabsWidth[0] <
tabsWrapperRef!.getBoundingClientRect().width
) {
while (
i < tabs.length - 1 &&
tabsRef.getBoundingClientRect().width + this.hiddenTabsWidth[0] <
tabsWrapperRef.getBoundingClientRect().width
tabsRef!.getBoundingClientRect().width + this.hiddenTabsWidth[0] <
tabsWrapperRef!.getBoundingClientRect().width
) {
hiddenTab = hiddenTabs.shift();
visibleTabs.splice(Number(hiddenTab.key), 0, hiddenTab);
visibleTabs.splice(Number(hiddenTab!.key), 0, hiddenTab!);
i++;
}
} else {
@ -123,7 +156,7 @@ export default class TabsHeader extends Component {
tabButtons[i].getBoundingClientRect().right >=
tabsWrapperRight - this.iconWidth
) {
if (tabButtons[i].value !== selected) {
if ((tabButtons[i] as HTMLButtonElement).value !== selected) {
hiddenTabs.unshift(...visibleTabs.splice(i, 1));
this.hiddenTabsWidth.unshift(
tabButtons[i].getBoundingClientRect().width
@ -142,7 +175,7 @@ export default class TabsHeader extends Component {
tabsWrapperRight - this.iconWidth
) {
hiddenTab = hiddenTabs.shift();
visibleTabs.splice(Number(hiddenTab.key), 0, hiddenTab);
visibleTabs.splice(Number(hiddenTab!.key), 0, hiddenTab!);
this.hiddenTabsWidth.shift();
i++;
}
@ -154,15 +187,15 @@ export default class TabsHeader extends Component {
this.setState({ subMenuOpened: false, contextMenu: undefined });
};
getTabsWrapperRef = (node) => {
getTabsWrapperRef: React.RefCallback<HTMLDivElement> = (node) => {
this.tabsWrapperRef = node;
};
getTabsRef = (node) => {
getTabsRef: React.RefCallback<HTMLDivElement> = (node) => {
this.tabsRef = node;
};
expandMenu = (e) => {
expandMenu: React.MouseEventHandler = (e) => {
const rect = e.currentTarget.children[0].getBoundingClientRect();
this.setState({
contextMenu: {
@ -202,9 +235,8 @@ export default class TabsHeader extends Component {
</TabsWrapper>
);
}
}
TabsHeader.propTypes = {
static propTypes = {
tabs: PropTypes.array.isRequired,
items: PropTypes.array.isRequired,
main: PropTypes.bool,
@ -213,3 +245,4 @@ TabsHeader.propTypes = {
collapsible: PropTypes.bool,
selected: PropTypes.string,
};
}

View File

@ -1,7 +1,7 @@
import React from 'react';
/* eslint-disable react/prop-types */
const Component = ({ selected }) => (
const Component = ({ selected }: { selected: string }) => (
<div
style={{
display: 'flex',
@ -17,7 +17,9 @@ const Component = ({ selected }) => (
);
/* eslint-enable react/prop-types */
const selector = (tab) => ({ selected: tab.name });
const selector = (tab: { name: string; value?: string }) => ({
selected: tab.name,
});
export const tabs = [
{
@ -37,6 +39,6 @@ export const tabs = [
},
];
export const simple10Tabs = [];
export const simple10Tabs: { name: string; value: string }[] = [];
for (let i = 1; i <= 10; i++)
simple10Tabs.push({ name: `Tab${i}`, value: `${i}` });

View File

@ -1 +0,0 @@
export default from './Tabs';

View File

@ -0,0 +1 @@
export { default } from './Tabs';

View File

@ -1,6 +1,11 @@
import styled from 'styled-components';
import { Position } from '../Tabs';
export const TabsContainer = styled.div`
interface StyleProps {
position: Position;
}
export const TabsContainer = styled.div<StyleProps>`
position: relative;
display: flex;
flex-direction: column;

View File

@ -1,6 +1,14 @@
import { css } from 'styled-components';
import { css, ThemedStyledProps } from 'styled-components';
import { Theme } from '../../themes/default';
export const style = ({ theme, main }) => css`
export interface StyleProps {
main: boolean | undefined;
}
export const style = ({
theme,
main,
}: ThemedStyledProps<StyleProps, Theme>) => css`
display: flex;
flex: 0 0 1;
padding-left: 1px;

View File

@ -1,7 +1,12 @@
import { css } from 'styled-components';
import { css, ThemedStyledProps } from 'styled-components';
import { ripple } from '../../utils/animations';
import { Theme } from '../../themes/default';
import { StyleProps } from './default';
export const style = ({ theme, main }) => css`
export const style = ({
theme,
main,
}: ThemedStyledProps<StyleProps, Theme>) => css`
display: flex;
flex: 0 0 1;
padding-left: 1px;

View File

@ -1,5 +1,6 @@
import React from 'react';
import React, { ReactNode } from 'react';
import styled from 'styled-components';
import { Story } from '@storybook/react';
import { MdPlayArrow } from 'react-icons/md';
import { MdFiberManualRecord } from 'react-icons/md';
import { MdKeyboardArrowLeft } from 'react-icons/md';
@ -16,6 +17,9 @@ import {
} from '../';
import { options } from '../Select/options';
import { simple10Tabs } from '../Tabs/data';
import { BorderPosition } from './styles/Toolbar';
import { TooltipPosition } from '../Button/Button';
import { Position } from '../Tabs/Tabs';
const Container = styled.div`
display: flex;
@ -35,12 +39,27 @@ export default {
component: Toolbar,
};
const Template = ({
interface TemplateArgs {
borderPosition: BorderPosition;
title?: string;
tooltipPosition: TooltipPosition;
disabled?: boolean;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
label: ReactNode;
}
const Template: Story<TemplateArgs> = ({
// eslint-disable-next-line react/prop-types
borderPosition,
// eslint-disable-next-line react/prop-types
title,
// eslint-disable-next-line react/prop-types
tooltipPosition,
// eslint-disable-next-line react/prop-types
disabled,
// eslint-disable-next-line react/prop-types
onClick,
// eslint-disable-next-line react/prop-types
label,
}) => (
<Container>
@ -104,17 +123,41 @@ Default.argTypes = {
},
};
const TabsTemplate = ({
interface TabsTemplateArgs {
title?: string;
tooltipPosition: TooltipPosition;
disabled?: boolean;
buttonOnClick?: React.MouseEventHandler<HTMLButtonElement>;
label: ReactNode;
selected?: string;
main?: boolean;
tabsOnClick: (value: string) => void;
collapsible?: boolean;
position: Position;
}
const TabsTemplate: Story<TabsTemplateArgs> = ({
// eslint-disable-next-line react/prop-types
title,
// eslint-disable-next-line react/prop-types
tooltipPosition,
// eslint-disable-next-line react/prop-types
disabled,
// eslint-disable-next-line react/prop-types
buttonOnClick,
// eslint-disable-next-line react/prop-types
label,
// eslint-disable-next-line react/prop-types
selected,
// eslint-disable-next-line react/prop-types
main,
tabOnClick,
// eslint-disable-next-line react/prop-types
tabsOnClick,
// eslint-disable-next-line react/prop-types
collapsible,
// eslint-disable-next-line react/prop-types
position,
// eslint-disable-next-line react/prop-types
}) => (
<Container>
<Toolbar>
@ -130,7 +173,7 @@ const TabsTemplate = ({
tabs={simple10Tabs}
selected={selected}
main={main}
onClick={tabOnClick}
onClick={tabsOnClick}
collapsible={collapsible}
position={position}
/>
@ -176,7 +219,7 @@ Tabs.argTypes = {
buttonOnClick: {
action: 'button clicked',
},
tabOnClick: {
tabsOnClick: {
action: 'tab selected',
},
position: {
@ -187,19 +230,48 @@ Tabs.argTypes = {
},
};
const WithSliderTemplate = ({
interface WithSliderTemplateArgs {
title?: string;
tooltipPosition: TooltipPosition;
playOnClick?: React.MouseEventHandler<HTMLButtonElement>;
value: number;
min: number;
max: number;
label?: string;
withValue?: boolean;
onChange: (value: number) => void;
previousStateOnClick?: React.MouseEventHandler<HTMLButtonElement>;
nextStateOnClick?: React.MouseEventHandler<HTMLButtonElement>;
selected?: string;
segmentedControlOnClick: (value: string) => void;
}
const WithSliderTemplate: Story<WithSliderTemplateArgs> = ({
// eslint-disable-next-line react/prop-types
title,
// eslint-disable-next-line react/prop-types
tooltipPosition,
// eslint-disable-next-line react/prop-types
playOnClick,
// eslint-disable-next-line react/prop-types
value,
// eslint-disable-next-line react/prop-types
min,
// eslint-disable-next-line react/prop-types
max,
// eslint-disable-next-line react/prop-types
label,
// eslint-disable-next-line react/prop-types
withValue,
// eslint-disable-next-line react/prop-types
onChange,
// eslint-disable-next-line react/prop-types
previousStateOnClick,
// eslint-disable-next-line react/prop-types
nextStateOnClick,
// eslint-disable-next-line react/prop-types
selected,
// eslint-disable-next-line react/prop-types
segmentedControlOnClick,
}) => (
<Container>

View File

@ -1,3 +0,0 @@
export Toolbar from './styles/Toolbar';
export Divider from './styles/Divider';
export Spacer from './styles/Spacer';

View File

@ -0,0 +1,3 @@
export { default as Toolbar } from './styles/Toolbar';
export { default as Divider } from './styles/Divider';
export { default as Spacer } from './styles/Spacer';

View File

@ -1,6 +1,16 @@
import styled from 'styled-components';
import styled, { ThemedStyledInterface } from 'styled-components';
import { Theme } from '../../themes/default';
const Toolbar = styled.div`
export type BorderPosition = 'top' | 'bottom';
export interface Props {
fullHeight?: boolean;
compact?: boolean;
borderPosition?: BorderPosition;
noBorder?: boolean;
}
const Toolbar = (styled as ThemedStyledInterface<Theme>).div<Props>`
display: flex;
flex-shrink: 0;
box-sizing: border-box;

View File

@ -3,6 +3,7 @@
export default {
scheme: 'default',
author: 'Mihail Diordiev (https://github.com/zalmoxisus)',
base00: '#ffffff',
base01: '#f3f3f3',
base02: '#e8e8e8',

Some files were not shown because too many files have changed in this diff Show More