From 98dbf00fc753c28dcb833c1f69f2a568cd7da048 Mon Sep 17 00:00:00 2001 From: Benjamin Kindle Date: Sun, 18 Feb 2024 12:16:00 -0500 Subject: [PATCH] feat: support actions addon (#70) Basically entirely replacing the actions addon ArgsEnhancers with our own that also add inlineQrl so it works with Qwik. Fixes #16 --- .github/workflows/preview-and-test.yml | 2 +- README.md | 8 +++ packages/qwik-app/package.json | 1 + .../src/components/button/button.stories.ts | 19 ++++++ .../qwik-app/src/components/button/button.tsx | 21 ++++++ .../src/addArgsHelpers.ts | 64 +++++++++++++++++++ .../storybook-framework-qwik/src/preview.ts | 26 +++++++- .../template/cli/button.stories.ts | 14 +--- .../template/cli/button.tsx | 4 +- .../template/cli/header.stories.ts | 22 ++----- .../template/cli/header.tsx | 2 +- yarn.lock | 3 +- 12 files changed, 152 insertions(+), 34 deletions(-) create mode 100644 packages/qwik-app/src/components/button/button.stories.ts create mode 100644 packages/qwik-app/src/components/button/button.tsx create mode 100644 packages/storybook-framework-qwik/src/addArgsHelpers.ts diff --git a/.github/workflows/preview-and-test.yml b/.github/workflows/preview-and-test.yml index d967cd4..92526db 100644 --- a/.github/workflows/preview-and-test.yml +++ b/.github/workflows/preview-and-test.yml @@ -49,6 +49,6 @@ jobs: - name: Storybook Test if: ${{ env.vercel_token != '' }} run: | - yarn workspace qwik-app test-storybook + yarn workspace qwik-app playwright install && yarn workspace qwik-app test-storybook env: TARGET_URL: "${{ steps.deploy.outputs.url }}" diff --git a/README.md b/README.md index 14cb3f2..fdd6155 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,14 @@ You can also add the decorator to individual stories or story files. Because this framework is shipped only as an ESM module, this may require that you add `"type": "module"` to your `package.json` (or create a package.json inside your .storybook folder to only make this setting apply to storybook). +## Troubleshooting + +If you are using `MDX` files and get an error like: + +`Failed to resolve import "react/jsx-dev-runtime" ` + +you may need to install `react` as a dev dependency. + ## Demo There is a simple example Storybook using the latest version of this package [here](https://storybook-framework-qwik.vercel.app/) diff --git a/packages/qwik-app/package.json b/packages/qwik-app/package.json index 44afc86..6329ee8 100644 --- a/packages/qwik-app/package.json +++ b/packages/qwik-app/package.json @@ -40,6 +40,7 @@ "eslint": "8.56.0", "eslint-plugin-qwik": "1.4.1", "node-fetch": "3.3.2", + "playwright": "1.41.2", "prettier": "3.2.5", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/packages/qwik-app/src/components/button/button.stories.ts b/packages/qwik-app/src/components/button/button.stories.ts new file mode 100644 index 0000000..a19cbab --- /dev/null +++ b/packages/qwik-app/src/components/button/button.stories.ts @@ -0,0 +1,19 @@ +import type { Meta, StoryObj } from "storybook-framework-qwik"; +import type { ButtonProps } from "./button"; +import { Button } from "./button"; + +const meta = { + title: "Button", + args: {}, + argTypes: { + onClick$: { action: "onClick" }, + }, + component: Button, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args +export const Primary: Story = {}; diff --git a/packages/qwik-app/src/components/button/button.tsx b/packages/qwik-app/src/components/button/button.tsx new file mode 100644 index 0000000..5c2bffd --- /dev/null +++ b/packages/qwik-app/src/components/button/button.tsx @@ -0,0 +1,21 @@ +import type { PropFunction } from "@builder.io/qwik"; +import { component$ } from "@builder.io/qwik"; + +export interface ButtonProps { + /** + * Button contents + */ + label: string; + /** + * Optional click handler + */ + onClick$?: PropFunction | undefined; +} + +export type onClickEvent = (event: MouseEvent, element: Element) => void; + +export const Button = component$( + ({ label = "click me!", onClick$ }) => { + return ; + }, +); diff --git a/packages/storybook-framework-qwik/src/addArgsHelpers.ts b/packages/storybook-framework-qwik/src/addArgsHelpers.ts new file mode 100644 index 0000000..b90d0ec --- /dev/null +++ b/packages/storybook-framework-qwik/src/addArgsHelpers.ts @@ -0,0 +1,64 @@ +// This file is entirely copied from @storybook/addon-actions (changing the action import) + +import type { Args, Renderer, ArgsEnhancer } from "@storybook/types"; +import { action } from "@storybook/addon-actions"; + +const isInInitialArgs = (name: string, initialArgs: Args) => + typeof initialArgs[name] === "undefined" && !(name in initialArgs); + +/** + * Automatically add action args for argTypes whose name + * matches a regex, such as `^on.*` for react-style `onClick` etc. + */ + +export const inferActionsFromArgTypesRegex: ArgsEnhancer = ( + context, +) => { + const { + initialArgs, + argTypes, + parameters: { actions }, + } = context; + if (!actions || actions.disable || !actions.argTypesRegex || !argTypes) { + return {}; + } + + const argTypesRegex = new RegExp(actions.argTypesRegex); + const argTypesMatchingRegex = Object.entries(argTypes).filter( + ([name]) => !!argTypesRegex.test(name), + ); + + return argTypesMatchingRegex.reduce((acc, [name, argType]) => { + if (isInInitialArgs(name, initialArgs)) { + acc[name] = action(name); + } + return acc; + }, {} as Args); +}; + +/** + * Add action args for list of strings. + */ +export const addActionsFromArgTypes: ArgsEnhancer = (context) => { + const { + initialArgs, + argTypes, + parameters: { actions }, + } = context; + if (actions?.disable || !argTypes) { + return {}; + } + + const argTypesWithAction = Object.entries(argTypes).filter( + ([name, argType]) => !!argType["action"], + ); + + return argTypesWithAction.reduce((acc, [name, argType]) => { + if (isInInitialArgs(name, initialArgs)) { + acc[name] = action( + typeof argType["action"] === "string" ? argType["action"] : name, + ); + } + return acc; + }, {} as Args); +}; diff --git a/packages/storybook-framework-qwik/src/preview.ts b/packages/storybook-framework-qwik/src/preview.ts index 1ca8e57..f519b7f 100644 --- a/packages/storybook-framework-qwik/src/preview.ts +++ b/packages/storybook-framework-qwik/src/preview.ts @@ -1,7 +1,11 @@ -import { render as renderQwik } from "@builder.io/qwik"; -import { ArgsStoryFn, RenderContext } from "@storybook/types"; +import { inlinedQrl, render as renderQwik } from "@builder.io/qwik"; +import { ArgsEnhancer, ArgsStoryFn, RenderContext } from "@storybook/types"; import { QwikRenderer } from "./types.js"; import { componentToJSX } from "./component-to-jsx.js"; +import { + addActionsFromArgTypes, + inferActionsFromArgTypesRegex, +} from "./addArgsHelpers.js"; export { parameters, argTypesEnhancers } from "./docs/config.js"; // returns the Qwik component as a JSX element () @@ -33,3 +37,21 @@ if (viteHotMeta) { document.location.reload(); }); } + +const actionsArgsEnhancers: ArgsEnhancer[] = [ + addActionsFromArgTypes, + inferActionsFromArgTypesRegex, +]; + +export const argsEnhancers: ArgsEnhancer[] = + // use the argsEnhancers from addon-actions, then wrap the actions in Qwik's inlinedQrl function so things work. + actionsArgsEnhancers.map((actionsEnhancer: ArgsEnhancer) => { + return ((context) => { + const argsWithActions = actionsEnhancer(context); + let finalArgs: any = {}; + Object.keys(argsWithActions).forEach((key) => { + finalArgs[key] = inlinedQrl(argsWithActions[key], key); + }); + return finalArgs; + }) as ArgsEnhancer; + }); diff --git a/packages/storybook-framework-qwik/template/cli/button.stories.ts b/packages/storybook-framework-qwik/template/cli/button.stories.ts index 36b508c..9b26fdc 100644 --- a/packages/storybook-framework-qwik/template/cli/button.stories.ts +++ b/packages/storybook-framework-qwik/template/cli/button.stories.ts @@ -1,20 +1,12 @@ -import { action } from "@storybook/addon-actions"; -import { $ } from "@builder.io/qwik"; import type { Meta, StoryObj } from "storybook-framework-qwik"; -import type { ButtonProps, onClickEvent } from "./button"; +import type { ButtonProps } from "./button"; import { Button } from "./button"; const meta = { title: "Button", - args: { - // automatic actions are not yet supported. - // See https://github.com/literalpie/storybook-framework-qwik/issues/16 - // For now, use the legacy addon-actions API wrapped in a $ to make your own QRL action. - onClick$: $((event, element) => { - action("click action")({ event, element }); - }), - }, + args: { }, argTypes: { + onClick$: { action: 'onClick'}, backgroundColor: { control: "color" }, }, component: Button, diff --git a/packages/storybook-framework-qwik/template/cli/button.tsx b/packages/storybook-framework-qwik/template/cli/button.tsx index daea968..524b06a 100644 --- a/packages/storybook-framework-qwik/template/cli/button.tsx +++ b/packages/storybook-framework-qwik/template/cli/button.tsx @@ -1,4 +1,4 @@ -import type { PropFunction, QwikMouseEvent } from "@builder.io/qwik"; +import type { PropFunction } from "@builder.io/qwik"; import { component$, useStylesScoped$ } from "@builder.io/qwik"; import buttonStyles from "./button.css?inline"; @@ -37,7 +37,7 @@ export const getClassForSize = (size: "small" | "medium" | "large") => { }; export type onClickEvent = ( - event: QwikMouseEvent, + event: MouseEvent, element: Element ) => void; diff --git a/packages/storybook-framework-qwik/template/cli/header.stories.ts b/packages/storybook-framework-qwik/template/cli/header.stories.ts index fc14dc5..106d2c0 100644 --- a/packages/storybook-framework-qwik/template/cli/header.stories.ts +++ b/packages/storybook-framework-qwik/template/cli/header.stories.ts @@ -1,8 +1,6 @@ import type { Meta, StoryObj } from "storybook-framework-qwik"; import type { HeaderProps } from "./header"; import { Header } from "./header"; -import { $ } from "@builder.io/qwik"; -import { action } from "@storybook/addon-actions"; const meta = { title: "Example/Header", @@ -13,20 +11,12 @@ const meta = { // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout layout: "fullscreen", }, - args: { - // automatic actions are not yet supported. - // See https://github.com/literalpie/storybook-framework-qwik/issues/16 - // For now, use the legacy addon-actions API wrapped in a $ to make your own QRL action. - onCreateAccount$: $<() => void>(() => { - action("Create Account Action")(); - }), - onLogin$: $<() => void>(() => { - action("Login Action")(); - }), - onLogout$: $<() => void>(() => { - action("Logout Action")(); - }), - }, + args: { }, + argTypes: { + onCreateAccount$: { action: 'onCreateAccount' }, + onLogin$: { action: 'onLogin' }, + onLogout$: { action: 'onLogout' }, + } } satisfies Meta; export default meta; diff --git a/packages/storybook-framework-qwik/template/cli/header.tsx b/packages/storybook-framework-qwik/template/cli/header.tsx index 58e9944..97828fe 100644 --- a/packages/storybook-framework-qwik/template/cli/header.tsx +++ b/packages/storybook-framework-qwik/template/cli/header.tsx @@ -26,7 +26,7 @@ export const Header = component$( viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" > - +