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: Refactor Props to use ArgsTable #10341

Merged
merged 10 commits into from
Apr 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
256 changes: 158 additions & 98 deletions addons/docs/src/blocks/Props.tsx
Original file line number Diff line number Diff line change
@@ -1,83 +1,101 @@
import React, { FunctionComponent, useContext } from 'react';

/* eslint-disable no-underscore-dangle */
import React, { FC, useContext, useEffect, useState, useCallback } from 'react';
import {
PropsTable,
PropsTableError,
PropsTableProps,
PropsTableRowsProps,
PropsTableSectionsProps,
PropDef,
TabsState,
ArgsTable,
ArgsTableProps,
ArgsTableError,
ArgTypes,
TabbedArgsTable,
} from '@storybook/components';
import { Args } from '@storybook/addons';
import { StoryStore } from '@storybook/client-api';
import Events from '@storybook/core-events';

import { DocsContext, DocsContextProps } from './DocsContext';
import { Component, CURRENT_SELECTION } from './types';
import { getComponentName } from './utils';
import { ArgTypesExtractor } from '../lib/docgen/types';
import { lookupStoryId } from './Story';

import { PropsExtractor } from '../lib/docgen/types';
import { extractProps as reactExtractProps } from '../frameworks/react/extractProps';
import { extractProps as vueExtractProps } from '../frameworks/vue/extractProps';

