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">>; 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> { export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
private static container: HTMLElement; private static container: HTMLElement;
private parent: Element | null = null; private parent: Element | null = null;

View file

@ -44,20 +44,10 @@ export interface IProps {
customReactionImagesEnabled?: boolean; customReactionImagesEnabled?: boolean;
} }
interface IState { export default class ReactionsRowButton extends React.PureComponent<IProps> {
tooltipRendered: boolean;
tooltipVisible: boolean;
}
export default class ReactionsRowButton extends React.PureComponent<IProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>; public context!: React.ContextType<typeof MatrixClientContext>;
public state = {
tooltipRendered: false,
tooltipVisible: false,
};
public onClick = (): void => { public onClick = (): void => {
const { mxEvent, myReactionEvent, content } = this.props; const { mxEvent, myReactionEvent, content } = this.props;
if (myReactionEvent) { 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 { public render(): React.ReactNode {
const { mxEvent, content, count, reactionEvents, myReactionEvent } = this.props; 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, 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()); const room = this.context.getRoom(mxEvent.getRoomId());
let label: string | undefined; let label: string | undefined;
let customReactionName: string | undefined; let customReactionName: string | undefined;
@ -156,20 +118,24 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
} }
return ( return (
<ReactionsRowButtonTooltip
mxEvent={this.props.mxEvent}
content={content}
reactionEvents={reactionEvents}
customReactionImagesEnabled={this.props.customReactionImagesEnabled}
>
<AccessibleButton <AccessibleButton
className={classes} className={classes}
aria-label={label} aria-label={label}
onClick={this.onClick} onClick={this.onClick}
disabled={this.props.disabled} disabled={this.props.disabled}
onMouseOver={this.onMouseOver}
onMouseLeave={this.onMouseLeave}
> >
{reactionContent} {reactionContent}
<span className="mx_ReactionsRowButton_count" aria-hidden="true"> <span className="mx_ReactionsRowButton_count" aria-hidden="true">
{count} {count}
</span> </span>
{tooltip}
</AccessibleButton> </AccessibleButton>
</ReactionsRowButtonTooltip>
); );
} }
} }

View file

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

View file

