diff --git a/apps/meteor/client/components/message/MessageContentBody.tsx b/apps/meteor/client/components/message/MessageContentBody.tsx index b3c612fbe7251..ada90d48e0a37 100644 --- a/apps/meteor/client/components/message/MessageContentBody.tsx +++ b/apps/meteor/client/components/message/MessageContentBody.tsx @@ -1,5 +1,6 @@ import { MessageBody, Skeleton } from '@rocket.chat/fuselage'; import { Markup } from '@rocket.chat/gazzodown'; +import type { ComponentProps } from 'react'; import React, { Suspense } from 'react'; import type { MessageWithMdEnforced } from '../../lib/parseMessageTextToAstMarkdown'; @@ -7,10 +8,10 @@ import GazzodownText from '../GazzodownText'; type MessageContentBodyProps = Pick & { searchText?: string; -}; +} & ComponentProps; -const MessageContentBody = ({ mentions, channels, md, searchText }: MessageContentBodyProps) => ( - +const MessageContentBody = ({ mentions, channels, md, searchText, ...props }: MessageContentBodyProps) => ( + }> diff --git a/apps/meteor/client/components/message/MessageHeader.tsx b/apps/meteor/client/components/message/MessageHeader.tsx index a6ad5feaff966..487c390a973f3 100644 --- a/apps/meteor/client/components/message/MessageHeader.tsx +++ b/apps/meteor/client/components/message/MessageHeader.tsx @@ -47,6 +47,7 @@ const MessageHeader = ({ message }: MessageHeaderProps): ReactElement => { openUserCard(e, message.u.username)} onKeyDown={(e: KeyboardEvent) => { @@ -72,7 +73,9 @@ const MessageHeader = ({ message }: MessageHeaderProps): ReactElement => { )} {shouldShowRolesList && } - {formatTime(message.ts)} + + {formatTime(message.ts)} + {message.private && {t('Only_you_can_see_this_message')}} diff --git a/apps/meteor/client/components/message/content/DiscussionMetrics.tsx b/apps/meteor/client/components/message/content/DiscussionMetrics.tsx index dc84828e607a9..a14c75761de5a 100644 --- a/apps/meteor/client/components/message/content/DiscussionMetrics.tsx +++ b/apps/meteor/client/components/message/content/DiscussionMetrics.tsx @@ -1,4 +1,11 @@ -import { MessageBlock, MessageMetrics, MessageMetricsItem, MessageMetricsReply } from '@rocket.chat/fuselage'; +import { + MessageBlock, + MessageMetrics, + MessageMetricsItem, + MessageMetricsItemIcon, + MessageMetricsItemLabel, + MessageMetricsReply, +} from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; @@ -26,8 +33,8 @@ const DiscussionMetrics = ({ lm, count, rid, drid }: DiscussionMetricsProps): Re {count ? t('message_counter', { count }) : t('Reply')} - - {lm ? format(lm) : t('No_messages_yet')} + + {lm ? format(lm) : t('No_messages_yet')} diff --git a/apps/meteor/client/components/message/content/Reactions.tsx b/apps/meteor/client/components/message/content/Reactions.tsx index 41daf046c5448..4126991fff88e 100644 --- a/apps/meteor/client/components/message/content/Reactions.tsx +++ b/apps/meteor/client/components/message/content/Reactions.tsx @@ -1,7 +1,9 @@ +import { useToolbar } from '@react-aria/toolbar'; import type { IMessage } from '@rocket.chat/core-typings'; import { MessageReactions, MessageReactionAction } from '@rocket.chat/fuselage'; -import type { ReactElement } from 'react'; -import React, { useContext } from 'react'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { HTMLAttributes, KeyboardEvent, ReactElement } from 'react'; +import React, { useContext, useRef } from 'react'; import { MessageListContext, useOpenEmojiPicker, useUserHasReacted } from '../list/MessageListContext'; import Reaction from './reactions/Reaction'; @@ -9,29 +11,42 @@ import { useToggleReactionMutation } from './reactions/useToggleReactionMutation type ReactionsProps = { message: IMessage; -}; +} & HTMLAttributes; -const Reactions = ({ message }: ReactionsProps): ReactElement => { +const Reactions = ({ message, ...props }: ReactionsProps): ReactElement => { + const t = useTranslation(); + const ref = useRef(null); const hasReacted = useUserHasReacted(message); const openEmojiPicker = useOpenEmojiPicker(message); const { username } = useContext(MessageListContext); const toggleReactionMutation = useToggleReactionMutation(); + const { toolbarProps } = useToolbar(props, ref); return ( - {message.reactions && - Object.entries(message.reactions).map(([name, reactions]) => ( - user !== username).map((username) => `@${username}`)} - messageId={message._id} - onClick={() => toggleReactionMutation.mutate({ mid: message._id, reaction: name })} - /> - ))} - + {message.reactions && ( +
+ {Object.entries(message.reactions).map(([name, reactions]) => ( + user !== username).map((username) => `@${username}`)} + messageId={message._id} + onKeyDown={(e: KeyboardEvent) => + (e.code === 'Space' || e.code === 'Enter') && toggleReactionMutation.mutate({ mid: message._id, reaction: name }) + } + onClick={() => toggleReactionMutation.mutate({ mid: message._id, reaction: name })} + /> + ))} +
+ )} + (e.code === 'Space' || e.code === 'Enter') && openEmojiPicker(e)} + onClick={openEmojiPicker} + />
); }; diff --git a/apps/meteor/client/components/message/content/ThreadMetrics.tsx b/apps/meteor/client/components/message/content/ThreadMetrics.tsx index bc9b6ab5e56fc..f3b6915a95dde 100644 --- a/apps/meteor/client/components/message/content/ThreadMetrics.tsx +++ b/apps/meteor/client/components/message/content/ThreadMetrics.tsx @@ -61,13 +61,12 @@ const ThreadMetrics = ({ unread, mention, all, rid, mid, counter, participants, {format(lm)} - - + + {(mention || all || unread) && ( diff --git a/apps/meteor/client/components/message/content/reactions/Reaction.tsx b/apps/meteor/client/components/message/content/reactions/Reaction.tsx index 893f6456c8c60..b2dea5eecfe9a 100644 --- a/apps/meteor/client/components/message/content/reactions/Reaction.tsx +++ b/apps/meteor/client/components/message/content/reactions/Reaction.tsx @@ -1,6 +1,6 @@ import { MessageReaction as MessageReactionTemplate, MessageReactionEmoji, MessageReactionCounter } from '@rocket.chat/fuselage'; -import { useTooltipClose, useTooltipOpen } from '@rocket.chat/ui-contexts'; -import type { ReactElement } from 'react'; +import { useTooltipClose, useTooltipOpen, useTranslation } from '@rocket.chat/ui-contexts'; +import type { ComponentProps, ReactElement } from 'react'; import React, { useRef, useContext } from 'react'; import { getEmojiClassNameAndDataTitle } from '../../../../lib/utils/renderEmoji'; @@ -14,10 +14,10 @@ type ReactionProps = { name: string; names: string[]; messageId: string; - onClick: () => void; -}; +} & ComponentProps; const Reaction = ({ hasReacted, counter, name, names, messageId, ...props }: ReactionProps): ReactElement => { + const t = useTranslation(); const ref = useRef(null); const openTooltip = useTooltipOpen(); const closeTooltip = useTooltipClose(); @@ -32,8 +32,7 @@ const Reaction = ({ hasReacted, counter, name, names, messageId, ...props }: Rea ref={ref} key={name} mine={mine} - tabIndex={0} - role='button' + aria-label={t('React_with__reaction__', { reaction: name })} // if data-tooltip is not set, the tooltip will close on first mouse enter data-tooltip='' onMouseEnter={async (e) => { diff --git a/apps/meteor/client/components/message/list/MessageListContext.tsx b/apps/meteor/client/components/message/list/MessageListContext.tsx index 542cca6bfc0a2..d525e2a4a39ef 100644 --- a/apps/meteor/client/components/message/list/MessageListContext.tsx +++ b/apps/meteor/client/components/message/list/MessageListContext.tsx @@ -7,7 +7,7 @@ export type MessageListContextValue = { useShowFollowing: ({ message }: { message: IMessage }) => boolean; useMessageDateFormatter: () => (date: Date) => string; useUserHasReacted: (message: IMessage) => (reaction: string) => boolean; - useOpenEmojiPicker: (message: IMessage) => (event: React.MouseEvent) => void; + useOpenEmojiPicker: (message: IMessage) => (event: React.MouseEvent | React.KeyboardEvent) => void; showRoles: boolean; showRealName: boolean; showUsername: boolean; diff --git a/apps/meteor/client/components/message/variants/RoomMessage.tsx b/apps/meteor/client/components/message/variants/RoomMessage.tsx index c17eb89c45962..dce673def68f1 100644 --- a/apps/meteor/client/components/message/variants/RoomMessage.tsx +++ b/apps/meteor/client/components/message/variants/RoomMessage.tsx @@ -2,7 +2,7 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { Message, MessageLeftContainer, MessageContainer, CheckBox } from '@rocket.chat/fuselage'; import { useToggle } from '@rocket.chat/fuselage-hooks'; import { MessageAvatar } from '@rocket.chat/ui-avatar'; -import { useUserId } from '@rocket.chat/ui-contexts'; +import { useTranslation, useUserId } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement } from 'react'; import React, { memo } from 'react'; @@ -47,6 +47,7 @@ const RoomMessage = ({ searchText, ...props }: RoomMessageProps): ReactElement => { + const t = useTranslation(); const uid = useUserId(); const editing = useIsMessageHighlight(message._id); const [displayIgnoredMessage, toggleDisplayIgnoredMessage] = useToggle(false); @@ -66,7 +67,9 @@ const RoomMessage = ({ ref={messageRef} id={message._id} role='listitem' + aria-roledescription={sequential ? t('sequential_message') : t('message')} tabIndex={0} + aria-labelledby={`${message._id}-displayName ${message._id}-time ${message._id}-content`} onClick={selecting ? toggleSelected : undefined} isSelected={selected} isEditing={editing} diff --git a/apps/meteor/client/components/message/variants/SystemMessage.tsx b/apps/meteor/client/components/message/variants/SystemMessage.tsx index d69b1ada10dc1..f9d5747a0ff97 100644 --- a/apps/meteor/client/components/message/variants/SystemMessage.tsx +++ b/apps/meteor/client/components/message/variants/SystemMessage.tsx @@ -60,6 +60,7 @@ const SystemMessage = ({ message, showUserAvatar, ...props }: SystemMessageProps return ( { + const t = useTranslation(); const uid = useUserId(); const editing = useIsMessageHighlight(message._id); const [ignored, toggleIgnoring] = useToggle((message as { ignored?: boolean }).ignored); @@ -38,6 +39,7 @@ const ThreadMessage = ({ message, sequential, unread, showUserAvatar }: ThreadMe return ( e.code === 'Enter' && handleThreadClick()} isSelected={isSelected} data-qa-selected={isSelected} - role='link' {...props} > {!sequential && ( diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index 7f4cf6694b7a9..29932e2762153 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -50,6 +50,7 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM <> {(!encrypted || normalizedMessage.e2e === 'done') && ( )} {isThreadMessage(message) && ( - +
  • + +
  • )} {system && } diff --git a/apps/meteor/client/views/room/body/RoomBody.tsx b/apps/meteor/client/views/room/body/RoomBody.tsx index ee677d4146213..c9e239fb723d2 100644 --- a/apps/meteor/client/views/room/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/body/RoomBody.tsx @@ -115,7 +115,7 @@ const RoomBody = (): ReactElement => { const { innerRef: restoreScrollPositionInnerRef } = useRestoreScrollPosition(room._id); - const { messageListRef, messageListProps } = useMessageListNavigation(); + const { messageListRef } = useMessageListNavigation(); const { handleNewMessageButtonClick, handleJumpToRecentButtonClick, handleComposerResize, hasNewMessages, newMessagesScrollRef } = useHasNewMessages(room._id, user?._id, atBottomRef, { @@ -283,7 +283,7 @@ const RoomBody = (): ReactElement => { > -
      +
        {canPreview ? ( <> {hasMorePreviousMessages ? ( diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/MessageBoxFormattingToolbar.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/MessageBoxFormattingToolbar.tsx index 79b210182d77f..7a4f7da208253 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/MessageBoxFormattingToolbar.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/MessageBoxFormattingToolbar.tsx @@ -29,6 +29,7 @@ const MessageBoxFormattingToolbar = ({ items, variant = 'large', composer, disab isPromptButton(featuredFormatter) ? featuredFormatter.prompt(composer) : composer.wrapSelection(featuredFormatter.pattern) } icon={featuredFormatter.icon} + title={t(featuredFormatter.label)} disabled={disabled} /> )} diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageItem.tsx b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageItem.tsx index 9165e267a7408..4e151bab4bec4 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageItem.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageItem.tsx @@ -39,6 +39,7 @@ export const ThreadMessageItem = ({ <> {showDivider && ( )} -
      • - {system ? ( - - ) : ( - - )} -
      • + {system ? ( + + ) : ( + + )} ); }; diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx index 71b71b74bde66..085ce657a6af5 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx @@ -1,7 +1,7 @@ import type { IMessage, IThreadMainMessage } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; import { useMergedRefs } from '@rocket.chat/fuselage-hooks'; -import { useSetting, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useSetting, useUserPreference, useTranslation } from '@rocket.chat/ui-contexts'; import { differenceInSeconds } from 'date-fns'; import type { ReactElement } from 'react'; import React, { Fragment } from 'react'; @@ -49,6 +49,7 @@ type ThreadMessageListProps = { }; const ThreadMessageList = ({ mainMessage }: ThreadMessageListProps): ReactElement => { + const t = useTranslation(); const { innerRef, bubbleRef, listStyle, ...bubbleDate } = useDateScroll(); const { messages, loading } = useLegacyThreadMessages(mainMessage._id); @@ -64,7 +65,7 @@ const ThreadMessageList = ({ mainMessage }: ThreadMessageListProps): ReactElemen const firstUnreadMessageId = useFirstUnreadMessageId(); const messageGroupingPeriod = Number(useSetting('Message_GroupingPeriod')); - const { messageListRef, messageListProps } = useMessageListNavigation(); + const { messageListRef } = useMessageListNavigation(); const listRef = useMergedRefs(listScrollRef, listJumpRef, messageListRef); const scrollRef = useMergedRefs(innerRef, listWrapperScrollRef); @@ -83,8 +84,8 @@ const ThreadMessageList = ({ mainMessage }: ThreadMessageListProps): ReactElemen is='ul' className={[listStyle, 'thread']} ref={listRef} + aria-label={t('Thread_message_list')} style={{ scrollBehavior: 'smooth', overflowX: 'hidden' }} - {...messageListProps} > {loading ? (
      • diff --git a/apps/meteor/client/views/room/hooks/useMessageListNavigation.ts b/apps/meteor/client/views/room/hooks/useMessageListNavigation.ts index 7a3e0b3c7316a..ac24862cb52ad 100644 --- a/apps/meteor/client/views/room/hooks/useMessageListNavigation.ts +++ b/apps/meteor/client/views/room/hooks/useMessageListNavigation.ts @@ -3,10 +3,6 @@ import type { RefCallback } from 'react'; import { useCallback } from 'react'; import { useFocusManager } from 'react-aria'; -type MessageListProps = { - 'aria-orientation': 'vertical' | 'horizontal'; -}; - const isListItem = (node: EventTarget) => (node as HTMLElement).getAttribute('role') === 'listitem' || (node as HTMLElement).getAttribute('role') === 'link'; const isMessageToolbarAction = (node: EventTarget) => (node as HTMLElement).parentElement?.getAttribute('role') === 'toolbar'; @@ -17,7 +13,7 @@ const isThreadMessage = (node: EventTarget) => (node as HTMLElement).classList.c * Custom hook to provide the room navigation by keyboard. * @param ref - A ref to the message list DOM element. */ -export const useMessageListNavigation = (): { messageListRef: RefCallback; messageListProps: MessageListProps } => { +export const useMessageListNavigation = (): { messageListRef: RefCallback } => { const roomFocusManager = useFocusManager(); const messageListRef = useCallback( @@ -94,9 +90,8 @@ export const useMessageListNavigation = (): { messageListRef: RefCallback { if (initialFocus) { - lastMessageFocused = node?.querySelector('li:last-child > [role=link]:first-child'); - lastMessageFocused?.focus(); - lastMessageFocused = null; + massageListFocusManager.focusLast({ accept: (node) => isListItem(node) }); + lastMessageFocused = document.activeElement as HTMLElement; initialFocus = false; } @@ -118,8 +113,5 @@ export const useMessageListNavigation = (): { messageListRef: RefCallback