mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-16 13:14:58 +08:00
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:
commit
88e8e2df03
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 we’re 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 we’re 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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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];
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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>
|
||||
`;
|
||||
|
@ -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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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>
|
||||
`;
|
Loading…
Reference in New Issue
Block a user