@sit-onyx/storybook-utils 1.0.0-beta.9 → 1.0.0-beta.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,5 @@
1
1
  <div align="center" style="text-align: center">
2
- <picture>
3
- <source media="(prefers-color-scheme: dark)" type="image/svg+xml" srcset="https://raw.githubusercontent.com/SchwarzIT/onyx/main/.github/onyx-logo-light.svg">
4
- <source media="(prefers-color-scheme: light)" type="image/svg+xml" srcset="https://raw.githubusercontent.com/SchwarzIT/onyx/main/.github/onyx-logo-dark.svg">
5
- <img alt="onyx logo" src="https://raw.githubusercontent.com/SchwarzIT/onyx/main/.github/onyx-logo-dark.svg" width="160px">
6
- </picture>
2
+ <img alt="onyx logo" src="https://raw.githubusercontent.com/SchwarzIT/onyx/main/.github/onyx-logo.svg" height="96px">
7
3
  </div>
8
4
 
9
5
  <br>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sit-onyx/storybook-utils",
3
3
  "description": "Storybook utilities for Vue",
4
- "version": "1.0.0-beta.9",
4
+ "version": "1.0.0-beta.91",
5
5
  "type": "module",
6
6
  "author": "Schwarz IT KG",
7
7
  "license": "Apache-2.0",
@@ -11,7 +11,7 @@
11
11
  "types": "./src/index.ts",
12
12
  "exports": {
13
13
  ".": "./src/index.ts",
14
- "./style.css": "./src/index.css"
14
+ "./style.css": "./src/style.css"
15
15
  },
16
16
  "homepage": "https://onyx.schwarz/development/packages/storybook-utils.html",
17
17
  "repository": {
@@ -23,18 +23,27 @@
23
23
  "url": "https://github.com/SchwarzIT/onyx/issues"
24
24
  },
25
25
  "peerDependencies": {
26
- "@storybook/vue3": ">= 8.2.0",
27
- "storybook": ">= 8.2.0",
28
- "storybook-dark-mode": ">= 4",
29
- "@sit-onyx/icons": "^1.0.0-beta.0",
30
- "sit-onyx": "^1.0.0-beta.8"
26
+ "@storybook/vue3": ">= 9.0.0",
27
+ "@vueless/storybook-dark-mode": ">= 9.0.0",
28
+ "storybook": ">= 9.0.0",
29
+ "vue-component-type-helpers": ">= 2",
30
+ "@sit-onyx/icons": "^1.0.0-beta.18",
31
+ "@sit-onyx/shared": "^1.0.0-beta.3"
31
32
  },
32
33
  "dependencies": {
33
- "deepmerge-ts": "^7.0.3"
34
+ "deepmerge-ts": "^7.1.5"
35
+ },
36
+ "devDependencies": {
37
+ "storybook": "^9.0.12",
38
+ "vue": "3.5.16",
39
+ "vue-component-type-helpers": "^2.2.10",
40
+ "@sit-onyx/icons": "^1.0.0-beta.18",
41
+ "@sit-onyx/shared": "^1.0.0-beta.3"
34
42
  },
35
43
  "scripts": {
36
44
  "build": "tsc --noEmit",
37
45
  "test": "vitest",
38
- "test:coverage": "vitest run --coverage"
46
+ "test:coverage": "vitest run --coverage",
47
+ "stylelint": "stylelint \"src/**/*.css\""
39
48
  }
40
49
  }
@@ -0,0 +1,29 @@
1
+ import type { StoryContextForEnhancers } from "storybook/internal/types";
2
+ import { expect, test } from "vitest";
3
+ import { enhanceEventArgTypes } from "./actions";
4
+
5
+ test("should enhance event arg types", () => {
6
+ const argTypes = {
7
+ someProp: {
8
+ name: "someProp",
9
+ table: { category: "props" },
10
+ },
11
+ click: {
12
+ name: "click",
13
+ table: { category: "events" },
14
+ },
15
+ } satisfies StoryContextForEnhancers["argTypes"];
16
+
17
+ const result = enhanceEventArgTypes({
18
+ argTypes,
19
+ } as unknown as StoryContextForEnhancers);
20
+
21
+ expect(result).toStrictEqual({
22
+ ...argTypes,
23
+ onClick: {
24
+ name: "onClick",
25
+ table: { disable: true },
26
+ action: "click",
27
+ },
28
+ });
29
+ });
package/src/actions.ts CHANGED
@@ -1,85 +1,144 @@
1
- import type { ArgTypes, Decorator, Meta } from "@storybook/vue3";
2
- import { deepmerge } from "deepmerge-ts";
1
+ import type { Decorator } from "@storybook/vue3";
2
+ import { action } from "storybook/actions";
3
3
  import { useArgs } from "storybook/internal/preview-api";
