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 13:16:13 +00:00
committed by GitHub
parent e43b696461
commit 2250f5e6a2
8 changed files with 195 additions and 20 deletions
@@ -35,7 +35,9 @@ describe("MemberTileView", () => {
});
it("should not display an E2EIcon when the e2E status = normal", () => {
const { container } = render(<RoomMemberTileView member={member} index={0} memberCount={1} />);
const { container } = render(
<RoomMemberTileView member={member} index={0} memberCount={1} onFocus={jest.fn()} />,
);
const e2eIcon = container.querySelector(".mx_E2EIconView");
expect(e2eIcon).toBeNull();
expect(container).toMatchSnapshot();
@@ -47,7 +49,9 @@ describe("MemberTileView", () => {
wasCrossSigningVerified: jest.fn().mockReturnValue(true),
} as unknown as UserVerificationStatus);
const { container } = render(<RoomMemberTileView member={member} index={0} memberCount={1} />);
const { container } = render(
<RoomMemberTileView member={member} index={0} memberCount={1} onFocus={jest.fn()} />,
);
await waitFor(async () => {
await userEvent.hover(container.querySelector(".mx_E2EIcon")!);
expect(screen.getByText("This user has not verified all of their sessions.")).toBeInTheDocument();
@@ -68,7 +72,9 @@ describe("MemberTileView", () => {
crossSigningVerified: true,
} as DeviceVerificationStatus);
const { container } = render(<RoomMemberTileView member={member} index={0} memberCount={1} />);
const { container } = render(
<RoomMemberTileView member={member} index={0} memberCount={1} onFocus={jest.fn()} />,
);
await waitFor(async () => {
await userEvent.hover(container.querySelector(".mx_E2EIcon")!);
@@ -81,15 +87,21 @@ describe("MemberTileView", () => {
it("renders user labels correctly", async () => {
member.powerLevel = 50;
const { container: container1 } = render(<RoomMemberTileView member={member} index={0} memberCount={1} />);
const { container: container1 } = render(
<RoomMemberTileView member={member} index={0} memberCount={1} onFocus={jest.fn()} />,
);
expect(container1).toHaveTextContent("Moderator");
member.powerLevel = 100;
const { container: container2 } = render(<RoomMemberTileView member={member} index={0} memberCount={1} />);
const { container: container2 } = render(
<RoomMemberTileView member={member} index={0} memberCount={1} onFocus={jest.fn()} />,
);
expect(container2).toHaveTextContent("Admin");
member.isInvite = true;
const { container: container3 } = render(<RoomMemberTileView member={member} index={0} memberCount={1} />);
const { container: container3 } = render(
<RoomMemberTileView member={member} index={0} memberCount={1} onFocus={jest.fn()} />,
);
expect(container3).toHaveTextContent("Invited");
});
});
@@ -110,7 +122,12 @@ describe("MemberTileView", () => {
it("renders ThreePidInvite correctly", async () => {
const [{ threePidInvite }] = getPending3PidInvites(room);
const { container } = render(
<ThreePidInviteTileView threePidInvite={threePidInvite!} memberIndex={0} memberCount={1} />,
<ThreePidInviteTileView
threePidInvite={threePidInvite!}
memberIndex={0}
memberCount={1}
onFocus={jest.fn()}
/>,
);
expect(container).toMatchSnapshot();
});
@@ -334,6 +334,82 @@ describe("ListView", () => {
expect(container).toBeInTheDocument();
});
it("should not scroll to top when clicking an item after manual scroll", () => {
// Create a larger list to enable meaningful scrolling
const largerItems = Array.from({ length: 50 }, (_, i) => ({
id: `item-${i}`,
name: `Item ${i}`,
}));
const mockOnClick = jest.fn();
mockGetItemComponent.mockImplementation(
(index: number, item: TestItemWithSeparator, context: any, onFocus: (e: React.FocusEvent) => void) => {
const itemKey = typeof item === "string" ? item : item.id;
const isFocused = context.tabIndexKey === itemKey;
return (
<div
className="mx_item"
data-testid={`row-${index}`}
tabIndex={isFocused ? 0 : -1}
onClick={() => mockOnClick(item)}
onFocus={onFocus}
>
{item === SEPARATOR_ITEM ? "---" : (item as TestItem).name}
</div>
);
},
);
const { container } = renderListViewWithHeight({ items: largerItems });
const listContainer = screen.getByRole("grid");
// Step 1: Focus the list initially (this sets tabIndexKey to first item: "item-0")
fireEvent.focus(listContainer);
// Verify first item is focused initially and tabIndexKey is set to first item
let items = container.querySelectorAll(".mx_item");
expect(items[0]).toHaveAttribute("tabindex", "0");
expect(items[0]).toHaveAttribute("data-testid", "row-0");
// Step 2: Simulate manual scrolling (mouse wheel, scroll bar drag, etc.)
// This changes which items are visible but DOES NOT change tabIndexKey
// tabIndexKey should still point to "item-0" but "item-0" is no longer visible
fireEvent.scroll(listContainer, { target: { scrollTop: 300 } });
// Step 3: After scrolling, different items should now be visible
// but tabIndexKey should still point to "item-0" (which is no longer visible)
items = container.querySelectorAll(".mx_item");
// Verify that item-0 is no longer in the DOM (because it's scrolled out of view)
const item0 = container.querySelector("[data-testid='row-0']");
expect(item0).toBeNull();
// Find a visible item to click on (should be items from further down the list)
const visibleItems = container.querySelectorAll(".mx_item");
expect(visibleItems.length).toBeGreaterThan(0);
const clickTargetItem = visibleItems[0]; // Click on the first visible item
// Click on the visible item
fireEvent.click(clickTargetItem);
// The click should trigger the onFocus callback, which updates the tabIndexKey
// This simulates the real user interaction where clicking an item focuses it
fireEvent.focus(clickTargetItem);
// Verify the click was handled
expect(mockOnClick).toHaveBeenCalled();
// With the fix applied: the clicked item should become focused (tabindex="0")
// This validates that the fix prevents unwanted scrolling back to the top
expect(clickTargetItem).toHaveAttribute("tabindex", "0");
// The key validation: ensure we haven't scrolled back to the top
// item-0 should still not be visible (if the fix is working)
const item0AfterClick = container.querySelector("[data-testid='row-0']");
expect(item0AfterClick).toBeNull();
});
});
describe("Accessibility", () => {