Commit 3abe3b48 authored by Mickaël Bourgier's avatar Mickaël Bourgier
Browse files

🏷 Add a OverridableComponent type + type the return of withStyles()

parent da236cd1
......@@ -29,6 +29,18 @@ class ComponentDoc extends Component {
} = 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>
......@@ -47,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>
)}
......
......@@ -43,7 +43,15 @@ const formatPropType = (propType, depth = 0) => {
return propType.value.map((child) => formatPropType(child)).join(' | ');
}
return propType.name.replace(/"/g, "'");
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;
......@@ -21,5 +21,25 @@ module.exports = function (source) {
// 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)}`;
};
......@@ -2,7 +2,7 @@ import React from 'react';
import classNames from 'classnames';
import { withStyles } from '../styles';
import { ObjectOf } from '../types';
import { ObjectOf, OverridableComponent } from '../types';
import styles from './Box.styles';
......@@ -36,13 +36,6 @@ export interface BoxProps {
| 'dark'
| string;
/**
* Composant à utiliser en tant que composant racine
*
* @default 'div'
*/
component?: React.ElementType;
/**
* Désactive la boîte : elle perd sa couleur et devient grise
*
......@@ -103,13 +96,10 @@ export interface BoxProps {
variant?: 'text' | 'outline' | 'plain' | 'gradient';
}
/**
* @summary Boîte stylisée en fonction des couleurs du thème
*/
export const Box: React.FC<BoxProps> = (props) => {
const Box: OverridableComponent<BoxProps, 'div'> = (props) => {
const {
classes,
className: classNameProps,
classes = {},
className: classNameProp,
color,
component: Component = 'div',
disabled = false,
......@@ -124,6 +114,9 @@ export const Box: React.FC<BoxProps> = (props) => {
...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(
......@@ -140,12 +133,14 @@ export const Box: React.FC<BoxProps> = (props) => {
[classes.interactive]: interactive,
[classes.transitions]: interactive && transitions,
},
classNameProps
classNameProp
)}
disabled={htmlDisabled}
{...otherProps}
/>
);
};
/**
* @summary Boîte stylisée en fonction des couleurs du thème
*/
export default withStyles(styles, { withTheme: true })(Box);
......@@ -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,12 +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');
const regex = /([^/]+)\/index\.js$/;
const regex = /([^/]+)\/index\.[jt]s$/;
files.forEach((file) => {
const [, component] = regex.exec(file);
......
import React from 'react';
import injectSheet from 'react-jss';
const withStyles = (styles, { withTheme = false } = {}) => {
type WithStylesReturn = <T extends React.ElementType>(innerComponnent: T) => T;
const withStyles = (styles, { withTheme = false } = {}): WithStylesReturn => {
const inject = ['classes'];
if (withTheme) {
......
// Inspired by: https://github.com/mui-org/material-ui/blob/225badba70d8288213a0f733cce91801958b4e95/packages/material-ui/src/OverridableComponent.d.ts
import React from 'react';
interface OverridableComponent<P, D extends React.ElementType> {
<C extends React.ElementType>(
props: PropsWithComponent<P, C, D> | PropsWithoutComponent<P, D>
): JSX.Element;
}
interface OverridableComponentProps<C extends React.ElementType, D> {
/**
* Composant à utiliser en tant que composant racinefew
*/
component: C;
/**
* Sentinel value
* @see /docs/webpack-loaders/react-docgen.js
*/
__componentDefaultValue?: D;
}
// prettier-ignore
type PropsWithComponent<P, C extends React.ElementType, D> =
& OverridableComponentProps<C, D>
& Omit<P, keyof OverridableComponentProps<C, D>>
& Omit<
React.ComponentPropsWithoutRef<C>,
keyof P | keyof OverridableComponentProps<C, D>
>;
// prettier-ignore
type PropsWithoutComponent<P, D extends React.ElementType> =
& { component?: undefined }
& Omit<P, 'component'>
& Omit<React.ComponentPropsWithoutRef<D>, keyof P | 'component'>;
export default OverridableComponent;
export { default as ObjectOf } from './ObjectOf';
export { default as OverridableComponent } from './OverridableComponent';
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment