Skip to content

Commit ba51f11

Browse files
[Chat] Added mentions list navigation by keyboard (#3958)
1 parent 909a9ec commit ba51f11

6 files changed

Lines changed: 92 additions & 23 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "patch",
3+
"area": "fix",
4+
"workstream": "Mentions",
5+
"comment": "Fixed an issues when mentions list wasn't scrolled when navigated by keyboard",
6+
"packageName": "@azure/communication-react",
7+
"email": "98852890+vhuseinova-msft@users.noreply.github.com",
8+
"dependentChangeType": "patch"
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "patch",
3+
"area": "fix",
4+
"workstream": "Mentions",
5+
"comment": "Fixed an issues when mentions list wasn't scrolled when navigated by keyboard",
6+
"packageName": "@azure/communication-react",
7+
"email": "98852890+vhuseinova-msft@users.noreply.github.com",
8+
"dependentChangeType": "patch"
9+
}

packages/react-components/src/components/MentionPopover.tsx

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@
33

44
import React, { useEffect, useRef, useState, useCallback } from 'react';
55
import { Persona, PersonaSize, Stack, mergeStyles, useTheme } from '@fluentui/react';
6+
import { mergeClasses } from '@fluentui/react-components';
67
import {
78
mentionPopoverContainerStyle,
89
headerStyleThemed,
9-
suggestionListStyle,
1010
suggestionItemStackStyle,
11-
suggestionItemWrapperStyle
11+
suggestionItemWrapperStyle,
12+
useSuggestionListStyle
1213
} from './styles/MentionPopover.style';
1314
/* @conditional-compile-remove(mention) */
1415
import { useIdentifiers } from '../identifiers';
1516
import { useLocale } from '../localization';
17+
import { useDefaultStackStyles } from './styles/Stack.style';
1618

1719
/**
1820
* Props for {@link _MentionPopover}.
@@ -165,10 +167,13 @@ export const _MentionPopover = (props: _MentionPopoverProps): JSX.Element => {
165167
const ids = useIdentifiers();
166168
const localeStrings = useLocale().strings;
167169
const popoverRef = useRef() as React.MutableRefObject<HTMLDivElement>;
170+
const suggestionsListRef = useRef<HTMLDivElement>(null);
168171

169172
const [position, setPosition] = useState<Position | undefined>();
170173
const [hoveredSuggestion, setHoveredSuggestion] = useState<Mention | undefined>(undefined);
171-
const [changedSelection, setChangedSelection] = useState<boolean | undefined>(undefined); // Selection UI as per teams
174+
175+
const suggestionListStyle = useSuggestionListStyle();
176+
const defaultStackStyles = useDefaultStackStyles();
172177

173178
const dismissPopoverWhenClickingOutside = useCallback(
174179
(e: MouseEvent) => {
@@ -181,12 +186,13 @@ export const _MentionPopover = (props: _MentionPopoverProps): JSX.Element => {
181186
);
182187

183188
useEffect(() => {
184-
if (changedSelection === undefined) {
185-
setChangedSelection(false);
186-
} else if (changedSelection === false) {
187-
setChangedSelection(true);
189+
if (suggestionsListRef.current && activeSuggestionIndex !== undefined && activeSuggestionIndex >= 0) {
190+
const selectedItem = suggestionsListRef.current.children[activeSuggestionIndex];
191+
if (selectedItem) {
192+
selectedItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
193+
}
188194
}
189-
}, [activeSuggestionIndex, changedSelection]);
195+
}, [activeSuggestionIndex]);
190196

191197
useEffect(() => {
192198
window && window.addEventListener('click', dismissPopoverWhenClickingOutside);
@@ -266,11 +272,7 @@ export const _MentionPopover = (props: _MentionPopoverProps): JSX.Element => {
266272
>
267273
<Stack
268274
horizontal
269-
className={suggestionItemStackStyle(
270-
theme,
271-
hoveredSuggestion?.id === suggestion.id,
272-
(changedSelection ?? false) && active
273-
)}
275+
className={suggestionItemStackStyle(theme, hoveredSuggestion?.id === suggestion.id, active)}
274276
>
275277
{personaRenderer(suggestion.displayText)}
276278
</Stack>
@@ -283,7 +285,6 @@ export const _MentionPopover = (props: _MentionPopoverProps): JSX.Element => {
283285
/* @conditional-compile-remove(mention) */
284286
ids,
285287
hoveredSuggestion,
286-
changedSelection,
287288
personaRenderer
288289
]
289290
);
@@ -317,18 +318,20 @@ export const _MentionPopover = (props: _MentionPopoverProps): JSX.Element => {
317318
<Stack.Item styles={headerStyleThemed(theme)} aria-label={title}>
318319
{getHeaderTitle()}
319320
</Stack.Item>
320-
<Stack
321+
{/* FluentUI v9 approach is used here instead of Stack because Stack doesn't have ref prop */}
322+
<div
323+
className={mergeClasses(defaultStackStyles.root, suggestionListStyle.root)}
321324
/* @conditional-compile-remove(mention) */
322325
data-ui-id={ids.mentionSuggestionList}
323-
className={suggestionListStyle}
326+
ref={suggestionsListRef}
324327
>
325328
{suggestions.map((suggestion, index) => {
326329
const active = index === activeSuggestionIndex;
327330
return onRenderSuggestionItem
328331
? onRenderSuggestionItem(suggestion, onSuggestionSelected, active)
329332
: defaultOnRenderSuggestionItem(suggestion, onSuggestionSelected, active);
330333
})}
331-
</Stack>
334+
</div>
332335
</Stack>
333336
)}
334337
</div>

