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:
@@ -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";
|
||||
|
||||
+39
@@ -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;
|
||||
}
|
||||
+93
@@ -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,
|
||||
},
|
||||
};
|
||||
+27
@@ -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();
|
||||
});
|
||||
});
|
||||
+104
@@ -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>
|
||||
);
|
||||
}
|
||||
+49
@@ -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";
|
||||
Reference in New Issue
Block a user