Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Addon-docs: Typescript inherited prop types #9110

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
50 changes: 45 additions & 5 deletions addons/docs/src/blocks/Props.tsx
Expand Up @@ -5,8 +5,8 @@ import {
PropsTable,
PropsTableError,
PropsTableProps,
PropsTableRowsProps,
PropsTableSectionsProps,
PropRowsProps,
PropDef,
TabsState,
} from '@storybook/components';
Expand Down Expand Up @@ -46,7 +46,7 @@ export const getComponentProps = (
component: Component,
{ exclude }: PropsProps,
{ parameters }: DocsContextProps
): PropsTableProps => {
): PropsTableProps | PropsTableSectionsProps => {
if (!component) {
return null;
}
Expand All @@ -59,9 +59,9 @@ export const getComponentProps = (
if (!extractProps) {
throw new Error(PropsTableError.PROPS_UNSUPPORTED);
}
let props = extractProps(component);
let props: PropsTableProps | PropsTableSectionsProps = extractProps(component);
if (!isNil(exclude)) {
const { rows } = props as PropsTableRowsProps;
const { rows } = props as PropRowsProps;
const { sections } = props as PropsTableSectionsProps;
if (rows) {
props = { rows: filterRows(rows, exclude) };
Expand All @@ -71,7 +71,47 @@ export const getComponentProps = (
});
}
}

if ((props as PropRowsProps).rows) {
const propSections = (props as PropRowsProps).rows.reduce(
(acc: { [key: string]: PropDef[] }, prop: PropDef) => {
if (prop.parent && prop.parent.name) {
if (!acc[prop.parent.name]) {
return { ...acc, [prop.parent.name]: [prop] };
}
return { ...acc, [prop.parent.name]: [...acc[prop.parent.name], prop] };
}
return acc;
},
{}
);
const propSectionsArray = Object.keys(propSections);
if (propSectionsArray.length > 1) {
// the props are with sections (inherited interfaces in typescript)

// find out what section is the components own props
// by default just use the first listed interface of props
let expanded: string[] = [propSectionsArray[0]];

// if any section names contain the componnet name, expand them by default
if (component.displayName) {
// find all sections that contain the component name
const nameMatch = propSectionsArray.filter(
section => section.indexOf(component.displayName) >= 0
);
//
if (nameMatch.length) {
expanded = nameMatch;
}
// if any section have only 1 prop, expand them by default
propSectionsArray.forEach(section => {
if (propSections[section].length === 1 && expanded.indexOf(section) < 0) {
expanded.push(section);
}
});
}
props = { sections: propSections, expanded };
}
}
return props;
} catch (err) {
return { error: err.message };
Expand Down
2 changes: 2 additions & 0 deletions addons/docs/src/frameworks/react/propTypes/handleProp.ts
Expand Up @@ -33,6 +33,8 @@ export function enhancePropTypesProp(extractedProp: ExtractedProp, rawDefaultPro
propDef.defaultValue = newDefaultValue;
}
}
// typescript information for types inheritance
propDef.parent = extractedProp.docgenInfo.parent;

return propDef;
}
Expand Down
3 changes: 2 additions & 1 deletion addons/docs/src/lib/docgen/types.ts
@@ -1,4 +1,4 @@
import { PropsTableProps } from '@storybook/components';
import { PropsTableSectionsProps, PropsTableProps, PropParent } from '@storybook/components';
import { Component } from '../../blocks/shared';

export type PropsExtractor = (component: Component) => PropsTableProps | null;
Expand Down Expand Up @@ -38,6 +38,7 @@ export interface DocgenInfo {
required: boolean;
description?: string;
defaultValue?: DocgenPropDefaultValue;
parent?: PropParent;
}

export enum TypeSystem {
Expand Down
25 changes: 25 additions & 0 deletions examples/official-storybook/components/TypescriptButton.tsx
@@ -0,0 +1,25 @@
import React, { FC } from 'react';

interface ButtonProps {
/** Own ButtonProps label */
label: string;

/** Another property */
property1?: number;
}

/** Component description imported from comments inside the component file
* This React component has its own properties but also accepts all the html `button` attributes.
*/
export const TypescriptButton: FC<ButtonProps & JSX.IntrinsicElements['button']> = ({
label,
...rest
}) => (
<button type="button" {...rest}>
{label}
</button>
);

TypescriptButton.defaultProps = {
label: 'label',
};
5 changes: 5 additions & 0 deletions examples/official-storybook/main.js
Expand Up @@ -67,6 +67,11 @@ module.exports = {
],
exclude: [/node_modules/, /dist/],
},
{
test: /\.(ts|tsx)$/,
exclude: [/node_modules/, /dist/, /\.(story|stories).(ts|tsx)$/],
loader: require.resolve('webpack-react-docgen-typescript'),
},
],
},
resolve: {
Expand Down
3 changes: 2 additions & 1 deletion examples/official-storybook/package.json
Expand Up @@ -62,6 +62,7 @@
"terser-webpack-plugin": "^2.1.2",
"ts-loader": "^6.0.0",
"uuid": "^3.3.2",
"webpack": "^4.33.0"
"webpack": "^4.33.0",
"webpack-react-docgen-typescript": "^0.9.5"
}
}
@@ -0,0 +1,11 @@
import React from 'react';
import { TypescriptButton } from '../../components/TypescriptButton';