@ -25,6 +25,7 @@ import {
THREAD_RELATION_TYPE, THREAD_RELATION_TYPE,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import { Optional } from "matrix-events-sdk"; import { Optional } from "matrix-events-sdk";
import { Tooltip } from "@vector-im/compound-web";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
@ -40,7 +41,6 @@ import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import VoiceRecordComposerTile from "./VoiceRecordComposerTile"; import VoiceRecordComposerTile from "./VoiceRecordComposerTile";
import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore"; import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore";
import { RecordingState } from "../../../audio/VoiceRecording"; import { RecordingState } from "../../../audio/VoiceRecording";
import Tooltip, { Alignment } from "../elements/Tooltip";
import ResizeNotifier from "../../../utils/ResizeNotifier"; import ResizeNotifier from "../../../utils/ResizeNotifier";
import { E2EStatus } from "../../../utils/ShieldUtils"; import { E2EStatus } from "../../../utils/ShieldUtils";
import SendMessageComposer, { SendMessageComposer as SendMessageComposerClass } from "./SendMessageComposer"; import SendMessageComposer, { SendMessageComposer as SendMessageComposerClass } from "./SendMessageComposer";
@ -110,7 +110,6 @@ interface IState {
} }
export class MessageComposer extends React.Component<IProps, IState> { export class MessageComposer extends React.Component<IProps, IState> {
private tooltipId = `mx_MessageComposer_${Math.random()}`;
private dispatcherRef?: string; private dispatcherRef?: string;
private messageComposerInput = createRef<SendMessageComposerClass>(); private messageComposerInput = createRef<SendMessageComposerClass>();
private voiceRecordingButton = createRef<VoiceRecordComposerTile>(); private voiceRecordingButton = createRef<VoiceRecordComposerTile>();
@ -568,12 +567,9 @@ export class MessageComposer extends React.Component<IProps, IState> {
} }
let recordingTooltip: JSX.Element | undefined; let recordingTooltip: JSX.Element | undefined;
if (this.state.recordingTimeLeftSeconds) {
const secondsLeft = Math.round(this.state.recordingTimeLeftSeconds); const isTooltipOpen = Boolean(this.state.recordingTimeLeftSeconds);
recordingTooltip = ( const secondsLeft = this.state.recordingTimeLeftSeconds ? Math.round(this.state.recordingTimeLeftSeconds) : 0;
<Tooltip id={this.tooltipId} label={formatTimeLeft(secondsLeft)} alignment={Alignment.Top} />
);
}
const threadId = const threadId =
this.props.relation?.rel_type === THREAD_RELATION_TYPE.name ? this.props.relation.event_id : null; this.props.relation?.rel_type === THREAD_RELATION_TYPE.name ? this.props.relation.event_id : null;
@ -599,13 +595,8 @@ export class MessageComposer extends React.Component<IProps, IState> {
}); });
return ( return (
<div <Tooltip open={isTooltipOpen} label={formatTimeLeft(secondsLeft)} placement="top">
className={classes} <div className={classes} ref={this.ref} role="region" aria-label={_t("a11y|message_composer")}>
ref={this.ref}
aria-describedby={this.state.recordingTimeLeftSeconds ? this.tooltipId : undefined}
role="region"
aria-label={_t("a11y|message_composer")}
>
{recordingTooltip} {recordingTooltip}
<div className="mx_MessageComposer_wrapper"> <div className="mx_MessageComposer_wrapper">
<ReplyPreview <ReplyPreview
@ -653,7 +644,9 @@ export class MessageComposer extends React.Component<IProps, IState> {
key="controls_send" key="controls_send"
onClick={this.sendMessage} onClick={this.sendMessage}
title={ title={
this.state.haveRecording ? _t("composer|send_button_voice_message") : undefined this.state.haveRecording
? _t("composer|send_button_voice_message")
: undefined
} }
/> />
)} )}
@ -661,6 +654,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
</div> </div>
</div> </div>
</div> </div>
</Tooltip>
); );
} }
} }

View file

