Skip to content

Commit ed6cc4c

Browse files
authored
Merge branch 'develop' into refactor/ReactionsRowButton
2 parents 671fecd + 1c5694e commit ed6cc4c

216 files changed

Lines changed: 1337 additions & 897 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.eslintrc.cjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ module.exports = {
199199
files: ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}", "playwright/**/*.ts", "*.ts"],
200200
extends: ["plugin:matrix-org/typescript", "plugin:matrix-org/react"],
201201
rules: {
202+
"@typescript-eslint/unbound-method": ["error", { ignoreStatic: true }],
202203
"@typescript-eslint/explicit-function-return-type": [
203204
"error",
204205
{
@@ -238,6 +239,7 @@ module.exports = {
238239
"@typescript-eslint/explicit-function-return-type": "off",
239240
"@typescript-eslint/explicit-member-accessibility": "off",
240241
"@typescript-eslint/no-empty-object-type": "off",
242+
"@typescript-eslint/unbound-method": "off",
241243

242244
// Jest/Playwright specific
243245

docs/MVVM.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ interface FooViewActions {
4747

4848
// ViewModel is an object (usually a class) that implements both the interfaces listed above.
4949
// https://github.com/element-hq/element-web/blob/develop/packages/shared-components/src/ViewModel.ts
50-
export type FooViewModel = ViewModel<FooViewSnapshot> & FooViewActions;
50+
export type FooViewModel = ViewModel<FooViewSnapshot, FooViewActions>;
5151

5252
interface FooViewProps {
5353
// Ideally the view only depends on the view model i.e you don't expect any other props here.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,6 @@
252252
"prettier": "3.8.1",
253253
"process": "^0.11.10",
254254
"raw-loader": "^4.0.2",
255-
"rimraf": "^6.0.0",
256255
"semver": "^7.5.2",
257256
"source-map-loader": "^5.0.0",
258257
"stylelint": "^17.0.0",

packages/shared-components/.eslintrc.cjs

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ module.exports = {
99
root: true,
1010
plugins: ["matrix-org", "eslint-plugin-react-compiler"],
1111
extends: [
12-
"plugin:matrix-org/babel",
1312
"plugin:matrix-org/react",
1413
"plugin:matrix-org/a11y",
14+
"plugin:matrix-org/typescript",
15+
"plugin:matrix-org/react",
1516
"plugin:storybook/recommended",
1617
],
1718
parserOptions: {
@@ -42,37 +43,36 @@ module.exports = {
4243
],
4344
},
4445
],
46+
47+
"@typescript-eslint/unbound-method": ["error", { ignoreStatic: true }],
48+
"@typescript-eslint/explicit-function-return-type": [
49+
"error",
50+
{
51+
allowExpressions: true,
52+
},
53+
],
54+
55+
// We're okay being explicit at the moment
56+
// "@typescript-eslint/no-empty-interface": "off",
57+
// We'd rather not do this but we do
58+
// "@typescript-eslint/ban-ts-comment": "off",
59+
// We're okay with assertion errors when we ask for them
60+
"@typescript-eslint/no-non-null-assertion": "off",
61+
"@typescript-eslint/no-empty-object-type": [
62+
"error",
63+
{
64+
// We do this sometimes to brand interfaces
65+
allowInterfaces: "with-single-extends",
66+
},
67+
],
68+
"storybook/meta-satisfies-type": "error",
4569
},
4670
overrides: [
4771
{
48-
files: ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}"],
49-
extends: ["plugin:matrix-org/typescript", "plugin:matrix-org/react"],
72+
files: ["src/**/*.test.{ts,tsx}"],
5073
rules: {
51-
"@typescript-eslint/explicit-function-return-type": [
52-
"error",
53-
{
54-
allowExpressions: true,
55-
},
56-
],
57-
58-
// Remove Babel things manually due to override limitations
59-
"@babel/no-invalid-this": ["off"],
60-
61-
// We're okay being explicit at the moment
62-
"@typescript-eslint/no-empty-interface": "off",
63-
// We disable this while we're transitioning
74+
"@typescript-eslint/unbound-method": "off",
6475
"@typescript-eslint/no-explicit-any": "off",
65-
// We'd rather not do this but we do
66-
"@typescript-eslint/ban-ts-comment": "off",
67-
// We're okay with assertion errors when we ask for them
68-
"@typescript-eslint/no-non-null-assertion": "off",
69-
"@typescript-eslint/no-empty-object-type": [
70-
"error",
71-
{
72-
// We do this sometimes to brand interfaces
73-
allowInterfaces: "with-single-extends",
74-
},
75-
],
7676
},
7777
},
7878
],

packages/shared-components/.storybook/main.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ const config: StorybookConfig = {
4949
},
5050
typescript: {
5151
reactDocgen: "react-docgen-typescript",
52+
reactDocgenTypescriptOptions: {
53+
// The default exclude is ["**/**.stories.tsx"] which prevents
54+
// docgen from extracting snapshot field descriptions from wrapper
55+
// components defined in story files.
56+
exclude: [],
57+
},
5258
},
5359
async viteFinal(config) {
5460
return mergeConfig(config, {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2026 Element Creations Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
/**
9+
* Copies the component description and props documentation from a View's
10+
* `__docgenInfo` (injected at build time by Storybook's react-docgen-typescript
11+
* Vite plugin) onto the story wrapper component.
12+
*
13+
* This lets Storybook's default `extractComponentDescription` pick up the
14+
* View's JSDoc and display per-field descriptions in the ArgTypes table.
15+
*
16+
* **Important:** the wrapper must be defined as a named variable *before*
17+
* being passed here so that react-docgen-typescript can extract its props.
18+
*
19+
* @example
20+
* ```ts
21+
* const MyViewWrapperImpl = (props: MyViewProps) => {
22+
* const vm = useMockedViewModel(props, {});
23+
* return <MyView vm={vm} />;
24+
* };
25+
* const MyViewWrapper = withViewDocs(MyViewWrapperImpl, MyView);
26+
* ```
27+
*/
28+
export function withViewDocs<T extends (...args: never[]) => unknown>(wrapper: T, view: object): T {
29+
const viewInfo = (view as { __docgenInfo?: DocgenInfo }).__docgenInfo;
30+
const viewDescription = viewInfo?.description;
31+
if (!viewDescription) return wrapper;
32+
33+
// The wrapper must be defined as a named variable (not inline) so that
34+
// react-docgen-typescript can extract its props. The docgen Vite plugin
35+
// appends a `Wrapper.__docgenInfo = { … }` assignment at the *end* of the
36+
// module, which runs **after** this function. We install a setter trap so
37+
// that the View's description is merged into the generated info.
38+
let stored: DocgenInfo | undefined = (wrapper as { __docgenInfo?: DocgenInfo }).__docgenInfo;
39+
Object.defineProperty(wrapper, "__docgenInfo", {
40+
get() {
41+
return stored;
42+
},
43+
set(incoming: DocgenInfo) {
44+
stored = {
45+
...incoming,
46+
description: incoming.description || viewDescription,
47+
};
48+
},
49+
configurable: true,
50+
enumerable: true,
51+
});
52+
53+
// Also apply immediately for the current state.
54+
stored = { ...stored, description: viewDescription };
55+
56+
return wrapper;
57+
}
58+
59+
interface DocgenInfo {
60+
description?: string;
61+
props?: Record<string, unknown>;
62+
}

packages/shared-components/README.md

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ instance should be provided as a prop.
6868

6969
Here's a basic example:
7070

71-
```jsx
71+
```tsx
7272
import { ViewExample } from "@element-hq/web-shared-components";
7373

7474
function MyApp() {
@@ -180,27 +180,32 @@ export const Disabled: Story = {
180180

181181
#### MVVM Component Stories
182182

183-
For MVVM components, create a wrapper component that uses `useMockedViewModel`:
183+
For MVVM components, create a wrapper component that uses `useMockedViewModel` and `withViewDocs`:
184184

185185
```tsx
186186
import React, { type JSX } from "react";
187187
import { fn } from "storybook/test";
188-
import type { Meta, StoryFn } from "@storybook/react-vite";
188+
import type { Meta, StoryObj } from "@storybook/react-vite";
189189
import { MyComponentView, type MyComponentViewSnapshot, type MyComponentViewActions } from "./MyComponentView";
190-
import { useMockedViewModel } from "../../useMockedViewModel";
190+
import { useMockedViewModel } from "../../viewmodel";
191+
import { withViewDocs } from "../../../.storybook/withViewDocs";
191192

192193
// Combine snapshot and actions for easier typing
193194
type MyComponentProps = MyComponentViewSnapshot & MyComponentViewActions;
194195

195-
// Wrapper component that creates a mocked ViewModel
196-
const MyComponentViewWrapper = ({ onAction, ...rest }: MyComponentProps): JSX.Element => {
196+
// Wrapper component that creates a mocked ViewModel.
197+
// Must be a named variable (not inline) for docgen to extract its props.
198+
const MyComponentViewWrapperImpl = ({ onAction, ...rest }: MyComponentProps): JSX.Element => {
197199
const vm = useMockedViewModel(rest, {
198200
onAction,
199201
});
200202
return <MyComponentView vm={vm} />;
201203
};
204+
// withViewDocs copies the View's JSDoc description onto the wrapper for Storybook autodocs
205+
const MyComponentViewWrapper = withViewDocs(MyComponentViewWrapperImpl, MyComponentView);
202206

203-
export default {
207+
// Must use `satisfies` (not `as` or `: Meta`) to preserve type info for docgen
208+
const meta = {
204209
title: "Category/MyComponentView",
205210
component: MyComponentViewWrapper,
206211
tags: ["autodocs"],
@@ -211,20 +216,29 @@ export default {
211216
// Action properties (callbacks)
212217
onAction: fn(),
213218
},
214-
} as Meta<typeof MyComponentViewWrapper>;
219+
} satisfies Meta<typeof MyComponentViewWrapper>;
215220

216-
const Template: StoryFn<typeof MyComponentViewWrapper> = (args) => <MyComponentViewWrapper {...args} />;
221+
export default meta;
222+
type Story = StoryObj<typeof MyComponentViewWrapper>;
217223

218-
export const Default = Template.bind({});
224+
export const Default: Story = {};
219225

220-
export const Loading = Template.bind({});
221-
Loading.args = {
222-
isLoading: true,
226+
export const Loading: Story = {
227+
args: {
228+
isLoading: true,
229+
},
223230
};
224231
```
225232

226233
Thanks to this approach, we can directly use primitives in the story arguments instead of a view model object.
227234

235+
> [!IMPORTANT]
236+
> Three requirements must be met for snapshot field documentation to appear in Storybook's ArgTypes table:
237+
>
238+
> 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.
239+
> 2. **`withViewDocs` call** — wraps the wrapper component with the original View to copy the View's JSDoc description.
240+
> 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.
241+
228242
#### Linking Figma Designs
229243

230244
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
239253
Example with Figma integration:
240254

241255
```tsx
242-
export default {
256+
const meta = {
243257
title: "Room List/RoomListSearchView",
244258
component: RoomListSearchViewWrapper,
245259
tags: ["autodocs"],
@@ -252,7 +266,9 @@ export default {
252266
url: "https://www.figma.com/design/vlmt46QDdE4dgXDiyBJXqp/ER-33-Left-Panel?node-id=98-1979",
253267
},
254268
},
255-
} as Meta<typeof RoomListSearchViewWrapper>;
269+
} satisfies Meta<typeof RoomListSearchViewWrapper>;
270+
271+
export default meta;
256272
```
257273

258274
The Figma design will appear in the "Design" tab in Storybook.
-60 Bytes
Loading
Loading
10 Bytes
Loading

0 commit comments

Comments
 (0)