-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
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
Blocks: Canvas stories #19804
Blocks: Canvas stories #19804
Changes from 2 commits
ffa44b5
9be25a1
8a66906
fe04b8d
1cb1c26
2941d92
99dea36
45ad9e8
60cea66
1745efc
56777b9
c2b8465
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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' | ||
); | ||
}, | ||
}, | ||
], | ||
}, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,76 +1,66 @@ | ||
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 { Framework } from '@storybook/types'; | ||
import type { PreviewProps as PurePreviewProps } from '../components'; | ||
import { Preview as PurePreview, PreviewSkeleton } from '../components'; | ||
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 = ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It turned out this was actually a hook, just not named as such (which surfaced errors) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The diff for this function may seem like a lot, but it maintains the exact same functionality and logic, it's just a refactoring. The refactoring was sparked by the need for |
||
{ withSource, mdxSource, children, ...props }: CanvasProps & { children?: ReactNode }, | ||
docsContext: DocsContextProps<Framework>, | ||
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, | ||
}, | ||
}; | ||
}; | ||
|
||
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 />; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 = ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also a hook, actually. |
||
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} />; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
mdxSource
prop should be deprecated (and moved to internal stories) and renamed to something that falls in line with the naming in parameters, sosource
orcode
. I haven't quite figured out what the difference between the two yet.any thoughts @tmeasday @shilman ?