4
- import { isReactive, reactive, watch } from "vue";
5
- import type { DefineStorybookActionsAndVModelsOptions, ExtractVueEventNames } from ".";
4
+ import type { ArgTypes, ArgTypesEnhancer, StrictInputType } from "storybook/internal/types";
5
+ import { h, isReactive, reactive, watch, type Component, type Events } from "vue";
6
+ import type { ComponentProps, ComponentSlots } from "vue-component-type-helpers";
7
+ import { EVENT_DOC_MAP } from "./events";
8
+
9
+ type ComponentEmits<Props extends ComponentProps<unknown>> = keyof {
10
+ [Key in keyof Props as Key extends `on${string}` ? Key : never]: true;
11
+ };
6
12
 
7
13
  /**
8
- * Utility to define Storybook meta for a given Vue component which will take care of defining argTypes for
9
- * the given events as well as implementing v-model handlers so that the Storybook controls are updated when you interact with the component.
10
- * Should be preferred over manually defining argTypes for *.stories.ts files.
14
+ * Wraps the original component and adds [Storybook action logging](https://storybook.js.org/docs/essentials/actions).
15
+ * This is useful for slotted child components that emit relevant events.
16
+ *
17
+ * Returns a wrapped component, which can be used in place of the original component.
11
18
  *
12
- * @example
13
19
  * ```ts
14
- * // Input.stories.ts
15
- * import { defineStorybookActionsAndVModels } from '@sit-onyx/storybook-utils';
16
- * import type { Meta } from '@storybook/vue3';
17
- * import Input from './Input.vue';
20
+ * import { createActionLoggerWrapper } from "@sit-onyx/storybook-utils";
21
+ * import _ChildComponent from "./_ChildComponent.vue";
22
+ *
23
+ * // Usual story setup...
18
24
  *
19
- * const meta: Meta<typeof Input> = {
20
- * title: 'components/Input',
21
- * ...defineStorybookActionsAndVModels({
22
- * component: Input,
23
- * events: ['update:modelValue', 'change'],
24
- * }),
25
- * };
25
+ * const ChildComponent = createActionLoggerWrapper(_ChildComponent, ["onChildEmit"]);
26
+ *
27
+ * export const Default = {
28
+ * args: {
29
+ * propName: 'Value'
30
+ * someSlot: () => h(ChildComponent, { label: "Item 1" }),
31
+ * },
32
+ * } satisfies Story;
26
33
  * ```
27
34
  */
