mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-07 07:21:17 +08:00
Merge branch 'next' into norbert/sb-798-figure-out-plan-for-package-structure-rework
This commit is contained in:
commit
6b9215730c
@ -403,7 +403,7 @@ jobs:
|
||||
- report-workflow-on-failure
|
||||
chromatic-internal-storybooks:
|
||||
executor:
|
||||
class: medium
|
||||
class: medium+
|
||||
name: sb_node_16_browsers
|
||||
steps:
|
||||
# switched this to the CircleCI helper to get the full git history for TurboSnap
|
||||
|
@ -1,4 +1,3 @@
|
||||
/* eslint-disable jest/no-standalone-expect */
|
||||
import React from 'react';
|
||||
import type { StoryObj, Meta } from '@storybook/react';
|
||||
import { CallStates } from '@storybook/instrumenter';
|
||||
|
@ -1,4 +1,3 @@
|
||||
/* eslint-disable jest/no-standalone-expect */
|
||||
import globalThis from 'global';
|
||||
import {
|
||||
within,
|
||||
|
@ -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",
|
||||
|
@ -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 = [
|
||||
{
|
||||
@ -18,7 +18,29 @@ const allStories = [
|
||||
titlePrefix: '@storybook-blocks',
|
||||
},
|
||||
];
|
||||
const blocksOnlyStories = ['../blocks/src/@(blocks|controls)/**/*.@(mdx|stories.@(tsx|ts|jsx|js))'];
|
||||
|
||||
/**
|
||||
* match all stories in blocks/src/blocks and blocks/src/controls EXCEPT blocks/src/blocks/internal
|
||||
* Examples:
|
||||
*
|
||||
* src/blocks/Canvas.stories.tsx - MATCH
|
||||
* src/blocks/internal/InternalCanvas.stories.tsx - IGNORED, internal stories
|
||||
* src/blocks/internal/nested/InternalCanvas.stories.tsx - IGNORED, internal stories
|
||||
*
|
||||
* src/blocks/Canvas.tsx - IGNORED, not story
|
||||
* src/blocks/nested/Canvas.stories.tsx - MATCH
|
||||
* src/blocks/nested/deep/Canvas.stories.tsx - MATCH
|
||||
*
|
||||
* src/controls/Boolean.stories.tsx - MATCH
|
||||
* src/controls/Boolean.tsx - IGNORED, not story
|
||||
*
|
||||
* src/components/ColorPalette.stories.tsx - MATCH
|
||||
* src/components/ColorPalette.tsx - IGNORED, not story
|
||||
*/
|
||||
const blocksOnlyStories = [
|
||||
'../blocks/src/@(blocks|controls)/!(internal)/**/*.@(mdx|stories.@(tsx|ts|jsx|js))',
|
||||
'../blocks/src/@(blocks|controls)/*.@(mdx|stories.@(tsx|ts|jsx|js))',
|
||||
];
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: isBlocksOnly ? blocksOnlyStories : allStories,
|
||||
@ -34,14 +56,17 @@ const config: StorybookConfig = {
|
||||
core: {
|
||||
disableTelemetry: true,
|
||||
},
|
||||
viteFinal: (vite, { configType }) => ({
|
||||
...vite,
|
||||
features: {
|
||||
interactionsDebugger: true,
|
||||
},
|
||||
viteFinal: (viteConfig, { configType }) => ({
|
||||
...viteConfig,
|
||||
plugins: [
|
||||
...(vite.plugins || []),
|
||||
...(viteConfig.plugins || []),
|
||||
csfPlugin({}),
|
||||
configType === 'PRODUCTION' ? pluginTurbosnap({ rootDir: vite.root || '' }) : [],
|
||||
configType === 'PRODUCTION' ? pluginTurbosnap({ rootDir: viteConfig.root || '' }) : [],
|
||||
],
|
||||
optimizeDeps: { ...vite.optimizeDeps, force: true },
|
||||
optimizeDeps: { ...viteConfig.optimizeDeps, force: true },
|
||||
}),
|
||||
};
|
||||
|
||||
|
83
code/ui/blocks/src/blocks/Canvas.stories.tsx
Normal file
83
code/ui/blocks/src/blocks/Canvas.stories.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Canvas } from './Canvas';
|
||||
import { Story as StoryComponent } from './Story';
|
||||
import * as BooleanStories from '../controls/Boolean.stories';
|
||||
|
||||
const meta: Meta<typeof Canvas> = {
|
||||
component: Canvas,
|
||||
parameters: {
|
||||
relativeCsfPaths: ['../controls/Boolean.stories'],
|
||||
},
|
||||
render: (args) => {
|
||||
return (
|
||||
<Canvas {...args}>
|
||||
<StoryComponent of={BooleanStories.Undefined} />
|
||||
</Canvas>
|
||||
);
|
||||
},
|
||||
};
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const BasicStory: Story = {};
|
||||
|
||||
export const WithSourceOpen: Story = {
|
||||
args: {
|
||||
withSource: 'open',
|
||||
},
|
||||
};
|
||||
export const WithSourceClosed: Story = {
|
||||
args: {
|
||||
withSource: 'closed',
|
||||
},
|
||||
};
|
||||
|
||||
// TODO: what is the purpose of mdxSource exactly?
|
||||
export const WithMdxSource: Story = {
|
||||
name: 'With MDX Source',
|
||||
args: {
|
||||
withSource: 'open',
|
||||
mdxSource: `const thisIsCustomSource = true;
|
||||
if (isSyntaxHighlighted) {
|
||||
console.log('syntax highlighting is working');
|
||||
}`,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutSource: Story = {
|
||||
args: {
|
||||
withSource: 'none',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithToolbar: Story = {
|
||||
args: {
|
||||
withToolbar: true,
|
||||
},
|
||||
};
|
||||
export const WithAdditionalActions: Story = {
|
||||
args: {
|
||||
additionalActions: [
|
||||
{
|
||||
title: 'Open in GitHub',
|
||||
onClick: () => {
|
||||
window.open(
|
||||
'https://github.com/storybookjs/storybook/blob/next/code/ui/blocks/src/controls/Boolean.stories.tsx',
|
||||
'_blank'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Go to documentation',
|
||||
onClick: () => {
|
||||
window.open(
|
||||
'https://storybook.js.org/docs/react/essentials/controls#annotation',
|
||||
'_blank'
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import type { FC, ReactElement, ReactNode, ReactNodeArray } from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import React, { Children, useContext } from 'react';
|
||||
import type { FC, ReactElement, ReactNode } from 'react';
|
||||
import type { Renderer } from '@storybook/types';
|
||||
import type { PreviewProps as PurePreviewProps } from '../components';
|
||||
import { Preview as PurePreview, PreviewSkeleton } from '../components';
|
||||
@ -7,62 +7,52 @@ import type { DocsContextProps } from './DocsContext';
|
||||
import { DocsContext } from './DocsContext';
|
||||
import type { SourceContextProps } from './SourceContainer';
|
||||
import { SourceContext } from './SourceContainer';
|
||||
import { getSourceProps, SourceState } from './Source';
|
||||
import { useSourceProps, SourceState } from './Source';
|
||||
import { useStories } from './useStory';
|
||||
|
||||
export { SourceState };
|
||||
|
||||
type CanvasProps = PurePreviewProps & {
|
||||
type CanvasProps = Omit<PurePreviewProps, 'isExpanded'> & {
|
||||
withSource?: SourceState;
|
||||
mdxSource?: string;
|
||||
};
|
||||
|
||||
const getPreviewProps = (
|
||||
const usePreviewProps = (
|
||||
{ withSource, mdxSource, children, ...props }: CanvasProps & { children?: ReactNode },
|
||||
docsContext: DocsContextProps<Renderer>,
|
||||
sourceContext: SourceContextProps
|
||||
) => {
|
||||
let sourceState = withSource;
|
||||
let isLoading = false;
|
||||
if (sourceState === SourceState.NONE) {
|
||||
return { isLoading, previewProps: props };
|
||||
}
|
||||
if (mdxSource) {
|
||||
return {
|
||||
isLoading,
|
||||
previewProps: {
|
||||
...props,
|
||||
withSource: getSourceProps({ code: decodeURI(mdxSource) }, docsContext, sourceContext),
|
||||
isExpanded: sourceState === SourceState.OPEN,
|
||||
},
|
||||
};
|
||||
}
|
||||
const childArray: ReactNodeArray = Array.isArray(children) ? children : [children];
|
||||
const storyChildren = childArray.filter(
|
||||
(c: ReactElement) => c.props && (c.props.id || c.props.name || c.props.of)
|
||||
) as ReactElement[];
|
||||
const targetIds = storyChildren.map(({ props: { id, of, name } }) => {
|
||||
if (id) return id;
|
||||
if (of) return docsContext.storyIdByModuleExport(of);
|
||||
/*
|
||||
get all story IDs by traversing through the children,
|
||||
filter out any non-story children (e.g. text nodes)
|
||||
and then get the id from each story depending on available props
|
||||
*/
|
||||
const storyIds = (Children.toArray(children) as ReactElement[])
|
||||
.filter((c) => c.props && (c.props.id || c.props.name || c.props.of))
|
||||
.map(({ props: { id, of, name } }) => {
|
||||
if (id) return id;
|
||||
if (of) return docsContext.storyIdByModuleExport(of);
|
||||
|
||||
return docsContext.storyIdByName(name);
|
||||
});
|
||||
|
||||
const sourceProps = getSourceProps({ ids: targetIds }, docsContext, sourceContext);
|
||||
if (!sourceState) sourceState = sourceProps.state;
|
||||
const storyIds = targetIds.map((targetId) => {
|
||||
return targetId;
|
||||
});
|
||||
return docsContext.storyIdByName(name);
|
||||
});
|
||||
|
||||
const stories = useStories(storyIds, docsContext);
|
||||
isLoading = stories.some((s) => !s);
|
||||
const isLoading = stories.some((s) => !s);
|
||||
const sourceProps = useSourceProps(
|
||||
mdxSource ? { code: decodeURI(mdxSource) } : { ids: storyIds },
|
||||
docsContext,
|
||||
sourceContext
|
||||
);
|
||||
if (withSource === SourceState.NONE) {
|
||||
return { isLoading, previewProps: props };
|
||||
}
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
previewProps: {
|
||||
...props, // pass through columns etc.
|
||||
withSource: sourceProps,
|
||||
isExpanded: sourceState === SourceState.OPEN,
|
||||
isExpanded: (withSource || sourceProps.state) === SourceState.OPEN,
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -70,7 +60,7 @@ const getPreviewProps = (
|
||||
export const Canvas: FC<CanvasProps> = (props) => {
|
||||
const docsContext = useContext(DocsContext);
|
||||
const sourceContext = useContext(SourceContext);
|
||||
const { isLoading, previewProps } = getPreviewProps(props, docsContext, sourceContext);
|
||||
const { isLoading, previewProps } = usePreviewProps(props, docsContext, sourceContext);
|
||||
const { children } = props;
|
||||
|
||||
if (isLoading) return <PreviewSkeleton />;
|
||||
|
@ -89,7 +89,7 @@ const getSnippet = (snippet: string, story?: Store_Story<any>): string => {
|
||||
type SourceStateProps = { state: SourceState };
|
||||
type PureSourceProps = ComponentProps<typeof PureSource>;
|
||||
|
||||
export const getSourceProps = (
|
||||
export const useSourceProps = (
|
||||
props: SourceProps,
|
||||
docsContext: DocsContextProps<any>,
|
||||
sourceContext: SourceContextProps
|
||||
@ -100,8 +100,7 @@ export const getSourceProps = (
|
||||
const singleProps = props as SingleSourceProps;
|
||||
const multiProps = props as MultiSourceProps;
|
||||
|
||||
let source = codeProps.code; // prefer user-specified code
|
||||
let { format } = codeProps; // prefer user-specified code
|
||||
let { format, code: source } = codeProps; // prefer user-specified code
|
||||
|
||||
const targetIds = multiProps.ids || [singleProps.id || primaryId];
|
||||
const storyIds = targetIds.map((targetId) => {
|
||||
@ -151,6 +150,6 @@ export const getSourceProps = (
|
||||
export const Source: FC<PureSourceProps> = (props) => {
|
||||
const sourceContext = useContext(SourceContext);
|
||||
const docsContext = useContext(DocsContext);
|
||||
const sourceProps = getSourceProps(props, docsContext, sourceContext);
|
||||
const sourceProps = useSourceProps(props, docsContext, sourceContext);
|
||||
return <PureSource {...sourceProps} />;
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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 };
|
||||
|
120
code/ui/blocks/src/blocks/internal/InternalCanvas.stories.tsx
Normal file
120
code/ui/blocks/src/blocks/internal/InternalCanvas.stories.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
/// <reference types="@types/jest" />;
|
||||
/// <reference types="@testing-library/jest-dom" />;
|
||||
import React from 'react';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { userEvent, waitFor, within } from '@storybook/testing-library';
|
||||
import { expect } from '@storybook/jest';
|
||||
import { Canvas } from '../Canvas';
|
||||
import { Story as StoryComponent } from '../Story';
|
||||
import * as BooleanStories from '../../controls/Boolean.stories';
|
||||
|
||||
const meta: Meta<typeof Canvas> = {
|
||||
title: 'Blocks/Internal/Canvas',
|
||||
component: Canvas,
|
||||
parameters: {
|
||||
relativeCsfPaths: ['../controls/Boolean.stories'],
|
||||
},
|
||||
render: (args) => {
|
||||
return (
|
||||
<Canvas {...args}>
|
||||
<StoryComponent of={BooleanStories.Undefined} />
|
||||
</Canvas>
|
||||
);
|
||||
},
|
||||
};
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const expectAmountOfStoriesInSource =
|
||||
(amount: number): Story['play'] =>
|
||||
async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Arrange - find the "Show code" button
|
||||
let showCodeButton = canvas.getByText('Show code');
|
||||
await waitFor(() => {
|
||||
showCodeButton = canvas.getByText('Show code');
|
||||
expect(showCodeButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Act - click button to show code
|
||||
await userEvent.click(showCodeButton);
|
||||
|
||||
// Assert - check that the correct amount of stories' source is shown
|
||||
await waitFor(async () => {
|
||||
const booleanControlNodes = await canvas.findAllByText('BooleanControl');
|
||||
expect(booleanControlNodes).toHaveLength(amount);
|
||||
});
|
||||
};
|
||||
|
||||
export const MultipleChildren: Story = {
|
||||
render: (args) => {
|
||||
return (
|
||||
<Canvas {...args}>
|
||||
<StoryComponent of={BooleanStories.True} />
|
||||
<StoryComponent of={BooleanStories.False} />
|
||||
</Canvas>
|
||||
);
|
||||
},
|
||||
play: expectAmountOfStoriesInSource(2),
|
||||
};
|
||||
|
||||
export const MultipleChildrenColumns: Story = {
|
||||
args: {
|
||||
isColumn: true,
|
||||
},
|
||||
render: (args) => {
|
||||
return (
|
||||
<Canvas {...args}>
|
||||
<StoryComponent of={BooleanStories.True} />
|
||||
<StoryComponent of={BooleanStories.False} />
|
||||
</Canvas>
|
||||
);
|
||||
},
|
||||
play: expectAmountOfStoriesInSource(2),
|
||||
};
|
||||
|
||||
export const MultipleChildrenThreeColumns: Story = {
|
||||
args: {
|
||||
columns: 3,
|
||||
},
|
||||
render: (args) => {
|
||||
return (
|
||||
<Canvas {...args}>
|
||||
<StoryComponent of={BooleanStories.True} />
|
||||
<StoryComponent of={BooleanStories.True} />
|
||||
<StoryComponent of={BooleanStories.True} />
|
||||
<StoryComponent of={BooleanStories.False} />
|
||||
<StoryComponent of={BooleanStories.False} />
|
||||
<StoryComponent of={BooleanStories.False} />
|
||||
<StoryComponent of={BooleanStories.Undefined} />
|
||||
<StoryComponent of={BooleanStories.Undefined} />
|
||||
<StoryComponent of={BooleanStories.Undefined} />
|
||||
</Canvas>
|
||||
);
|
||||
},
|
||||
play: expectAmountOfStoriesInSource(9),
|
||||
};
|
||||
|
||||
export const MixedChildrenStories: Story = {
|
||||
args: { isColumn: true },
|
||||
render: (args) => {
|
||||
return (
|
||||
<Canvas {...args}>
|
||||
<h1>Headline for Boolean Controls true</h1>
|
||||
<StoryComponent of={BooleanStories.True} />
|
||||
<h1>Headline for Boolean Controls undefined</h1>
|
||||
<StoryComponent of={BooleanStories.Undefined} />
|
||||
</Canvas>
|
||||
);
|
||||
},
|
||||
play: async (args) => {
|
||||
// this function will also expand the source code
|
||||
await expectAmountOfStoriesInSource(2)(args);
|
||||
const canvas = within(args.canvasElement);
|
||||
|
||||
// Assert - only find two headlines, those in the story, and none in the source code
|
||||
expect(canvas.queryAllByText(/Headline for Boolean Controls/i)).toHaveLength(2);
|
||||
},
|
||||
};
|
8
code/ui/blocks/src/blocks/internal/README.md
Normal file
8
code/ui/blocks/src/blocks/internal/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
# Internal `@storybook/blocks` Stories
|
||||
|
||||
This directory contains stories that are not suitable for public documentation, but that we still want to keep to ensure things don't break.
|
||||
|
||||
Some blocks have deprecated features that users shouldn't use moving forward, and these internal stories represents those.
|
||||
That way we can still test them and ensure the features work, until they are removed for good.
|
||||
|
||||
This directory is not part of the (public) Blocks Storybook, but are included in the full UI Storybook.
|
@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -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}
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"rootDir": "./src",
|
||||
"types": ["jest"]
|
||||
},
|
||||
|
@ -28,6 +28,13 @@ module.exports = {
|
||||
'import/extensions': ['error', 'always'],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.stories.*'],
|
||||
rules: {
|
||||
// allow expect in stories
|
||||
'jest/no-standalone-expect': ['off'],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'*.js',
|
||||
|
Loading…
x
Reference in New Issue
Block a user