Merge pull request #19805 from storybookjs/jeppe/sb-898-blocks-story

This commit is contained in:
Jeppe Reinhold 2022-11-16 15:20:02 +01:00 committed by GitHub
commit 2d815dc3cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 179 additions and 27 deletions

View File

@ -76,9 +76,9 @@
"publish:debug": "npm run publish:latest -- --npm-tag=debug --no-push",
"publish:latest": "lerna publish --exact --concurrency 1 --force-publish",
"publish:next": "npm run publish:latest -- --npm-tag=next",
"storybook:blocks": "BLOCKS_ONLY=true yarn storybook:ui",
"storybook:blocks:build": "BLOCKS_ONLY=true yarn storybook:ui:build",
"storybook:blocks:chromatic": "BLOCKS_ONLY=true yarn storybook:ui:chromatic --project-token=${CHROMATIC_TOKEN_STORYBOOK_BLOCKS:-MISSING_PROJECT_TOKEN}",
"storybook:blocks": "STORYBOOK_BLOCKS_ONLY=true yarn storybook:ui",
"storybook:blocks:build": "STORYBOOK_BLOCKS_ONLY=true yarn storybook:ui:build",
"storybook:blocks:chromatic": "STORYBOOK_BLOCKS_ONLY=true yarn storybook:ui:chromatic --project-token=${CHROMATIC_TOKEN_STORYBOOK_BLOCKS:-MISSING_PROJECT_TOKEN}",
"storybook:ui": "NODE_OPTIONS=\"--preserve-symlinks --preserve-symlinks-main\" ./lib/cli/bin/index.js dev --port 6006 --config-dir ./ui/.storybook --no-manager-cache",
"storybook:ui:build": "NODE_OPTIONS=\"--preserve-symlinks --preserve-symlinks-main\" ./lib/cli/bin/index.js build --config-dir ./ui/.storybook",
"storybook:ui:chromatic": "yarn chromatic --build-script-name storybook:ui:build --storybook-config-dir ./ui/.storybook --storybook-base-dir ./code/ui --project-token=${CHROMATIC_TOKEN_STORYBOOK_UI:-MISSING_PROJECT_TOKEN} --only-changed --exit-zero-on-changes --exit-once-uploaded",

View File

