Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Message keyboard navigability #31549

Merged
merged 18 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 15 additions & 16 deletions apps/meteor/client/components/message/MessageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
MessageNameContainer,
} from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import type { KeyboardEvent, ReactElement } from 'react';
import React, { memo } from 'react';

import { getUserDisplayName } from '../../../lib/getUserDisplayName';
Expand Down Expand Up @@ -45,31 +45,30 @@ const MessageHeader = ({ message }: MessageHeaderProps): ReactElement => {

return (
<FuselageMessageHeader>
<MessageNameContainer>
<MessageNameContainer
tabIndex={0}
role='button'
aria-label={getUserDisplayName(user.name, user.username, showRealName)}
{...(user.username !== undefined &&
chat?.userCard && {
onClick: (e) => chat?.userCard.openUserCard(e, message.u.username),
onKeyDown: (e: KeyboardEvent<HTMLSpanElement>) => {
(e.code === 'Enter' || e.code === 'Space') && chat?.userCard.openUserCard(e, message.u.username);
},
style: { cursor: 'pointer' },
})}
>
<MessageName
{...(!showUsername && { 'data-qa-type': 'username' })}
title={!showUsername && !usernameAndRealNameAreSame ? `@${user.username}` : undefined}
data-username={user.username}
{...(user.username !== undefined &&
chat?.userCard && {
onClick: (e) => chat?.userCard.openUserCard(e, message.u.username),
style: { cursor: 'pointer' },
})}
>
{message.alias || getUserDisplayName(user.name, user.username, showRealName)}
</MessageName>
{showUsername && (
<>
{' '}
<MessageUsername
data-username={user.username}
data-qa-type='username'
{...(user.username !== undefined &&
chat?.userCard && {
onClick: (e) => chat?.userCard.openUserCard(e, message.u.username),
style: { cursor: 'pointer' },
})}
>
<MessageUsername data-username={user.username} data-qa-type='username'>
@{user.username}
</MessageUsername>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useToolbar } from '@react-aria/toolbar';
import type { IMessage, IRoom, ISubscription, ITranslatedMessage } from '@rocket.chat/core-typings';
import { isThreadMessage, isRoomFederated, isVideoConfMessage } from '@rocket.chat/core-typings';
import { MessageToolbar as FuselageMessageToolbar, MessageToolbarItem } from '@rocket.chat/fuselage';
import { useFeaturePreview } from '@rocket.chat/ui-client';
import { useUser, useSettings, useTranslation, useMethod, useLayoutHiddenActions } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import type { ReactElement } from 'react';
import React, { memo, useMemo } from 'react';
import type { ComponentProps, ReactElement } from 'react';
import React, { memo, useMemo, useRef } from 'react';

import type { MessageActionContext } from '../../../../app/ui-utils/client/lib/MessageAction';
import { MessageAction } from '../../../../app/ui-utils/client/lib/MessageAction';
Expand Down Expand Up @@ -45,19 +46,23 @@ type MessageToolbarProps = {
room: IRoom;
subscription?: ISubscription;
onChangeMenuVisibility: (visible: boolean) => void;
};
} & ComponentProps<typeof FuselageMessageToolbar>;

const MessageToolbar = ({
message,
messageContext,
room,
subscription,
onChangeMenuVisibility,
...props
}: MessageToolbarProps): ReactElement | null => {
const t = useTranslation();
const user = useUser() ?? undefined;
const settings = useSettings();

const toolbarRef = useRef(null);
const { toolbarProps } = useToolbar(props, toolbarRef);

const quickReactionsEnabled = useFeaturePreview('quickReactions');

const setReaction = useMethod('setReaction');
Expand Down Expand Up @@ -106,7 +111,7 @@ const MessageToolbar = ({
};

return (
<FuselageMessageToolbar>
<FuselageMessageToolbar ref={toolbarRef} {...toolbarProps} {...props}>
{quickReactionsEnabled &&
isReactionAllowed &&
quickReactions.slice(0, 3).map(({ emoji, image }) => {
Expand Down
11 changes: 7 additions & 4 deletions apps/meteor/client/components/message/variants/RoomMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Message, MessageLeftContainer, MessageContainer, CheckBox } from '@rock
import { useToggle } from '@rocket.chat/fuselage-hooks';
import { MessageAvatar } from '@rocket.chat/ui-avatar';
import { useUserId } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import type { ComponentProps, ReactElement } from 'react';
import React, { useRef, memo } from 'react';

import type { MessageActionContext } from '../../../../app/ui-utils/client/lib/MessageAction';
Expand Down Expand Up @@ -33,7 +33,7 @@ type RoomMessageProps = {
context?: MessageActionContext;
ignoredUser?: boolean;
searchText?: string;
};
} & ComponentProps<typeof Message>;

const RoomMessage = ({
message,
Expand All @@ -45,6 +45,7 @@ const RoomMessage = ({
context,
ignoredUser,
searchText,
...props
}: RoomMessageProps): ReactElement => {
const uid = useUserId();
const editing = useIsMessageHighlight(message._id);
Expand All @@ -60,10 +61,13 @@ const RoomMessage = ({
useCountSelected();

useJumpToMessage(message._id, messageRef);

return (
<Message
ref={messageRef}
id={message._id}
role='listitem'
tabIndex={0}
onClick={selecting ? toggleSelected : undefined}
isSelected={selected}
isEditing={editing}
Expand All @@ -78,6 +82,7 @@ const RoomMessage = ({
data-own={message.u._id === uid}
data-qa-type='message'
aria-busy={message.temp}
{...props}
>
<MessageLeftContainer>
{!sequential && message.u.username && !selecting && showUserAvatar && (
Expand All @@ -95,10 +100,8 @@ const RoomMessage = ({
{selecting && <CheckBox checked={selected} onChange={toggleSelected} />}
{sequential && <StatusIndicators message={message} />}
</MessageLeftContainer>

<MessageContainer>
{!sequential && <MessageHeader message={message} />}

{ignored ? (
<IgnoredContent onShowMessageIgnored={toggleDisplayIgnoredMessage} />
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import { UserAvatar } from '@rocket.chat/ui-avatar';
import type { TranslationKey } from '@rocket.chat/ui-contexts';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import type { ComponentProps, ReactElement } from 'react';
import React, { memo } from 'react';

import { MessageTypes } from '../../../../app/ui-utils/client';
Expand All @@ -37,9 +37,9 @@ import { useMessageListShowRealName, useMessageListShowUsername } from '../list/
type SystemMessageProps = {
message: IMessage;
showUserAvatar: boolean;
};
} & ComponentProps<typeof MessageSystem>;

const SystemMessage = ({ message, showUserAvatar }: SystemMessageProps): ReactElement => {
const SystemMessage = ({ message, showUserAvatar, ...props }: SystemMessageProps): ReactElement => {
const t = useTranslation();
const formatTime = useFormatTime();
const formatDateAndTime = useFormatDateAndTime();
Expand All @@ -59,11 +59,14 @@ const SystemMessage = ({ message, showUserAvatar }: SystemMessageProps): ReactEl

return (
<MessageSystem
role='listitem'
tabIndex={0}
onClick={isSelecting ? toggleSelected : undefined}
isSelected={isSelected}
data-qa-selected={isSelected}
data-qa='system-message'
data-system-message-type={message.t}
{...props}
>
<MessageSystemLeftContainer>
{!isSelecting && showUserAvatar && <UserAvatar username={message.u.username} size='x18' />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ const ThreadMessage = ({ message, sequential, unread, showUserAvatar }: ThreadMe

return (
<Message
role='listitem'
tabIndex={0}
id={message._id}
ref={messageRef}
isEditing={editing}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from '@rocket.chat/fuselage';
import { MessageAvatar } from '@rocket.chat/ui-avatar';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import type { ComponentProps, ReactElement } from 'react';
import React, { memo } from 'react';

import { MessageTypes } from '../../../../app/ui-utils/client';
Expand All @@ -36,7 +36,7 @@ type ThreadMessagePreviewProps = {
message: IThreadMessage;
showUserAvatar: boolean;
sequential: boolean;
};
} & ComponentProps<typeof ThreadMessage>;

const ThreadMessagePreview = ({ message, showUserAvatar, sequential, ...props }: ThreadMessagePreviewProps): ReactElement => {
const parentMessage = useParentMessage(message.tmid);
Expand All @@ -56,23 +56,30 @@ const ThreadMessagePreview = ({ message, showUserAvatar, sequential, ...props }:

const goToThread = useGoToThread();

const handleThreadClick = () => {
if (!isSelecting) {
if (!sequential) {
return parentMessage.isSuccess && goToThread({ rid: message.rid, tmid: message.tmid, msg: parentMessage.data?._id });
}

return goToThread({ rid: message.rid, tmid: message.tmid, msg: message._id });
}

return toggleSelected();
};

return (
<ThreadMessage
{...props}
onClick={isSelecting ? toggleSelected : undefined}
tabIndex={0}
onClick={handleThreadClick}
onKeyDown={(e) => e.code === 'Enter' && handleThreadClick()}
isSelected={isSelected}
data-qa-selected={isSelected}
role='link'
{...props}
>
{!sequential && (
<ThreadMessageRow
role='link'
onClick={
!isSelecting && parentMessage.isSuccess
? () => goToThread({ rid: message.rid, tmid: message.tmid, msg: parentMessage.data?._id })
: undefined
}
>
<ThreadMessageRow>
<ThreadMessageLeftContainer>
<ThreadMessageIconThread />
</ThreadMessageLeftContainer>
Expand Down Expand Up @@ -100,7 +107,7 @@ const ThreadMessagePreview = ({ message, showUserAvatar, sequential, ...props }:
</ThreadMessageContainer>
</ThreadMessageRow>
)}
<ThreadMessageRow onClick={!isSelecting ? () => goToThread({ rid: message.rid, tmid: message.tmid, msg: message._id }) : undefined}>
<ThreadMessageRow>
<ThreadMessageLeftContainer>
{!isSelecting && showUserAvatar && (
<MessageAvatar
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ const EmojiElement = ({ emoji, image, onClick, small = false, ...props }: EmojiE

return (
<IconButton
{...props}
{...(small && { className: emojiSmallClass })}
small={small}
medium={!small}
Expand All @@ -40,6 +39,7 @@ const EmojiElement = ({ emoji, image, onClick, small = false, ...props }: EmojiE
data-emoji={emoji}
aria-label={emoji}
icon={emojiElement}
{...props}
/>
);
};
Expand Down
53 changes: 27 additions & 26 deletions apps/meteor/client/views/room/Room.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React, { createElement, lazy, memo, Suspense } from 'react';
import { FocusScope } from 'react-aria';
import { ErrorBoundary } from 'react-error-boundary';

import { ContextualbarSkeleton } from '../../components/Contextualbar';
Expand All @@ -17,38 +17,39 @@ import { SelectedMessagesProvider } from './providers/SelectedMessagesProvider';
const UiKitContextualBar = lazy(() => import('./contextualBar/uikit/UiKitContextualBar'));

const Room = (): ReactElement => {
const t = useTranslation();
const room = useRoom();
const toolbox = useRoomToolbox();
const contextualBarView = useAppsContextualBar();

return (
<ChatProvider>
<MessageHighlightProvider>
<RoomLayout
aria-label={t('Channel')}
data-qa-rc-room={room._id}
header={<Header room={room} />}
body={<RoomBody />}
aside={
(toolbox.tab?.tabComponent && (
<ErrorBoundary fallback={null}>
<SelectedMessagesProvider>
<Suspense fallback={<ContextualbarSkeleton />}>{createElement(toolbox.tab.tabComponent)}</Suspense>
</SelectedMessagesProvider>
</ErrorBoundary>
)) ||
(contextualBarView && (
<ErrorBoundary fallback={null}>
<SelectedMessagesProvider>
<Suspense fallback={<ContextualbarSkeleton />}>
<UiKitContextualBar key={contextualBarView.id} initialView={contextualBarView} />
</Suspense>
</SelectedMessagesProvider>
</ErrorBoundary>
))
}
/>
<FocusScope>
<RoomLayout
data-qa-rc-room={room._id}
aria-label={room.t === 'd' ? `Conversation with ${room.name}` : `Channel ${room.name}`}
header={<Header room={room} />}
dougfabris marked this conversation as resolved.
Show resolved Hide resolved
body={<RoomBody />}
aside={
(toolbox.tab?.tabComponent && (
<ErrorBoundary fallback={null}>
<SelectedMessagesProvider>
<Suspense fallback={<ContextualbarSkeleton />}>{createElement(toolbox.tab.tabComponent)}</Suspense>
</SelectedMessagesProvider>
</ErrorBoundary>
)) ||
(contextualBarView && (
<ErrorBoundary fallback={null}>
<SelectedMessagesProvider>
<Suspense fallback={<ContextualbarSkeleton />}>
<UiKitContextualBar key={contextualBarView.id} initialView={contextualBarView} />
</Suspense>
</SelectedMessagesProvider>
</ErrorBoundary>
))
}
/>
</FocusScope>
</MessageHighlightProvider>
</ChatProvider>
);
Expand Down
Loading
Loading