Skip to content

Commit

Permalink
Fix keyboard navigation for message menu button (#5576)
Browse files Browse the repository at this point in the history
  • Loading branch information
vhuseinova-msft authored Jan 21, 2025
1 parent c2198c9 commit 765d226
Show file tree
Hide file tree
Showing 25 changed files with 368 additions and 132 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"type": "patch",
"area": "fix",
"workstream": "A11y",
"comment": "Fix keyboard navigation for message menu button",
"packageName": "@azure/communication-react",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"type": "patch",
"area": "fix",
"workstream": "A11y",
"comment": "Fix keyboard navigation for message menu button",
"packageName": "@azure/communication-react",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import { IPersona, PersonaSize, mergeStyles, Persona } from '@fluentui/react';
import { mergeClasses } from '@fluentui/react-components';
import { createStyleFromV8Style } from '../../styles/v8StyleShim';
import { ChatMessage as FluentChatMessage } from '@fluentui-contrib/react-chat';
import { getFluentUIAttachedValue } from '../../utils/ChatMessageComponentUtils';
import {
getFluentUIAttachedValue,
removeFluentUIKeyboardNavigationStyles
} from '../../utils/ChatMessageComponentUtils';
import { ChatMessageComponentWrapperProps } from '../ChatMessageComponentWrapper';
/* @conditional-compile-remove(data-loss-prevention) */
import { BlockedMessage } from '../../../types/ChatMessage';
Expand Down Expand Up @@ -140,8 +143,13 @@ export const FluentChatMessageComponent = (props: FluentChatMessageComponentWrap
);
}, [message.senderDisplayName, message.senderId, onRenderAvatar, shouldShowAvatar]);

const setMessageContainerRef = useCallback((node: HTMLDivElement | null) => {
removeFluentUIKeyboardNavigationStyles(node);
}, []);

