New room list: add keyboard navigation support (#29805)

* feat: support up/down arrow navigation in the new room list

* feat: support tabbing in the new room list

* test: update snapshots

* test(e2e): fix room list test

* test(new room list): add landmark navigation test

* test(e2e): update screenshot test

* test: add test to `RoomListItemView`

* test(e2e): add keyboard navigation tests

* refactor: rename `setIsHover` on `setIsHoverWithDelay`
This commit is contained in:
Florian Duros
2025-05-06 18:09:23 +02:00
committed by GitHub
parent 6ba21dafa7
commit 74fbd892a1
9 changed files with 211 additions and 47 deletions

View File

@@ -8,6 +8,7 @@
import React from "react";
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
import { render } from "jest-matrix-react";
import { fireEvent } from "@testing-library/dom";
import { mkRoom, stubClient } from "../../../../../test-utils";
import { type RoomListViewState } from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel";
@@ -15,6 +16,7 @@ import { RoomList } from "../../../../../../src/components/views/rooms/RoomListP
import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
import { SecondaryFilters } from "../../../../../../src/components/viewmodels/roomlist/useFilteredRooms";
import { SortOption } from "../../../../../../src/components/viewmodels/roomlist/useSorter";
import { Landmark, LandmarkNavigation } from "../../../../../../src/accessibility/LandmarkNavigation";
describe("<RoomList />", () => {
let matrixClient: MatrixClient;
@@ -53,4 +55,16 @@ describe("<RoomList />", () => {
const { asFragment } = render(<RoomList vm={vm} />);
expect(asFragment()).toMatchSnapshot();
});
it.each([
{ shortcut: { key: "F6", ctrlKey: true, shiftKey: true }, isPreviousLandmark: true, label: "PreviousLandmark" },
{ shortcut: { key: "F6", ctrlKey: true }, isPreviousLandmark: false, label: "NextLandmark" },
])("should navigate to the landmark on NextLandmark.$label action", ({ shortcut, isPreviousLandmark }) => {
const spyFindLandmark = jest.spyOn(LandmarkNavigation, "findAndFocusNextLandmark").mockReturnValue();
const { getByTestId } = render(<RoomList vm={vm} />);
const roomList = getByTestId("room-list");
fireEvent.keyDown(roomList, shortcut);
expect(spyFindLandmark).toHaveBeenCalledWith(Landmark.ROOM_LIST, isPreviousLandmark);
});
});

View File

@@ -91,6 +91,17 @@ describe("<RoomListItemView />", () => {
await waitFor(() => expect(screen.getByRole("button", { name: "More Options" })).toBeInTheDocument());
});
test("should hover decoration if focused", async () => {
const user = userEvent.setup();
render(<RoomListItemView room={room} isSelected={false} />, withClientContextRenderOptions(matrixClient));
const listItem = screen.getByRole("button", { name: `Open room ${room.name}` });
await user.click(listItem);
expect(listItem).toHaveClass("mx_RoomListItemView_hover");
await user.tab();
await waitFor(() => expect(listItem).not.toHaveClass("mx_RoomListItemView_hover"));
});
test("should be selected if isSelected=true", async () => {
const { asFragment } = render(<RoomListItemView room={room} isSelected={true} />);
expect(screen.queryByRole("button", { name: `Open room ${room.name}` })).toHaveAttribute(

View File

@@ -15,7 +15,7 @@ exports[`<RoomList /> should render a room list 1`] = `
class="ReactVirtualized__Grid ReactVirtualized__List mx_RoomList_List"
role="grid"
style="box-sizing: border-box; direction: ltr; height: 1500px; position: relative; width: 1500px; will-change: transform; overflow-x: hidden; overflow-y: hidden;"
tabindex="0"
tabindex="-1"
>
<div
class="ReactVirtualized__Grid__innerScrollContainer"
@@ -28,6 +28,7 @@ exports[`<RoomList /> should render a room list 1`] = `
class="mx_RoomListItemView mx_RoomListItemView_empty"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 0px; width: 100%;"
tabindex="0"
type="button"
>
<div
@@ -79,6 +80,7 @@ exports[`<RoomList /> should render a room list 1`] = `
class="mx_RoomListItemView mx_RoomListItemView_empty"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 48px; width: 100%;"
tabindex="-1"
type="button"
>
<div
@@ -130,6 +132,7 @@ exports[`<RoomList /> should render a room list 1`] = `
class="mx_RoomListItemView mx_RoomListItemView_empty"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 96px; width: 100%;"
tabindex="-1"
type="button"
>
<div
@@ -181,6 +184,7 @@ exports[`<RoomList /> should render a room list 1`] = `
class="mx_RoomListItemView mx_RoomListItemView_empty"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 144px; width: 100%;"
tabindex="-1"
type="button"
>
<div
@@ -232,6 +236,7 @@ exports[`<RoomList /> should render a room list 1`] = `
class="mx_RoomListItemView mx_RoomListItemView_empty"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 192px; width: 100%;"
tabindex="-1"
type="button"
>
<div
@@ -283,6 +288,7 @@ exports[`<RoomList /> should render a room list 1`] = `
class="mx_RoomListItemView mx_RoomListItemView_empty"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 240px; width: 100%;"
tabindex="-1"
type="button"
>
<div
@@ -334,6 +340,7 @@ exports[`<RoomList /> should render a room list 1`] = `
class="mx_RoomListItemView mx_RoomListItemView_empty"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 288px; width: 100%;"
tabindex="-1"
type="button"
>
<div
@@ -385,6 +392,7 @@ exports[`<RoomList /> should render a room list 1`] = `
class="mx_RoomListItemView mx_RoomListItemView_empty"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 336px; width: 100%;"
tabindex="-1"
type="button"
>
<div
@@ -436,6 +444,7 @@ exports[`<RoomList /> should render a room list 1`] = `
class="mx_RoomListItemView mx_RoomListItemView_empty"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 384px; width: 100%;"
tabindex="-1"
type="button"
>
<div
@@ -487,6 +496,7 @@ exports[`<RoomList /> should render a room list 1`] = `
class="mx_RoomListItemView mx_RoomListItemView_empty"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 432px; width: 100%;"
tabindex="-1"
type="button"
>
<div

View File

@@ -6,6 +6,7 @@ exports[`<RoomListItemView /> should be selected if isSelected=true 1`] = `
aria-label="Open room room1"
aria-selected="true"
class="mx_RoomListItemView mx_RoomListItemView_empty mx_RoomListItemView_selected"
tabindex="-1"
type="button"
>
<div
@@ -60,6 +61,7 @@ exports[`<RoomListItemView /> should display notification decoration 1`] = `
aria-label="Open room room1"
aria-selected="false"
class="mx_RoomListItemView mx_RoomListItemView_notification_decoration"
tabindex="-1"
type="button"
>
<div
@@ -126,6 +128,7 @@ exports[`<RoomListItemView /> should render a room item 1`] = `
aria-label="Open room room1"
aria-selected="false"
class="mx_RoomListItemView mx_RoomListItemView_empty"
tabindex="-1"
type="button"
>
<div
@@ -180,6 +183,7 @@ exports[`<RoomListItemView /> should render a room item with a message preview 1
aria-label="Open room room1"
aria-selected="false"
class="mx_RoomListItemView mx_RoomListItemView_empty"
tabindex="-1"
type="button"
>
<div