Show hover elements when keyboard focus is within an event tile (#31078)
* Show timestamps when keyboard focus is within an event tile Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Ensure toolbar navigation pattern works in MessageActionBar This requires all buttons within to be roving by using the ref callback given by useRovingTabIndex Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Use PureComponent in EventTile to avoid mass re-rendering due to transitive onFocus/onBlur calls Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Remove unused timestamp event tile prop Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Use MessageTimestamp to generate the wrapping anchor so that focusing it brings up the tooltip Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Tweak MessageTimestamp Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Switch back to Component as we specify a shouldComponentUpdate already Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update jest tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update playwright timestamp masks Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Revert snapshot Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update snapshot Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix IRC layout Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Use PureComponent in EventTile to avoid mass re-rendering due to transitive onFocus/onBlur calls Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Remove unused timestamp event tile prop Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Use MessageTimestamp to generate the wrapping anchor so that focusing it brings up the tooltip Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Tweak MessageTimestamp Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Switch back to Component as we specify a shouldComponentUpdate already Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update jest tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update playwright timestamp masks Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Revert snapshot Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update snapshot Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix IRC layout Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Lint styles Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix layout picker Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update snapshots Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update screenshots Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix pcss comment Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Playwright Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Playwright Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
committed by
GitHub
parent
7e04998a58
commit
f6731ec318
@@ -19,7 +19,6 @@ import MessageContextMenu from "../context_menus/MessageContextMenu";
|
||||
import { aboveLeftOf } from "../../structures/ContextMenu";
|
||||
import MessageTimestamp from "../messages/MessageTimestamp";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { formatFullDate } from "../../../DateUtils";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { type RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
@@ -457,18 +456,15 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||
const senderName = mxEvent.sender?.name ?? mxEvent.getSender();
|
||||
const sender = <div className="mx_ImageView_info_sender">{senderName}</div>;
|
||||
const messageTimestamp = (
|
||||
<a
|
||||
<MessageTimestamp
|
||||
href={permalink}
|
||||
onClick={this.onPermalinkClicked}
|
||||
aria-label={formatFullDate(new Date(mxEvent.getTs()), showTwelveHour, false)}
|
||||
>
|
||||
<MessageTimestamp
|
||||
showFullDate={true}
|
||||
showTwelveHour={showTwelveHour}
|
||||
ts={mxEvent.getTs()}
|
||||
showSeconds={false}
|
||||
/>
|
||||
</a>
|
||||
showFullDate={true}
|
||||
showTwelveHour={showTwelveHour}
|
||||
ts={mxEvent.getTs()}
|
||||
showSeconds={false}
|
||||
inhibitTooltip
|
||||
/>
|
||||
);
|
||||
const avatar = (
|
||||
<MemberAvatar
|
||||
|
||||
@@ -24,6 +24,24 @@ interface IProps {
|
||||
showFullDate?: boolean;
|
||||
showSeconds?: boolean;
|
||||
showRelative?: boolean;
|
||||
|
||||
/**
|
||||
* If set to true then no tooltip will be shown
|
||||
*/
|
||||
inhibitTooltip?: boolean;
|
||||
|
||||
/**
|
||||
* If specified, will be rendered as an anchor bearing the href, a `span` element will be used otherwise
|
||||
*/
|
||||
href?: string;
|
||||
/**
|
||||
* Optional onClick handler to attach to the DOM element
|
||||
*/
|
||||
onClick?: React.MouseEventHandler<HTMLElement>;
|
||||
/**
|
||||
* Optional onContextMenu handler to attach to the DOM element
|
||||
*/
|
||||
onContextMenu?: React.MouseEventHandler<HTMLElement>;
|
||||
}
|
||||
|
||||
export default class MessageTimestamp extends React.Component<IProps> {
|
||||
@@ -52,12 +70,41 @@ export default class MessageTimestamp extends React.Component<IProps> {
|
||||
icon = <LateIcon className="mx_MessageTimestamp_lateIcon" width="16" height="16" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip description={label} caption={caption}>
|
||||
<span className="mx_MessageTimestamp" aria-hidden={true} aria-live="off">
|
||||
let content;
|
||||
if (this.props.href) {
|
||||
content = (
|
||||
<a
|
||||
href={this.props.href}
|
||||
onClick={this.props.onClick}
|
||||
onContextMenu={this.props.onContextMenu}
|
||||
className="mx_MessageTimestamp"
|
||||
aria-live="off"
|
||||
>
|
||||
{icon}
|
||||
{timestamp}
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<span
|
||||
onClick={this.props.onClick}
|
||||
onContextMenu={this.props.onContextMenu}
|
||||
className="mx_MessageTimestamp"
|
||||
aria-hidden={true}
|
||||
aria-live="off"
|
||||
tabIndex={this.props.onClick || !this.props.inhibitTooltip ? 0 : undefined}
|
||||
>
|
||||
{icon}
|
||||
{timestamp}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.inhibitTooltip) return content;
|
||||
|
||||
return (
|
||||
<Tooltip description={label} caption={caption}>
|
||||
{content}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ import ReplyChain from "../elements/ReplyChain";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { Layout } from "../../../settings/enums/Layout";
|
||||
import { formatTime } from "../../../DateUtils";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { DecryptionFailureBody } from "../messages/DecryptionFailureBody";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
@@ -253,6 +252,7 @@ interface IState {
|
||||
reactions?: Relations | null | undefined;
|
||||
|
||||
hover: boolean;
|
||||
focusWithin: boolean;
|
||||
|
||||
// Position of the context menu
|
||||
contextMenu?: {
|
||||
@@ -299,6 +299,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
declare public context: React.ContextType<typeof RoomContext>;
|
||||
|
||||
private unmounted = false;
|
||||
private readonly id = uniqueId();
|
||||
|
||||
public constructor(props: EventTileProps, context: React.ContextType<typeof RoomContext>) {
|
||||
super(props, context);
|
||||
@@ -316,6 +317,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
reactions: this.getReactions(),
|
||||
|
||||
hover: false,
|
||||
focusWithin: false,
|
||||
|
||||
thread,
|
||||
};
|
||||
@@ -922,7 +924,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
public render(): ReactNode {
|
||||
const msgtype = this.props.mxEvent.getContent().msgtype;
|
||||
const eventType = this.props.mxEvent.getType();
|
||||
const id = uniqueId();
|
||||
|
||||
const {
|
||||
hasRenderer,
|
||||
@@ -1122,6 +1123,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
(this.props.alwaysShowTimestamps ||
|
||||
this.props.last ||
|
||||
this.state.hover ||
|
||||
this.state.focusWithin ||
|
||||
this.state.actionBarFocused ||
|
||||
Boolean(this.state.contextMenu));
|
||||
|
||||
@@ -1135,20 +1137,32 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
ts = this.props.mxEvent.getTs();
|
||||
}
|
||||
|
||||
const messageTimestamp = (
|
||||
const messageTimestampProps = {
|
||||
showRelative: this.context.timelineRenderingType === TimelineRenderingType.ThreadsList,
|
||||
showTwelveHour: this.props.isTwelveHour,
|
||||
ts,
|
||||
receivedTs: getLateEventInfo(this.props.mxEvent)?.received_ts,
|
||||
};
|
||||
const messageTimestamp = <MessageTimestamp {...messageTimestampProps} />;
|
||||
const linkedMessageTimestamp = (
|
||||
<MessageTimestamp
|
||||
showRelative={this.context.timelineRenderingType === TimelineRenderingType.ThreadsList}
|
||||
showTwelveHour={this.props.isTwelveHour}
|
||||
ts={ts}
|
||||
receivedTs={getLateEventInfo(this.props.mxEvent)?.received_ts}
|
||||
{...messageTimestampProps}
|
||||
href={permalink}
|
||||
onClick={this.onPermalinkClicked}
|
||||
onContextMenu={this.onTimestampContextMenu}
|
||||
/>
|
||||
);
|
||||
|
||||
const timestamp = showTimestamp && ts ? messageTimestamp : null;
|
||||
const useIRCLayout = this.props.layout === Layout.IRC;
|
||||
// Used to simplify the UI layout where necessary by not conditionally rendering an element at the start
|
||||
const dummyTimestamp = useIRCLayout ? <span className="mx_MessageTimestamp" /> : null;
|
||||
const timestamp = showTimestamp && ts ? messageTimestamp : dummyTimestamp;
|
||||
const linkedTimestamp =
|
||||
timestamp !== dummyTimestamp && !this.props.hideTimestamp ? linkedMessageTimestamp : dummyTimestamp;
|
||||
|
||||
let pinnedMessageBadge: JSX.Element | undefined;
|
||||
if (PinningUtils.isPinned(MatrixClientPeg.safeGet(), this.props.mxEvent)) {
|
||||
pinnedMessageBadge = <PinnedMessageBadge aria-describedby={id} tabIndex={0} />;
|
||||
pinnedMessageBadge = <PinnedMessageBadge aria-describedby={this.id} tabIndex={0} />;
|
||||
}
|
||||
|
||||
let reactionsRow: JSX.Element | undefined;
|
||||
@@ -1165,21 +1179,8 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
// If we have reactions or a pinned message badge, we need a footer
|
||||
const hasFooter = Boolean((reactionsRow && this.state.reactions) || pinnedMessageBadge);
|
||||
|
||||
const linkedTimestamp = !this.props.hideTimestamp ? (
|
||||
<a
|
||||
href={permalink}
|
||||
onClick={this.onPermalinkClicked}
|
||||
aria-label={formatTime(new Date(this.props.mxEvent.getTs()), this.props.isTwelveHour)}
|
||||
onContextMenu={this.onTimestampContextMenu}
|
||||
>
|
||||
{timestamp}
|
||||
</a>
|
||||
) : null;
|
||||
|
||||
const useIRCLayout = this.props.layout === Layout.IRC;
|
||||
const groupTimestamp = !useIRCLayout ? linkedTimestamp : null;
|
||||
const ircTimestamp = useIRCLayout ? linkedTimestamp : null;
|
||||
const bubbleTimestamp = this.props.layout === Layout.Bubble ? messageTimestamp : undefined;
|
||||
const groupPadlock = !useIRCLayout && !isBubbleMessage && this.renderE2EPadlock();
|
||||
const ircPadlock = useIRCLayout && !isBubbleMessage && this.renderE2EPadlock();
|
||||
|
||||
@@ -1210,7 +1211,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
forExport={this.props.forExport}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
layout={this.props.layout}
|
||||
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.hover}
|
||||
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.hover || this.state.focusWithin}
|
||||
isQuoteExpanded={isQuoteExpanded}
|
||||
setQuoteExpanded={this.setQuoteExpanded}
|
||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||
@@ -1237,13 +1238,20 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
"data-event-id": this.props.mxEvent.getId(),
|
||||
"onMouseEnter": () => this.setState({ hover: true }),
|
||||
"onMouseLeave": () => this.setState({ hover: false }),
|
||||
"onFocus": () => this.setState({ focusWithin: true }),
|
||||
"onBlur": () => this.setState({ focusWithin: false }),
|
||||
},
|
||||
[
|
||||
<div className="mx_EventTile_senderDetails" key="mx_EventTile_senderDetails">
|
||||
{avatar}
|
||||
{sender}
|
||||
</div>,
|
||||
<div id={id} className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
||||
<div
|
||||
id={this.id}
|
||||
className={lineClasses}
|
||||
key="mx_EventTile_line"
|
||||
onContextMenu={this.onContextMenu}
|
||||
>
|
||||
{this.renderContextMenu()}
|
||||
{replyChain}
|
||||
{renderTile(TimelineRenderingType.Thread, {
|
||||
@@ -1260,9 +1268,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
showHiddenEvents: this.context.showHiddenEvents,
|
||||
})}
|
||||
{actionBar}
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{timestamp}
|
||||
</a>
|
||||
{linkedTimestamp}
|
||||
{msgOption}
|
||||
</div>,
|
||||
hasFooter && (
|
||||
@@ -1294,6 +1300,8 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
"data-has-reply": !!replyChain,
|
||||
"onMouseEnter": () => this.setState({ hover: true }),
|
||||
"onMouseLeave": () => this.setState({ hover: false }),
|
||||
"onFocus": () => this.setState({ focusWithin: true }),
|
||||
"onBlur": () => this.setState({ focusWithin: false }),
|
||||
"onClick": (ev: MouseEvent) => {
|
||||
const target = ev.currentTarget as HTMLElement;
|
||||
let index = -1;
|
||||
@@ -1425,13 +1433,20 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
"data-has-reply": !!replyChain,
|
||||
"onMouseEnter": () => this.setState({ hover: true }),
|
||||
"onMouseLeave": () => this.setState({ hover: false }),
|
||||
"onFocus": () => this.setState({ focusWithin: true }),
|
||||
"onBlur": () => this.setState({ focusWithin: false }),
|
||||
},
|
||||
<>
|
||||
{ircTimestamp}
|
||||
{sender}
|
||||
{ircPadlock}
|
||||
{avatar}
|
||||
<div id={id} className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
||||
<div
|
||||
id={this.id}
|
||||
className={lineClasses}
|
||||
key="mx_EventTile_line"
|
||||
onContextMenu={this.onContextMenu}
|
||||
>
|
||||
{this.renderContextMenu()}
|
||||
{groupTimestamp}
|
||||
{groupPadlock}
|
||||
@@ -1442,7 +1457,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
// overrides
|
||||
ref: this.tile,
|
||||
isSeeingThroughMessageHiddenForModeration,
|
||||
timestamp: bubbleTimestamp,
|
||||
|
||||
// appease TS
|
||||
highlights: this.props.highlights,
|
||||
|
||||
@@ -64,7 +64,6 @@ export interface EventTileTypeProps
|
||||
| "inhibitInteraction"
|
||||
> {
|
||||
ref?: React.RefObject<any>; // `any` because it's effectively impossible to convince TS of a reasonable type
|
||||
timestamp?: JSX.Element;
|
||||
maxImageHeight?: number; // pixels
|
||||
overrideBodyTypes?: Record<string, React.ComponentType<IBodyProps>>;
|
||||
overrideEventTypes?: Record<string, React.ComponentType<IBodyProps>>;
|
||||
@@ -288,7 +287,6 @@ export function renderTile(
|
||||
callEventGrouper,
|
||||
getRelationsForEvent,
|
||||
isSeeingThroughMessageHiddenForModeration,
|
||||
timestamp,
|
||||
inhibitInteraction,
|
||||
showHiddenEvents,
|
||||
} = props;
|
||||
@@ -336,7 +334,6 @@ export function renderTile(
|
||||
callEventGrouper,
|
||||
getRelationsForEvent,
|
||||
isSeeingThroughMessageHiddenForModeration,
|
||||
timestamp,
|
||||
inhibitInteraction,
|
||||
showHiddenEvents,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user