Merge pull request #12549 from matrix-org/florianduros/tooltip/legacy-tooltip

Tooltip: Use tooltip compound instead of react-sdk tooltip
This commit is contained in:
David Baker 2024-05-23 09:53:05 +01:00 committed by GitHub
commit 88e8e2df03
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 351 additions and 279 deletions

View File

@ -57,6 +57,9 @@ export interface ITooltipProps {
type State = Partial<Pick<CSSProperties, "display" | "right" | "top" | "transform" | "left">>;
/**
* @deprecated Use [compound tooltip](https://element-hq.github.io/compound-web/?path=/docs/tooltip--docs) instead
*/
export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
private static container: HTMLElement;
private parent: Element | null = null;

View File

@ -44,20 +44,10 @@ export interface IProps {
customReactionImagesEnabled?: boolean;
}
interface IState {
tooltipRendered: boolean;
tooltipVisible: boolean;
}
export default class ReactionsRowButton extends React.PureComponent<IProps, IState> {
export default class ReactionsRowButton extends React.PureComponent<IProps> {
public static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
public state = {
tooltipRendered: false,
tooltipVisible: false,
};
public onClick = (): void => {
const { mxEvent, myReactionEvent, content } = this.props;
if (myReactionEvent) {
@ -74,21 +64,6 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
}
};
public onMouseOver = (): void => {
this.setState({
// To avoid littering the DOM with a tooltip for every reaction,
// only render it on first use.
tooltipRendered: true,
tooltipVisible: true,
});
};
public onMouseLeave = (): void => {
this.setState({
tooltipVisible: false,
});
};
public render(): React.ReactNode {
const { mxEvent, content, count, reactionEvents, myReactionEvent } = this.props;
@ -97,19 +72,6 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
mx_ReactionsRowButton_selected: !!myReactionEvent,
});
let tooltip: JSX.Element | undefined;
if (this.state.tooltipRendered) {
tooltip = (
<ReactionsRowButtonTooltip
mxEvent={this.props.mxEvent}
content={content}
reactionEvents={reactionEvents}
visible={this.state.tooltipVisible}
customReactionImagesEnabled={this.props.customReactionImagesEnabled}
/>
);
}
const room = this.context.getRoom(mxEvent.getRoomId());
let label: string | undefined;
let customReactionName: string | undefined;
@ -156,20 +118,24 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
}
return (
<AccessibleButton
className={classes}
aria-label={label}
onClick={this.onClick}
disabled={this.props.disabled}
onMouseOver={this.onMouseOver}
onMouseLeave={this.onMouseLeave}
<ReactionsRowButtonTooltip
mxEvent={this.props.mxEvent}
content={content}
reactionEvents={reactionEvents}
customReactionImagesEnabled={this.props.customReactionImagesEnabled}
>
{reactionContent}
<span className="mx_ReactionsRowButton_count" aria-hidden="true">
{count}
</span>
{tooltip}
</AccessibleButton>
<AccessibleButton
className={classes}
aria-label={label}
onClick={this.onClick}
disabled={this.props.disabled}
>
{reactionContent}
<span className="mx_ReactionsRowButton_count" aria-hidden="true">
{count}
</span>
</AccessibleButton>
</ReactionsRowButtonTooltip>
);
}
}

View File