@ -2,7 +2,7 @@ import { vite as csfPlugin } from '@storybook/csf-plugin';
import pluginTurbosnap from 'vite-plugin-turbosnap';
import type { StorybookConfig } from '../../frameworks/react-vite/dist';
const isBlocksOnly = process.env.BLOCKS_ONLY === 'true';
const isBlocksOnly = process.env.STORYBOOK_BLOCKS_ONLY === 'true';
const allStories = [
{
@ -56,6 +56,9 @@ const config: StorybookConfig = {
core: {
disableTelemetry: true,
},
features: {
interactionsDebugger: true,
},
viteFinal: (viteConfig, { configType }) => ({
...viteConfig,
plugins: [

View File

@ -1,3 +1,5 @@
/// <reference types="vite/client" />
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { Story as StoryComponent } from './Story';
@ -6,15 +8,109 @@ import * as BooleanStories from '../controls/Boolean.stories';
const meta: Meta<typeof StoryComponent> = {
component: StoryComponent,
parameters: {
relativeCsfPaths: ['../controls/Boolean.stories'],
relativeCsfPaths: ['../controls/Boolean.stories', '../blocks/Story.stories'],
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const BasicOf: Story = {
export const Of: Story = {
args: {
of: BooleanStories.Undefined,
},
};
export const OfWithMeta: Story = {
args: {
of: BooleanStories.True,
meta: BooleanStories.default,
},
};
const blocksAwareId = `${
import.meta.env.STORYBOOK_BLOCKS_ONLY === 'true' ? '' : 'storybook-blocks-'
}controls-boolean--false`;
export const Id: Story = {
args: {
id: blocksAwareId,
},
};
export const Name: Story = {
args: {
name: 'True',
},
};
export const SimpleSizeTest: Story = {
render: () => {
return (
<div
style={{
background: '#fd5c9355',
padding: '3rem',
height: '1000px',
width: '800px',
// a global decorator is applying a default padding that we want to negate here
margin: '-4rem -20px',
}}
>
<p>
This story does nothing. Its only purpose is to show how its size renders in different
conditions (inline/iframe/fixed height) when used in a <code>{'<Story />'}</code> block.
</p>
<p>
It has a fixed <code>height</code> of <code>1000px</code> and a fixed <code>width</code>{' '}
of <code>800px</code>
</p>
</div>
);
},
};
export const Inline: Story = {
args: {
of: SimpleSizeTest,
inline: true,
},
};
export const InlineWithHeight: Story = {
...Inline,
args: {
of: SimpleSizeTest,
inline: true,
height: '300px',
},
};
export const Iframe: Story = {
...Inline,
args: {
of: SimpleSizeTest,
inline: false,
},
};
export const IframeWithHeight: Story = {
...Inline,
args: {
of: SimpleSizeTest,
inline: false,
height: '300px',
},
};
export const WithDefaultInteractions: Story = {
args: {
of: BooleanStories.Toggling,
},
};
export const WithInteractionsAutoplayInStory: Story = {
args: {
of: BooleanStories.TogglingInDocs,
},
};
// TODO: types suggest that <Story /> can take ProjectAnnotations, but it doesn't seem to do anything with them
// Such as parameters, decorators, etc.
// they seem to be taken from the story itself, and not from the <Story /> call

View File

@ -1,4 +1,4 @@
import type { FC, ReactNode, ElementType, ComponentProps } from 'react';
import type { FC, ComponentProps } from 'react';
import React, { useContext, useRef, useEffect, useState } from 'react';
import type {
Renderer,
@ -25,7 +25,6 @@ type CommonProps = StoryAnnotations & {
type StoryDefProps = {
name: string;
children: ReactNode;
};
type StoryRefProps = {
@ -36,7 +35,6 @@ type StoryRefProps = {
type StoryImportProps = {
name: string;
story: ElementType;
};
export type StoryProps = (StoryDefProps | StoryRefProps | StoryImportProps) & CommonProps;
@ -49,8 +47,7 @@ export const getStoryId = (props: StoryProps, context: DocsContextProps): StoryI
}
const { name } = props as StoryDefProps;
const inputId = id;
return inputId || context.storyIdByName(name);
return id || context.storyIdByName(name);
};
export const getStoryProps = <TFramework extends Renderer>(
@ -87,14 +84,16 @@ const Story: FC<StoryProps> = (props) => {
const [showLoader, setShowLoader] = useState(true);
useEffect(() => {
let cleanup: () => void;
if (story && storyRef.current) {
const element = storyRef.current as HTMLElement;
const { autoplay } = story.parameters.docs || {};
cleanup = context.renderStoryToElement(story, element, { autoplay });
setShowLoader(false);
if (!(story && storyRef.current)) {
return () => {};
}
return () => cleanup && cleanup();
const element = storyRef.current as HTMLElement;
const { autoplay } = story.parameters.docs || {};
const cleanup = context.renderStoryToElement(story, element, { autoplay });
setShowLoader(false);
return () => {
cleanup();
};
}, [context, story]);
if (!story) {
@ -115,7 +114,7 @@ const Story: FC<StoryProps> = (props) => {
return (
<div id={storyBlockIdFromId(story.id)}>
{height ? (
<style>{`#story--${story.id} { min-height: ${height}px; transform: translateZ(0); overflow: auto }`}</style>
<style>{`#story--${story.id} { min-height: ${height}; transform: translateZ(0); overflow: auto }`}</style>
) : null}
{showLoader && <StorySkeleton />}
<div
@ -134,9 +133,4 @@ const Story: FC<StoryProps> = (props) => {
);
};
Story.defaultProps = {
children: null,
name: null,
};
export { Story };

View File

@ -1,10 +1,20 @@
import { expect } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { within, fireEvent } from '@storybook/testing-library';
import { addons } from '@storybook/addons';
import { RESET_STORY_ARGS, STORY_ARGS_UPDATED } from '@storybook/core-events';
import { BooleanControl } from './Boolean';
const meta = {
component: BooleanControl,
tags: ['docsPage'],
parameters: { withRawArg: 'value', controls: { include: ['value'] } },
parameters: {
withRawArg: 'value',
controls: { include: ['value'] },
notes: 'These are notes for the Boolean control stories',
info: 'This is info for the Boolean control stories',
jsx: { useBooleanShorthandSyntax: false },
},
args: { name: 'boolean' },
} as Meta<typeof BooleanControl>;
@ -26,3 +36,45 @@ export const Undefined: StoryObj<typeof BooleanControl> = {
value: undefined,
},
};
export const Toggling: StoryObj<typeof BooleanControl> = {
args: {
value: undefined,
},
play: async ({ canvasElement, id }) => {
const channel = addons.getChannel();
channel.emit(RESET_STORY_ARGS, { storyId: id });
await new Promise<void>((resolve) => {
channel.once(STORY_ARGS_UPDATED, resolve);
});
const canvas = within(canvasElement);
// from Undefined to False
const setBooleanControl = canvas.getByText('Set boolean');
await fireEvent.click(setBooleanControl);
let toggle = await canvas.findByTitle('Change to true');
expect(toggle).toBeInTheDocument();
// from False to True
await fireEvent.click(toggle);
toggle = await canvas.findByTitle('Change to false');
expect(toggle).toBeInTheDocument();
// from True to False
await fireEvent.click(toggle);
toggle = await canvas.findByTitle('Change to true');
expect(toggle).toBeInTheDocument();
},
};
export const TogglingInDocs: StoryObj<typeof BooleanControl> = {
...Toggling,
parameters: {
docs: {
autoplay: true,
},
},
};

View File

@ -86,6 +86,11 @@ const Label = styled.label(({ theme }) => ({
const parse = (value: string | null): boolean => value === 'true';
export type BooleanProps = ControlProps<BooleanValue> & BooleanConfig;
/**
* # Boolean control
* Renders a switch toggle with "True" or "False".
* or if the value is `undefined`, renders a button to set the boolean.
*/
export const BooleanControl: FC<BooleanProps> = ({ name, value, onChange, onBlur, onFocus }) => {
const onSetFalse = useCallback(() => onChange(false), [onChange]);
if (value === undefined) {
@ -95,13 +100,14 @@ export const BooleanControl: FC<BooleanProps> = ({ name, value, onChange, onBlur
</Form.Button>
);
}
const controlId = getControlId(name);
const parsedValue = typeof value === 'string' ? parse(value) : value;
return (
<Label htmlFor={name} title={parsedValue ? 'Change to false' : 'Change to true'}>
<Label htmlFor={controlId} title={parsedValue ? 'Change to false' : 'Change to true'}>
<input
id={getControlId(name)}
id={controlId}
type="checkbox"
onChange={(e) => onChange(e.target.checked)}
checked={parsedValue}

View File

@ -1,6 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"module": "esnext",
"rootDir": "./src",
"types": ["jest"]
},