@ -16,18 +16,17 @@ limitations under the License.
import React, { PropsWithChildren } from "react"; import React, { PropsWithChildren } from "react";
import { User } from "matrix-js-sdk/src/matrix"; import { User } from "matrix-js-sdk/src/matrix";
import { Tooltip } from "@vector-im/compound-web";
import ReadReceiptMarker, { IReadReceiptInfo } from "./ReadReceiptMarker"; import ReadReceiptMarker, { IReadReceiptInfo } from "./ReadReceiptMarker";
import { IReadReceiptProps } from "./EventTile"; import { IReadReceiptProps } from "./EventTile";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import MemberAvatar from "../avatars/MemberAvatar"; import MemberAvatar from "../avatars/MemberAvatar";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import { Alignment } from "../elements/Tooltip";
import { formatDate } from "../../../DateUtils"; import { formatDate } from "../../../DateUtils";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import ContextMenu, { aboveLeftOf, MenuItem, useContextMenu } from "../../structures/ContextMenu"; import ContextMenu, { aboveLeftOf, MenuItem, useContextMenu } from "../../structures/ContextMenu";
import { useTooltip } from "../../../utils/useTooltip";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { useRovingTabIndex } from "../../../accessibility/RovingTabIndex"; import { useRovingTabIndex } from "../../../accessibility/RovingTabIndex";
import { formatList } from "../../../utils/FormattingUtils"; import { formatList } from "../../../utils/FormattingUtils";
@ -87,18 +86,6 @@ export function ReadReceiptGroup({
const tooltipMembers: string[] = readReceipts.map((it) => it.roomMember?.name ?? it.userId); const tooltipMembers: string[] = readReceipts.map((it) => it.roomMember?.name ?? it.userId);
const tooltipText = readReceiptTooltip(tooltipMembers, maxAvatars); 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 // return early if there are no read receipts
if (readReceipts.length === 0) { if (readReceipts.length === 0) {
// We currently must include `mx_ReadReceiptGroup_container` in // We currently must include `mx_ReadReceiptGroup_container` in
@ -185,6 +172,11 @@ export function ReadReceiptGroup({
return ( return (
<div className="mx_EventTile_msgOption"> <div className="mx_EventTile_msgOption">
<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")}> <div className="mx_ReadReceiptGroup" role="group" aria-label={_t("timeline|read_receipts_label")}>
<AccessibleButton <AccessibleButton
className="mx_ReadReceiptGroup_button" className="mx_ReadReceiptGroup_button"
@ -192,10 +184,6 @@ export function ReadReceiptGroup({
aria-label={tooltipText} aria-label={tooltipText}
aria-haspopup="true" aria-haspopup="true"
onClick={openMenu} onClick={openMenu}
onMouseOver={showTooltip}
onMouseLeave={hideTooltip}
onFocus={showTooltip}
onBlur={hideTooltip}
> >
{remText} {remText}
<span <span
@ -210,9 +198,9 @@ export function ReadReceiptGroup({
{avatars} {avatars}
</span> </span>
</AccessibleButton> </AccessibleButton>
{tooltip}
{contextMenu} {contextMenu}
</div> </div>
</Tooltip>
</div> </div>
); );
} }
@ -222,25 +210,17 @@ interface ReadReceiptPersonProps extends IReadReceiptProps {
onAfterClick?: () => void; onAfterClick?: () => void;
} }
function ReadReceiptPerson({ // Export for testing
export function ReadReceiptPerson({
userId, userId,
roomMember, roomMember,
ts, ts,
isTwelveHour, isTwelveHour,
onAfterClick, onAfterClick,
}: ReadReceiptPersonProps): JSX.Element { }: 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 ( return (
<Tooltip label={roomMember?.rawDisplayName ?? userId} caption={userId} placement="top">
<div>
<MenuItem <MenuItem
className="mx_ReadReceiptGroup_person" className="mx_ReadReceiptGroup_person"
onClick={() => { onClick={() => {
@ -255,11 +235,6 @@ function ReadReceiptPerson({
}); });
onAfterClick?.(); onAfterClick?.();
}} }}
onMouseOver={showTooltip}
onMouseLeave={hideTooltip}
onFocus={showTooltip}
onBlur={hideTooltip}
onWheel={hideTooltip}
> >
<MemberAvatar <MemberAvatar
member={roomMember} member={roomMember}
@ -274,8 +249,9 @@ function ReadReceiptPerson({
<p>{roomMember?.name ?? userId}</p> <p>{roomMember?.name ?? userId}</p>
<p className="mx_ReadReceiptGroup_secondary">{formatDate(new Date(ts), isTwelveHour)}</p> <p className="mx_ReadReceiptGroup_secondary">{formatDate(new Date(ts), isTwelveHour)}</p>
</div> </div>
{tooltip}
</MenuItem> </MenuItem>
</div>
</Tooltip>
); );
} }

View file

@ -3483,7 +3483,7 @@
"add_reaction_prompt": "Add reaction", "add_reaction_prompt": "Add reaction",
"custom_reaction_fallback_label": "Custom reaction", "custom_reaction_fallback_label": "Custom reaction",
"label": "%(reactors)s reacted with %(content)s", "label": "%(reactors)s reacted with %(content)s",
"tooltip": "<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>" "tooltip_caption": "reacted with %(shortName)s"
}, },
"read_receipt_title": { "read_receipt_title": {
"one": "Seen by %(count)s person", "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(); 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 2
</span> </span>
<div />
</div> </div>
</DocumentFragment> </DocumentFragment>
`; `;
@ -95,7 +94,27 @@ exports[`ReactionsRowButton renders reaction row button emojis correctly 2`] = `
> >
2 2
</span> </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> </div>
</DocumentFragment> </DocumentFragment>
`; `;

View file

@ -14,8 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License. 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 * 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("ReadReceiptGroup", () => {
describe("TooltipText", () => { describe("TooltipText", () => {
@ -79,4 +91,55 @@ describe("ReadReceiptGroup", () => {
expect(determineAvatarPosition(5, 4)).toEqual({ hidden: true, position: 0 }); 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>
`;