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

[Storybook] General consistency pass #7245

Merged
merged 12 commits into from
Oct 3, 2023
21 changes: 15 additions & 6 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ import { writingModeStyles } from './writing_mode.styles';
// once all EUI components are converted to Emotion
import '../dist/eui_theme_light.css';

/**
* Prop controls
*/

import type { CommonProps } from '../src/components/common';
import { hideStorybookControls } from './utils';

const preview: Preview = {
decorators: [
(Story, context) => (
Expand Down Expand Up @@ -86,6 +93,7 @@ const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
backgrounds: { disable: true }, // Use colorMode instead
options: { showPanel: true }, // default to showing the controls panel
controls: {
expanded: true,
sort: 'requiredFirst',
Expand All @@ -100,12 +108,13 @@ const preview: Preview = {
},
// Due to CommonProps, these props appear on almost every Story, but generally
// aren't super useful to test - let's disable them by default and (if needed)
// individual stories can re-enable them
argTypes: {
css: { table: { disable: true } },
className: { table: { disable: true } },
'data-test-subj': { table: { disable: true } },
},
// individual stories can re-enable them, e.g. by passing
// `argTypes: { 'data-test-subj': { table: { disable: false } } }`
argTypes: hideStorybookControls<CommonProps>([
'css',
'className',
'data-test-subj',
]),
};

export default preview;
49 changes: 49 additions & 0 deletions .storybook/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { hideStorybookControls, disableStorybookControls } from './utils';

describe('hideStorybookControls', () => {
it('outputs the expected `argTypes` object when passed prop name strings', () => {
expect(
hideStorybookControls(['isDisabled', 'isLoading', 'isInvalid'])
).toEqual({
isDisabled: { table: { disable: true } },
isLoading: { table: { disable: true } },
isInvalid: { table: { disable: true } },
});
});

it('throws a typescript error if a generic is passed and the prop names do not match', () => {
type TestComponentProps = { hello: boolean; world: boolean };
// No typescript error
hideStorybookControls<TestComponentProps>(['hello', 'world']);
// @ts-expect-error - will fail `yarn lint` if a TS error is *not* produced
hideStorybookControls<TestComponentProps>(['hello', 'world', 'error']);
});
});

describe('disableStorybookControls', () => {
it('outputs the expected `argTypes` object when passed prop name strings', () => {
expect(
disableStorybookControls(['isDisabled', 'isLoading', 'isInvalid'])
).toEqual({
isDisabled: { control: false },
isLoading: { control: false },
isInvalid: { control: false },
});
});

it('throws a typescript error if a generic is passed and the prop names do not match', () => {
type TestComponentProps = { hello: boolean; world: boolean };
// No typescript error
disableStorybookControls<TestComponentProps>(['hello', 'world']);
// @ts-expect-error - will fail `yarn lint` if a TS error is *not* produced
disableStorybookControls<TestComponentProps>(['hello', 'world', 'error']);
});
});
64 changes: 64 additions & 0 deletions .storybook/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

/**
* argTypes configurations
*/

/**
* Completely hide props from Storybook's controls panel.
* Should be passed or spread to `argTypes`
*/
export const hideStorybookControls = <Props>(
propNames: Array<keyof Props>
): Record<keyof Props, typeof HIDE_CONTROL> | {} => {
return propNames.reduce(
(obj, name) => ({ ...obj, [name]: HIDE_CONTROL }),
{}
);
};
const HIDE_CONTROL = { table: { disable: true } };

/**
* Leave props visible in Storybook's controls panel, but disable them
* from being controllable (renders a `-`).
*
* Should be passed or spread to `argTypes`
*/
export const disableStorybookControls = <Props>(
propNames: Array<keyof Props>
): Record<keyof Props, typeof DISABLE_CONTROL> | {} => {
return propNames.reduce(
(obj, name) => ({ ...obj, [name]: DISABLE_CONTROL }),
{}
);
};
const DISABLE_CONTROL = { control: false };

/**
* parameters configurations
*/

/**
* Will hide all props/controls. Pass to `parameters`
*
* TODO: Figure out some way to not show Storybook's "setup" text?
*/
export const hideAllStorybookControls = {
controls: { exclude: /.*/g },
};

/**
* Will hide the control/addon panel entirely for a specific story.
* Should be passed or spread to to `parameters`.
*
* Note that users can choose to re-show the panel in the UI
*/
export const hidePanel = {
options: { showPanel: false },
};
1 change: 1 addition & 0 deletions scripts/jest/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const config = {
'<rootDir>/scripts/babel',
'<rootDir>/scripts/tests',
'<rootDir>/scripts/eslint-plugin',
'<rootDir>/.storybook',
],
collectCoverageFrom: [
'src/{components,services,global_styling}/**/*.{ts,tsx,js,jsx}',
Expand Down
17 changes: 10 additions & 7 deletions src/components/button/button_empty/button_empty.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,8 @@ const meta: Meta<EuiButtonEmptyProps> = {
},
iconType: { control: 'text' },
},
};

export default meta;
type Story = StoryObj<EuiButtonEmptyProps>;

export const Playground: Story = {
args: {
children: 'Tertiary action',
// Component defaults
color: 'primary',
size: 'm',
iconSize: 'm',
Expand All @@ -37,3 +31,12 @@ export const Playground: Story = {
isSelected: false,
},
};

export default meta;
type Story = StoryObj<EuiButtonEmptyProps>;

export const Playground: Story = {
args: {
children: 'Tertiary action',
},
};
49 changes: 27 additions & 22 deletions src/components/button/button_group/button_group.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import React, { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { disableStorybookControls } from '../../../../.storybook/utils';

import {
EuiButtonGroup,
Expand All @@ -19,11 +20,6 @@ const meta: Meta<EuiButtonGroupProps> = {
title: 'EuiButtonGroup',
// @ts-ignore This still works for Storybook controls, even though Typescript complains
component: EuiButtonGroup,
parameters: {
controls: {
exclude: ['data-test-subj'],
},
},
argTypes: {
type: {
options: ['single', 'multi'],
Expand All @@ -44,6 +40,15 @@ const meta: Meta<EuiButtonGroupProps> = {
control: 'select',
},
},
args: {
// Component defaults
type: 'single',
buttonSize: 's',
color: 'text',
isDisabled: false,
isFullWidth: false,
isIconOnly: false,
},
};

export default meta;
Expand Down Expand Up @@ -76,6 +81,17 @@ const EuiButtonGroupSingle = (props: any) => {
);
};

export const SingleSelection: Story = {
render: ({ ...args }) => <EuiButtonGroupSingle {...args} />,
args: {
legend: 'EuiButtonGroup - single selection',
options,
type: 'single',
idSelected: 'button1',
},
argTypes: disableStorybookControls(['type']),
};

const EuiButtonGroupMulti = (props: any) => {
const [idToSelectedMap, setIdToSelectedMap] = useState<
Record<string, boolean>
Expand All @@ -100,24 +116,13 @@ const EuiButtonGroupMulti = (props: any) => {
);
};

export const Playground: Story = {
render: ({ ...args }) => {
if (args.type === 'multi') {
return <EuiButtonGroupMulti {...args} />;
} else {
return <EuiButtonGroupSingle {...args} />;
}
},
export const MultiSelection: Story = {
render: ({ ...args }) => <EuiButtonGroupMulti {...args} />,
args: {
legend: 'EuiButtonGroup demo',
type: 'single',
legend: 'EuiButtonGroup - multiple selections',
options,
idSelected: 'button1',
type: 'multi',
idToSelectedMap: { button1: true },
buttonSize: 's',
color: 'text',
isDisabled: false,
isFullWidth: false,
isIconOnly: false,
} as any,
},
argTypes: disableStorybookControls(['type']),
};
17 changes: 10 additions & 7 deletions src/components/button/button_icon/button_icon.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,8 @@ import { EuiButtonIcon, EuiButtonIconProps } from './button_icon';
const meta: Meta<EuiButtonIconProps> = {
title: 'EuiButtonIcon',
component: EuiButtonIcon,
};

export default meta;
type Story = StoryObj<EuiButtonIconProps>;

export const Playground: Story = {
args: {
iconType: 'faceHappy',
// Component defaults
color: 'primary',
display: 'empty',
size: 'xs',
Expand All @@ -30,3 +24,12 @@ export const Playground: Story = {
isSelected: false,
},
};

export default meta;
type Story = StoryObj<EuiButtonIconProps>;

export const Playground: Story = {
args: {
iconType: 'faceHappy',
},
};
11 changes: 7 additions & 4 deletions src/components/collapsible_nav/collapsible_nav.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ import { EuiCollapsibleNav, EuiCollapsibleNavProps } from './collapsible_nav';
const meta: Meta<EuiCollapsibleNavProps> = {
title: 'EuiCollapsibleNav',
component: EuiCollapsibleNav,
args: {
// Component defaults
isDocked: false,
dockedBreakpoint: 'l',
showButtonIfDocked: false,
size: 320,
},
// TODO: Improve props inherited from EuiFlyout, ideally through
// a DRY import from `flyout.stories.tsx` once that's created
};
Expand Down Expand Up @@ -43,9 +50,5 @@ export const Playground: Story = {
args: {
children: 'Collapsible nav content',
isOpen: true,
isDocked: false,
dockedBreakpoint: 'l',
showButtonIfDocked: false,
size: 240,
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ const meta: Meta<EuiCollapsibleNavGroupProps> = {
isDisabled: { if: { arg: 'isCollapsible' } },
element: { if: { arg: 'isCollapsible' } },
},
args: {
iconType: 'logoElastic',
// Component defaults
iconSize: 'l',
titleSize: 'xxs',
titleElement: 'h3',
background: 'none',
},
};

export default meta;
Expand All @@ -51,12 +59,7 @@ type Story = StoryObj<EuiCollapsibleNavGroupProps>;
export const Accordion: Story = {
args: {
children: 'This is an accordion group with a title',
background: 'none',
title: 'Nav group - accordion',
iconType: 'logoElastic',
iconSize: 'l',
titleElement: 'h3',
titleSize: 'xxs',
initialIsOpen: true,
isCollapsible: true,
},
Expand All @@ -65,19 +68,12 @@ export const Accordion: Story = {
export const NonAccordion: StoryObj<EuiCollapsibleNavGroupProps> = {
args: {
children: 'This is a group with a title',
background: 'none',
title: 'Nav group - non-accordion',
iconType: 'logoElastic',
iconSize: 'l',
titleElement: 'h3',
titleSize: 'xxs',
isCollapsible: false,
},
};

export const NoTitle: Story = {
args: {
children: 'This is a group without a title',
background: 'none',
},
};
Loading
Loading