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
11 changed files with 351 additions and 279 deletions
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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();
|
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
|
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>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -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,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 a new issue