From b08cf5fdaa7273d5481af4bcc11c7c2e6685258e Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 23 Feb 2026 17:03:49 +0100 Subject: [PATCH] Fix documentation of view component in storybook and migrate to CSF3 format (#32604) * chore: add a way to keep story doc in wrapper * chore: use `withViewDocs` * doc: update SC readme * doc: update copyright --- packages/shared-components/.storybook/main.ts | 6 ++ .../.storybook/withViewDocs.ts | 62 ++++++++++++++++ packages/shared-components/README.md | 46 ++++++++---- .../AudioPlayerView.stories.tsx | 41 +++++++---- .../EncryptionEventView.stories.tsx | 70 ++++++++++-------- .../DecryptionFailureBodyView.stories.tsx | 66 +++++++++-------- .../MessageTimestampView.stories.tsx | 71 +++++++++++-------- .../ReactionsRowButtonTooltip.stories.tsx | 66 +++++++++-------- .../DisambiguatedProfile.stories.tsx | 70 +++++++++--------- .../WidgetContextMenuView.stories.tsx | 29 ++++---- .../RoomListHeaderView.stories.tsx | 29 ++++---- .../RoomListItemView.stories.tsx | 4 +- .../RoomListSearchView.stories.tsx | 40 ++++++----- .../RoomListView/RoomListView.stories.tsx | 4 +- .../VirtualizedRoomListView.stories.tsx | 10 +-- .../RoomStatusBarView.stories.tsx | 66 +++++++++-------- 16 files changed, 422 insertions(+), 258 deletions(-) create mode 100644 packages/shared-components/.storybook/withViewDocs.ts diff --git a/packages/shared-components/.storybook/main.ts b/packages/shared-components/.storybook/main.ts index 834cd6dc5b..a2ff26fb5a 100644 --- a/packages/shared-components/.storybook/main.ts +++ b/packages/shared-components/.storybook/main.ts @@ -49,6 +49,12 @@ const config: StorybookConfig = { }, typescript: { reactDocgen: "react-docgen-typescript", + reactDocgenTypescriptOptions: { + // The default exclude is ["**/**.stories.tsx"] which prevents + // docgen from extracting snapshot field descriptions from wrapper + // components defined in story files. + exclude: [], + }, }, async viteFinal(config) { return mergeConfig(config, { diff --git a/packages/shared-components/.storybook/withViewDocs.ts b/packages/shared-components/.storybook/withViewDocs.ts new file mode 100644 index 0000000000..75385830ee --- /dev/null +++ b/packages/shared-components/.storybook/withViewDocs.ts @@ -0,0 +1,62 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +/** + * Copies the component description and props documentation from a View's + * `__docgenInfo` (injected at build time by Storybook's react-docgen-typescript + * Vite plugin) onto the story wrapper component. + * + * This lets Storybook's default `extractComponentDescription` pick up the + * View's JSDoc and display per-field descriptions in the ArgTypes table. + * + * **Important:** the wrapper must be defined as a named variable *before* + * being passed here so that react-docgen-typescript can extract its props. + * + * @example + * ```ts + * const MyViewWrapperImpl = (props: MyViewProps) => { + * const vm = useMockedViewModel(props, {}); + * return ; + * }; + * const MyViewWrapper = withViewDocs(MyViewWrapperImpl, MyView); + * ``` + */ +export function withViewDocs unknown>(wrapper: T, view: object): T { + const viewInfo = (view as { __docgenInfo?: DocgenInfo }).__docgenInfo; + const viewDescription = viewInfo?.description; + if (!viewDescription) return wrapper; + + // The wrapper must be defined as a named variable (not inline) so that + // react-docgen-typescript can extract its props. The docgen Vite plugin + // appends a `Wrapper.__docgenInfo = { … }` assignment at the *end* of the + // module, which runs **after** this function. We install a setter trap so + // that the View's description is merged into the generated info. + let stored: DocgenInfo | undefined = (wrapper as { __docgenInfo?: DocgenInfo }).__docgenInfo; + Object.defineProperty(wrapper, "__docgenInfo", { + get() { + return stored; + }, + set(incoming: DocgenInfo) { + stored = { + ...incoming, + description: incoming.description || viewDescription, + }; + }, + configurable: true, + enumerable: true, + }); + + // Also apply immediately for the current state. + stored = { ...stored, description: viewDescription }; + + return wrapper; +} + +interface DocgenInfo { + description?: string; + props?: Record; +} diff --git a/packages/shared-components/README.md b/packages/shared-components/README.md index 993450a65f..3653bf9d21 100644 --- a/packages/shared-components/README.md +++ b/packages/shared-components/README.md @@ -68,7 +68,7 @@ instance should be provided as a prop. Here's a basic example: -```jsx +```tsx import { ViewExample } from "@element-hq/web-shared-components"; function MyApp() { @@ -180,27 +180,32 @@ export const Disabled: Story = { #### MVVM Component Stories -For MVVM components, create a wrapper component that uses `useMockedViewModel`: +For MVVM components, create a wrapper component that uses `useMockedViewModel` and `withViewDocs`: ```tsx import React, { type JSX } from "react"; import { fn } from "storybook/test"; -import type { Meta, StoryFn } from "@storybook/react-vite"; +import type { Meta, StoryObj } from "@storybook/react-vite"; import { MyComponentView, type MyComponentViewSnapshot, type MyComponentViewActions } from "./MyComponentView"; -import { useMockedViewModel } from "../../useMockedViewModel"; +import { useMockedViewModel } from "../../viewmodel"; +import { withViewDocs } from "../../../.storybook/withViewDocs"; // Combine snapshot and actions for easier typing type MyComponentProps = MyComponentViewSnapshot & MyComponentViewActions; -// Wrapper component that creates a mocked ViewModel -const MyComponentViewWrapper = ({ onAction, ...rest }: MyComponentProps): JSX.Element => { +// Wrapper component that creates a mocked ViewModel. +// Must be a named variable (not inline) for docgen to extract its props. +const MyComponentViewWrapperImpl = ({ onAction, ...rest }: MyComponentProps): JSX.Element => { const vm = useMockedViewModel(rest, { onAction, }); return ; }; +// withViewDocs copies the View's JSDoc description onto the wrapper for Storybook autodocs +const MyComponentViewWrapper = withViewDocs(MyComponentViewWrapperImpl, MyComponentView); -export default { +// Must use `satisfies` (not `as` or `: Meta`) to preserve type info for docgen +const meta = { title: "Category/MyComponentView", component: MyComponentViewWrapper, tags: ["autodocs"], @@ -211,20 +216,29 @@ export default { // Action properties (callbacks) onAction: fn(), }, -} as Meta; +} satisfies Meta; -const Template: StoryFn = (args) => ; +export default meta; +type Story = StoryObj; -export const Default = Template.bind({}); +export const Default: Story = {}; -export const Loading = Template.bind({}); -Loading.args = { - isLoading: true, +export const Loading: Story = { + args: { + isLoading: true, + }, }; ``` Thanks to this approach, we can directly use primitives in the story arguments instead of a view model object. +> [!IMPORTANT] +> Three requirements must be met for snapshot field documentation to appear in Storybook's ArgTypes table: +> +> 1. **Named wrapper variable** — the wrapper must be assigned to a named `const` (e.g. `MyComponentViewWrapperImpl`) before being passed to `withViewDocs`, so that `react-docgen-typescript` can extract its props. +> 2. **`withViewDocs` call** — wraps the wrapper component with the original View to copy the View's JSDoc description. +> 3. **`satisfies Meta`** — the meta object must use `satisfies Meta<...>` (not `as Meta<...>` or `: Meta<...> =`). Type assertions and annotations erase the inferred component type that docgen relies on. + #### Linking Figma Designs This package uses [@storybook/addon-designs](https://github.com/storybookjs/addon-designs) to embed Figma designs directly in Storybook. This helps developers compare their implementation with the design specs. @@ -239,7 +253,7 @@ This package uses [@storybook/addon-designs](https://github.com/storybookjs/addo Example with Figma integration: ```tsx -export default { +const meta = { title: "Room List/RoomListSearchView", component: RoomListSearchViewWrapper, tags: ["autodocs"], @@ -252,7 +266,9 @@ export default { url: "https://www.figma.com/design/vlmt46QDdE4dgXDiyBJXqp/ER-33-Left-Panel?node-id=98-1979", }, }, -} as Meta; +} satisfies Meta; + +export default meta; ``` The Figma design will appear in the "Design" tab in Storybook. diff --git a/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.stories.tsx b/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.stories.tsx index 8e2c91eadc..b81b79ea57 100644 --- a/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.stories.tsx +++ b/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.stories.tsx @@ -8,12 +8,18 @@ import React, { type JSX } from "react"; import { fn } from "storybook/test"; -import type { Meta, StoryFn } from "@storybook/react-vite"; +import type { Meta, StoryObj } from "@storybook/react-vite"; import { AudioPlayerView, type AudioPlayerViewActions, type AudioPlayerViewSnapshot } from "./AudioPlayerView"; import { useMockedViewModel } from "../../viewmodel"; +import { withViewDocs } from "../../../.storybook/withViewDocs"; type AudioPlayerProps = AudioPlayerViewSnapshot & AudioPlayerViewActions; -const AudioPlayerViewWrapper = ({ togglePlay, onKeyDown, onSeekbarChange, ...rest }: AudioPlayerProps): JSX.Element => { +const AudioPlayerViewWrapperImpl = ({ + togglePlay, + onKeyDown, + onSeekbarChange, + ...rest +}: AudioPlayerProps): JSX.Element => { const vm = useMockedViewModel(rest, { togglePlay, onKeyDown, @@ -21,8 +27,9 @@ const AudioPlayerViewWrapper = ({ togglePlay, onKeyDown, onSeekbarChange, ...res }); return ; }; +const AudioPlayerViewWrapper = withViewDocs(AudioPlayerViewWrapperImpl, AudioPlayerView); -export default { +const meta = { title: "Audio/AudioPlayerView", component: AudioPlayerViewWrapper, tags: ["autodocs"], @@ -44,23 +51,27 @@ export default { onKeyDown: fn(), onSeekbarChange: fn(), }, -} as Meta; +} satisfies Meta; -const Template: StoryFn = (args) => ; +export default meta; +type Story = StoryObj; -export const Default = Template.bind({}); +export const Default: Story = {}; -export const NoMediaName = Template.bind({}); -NoMediaName.args = { - mediaName: undefined, +export const NoMediaName: Story = { + args: { + mediaName: undefined, + }, }; -export const NoSize = Template.bind({}); -NoSize.args = { - sizeBytes: undefined, +export const NoSize: Story = { + args: { + sizeBytes: undefined, + }, }; -export const HasError = Template.bind({}); -HasError.args = { - error: true, +export const HasError: Story = { + args: { + error: true, + }, }; diff --git a/packages/shared-components/src/event-tiles/EncryptionEventView/EncryptionEventView.stories.tsx b/packages/shared-components/src/event-tiles/EncryptionEventView/EncryptionEventView.stories.tsx index 04066d1dbe..8a41e47adf 100644 --- a/packages/shared-components/src/event-tiles/EncryptionEventView/EncryptionEventView.stories.tsx +++ b/packages/shared-components/src/event-tiles/EncryptionEventView/EncryptionEventView.stories.tsx @@ -7,19 +7,21 @@ import React, { type JSX } from "react"; -import type { Meta, StoryFn } from "@storybook/react-vite"; +import type { Meta, StoryObj } from "@storybook/react-vite"; import { EncryptionEventView, EncryptionEventState, type EncryptionEventViewSnapshot } from "./EncryptionEventView"; import { useMockedViewModel } from "../../viewmodel/useMockedViewModel"; +import { withViewDocs } from "../../../.storybook/withViewDocs"; type EncryptionEventProps = EncryptionEventViewSnapshot; -const EncryptionEventViewWrapper = ({ ...rest }: EncryptionEventProps): JSX.Element => { +const EncryptionEventViewWrapperImpl = ({ ...rest }: EncryptionEventProps): JSX.Element => { const vm = useMockedViewModel(rest, {}); return ; }; +const EncryptionEventViewWrapper = withViewDocs(EncryptionEventViewWrapperImpl, EncryptionEventView); -export default { +const meta = { title: "Event/EncryptionEvent", component: EncryptionEventViewWrapper, tags: ["autodocs"], @@ -37,46 +39,54 @@ export default { userName: "Alice", className: "", }, -} as Meta; +} satisfies Meta; -const Template: StoryFn = (args) => ; +export default meta; +type Story = StoryObj; -export const Default = Template.bind({}); +export const Default: Story = {}; -export const StateEncryptionEnabled = Template.bind({}); -StateEncryptionEnabled.args = { - state: EncryptionEventState.ENABLED, - encryptedStateEvents: true, +export const StateEncryptionEnabled: Story = { + args: { + state: EncryptionEventState.ENABLED, + encryptedStateEvents: true, + }, }; -export const ParametersChanged = Template.bind({}); -ParametersChanged.args = { - state: EncryptionEventState.CHANGED, +export const ParametersChanged: Story = { + args: { + state: EncryptionEventState.CHANGED, + }, }; -export const DisableAttempt = Template.bind({}); -DisableAttempt.args = { - state: EncryptionEventState.DISABLE_ATTEMPT, +export const DisableAttempt: Story = { + args: { + state: EncryptionEventState.DISABLE_ATTEMPT, + }, }; -export const EnabledDirectMessage = Template.bind({}); -EnabledDirectMessage.args = { - state: EncryptionEventState.ENABLED_DM, - userName: "Alice", +export const EnabledDirectMessage: Story = { + args: { + state: EncryptionEventState.ENABLED_DM, + userName: "Alice", + }, }; -export const EnabledLocalRoom = Template.bind({}); -EnabledLocalRoom.args = { - state: EncryptionEventState.ENABLED_LOCAL, +export const EnabledLocalRoom: Story = { + args: { + state: EncryptionEventState.ENABLED_LOCAL, + }, }; -export const Unsupported = Template.bind({}); -Unsupported.args = { - state: EncryptionEventState.UNSUPPORTED, +export const Unsupported: Story = { + args: { + state: EncryptionEventState.UNSUPPORTED, + }, }; -export const WithTimestamp = Template.bind({}); -WithTimestamp.args = { - state: EncryptionEventState.ENABLED, - timestamp: 14:56, +export const WithTimestamp: Story = { + args: { + state: EncryptionEventState.ENABLED, + timestamp: 14:56, + }, }; diff --git a/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx b/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx index 741f7420de..590583a9f8 100644 --- a/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx +++ b/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx @@ -7,23 +7,25 @@ import React, { type JSX } from "react"; -import type { Meta, StoryFn } from "@storybook/react-vite"; +import type { Meta, StoryObj } from "@storybook/react-vite"; import { DecryptionFailureBodyView, DecryptionFailureReason, type DecryptionFailureBodyViewSnapshot, } from "./DecryptionFailureBodyView"; import { useMockedViewModel } from "../../viewmodel/useMockedViewModel"; +import { withViewDocs } from "../../../.storybook/withViewDocs"; type DecryptionFailureBodyProps = DecryptionFailureBodyViewSnapshot; -const DecryptionFailureBodyViewWrapper = ({ ...rest }: DecryptionFailureBodyProps): JSX.Element => { +const DecryptionFailureBodyViewWrapperImpl = ({ ...rest }: DecryptionFailureBodyProps): JSX.Element => { const vm = useMockedViewModel(rest, {}); return ; }; +const DecryptionFailureBodyViewWrapper = withViewDocs(DecryptionFailureBodyViewWrapperImpl, DecryptionFailureBodyView); -export default { +const meta = { title: "MessageBody/DecryptionFailureBodyView", component: DecryptionFailureBodyViewWrapper, tags: ["autodocs"], @@ -40,42 +42,46 @@ export default { isLocalDeviceVerified: true, extraClassNames: ["extra_class"], }, -} as Meta; +} satisfies Meta; -const Template: StoryFn = (args) => ( - -); +export default meta; +type Story = StoryObj; -export const Default = Template.bind({}); +export const Default: Story = {}; -export const HasExtraClassNames = Template.bind({}); -HasExtraClassNames.args = { - decryptionFailureReason: DecryptionFailureReason.UNABLE_TO_DECRYPT, - extraClassNames: ["extra_class_1", "extra_class_2"], +export const HasExtraClassNames: Story = { + args: { + decryptionFailureReason: DecryptionFailureReason.UNABLE_TO_DECRYPT, + extraClassNames: ["extra_class_1", "extra_class_2"], + }, }; -export const HasErrorClassName = Template.bind({}); -HasErrorClassName.args = { - decryptionFailureReason: DecryptionFailureReason.UNSIGNED_SENDER_DEVICE, - extraClassNames: undefined, +export const HasErrorClassName: Story = { + args: { + decryptionFailureReason: DecryptionFailureReason.UNSIGNED_SENDER_DEVICE, + extraClassNames: undefined, + }, }; -export const HasErrorBlockIcon = Template.bind({}); -HasErrorBlockIcon.args = { - decryptionFailureReason: DecryptionFailureReason.SENDER_IDENTITY_PREVIOUSLY_VERIFIED, - extraClassNames: undefined, +export const HasErrorBlockIcon: Story = { + args: { + decryptionFailureReason: DecryptionFailureReason.SENDER_IDENTITY_PREVIOUSLY_VERIFIED, + extraClassNames: undefined, + }, }; -export const HasBackupConfiguredVerifiedFalse = Template.bind({}); -HasBackupConfiguredVerifiedFalse.args = { - decryptionFailureReason: DecryptionFailureReason.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED, - isLocalDeviceVerified: false, - extraClassNames: undefined, +export const HasBackupConfiguredVerifiedFalse: Story = { + args: { + decryptionFailureReason: DecryptionFailureReason.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED, + isLocalDeviceVerified: false, + extraClassNames: undefined, + }, }; -export const HasBackupConfiguredVerifiedTrue = Template.bind({}); -HasBackupConfiguredVerifiedTrue.args = { - decryptionFailureReason: DecryptionFailureReason.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED, - isLocalDeviceVerified: true, - extraClassNames: undefined, +export const HasBackupConfiguredVerifiedTrue: Story = { + args: { + decryptionFailureReason: DecryptionFailureReason.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED, + isLocalDeviceVerified: true, + extraClassNames: undefined, + }, }; diff --git a/packages/shared-components/src/message-body/MessageTimestampView/MessageTimestampView.stories.tsx b/packages/shared-components/src/message-body/MessageTimestampView/MessageTimestampView.stories.tsx index e512012a57..13077b6c03 100644 --- a/packages/shared-components/src/message-body/MessageTimestampView/MessageTimestampView.stories.tsx +++ b/packages/shared-components/src/message-body/MessageTimestampView/MessageTimestampView.stories.tsx @@ -8,24 +8,26 @@ import React, { type ReactNode } from "react"; import { expect, userEvent, within } from "storybook/test"; -import type { Meta, StoryFn } from "@storybook/react-vite"; +import type { Meta, StoryObj } from "@storybook/react-vite"; import { MessageTimestampView, type MessageTimestampViewActions, type MessageTimestampViewSnapshot, } from "./MessageTimestampView"; import { useMockedViewModel } from "../../viewmodel/useMockedViewModel"; +import { withViewDocs } from "../../../.storybook/withViewDocs"; type MessageTimestampProps = MessageTimestampViewSnapshot & MessageTimestampViewActions; -const MessageTimestampWrapper = ({ onClick, onContextMenu, ...rest }: MessageTimestampProps): ReactNode => { +const MessageTimestampWrapperImpl = ({ onClick, onContextMenu, ...rest }: MessageTimestampProps): ReactNode => { const vm = useMockedViewModel(rest, { onClick, onContextMenu, }); return ; }; +const MessageTimestampWrapper = withViewDocs(MessageTimestampWrapperImpl, MessageTimestampView); -export default { +const meta = { title: "MessageBody/MessageTimestamp", component: MessageTimestampWrapper, tags: ["autodocs"], @@ -37,44 +39,51 @@ export default { className: "", href: "", }, -} as Meta; +} satisfies Meta; -const Template: StoryFn = (args) => ; +export default meta; +type Story = StoryObj; -export const Default = Template.bind({}); -Default.play = async ({ canvasElement }) => { - const canvas = within(canvasElement); - await userEvent.hover(canvas.getByText("04:58")); - await expect(within(canvasElement.ownerDocument.body).findByRole("tooltip")).resolves.toBeInTheDocument(); +export const Default: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.hover(canvas.getByText("04:58")); + await expect(within(canvasElement.ownerDocument.body).findByRole("tooltip")).resolves.toBeInTheDocument(); + }, }; -export const HasTsReceivedAt = Template.bind({}); -HasTsReceivedAt.args = { - tsReceivedAt: "Thu, 17 Nov 2022, 4:58:33 pm", -}; -HasTsReceivedAt.play = async ({ canvasElement }) => { - const canvas = within(canvasElement); - await userEvent.hover(canvas.getByText("04:58")); - await expect(within(canvasElement.ownerDocument.body).findByRole("tooltip")).resolves.toBeInTheDocument(); +export const HasTsReceivedAt: Story = { + args: { + tsReceivedAt: "Thu, 17 Nov 2022, 4:58:33 pm", + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.hover(canvas.getByText("04:58")); + await expect(within(canvasElement.ownerDocument.body).findByRole("tooltip")).resolves.toBeInTheDocument(); + }, }; -export const HasInhibitTooltip = Template.bind({}); -HasInhibitTooltip.args = { - inhibitTooltip: true, +export const HasInhibitTooltip: Story = { + args: { + inhibitTooltip: true, + }, }; -export const HasExtraClassNames = Template.bind({}); -HasExtraClassNames.args = { - className: "extra_class_1 extra_class_2", +export const HasExtraClassNames: Story = { + args: { + className: "extra_class_1 extra_class_2", + }, }; -export const HasHref = Template.bind({}); -HasHref.args = { - href: "~", +export const HasHref: Story = { + args: { + href: "~", + }, }; -export const HasActions = Template.bind({}); -HasActions.args = { - onClick: () => console.log("Clicked message timestamp"), - onContextMenu: () => console.log("Context menu on message timestamp"), +export const HasActions: Story = { + args: { + onClick: () => console.log("Clicked message timestamp"), + onContextMenu: () => console.log("Context menu on message timestamp"), + }, }; diff --git a/packages/shared-components/src/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.stories.tsx b/packages/shared-components/src/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.stories.tsx index ff8fa315b5..4d5095e334 100644 --- a/packages/shared-components/src/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.stories.tsx +++ b/packages/shared-components/src/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.stories.tsx @@ -7,8 +7,9 @@ import React, { type JSX, type PropsWithChildren } from "react"; -import type { Meta, StoryFn } from "@storybook/react-vite"; +import type { Meta, StoryObj } from "@storybook/react-vite"; import { useMockedViewModel } from "../../viewmodel"; +import { withViewDocs } from "../../../.storybook/withViewDocs"; import { ReactionsRowButtonTooltipView, type ReactionsRowButtonTooltipViewSnapshot, @@ -16,12 +17,16 @@ import { type WrapperProps = ReactionsRowButtonTooltipViewSnapshot & PropsWithChildren; -const ReactionsRowButtonTooltipViewWrapper = ({ children, ...snapshotProps }: WrapperProps): JSX.Element => { +const ReactionsRowButtonTooltipViewWrapperImpl = ({ children, ...snapshotProps }: WrapperProps): JSX.Element => { const vm = useMockedViewModel(snapshotProps, {}); return {children}; }; +const ReactionsRowButtonTooltipViewWrapper = withViewDocs( + ReactionsRowButtonTooltipViewWrapperImpl, + ReactionsRowButtonTooltipView, +); -export default { +const meta = { title: "MessageBody/ReactionsRowButtonTooltip", component: ReactionsRowButtonTooltipViewWrapper, tags: ["autodocs"], @@ -32,38 +37,41 @@ export default { args: { children: , }, -} as Meta; +} satisfies Meta; -const Template: StoryFn = (args) => ( - -); +export default meta; +type Story = StoryObj; -export const Default = Template.bind({}); -Default.args = { - formattedSenders: "Alice, Bob and Charlie", - caption: ":thumbsup:", - tooltipOpen: true, +export const Default: Story = { + args: { + formattedSenders: "Alice, Bob and Charlie", + caption: ":thumbsup:", + tooltipOpen: true, + }, }; -export const ManySenders = Template.bind({}); -ManySenders.args = { - formattedSenders: "Alice, Bob, Charlie, David, Eve, Frank and 2 others", - caption: ":heart:", - children: , - tooltipOpen: true, +export const ManySenders: Story = { + args: { + formattedSenders: "Alice, Bob, Charlie, David, Eve, Frank and 2 others", + caption: ":heart:", + children: , + tooltipOpen: true, + }, }; -export const WithoutCaption = Template.bind({}); -WithoutCaption.args = { - formattedSenders: "Alice and Bob", - caption: undefined, - children: , - tooltipOpen: true, +export const WithoutCaption: Story = { + args: { + formattedSenders: "Alice and Bob", + caption: undefined, + children: , + tooltipOpen: true, + }, }; -export const NoTooltip = Template.bind({}); -NoTooltip.args = { - formattedSenders: undefined, - caption: undefined, - children: , +export const NoTooltip: Story = { + args: { + formattedSenders: undefined, + caption: undefined, + children: , + }, }; diff --git a/packages/shared-components/src/profile/DisambiguatedProfile/DisambiguatedProfile.stories.tsx b/packages/shared-components/src/profile/DisambiguatedProfile/DisambiguatedProfile.stories.tsx index 1f9128d3e0..b3231a5f75 100644 --- a/packages/shared-components/src/profile/DisambiguatedProfile/DisambiguatedProfile.stories.tsx +++ b/packages/shared-components/src/profile/DisambiguatedProfile/DisambiguatedProfile.stories.tsx @@ -8,22 +8,24 @@ import React, { type JSX } from "react"; import { fn } from "storybook/test"; -import type { Meta, StoryFn } from "@storybook/react-vite"; +import type { Meta, StoryObj } from "@storybook/react-vite"; import { DisambiguatedProfileView, type DisambiguatedProfileViewSnapshot, type DisambiguatedProfileViewActions, } from "./DisambiguatedProfileView"; import { useMockedViewModel } from "../../viewmodel"; +import { withViewDocs } from "../../../.storybook/withViewDocs"; type DisambiguatedProfileProps = DisambiguatedProfileViewSnapshot & DisambiguatedProfileViewActions; -const DisambiguatedProfileViewWrapper = ({ onClick, ...rest }: DisambiguatedProfileProps): JSX.Element => { +const DisambiguatedProfileViewWrapperImpl = ({ onClick, ...rest }: DisambiguatedProfileProps): JSX.Element => { const vm = useMockedViewModel(rest, { onClick }); return ; }; +const DisambiguatedProfileViewWrapper = withViewDocs(DisambiguatedProfileViewWrapperImpl, DisambiguatedProfileView); -export default { +const meta = { title: "Profile/DisambiguatedProfile", component: DisambiguatedProfileViewWrapper, tags: ["autodocs"], @@ -40,44 +42,48 @@ export default { emphasizeDisplayName: true, onClick: fn(), }, -} as Meta; +} satisfies Meta; -const Template: StoryFn = (args) => ( - -); +export default meta; +type Story = StoryObj; -export const Default = Template.bind({}); +export const Default: Story = {}; -export const WithMxid = Template.bind({}); -WithMxid.args = { - displayName: "Alice", - displayIdentifier: "@alice:example.org", - colorClass: "mx_Username_color1", +export const WithMxid: Story = { + args: { + displayName: "Alice", + displayIdentifier: "@alice:example.org", + colorClass: "mx_Username_color1", + }, }; -export const WithColorClass = Template.bind({}); -WithColorClass.args = { - displayName: "Bob", - colorClass: "mx_Username_color3", +export const WithColorClass: Story = { + args: { + displayName: "Bob", + colorClass: "mx_Username_color3", + }, }; -export const Emphasized = Template.bind({}); -Emphasized.args = { - displayName: "Charlie", - emphasizeDisplayName: true, +export const Emphasized: Story = { + args: { + displayName: "Charlie", + emphasizeDisplayName: true, + }, }; -export const WithTooltip = Template.bind({}); -WithTooltip.args = { - displayName: "Diana", - title: "Diana (@diana:example.org)", +export const WithTooltip: Story = { + args: { + displayName: "Diana", + title: "Diana (@diana:example.org)", + }, }; -export const FullExample = Template.bind({}); -FullExample.args = { - displayName: "Eve", - displayIdentifier: "@eve:matrix.org", - colorClass: "mx_Username_color5", - title: "Eve (@eve:matrix.org)", - emphasizeDisplayName: true, +export const FullExample: Story = { + args: { + displayName: "Eve", + displayIdentifier: "@eve:matrix.org", + colorClass: "mx_Username_color5", + title: "Eve (@eve:matrix.org)", + emphasizeDisplayName: true, + }, }; diff --git a/packages/shared-components/src/right-panel/WidgetContextMenu/WidgetContextMenuView.stories.tsx b/packages/shared-components/src/right-panel/WidgetContextMenu/WidgetContextMenuView.stories.tsx index 87d3515d0c..3e3c2daeeb 100644 --- a/packages/shared-components/src/right-panel/WidgetContextMenu/WidgetContextMenuView.stories.tsx +++ b/packages/shared-components/src/right-panel/WidgetContextMenu/WidgetContextMenuView.stories.tsx @@ -10,17 +10,18 @@ import { fn } from "storybook/test"; import { IconButton } from "@vector-im/compound-web"; import TriggerIcon from "@vector-im/compound-design-tokens/assets/web/icons/overflow-horizontal"; -import type { Meta, StoryFn } from "@storybook/react-vite"; +import type { Meta, StoryObj } from "@storybook/react-vite"; import { type WidgetContextMenuAction, type WidgetContextMenuSnapshot, WidgetContextMenuView, } from "./WidgetContextMenuView"; import { useMockedViewModel } from "../../viewmodel/useMockedViewModel"; +import { withViewDocs } from "../../../.storybook/withViewDocs"; type WidgetContextMenuViewModelProps = WidgetContextMenuSnapshot & WidgetContextMenuAction; -const WidgetContextMenuViewWrapper = ({ +const WidgetContextMenuViewWrapperImpl = ({ onStreamAudioClick, onEditClick, onSnapshotClick, @@ -41,8 +42,9 @@ const WidgetContextMenuViewWrapper = ({ }); return ; }; +const WidgetContextMenuViewWrapper = withViewDocs(WidgetContextMenuViewWrapperImpl, WidgetContextMenuView); -export default { +const meta = { title: "RightPanel/WidgetContextMenuView", component: WidgetContextMenuViewWrapper, tags: ["autodocs"], @@ -54,7 +56,6 @@ export default { showSnapshotButton: true, showMoveButtons: [true, true], canModify: true, - widgetMessaging: undefined, isMenuOpened: true, trigger: ( @@ -69,16 +70,18 @@ export default { onFinished: fn(), onMoveButton: fn(), }, -} as Meta; +} satisfies Meta; -const Template: StoryFn = (args) => ; +export default meta; +type Story = StoryObj; -export const Default = Template.bind({}); +export const Default: Story = {}; -export const OnlyBasicModification = Template.bind({}); -OnlyBasicModification.args = { - showSnapshotButton: false, - showMoveButtons: [false, false], - showStreamAudioStreamButton: false, - showEditButton: false, +export const OnlyBasicModification: Story = { + args: { + showSnapshotButton: false, + showMoveButtons: [false, false], + showStreamAudioStreamButton: false, + showEditButton: false, + }, }; diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx index bca9e709d2..00304c7b13 100644 --- a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx +++ b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx @@ -8,18 +8,19 @@ import React, { type JSX } from "react"; import { fn } from "storybook/test"; -import type { Meta, StoryFn } from "@storybook/react-vite"; +import type { Meta, StoryObj } from "@storybook/react-vite"; import { RoomListHeaderView, type RoomListHeaderViewActions, type RoomListHeaderViewSnapshot, } from "./RoomListHeaderView"; import { useMockedViewModel } from "../../viewmodel"; +import { withViewDocs } from "../../../.storybook/withViewDocs"; import { defaultSnapshot } from "./default-snapshot"; type RoomListHeaderProps = RoomListHeaderViewSnapshot & RoomListHeaderViewActions; -const RoomListHeaderViewWrapper = ({ +const RoomListHeaderViewWrapperImpl = ({ createChatRoom, createRoom, createVideoRoom, @@ -44,8 +45,9 @@ const RoomListHeaderViewWrapper = ({ }); return ; }; +const RoomListHeaderViewWrapper = withViewDocs(RoomListHeaderViewWrapperImpl, RoomListHeaderView); -export default { +const meta = { title: "Room List/RoomListHeaderView", component: RoomListHeaderViewWrapper, tags: ["autodocs"], @@ -67,18 +69,21 @@ export default { url: "https://www.figma.com/design/vlmt46QDdE4dgXDiyBJXqp/ER-33-Left-Panel?node-id=2925-19173", }, }, -} as Meta; +} satisfies Meta; -const Template: StoryFn = (args) => ; +export default meta; +type Story = StoryObj; -export const Default = Template.bind({}); +export const Default: Story = {}; -export const NoSpaceMenu = Template.bind({}); -NoSpaceMenu.args = { - displaySpaceMenu: false, +export const NoSpaceMenu: Story = { + args: { + displaySpaceMenu: false, + }, }; -export const NoComposeMenu = Template.bind({}); -NoComposeMenu.args = { - displayComposeMenu: false, +export const NoComposeMenu: Story = { + args: { + displayComposeMenu: false, + }, }; diff --git a/packages/shared-components/src/room-list/RoomListItemView/RoomListItemView.stories.tsx b/packages/shared-components/src/room-list/RoomListItemView/RoomListItemView.stories.tsx index 72b619275e..507bb80766 100644 --- a/packages/shared-components/src/room-list/RoomListItemView/RoomListItemView.stories.tsx +++ b/packages/shared-components/src/room-list/RoomListItemView/RoomListItemView.stories.tsx @@ -12,6 +12,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Room } from "./RoomListItemView"; import { RoomListItemView, type RoomListItemSnapshot, type RoomListItemActions } from "./RoomListItemView"; import { useMockedViewModel } from "../../viewmodel"; +import { withViewDocs } from "../../../.storybook/withViewDocs"; import { defaultSnapshot } from "./default-snapshot"; import { renderAvatar } from "../story-mocks"; @@ -26,7 +27,7 @@ type RoomListItemProps = RoomListItemSnapshot & }; // Wrapper component that creates a mocked ViewModel -const RoomListItemWrapper = ({ +const RoomListItemWrapperImpl = ({ onOpenRoom, onMarkAsRead, onMarkAsUnread, @@ -67,6 +68,7 @@ const RoomListItemWrapper = ({ /> ); }; +const RoomListItemWrapper = withViewDocs(RoomListItemWrapperImpl, RoomListItemView); const meta = { title: "Room List/RoomListItemView", diff --git a/packages/shared-components/src/room-list/RoomListSearchView/RoomListSearchView.stories.tsx b/packages/shared-components/src/room-list/RoomListSearchView/RoomListSearchView.stories.tsx index d862cc4295..ff9578f2fb 100644 --- a/packages/shared-components/src/room-list/RoomListSearchView/RoomListSearchView.stories.tsx +++ b/packages/shared-components/src/room-list/RoomListSearchView/RoomListSearchView.stories.tsx @@ -8,17 +8,18 @@ import React, { type JSX } from "react"; import { fn } from "storybook/test"; -import type { Meta, StoryFn } from "@storybook/react-vite"; +import type { Meta, StoryObj } from "@storybook/react-vite"; import { RoomListSearchView, type RoomListSearchViewActions, type RoomListSearchViewSnapshot, } from "./RoomListSearchView"; import { useMockedViewModel } from "../../viewmodel"; +import { withViewDocs } from "../../../.storybook/withViewDocs"; type RoomListSearchProps = RoomListSearchViewSnapshot & RoomListSearchViewActions; -const RoomListSearchViewWrapper = ({ +const RoomListSearchViewWrapperImpl = ({ onSearchClick, onDialPadClick, onExploreClick, @@ -31,8 +32,9 @@ const RoomListSearchViewWrapper = ({ }); return ; }; +const RoomListSearchViewWrapper = withViewDocs(RoomListSearchViewWrapperImpl, RoomListSearchView); -export default { +const meta = { title: "Room List/RoomListSearchView", component: RoomListSearchViewWrapper, tags: ["autodocs"], @@ -50,25 +52,29 @@ export default { url: "https://www.figma.com/design/vlmt46QDdE4dgXDiyBJXqp/ER-33-Left-Panel-2025?node-id=98-1979&t=vafb4zoYMNLRuAbh-4", }, }, -} as Meta; +} satisfies Meta; -const Template: StoryFn = (args) => ; +export default meta; +type Story = StoryObj; -export const Default = Template.bind({}); +export const Default: Story = {}; -export const WithDialPad = Template.bind({}); -WithDialPad.args = { - displayDialButton: true, +export const WithDialPad: Story = { + args: { + displayDialButton: true, + }, }; -export const WithoutExplore = Template.bind({}); -WithoutExplore.args = { - displayExploreButton: false, +export const WithoutExplore: Story = { + args: { + displayExploreButton: false, + }, }; -export const AllButtons = Template.bind({}); -AllButtons.args = { - displayExploreButton: true, - displayDialButton: true, - searchShortcut: "⌘ K", +export const AllButtons: Story = { + args: { + displayExploreButton: true, + displayDialButton: true, + searchShortcut: "⌘ K", + }, }; diff --git a/packages/shared-components/src/room-list/RoomListView/RoomListView.stories.tsx b/packages/shared-components/src/room-list/RoomListView/RoomListView.stories.tsx index 8e183fff4a..05fb9af6cc 100644 --- a/packages/shared-components/src/room-list/RoomListView/RoomListView.stories.tsx +++ b/packages/shared-components/src/room-list/RoomListView/RoomListView.stories.tsx @@ -13,6 +13,7 @@ import type { Room } from "../RoomListItemView"; import type { FilterId } from "../RoomListPrimaryFilters"; import { RoomListView, type RoomListSnapshot, type RoomListViewActions } from "./RoomListView"; import { useMockedViewModel } from "../../viewmodel"; +import { withViewDocs } from "../../../.storybook/withViewDocs"; import { renderAvatar, createGetRoomItemViewModel, @@ -26,7 +27,7 @@ type RoomListViewProps = RoomListSnapshot & RoomListViewActions & { renderAvatar const mockFilterIds: FilterId[] = ["unread", "people", "rooms", "favourite"]; // Wrapper component that creates a mocked ViewModel -const RoomListViewWrapper = ({ +const RoomListViewWrapperImpl = ({ onToggleFilter, createChatRoom, createRoom, @@ -44,6 +45,7 @@ const RoomListViewWrapper = ({ }); return ; }; +const RoomListViewWrapper = withViewDocs(RoomListViewWrapperImpl, RoomListView); const meta = { title: "Room List/RoomListView", diff --git a/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.stories.tsx b/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.stories.tsx index aa12524a97..ff2d7a3cd6 100644 --- a/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.stories.tsx +++ b/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.stories.tsx @@ -13,6 +13,7 @@ import type { Room } from "../RoomListItemView"; import { VirtualizedRoomListView, type RoomListViewState } from "./VirtualizedRoomListView"; import type { RoomListSnapshot, RoomListViewActions } from "../RoomListView"; import { useMockedViewModel } from "../../viewmodel"; +import { withViewDocs } from "../../../.storybook/withViewDocs"; import type { FilterId } from "../RoomListPrimaryFilters"; import { renderAvatar, createGetRoomItemViewModel, mockRoomIds } from "../story-mocks"; @@ -22,7 +23,7 @@ type RoomListStoryProps = RoomListSnapshot & RoomListViewActions & { renderAvata const storyRoomIds = mockRoomIds.slice(0, 10); // Wrapper component that creates a mocked ViewModel -const RoomListWrapper = ({ +const RoomListWrapperImpl = ({ onToggleFilter, createChatRoom, createRoom, @@ -45,6 +46,7 @@ const RoomListWrapper = ({ ); }; +const RoomListWrapper = withViewDocs(RoomListWrapperImpl, VirtualizedRoomListView); const mockFilterIds: FilterId[] = ["unread", "people"]; @@ -54,7 +56,7 @@ const defaultRoomListState: RoomListViewState = { filterKeys: undefined, }; -const meta: Meta = { +const meta = { title: "Room List/VirtualizedRoomListView", component: RoomListWrapper, tags: ["autodocs"], @@ -86,9 +88,9 @@ const meta: Meta = { ), ], -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = {}; diff --git a/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.stories.tsx b/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.stories.tsx index 07b890139e..b0147afaec 100644 --- a/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.stories.tsx +++ b/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.stories.tsx @@ -4,11 +4,12 @@ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial * Please see LICENSE files in the repository root for full details. */ -import { type Meta, type StoryFn } from "@storybook/react-vite"; +import { type Meta, type StoryObj } from "@storybook/react-vite"; import React, { type JSX } from "react"; import { fn } from "storybook/test"; import { useMockedViewModel } from "../../viewmodel"; +import { withViewDocs } from "../../../.storybook/withViewDocs"; import { RoomStatusBarState, RoomStatusBarView, @@ -18,7 +19,7 @@ import { type RoomStatusBarProps = RoomStatusBarViewSnapshot & RoomStatusBarViewActions; -const RoomStatusBarViewWrapper = ({ +const RoomStatusBarViewWrapperImpl = ({ onResendAllClick, onDeleteAllClick, onRetryRoomCreationClick, @@ -33,8 +34,9 @@ const RoomStatusBarViewWrapper = ({ }); return ; }; +const RoomStatusBarViewWrapper = withViewDocs(RoomStatusBarViewWrapperImpl, RoomStatusBarView); -export default { +const meta = { title: "room/RoomStatusBarView", component: RoomStatusBarViewWrapper, tags: ["autodocs"], @@ -45,61 +47,69 @@ export default { onRetryRoomCreationClick: fn(), onTermsAndConditionsClicked: fn(), }, -} as Meta; +} satisfies Meta; -const Template: StoryFn = (args) => ; +export default meta; +type Story = StoryObj; /** * Rendered when the client has lost connection with the server. */ -export const WithConnectionLost = Template.bind({}); -WithConnectionLost.args = { - state: RoomStatusBarState.ConnectionLost, +export const WithConnectionLost: Story = { + args: { + state: RoomStatusBarState.ConnectionLost, + }, }; /** * Rendered when the client needs the user to consent to some terms and conditions before * they can perform any room actions. */ -export const WithConsentLink = Template.bind({}); -WithConsentLink.args = { - state: RoomStatusBarState.NeedsConsent, - consentUri: "#example", +export const WithConsentLink: Story = { + args: { + state: RoomStatusBarState.NeedsConsent, + consentUri: "#example", + }, }; /** * Rendered when the server has hit a usage limit and is forbidding the user from performing * any actions in the room. There is an optional parameter to link to an admin to contact. */ -export const WithResourceLimit = Template.bind({}); -WithResourceLimit.args = { - state: RoomStatusBarState.ResourceLimited, - resourceLimit: "hs_disabled", - adminContactHref: "#example", +export const WithResourceLimit: Story = { + args: { + state: RoomStatusBarState.ResourceLimited, + resourceLimit: "hs_disabled", + adminContactHref: "#example", + }, }; /** * Rendered when the client has some unsent messages in the room, stored locally. */ -export const WithUnsentMessages = Template.bind({}); -WithUnsentMessages.args = { - state: RoomStatusBarState.UnsentMessages, - isResending: false, +export const WithUnsentMessages: Story = { + args: { + state: RoomStatusBarState.UnsentMessages, + isResending: false, + }, }; /** * Rendered when the client has some unsent messages in the room, stored locally and is * trying to send them. */ -export const WithUnsentMessagesSending = Template.bind({}); -WithUnsentMessagesSending.args = { - state: RoomStatusBarState.UnsentMessages, - isResending: true, +export const WithUnsentMessagesSending: Story = { + args: { + state: RoomStatusBarState.UnsentMessages, + isResending: true, + }, }; + /** * Rendered when a local room has failed to be created. */ -export const WithLocalRoomRetry = Template.bind({}); -WithLocalRoomRetry.args = { - state: RoomStatusBarState.LocalRoomFailed, +export const WithLocalRoomRetry: Story = { + args: { + state: RoomStatusBarState.LocalRoomFailed, + }, };