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:
@@ -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>;
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
+40
-30
@@ -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>,
|
||||
},
|
||||
};
|
||||
|
||||
+36
-30
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
+40
-31
@@ -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"),
|
||||
},
|
||||
};
|
||||
|
||||
+37
-29
@@ -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>,
|
||||
},
|
||||
};
|
||||
|
||||
+38
-32
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
+16
-13
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
+17
-12
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
+3
-1
@@ -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",
|
||||
|
||||
+23
-17
@@ -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",
|
||||
|
||||
+6
-4
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user