diff --git a/code/lib/preview-api/template/stories/argMapping.stories.ts b/code/lib/preview-api/template/stories/argMapping.stories.ts index d5fa145700a7..655647660c9d 100644 --- a/code/lib/preview-api/template/stories/argMapping.stories.ts +++ b/code/lib/preview-api/template/stories/argMapping.stories.ts @@ -22,7 +22,7 @@ export default { decorators: [ (storyFn: PartialStoryFn, context: StoryContext) => { return storyFn({ - args: { object: { ...context.args } }, + args: { object: context.args }, }); }, ], diff --git a/code/lib/preview-api/template/stories/argTypes.stories.ts b/code/lib/preview-api/template/stories/argTypes.stories.ts index e0d2115d10ae..93b9540ebb40 100644 --- a/code/lib/preview-api/template/stories/argTypes.stories.ts +++ b/code/lib/preview-api/template/stories/argTypes.stories.ts @@ -8,7 +8,7 @@ export default { // Compose all the argTypes into `object`, so the pre component only needs a single prop decorators: [ (storyFn: PartialStoryFn, context: StoryContext) => - storyFn({ args: { object: { ...context.argTypes } } }), + storyFn({ args: { object: context.argTypes } }), ], argTypes: { componentArg: { type: 'string' }, diff --git a/code/lib/preview-api/template/stories/args.stories.ts b/code/lib/preview-api/template/stories/args.stories.ts index aa9719c4bf2c..7c80ddb679ec 100644 --- a/code/lib/preview-api/template/stories/args.stories.ts +++ b/code/lib/preview-api/template/stories/args.stories.ts @@ -20,8 +20,7 @@ export default { decorators: [ (storyFn: PartialStoryFn, context: StoryContext) => { const { argNames } = context.parameters; - const args = { ...context.args }; - const object = argNames ? pick(args, argNames) : args; + const object = argNames ? pick(context.args, argNames) : context.args; return storyFn({ args: { object } }); }, ], diff --git a/code/lib/preview-api/template/stories/decorators.stories.ts b/code/lib/preview-api/template/stories/decorators.stories.ts index a36eb100a739..91964f5002d3 100644 --- a/code/lib/preview-api/template/stories/decorators.stories.ts +++ b/code/lib/preview-api/template/stories/decorators.stories.ts @@ -40,13 +40,15 @@ export const Hooks = { // decorator that uses hooks (storyFn: PartialStoryFn, context: StoryContext) => { useEffect(() => {}); + return storyFn({ args: { ...context.args, text: `story ${context.args['text']}` } }); }, // conditional decorator, runs before the above - (storyFn: PartialStoryFn, context: StoryContext) => - context.args.condition + (storyFn: PartialStoryFn, context: StoryContext) => { + return context.args.condition ? storyFn() - : (context.originalStoryFn as ArgsStoryFn)(context.args, context), + : (context.originalStoryFn as ArgsStoryFn)(context.unmappedArgs, context); + }, ], args: { text: 'text', diff --git a/code/lib/preview-api/template/stories/hooks.stories.ts b/code/lib/preview-api/template/stories/hooks.stories.ts index 15a4fe90a2ea..4f937686c18b 100644 --- a/code/lib/preview-api/template/stories/hooks.stories.ts +++ b/code/lib/preview-api/template/stories/hooks.stories.ts @@ -8,6 +8,8 @@ export default { }; export const UseState = { + // parameters: { inheritAttrs: true }, + args: { label: 'Clicked 0 times' }, decorators: [ (story: PartialStoryFn) => { const [count, setCount] = useState(0); diff --git a/code/package.json b/code/package.json index 1a39ee3fba89..1b29d39ee0a3 100644 --- a/code/package.json +++ b/code/package.json @@ -246,6 +246,7 @@ "node-gyp": "^9.3.1", "nx": "16.2.1", "nx-cloud": "16.0.5", + "playwright-core": "^1.35.0", "prettier": "2.8.0", "process": "^0.11.10", "raf": "^3.4.1", diff --git a/code/renderers/vue3/package.json b/code/renderers/vue3/package.json index afd52124170b..e2aa670a703a 100644 --- a/code/renderers/vue3/package.json +++ b/code/renderers/vue3/package.json @@ -66,6 +66,7 @@ "vue-tsc": "latest" }, "peerDependencies": { + "lodash": "*", "vue": "^3.0.0" }, "engines": { diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index 0835bad6da18..9df22fa337e6 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -1,7 +1,8 @@ -import type { Component, ComponentOptions, ConcreteComponent } from 'vue'; +import type { ConcreteComponent, Component } from 'vue'; import { h } from 'vue'; -import type { DecoratorFunction, LegacyStoryFn, StoryContext } from '@storybook/types'; +import type { DecoratorFunction, StoryContext, LegacyStoryFn } from '@storybook/types'; import { sanitizeStoryContextUpdate } from '@storybook/preview-api'; + import type { VueRenderer } from './types'; /* @@ -11,59 +12,51 @@ import type { VueRenderer } from './types'; method on the ComponentOptions so end-users don't need to specify a "thunk" as a decorator. */ -function normalizeFunctionalComponent(options: ConcreteComponent): ComponentOptions { - return typeof options === 'function' ? { render: options, name: options.name } : options; -} - function prepare( rawStory: VueRenderer['storyResult'], innerStory?: ConcreteComponent ): Component | null { - const story = rawStory as ComponentOptions; - if (story === null) { - return null; + const story: Component = rawStory; + + if (!story || typeof story === 'function') { + return story; } - if (typeof story === 'function') return story; // we don't need to wrap a functional component nor to convert it to a component options + if (innerStory) { return { // Normalize so we can always spread an object - ...normalizeFunctionalComponent(story), + ...story, // we don't to normalize the story if it's a functional component as it's already returned components: { ...(story.components || {}), story: innerStory }, }; } - return { - render() { - return h(story); - }, - }; + const render = () => h(story); + return { render }; } export function decorateStory( storyFn: LegacyStoryFn, decorators: DecoratorFunction[] ): LegacyStoryFn { - return decorators.reduce( - (decorated: LegacyStoryFn, decorator) => (context: StoryContext) => { - let story: VueRenderer['storyResult'] | undefined; - - const decoratedStory: VueRenderer['storyResult'] = decorator((update) => { - const sanitizedUpdate = sanitizeStoryContextUpdate(update); - // update the args in a reactive way - if (update) sanitizedUpdate.args = Object.assign(context.args, sanitizedUpdate.args); - story = decorated({ ...context, ...sanitizedUpdate }); - return story; + const decoratedStoryFn = decorators.reduce((decoratedFn, decorator) => { + const decoratedFunc = (context: StoryContext) => + decorator((update) => { + const mergedContext = { ...context, ...sanitizeStoryContextUpdate(update) }; + context.args = mergedContext.args; + const storyResult = decoratedFn(context); + return storyResult; }, context); - if (!story) story = decorated(context); - - if (decoratedStory === story) { - return story; - } + return (context: StoryContext) => { + const story = decoratedFunc(context); + const innerStory = () => h(story, context.args); + return prepare(story, innerStory) as VueRenderer['storyResult']; + }; + }, storyFn); - const innerStory = () => h(story!); - return prepare(decoratedStory, innerStory) as VueRenderer['storyResult']; - }, - (context) => prepare(storyFn(context)) as LegacyStoryFn - ); + return (context: StoryContext) => { + const story = decoratedStoryFn(context); + story.inheritAttrs ??= context.parameters.inheritAttrs ?? false; + return prepare(story) as LegacyStoryFn; + }; } diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 552438220600..120911cc0351 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -1,10 +1,18 @@ /* eslint-disable no-param-reassign */ -import type { App } from 'vue'; -import { createApp, h, reactive, isVNode, isReactive } from 'vue'; -import type { ArgsStoryFn, RenderContext } from '@storybook/types'; +import type { App, ConcreteComponent } from 'vue'; +import { createApp, h, reactive, isReactive } from 'vue'; +import type { RenderContext, ArgsStoryFn } from '@storybook/types'; import type { Args, StoryContext } from '@storybook/csf'; -import type { StoryFnVueReturnType, StoryID, VueRenderer } from './types'; +import type { VueRenderer, StoryID } from './types'; + +const slotsMap = new Map< + StoryID, + { + component?: Omit, 'props'>; + reactiveSlots?: Args; + } +>(); export const render: ArgsStoryFn = (props, context) => { const { id, component: Component } = context; @@ -14,7 +22,7 @@ export const render: ArgsStoryFn = (props, context) => { ); } - return () => h(Component, props, generateSlots(context)); + return h(Component, props, createOrUpdateSlots(context)); }; // set of setup functions that will be called when story is created @@ -51,10 +59,9 @@ export function renderToCanvas( // normally storyFn should be call once only in setup function,but because the nature of react and how storybook rendering the decorators // we need to call here to run the decorators again // i may wrap each decorator in memoized function to avoid calling it if the args are not changed - const element = storyFn(); // call the story function to get the root element with all the decorators - const args = getArgs(element, storyContext); // get args in case they are altered by decorators otherwise use the args from the context + storyFn(); // call the story function to run the decorators when the args are changed - updateArgs(existingApp.reactiveArgs, args); + updateArgs(existingApp.reactiveArgs, storyContext.args); return () => { teardown(existingApp.vueApp, canvasElement); }; @@ -66,7 +73,7 @@ export function renderToCanvas( setup() { storyContext.args = reactive(storyContext.args); const rootElement = storyFn(); // call the story function to get the root element with all the decorators - const args = getArgs(rootElement, storyContext); // get args in case they are altered by decorators otherwise use the args from the context + const { args } = storyContext; // get the args from the story context const appState = { vueApp, reactiveArgs: reactive(args), @@ -74,9 +81,7 @@ export function renderToCanvas( map.set(canvasElement, appState); return () => { - // not passing args here as props - // treat the rootElement as a component without props - return h(rootElement); + return h(rootElement, appState.reactiveArgs); }; }, }); @@ -107,15 +112,6 @@ function generateSlots(context: StoryContext) { return reactive(Object.fromEntries(slots)); } -/** - * get the args from the root element props if it is a vnode otherwise from the context - * @param element is the root element of the story - * @param storyContext is the story context - */ - -function getArgs(element: StoryFnVueReturnType, storyContext: StoryContext) { - return element.props && isVNode(element) ? element.props : storyContext.args; -} /** * update the reactive args @@ -151,3 +147,15 @@ function teardown( storybookApp?.unmount(); if (map.has(canvasElement)) map.delete(canvasElement); } + +function createOrUpdateSlots(context: StoryContext) { + const { id: storyID, component } = context; + const slots = generateSlots(context); + if (slotsMap.has(storyID)) { + const app = slotsMap.get(storyID); + if (app?.reactiveSlots) updateArgs(app.reactiveSlots, slots); + return app?.reactiveSlots; + } + slotsMap.set(storyID, { component, reactiveSlots: slots }); + return slots; +} diff --git a/code/renderers/vue3/template/stories_vue3-vite-default-ts/CustomRenderFunctionalComponent.stories.ts b/code/renderers/vue3/template/stories_vue3-vite-default-ts/CustomRenderFunctionalComponent.stories.ts index bf70bc34f074..4bd04879b2e7 100644 --- a/code/renderers/vue3/template/stories_vue3-vite-default-ts/CustomRenderFunctionalComponent.stories.ts +++ b/code/renderers/vue3/template/stories_vue3-vite-default-ts/CustomRenderFunctionalComponent.stories.ts @@ -9,13 +9,13 @@ const meta = { // storybook render function is not a functional component. it returns a functional component or a component options render: (args) => { // create the slot contents as a functional components - const header = ({ title }: { title: string }) => h('h3', `${args.header} - Title: ${title}`); + const header = ({ title }: { title: string }) => h('h4', `${args.header} - Title: ${title}`); const defaultSlot = () => h('p', `${args.default}`); const footer = () => h('p', `${args.footer}`); // vue render function is a functional components return () => h('div', [ - `Custom render uses a functional component, and passes slots to the component:`, + h('h3', 'Custom render returns a functional component'), h(Reactivity, args, { header, default: defaultSlot, footer }), ]); }, diff --git a/code/renderers/vue3/template/stories_vue3-vite-default-ts/CustomRenderOptionsArgsFromData.stories.ts b/code/renderers/vue3/template/stories_vue3-vite-default-ts/CustomRenderOptionsArgsFromData.stories.ts index 9c904283d3bd..3e86a9dc6e19 100644 --- a/code/renderers/vue3/template/stories_vue3-vite-default-ts/CustomRenderOptionsArgsFromData.stories.ts +++ b/code/renderers/vue3/template/stories_vue3-vite-default-ts/CustomRenderOptionsArgsFromData.stories.ts @@ -23,9 +23,10 @@ const meta = { components: { Reactivity, }, - template: `
Custom render uses options api and binds args to data: + template: `
+

Custom render returns options API Component

- + diff --git a/code/renderers/vue3/template/stories_vue3-vite-default-ts/ReactiveDecorators.stories.ts b/code/renderers/vue3/template/stories_vue3-vite-default-ts/ReactiveDecorators.stories.ts index 143cd1784559..e6328439a02a 100644 --- a/code/renderers/vue3/template/stories_vue3-vite-default-ts/ReactiveDecorators.stories.ts +++ b/code/renderers/vue3/template/stories_vue3-vite-default-ts/ReactiveDecorators.stories.ts @@ -13,10 +13,10 @@ const meta = { default: { control: { type: 'text' } }, }, args: { - label: 'If you see this then the label arg was not reactive.', - default: 'If you see this then the default slot was not reactive.', - header: 'If you see this, the header slot was not reactive.', // this can be useless if you have custom render function that overrides the slot - footer: 'If you see this, the footer slot was not reactive.', + label: 'initial label', + default: 'initial default slot.', + header: 'initial header slot', // this can be useless if you have custom render function that overrides the slot + footer: 'initial footer slot.', }, play: async ({ canvasElement, id, args }) => { const channel = (globalThis as any).__STORYBOOK_ADDONS_CHANNEL__; @@ -51,7 +51,9 @@ export const DecoratorFunctionalComponent: Story = { decorators: [ (storyFn, context) => { const story = storyFn(); - return () => h('div', [h('h2', ['Decorator not using args']), [h(story)]]); + return () => { + return h('div', { style: 'border: 5px solid red' }, h(story)); + }; }, ], }; @@ -60,8 +62,10 @@ export const DecoratorFunctionalComponentArgsFromContext: Story = { decorators: [ (storyFn, context) => { const story = storyFn(); - return () => - h('div', [h('h2', ['Decorator using args.label: ', context.args.label]), [h(story)]]); + + return () => { + return h('div', { style: 'border: 5px solid blue' }, h(story, context.args)); + }; }, ], }; @@ -70,7 +74,8 @@ export const DecoratorComponentOptions: Story = { decorators: [ (storyFn, context) => { return { - template: '

Decorator not using args

', + components: { story: storyFn() }, + template: `
`, }; }, ], @@ -80,8 +85,9 @@ export const DecoratorComponentOptionsArgsFromData: Story = { decorators: [ (storyFn, context) => { return { + components: { story: storyFn() }, data: () => ({ args: context.args }), - template: '

Decorator using args.label: {{args.label}}

', + template: `
`, }; }, ], diff --git a/code/renderers/vue3/template/stories_vue3-vite-default-ts/Reactivity.vue b/code/renderers/vue3/template/stories_vue3-vite-default-ts/Reactivity.vue index 89880cb5b6cd..7e7c0c3631af 100644 --- a/code/renderers/vue3/template/stories_vue3-vite-default-ts/Reactivity.vue +++ b/code/renderers/vue3/template/stories_vue3-vite-default-ts/Reactivity.vue @@ -2,7 +2,7 @@ defineProps<{ label: string }>(); diff --git a/code/yarn.lock b/code/yarn.lock index b26865c09033..288c738bed17 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -7276,6 +7276,7 @@ __metadata: node-gyp: ^9.3.1 nx: 16.2.1 nx-cloud: 16.0.5 + playwright-core: ^1.35.0 prettier: 2.8.0 process: ^0.11.10 raf: ^3.4.1 @@ -7653,6 +7654,7 @@ __metadata: vue-component-type-helpers: latest vue-tsc: latest peerDependencies: + lodash: "*" vue: ^3.0.0 languageName: unknown linkType: soft