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
This commit is contained in:
Florian Duros
2026-02-23 17:03:49 +01:00
committed by GitHub
parent 77670eb369
commit b08cf5fdaa
16 changed files with 422 additions and 258 deletions
@@ -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, {
@@ -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 <MyView vm={vm} />;
* };
* const MyViewWrapper = withViewDocs(MyViewWrapperImpl, MyView);
* ```
*/
export function withViewDocs<T extends (...args: never[]) => 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<string, unknown>;
}
+31 -15
View File
@@ -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 <MyComponentView vm={vm} />;
};
// 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<typeof MyComponentViewWrapper>;
} satisfies Meta<typeof MyComponentViewWrapper>;
const Template: StoryFn<typeof MyComponentViewWrapper> = (args) => <MyComponentViewWrapper {...args} />;
export default meta;
type Story = StoryObj<typeof MyComponentViewWrapper>;
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<typeof RoomListSearchViewWrapper>;
} satisfies Meta<typeof RoomListSearchViewWrapper>;
export default meta;
```
The Figma design will appear in the "Design" tab in Storybook.
@@ -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 <AudioPlayerView vm={vm} />;
};
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<typeof AudioPlayerViewWrapper>;
} satisfies Meta<typeof AudioPlayerViewWrapper>;
const Template: StoryFn<typeof AudioPlayerViewWrapper> = (args) => <AudioPlayerViewWrapper {...args} />;
export default meta;
type Story = StoryObj<typeof meta>;
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,
},
};
@@ -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 <EncryptionEventView vm={vm} />;
};
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<typeof EncryptionEventViewWrapper>;
} satisfies Meta<typeof EncryptionEventViewWrapper>;
const Template: StoryFn<typeof EncryptionEventViewWrapper> = (args) => <EncryptionEventViewWrapper {...args} />;
export default meta;
type Story = StoryObj<typeof meta>;
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: <span>14:56</span>,
export const WithTimestamp: Story = {
args: {
state: EncryptionEventState.ENABLED,
timestamp: <span>14:56</span>,
},
};
@@ -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 <DecryptionFailureBodyView vm={vm} />;
};
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<typeof DecryptionFailureBodyViewWrapper>;
} satisfies Meta<typeof DecryptionFailureBodyViewWrapper>;
const Template: StoryFn<typeof DecryptionFailureBodyViewWrapper> = (args) => (
<DecryptionFailureBodyViewWrapper {...args} />
);
export default meta;
type Story = StoryObj<typeof meta>;
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,
},
};
@@ -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 <MessageTimestampView vm={vm} />;
};
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<typeof MessageTimestampWrapper>;
} satisfies Meta<typeof MessageTimestampWrapper>;
const Template: StoryFn<typeof MessageTimestampWrapper> = (args) => <MessageTimestampWrapper {...args} />;
export default meta;
type Story = StoryObj<typeof meta>;
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"),
},
};
@@ -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 <ReactionsRowButtonTooltipView vm={vm}>{children}</ReactionsRowButtonTooltipView>;
};
const ReactionsRowButtonTooltipViewWrapper = withViewDocs(
ReactionsRowButtonTooltipViewWrapperImpl,
ReactionsRowButtonTooltipView,
);
export default {
const meta = {
title: "MessageBody/ReactionsRowButtonTooltip",
component: ReactionsRowButtonTooltipViewWrapper,
tags: ["autodocs"],
@@ -32,38 +37,41 @@ export default {
args: {
children: <button>👍 3</button>,
},
} as Meta<typeof ReactionsRowButtonTooltipViewWrapper>;
} satisfies Meta<typeof ReactionsRowButtonTooltipViewWrapper>;
const Template: StoryFn<typeof ReactionsRowButtonTooltipViewWrapper> = (args) => (
<ReactionsRowButtonTooltipViewWrapper {...args} />
);
export default meta;
type Story = StoryObj<typeof meta>;
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: <button> 8</button>,
tooltipOpen: true,
export const ManySenders: Story = {
args: {
formattedSenders: "Alice, Bob, Charlie, David, Eve, Frank and 2 others",
caption: ":heart:",
children: <button> 8</button>,
tooltipOpen: true,
},
};
export const WithoutCaption = Template.bind({});
WithoutCaption.args = {
formattedSenders: "Alice and Bob",
caption: undefined,
children: <button>🎉 2</button>,
tooltipOpen: true,
export const WithoutCaption: Story = {
args: {
formattedSenders: "Alice and Bob",
caption: undefined,
children: <button>🎉 2</button>,
tooltipOpen: true,
},
};
export const NoTooltip = Template.bind({});
NoTooltip.args = {
formattedSenders: undefined,
caption: undefined,
children: <button>👍 1</button>,
export const NoTooltip: Story = {
args: {
formattedSenders: undefined,
caption: undefined,
children: <button>👍 1</button>,
},
};
@@ -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 <DisambiguatedProfileView vm={vm} />;
};
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<typeof DisambiguatedProfileViewWrapper>;
} satisfies Meta<typeof DisambiguatedProfileViewWrapper>;
const Template: StoryFn<typeof DisambiguatedProfileViewWrapper> = (args) => (
<DisambiguatedProfileViewWrapper {...args} />
);
export default meta;
type Story = StoryObj<typeof meta>;
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,
},
};
@@ -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 <WidgetContextMenuView vm={vm} />;
};
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: (
<IconButton size="24px" aria-label="context menu trigger button" inert={true} tabIndex={-1}>
@@ -69,16 +70,18 @@ export default {
onFinished: fn(),
onMoveButton: fn(),
},
} as Meta<typeof WidgetContextMenuViewWrapper>;
} satisfies Meta<typeof WidgetContextMenuViewWrapper>;
const Template: StoryFn<typeof WidgetContextMenuViewWrapper> = (args) => <WidgetContextMenuViewWrapper {...args} />;
export default meta;
type Story = StoryObj<typeof WidgetContextMenuViewWrapper>;
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,
},
};
@@ -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 <RoomListHeaderView vm={vm} />;
};
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<typeof RoomListHeaderViewWrapper>;
} satisfies Meta<typeof RoomListHeaderViewWrapper>;
const Template: StoryFn<typeof RoomListHeaderViewWrapper> = (args) => <RoomListHeaderViewWrapper {...args} />;
export default meta;
type Story = StoryObj<typeof meta>;
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,
},
};
@@ -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",
@@ -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 <RoomListSearchView vm={vm} />;
};
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<typeof RoomListSearchViewWrapper>;
} satisfies Meta<typeof RoomListSearchViewWrapper>;
const Template: StoryFn<typeof RoomListSearchViewWrapper> = (args) => <RoomListSearchViewWrapper {...args} />;
export default meta;
type Story = StoryObj<typeof meta>;
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",
},
};
@@ -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 <RoomListView vm={vm} renderAvatar={renderAvatarProp} />;
};
const RoomListViewWrapper = withViewDocs(RoomListViewWrapperImpl, RoomListView);
const meta = {
title: "Room List/RoomListView",
@@ -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 = ({
</div>
);
};
const RoomListWrapper = withViewDocs(RoomListWrapperImpl, VirtualizedRoomListView);
const mockFilterIds: FilterId[] = ["unread", "people"];
@@ -54,7 +56,7 @@ const defaultRoomListState: RoomListViewState = {
filterKeys: undefined,
};
const meta: Meta<RoomListStoryProps> = {
const meta = {
title: "Room List/VirtualizedRoomListView",
component: RoomListWrapper,
tags: ["autodocs"],
@@ -86,9 +88,9 @@ const meta: Meta<RoomListStoryProps> = {
</div>
),
],
};
} satisfies Meta<typeof RoomListWrapper>;
export default meta;
type Story = StoryObj<RoomListStoryProps>;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
@@ -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 <RoomStatusBarView vm={vm} />;
};
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<typeof RoomStatusBarViewWrapper>;
} satisfies Meta<typeof RoomStatusBarViewWrapper>;
const Template: StoryFn<typeof RoomStatusBarViewWrapper> = (args) => <RoomStatusBarViewWrapper {...args} />;
export default meta;
type Story = StoryObj<typeof RoomStatusBarViewWrapper>;
/**
* 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,
},
};