interface PropsProps {
interface BaseProps {
exclude?: string[];
of?: '.' | Component;
components?: {
}

type OfProps = BaseProps & {
of: '.' | Component;
};

type ComponentsProps = BaseProps & {
components: {
[label: string]: Component;
};
}
};

// FIXME: remove in SB6.0 & require config
const inferPropsExtractor = (framework: string): PropsExtractor | null => {
switch (framework) {
case 'react':
return reactExtractProps;
case 'vue':
return vueExtractProps;
default:
return null;
}
type StoryProps = BaseProps & {
story: '.' | string;
showComponents?: boolean;
};

const filterRows = (rows: PropDef[], exclude: string[]) =>
rows && rows.filter((row: PropDef) => !exclude.includes(row.name));
type PropsProps = BaseProps | OfProps | ComponentsProps | StoryProps;

export const getComponentProps = (
component: Component,
{ exclude }: PropsProps,
{ parameters }: DocsContextProps
): PropsTableProps => {
if (!component) {
return null;
const useArgs = (storyId: string, storyStore: StoryStore): [Args, (args: Args) => void] => {
const story = storyStore.fromId(storyId);
if (!story) {
throw new Error(`Unknown story: ${storyId}`);
}
try {
const params = parameters || {};
const { framework = null } = params;

const { extractProps = inferPropsExtractor(framework) }: { extractProps: PropsExtractor } =
params.docs || {};
if (!extractProps) {
throw new Error(PropsTableError.PROPS_UNSUPPORTED);
}
let props = extractProps(component);
if (exclude != null) {
const { rows } = props as PropsTableRowsProps;
const { sections } = props as PropsTableSectionsProps;
if (rows) {
props = { rows: filterRows(rows, exclude) };
} else if (sections) {
Object.keys(sections).forEach((section) => {
sections[section] = filterRows(sections[section], exclude);
});
const { args: initialArgs } = story;
const [args, setArgs] = useState(initialArgs);
useEffect(() => {
const cb = (changedId: string, newArgs: Args) => {
if (changedId === storyId) {
setArgs(newArgs);
}
}
};
storyStore._channel.on(Events.STORY_ARGS_UPDATED, cb);
return () => storyStore._channel.off(Events.STORY_ARGS_UPDATED, cb);
}, [storyId]);
const updateArgs = useCallback((newArgs) => storyStore.updateStoryArgs(storyId, newArgs), [
storyId,
]);
return [args, updateArgs];
};

return props;
} catch (err) {
return { error: err.message };
const filterArgTypes = (argTypes: ArgTypes, exclude?: string[]) => {
if (!exclude) {
return argTypes;
}
return (
argTypes &&
Object.entries(argTypes).reduce((acc, entry) => {
const [key, val] = entry;
const name = val.name || key;
if (!exclude.includes(name)) {
acc[key] = val;
}
return acc;
}, {} as ArgTypes)
);
};

export const extractComponentArgTypes = (
component: Component,
{ parameters }: DocsContextProps,
exclude?: string[]
): ArgTypes => {
const params = parameters || {};
const { extractArgTypes }: { extractArgTypes: ArgTypesExtractor } = params.docs || {};
if (!extractArgTypes) {
throw new Error(ArgsTableError.ARGS_UNSUPPORTED);
}
let argTypes = extractArgTypes(component);
argTypes = filterArgTypes(argTypes, exclude);

return argTypes;
};

export const getComponent = (props: PropsProps = {}, context: DocsContextProps): Component => {
const { of } = props;
const { of } = props as OfProps;
const { parameters = {} } = context;
const { component } = parameters;

Expand All @@ -86,60 +104,102 @@ export const getComponent = (props: PropsProps = {}, context: DocsContextProps):
if (of === CURRENT_SELECTION) {
return null;
}
throw new Error(PropsTableError.NO_COMPONENT);
throw new Error(ArgsTableError.NO_COMPONENT);
}
return target;
};

const PropsContainer: FunctionComponent<PropsProps> = (props) => {
const addComponentTabs = (
tabs: Record<string, ArgsTableProps>,
components: Record<string, Component>,
context: DocsContextProps,
exclude?: string[]
) =>
Object.entries(components).reduce(
(acc, [label, component]) => {
acc[label] = { rows: extractComponentArgTypes(component, context, exclude) };
return acc;
},
{ ...tabs }
);

export const StoryTable: FC<StoryProps & { components: Record<string, Component> }> = (props) => {
const context = useContext(DocsContext);
const {
id: currentId,
parameters: { argTypes },
storyStore,
} = context;
const { story, showComponents, components, exclude } = props;
let storyArgTypes;
try {
let storyId;
if (story === CURRENT_SELECTION) {
storyId = currentId;
storyArgTypes = argTypes;
} else {
storyId = lookupStoryId(story, context);
const data = storyStore.fromId(storyId);
storyArgTypes = data.parameters.argTypes;
}
storyArgTypes = filterArgTypes(storyArgTypes, exclude);
const [args, updateArgs] = useArgs(storyId, storyStore);
let tabs = { Story: { rows: storyArgTypes, args, updateArgs } } as Record<
string,
ArgsTableProps
>;
if (showComponents) {
tabs = addComponentTabs(tabs, components, context, exclude);
}

return <TabbedArgsTable tabs={tabs} />;
} catch (err) {
return <ArgsTable error={err.message} />;
}
};

export const ComponentsTable: FC<ComponentsProps> = (props) => {
const context = useContext(DocsContext);
const { components, exclude } = props;

const tabs = addComponentTabs({}, components, context, exclude);
return <TabbedArgsTable tabs={tabs} />;
};

export const Props: FC<PropsProps> = (props) => {
const context = useContext(DocsContext);
const { components } = props;
const {
parameters: { subcomponents },
} = context;

let allComponents = components;
if (!allComponents) {
const main = getComponent(props, context);
const mainLabel = getComponentName(main);
const mainProps = getComponentProps(main, props, context);
const { exclude, components } = props as ComponentsProps;
const { story } = props as StoryProps;

if (!subcomponents || typeof subcomponents !== 'object') {
return mainProps && <PropsTable {...mainProps} />;
}
let allComponents = components;
const main = getComponent(props, context);

if (!allComponents && main) {
const mainLabel = getComponentName(main);
allComponents = { [mainLabel]: main, ...subcomponents };
}

const tabs: { label: string; table: PropsTableProps }[] = [];
Object.entries(allComponents).forEach(([label, component]) => {
tabs.push({
label,
table: getComponentProps(component, props, context),
});
});
if (story) {
return <StoryTable {...(props as StoryProps)} components={allComponents} />;
}

return (
<TabsState>
{tabs.map(({ label, table }) => {
if (!table) {
return null;
}
const id = `prop_table_div_${label}`;
return (
<div key={id} id={id} title={label}>
{({ active }: { active: boolean }) =>
active ? <PropsTable key={`prop_table_${label}`} {...table} /> : null
}
</div>
);
})}
</TabsState>
);
};
if (!components && !subcomponents) {
let mainProps;
try {
mainProps = { rows: extractComponentArgTypes(main, context, exclude) };
} catch (err) {
mainProps = { error: err.message };
}
return <ArgsTable {...mainProps} />;
}

PropsContainer.defaultProps = {
of: '.',
return <ComponentsTable exclude={exclude} components={allComponents} />;
};

export { PropsContainer as Props };
Props.defaultProps = {
of: CURRENT_SELECTION,
};
35 changes: 18 additions & 17 deletions addons/docs/src/blocks/Story.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { createElement, ElementType, FunctionComponent, ReactNode } from 'react';
import { MDXProvider } from '@mdx-js/react';
import { components as docsComponents } from '@storybook/components/html';
import { Story, StoryProps as PureStoryProps } from '@storybook/components';
import { Story as PureStory, StoryProps as PureStoryProps } from '@storybook/components';
import { toId, storyNameFromExport } from '@storybook/csf';
import { CURRENT_SELECTION } from './types';

Expand Down Expand Up @@ -39,22 +39,23 @@ const inferInlineStories = (framework: string): boolean => {
}
};

export const getStoryProps = (
props: StoryProps,
{ id: currentId, storyStore, mdxStoryNameToKey, mdxComponentMeta }: DocsContextProps | null
): PureStoryProps => {
export const lookupStoryId = (
storyName: string,
{ mdxStoryNameToKey, mdxComponentMeta }: DocsContextProps
) =>
toId(
mdxComponentMeta.id || mdxComponentMeta.title,
storyNameFromExport(mdxStoryNameToKey[storyName])
);

export const getStoryProps = (props: StoryProps, context: DocsContextProps): PureStoryProps => {
const { id } = props as StoryRefProps;
const { name } = props as StoryDefProps;
const inputId = id === CURRENT_SELECTION ? currentId : id;
const previewId =
inputId ||
toId(
mdxComponentMeta.id || mdxComponentMeta.title,
storyNameFromExport(mdxStoryNameToKey[name])
);
const inputId = id === CURRENT_SELECTION ? context.id : id;
const previewId = inputId || lookupStoryId(name, context);

const { height, inline } = props;
const data = storyStore.fromId(previewId);
const data = context.storyStore.fromId(previewId);
const { framework = null } = (data && data.parameters) || {};

const docsParam = (data && data.parameters && data.parameters.docs) || {};
Expand Down Expand Up @@ -87,7 +88,7 @@ export const getStoryProps = (
};
};

const StoryContainer: FunctionComponent<StoryProps> = (props) => (
const Story: FunctionComponent<StoryProps> = (props) => (
<DocsContext.Consumer>
{(context) => {
const storyProps = getStoryProps(props, context);
Expand All @@ -97,17 +98,17 @@ const StoryContainer: FunctionComponent<StoryProps> = (props) => (
return (
<div id={storyBlockIdFromId(storyProps.id)}>
<MDXProvider components={resetComponents}>
<Story {...storyProps} />
<PureStory {...storyProps} />
</MDXProvider>
</div>
);
}}
</DocsContext.Consumer>
);

StoryContainer.defaultProps = {
Story.defaultProps = {
children: null,
name: null,
};

export { StoryContainer as Story };
export { Story };
Loading