28
- export const defineStorybookActionsAndVModels = <T>(
29
- options: DefineStorybookActionsAndVModelsOptions<T>,
30
- ): Meta => {
31
- const defaultMeta = {
32
- argTypes: {
33
- ...defineActions(options.events),
34
- ...{}, // this is needed to fix a type issue
35
- },
36
- decorators: [withVModelDecorator(options.events)],
37
- } satisfies Meta;
38
-
39
- return deepmerge(options, defaultMeta);
35
+ export const createActionLoggerWrapper =
36
+ <C extends Component>(component: C, emitsToLog: ComponentEmits<ComponentProps<C>>[]) =>
37
+ (props: ComponentProps<C>, ctx: { slots: ComponentSlots<C> }) => {
38
+ const entries = emitsToLog.map((emitName) => [
39
+ emitName,
40
+ // Log action in the format of `<component name> ~ <emit name>`
41
+ action(`${(component as { __name: string }).__name} ~ ${String(emitName)}`),
42
+ ]);
43
+ const eventHandler = Object.fromEntries(entries);
44
+ return h(
45
+ component,
46
+ {
47
+ ...eventHandler,
48
+ ...props,
49
+ },
50
+ ctx.slots,
51
+ );
52
+ };
53
+
54
+ /**
55
+ * Adds actions for all argTypes of the 'event' category, so that they are logged via the actions plugin.
56
+ */
57
+ export const enhanceEventArgTypes: ArgTypesEnhancer = ({ argTypes }) => {
58
+ Object.values(argTypes)
59
+ .filter(({ table }) => table?.category === "events")
60
+ .forEach(({ name }) => {
61
+ const eventName = `on${capitalizeFirstLetter(name)}`;
62
+ if (eventName in argTypes) {
63
+ return;
64
+ }
65
+ argTypes[eventName] = {
66
+ name: eventName,
67
+ table: { disable: true }, // do not add a second table entry for event name prefixed with "on"
68
+ action: name,
69
+ };
70
+ });
71
+ return argTypes;
40
72
  };
41
73
 
42
74
  /**
43
- * Defines Storybook actions ("argTypes") for the given events.
44
- * Reason for this wrapper function is that Storybook expects event names to be prefixed
45
- * with "on", e.g. "onClick".
46
- *
47
- * However in Vue, the event names are plain like "click" instead of "onClick" because
48
- * otherwise we would use it like "@on-click="..."" which is redundant.
75
+ * Allows logging and documentation for the passed event listener names in Storybook.
76
+ * Will be documented in a extra "Relevant HTML events" section in the Storybook documentation.
49
77
  *
50
- * So this utility will remove the on[eventName] entry from the Storybook panel/table
51
- * and register the correct eventName as action so it is logged in the "Actions" tab.
78
+ * @example
79
+ * ```typescript
80
+ * const meta: Meta<typeof OnyxButton> = {
81
+ * title: "Buttons/Button",
82
+ * component: OnyxButton,
83
+ * argTypes: {
84
+ * somethingElse: { ...someOtherArgType },
85
+ * ...withNativeEventLogging(["onClick"]),
86
+ * },
87
+ *};
88
+ * ```
52
89
  *
53
- * @example defineActions(["click", "input"])
90
+ * @param relevantEvents a list of event names that should be logged
91
+ * @returns Storybook ArgTypes object
54
92
  */
55
- export const defineActions = <T>(events: ExtractVueEventNames<T>[]): ArgTypes => {
56
- return events.reduce<ArgTypes>((argTypes, eventName) => {
57
- argTypes[`on${capitalizeFirstLetter(eventName)}`] = {
58
- table: { disable: true },
59
- action: eventName,
93
+ export const withNativeEventLogging = (relevantEvents: (keyof Events)[]) =>
94
+ relevantEvents.reduce((argTypes, eventName) => {
95
+ const { constructor, event } = EVENT_DOC_MAP[eventName];
96
+ argTypes[eventName] = {
97
+ name: event.name,
98
+ control: false,
99
+ description: `The native HTML [${event.name}](${event.url}) event which dispatches an [${constructor.name}](${constructor.url}).`,
100
+ table: {
101
+ category: "Relevant HTML events",
102
+ type: { summary: constructor.name },
103
+ },
104
+ action: event.name,
60
105
  };
61
-
62
- argTypes[eventName] = { control: false };
63
106
  return argTypes;
64
- }, {});
107
+ }, {} as ArgTypes);
108
+
109
+ export type WithVModelDecoratorOptions = {
110
+ /**
111
+ * The matcher for the v-model events.
112
+ * @default /^update:/
113
+ */
114
+ filter: (argType: StrictInputType, index: number, array: StrictInputType[]) => boolean;
65
115
  };
66
116
 
67
117
  /**
68
- * Defines a custom decorator that will implement event handlers for all v-models
69
- * so that the Storybook controls are updated live when the user interacts with the component
118
+ * Defines a custom decorator that will implement event handlers for all v-models,
119
+ * so that the Storybook controls are updated live when the user interacts with the component.
120
+ * This ensures that the story and component props stay in sync.
70
121
  *
71
122
  * @example
72
123
  * ```ts
73
- * import Input from './Input.vue';
124
+ * // .storybook/preview.ts
74
125
  *
75
126
  * {
76
- * decorators: [withVModelDecorator<typeof Input>(["update:modelValue"])]
127
+ * decorators: [withVModelDecorator()]
77
128
  * }
78
129
  * ```
79
130
  */
80
- export const withVModelDecorator = <T>(events: ExtractVueEventNames<T>[]): Decorator => {
131
+
132
+ export const withVModelDecorator = (options?: WithVModelDecoratorOptions): Decorator => {
81
133
  return (story, ctx) => {
82
- const vModelEvents = events.filter((event) => event.startsWith("update:"));
134
+ const vModelFilter =
135
+ options?.filter ||
136
+ (({ table, name }) => table?.category === "events" && name.startsWith("update:"));
137
+
138
+ const vModelEvents = Object.values(ctx.argTypes)
139
+ .filter(vModelFilter)
140
+ .map(({ name }) => name);
141
+
83
142
  if (!vModelEvents.length) return story();
84
143
 
85
144
  const [args, updateArgs] = useArgs();