packages/react-components/src/components/TextFieldWithMention/TextFieldWithMention.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ export const TextFieldWithMention = (props: TextFieldWithMentionProps): JSX.Elem
220220
if (isEnterKeyEventFromCompositionSession(ev)) {
221221
return;
222222
}
223-
223+
let isActiveSuggestionIndexUpdated = false;
224224
if (mentionSuggestions.length > 0) {
225225
if (ev.key === 'ArrowUp') {
226226
ev.preventDefault();
@@ -229,15 +229,20 @@ export const TextFieldWithMention = (props: TextFieldWithMentionProps): JSX.Elem
229229
? mentionSuggestions.length - 1
230230
: Math.max(activeSuggestionIndex - 1, 0);
231231
setActiveSuggestionIndex(newActiveIndex);
232+
isActiveSuggestionIndexUpdated = true;
232233
} else if (ev.key === 'ArrowDown') {
233234
ev.preventDefault();
234235
const newActiveIndex =
235236
activeSuggestionIndex === undefined
236237
? 0
237238
: Math.min(activeSuggestionIndex + 1, mentionSuggestions.length - 1);
238239
setActiveSuggestionIndex(newActiveIndex);
240+
isActiveSuggestionIndexUpdated = true;
239241
} else if (ev.key === 'Escape') {
240242
updateMentionSuggestions([]);
243+
// reset active suggestion index when suggestions are closed
244+
setActiveSuggestionIndex(undefined);
245+
isActiveSuggestionIndexUpdated = true;
241246
}
242247
}
243248
if (ev.key === 'Enter' && (ev.shiftKey === false || !supportNewline)) {
@@ -253,6 +258,10 @@ export const TextFieldWithMention = (props: TextFieldWithMentionProps): JSX.Elem
253258
}
254259

255260
onEnterKeyDown && onEnterKeyDown();
261+
} else if (!isActiveSuggestionIndexUpdated) {
262+
// Update the active suggestion index if the user is typing,
263+
// otherwise the focus will be lost
264+
setActiveSuggestionIndex(undefined);
256265
}
257266
onKeyDown && onKeyDown(ev);
258267
},

packages/react-components/src/components/styles/MentionPopover.style.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
import { IStackStyles, mergeStyles, Theme } from '@fluentui/react';
5+
import { makeStyles, shorthands } from '@fluentui/react-components';
56

67
/**
78
* @private
@@ -41,18 +42,22 @@ export const suggestionListContainerStyle = mergeStyles({
4142
/**
4243
* @private
4344
*/
44-
export const suggestionListStyle = mergeStyles({
45-
padding: '0.25rem 0rem 0',
46-
overflow: 'visible',
47-
overflowY: 'scroll'
45+
export const useSuggestionListStyle = makeStyles({
46+
root: {
47+
...shorthands.padding('0.25rem', '0rem', '0rem'),
48+
...shorthands.overflow('visible'),
49+
overflowY: 'scroll'
50+
}
4851
});
4952

5053
/**
5154
* @private
5255
*/
5356
export const suggestionItemWrapperStyle = (theme: Theme): string => {
5457
return mergeStyles({
55-
margin: '0.05rem 0',
58+
margin: '0.0625rem 0',
59+
'scroll-margin-top': '0.0625rem',
60+
'scroll-margin-bottom': '0.0625rem',
5661
'&:focus-visible': {
5762
outline: `${theme.palette.black} solid 0.1rem`
5863
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { makeStyles } from '@fluentui/react-components';
5+
6+
// These are styles that should be used during the migration from FluentUI v8 Stack component to flex.
7+
// https://react.fluentui.dev/?path=/docs/concepts-migration-from-v8-components-flex-stack--page
8+
9+
/**
10+
* @private
11+
*/
12+
export const useDefaultStackStyles = makeStyles({
13+
root: {
14+
display: 'flex',
15+
flexDirection: 'column',
16+
flexWrap: 'nowrap',
17+
width: 'auto',
18+
height: 'auto',
19+
boxSizing: 'border-box',
20+
'> *': {
21+
textOverflow: 'ellipsis'
22+
}
23+
// As per CSS props, these two styles are not in the original Stack styles
24+
// (and they have higher specificity that just classes and will be applied instead of rules in classes).
25+
// They also break a layout for the first selected item in MentionPopover if applied.
26+
// Please check that component, if you want to apply these styles
27+
// '> :not(:first-child)': {
28+
// marginTop: '0px'
29+
// },
30+
// '> *:not(.ms-StackItem)': {
31+
// flexShrink: 1
32+
// }
33+
}
34+
});

0 commit comments

Comments
 (0)