Skip to content

Commit 765d226

Browse files
Fix keyboard navigation for message menu button (#5576)
1 parent c2198c9 commit 765d226

File tree

25 files changed

+368
-132
lines changed

25 files changed

+368
-132
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "patch",
3+
"area": "fix",
4+
"workstream": "A11y",
5+
"comment": "Fix keyboard navigation for message menu button",
6+
"packageName": "@azure/communication-react",
7+
"email": "[email protected]",
8+
"dependentChangeType": "patch"
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "patch",
3+
"area": "fix",
4+
"workstream": "A11y",
5+
"comment": "Fix keyboard navigation for message menu button",
6+
"packageName": "@azure/communication-react",
7+
"email": "[email protected]",
8+
"dependentChangeType": "patch"
9+
}

packages/react-components/src/components/ChatMessage/MessageComponents/FluentChatMessageComponent.tsx

+10-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import { IPersona, PersonaSize, mergeStyles, Persona } from '@fluentui/react';
1313
import { mergeClasses } from '@fluentui/react-components';
1414
import { createStyleFromV8Style } from '../../styles/v8StyleShim';
1515
import { ChatMessage as FluentChatMessage } from '@fluentui-contrib/react-chat';
16-
import { getFluentUIAttachedValue } from '../../utils/ChatMessageComponentUtils';
16+
import {
17+
getFluentUIAttachedValue,
18+
removeFluentUIKeyboardNavigationStyles
19+
} from '../../utils/ChatMessageComponentUtils';
1720
import { ChatMessageComponentWrapperProps } from '../ChatMessageComponentWrapper';
1821
/* @conditional-compile-remove(data-loss-prevention) */
1922
import { BlockedMessage } from '../../../types/ChatMessage';
@@ -140,8 +143,13 @@ export const FluentChatMessageComponent = (props: FluentChatMessageComponentWrap
140143
);
141144
}, [message.senderDisplayName, message.senderId, onRenderAvatar, shouldShowAvatar]);
142145

