diff --git a/code/lib/store/template/stories/args.stories.ts b/code/lib/store/template/stories/args.stories.ts index 16b4e7bc0d03..e1e6eb7104f4 100644 --- a/code/lib/store/template/stories/args.stories.ts +++ b/code/lib/store/template/stories/args.stories.ts @@ -3,6 +3,7 @@ import type { PartialStoryFn, PlayFunctionContext, StoryContext } from '@storybo import { within } from '@storybook/testing-library'; import { expect } from '@storybook/jest'; import { pick } from 'lodash'; +import { STORY_ARGS_UPDATED, UPDATE_STORY_ARGS, RESET_STORY_ARGS } from '@storybook/core-events'; export default { component: globalThis.Components.Pre, @@ -71,10 +72,11 @@ export const Events = { const channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__; await within(canvasElement).findByText(/initial/); - await channel.emit('updateStoryArgs', { storyId: id, updatedArgs: { test: 'updated' } }); + await channel.emit(UPDATE_STORY_ARGS, { storyId: id, updatedArgs: { test: 'updated' } }); await within(canvasElement).findByText(/updated/); - await channel.emit('resetStoryArgs', { storyId: id }); + await channel.emit(RESET_STORY_ARGS, { storyId: id }); + await new Promise((resolve) => channel.once(STORY_ARGS_UPDATED, resolve)); await within(canvasElement).findByText(/initial/); }, }; diff --git a/code/lib/store/template/stories/rendering.stories.ts b/code/lib/store/template/stories/rendering.stories.ts index eae4c11f4d9d..4b2181bb5ea6 100644 --- a/code/lib/store/template/stories/rendering.stories.ts +++ b/code/lib/store/template/stories/rendering.stories.ts @@ -2,7 +2,12 @@ import { global as globalThis } from '@storybook/global'; import type { PlayFunctionContext } from '@storybook/types'; import { within, waitFor } from '@storybook/testing-library'; import { expect } from '@storybook/jest'; -import { FORCE_REMOUNT, RESET_STORY_ARGS, UPDATE_STORY_ARGS } from '@storybook/core-events'; +import { + FORCE_REMOUNT, + RESET_STORY_ARGS, + STORY_ARGS_UPDATED, + UPDATE_STORY_ARGS, +} from '@storybook/core-events'; export default { component: globalThis.Components.Button, @@ -40,21 +45,27 @@ export const ForceRemount = { export const ChangeArgs = { play: async ({ canvasElement, id }: PlayFunctionContext) => { const channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__; + + await channel.emit(RESET_STORY_ARGS, { storyId: id }); + await new Promise((resolve) => { + channel.once(STORY_ARGS_UPDATED, resolve); + }); + const button = await within(canvasElement).findByRole('button'); await button.focus(); await expect(button).toHaveFocus(); // Web-components: https://github.com/storybookjs/storybook/issues/19415 // Preact: https://github.com/storybookjs/storybook/issues/19504 + if (['web-components', 'html', 'preact'].includes(globalThis.storybookRenderer)) return; // When we change the args to the button, it should not remount await channel.emit(UPDATE_STORY_ARGS, { storyId: id, updatedArgs: { label: 'New Text' } }); + await within(canvasElement).findByText(/New Text/); await expect(button).toHaveFocus(); await channel.emit(RESET_STORY_ARGS, { storyId: id }); - await within(canvasElement).findByText(/Click me/); - await expect(button).toHaveFocus(); }, }; diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index f93135251e3b..e2a56ec7204b 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -1,5 +1,5 @@ import type { ConcreteComponent, Component, ComponentOptions } from 'vue'; -import { h, isReactive, reactive } from 'vue'; +import { h } from 'vue'; import type { DecoratorFunction, StoryContext, LegacyStoryFn } from '@storybook/types'; import { sanitizeStoryContextUpdate } from '@storybook/preview-api'; @@ -47,16 +47,11 @@ export function decorateStory( return decorators.reduce( (decorated: LegacyStoryFn, decorator) => (context: StoryContext) => { let story: VueRenderer['storyResult'] | undefined; - if (!isReactive(context.args)) context.args = reactive(context.args); - const decoratedStory: VueRenderer['storyResult'] = decorator((update) => { - const updatedArgs = - update?.args && !isReactive(update.args) - ? { ...update, args: reactive(update.args) } - : update; + const decoratedStory: VueRenderer['storyResult'] = decorator((update) => { story = decorated({ ...context, - ...sanitizeStoryContextUpdate(updatedArgs), + ...sanitizeStoryContextUpdate(update), }); return story; }, context); @@ -68,7 +63,8 @@ export function decorateStory( if (decoratedStory === story) { return story; } - return prepare(decoratedStory, story) as VueRenderer['storyResult']; + + return prepare(decoratedStory, h(story, context.args)) as VueRenderer['storyResult']; }, (context) => prepare(storyFn(context)) as LegacyStoryFn ); diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index b05f62796b28..6c2c68559812 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -1,8 +1,6 @@ /* eslint-disable no-param-reassign */ -import { dedent } from 'ts-dedent'; -import { createApp, h, reactive, toRefs } from 'vue'; +import { createApp, h, reactive } from 'vue'; import type { RenderContext, ArgsStoryFn } from '@storybook/types'; - import type { Args, StoryContext } from '@storybook/csf'; import type { StoryFnVueReturnType, VueRenderer } from './types'; @@ -27,39 +25,21 @@ const map = new Map< { vueApp: ReturnType; reactiveArgs: any } >(); +const elementMap = new Map(); + export function renderToCanvas( - { - storyFn, - forceRemount, - showMain, - showError, - showException, - name, - title, - storyContext, - }: RenderContext, + { storyFn, forceRemount, showMain, showException, storyContext }: RenderContext, canvasElement: VueRenderer['canvasElement'] ) { - let { reactiveArgs } = useReactive(storyContext); // fetch the story with the updated context (with reactive args) - const element: StoryFnVueReturnType = storyFn(storyContext); - - if (!element) { - showError({ - title: `Expecting a Vue component from the story: "${name}" of "${title}".`, - description: dedent` - Did you forget to return the Vue component from the story? - Use "() => ({ template: '' })" or "() => ({ components: MyComp, template: '' })" when defining the story. - `, - }); - return () => {}; - } - // getting the props from the render function + storyContext.args = reactive(storyContext.args); + const element: StoryFnVueReturnType = storyFn(); + elementMap.set(canvasElement, element); + const props = (element as any).render?.().props; - if (props) reactiveArgs = reactive(props); + const reactiveArgs = props ? reactive(props) : storyContext.args; const existingApp = map.get(canvasElement); - if (existingApp && !forceRemount) { updateArgs(existingApp.reactiveArgs, reactiveArgs); return () => { @@ -67,10 +47,14 @@ export function renderToCanvas( }; } + if (existingApp && forceRemount) teardown(existingApp.vueApp, canvasElement); + const storybookApp = createApp({ render() { + const renderedElement: any = elementMap.get(canvasElement); + const current = renderedElement && renderedElement.template ? renderedElement : element; map.set(canvasElement, { vueApp: storybookApp, reactiveArgs }); - return h(element, reactiveArgs); + return h(current, reactiveArgs); }, }); @@ -120,15 +104,3 @@ function teardown( storybookApp?.unmount(); if (map.has(canvasElement)) map.delete(canvasElement); } - -/** - * create a reactive args and return it and the refs to avoid losing reactivity when passing it to the story - * @param storyContext - * @returns - */ - -function useReactive(storyContext: StoryContext) { - const reactiveArgs = reactive(storyContext.args || {}); - storyContext.args = toRefs(reactiveArgs); - return { reactiveArgs, refsArgs: storyContext.args }; -}