feat(react-dock): convert to TypeScript (#607)

* feature(react-dock): convert to TypeScript

* And that

* Fun

* pretty

* More fun

* Try that
This commit is contained in:
Nathan Bierema 2020-08-24 00:37:43 -04:00 committed by GitHub
parent 97adc01b78
commit 78ded9e0ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 251 additions and 118 deletions

View File

@ -27,7 +27,8 @@
"ts-node": "^9.0.0",
"typescript": "^3.9.7",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12"
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"
},
"scripts": {
"lerna": "lerna",

View File

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

View File

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

View File

@ -39,24 +39,29 @@ const Remove = styled(BsX)`
cursor: pointer;
`;
const positions = ['left', 'top', 'right', 'bottom'];
const dimModes = ['transparent', 'none', 'opaque'];
const positions = ['left', 'top', 'right', 'bottom'] as const;
const dimModes = ['transparent', 'none', 'opaque'] as const;
class App extends Component {
constructor(props) {
super(props);
this.state = {
positionIdx: 0,
dimModeIdx: 0,
isVisible: true,
fluid: true,
customAnimation: false,
slow: false,
size: 0.25,
};
}
interface State {
positionIdx: number;
dimModeIdx: number;
isVisible: boolean;
fluid: boolean;
customAnimation: boolean;
slow: boolean;
size: number;
}
componentDidMount() {}
class App extends Component<never, State> {
state: State = {
positionIdx: 0,
dimModeIdx: 0,
isVisible: true,
fluid: true,
customAnimation: false,
slow: false,
size: 0.25,
};
render() {
const duration = this.state.slow ? 2000 : 200;
@ -166,11 +171,11 @@ class App extends Component {
);
}
handleVisibleChange = (isVisible) => {
handleVisibleChange = (isVisible: boolean) => {
this.setState({ isVisible });
};
handleSizeChange = (size) => {
handleSizeChange = (size: number) => {
this.setState({ size });
};

View File

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

View File

@ -0,0 +1,3 @@
module.exports = {
preset: 'ts-jest',
};

View File

@ -2,63 +2,65 @@
"name": "react-dock",
"version": "0.2.4",
"description": "Resizable dockable react component",
"scripts": {
"build-lib": "babel src --out-dir lib",
"build-demo": "NODE_ENV=production webpack -p",
"stats": "webpack --profile --json > stats.json",
"start": "webpack-dev-server",
"preversion": "npm run lint && npm run test",
"version": "npm run build-demo && git add -A .",
"postversion": "git push",
"prepublish": "npm run build-lib",
"test": "jest"
},
"main": "lib/index.js",
"files": [
"lib",
"src"
],
"repository": {
"type": "git",
"url": "https://github.com/reduxjs/redux-devtools.git"
},
"keywords": [
"react",
"reactjs",
"dock",
"sidebar"
],
"author": "Alexander <alexkuz@gmail.com> (http://kuzya.org/)",
"license": "MIT",
"homepage": "https://github.com/reduxjs/redux-devtools",
"bugs": {
"url": "https://github.com/reduxjs/redux-devtools/issues"
},
"homepage": "https://github.com/reduxjs/redux-devtools",
"license": "MIT",
"author": "Alexander <alexkuz@gmail.com> (http://kuzya.org/)",
"files": [
"lib",
"src"
],
"main": "lib/index.js",
"types": "lib/index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/reduxjs/redux-devtools.git"
},
"scripts": {
"start": "webpack-dev-server",
"stats": "webpack --profile --json > stats.json",
"build:demo": "NODE_ENV=production webpack -p",
"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 umd",
"test": "jest",
"lint": "eslint . --ext .ts,.tsx",
"lint:fix": "eslint . --ext .ts,.tsx --fix",
"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": {
"@types/prop-types": "^15.7.3",
"lodash.debounce": "^4.0.8",
"prop-types": "^15.7.2"
},
"devDependencies": {
"@babel/cli": "^7.10.5",
"@babel/core": "^7.11.1",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-proposal-export-default-from": "^7.10.4",
"@babel/preset-env": "^7.11.0",
"@babel/preset-react": "^7.10.4",
"babel-loader": "^8.1.0",
"@types/lodash.debounce": "^4.0.6",
"@types/react": "^16.9.46",
"@types/react-dom": "^16.9.8",
"@types/react-test-renderer": "^16.9.3",
"@types/styled-components": "^5.1.2",
"react": "^16.13.1",
"react-bootstrap": "^1.3.0",
"react-dom": "^16.13.1",
"react-hot-loader": "^4.12.21",
"react-icons": "^3.10.0",
"react-pure-render": "^1.0.2",
"react-test-renderer": "^16.13.1",
"styled-components": "^5.1.1",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"
"styled-components": "^5.1.1"
},
"peerDependencies": {
"@types/react": "^16.3.18",
"react": "^16.3.0"
},
"dependencies": {
"lodash.debounce": "^4.0.8",
"prop-types": "^15.7.2"
}
}

View File

@ -3,8 +3,12 @@ import PropTypes from 'prop-types';
import debounce from 'lodash.debounce';
import autoprefix from './autoprefix';
function autoprefixes(styles) {
return Object.keys(styles).reduce(
interface Styles {
[key: string]: React.CSSProperties;
}
function autoprefixes(styles: Styles) {
return Object.keys(styles).reduce<Styles>(
(obj, key) => ((obj[key] = autoprefix(styles[key])), obj),
{}
);
@ -74,21 +78,21 @@ const styles = autoprefixes({
},
});
function getTransitions(duration) {
function getTransitions(duration: number) {
return ['left', 'top', 'width', 'height'].map(
(p) => `${p} ${duration / 1000}s ease-out`
);
}
function getDockStyles(
{ fluid, dockStyle, dockHiddenStyle, duration, position, isVisible },
{ size, isResizing, fullWidth, fullHeight }
{ fluid, dockStyle, dockHiddenStyle, duration, position, isVisible }: Props,
{ size, isResizing, fullWidth, fullHeight }: State
) {
let posStyle;
const absSize = fluid ? size * 100 + '%' : size + 'px';
const absSize = fluid ? `${size * 100}%` : `${size}px`;
function getRestSize(fullSize) {
return fluid ? 100 - size * 100 + '%' : fullSize - size + 'px';
function getRestSize(fullSize: number) {
return fluid ? `${100 - size * 100}%` : `${fullSize - size}px`;
}
switch (position) {
@ -139,8 +143,8 @@ function getDockStyles(
}
function getDimStyles(
{ dimMode, dimStyle, duration, isVisible },
{ isTransitionStarted }
{ dimMode, dimStyle, duration, isVisible }: Props,
{ isTransitionStarted }: State
) {
return [
styles.dim,
@ -155,7 +159,7 @@ function getDimStyles(
];
}
function getResizerStyles(position) {
function getResizerStyles(position: 'left' | 'right' | 'top' | 'bottom') {
let resizerStyle;
const size = 10;
@ -201,23 +205,57 @@ function getResizerStyles(position) {
return [styles.resizer, autoprefix(resizerStyle)];
}
function getFullSize(position, fullWidth, fullHeight) {
function getFullSize(
position: 'left' | 'right' | 'top' | 'bottom',
fullWidth: number,
fullHeight: number
) {
return position === 'left' || position === 'right' ? fullWidth : fullHeight;
}
export default class Dock extends Component {
constructor(props) {
super(props);
this.state = {
isControlled: typeof props.size !== 'undefined',
size: props.size || props.defaultSize,
isDimHidden: !props.isVisible,
fullWidth: typeof window !== 'undefined' && window.innerWidth,
fullHeight: typeof window !== 'undefined' && window.innerHeight,
isTransitionStarted: false,
isWindowResizing: false,
};
}
interface Props {
position: 'left' | 'right' | 'top' | 'bottom';
zIndex: number;
fluid: boolean;
size?: number;
defaultSize: number;
dimMode: 'none' | 'transparent' | 'opaque';
isVisible?: boolean;
onVisibleChange?: (isVisible: boolean) => void;
onSizeChange?: (size: number) => void;
dimStyle?: React.CSSProperties | null;
dockStyle?: React.CSSProperties | null;
dockHiddenStyle?: React.CSSProperties | null;
duration: number;
children?: React.FunctionComponent<{
position: 'left' | 'right' | 'top' | 'bottom';
isResizing: boolean | undefined;
size: number;
isVisible: boolean | undefined;
}>;
}
interface State {
isControlled: boolean;
size: number;
isDimHidden: boolean;
fullWidth: number;
fullHeight: number;
isTransitionStarted: boolean;
isWindowResizing: unknown;
isResizing?: boolean;
}
export default class Dock extends Component<Props, State> {
state: State = {
isControlled: typeof this.props.size !== 'undefined',
size: this.props.size || this.props.defaultSize,
isDimHidden: !this.props.isVisible,
fullWidth: window.innerWidth,
fullHeight: window.innerHeight,
isTransitionStarted: false,
isWindowResizing: false,
};
static propTypes = {
position: PropTypes.oneOf(['left', 'right', 'top', 'bottom']),
@ -250,9 +288,7 @@ export default class Dock extends Component {
window.addEventListener('mousemove', this.handleMouseMove);
window.addEventListener('resize', this.handleResize);
if (!window.fullWidth) {
this.updateWindowSize();
}
this.updateWindowSize();
}
componentWillUnmount() {
@ -263,12 +299,12 @@ export default class Dock extends Component {
window.removeEventListener('resize', this.handleResize);
}
UNSAFE_componentWillReceiveProps(nextProps) {
UNSAFE_componentWillReceiveProps(nextProps: Props) {
const isControlled = typeof nextProps.size !== 'undefined';
this.setState({ isControlled });
if (isControlled && this.props.size !== nextProps.size) {
if (isControlled && nextProps.size && this.props.size !== nextProps.size) {
this.setState({ size: nextProps.size });
} else if (this.props.fluid !== nextProps.fluid) {
this.updateSize(nextProps);
@ -281,7 +317,7 @@ export default class Dock extends Component {
}
}
updateSize(props) {
updateSize(props: Props) {
const { fullWidth, fullHeight } = this.state;
this.setState({
@ -291,7 +327,7 @@ export default class Dock extends Component {
});
}
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps: Props) {
if (this.props.isVisible !== prevProps.isVisible) {
if (!this.props.isVisible) {
window.setTimeout(() => this.hideDim(), this.props.duration);
@ -367,7 +403,7 @@ export default class Dock extends Component {
}
};
updateWindowSize = (windowResize) => {
updateWindowSize = (windowResize?: true) => {
const sizeState = {
fullWidth: window.innerWidth,
fullHeight: window.innerHeight,
@ -407,18 +443,18 @@ export default class Dock extends Component {
this.setState({ isResizing: false });
};
handleMouseMove = (e) => {
handleMouseMove = (e: MouseEvent | TouchEvent) => {
if (!this.state.isResizing || this.state.isWindowResizing) return;
if (!e.touches) e.preventDefault();
if (!(e as TouchEvent).touches) e.preventDefault();
const { position, fluid } = this.props;
const { fullWidth, fullHeight, isControlled } = this.state;
let { clientX: x, clientY: y } = e;
let { clientX: x, clientY: y } = e as MouseEvent;
if (e.touches) {
x = e.touches[0].clientX;
y = e.touches[0].clientY;
if ((e as TouchEvent).touches) {
x = (e as TouchEvent).touches[0].clientX;
y = (e as TouchEvent).touches[0].clientY;
}
let size;

View File

@ -1,6 +1,8 @@
// Same as https://github.com/SimenB/react-vendor-prefixes/blob/master/src/index.js,
// but dumber
import { CSSProperties } from 'react';
const vendorSpecificProperties = [
'animation',
'animationDelay',
@ -34,8 +36,8 @@ const vendorSpecificProperties = [
const prefixes = ['Moz', 'Webkit', 'ms', 'O'];
function prefixProp(key, value) {
return prefixes.reduce(
function prefixProp<Value>(key: string, value: Value) {
return prefixes.reduce<{ [key: string]: Value }>(
(obj, pre) => (
(obj[pre + key[0].toUpperCase() + key.substr(1)] = value), obj
),
@ -43,13 +45,13 @@ function prefixProp(key, value) {
);
}
export default function autoprefix(style) {
export default function autoprefix(style: CSSProperties) {
return Object.keys(style).reduce(
(obj, key) =>
vendorSpecificProperties.indexOf(key) !== -1
? {
...obj,
...prefixProp(key, style[key]),
...prefixProp(key, style[key as keyof CSSProperties]),
}
: obj,
style

View File

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

View File

@ -0,0 +1,2 @@
import Dock from './Dock';
export default Dock;

View File

@ -1,14 +1,14 @@
import React from 'react';
import ShallowRenderer from 'react-test-renderer/shallow';
import { createRenderer } from 'react-test-renderer/shallow';
import Dock from '../src/Dock';
describe('Dock component', function () {
it('should have shallow rendering', function () {
const shallowRenderer = new ShallowRenderer();
const renderer = createRenderer();
const DockEl = <Dock />;
shallowRenderer.render(DockEl);
renderer.render(DockEl);
const result = shallowRenderer.getRenderOutput();
const result = renderer.getRenderOutput();
expect(DockEl.props).toEqual({
position: 'left',

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
var path = require('path');
var webpack = require('webpack');
import * as path from 'path';
import * as webpack from 'webpack';
var isProduction = process.env.NODE_ENV === 'production';
const isProduction = process.env.NODE_ENV === 'production';
module.exports = {
mode: isProduction ? 'production' : 'development',
@ -20,12 +20,12 @@ module.exports = {
},
plugins: isProduction ? [] : [new webpack.HotModuleReplacementPlugin()],
resolve: {
extensions: ['.js', '.jsx'],
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
module: {
rules: [
{
test: /\.jsx?$/,
test: /\.(js|ts)x?$/,
loader: 'babel-loader',
include: [
path.join(__dirname, 'src'),
@ -37,7 +37,6 @@ module.exports = {
devServer: isProduction
? null
: {
quiet: true,
publicPath: '/static/',
port: 3000,
contentBase: './demo/',

View File

@ -51,7 +51,7 @@
"react-base16-styling": "^0.7.0"
},
"devDependencies": {
"@types/react": "^16.3.18",
"@types/react": "^16.9.46",
"@types/react-test-renderer": "^16.9.3",
"react": "^16.13.1",
"react-test-renderer": "^16.13.1"

View File

@ -6,6 +6,8 @@
"strict": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
"forceConsistentCasingInFileNames": true,
// See https://github.com/DefinitelyTyped/DefinitelyTyped/issues/33311
"types": ["node", "jest"]
}
}

View File

@ -3136,7 +3136,7 @@
dependencies:
"@types/node" "*"
"@types/hoist-non-react-statics@^3.3.0":
"@types/hoist-non-react-statics@*", "@types/hoist-non-react-statics@^3.3.0":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
@ -3217,6 +3217,13 @@
dependencies:
"@types/lodash" "*"
"@types/lodash.debounce@^4.0.6":
version "4.0.6"
resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.6.tgz#c5a2326cd3efc46566c47e4c0aa248dc0ee57d60"
integrity sha512-4WTmnnhCfDvvuLMaF3KV4Qfki93KebocUF45msxhYyjMttZDQYzHkO639ohhk8+oco2cluAFL3t5+Jn4mleylQ==
dependencies:
"@types/lodash" "*"
"@types/lodash@*", "@types/lodash@^4.14.159":
version "4.14.159"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.159.tgz#61089719dc6fdd9c5cb46efc827f2571d1517065"
@ -3294,6 +3301,13 @@
dependencies:
"@types/react" "*"
"@types/react-native@*":
version "0.63.8"
resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.63.8.tgz#73ec087122c64c309eeaf150b565b8d755f0fb1f"
integrity sha512-QRwGFRTyGafRVTUS+0GYyJrlpmS3boyBaFI0ULSc+mh/lQNxrzbdQvoL2k5X7+t9hxyqA4dTQAlP6l0ir/fNJQ==
dependencies:
"@types/react" "*"
"@types/react-redux@^7.1.9":
version "7.1.9"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.9.tgz#280c13565c9f13ceb727ec21e767abe0e9b4aec3"
@ -3344,6 +3358,16 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
"@types/styled-components@^5.1.2":
version "5.1.2"
resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.2.tgz#652af475b4af917b355ea1c3068acae63d46455f"
integrity sha512-HNocYLfrsnNNm8NTS/W53OERSjRA8dx5Bn6wBd2rXXwt4Z3s+oqvY6/PbVt3e6sgtzI63GX//WiWiRhWur08qQ==
dependencies:
"@types/hoist-non-react-statics" "*"
"@types/react" "*"
"@types/react-native" "*"
csstype "^3.0.2"
"@types/tapable@*", "@types/tapable@^1.0.5":
version "1.0.6"
resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.6.tgz#a9ca4b70a18b270ccb2bc0aaafefd1d486b7ea74"