146+
const setMessageContainerRef = useCallback((node: HTMLDivElement | null) => {
147+
removeFluentUIKeyboardNavigationStyles(node);
148+
}, []);
149+
143150
const messageBodyProps = useMemo(() => {
144151
return {
152+
ref: setMessageContainerRef,
145153
// chatItemMessageContainer used in className and style prop as style prop can't handle CSS selectors
146154
className: mergeClasses(
147155
chatMessageRenderStyles.bodyCommon,
@@ -157,6 +165,7 @@ export const FluentChatMessageComponent = (props: FluentChatMessageComponentWrap
157165
styles?.chatItemMessageContainer !== undefined ? createStyleFromV8Style(styles?.chatItemMessageContainer) : {}
158166
};
159167
}, [
168+
setMessageContainerRef,
160169
chatMessageRenderStyles.bodyCommon,
161170
chatMessageRenderStyles.bodyWithoutAvatar,
162171
chatMessageRenderStyles.bodyHiddenAvatar,

packages/react-components/src/components/ChatMessage/MyMessageComponents/ChatMyMessageComponent.tsx

+13-8
Original file line numberDiff line numberDiff line change
@@ -108,18 +108,20 @@ export type ChatMyMessageComponentProps = {
108108
onInsertInlineImage?: (imageAttributes: Record<string, string>, messageId: string) => void;
109109
/* @conditional-compile-remove(rich-text-editor-image-upload) */
110110
inlineImagesWithProgress?: AttachmentMetadataInProgress[];
111-
// Optional callback called when editing is complete (submitted or cancelled).
112-
onEditComplete?: () => void;
113111
};
114112

115113
/**
116114
* @private
117115
*/
118116
export const ChatMyMessageComponent = (props: ChatMyMessageComponentProps): JSX.Element => {
119-
const { onDeleteMessage, onSendMessage, message, onEditComplete, onCancelEditMessage, onUpdateMessage } = props;
117+
const { onDeleteMessage, onSendMessage, message, onCancelEditMessage, onUpdateMessage } = props;
120118
const [isEditing, setIsEditing] = useState(false);
119+
const [focusMessageAfterEditing, setFocusMessageAfterEditing] = useState(false);
121120

122-
const onEditClick = useCallback(() => setIsEditing(true), [setIsEditing]);
121+
const onEditClick = useCallback(() => {
122+
setIsEditing(true);
123+
setFocusMessageAfterEditing(false);
124+
}, []);
123125

124126
const clientMessageId = 'clientMessageId' in message ? message.clientMessageId : undefined;
125127
const content = 'content' in message ? message.content : undefined;
@@ -178,18 +180,20 @@ export const ChatMyMessageComponent = (props: ChatMyMessageComponentProps): JSX.
178180
{ attachments: attachments }
179181
);
180182
setIsEditing(false);
181-
onEditComplete?.();
183+
184+
setFocusMessageAfterEditing(true);
182185
},
183-
[message, onEditComplete, onUpdateMessage]
186+
[message, onUpdateMessage]
184187
);
185188

186189
const onCancelHandler = useCallback(
187190
(messageId: string) => {
188191
onCancelEditMessage?.(messageId);
189192
setIsEditing(false);
190-
onEditComplete?.();
193+
194+
setFocusMessageAfterEditing(true);
191195
},
192-
[onEditComplete, onCancelEditMessage]
196+
[onCancelEditMessage]
193197
);
194198

195199
if (isEditing && message.messageType === 'chat') {
@@ -227,6 +231,7 @@ export const ChatMyMessageComponent = (props: ChatMyMessageComponentProps): JSX.
227231
inlineImageOptions={props.inlineImageOptions}
228232
/* @conditional-compile-remove(mention) */
229233
mentionDisplayOptions={props.mentionOptions?.displayOptions}
234+
shouldFocusFluentMessageBody={focusMessageAfterEditing}
230235
/>
231236
);
232237
}

packages/react-components/src/components/ChatMessage/MyMessageComponents/ChatMyMessageComponentAsMessageBubble.tsx

+66-29
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { Text, mergeStyles } from '@fluentui/react';
55
import { ChatMyMessage } from '@fluentui-contrib/react-chat';
66
import { _formatString } from '@internal/acs-ui-common';
7-
import React, { useCallback, useMemo, useRef, useState } from 'react';
7+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
88
import {
99
chatMessageDateStyle,
1010
chatMessageFailedTagStyle,
@@ -28,7 +28,11 @@ import { useLocale } from '../../../localization';
2828
import { MentionDisplayOptions } from '../../MentionPopover';
2929
import { createStyleFromV8Style } from '../../styles/v8StyleShim';
3030
import { mergeClasses } from '@fluentui/react-components';
31-
import { useChatMyMessageStyles, useChatMessageCommonStyles } from '../../styles/MessageThread.styles';
31+
import {
32+
useChatMyMessageStyles,
33+
useChatMessageCommonStyles,
34+
chatMyMessageActionMenuClassName
35+
} from '../../styles/MessageThread.styles';
3236
import {
3337
generateCustomizedTimestamp,
3438
generateDefaultTimestamp,
@@ -53,6 +57,8 @@ type ChatMyMessageComponentAsMessageBubbleProps = {
5357
* Whether the status indicator for each message is displayed or not.
5458
*/
5559
showMessageStatus?: boolean;
60+
// Focus on the message body after the message is edited
61+
shouldFocusFluentMessageBody: boolean;
5662
remoteParticipantsCount?: number;
5763
onActionButtonClick: (
5864
message: ChatMessage,
@@ -116,7 +122,8 @@ const MessageBubble = (props: ChatMyMessageComponentAsMessageBubbleProps): JSX.E
116122
mentionDisplayOptions,
117123
onDisplayDateTimeString,
118124
onRenderAttachmentDownloads,
119-
actionsForAttachment
125+
actionsForAttachment,
126+
shouldFocusFluentMessageBody
120127
} = props;
121128

122129
const formattedTimestamp = useMemo(() => {
@@ -166,6 +173,15 @@ const MessageBubble = (props: ChatMyMessageComponentAsMessageBubbleProps): JSX.E
166173
theme
167174
});
168175

176+
useEffect(() => {
177+
if (shouldFocusFluentMessageBody) {
178+
// set focus in the next render cycle to avoid focus being stolen by other components
179+
setTimeout(() => {
180+
messageRef.current?.focus();
181+
});
182+
}
183+
}, [shouldFocusFluentMessageBody]);
184+
169185
const onActionFlyoutDismiss = useCallback((): void => {
170186
// When the flyout dismiss is called, since we control if the action flyout is visible
171187
// or not we need to set the target to undefined here to actually hide the action flyout
@@ -185,19 +201,57 @@ const MessageBubble = (props: ChatMyMessageComponentAsMessageBubbleProps): JSX.E
185201
}
186202
}, [message, messageStatus, strings.editedTag, strings.failToSendTag, theme]);
187203

204+
const isBlockedMessage =
205+
false || /* @conditional-compile-remove(data-loss-prevention) */ message.messageType === 'blocked';
206+
const chatMyMessageStyles = useChatMyMessageStyles();
207+
const chatMessageCommonStyles = useChatMessageCommonStyles();
208+
209+
const attached = message.attached === true ? 'center' : message.attached === 'bottom' ? 'bottom' : 'top';
210+
211+
const getActionsMenu = useCallback(() => {
212+
return (
213+
<div
214+
className={mergeClasses(
215+
// add the static class name to use it in useChatMyMessageStyles
216+
chatMyMessageActionMenuClassName,
217+
chatMyMessageStyles.menu,
218+
// Make actions menu visible when the message is focused or the flyout is shown
219+
focused || chatMessageActionFlyoutTarget?.current
220+
? chatMyMessageStyles.menuVisible
221+
: chatMyMessageStyles.menuHidden
222+
)}
223+
>
224+
{actionMenuProps?.children}
225+
</div>
226+
);
227+
}, [
228+
actionMenuProps?.children,
229+
chatMessageActionFlyoutTarget,
230+
chatMyMessageStyles.menu,
231+
chatMyMessageStyles.menuHidden,
232+
chatMyMessageStyles.menuVisible,
233+
focused
234+
]);
235+
188236
const getContent = useCallback(() => {
189-
return getMessageBubbleContent(
190-
message,
191-
strings,
192-
userId,
193-
inlineImageOptions,
194-
/* @conditional-compile-remove(mention) */
195-
mentionDisplayOptions,
196-
onRenderAttachmentDownloads,
197-
actionsForAttachment
237+
return (
238+
<div>
239+
{getMessageBubbleContent(
240+
message,
241+
strings,
242+
userId,
243+
inlineImageOptions,
244+
/* @conditional-compile-remove(mention) */
245+
mentionDisplayOptions,
246+
onRenderAttachmentDownloads,
247+
actionsForAttachment
248+
)}
249+
{getActionsMenu()}
250+
</div>
198251
);
199252
}, [
200253
actionsForAttachment,
254+
getActionsMenu,
201255
inlineImageOptions,
202256
/* @conditional-compile-remove(mention) */ mentionDisplayOptions,
203257
message,
@@ -206,12 +260,6 @@ const MessageBubble = (props: ChatMyMessageComponentAsMessageBubbleProps): JSX.E
206260
userId
207261
]);
208262

209-
const isBlockedMessage =
210-
false || /* @conditional-compile-remove(data-loss-prevention) */ message.messageType === 'blocked';
211-
const chatMyMessageStyles = useChatMyMessageStyles();
212-
const chatMessageCommonStyles = useChatMessageCommonStyles();
213-
214-
const attached = message.attached === true ? 'center' : message.attached === 'bottom' ? 'bottom' : 'top';
215263
const chatMessage = (
216264
<>
217265
<div key={props.message.messageId}>
@@ -271,17 +319,6 @@ const MessageBubble = (props: ChatMyMessageComponentAsMessageBubbleProps): JSX.E
271319
</Text>
272320
}
273321
details={getMessageDetails()}
274-
actions={{
275-
children: actionMenuProps?.children,
276-
className: mergeClasses(
277-
chatMyMessageStyles.menu,
278-
// Make actions menu visible when the message is focused or the flyout is shown
279-
focused || chatMessageActionFlyoutTarget?.current
280-
? chatMyMessageStyles.menuVisible
281-
: chatMyMessageStyles.menuHidden,
282-
attached !== 'top' ? chatMyMessageStyles.menuAttached : undefined
283-
)
284-
}}
285322
onTouchStart={() => setWasInteractionByTouch(true)}
286323
onPointerDown={() => setWasInteractionByTouch(false)}
287324
onKeyDown={() => setWasInteractionByTouch(false)}

packages/react-components/src/components/ChatMessage/MyMessageComponents/FluentChatMyMessageComponent.tsx

+12-12
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the MIT License.
33

44
import { MessageStatus, _formatString } from '@internal/acs-ui-common';
5-
import React, { useCallback, useMemo, useRef } from 'react';
5+
import React, { useCallback, useMemo } from 'react';
66
import { MessageProps, _ChatMessageProps } from '../../MessageThread';
77
import { ChatMessage } from '../../../types';
88
/* @conditional-compile-remove(data-loss-prevention) */
@@ -14,7 +14,10 @@ import { createStyleFromV8Style } from '../../styles/v8StyleShim';
1414
import { MessageStatusIndicatorProps } from '../../MessageStatusIndicator';
1515
import { ChatMyMessageComponent } from './ChatMyMessageComponent';
1616
import { ChatMyMessage as FluentChatMyMessage } from '@fluentui-contrib/react-chat';
17-
import { getFluentUIAttachedValue } from '../../utils/ChatMessageComponentUtils';
17+
import {
18+
getFluentUIAttachedValue,
19+
removeFluentUIKeyboardNavigationStyles
20+
} from '../../utils/ChatMessageComponentUtils';
1821
import type { FluentChatMessageComponentWrapperProps } from '../MessageComponents/FluentChatMessageComponent';
1922

2023
/**
@@ -59,11 +62,6 @@ export const FluentChatMyMessageComponent = (props: FluentChatMessageComponentWr
5962
onInsertInlineImage
6063
} = props;
6164
const chatMessageRenderStyles = useChatMessageRenderStyles();
62-
const fluentMessageBodyRef = useRef<HTMLDivElement>(null);
63-
64-
const onEditComplete = useCallback(() => {
65-
fluentMessageBodyRef.current?.focus();
66-
}, []);
6765

6866
// To rerender the defaultChatMessageRenderer if app running across days(every new day chat time stamp
6967
// needs to be regenerated), the dependency on "new Date().toDateString()"" is added.
@@ -76,7 +74,6 @@ export const FluentChatMyMessageComponent = (props: FluentChatMessageComponentWr
7674
return (
7775
<ChatMyMessageComponent
7876
{...messageProps}
79-
onEditComplete={onEditComplete}
8077
onRenderAttachmentDownloads={onRenderAttachmentDownloads}
8178
strings={messageProps.strings}
8279
message={messageProps.message}
@@ -133,8 +130,7 @@ export const FluentChatMyMessageComponent = (props: FluentChatMessageComponentWr
133130
/* @conditional-compile-remove(rich-text-editor-image-upload) */
134131
onInsertInlineImage,
135132
/* @conditional-compile-remove(rich-text-editor-image-upload) */
136-
inlineImagesWithProgress,
137-
onEditComplete
133+
inlineImagesWithProgress
138134
]
139135
);
140136

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

194+
const setMessageContainerRef = useCallback((node: HTMLDivElement | null) => {
195+
removeFluentUIKeyboardNavigationStyles(node);
196+
}, []);
197+
198198
const myMessageBodyProps = useMemo(() => {
199199
return {
200200
className: mergeClasses(chatMessageRenderStyles.bodyCommon, chatMessageRenderStyles.bodyMyMessage),
201-
ref: fluentMessageBodyRef
201+
ref: setMessageContainerRef
202202
};
203-
}, [chatMessageRenderStyles.bodyCommon, chatMessageRenderStyles.bodyMyMessage]);
203+
}, [chatMessageRenderStyles.bodyCommon, chatMessageRenderStyles.bodyMyMessage, setMessageContainerRef]);
204204

205205
const myMessageStatusIcon = useMemo(() => {
206206
return (

0 commit comments

Comments
 (0)