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

Controls: Add ArgsTable sorting #13125

Merged
merged 7 commits into from
Mar 15, 2021
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
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