Commit 5533ff78 authored by Mickaël Bourgier's avatar Mickaël Bourgier
Browse files

Merge branch 'typescript' into 'master'

Add Typescript support

See merge request !11
parents 0433f1e6 8c95163e
Pipeline #1774 passed with stage
in 2 minutes and 1 second
......@@ -6,6 +6,8 @@
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:jest/recommended",
"plugin:react/recommended"
],
......@@ -23,18 +25,36 @@
"rules": {
"react/prop-types": "off"
}
},
{
"files": ["**/*.js"],
"rules": {
"@typescript-eslint/explicit-function-return-type": "off"
}
},
{
"files": ["**/*.ts", "**/*.tsx"],
"rules": {
"react/prop-types": "off"
}
}
],
"parser": "babel-eslint",
"parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module"
},
"plugins": [
"jest"
"jest",
"@typescript-eslint"
],
"rules": {
"no-alert": "error",
"no-console": "error",
"no-unused-vars": ["error", {
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-unused-vars": ["error", {
"ignoreRestSiblings": true
}],
"no-alert": "error",
"no-console": "error",
"no-unused-vars": "off",
"react/no-unescaped-entities": 0
},
"settings": {
......
......@@ -6,6 +6,7 @@ test:
before_script:
- yarn install
script:
- yarn build:check
- yarn test --coverage
- yarn lint
- yarn prettier:check
const test = process.env.NODE_ENV === 'test';
const cjs = process.env.BABEL_ENV === 'cjs';
const es = process.env.BABEL_ENV === 'es';
module.exports = {
presets: [
[
'@babel/preset-env',
{
modules: test || cjs ? 'cjs' : false
}
],
'@babel/preset-react'
],
presets: [['@babel/preset-env', { modules: false }], '@babel/preset-react'],
plugins: ['@babel/plugin-proposal-class-properties', 'dev-expression'],
ignore: [
...(cjs || es ? [/demos\/.+\.js/] : []),
...(test ? [] : [/\.test\.js/])
]
};
......@@ -25,9 +25,22 @@ class ComponentDoc extends Component {
const {
description = '',
props = {},
// eslint-disable-next-line @typescript-eslint/no-var-requires
} = require(`!!react-docgen!@webalt/react/${name}/${name}`);
const longDescription = description.replace(/^@summary .+$/m, '');
// TODO chunk props by parent
const filteredProps = Object.assign(
{},
...Object.keys(props)
.filter(
(propName) =>
!('parent' in props[propName]) ||
props[propName].parent.name === `${name}Props`
)
.map((propName) => ({ [propName]: props[propName] }))
);
return (
<Fragment>
<h1 className='display-4'>{name}</h1>
......@@ -46,8 +59,8 @@ class ComponentDoc extends Component {
))}
<h2>Props</h2>
{props ? (
<ComponentPropsTable props={props} />
{Object.keys(filteredProps).length > 0 ? (
<ComponentPropsTable props={filteredProps} />
) : (
<p>Ce composant ne requiert aucune prop.</p>
)}
......
......@@ -22,6 +22,7 @@ class Home extends Component {
...components.map((name) => {
const {
description,
// eslint-disable-next-line @typescript-eslint/no-var-requires
} = require(`!!react-docgen!@webalt/react/${name}/${name}`);
let shortDescription = '';
......
......@@ -7,6 +7,7 @@ const formatPropType = (propType, depth = 0) => {
case 'arrayOf':
return `[ ${formatPropType(propType.value)}, ... ]`;
case 'bool':
case 'boolean':
return 'Boolean';
case 'element':
return 'Element';
......@@ -42,7 +43,15 @@ const formatPropType = (propType, depth = 0) => {
return propType.value.map((child) => formatPropType(child)).join(' | ');
}
return '???';
return (
propType.name
.replace(/"/g, "'")
// FIXME: the following type is present in props of OverridableComponents
// (only props "inherited" from the default `component` prop), maybe due
// to a misuse of Typescript.
.replace(' | ComponentPropsWithRef<C>[string]', '')
.replace('ComponentPropsWithRef<C>[string] | ', '')
);
};
export default formatPropType;
const parse = require('react-docgen').parse;
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path');
const parseJs = require('react-docgen').parse;
const parseTs = require('react-docgen-typescript').withCustomConfig(
path.resolve(__dirname, '../../tsconfig.json')
).parse;
module.exports = function (source) {
this.cacheable && this.cacheable();
......@@ -6,12 +12,34 @@ module.exports = function (source) {
let value = {};
try {
value = parse(source, null, null, {
filename: this.resourcePath,
});
value = /\.tsx?$/.test(this.resourcePath)
? parseTs(this.resourcePath)[0]
: parseJs(source, null, null, {
filename: this.resourcePath,
});
} catch (e) {
// ignore exception
}
// Hacky stuff to automatically set the default value and change the type of
// the "component" prop if it comes from OverridableComponent.
//
// With that, using OverridableComponent<Props, Foo> as a component type will
// document its `component` prop as a `ReactElement` with a `Foo` default
// value.
if (
value &&
value.props &&
'__componentDefaultValue' in value.props &&
'component' in value.props
) {
value.props.component.type = { name: 'ReactElement' };
value.props.component.defaultValue = {
value: value.props.__componentDefaultValue.type.name.replace(/"/g, "'"),
};
delete value.props.__componentDefaultValue;
}
return `module.exports = ${JSON.stringify(value, undefined, 2)}`;
};
module.exports = {
coverageReporters: ['text-summary'],
moduleNameMapper: {
'^@webalt/react(.*)$': '<rootDir>/src/$1'
'^@webalt/react(.*)$': '<rootDir>/src/$1',
},
preset: 'ts-jest/presets/js-with-ts',
roots: ['<rootDir>/src'],
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect']
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
};
......@@ -23,19 +23,21 @@
"dist",
"src"
],
"main": "dist/cjs/index.js",
"main": "dist/index.js",
"module": "dist/es/index.js",
"typings": "dist/index.d.ts",
"scripts": {
"build": "rm -rf dist && yarn build:cjs && yarn build:es",
"build:cjs": "BABEL_ENV=cjs babel src --out-dir dist/cjs",
"build:es": "BABEL_ENV=es babel src --out-dir dist/es",
"dev": "WEBPACK_TARGET=docs yarn docs:dev",
"docs:build": "rm -rf docs/dist && NODE_ENV=production WEBPACK_TARGET=docs webpack",
"build:cjs": "tsc --rootDir ./src --outDir ./dist --declaration",
"build:es": "tsc --rootDir ./src --outDir ./dist/es --declaration --target ES6 --moduleResolution node",
"build:check": "tsc --noEmit",
"dev": "yarn docs:dev",
"docs:build": "rm -rf docs/dist && NODE_ENV=production webpack",
"docs:dev": "webpack-dev-server --hot",
"lint": "eslint ./docs ./src",
"prepublishOnly": "yarn prettier && yarn lint && yarn build",
"prettier": "prettier --write './src/**/*.js' './docs/**/*.js'",
"prettier:check": "prettier --check './src/**/*.js' './docs/**/*.js'",
"lint": "eslint './docs/**/*.{js,ts,tsx}' './src/**/*.{js,ts,tsx}'",
"prepublishOnly": "yarn prettier:check && yarn lint && yarn build",
"prettier": "prettier --write './src/**/*.{js,ts,tsx}' './docs/**/*.{js,ts,tsx}'",
"prettier:check": "prettier --check './src/**/*.{js,ts,tsx}' './docs/**/*.{js,ts,tsx}'",
"test": "jest"
},
"peerDependencies": {
......@@ -61,9 +63,12 @@
"@babel/preset-react": "^7.0.0",
"@testing-library/jest-dom": "^5.1.1",
"@testing-library/react": "^10.0.4",
"@types/jest": "^25.2.1",
"@types/react": "^16.9.34",
"@types/react-dom": "^16.9.7",
"@typescript-eslint/eslint-plugin": "^2.31.0",
"@typescript-eslint/parser": "^2.31.0",
"babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^10.0.1",
"babel-jest": "^26.0.0",
"babel-loader": "^8.0.5",
"codemirror": "^5.42.0",
"css-loader": "^3.4.2",
......@@ -74,12 +79,13 @@
"html-loader": "^1.1.0",
"html-webpack-harddisk-plugin": "^1.0.1",
"html-webpack-plugin": "^4.3.0",
"jest": "^26.0.0",
"jest": "^26.0.1",
"prettier": "2.0.5",
"raw-loader": "^4.0.0",
"react": "^16.8.6",
"react-codemirror2": "^7.1.0",
"react-docgen": "^5.2.1",
"react-docgen-typescript": "^1.16.4",
"react-dom": "^16.8.6",
"react-hot-loader": "^4.0.0",
"react-icons": "^3.4.0",
......@@ -89,6 +95,9 @@
"react-router-hash-link": "^1.2.1",
"react-test-renderer": "^16.8.6",
"style-loader": "^1.1.3",
"ts-jest": "^25.4.0",
"ts-loader": "^7.0.2",
"typescript": "^3.8.3",
"uglifyjs-webpack-plugin": "^2.1.1",
"webpack": "^4.2.0",
"webpack-cli": "^3.1.2",
......
module.exports = {
singleQuote: true,
jsxSingleQuote: true
jsxSingleQuote: true,
};
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import { withStyles } from '../styles';
import { ObjectOf, OverridableComponent } from '../types';
import styles from './Box.styles';
/**
* @summary Boîte stylisée en fonction des couleurs du thème
*/
class Box extends Component {
render() {
const {
classes,
className: classNameProps,
color,
component: Component = 'div',
disabled,
htmlDisabled,
interactive = false,
muted,
shadow = false,
shape = 'square',
theme,
transitions = true,
variant = 'plain',
...otherProps
} = this.props;
return (
<Component
className={classNames(
classes.root,
classes[`${color}Color`],
classes[`${variant}Variant`],
classes[`${shape}Shape`],
{
[classes.shadow]: shadow,
[classes.muted]: muted,
[classes.disabled]: disabled,
[classes.interactive]: interactive,
[classes.transitions]: interactive && transitions,
},
classNameProps
)}
disabled={htmlDisabled}
{...otherProps}
/>
);
}
}
Box.propTypes = {
export interface BoxProps {
/**
* Contenu du composant
*/
children: PropTypes.node,
children?: React.ReactNode;
/**
* Objet permettant de modifier les classes utilisées par le composant
*/
classes: PropTypes.object,
classes?: ObjectOf<string>;
/**
* Nom(s) de classe(s) CSS à inclure dans le composant racine
*/
className: PropTypes.string,
className?: string;
/**
* Couleur de la boîte
*/
color: PropTypes.oneOfType([
PropTypes.oneOf([
'primary',
'secondary',
'success',
'danger',
'warning',
'info',
'light',
'dark',
]),
PropTypes.string,
]),
/**
* Composant à utiliser en tant que composant racine
*/
component: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func,
PropTypes.object,
]),
color?:
| 'primary'
| 'secondary'
| 'success'
| 'danger'
| 'warning'
| 'info'
| 'light'
| 'dark'
| string;
/**
* Désactive la boîte : elle perd sa couleur et devient grise
*
* @default false
*/
disabled: PropTypes.bool,
disabled?: boolean;
/**
* Permet de fournir la prop `disabled` à l'élément sous-jacent
*/
htmlDisabled: PropTypes.bool,
htmlDisabled?: boolean;
/**
* Active les effets sur les pseudo-classe `:hover`, `:focus` et `:active`
*
* @default false
*/
interactive: PropTypes.bool,
interactive?: boolean;
/**
* Rend la boîte muette : elle garde sa couleur mais devient peu visible
*
* @default false
*/
muted: PropTypes.bool,
muted?: boolean;
/**
* Ajoute une ombre
*
* @default false
*/
shadow: PropTypes.bool,
shadow?: boolean;
/**
* Forme de la boîte
*
* @default 'square'
*/
shape: PropTypes.oneOf(['circle', 'rounded', 'square']),
shape?: 'circle' | 'rounded' | 'square';
/** @ignore */
theme: PropTypes.object,
theme?: ObjectOf<string>;
/**
* Active ou non les transitions sur les propriétés qui changent avec les
* intéractions de l'utilisateur (uniquement utilisée en combinaison avec la
* prop `interactive`)
*
* @default true
*/
transitions: PropTypes.bool,
transitions?: boolean;
/**
* Variante de la boîte
*
* @default 'plain'
*/
variant: PropTypes.oneOf(['text', 'outline', 'plain', 'gradient']),
variant?: 'text' | 'outline' | 'plain' | 'gradient';
}
const Box: OverridableComponent<BoxProps, 'div'> = (props) => {
const {
classes = {},
className: classNameProp,
color,
component: Component = 'div',
disabled = false,
htmlDisabled,
interactive = false,
muted = false,
shadow = false,
shape = 'square',
theme,
transitions = true,
variant = 'plain',
...otherProps
} = props;
// Bypass type checking, user must use `htmlDisabled` prop only when the `disabled` prop is available in `component`
(otherProps as { disabled?: boolean }).disabled = htmlDisabled;
return (
<Component
className={classNames(
classes.root,
classes[`${color}Color`],
classes[`${variant}Variant`],
classes[`${shape}Shape`],
{
[classes.shadow]: shadow,
[classes.muted]: muted,
[classes.disabled]: disabled,
[classes.interactive]: interactive,
[classes.transitions]: interactive && transitions,
},
classNameProp
)}
{...otherProps}
/>
);
};
/**
* @summary Boîte stylisée en fonction des couleurs du thème
*/
export default withStyles(styles, { withTheme: true })(Box);
......@@ -43,21 +43,14 @@ function Modal({
setPrevOpen(props.open);
}
useEffect(() => {
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, []);
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside, false);
return () => {
document.removeEventListener('mousedown', handleClickOutside, false);
};
}, []);
const exit = (e) => {
if (props.onClose) {
props.onClose(e);
if (!getHasTransition(props)) {
setExited(true);
}
}
};
const handleKeyDown = (e) => {
if (e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27) {
......@@ -78,6 +71,22 @@ function Modal({
exit(e);
};
useEffect(() => {
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, []);
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside, false);
return () => {
document.removeEventListener('mousedown', handleClickOutside, false);
};
}, []);
const hasTransition = getHasTransition(props);
const handleExited = () => {
......@@ -90,15 +99,6 @@ function Modal({
}
};
const exit = (e) => {
if (props.onClose) {
props.onClose(e);
if (!getHasTransition(props)) {
setExited(true);
}
}
};
const childProps = {};
if (hasTransition) {
......
......@@ -4,8 +4,8 @@ import path from 'path';
describe('@webalt/react', () => {
it('must not use "@webalt/react" in imports', () => {
const files = glob.sync(path.join(__dirname, './**/*.js'), {
ignore: ['**/demos/*.js', '**/*.test.js'],
const files = glob.sync(path.join(__dirname, './**/*.{js,ts,tsx}'), {
ignore: ['**/demos/*.{js,ts,tsx}', '**/*.test.{js,ts,tsx}'],
});
const regex = /^.+from\s*['"]@webalt\/react.+$/m;
......@@ -39,11 +39,12 @@ describe('@webalt/react', () => {
});
it('must export all components', () => {
const files = glob.sync(path.join(__dirname, './[A-Z]*/index.js'));
const files = glob.sync(path.join(__dirname, './[A-Z]*/index.{js,ts,tsx}'));
// eslint-disable-next-line @typescript-eslint/no-var-requires
const index = require('./index');