const messageBodyProps = useMemo(() => {
return {
ref: setMessageContainerRef,
// chatItemMessageContainer used in className and style prop as style prop can't handle CSS selectors
className: mergeClasses(
chatMessageRenderStyles.bodyCommon,
Expand All @@ -157,6 +165,7 @@ export const FluentChatMessageComponent = (props: FluentChatMessageComponentWrap
styles?.chatItemMessageContainer !== undefined ? createStyleFromV8Style(styles?.chatItemMessageContainer) : {}
};
}, [
setMessageContainerRef,
chatMessageRenderStyles.bodyCommon,
chatMessageRenderStyles.bodyWithoutAvatar,
chatMessageRenderStyles.bodyHiddenAvatar,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,18 +108,20 @@ export type ChatMyMessageComponentProps = {
onInsertInlineImage?: (imageAttributes: Record<string, string>, messageId: string) => void;
/* @conditional-compile-remove(rich-text-editor-image-upload) */
inlineImagesWithProgress?: AttachmentMetadataInProgress[];
// Optional callback called when editing is complete (submitted or cancelled).
onEditComplete?: () => void;
};

/**
* @private
*/
export const ChatMyMessageComponent = (props: ChatMyMessageComponentProps): JSX.Element => {
const { onDeleteMessage, onSendMessage, message, onEditComplete, onCancelEditMessage, onUpdateMessage } = props;
const { onDeleteMessage, onSendMessage, message, onCancelEditMessage, onUpdateMessage } = props;
const [isEditing, setIsEditing] = useState(false);
const [focusMessageAfterEditing, setFocusMessageAfterEditing] = useState(false);

const onEditClick = useCallback(() => setIsEditing(true), [setIsEditing]);
const onEditClick = useCallback(() => {
setIsEditing(true);
setFocusMessageAfterEditing(false);
}, []);

const clientMessageId = 'clientMessageId' in message ? message.clientMessageId : undefined;
const content = 'content' in message ? message.content : undefined;
Expand Down Expand Up @@ -178,18 +180,20 @@ export const ChatMyMessageComponent = (props: ChatMyMessageComponentProps): JSX.
{ attachments: attachments }
);
setIsEditing(false);
onEditComplete?.();

setFocusMessageAfterEditing(true);
},
[message, onEditComplete, onUpdateMessage]
[message, onUpdateMessage]
);

const onCancelHandler = useCallback(
(messageId: string) => {
onCancelEditMessage?.(messageId);
setIsEditing(false);
onEditComplete?.();

setFocusMessageAfterEditing(true);
},
[onEditComplete, onCancelEditMessage]
[onCancelEditMessage]
);

if (isEditing && message.messageType === 'chat') {
Expand Down Expand Up @@ -227,6 +231,7 @@ export const ChatMyMessageComponent = (props: ChatMyMessageComponentProps): JSX.
inlineImageOptions={props.inlineImageOptions}
/* @conditional-compile-remove(mention) */
mentionDisplayOptions={props.mentionOptions?.displayOptions}
shouldFocusFluentMessageBody={focusMessageAfterEditing}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { Text, mergeStyles } from '@fluentui/react';
import { ChatMyMessage } from '@fluentui-contrib/react-chat';
import { _formatString } from '@internal/acs-ui-common';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
chatMessageDateStyle,
chatMessageFailedTagStyle,
Expand All @@ -28,7 +28,11 @@ import { useLocale } from '../../../localization';
import { MentionDisplayOptions } from '../../MentionPopover';
import { createStyleFromV8Style } from '../../styles/v8StyleShim';
import { mergeClasses } from '@fluentui/react-components';
import { useChatMyMessageStyles, useChatMessageCommonStyles } from '../../styles/MessageThread.styles';
import {
useChatMyMessageStyles,
useChatMessageCommonStyles,
chatMyMessageActionMenuClassName
} from '../../styles/MessageThread.styles';
import {
generateCustomizedTimestamp,
generateDefaultTimestamp,
Expand All @@ -53,6 +57,8 @@ type ChatMyMessageComponentAsMessageBubbleProps = {
* Whether the status indicator for each message is displayed or not.
*/
showMessageStatus?: boolean;
// Focus on the message body after the message is edited
shouldFocusFluentMessageBody: boolean;
remoteParticipantsCount?: number;
onActionButtonClick: (
message: ChatMessage,
Expand Down Expand Up @@ -116,7 +122,8 @@ const MessageBubble = (props: ChatMyMessageComponentAsMessageBubbleProps): JSX.E
mentionDisplayOptions,
onDisplayDateTimeString,
onRenderAttachmentDownloads,
actionsForAttachment
actionsForAttachment,
shouldFocusFluentMessageBody
} = props;

const formattedTimestamp = useMemo(() => {
Expand Down Expand Up @@ -166,6 +173,15 @@ const MessageBubble = (props: ChatMyMessageComponentAsMessageBubbleProps): JSX.E
theme
});

useEffect(() => {
if (shouldFocusFluentMessageBody) {
// set focus in the next render cycle to avoid focus being stolen by other components
setTimeout(() => {
messageRef.current?.focus();
});
}
}, [shouldFocusFluentMessageBody]);

const onActionFlyoutDismiss = useCallback((): void => {
// When the flyout dismiss is called, since we control if the action flyout is visible
// or not we need to set the target to undefined here to actually hide the action flyout
Expand All @@ -185,19 +201,57 @@ const MessageBubble = (props: ChatMyMessageComponentAsMessageBubbleProps): JSX.E
}
}, [message, messageStatus, strings.editedTag, strings.failToSendTag, theme]);

const isBlockedMessage =
false || /* @conditional-compile-remove(data-loss-prevention) */ message.messageType === 'blocked';
const chatMyMessageStyles = useChatMyMessageStyles();
const chatMessageCommonStyles = useChatMessageCommonStyles();

const attached = message.attached === true ? 'center' : message.attached === 'bottom' ? 'bottom' : 'top';

const getActionsMenu = useCallback(() => {
return (
<div
className={mergeClasses(
// add the static class name to use it in useChatMyMessageStyles
chatMyMessageActionMenuClassName,
chatMyMessageStyles.menu,
// Make actions menu visible when the message is focused or the flyout is shown
focused || chatMessageActionFlyoutTarget?.current
? chatMyMessageStyles.menuVisible
: chatMyMessageStyles.menuHidden
)}
>
{actionMenuProps?.children}
</div>
);
}, [
actionMenuProps?.children,
chatMessageActionFlyoutTarget,
chatMyMessageStyles.menu,
chatMyMessageStyles.menuHidden,
chatMyMessageStyles.menuVisible,
focused
]);

const getContent = useCallback(() => {
return getMessageBubbleContent(
message,
strings,
userId,
inlineImageOptions,
/* @conditional-compile-remove(mention) */
mentionDisplayOptions,
onRenderAttachmentDownloads,
actionsForAttachment
return (
<div>
{getMessageBubbleContent(
message,
strings,
userId,
inlineImageOptions,
/* @conditional-compile-remove(mention) */
mentionDisplayOptions,
onRenderAttachmentDownloads,
actionsForAttachment
)}
{getActionsMenu()}
</div>
);
}, [
actionsForAttachment,
getActionsMenu,
inlineImageOptions,
/* @conditional-compile-remove(mention) */ mentionDisplayOptions,
message,
Expand All @@ -206,12 +260,6 @@ const MessageBubble = (props: ChatMyMessageComponentAsMessageBubbleProps): JSX.E
userId
]);

const isBlockedMessage =
false || /* @conditional-compile-remove(data-loss-prevention) */ message.messageType === 'blocked';
const chatMyMessageStyles = useChatMyMessageStyles();
const chatMessageCommonStyles = useChatMessageCommonStyles();

const attached = message.attached === true ? 'center' : message.attached === 'bottom' ? 'bottom' : 'top';
const chatMessage = (
<>
<div key={props.message.messageId}>
Expand Down Expand Up @@ -271,17 +319,6 @@ const MessageBubble = (props: ChatMyMessageComponentAsMessageBubbleProps): JSX.E
</Text>
}
details={getMessageDetails()}
actions={{
children: actionMenuProps?.children,
className: mergeClasses(
chatMyMessageStyles.menu,
// Make actions menu visible when the message is focused or the flyout is shown
focused || chatMessageActionFlyoutTarget?.current
? chatMyMessageStyles.menuVisible
: chatMyMessageStyles.menuHidden,
attached !== 'top' ? chatMyMessageStyles.menuAttached : undefined
)
}}
onTouchStart={() => setWasInteractionByTouch(true)}
onPointerDown={() => setWasInteractionByTouch(false)}
onKeyDown={() => setWasInteractionByTouch(false)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the MIT License.

import { MessageStatus, _formatString } from '@internal/acs-ui-common';
import React, { useCallback, useMemo, useRef } from 'react';
import React, { useCallback, useMemo } from 'react';
import { MessageProps, _ChatMessageProps } from '../../MessageThread';
import { ChatMessage } from '../../../types';
/* @conditional-compile-remove(data-loss-prevention) */
Expand All @@ -14,7 +14,10 @@ import { createStyleFromV8Style } from '../../styles/v8StyleShim';
import { MessageStatusIndicatorProps } from '../../MessageStatusIndicator';
import { ChatMyMessageComponent } from './ChatMyMessageComponent';
import { ChatMyMessage as FluentChatMyMessage } from '@fluentui-contrib/react-chat';
import { getFluentUIAttachedValue } from '../../utils/ChatMessageComponentUtils';
import {
getFluentUIAttachedValue,
removeFluentUIKeyboardNavigationStyles
} from '../../utils/ChatMessageComponentUtils';
import type { FluentChatMessageComponentWrapperProps } from '../MessageComponents/FluentChatMessageComponent';

/**
Expand Down Expand Up @@ -59,11 +62,6 @@ export const FluentChatMyMessageComponent = (props: FluentChatMessageComponentWr
onInsertInlineImage
} = props;
const chatMessageRenderStyles = useChatMessageRenderStyles();
const fluentMessageBodyRef = useRef<HTMLDivElement>(null);

const onEditComplete = useCallback(() => {
fluentMessageBodyRef.current?.focus();
}, []);

// To rerender the defaultChatMessageRenderer if app running across days(every new day chat time stamp
// needs to be regenerated), the dependency on "new Date().toDateString()"" is added.
Expand All @@ -76,7 +74,6 @@ export const FluentChatMyMessageComponent = (props: FluentChatMessageComponentWr
return (
<ChatMyMessageComponent
{...messageProps}
onEditComplete={onEditComplete}
onRenderAttachmentDownloads={onRenderAttachmentDownloads}
strings={messageProps.strings}
message={messageProps.message}
Expand Down Expand Up @@ -133,8 +130,7 @@ export const FluentChatMyMessageComponent = (props: FluentChatMessageComponentWr
/* @conditional-compile-remove(rich-text-editor-image-upload) */
onInsertInlineImage,
/* @conditional-compile-remove(rich-text-editor-image-upload) */
inlineImagesWithProgress,
onEditComplete
inlineImagesWithProgress
]
);

Expand Down Expand Up @@ -195,12 +191,16 @@ export const FluentChatMyMessageComponent = (props: FluentChatMessageComponentWr
};
}, [chatMessageRenderStyles.rootCommon, chatMessageRenderStyles.rootMyMessage, styles?.myChatItemMessageContainer]);

const setMessageContainerRef = useCallback((node: HTMLDivElement | null) => {
removeFluentUIKeyboardNavigationStyles(node);
}, []);

const myMessageBodyProps = useMemo(() => {
return {
className: mergeClasses(chatMessageRenderStyles.bodyCommon, chatMessageRenderStyles.bodyMyMessage),
ref: fluentMessageBodyRef
ref: setMessageContainerRef
};
}, [chatMessageRenderStyles.bodyCommon, chatMessageRenderStyles.bodyMyMessage]);
}, [chatMessageRenderStyles.bodyCommon, chatMessageRenderStyles.bodyMyMessage, setMessageContainerRef]);

const myMessageStatusIcon = useMemo(() => {
return (
Expand Down
Loading

0 comments on commit 765d226

Please sign in to comment.