@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import React, { PropsWithChildren } from "react";
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import { Tooltip } from "@vector-im/compound-web";
import { unicodeToShortcode } from "../../../HtmlUtils";
import { _t } from "../../../languageHandler";
import { formatList } from "../../../utils/FormattingUtils";
import Tooltip from "../elements/Tooltip";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { REACTION_SHORTCODE_KEY } from "./ReactionsRow";
interface IProps {
@ -30,20 +30,18 @@ interface IProps {
content: string;
// A list of Matrix reaction events for this key
reactionEvents: MatrixEvent[];
visible: boolean;
// Whether to render custom image reactions
customReactionImagesEnabled?: boolean;
}
export default class ReactionsRowButtonTooltip extends React.PureComponent<IProps> {
export default class ReactionsRowButtonTooltip extends React.PureComponent<PropsWithChildren<IProps>> {
public static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
public render(): React.ReactNode {
const { content, reactionEvents, mxEvent, visible } = this.props;
const { content, reactionEvents, mxEvent, children } = this.props;
const room = this.context.getRoom(mxEvent.getRoomId());
let tooltipLabel: JSX.Element | undefined;
if (room) {
const senders: string[] = [];
let customReactionName: string | undefined;
@ -57,34 +55,16 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent<IProp
undefined;
}
const shortName = unicodeToShortcode(content) || customReactionName;
tooltipLabel = (
<div>
{_t(
"timeline|reactions|tooltip",
{
shortName,
},
{
reactors: () => {
return <div className="mx_Tooltip_title">{formatList(senders, 6)}</div>;
},
reactedWith: (sub) => {
if (!shortName) {
return null;
}
return <div className="mx_Tooltip_sub">{sub}</div>;
},
},
)}
</div>
const formattedSenders = formatList(senders, 6);
const caption = shortName ? _t("timeline|reactions|tooltip_caption", { shortName }) : undefined;
return (
<Tooltip label={formattedSenders} caption={caption} placement="right">
{children}
</Tooltip>
);
}
let tooltip: JSX.Element | undefined;
if (tooltipLabel) {
tooltip = <Tooltip visible={visible} label={tooltipLabel} />;
}
return tooltip;
return children;
}
}

View File

@ -25,6 +25,7 @@ import {
THREAD_RELATION_TYPE,
} from "matrix-js-sdk/src/matrix";
import { Optional } from "matrix-events-sdk";
import { Tooltip } from "@vector-im/compound-web";
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
@ -40,7 +41,6 @@ import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import VoiceRecordComposerTile from "./VoiceRecordComposerTile";
import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore";
import { RecordingState } from "../../../audio/VoiceRecording";
import Tooltip, { Alignment } from "../elements/Tooltip";
import ResizeNotifier from "../../../utils/ResizeNotifier";
import { E2EStatus } from "../../../utils/ShieldUtils";
import SendMessageComposer, { SendMessageComposer as SendMessageComposerClass } from "./SendMessageComposer";
@ -110,7 +110,6 @@ interface IState {
}
export class MessageComposer extends React.Component<IProps, IState> {
private tooltipId = `mx_MessageComposer_${Math.random()}`;
private dispatcherRef?: string;
private messageComposerInput = createRef<SendMessageComposerClass>();
private voiceRecordingButton = createRef<VoiceRecordComposerTile>();
@ -568,12 +567,9 @@ export class MessageComposer extends React.Component<IProps, IState> {
}
let recordingTooltip: JSX.Element | undefined;
if (this.state.recordingTimeLeftSeconds) {
const secondsLeft = Math.round(this.state.recordingTimeLeftSeconds);
recordingTooltip = (
<Tooltip id={this.tooltipId} label={formatTimeLeft(secondsLeft)} alignment={Alignment.Top} />
);
}
const isTooltipOpen = Boolean(this.state.recordingTimeLeftSeconds);
const secondsLeft = this.state.recordingTimeLeftSeconds ? Math.round(this.state.recordingTimeLeftSeconds) : 0;
const threadId =
this.props.relation?.rel_type === THREAD_RELATION_TYPE.name ? this.props.relation.event_id : null;
@ -599,68 +595,66 @@ export class MessageComposer extends React.Component<IProps, IState> {
});
return (
<div
className={classes}
ref={this.ref}
aria-describedby={this.state.recordingTimeLeftSeconds ? this.tooltipId : undefined}
role="region"
aria-label={_t("a11y|message_composer")}
>
{recordingTooltip}
<div className="mx_MessageComposer_wrapper">
<ReplyPreview
replyToEvent={this.props.replyToEvent}
permalinkCreator={this.props.permalinkCreator}
/>
<div className="mx_MessageComposer_row">
{e2eIcon}
{composer}
<div className="mx_MessageComposer_actions">
{controls}
{canSendMessages && (
<MessageComposerButtons
addEmoji={this.addEmoji}
haveRecording={this.state.haveRecording}
isMenuOpen={this.state.isMenuOpen}
isStickerPickerOpen={this.state.isStickerPickerOpen}
menuPosition={menuPosition}
relation={this.props.relation}
onRecordStartEndClick={this.onRecordStartEndClick}
setStickerPickerOpen={this.setStickerPickerOpen}
showLocationButton={
!window.electron && SettingsStore.getValue(UIFeature.LocationSharing)
}
showPollsButton={this.state.showPollsButton}
showStickersButton={this.showStickersButton}
isRichTextEnabled={this.state.isRichTextEnabled}
onComposerModeClick={this.onRichTextToggle}
toggleButtonMenu={this.toggleButtonMenu}
showVoiceBroadcastButton={this.state.showVoiceBroadcastButton}
onStartVoiceBroadcastClick={() => {
setUpVoiceBroadcastPreRecording(
this.props.room,
MatrixClientPeg.safeGet(),
SdkContextClass.instance.voiceBroadcastPlaybacksStore,
SdkContextClass.instance.voiceBroadcastRecordingsStore,
SdkContextClass.instance.voiceBroadcastPreRecordingStore,
);
this.toggleButtonMenu();
}}
/>
)}
{showSendButton && (
<SendButton
key="controls_send"
onClick={this.sendMessage}
title={
this.state.haveRecording ? _t("composer|send_button_voice_message") : undefined
}
/>
)}
<Tooltip open={isTooltipOpen} label={formatTimeLeft(secondsLeft)} placement="top">
<div className={classes} ref={this.ref} role="region" aria-label={_t("a11y|message_composer")}>
{recordingTooltip}
<div className="mx_MessageComposer_wrapper">
<ReplyPreview
replyToEvent={this.props.replyToEvent}
permalinkCreator={this.props.permalinkCreator}
/>
<div className="mx_MessageComposer_row">
{e2eIcon}
{composer}
<div className="mx_MessageComposer_actions">
{controls}
{canSendMessages && (
<MessageComposerButtons
addEmoji={this.addEmoji}
haveRecording={this.state.haveRecording}
isMenuOpen={this.state.isMenuOpen}
isStickerPickerOpen={this.state.isStickerPickerOpen}
menuPosition={menuPosition}
relation={this.props.relation}
onRecordStartEndClick={this.onRecordStartEndClick}
setStickerPickerOpen={this.setStickerPickerOpen}
showLocationButton={
!window.electron && SettingsStore.getValue(UIFeature.LocationSharing)
}
showPollsButton={this.state.showPollsButton}
showStickersButton={this.showStickersButton}
isRichTextEnabled={this.state.isRichTextEnabled}
onComposerModeClick={this.onRichTextToggle}
toggleButtonMenu={this.toggleButtonMenu}
showVoiceBroadcastButton={this.state.showVoiceBroadcastButton}
onStartVoiceBroadcastClick={() => {
setUpVoiceBroadcastPreRecording(
this.props.room,
MatrixClientPeg.safeGet(),
SdkContextClass.instance.voiceBroadcastPlaybacksStore,
SdkContextClass.instance.voiceBroadcastRecordingsStore,
SdkContextClass.instance.voiceBroadcastPreRecordingStore,
);
this.toggleButtonMenu();
}}
/>
)}
{showSendButton && (
<SendButton
key="controls_send"
onClick={this.sendMessage}
title={
this.state.haveRecording
? _t("composer|send_button_voice_message")
: undefined
}
/>
)}
</div>
</div>
</div>
</div>
</div>
</Tooltip>
);
}
}