export default {
title: `Addons/Docs/typescript`,
component: TypescriptButton,
};

export const Basic = () => <TypescriptButton label="Button" />;

export const AnotherStory = () => <TypescriptButton label="Secod" property1={10} />;
109 changes: 109 additions & 0 deletions lib/components/src/blocks/PropsTable/CollapsibleRow.tsx
@@ -0,0 +1,109 @@
import React, { FC } from 'react';
import { styled } from '@storybook/theming';
import { transparentize } from 'polished';
import { relative } from 'path';
import { PropDef } from './PropDef';
import { PropRows } from './PropRows';
import { Icons } from '../../icon/icon';

const ExpanderIcon = styled(Icons)(({ theme }) => ({
marginRight: 8,
marginLeft: -10,
marginTop: -2, // optical alignment
height: 12,
width: 12,
color:
theme.base === 'light'
? transparentize(0.25, theme.color.defaultText)
: transparentize(0.3, theme.color.defaultText),
border: 'none',
}));

const ClickIntercept = styled.button<{}>(() => ({
// reset button style
background: 'none',
border: 'none',
padding: '0',
font: 'inherit',
outline: 'inherit',

// add custom style
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
height: '100%',
width: '100%',
color: 'transparent',
cursor: 'row-resize !important',
}));

export interface CollapsibleRowProps {
section: string;
expanded: boolean;
rows: PropDef[];
numRows: number;
}

const NameTh = styled.th(({ theme }) => ({
fontWeight: theme.typography.weight.bold,
color: `${theme.color.defaultText} !important`, // overrides the default th style
display: 'flex',
alignItems: 'center',
position: 'relative',
}));

const Th = styled.th(({ theme }) => ({
fontWeight: theme.typography.weight.regular,
color: transparentize(0.2, theme.color.defaultText),
position: 'relative',
}));

const Tr = styled.tr(({ theme }) => ({
'&& > th': {
paddingTop: 10,
paddingBottom: 10,
},
'&:hover > th': {
backgroundColor: theme.background.hoverable,
boxShadow: `${theme.color.mediumlight} 0 - 1px 0 0 inset`,
},
}));

export const CollapsibleRow: FC<CollapsibleRowProps> = ({ section, expanded, numRows, rows }) => {
const [isExpanded, setIsExpanded] = React.useState(expanded);

let titleHelperText = `Show ${section}'s ${numRows} prop${numRows !== 1 ? 's' : ''}`;
if (isExpanded) {
titleHelperText = `Hide ${section}'s ${numRows} prop${numRows !== 1 ? 's' : ''}`;
}

return (
<>
<Tr>
<NameTh colSpan={1}>
<ClickIntercept
onClick={expanded === undefined ? undefined : () => setIsExpanded(!isExpanded)}
title={titleHelperText}
>
{titleHelperText}
</ClickIntercept>

{isExpanded ? <ExpanderIcon icon="arrowdown" /> : <ExpanderIcon icon="arrowright" />}
{section}
</NameTh>
<Th colSpan={2}>
<ClickIntercept
onClick={expanded === undefined ? undefined : () => setIsExpanded(!isExpanded)}
title={titleHelperText}
>
{titleHelperText}
</ClickIntercept>
{!isExpanded && `${numRows} prop${numRows !== 1 ? 's' : ''}`}
</Th>
</Tr>
{isExpanded && <PropRows section={section} rows={rows} />}
</>
);
};
7 changes: 7 additions & 0 deletions lib/components/src/blocks/PropsTable/PropDef.ts
Expand Up @@ -20,11 +20,18 @@ export interface PropSummaryValue {
export type PropType = PropSummaryValue;
export type PropDefaultValue = PropSummaryValue;

// the parent interface in case of typescript
export interface PropParent {
fileName?: string;
name: string;
}

export interface PropDef {
name: string;
type: PropType;
required: boolean;
description?: string;
defaultValue?: PropDefaultValue;
jsDocTags?: JsDocTags;
parent?: PropParent;
}
2 changes: 1 addition & 1 deletion lib/components/src/blocks/PropsTable/PropRow.tsx
Expand Up @@ -60,7 +60,7 @@ export const PropRow: FC<PropRowProps> = ({
return (
<tr>
<td>
<Name>{name}</Name>
<Name title={name}>{name}</Name>
{required ? <Required title="Required">*</Required> : null}
</td>
<td>
Expand Down
16 changes: 16 additions & 0 deletions lib/components/src/blocks/PropsTable/PropRows.tsx
@@ -0,0 +1,16 @@
import React, { FC } from 'react';
import { PropDef } from './PropDef';
import { PropRow } from './PropRow';

export interface PropRowsProps {
rows: PropDef[];
section?: string;
}

export const PropRows: FC<PropRowsProps> = ({ section, rows }) => (
<>
{rows.map(row => (
<PropRow key={`${section || ''}${row.name}`} row={row} />
))}
</>
);