Skip to content

Commit

Permalink
Merge pull request #13125 from storybookjs/feat/sortArgs
Browse files Browse the repository at this point in the history
Controls: Add ArgsTable sorting
  • Loading branch information
shilman authored Mar 15, 2021
2 parents 6c0919a + 08ed392 commit 3be68b5
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 37 deletions.
6 changes: 4 additions & 2 deletions addons/controls/src/ControlsPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { FC } from 'react';
import { useArgs, useArgTypes, useParameter } from '@storybook/api';
import { ArgsTable, NoControlsWarning } from '@storybook/components';
import { ArgsTable, NoControlsWarning, SortType } from '@storybook/components';

import { PARAM_KEY } from './constants';

interface ControlsParameters {
sort?: SortType;
expanded?: boolean;
hideNoControlsWarning?: boolean;
}
Expand All @@ -13,7 +14,7 @@ export const ControlsPanel: FC = () => {
const [args, updateArgs, resetArgs] = useArgs();
const rows = useArgTypes();
const isArgsStory = useParameter<boolean>('__isArgsStory', false);
const { expanded, hideNoControlsWarning = false } = useParameter<ControlsParameters>(
const { expanded, sort, hideNoControlsWarning = false } = useParameter<ControlsParameters>(
PARAM_KEY,
{}
);
Expand All @@ -32,6 +33,7 @@ export const ControlsPanel: FC = () => {
updateArgs,
resetArgs,
inAddonPanel: true,
sort,
}}
/>
</>
Expand Down
17 changes: 12 additions & 5 deletions addons/docs/src/blocks/ArgsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ArgsTableProps as PureArgsTableProps,
ArgsTableError,
ArgTypes,
SortType,
TabbedArgsTable,
} from '@storybook/components';
import { Args } from '@storybook/addons';
Expand All @@ -22,6 +23,7 @@ import { lookupStoryId } from './Story';
interface BaseProps {
include?: PropDescriptor;
exclude?: PropDescriptor;
sort?: SortType;
}

