diff --git a/addons/docs/src/frameworks/svelte/config.ts b/addons/docs/src/frameworks/svelte/config.ts index 4a4fd37a39fb..a5e41a726139 100644 --- a/addons/docs/src/frameworks/svelte/config.ts +++ b/addons/docs/src/frameworks/svelte/config.ts @@ -1,6 +1,7 @@ import { extractArgTypes } from './extractArgTypes'; import { extractComponentDescription } from '../../lib/docgen'; import { prepareForInline } from './prepareForInline'; +import { sourceDecorator } from './sourceDecorator'; export const parameters = { docs: { @@ -10,3 +11,5 @@ export const parameters = { extractComponentDescription, }, }; + +export const decorators = [sourceDecorator]; diff --git a/addons/docs/src/frameworks/svelte/sourceDecorator.test.ts b/addons/docs/src/frameworks/svelte/sourceDecorator.test.ts new file mode 100644 index 000000000000..e8e2dcc2be02 --- /dev/null +++ b/addons/docs/src/frameworks/svelte/sourceDecorator.test.ts @@ -0,0 +1,44 @@ +import { Args } from '@storybook/api'; +import { generateSvelteSource } from './sourceDecorator'; + +expect.addSnapshotSerializer({ + print: (val: any) => val, + test: (val) => typeof val === 'string', +}); + +function generateForArgs(args: Args, slotProperty: string = null) { + return generateSvelteSource({ name: 'Component' }, args, {}, slotProperty); +} + +describe('generateSvelteSource', () => { + test('boolean true', () => { + expect(generateForArgs({ bool: true })).toMatchInlineSnapshot(``); + }); + test('boolean false', () => { + expect(generateForArgs({ bool: false })).toMatchInlineSnapshot(``); + }); + test('null property', () => { + expect(generateForArgs({ propnull: null })).toMatchInlineSnapshot(``); + }); + test('string property', () => { + expect(generateForArgs({ str: 'mystr' })).toMatchInlineSnapshot(``); + }); + test('number property', () => { + expect(generateForArgs({ count: 42 })).toMatchInlineSnapshot(``); + }); + test('object property', () => { + expect(generateForArgs({ obj: { x: true } })).toMatchInlineSnapshot( + `` + ); + }); + test('multiple properties', () => { + expect(generateForArgs({ a: 1, b: 2 })).toMatchInlineSnapshot(``); + }); + test('slot property', () => { + expect(generateForArgs({ content: 'xyz', myProp: 'abc' }, 'content')).toMatchInlineSnapshot(` + + xyz + + `); + }); +}); diff --git a/addons/docs/src/frameworks/svelte/sourceDecorator.ts b/addons/docs/src/frameworks/svelte/sourceDecorator.ts new file mode 100644 index 000000000000..7b7998f18908 --- /dev/null +++ b/addons/docs/src/frameworks/svelte/sourceDecorator.ts @@ -0,0 +1,167 @@ +import { addons, StoryContext } from '@storybook/addons'; +import { ArgTypes, Args } from '@storybook/api'; + +import { SourceType, SNIPPET_RENDERED } from '../../shared'; + +/** + * Check if the sourcecode should be generated. + * + * @param context StoryContext + */ +const skipSourceRender = (context: StoryContext) => { + const sourceParams = context?.parameters.docs?.source; + const isArgsStory = context?.parameters.__isArgsStory; + + // always render if the user forces it + if (sourceParams?.type === SourceType.DYNAMIC) { + return false; + } + + // never render if the user is forcing the block to render code, or + // if the user provides code, or if it's not an args story. + return !isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE; +}; + +/** + * Transform a key/value to a svelte declaration as string. + * + * Default values are ommited + * + * @param key Key + * @param value Value + * @param argTypes Component ArgTypes + */ +function toSvelteProperty(key: string, value: any, argTypes: ArgTypes): string { + if (value === undefined || value === null) { + return null; + } + + // default value ? + if (argTypes[key] && argTypes[key].defaultValue === value) { + return null; + } + + if (value === true) { + return key; + } + + if (typeof value === 'string') { + return `${key}=${JSON.stringify(value)}`; + } + + return `${key}={${JSON.stringify(value)}}`; +} + +/** + * Extract a component name. + * + * @param component Component + */ +function getComponentName(component: any): string { + const { __docgen = {} } = component; + let { name } = __docgen; + + if (!name) { + return component.name; + } + + if (name.endsWith('.svelte')) { + name = name.substring(0, name.length - 7); + } + return name; +} + +/** + * Generate a svelte template. + * + * @param component Component + * @param args Args + * @param argTypes ArgTypes + * @param slotProperty Property used to simulate a slot + */ +export function generateSvelteSource( + component: any, + args: Args, + argTypes: ArgTypes, + slotProperty: string +): string { + const name = getComponentName(component); + + if (!name) { + return null; + } + + const props = Object.entries(args) + .filter(([k]) => k !== slotProperty) + .map(([k, v]) => toSvelteProperty(k, v, argTypes)) + .filter((p) => p) + .join(' '); + + const slotValue = slotProperty ? args[slotProperty] : null; + + if (slotValue) { + return `<${name} ${props}>\n ${slotValue}\n`; + } + + return `<${name} ${props}/>`; +} + +/** + * Check if the story component is a wrapper to the real component. + * + * A component can be annoted with @wrapper to indicate that + * it's just a wrapper for the real tested component. If it's the case + * then the code generated references the real component, not the wrapper. + * + * moreover, a wrapper can annotate a property with @slot : this property + * is then assumed to be an alias to the default slot. + * + * @param component Component + */ +function getWrapperProperties(component: any) { + const { __docgen } = component; + if (!__docgen) { + return { wrapper: false }; + } + + // the component should be declared as a wrapper + if (!__docgen.keywords.find((kw: any) => kw.name === 'wrapper')) { + return { wrapper: false }; + } + + const slotProp = __docgen.data.find((prop: any) => + prop.keywords.find((kw: any) => kw.name === 'slot') + ); + return { wrapper: true, slotProperty: slotProp?.name as string }; +} + +/** + * Svelte source decorator. + * @param storyFn Fn + * @param context StoryContext + */ +export const sourceDecorator = (storyFn: any, context: StoryContext) => { + const story = storyFn(); + + if (skipSourceRender(context)) { + return story; + } + + const channel = addons.getChannel(); + + const { parameters = {}, args = {} } = context || {}; + let { Component: component = {} } = story; + + const { wrapper, slotProperty } = getWrapperProperties(component); + if (wrapper) { + component = parameters.component; + } + + const source = generateSvelteSource(component, args, context?.argTypes, slotProperty); + + if (source) { + channel.emit(SNIPPET_RENDERED, (context || {}).id, source); + } + + return story; +}; diff --git a/examples/svelte-kitchen-sink/src/stories/__snapshots__/button.stories.storyshot b/examples/svelte-kitchen-sink/src/stories/__snapshots__/button.stories.storyshot index df6542344eef..33a9896cd07a 100644 --- a/examples/svelte-kitchen-sink/src/stories/__snapshots__/button.stories.storyshot +++ b/examples/svelte-kitchen-sink/src/stories/__snapshots__/button.stories.storyshot @@ -20,7 +20,7 @@ exports[`Storyshots Button Rounded 1`] = ` - You clicked + Rounded : 0 @@ -63,7 +63,7 @@ exports[`Storyshots Button Square 1`] = ` - You clicked + Square : 0 diff --git a/examples/svelte-kitchen-sink/src/stories/button.stories.js b/examples/svelte-kitchen-sink/src/stories/button.stories.js index 92e5cfeb8526..51dd804077d5 100644 --- a/examples/svelte-kitchen-sink/src/stories/button.stories.js +++ b/examples/svelte-kitchen-sink/src/stories/button.stories.js @@ -1,8 +1,9 @@ import ButtonView from './views/ButtonView.svelte'; +import Button from '../components/Button.svelte'; export default { title: 'Button', - component: ButtonView, + component: Button, }; const Template = (args) => ({ @@ -15,11 +16,11 @@ const Template = (args) => ({ export const Rounded = Template.bind({}); Rounded.args = { rounded: true, - message: 'Squared text', + text: 'Rounded', }; export const Square = Template.bind({}); Square.args = { rounded: false, - message: 'Squared text', + text: 'Square', }; diff --git a/examples/svelte-kitchen-sink/src/stories/views/ButtonView.svelte b/examples/svelte-kitchen-sink/src/stories/views/ButtonView.svelte index 3d6da76ecdb2..7306df6a054a 100644 --- a/examples/svelte-kitchen-sink/src/stories/views/ButtonView.svelte +++ b/examples/svelte-kitchen-sink/src/stories/views/ButtonView.svelte @@ -1,9 +1,10 @@