Fix: Clicking on an item in the member list causes it to scroll to the top rather than show the profile view (#30455)

* Fix issue and add test

* Fix MemberTileView

* Add e2e test and comment
This commit is contained in:
David Langley
2025-08-01 14:16:13 +01:00
committed by GitHub
parent e43b696461
commit 2250f5e6a2
8 changed files with 195 additions and 20 deletions

View File

@@ -42,7 +42,12 @@ export interface IListViewProps<Item, Context>
* @param context - The context object containing the focused key and any additional data
* @returns JSX element representing the rendered item
*/
getItemComponent: (index: number, item: Item, context: ListContext<Context>) => JSX.Element;
getItemComponent: (
index: number,
item: Item,
context: ListContext<Context>,
onFocus: (e: React.FocusEvent) => void,
) => JSX.Element;
/**
* Optional additional context data to pass to each rendered item.
@@ -217,6 +222,20 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
virtuosoDomRef.current = element;
}, []);
const getItemComponentInternal = useCallback(
(index: number, item: Item, context: ListContext<Context>): JSX.Element => {
const onFocus = (e: React.FocusEvent): void => {
// If one of the item components has been focused directly, set the focused and tabIndex state
// and stop propagation so the ListViews onFocus doesn't also handle it.
const key = getItemKey(item);
setIsFocused(true);
setTabIndexKey(key);
e.stopPropagation();
};
return getItemComponent(index, item, context, onFocus);
},
[getItemComponent, getItemKey],
);
/**
* Handles focus events on the list.
* Sets the focused state and scrolls to the focused item if it is not currently visible.
@@ -265,7 +284,7 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
data={props.items}
onFocus={onFocus}
onBlur={onBlur}
itemContent={props.getItemComponent}
itemContent={getItemComponentInternal}
{...virtuosoProps}
/>
);

View File

@@ -41,7 +41,12 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
}, []);
const getItemComponent = useCallback(
(index: number, item: MemberWithSeparator, context: ListContext<any>): JSX.Element => {
(
index: number,
item: MemberWithSeparator,
context: ListContext<any>,
onFocus: (e: React.FocusEvent) => void,
): JSX.Element => {
const itemKey = getItemKey(item);
const isRovingItem = itemKey === context.tabIndexKey;
const focused = isRovingItem && context.focused;
@@ -56,6 +61,7 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
tabIndex={isRovingItem ? 0 : -1}
index={index}
memberCount={memberCount}
onFocus={onFocus}
/>
);
} else {
@@ -66,6 +72,7 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
tabIndex={isRovingItem ? 0 : -1}
memberIndex={index - 1} // Adjust as invites are below the separator
memberCount={memberCount}
onFocus={onFocus}
/>
);
}

View File

@@ -24,6 +24,7 @@ interface IProps {
showPresence?: boolean;
focused?: boolean;
tabIndex?: number;
onFocus: (e: React.FocusEvent) => void;
}
export function RoomMemberTileView(props: IProps): JSX.Element {
@@ -59,6 +60,7 @@ export function RoomMemberTileView(props: IProps): JSX.Element {
return (
<MemberTileView
onClick={vm.onClick}
onFocus={props.onFocus}
avatarJsx={av}
presenceJsx={presenceJSX}
nameJsx={nameJSX}

View File

@@ -19,6 +19,7 @@ interface Props {
memberCount: number;
focused?: boolean;
tabIndex?: number;
onFocus: (e: React.FocusEvent) => void;
}
export function ThreePidInviteTileView(props: Props): JSX.Element {
@@ -39,6 +40,7 @@ export function ThreePidInviteTileView(props: Props): JSX.Element {
iconJsx={iconJsx}
focused={props.focused}
tabIndex={props.tabIndex}
onFocus={props.onFocus}
/>
);
}

View File

@@ -13,6 +13,7 @@ interface Props {
avatarJsx: JSX.Element;
nameJsx: JSX.Element | string;
onClick: () => void;
onFocus: (e: React.FocusEvent) => void;
memberIndex: number;
memberCount: number;
ariaLabel?: string;
@@ -41,6 +42,7 @@ export function MemberTileView(props: Props): JSX.Element {
ref={ref}
className="mx_MemberTileView"
onClick={props.onClick}
onFocus={props.onFocus}
aria-label={props?.ariaLabel}
tabIndex={props.tabIndex}
role="option"