Refactor Reactions Row Button to shared-components (#31993)

* Refactoring of ReactionRowButton to shared component MVVM

* Removal of old component and creation of unit tests

* Update

* Update tests

* Update tests to mimic VM

* Update Lint Spacing

* Added onKeyDown to follow wcag rules

* Remove Unused code

* Update screenshots

* Removal of unessecery test and story

* Update snapshot

* Refactor reactions row VMs to granular setters and merge cheap snapshot updates

* Elist Fix

* Revert ReactionRowButtonToolTip Test

* Fix ReactionsRowButtonViewModel tooltip sync to use tooltip setProps

* Add dedicated ReactionsRowButtonViewModel unit tests for setters, tooltip sync, and click actions

* Better Wording On Functions

* Update snapshot

* Update packages/shared-components/src/message-body/ReactionsRowButton/ReactionsRowButtonView.tsx

Co-authored-by: Florian Duros <florian.duros@ormaz.fr>

* use native button and tighten view model

* Update Snapshots + small fixes on reactionrow

* Removal of Null on viewmodel and adapting ReactionRow

* Update test and removal of unused test since me MVVMD ReactionRowButton

* align assertions with refactored update behavior

* FIx issue with classNames component

* Update snapshot

* Removal of old test snapshot

* Update Snapshot

* Implement Css + Snapshot Updates

* Update Snapshot and css to match old component style

* restore MatrixClientContext fallback in ReactionsRow for export/test rendering

* restore client fallback in ReactionsRow to preserve export rendering

* Remove Unused Pcss FIle

* Update Css

* Update misstake always having button default to disabled render

* Remove unsimiler css to original component

* Update Snapshot to reflect css adjustments

* Update css

* Update font to compund

* Update css to reflect old component

* Update css to compund

* Update Snapshot and css

* Update css

* Update HTML snapshot

* Update css

* Update Css

* Update snapshots

* Update HTML snapshot

* Update css + snapshot

* Update HTML snapshot

* Removal of mx css

* Update snapshot based on css removal

* Update Html snapshot

* Apply suggestion from @florianduros

Co-authored-by: Florian Duros <florian.duros@ormaz.fr>

* remove setContext from ReactionsRowButtonViewModel

* Update packages/shared-components/src/message-body/ReactionsRowButton/ReactionsRowButtonView.tsx

Co-authored-by: Florian Duros <florian.duros@ormaz.fr>

* add tooltipVm to ReactionsRowButtonViewSnapshot

* added compound token variables

* remove className from content and count inner elements

* use useMatrixClientContext() directly for ReactionsRowButtonViewModel

* Update snapshots

* Update snapshot + fix Typescript error on test file

* Removal of line-height in css

* Added line-height back and removed font: inherit;

* derive ReactionsRowButton className/ariaLabel types from HTML button attrs

* Update packages/shared-components/src/message-body/ReactionsRowButton/ReactionsRowButtonView.tsx

Co-authored-by: Florian Duros <florian.duros@ormaz.fr>

* Update src/viewmodels/message-body/ReactionsRowButtonViewModel.ts

Co-authored-by: Florian Duros <florian.duros@ormaz.fr>

* Update src/viewmodels/message-body/ReactionsRowButtonViewModel.ts

Co-authored-by: Florian Duros <florian.duros@ormaz.fr>

* Update test/viewmodels/message-body/ReactionsRowButtonViewModel-test.tsx

Co-authored-by: Florian Duros <florian.duros@ormaz.fr>

* Update snapshots and lint issues

* Update model to respond to changes

* Update aria label on view

---------

Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
This commit is contained in:
Zack
2026-02-25 12:18:03 +01:00
committed by GitHub
parent 05598a3229
commit e26cbba541
19 changed files with 760 additions and 869 deletions
+1
View File
@@ -20,6 +20,7 @@ export * from "./message-body/MediaBody";
export * from "./message-body/MessageTimestampView";
export * from "./message-body/DecryptionFailureBodyView";
export * from "./message-body/ReactionsRowButtonTooltip";
export * from "./message-body/ReactionsRowButton";
export * from "./message-body/TimelineSeparator/";
export * from "./pill-input/Pill";
export * from "./pill-input/PillInput";
@@ -0,0 +1,39 @@
/*
* 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.
*/
.reactionsRowButton {
display: inline-flex;
all: unset;
line-height: var(--cpd-font-size-heading-sm);
padding: 1px var(--cpd-space-1-5x);
border: 1px solid var(--cpd-color-gray-400);
border-radius: 10px;
background-color: var(--cpd-color-gray-200);
user-select: none;
align-items: center;
}
.reactionsRowButtonSelected {
background-color: var(--cpd-color-green-300);
border-color: var(--cpd-color-green-800);
}
.reactionsRowButtonDisabled {
cursor: not-allowed;
}
.reactionsRowButtonContent {
max-width: 100px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
padding-right: var(--cpd-space-1x);
}
.reactionsRowButtonCount {
white-space: nowrap;
}
@@ -0,0 +1,93 @@
/*
* 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.
*/
import React, { type JSX } from "react";
import { fn } from "storybook/test";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { useMockedViewModel } from "../../viewmodel";
import { type ReactionsRowButtonTooltipViewSnapshot } from "../ReactionsRowButtonTooltip";
import {
ReactionsRowButtonView,
type ReactionsRowButtonViewSnapshot,
type ReactionsRowButtonViewActions,
} from "./ReactionsRowButtonView";
type WrapperProps = Omit<ReactionsRowButtonViewSnapshot, "tooltipVm"> &
Partial<ReactionsRowButtonViewActions> & {
ariaLabel?: string;
tooltipFormattedSenders?: ReactionsRowButtonTooltipViewSnapshot["formattedSenders"];
tooltipCaption?: ReactionsRowButtonTooltipViewSnapshot["caption"];
tooltipOpen?: ReactionsRowButtonTooltipViewSnapshot["tooltipOpen"];
};
const ReactionsRowButtonViewWrapper = ({
tooltipFormattedSenders,
tooltipCaption,
tooltipOpen,
onClick,
...snapshotProps
}: WrapperProps): JSX.Element => {
const tooltipVm = useMockedViewModel(
{
formattedSenders: tooltipFormattedSenders,
caption: tooltipCaption,
tooltipOpen,
},
{},
);
const vm = useMockedViewModel(
{
...snapshotProps,
tooltipVm,
},
{
onClick: onClick ?? fn(),
},
);
return <ReactionsRowButtonView vm={vm} />;
};
const meta = {
title: "MessageBody/ReactionsRowButton",
component: ReactionsRowButtonViewWrapper,
tags: ["autodocs"],
args: {
content: "👍",
count: 2,
ariaLabel: "Alice and Bob reacted with 👍",
isSelected: false,
isDisabled: false,
imageSrc: undefined,
imageAlt: undefined,
tooltipFormattedSenders: undefined,
tooltipCaption: undefined,
tooltipOpen: true,
},
} satisfies Meta<typeof ReactionsRowButtonViewWrapper>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Selected: Story = {
args: {
isSelected: true,
},
};
export const WithTooltip: Story = {
args: {
count: 3,
tooltipFormattedSenders: "Alice, Bob and Charlie",
tooltipCaption: ":thumbsup:",
tooltipOpen: true,
},
};
@@ -0,0 +1,27 @@
/*
* 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.
*/
import { composeStories } from "@storybook/react-vite";
import { render } from "@test-utils";
import React from "react";
import { describe, it, expect } from "vitest";
import * as stories from "./ReactionsRowButton.stories";
const { Default, Selected } = composeStories(stories);
describe("ReactionsRowButton", () => {
it("renders the default reaction button", () => {
const { container } = render(<Default />);
expect(container).toMatchSnapshot();
});
it("renders the selected reaction button", () => {
const { container } = render(<Selected />);
expect(container).toMatchSnapshot();
});
});
@@ -0,0 +1,104 @@
/*
* 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.
*/
import React, { type HTMLAttributes, type JSX } from "react";
import classNames from "classnames";
import { type ViewModel, useViewModel } from "../../viewmodel";
import { ReactionsRowButtonTooltipView, type ReactionsRowButtonTooltipViewModel } from "../ReactionsRowButtonTooltip";
import styles from "./ReactionsRowButton.module.css";
export interface ReactionsRowButtonViewSnapshot extends Pick<
HTMLAttributes<HTMLButtonElement>,
"className" | "aria-label"
> {
/**
* The reaction content to display when not using a custom image.
*/
content?: string;
/**
* The total number of reactions for this content.
*/
count: number;
/**
* Whether the reaction button is selected by the current user.
*/
isSelected: boolean;
/**
* Whether the reaction button is disabled.
* @default false
*/
isDisabled?: boolean;
/**
* The image URL to render when using a custom reaction image.
*/
imageSrc?: string;
/**
* The alt text for the custom reaction image.
*/
imageAlt?: string;
/**
* View model for the tooltip wrapper.
*/
tooltipVm: ReactionsRowButtonTooltipViewModel;
}
export interface ReactionsRowButtonViewActions {
/**
* Called when the user activates the reaction button.
*/
onClick: () => void;
}
export type ReactionsRowButtonViewModel = ViewModel<ReactionsRowButtonViewSnapshot> & ReactionsRowButtonViewActions;
interface ReactionsRowButtonViewProps {
/**
* The view model for the reactions row button.
*/
vm: ReactionsRowButtonViewModel;
}
/**
* Renders the reaction button in a reactions row.
*/
export function ReactionsRowButtonView({ vm }: Readonly<ReactionsRowButtonViewProps>): JSX.Element {
const snapshot = useViewModel(vm) as ReactionsRowButtonViewSnapshot & { ariaLabel?: string };
const { content, count, className, isSelected, isDisabled, imageSrc, imageAlt, tooltipVm } = snapshot;
const ariaLabel = snapshot["aria-label"] ?? snapshot.ariaLabel;
const ariaDisabled = isDisabled ? true : undefined;
const classes = classNames(className, styles.reactionsRowButton, {
[styles.reactionsRowButtonSelected]: isSelected,
[styles.reactionsRowButtonDisabled]: isDisabled,
});
const reactionContent = imageSrc ? (
<img className={styles.reactionsRowButtonContent} alt={imageAlt ?? ""} src={imageSrc} width="16" height="16" />
) : (
<span className={styles.reactionsRowButtonContent} aria-hidden="true">
{content ?? ""}
</span>
);
return (
<ReactionsRowButtonTooltipView vm={tooltipVm}>
<button
type="button"
className={classes}
tabIndex={0}
aria-label={ariaLabel}
aria-disabled={ariaDisabled}
onClick={isDisabled ? undefined : vm.onClick}
>
{reactionContent}
<span className={styles.reactionsRowButtonCount} aria-hidden="true">
{count}
</span>
</button>
</ReactionsRowButtonTooltipView>
);
}
@@ -0,0 +1,49 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`ReactionsRowButton > renders the default reaction button 1`] = `
<div>
<button
aria-label="Alice and Bob reacted with 👍"
class="reactionsRowButton"
tabindex="0"
type="button"
>
<span
aria-hidden="true"
class="reactionsRowButtonContent"
>
👍
</span>
<span
aria-hidden="true"
class="reactionsRowButtonCount"
>
2
</span>
</button>
</div>
`;
exports[`ReactionsRowButton > renders the selected reaction button 1`] = `
<div>
<button
aria-label="Alice and Bob reacted with 👍"
class="reactionsRowButton reactionsRowButtonSelected"
tabindex="0"
type="button"
>
<span
aria-hidden="true"
class="reactionsRowButtonContent"
>
👍
</span>
<span
aria-hidden="true"
class="reactionsRowButtonCount"
>
2
</span>
</button>
</div>
`;
@@ -0,0 +1,13 @@
/*
* 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.
*/
export {
ReactionsRowButtonView,
type ReactionsRowButtonViewSnapshot,
type ReactionsRowButtonViewModel,
type ReactionsRowButtonViewActions,
} from "./ReactionsRowButtonView";