View File

@ -16,18 +16,17 @@ limitations under the License.
import React, { PropsWithChildren } from "react";
import { User } from "matrix-js-sdk/src/matrix";
import { Tooltip } from "@vector-im/compound-web";
import ReadReceiptMarker, { IReadReceiptInfo } from "./ReadReceiptMarker";
import { IReadReceiptProps } from "./EventTile";
import AccessibleButton from "../elements/AccessibleButton";
import MemberAvatar from "../avatars/MemberAvatar";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import { Alignment } from "../elements/Tooltip";
import { formatDate } from "../../../DateUtils";
import { Action } from "../../../dispatcher/actions";
import dis from "../../../dispatcher/dispatcher";
import ContextMenu, { aboveLeftOf, MenuItem, useContextMenu } from "../../structures/ContextMenu";
import { useTooltip } from "../../../utils/useTooltip";
import { _t } from "../../../languageHandler";
import { useRovingTabIndex } from "../../../accessibility/RovingTabIndex";
import { formatList } from "../../../utils/FormattingUtils";
@ -87,18 +86,6 @@ export function ReadReceiptGroup({
const tooltipMembers: string[] = readReceipts.map((it) => it.roomMember?.name ?? it.userId);
const tooltipText = readReceiptTooltip(tooltipMembers, maxAvatars);
const [{ showTooltip, hideTooltip }, tooltip] = useTooltip({
label: (
<>
<div className="mx_Tooltip_title">
{_t("timeline|read_receipt_title", { count: readReceipts.length })}
</div>
<div className="mx_Tooltip_sub">{tooltipText}</div>
</>
),
alignment: Alignment.TopRight,
});
// return early if there are no read receipts
if (readReceipts.length === 0) {
// We currently must include `mx_ReadReceiptGroup_container` in
@ -185,34 +172,35 @@ export function ReadReceiptGroup({
return (
<div className="mx_EventTile_msgOption">
<div className="mx_ReadReceiptGroup" role="group" aria-label={_t("timeline|read_receipts_label")}>
<AccessibleButton
className="mx_ReadReceiptGroup_button"
ref={button}
aria-label={tooltipText}
aria-haspopup="true"
onClick={openMenu}
onMouseOver={showTooltip}
onMouseLeave={hideTooltip}
onFocus={showTooltip}
onBlur={hideTooltip}
>
{remText}
<span
className="mx_ReadReceiptGroup_container"
style={{
width:
Math.min(maxAvatars, readReceipts.length) * READ_AVATAR_OFFSET +
READ_AVATAR_SIZE -
READ_AVATAR_OFFSET,
}}
<Tooltip
label={_t("timeline|read_receipt_title", { count: readReceipts.length })}
caption={tooltipText}
placement="top-end"
>
<div className="mx_ReadReceiptGroup" role="group" aria-label={_t("timeline|read_receipts_label")}>
<AccessibleButton
className="mx_ReadReceiptGroup_button"
ref={button}
aria-label={tooltipText}
aria-haspopup="true"
onClick={openMenu}
>
{avatars}
</span>
</AccessibleButton>
{tooltip}
{contextMenu}
</div>
{remText}
<span
className="mx_ReadReceiptGroup_container"
style={{
width:
Math.min(maxAvatars, readReceipts.length) * READ_AVATAR_OFFSET +
READ_AVATAR_SIZE -
READ_AVATAR_OFFSET,
}}
>
{avatars}
</span>
</AccessibleButton>
{contextMenu}
</div>
</Tooltip>
</div>
);
}
@ -222,60 +210,48 @@ interface ReadReceiptPersonProps extends IReadReceiptProps {
onAfterClick?: () => void;
}
function ReadReceiptPerson({
// Export for testing
export function ReadReceiptPerson({
userId,
roomMember,
ts,
isTwelveHour,
onAfterClick,
}: ReadReceiptPersonProps): JSX.Element {
const [{ showTooltip, hideTooltip }, tooltip] = useTooltip({
alignment: Alignment.Top,
tooltipClassName: "mx_ReadReceiptGroup_person--tooltip",
label: (
<>
<div className="mx_Tooltip_title">{roomMember?.rawDisplayName ?? userId}</div>
<div className="mx_Tooltip_sub">{userId}</div>
</>
),
});
return (
<MenuItem
className="mx_ReadReceiptGroup_person"
onClick={() => {
dis.dispatch({
action: Action.ViewUser,
// XXX: We should be using a real member object and not assuming what the receiver wants.
// The ViewUser action leads to the RightPanelStore, and RightPanelStoreIPanelState defines the
// member property of IRightPanelCardState as `RoomMember | User`, so were fine for now, but we
// should definitely clean this up later
member: roomMember ?? ({ userId } as User),
push: false,
});
onAfterClick?.();
}}
onMouseOver={showTooltip}
onMouseLeave={hideTooltip}
onFocus={showTooltip}
onBlur={hideTooltip}
onWheel={hideTooltip}
>
<MemberAvatar
member={roomMember}
fallbackUserId={userId}
size="24px"
aria-hidden="true"
aria-live="off"
resizeMethod="crop"
hideTitle
/>
<div className="mx_ReadReceiptGroup_name">
<p>{roomMember?.name ?? userId}</p>
<p className="mx_ReadReceiptGroup_secondary">{formatDate(new Date(ts), isTwelveHour)}</p>
<Tooltip label={roomMember?.rawDisplayName ?? userId} caption={userId} placement="top">
<div>
<MenuItem
className="mx_ReadReceiptGroup_person"
onClick={() => {
dis.dispatch({
action: Action.ViewUser,
// XXX: We should be using a real member object and not assuming what the receiver wants.
// The ViewUser action leads to the RightPanelStore, and RightPanelStoreIPanelState defines the
// member property of IRightPanelCardState as `RoomMember | User`, so were fine for now, but we
// should definitely clean this up later
member: roomMember ?? ({ userId } as User),
push: false,
});
onAfterClick?.();
}}
>
<MemberAvatar
member={roomMember}
fallbackUserId={userId}
size="24px"
aria-hidden="true"
aria-live="off"
resizeMethod="crop"
hideTitle
/>
<div className="mx_ReadReceiptGroup_name">
<p>{roomMember?.name ?? userId}</p>
<p className="mx_ReadReceiptGroup_secondary">{formatDate(new Date(ts), isTwelveHour)}</p>
</div>
</MenuItem>
</div>
{tooltip}
</MenuItem>
</Tooltip>
);
}

View File

@ -3483,7 +3483,7 @@
"add_reaction_prompt": "Add reaction",
"custom_reaction_fallback_label": "Custom reaction",
"label": "%(reactors)s reacted with %(content)s",
"tooltip": "<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>"
"tooltip_caption": "reacted with %(shortName)s"
},
"read_receipt_title": {
"one": "Seen by %(count)s person",

View File

@ -1,37 +0,0 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ComponentProps, useState } from "react";
import Tooltip from "../components/views/elements/Tooltip";
interface TooltipEvents {
showTooltip: () => void;
hideTooltip: () => void;
}
export function useTooltip(props: ComponentProps<typeof Tooltip>): [TooltipEvents, JSX.Element | null] {
const [isVisible, setIsVisible] = useState(false);
const showTooltip = (): void => setIsVisible(true);
const hideTooltip = (): void => setIsVisible(false);
// No need to fill up the DOM with hidden tooltip elements. Only add the
// tooltip when we're hovering over the item (performance)
const tooltip = <Tooltip {...props} visible={isVisible} />;
return [{ showTooltip, hideTooltip }, tooltip];
}

View File

@ -116,4 +116,18 @@ describe("ReactionsRowButton", () => {
expect(root.asFragment()).toMatchSnapshot();
});
it("renders without a room", () => {
mockClient.getRoom.mockImplementation(() => null);
const props = createProps({});
const root = render(
<MatrixClientContext.Provider value={mockClient}>
<ReactionsRowButton {...props} />
</MatrixClientContext.Provider>,
);
expect(root.asFragment()).toMatchSnapshot();
});
});

View File

@ -46,7 +46,6 @@ exports[`ReactionsRowButton renders reaction row button custom image reactions c
>
2
</span>
<div />
</div>
</DocumentFragment>
`;
@ -95,7 +94,27 @@ exports[`ReactionsRowButton renders reaction row button emojis correctly 2`] = `
>
2
</span>
<div />
</div>
</DocumentFragment>
`;
exports[`ReactionsRowButton renders without a room 1`] = `
<DocumentFragment>
<div
class="mx_AccessibleButton mx_ReactionsRowButton"
role="button"
tabindex="0"
>
<span
aria-hidden="true"
class="mx_ReactionsRowButton_content"
/>
<span
aria-hidden="true"
class="mx_ReactionsRowButton_count"
>
2
</span>
</div>
</DocumentFragment>
`;

View File

@ -14,8 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { determineAvatarPosition, readReceiptTooltip } from "../../../../src/components/views/rooms/ReadReceiptGroup";
import React, { ComponentProps } from "react";
import { render, screen, waitFor } from "@testing-library/react";
import { RoomMember } from "matrix-js-sdk/src/matrix";
import userEvent from "@testing-library/user-event";
import {
determineAvatarPosition,
ReadReceiptPerson,
readReceiptTooltip,
} from "../../../../src/components/views/rooms/ReadReceiptGroup";
import * as languageHandler from "../../../../src/languageHandler";
import { stubClient } from "../../../test-utils";
import dispatcher from "../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../src/dispatcher/actions";
describe("ReadReceiptGroup", () => {
describe("TooltipText", () => {
@ -79,4 +91,55 @@ describe("ReadReceiptGroup", () => {
expect(determineAvatarPosition(5, 4)).toEqual({ hidden: true, position: 0 });
});
});
describe("<ReadReceiptPerson />", () => {
stubClient();
const ROOM_ID = "roomId";
const USER_ID = "@alice:example.org";
const member = new RoomMember(ROOM_ID, USER_ID);
member.rawDisplayName = "Alice";
member.getMxcAvatarUrl = () => "http://placekitten.com/400/400";
const renderReadReceipt = (props?: Partial<ComponentProps<typeof ReadReceiptPerson>>) => {
const currentDate = new Date(2024, 4, 15).getTime();
return render(<ReadReceiptPerson userId={USER_ID} roomMember={member} ts={currentDate} {...props} />);
};
beforeEach(() => {
jest.spyOn(dispatcher, "dispatch");
});
it("should render", () => {
const { container } = renderReadReceipt();
expect(container).toMatchSnapshot();
});
it("should display a tooltip", async () => {
renderReadReceipt();
await userEvent.hover(screen.getByRole("menuitem"));
await waitFor(() => {
const tooltip = screen.getByRole("tooltip", { name: member.rawDisplayName });
expect(tooltip).toMatchSnapshot();
});
});
it("should send an event when clicked", async () => {
const onAfterClick = jest.fn();
renderReadReceipt({ onAfterClick });
screen.getByRole("menuitem").click();
expect(onAfterClick).toHaveBeenCalled();
expect(dispatcher.dispatch).toHaveBeenCalledWith(
expect.objectContaining({
action: Action.ViewUser,
member,
push: false,
}),
);
});
});
});

View File

@ -0,0 +1,94 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ReadReceiptGroup <ReadReceiptPerson /> should display a tooltip 1`] = `
<div
aria-describedby="floating-ui-5"
aria-labelledby="floating-ui-4"
class="_tooltip_svz44_17"
id="floating-ui-6"
role="tooltip"
style="position: absolute; left: 0px; top: 0px; transform: translate(0px, 0px);"
tabindex="-1"
>
<svg
aria-hidden="true"
class="_arrow_svz44_34"
height="10"
style="position: absolute; pointer-events: none; top: 100%;"
viewBox="0 0 10 10"
width="10"
>
<path
d="M0,0 H10 L5,6 Q5,6 5,6 Z"
stroke="none"
/>
<clippath
id="floating-ui-9"
>
<rect
height="10"
width="10"
x="0"
y="0"
/>
</clippath>
</svg>
<span
id="floating-ui-4"
>
Alice
</span>
<span
class="_caption_svz44_29 cpd-theme-dark"
id="floating-ui-5"
>
@alice:example.org
</span>
</div>
`;
exports[`ReadReceiptGroup <ReadReceiptPerson /> should render 1`] = `
<div>
<div>
<div
class="mx_AccessibleButton mx_ReadReceiptGroup_person"
role="menuitem"
tabindex="-1"
>
<span
aria-hidden="true"
aria-label="Profile picture"
aria-live="off"
class="_avatar_mcap2_17 mx_BaseAvatar"
data-color="3"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 24px;"
>
<img
alt=""
class="_image_mcap2_50"
data-type="round"
height="24px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url//placekitten.com/400/400"
width="24px"
/>
</span>
<div
class="mx_ReadReceiptGroup_name"
>
<p>
@alice:example.org
</p>
<p
class="mx_ReadReceiptGroup_secondary"
>
Wed, 15 May, 0:00
</p>
</div>
</div>
</div>
</div>
`;