type OfProps = BaseProps & {
Expand Down Expand Up @@ -111,11 +113,13 @@ const addComponentTabs = (
components: Record<string, Component>,
context: DocsContextProps,
include?: PropDescriptor,
exclude?: PropDescriptor
exclude?: PropDescriptor,
sort?: SortType
) => ({
...tabs,
...mapValues(components, (comp) => ({
rows: extractComponentArgTypes(comp, context, include, exclude),
sort,
})),
});

Expand Down Expand Up @@ -199,14 +203,16 @@ export const ComponentsTable: FC<ComponentsProps> = (props) => {

export const ArgsTable: FC<ArgsTableProps> = (props) => {
const context = useContext(DocsContext);
const { parameters: { subcomponents } = {} } = context;
const { parameters: { subcomponents, controls } = {} } = context;

const { include, exclude, components } = props as ComponentsProps;
const { include, exclude, components, sort: sortProp } = props as ComponentsProps;
const { story } = props as StoryProps;

const sort = sortProp || controls?.sort;

const main = getComponent(props, context);
if (story) {
return <StoryTable {...(props as StoryProps)} component={main} subcomponents={subcomponents} />;
return <StoryTable {...(props as StoryProps)} component={main} {...{ subcomponents, sort }} />;
}

if (!components && !subcomponents) {
Expand All @@ -220,14 +226,15 @@ export const ArgsTable: FC<ArgsTableProps> = (props) => {
}

if (components) {
return <ComponentsTable {...(props as ComponentsProps)} components={components} />;
return <ComponentsTable {...(props as ComponentsProps)} {...{ components, sort }} />;
}

const mainLabel = getComponentName(main);
return (
<ComponentsTable
{...(props as ComponentsProps)}
components={{ [mainLabel]: main, ...subcomponents }}
sort={sort}
/>
);
};
Expand Down
70 changes: 43 additions & 27 deletions docs/essentials/controls.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ By default, Storybook will render a free text input for the `variant` arg:

![Essential addon Controls using a string](addon-controls-args-variant-string.png)


This works as long as you type a valid string into the auto-generated text control, but it's not the best UI for our scenario, given that the component only accepts `primary` or `secondary` as variants. Let’s replace it with Storybook’s radio component.

We can specify which controls get used by declaring a custom [argType](../api/argtypes.md) for the `variant` property. ArgTypes encode basic metadata for args, such as name, description, defaultValue for an arg. These get automatically filled in by Storybook Docs.
Expand All @@ -87,13 +86,13 @@ This replaces the input with a radio group for a more intuitive experience.

For a few types, Controls will automatically infer them by using [regex](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp). You can change the matchers for a regex that suits you better.

| Data type | Default regex | Description |
| :---------: | :-----------------------: | :-------------------------------------------------------: |
| **color** | `/(background\|color)$/i` | Will display a color picker UI for the args that match it |
| **date** | `/Date$/` | Will display a date picker UI for the args that match it |

| Data type | Default regex | Description |
| :-------: | :-----------------------: | :-------------------------------------------------------: |
| **color** | `/(background\|color)$/i` | Will display a color picker UI for the args that match it |
| **date** | `/Date$/` | Will display a date picker UI for the args that match it |

To do so, use the `matchers` property in `controls` parameter:

<!-- prettier-ignore-start -->

<CodeSnippets
Expand Down Expand Up @@ -178,23 +177,23 @@ As shown above, you can configure individual controls with the “control" annot

Here is the full list of available controls you can use:

| Data Type | Control Type | Description | Options |
| :---------- | :----------: | :------------------------------------------------------------- | :------------: |
| **boolean** | boolean | checkbox input | - |
| **number** | number | a numeric text box input | min, max, step |
| | range | a range slider input | min, max, step |
| **object** | object | json editor text input | - |
| **array** | object | json editor text input | - |
| | file | a file input that gives you a array of urls | accept |
| **enum** | radio | radio buttons input | - |
| | inline-radio | inline radio buttons input | - |
| | check | multi-select checkbox input | - |
| | inline-check | multi-select inline checkbox input | - |
| | select | select dropdown input | - |
| | multi-select | multi-select dropdown input | - |
| **string** | text | simple text input | - |
| | color | color picker input that assumes strings are color values | - |
| | date | date picker input | - |
| Data Type | Control Type | Description | Options |
| :---------- | :----------: | :------------------------------------------------------- | :------------: |
| **boolean** | boolean | checkbox input | - |
| **number** | number | a numeric text box input | min, max, step |
| | range | a range slider input | min, max, step |
| **object** | object | json editor text input | - |
| **array** | object | json editor text input | - |
| | file | a file input that gives you a array of urls | accept |
| **enum** | radio | radio buttons input | - |
| | inline-radio | inline radio buttons input | - |
| | check | multi-select checkbox input | - |
| | inline-check | multi-select inline checkbox input | - |
| | select | select dropdown input | - |
| | multi-select | multi-select dropdown input | - |
| **string** | text | simple text input | - |
| | color | color picker input that assumes strings are color values | - |
| | date | date picker input | - |

If you need to customize a control for a number data type in your story, you can do it like so:

Expand Down Expand Up @@ -303,10 +302,27 @@ Consider the following story snippets:
<!-- prettier-ignore-start -->

<CodeSnippets
paths={[
'common/component-story-disable-controls-regex.js.mdx',
'common/component-story-disable-controls-regex.mdx.mdx'
]}
paths={[
'common/component-story-disable-controls-regex.js.mdx',
'common/component-story-disable-controls-regex.mdx.mdx'
]}
/>

<!-- prettier-ignore-end -->

## Sorting controls

By default, controls are unsorted and use whatever order the args data is processed in (`none`). It can also be configured to sort alphabetically by arg name (`alpha`) or alphabetically required args first (`requiredFirst`).

Consider the following snippet to force required args first:

<!-- prettier-ignore-start -->

<CodeSnippets
paths={[
'common/component-story-sort-controls.js.mdx',
'common/component-story-sort-controls.mdx.mdx'
]}
/>

<!-- prettier-ignore-end -->
11 changes: 11 additions & 0 deletions docs/snippets/common/component-story-sort-controls.js.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
```js
// YourComponent.stories.js | YourComponent.stories.ts

import { YourComponent } from './your-component'

export default {
title:'My Story',
component: YourComponent,
parameters: { controls: { sort: 'requiredFirst' } },
};
```
12 changes: 12 additions & 0 deletions docs/snippets/common/component-story-sort-controls.mdx.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
```md
<!--- YourComponent.stories.mdx -->

import { Meta } from '@storybook/addon-docs/blocks';
import { YourComponent } from './your-component'

<Meta
title='My Story'
component={YourComponent}
parameters={{ controls: { sort: 'requiredFirst' } }}
/>
```
33 changes: 33 additions & 0 deletions examples/official-storybook/stories/controls-sort.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';

export default {
title: 'Addons/Controls-Sort',
argTypes: {
x: { type: { required: true } },
y: { type: { required: true }, table: { category: 'foo' } },
z: {},
a: { type: { required: true } },
b: { table: { category: 'foo' } },
c: {},
},
args: {
x: 'x',
y: 'y',
z: 'z',
a: 'a',
b: 'b',
c: 'c',
},
parameters: { chromatic: { disable: true } },
};

const Template = (args: any) => <div>{args && <pre>{JSON.stringify(args, null, 2)}</pre>}</div>;

export const None = Template.bind({});
None.parameters = { controls: { sort: 'none' } };

export const Alpha = Template.bind({});
Alpha.parameters = { controls: { sort: 'alpha' } };

export const RequiredFirst = Template.bind({});
RequiredFirst.parameters = { controls: { sort: 'requiredFirst' } };
50 changes: 47 additions & 3 deletions lib/components/src/blocks/ArgsTable/ArgsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,15 @@ export enum ArgsTableError {
ARGS_UNSUPPORTED = 'Args unsupported. See Args documentation for your framework.',
}

export type SortType = 'alpha' | 'requiredFirst' | 'none';
type SortFn = (a: ArgType, b: ArgType) => number;

const sortFns: Record<SortType, SortFn | null> = {
alpha: (a: ArgType, b: ArgType) => a.name.localeCompare(b.name),
requiredFirst: (a: ArgType, b: ArgType) =>
Number(!!b.type?.required) - Number(!!a.type?.required) || a.name.localeCompare(b.name),
none: undefined,
};
export interface ArgsTableRowProps {
rows: ArgTypes;
args?: Args;
Expand All @@ -234,6 +243,7 @@ export interface ArgsTableRowProps {
compact?: boolean;
inAddonPanel?: boolean;
initialExpandedArgs?: boolean;
sort?: SortType;
}

export interface ArgsTableErrorProps {
Expand All @@ -254,7 +264,7 @@ type Sections = {
sections: Record<string, Section>;
};

const groupRows = (rows: ArgType) => {
const groupRows = (rows: ArgType, sort: SortType) => {
const sections: Sections = { ungrouped: [], ungroupedSubsections: {}, sections: {} };
if (!rows) return sections;

Expand All @@ -278,7 +288,37 @@ const groupRows = (rows: ArgType) => {
sections.ungrouped.push({ key, ...row });
}
});
return sections;

// apply sort
const sortFn = sortFns[sort];

const sortSubsection = (record: Record<string, Subsection>) => {
if (!sortFn) return record;
return Object.keys(record).reduce<Record<string, Subsection>>(
(acc, cur) => ({
...acc,
[cur]: record[cur].sort(sortFn),
}),
{}
);
};

const sorted = {
ungrouped: sections.ungrouped.sort(sortFn),
ungroupedSubsections: sortSubsection(sections.ungroupedSubsections),
sections: Object.keys(sections.sections).reduce<Record<string, Section>>(
(acc, cur) => ({
...acc,
[cur]: {
ungrouped: sections.sections[cur].ungrouped.sort(sortFn),
subsections: sortSubsection(sections.sections[cur].subsections),
},
}),
{}
),
};

return sorted;
};

/**
Expand Down Expand Up @@ -306,9 +346,13 @@ export const ArgsTable: FC<ArgsTableProps> = (props) => {
compact,
inAddonPanel,
initialExpandedArgs,
sort = 'none',
} = props as ArgsTableRowProps;

const groups = groupRows(pickBy(rows, (row) => !row?.table?.disable));
const groups = groupRows(
pickBy(rows, (row) => !row?.table?.disable),
sort
);

if (
groups.ungrouped.length === 0 &&
Expand Down

0 comments on commit 3be68b5

Please sign in to comment.