* implement basic scrolling and keyboard navigation * Update focus style and improve keyboard navigation * lint * Use avatar tootltip for the title rather than the whole button It's more performant and feels less glitchy than the button tooltip moving around when you scroll. * lint * Add tooltip for invite buttons active state As we have for other icon based buttons in the right panel/app * Fix location of scrollToIndex and add useCallback * Improve voiceover experience - As well as stylng cells, set the tabIndex(roving) - Natively focus the div with .focus() so screen reader actually moves over the cells - improve labels and roles * Fix jest tests * Add aria index/counts and remove repeating "Open" string in label * update snapshot * Add the rest of the keyboard navigation and handle the case when the list looses focus. * lint and update snapshot * lint * Only focus first/lastFocsed cell if focus.currentTarget is the overall list. So it isn't erroneously called during onClick of an item. * Put back overscan and fix formatting * Extract ListView out of MemberList * lint and fix e2e test * Update screenshot It looks like it is slightly better center aligned in the new list, as if maybe it was 1 px to high with the old one. * Fix default overscan value and add ListView tests * Just leave the avatar as it was * We removed the tooltip that showed power level. Removing string. * Use key rather than index to track focus. * Remove overscan, fix typos, fix scrollToItem logic * Use listbox role for member list and correct position/count values to account for the separator * Fix inadvertant scrolling of the timeline when using pageUp/pageDown * Always set the roving tab index regardless of whether we are actually focused. Fixes the issue of not being able to shift+t * Add aria-hidden to items within the option to avoid the SR calling it a group. Also * Make sure there is a roving tab set if the last one has been removed from the list. * Update snapshot
163 lines
5.8 KiB
TypeScript
163 lines
5.8 KiB
TypeScript
/*
|
|
Copyright 2024 New Vector Ltd.
|
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
Please see LICENSE files in the repository root for full details.
|
|
*/
|
|
|
|
import React, { act } from "react";
|
|
import { render, type RenderResult, waitFor } from "jest-matrix-react";
|
|
import { VirtuosoMockContext } from "react-virtuoso";
|
|
import {
|
|
Room,
|
|
type MatrixClient,
|
|
type RoomState,
|
|
RoomMember,
|
|
User,
|
|
EventType,
|
|
RoomStateEvent,
|
|
} from "matrix-js-sdk/src/matrix";
|
|
import { KnownMembership } from "matrix-js-sdk/src/types";
|
|
|
|
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
|
|
import * as TestUtils from "../../../../../test-utils";
|
|
import { SDKContext } from "../../../../../../src/contexts/SDKContext";
|
|
import { TestSdkContext } from "../../../../TestSdkContext";
|
|
import MemberListView from "../../../../../../src/components/views/rooms/MemberList/MemberListView";
|
|
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
|
|
|
export function createRoom(client: MatrixClient, opts = {}) {
|
|
const roomId = "!" + Math.random().toString().slice(2, 10) + ":domain";
|
|
const room = new Room(roomId, client, client.getUserId()!);
|
|
room.updateMyMembership(KnownMembership.Join);
|
|
if (opts) {
|
|
Object.assign(room, opts);
|
|
}
|
|
return room;
|
|
}
|
|
|
|
export type Rendered = {
|
|
client: MatrixClient;
|
|
root: RenderResult;
|
|
memberListRoom: Room;
|
|
adminUsers: RoomMember[];
|
|
moderatorUsers: RoomMember[];
|
|
defaultUsers: RoomMember[];
|
|
reRender: () => Promise<void>;
|
|
};
|
|
|
|
export async function renderMemberList(
|
|
enablePresence: boolean,
|
|
roomSetup?: (room: Room) => void,
|
|
usersPerLevel: number = 2,
|
|
): Promise<Rendered> {
|
|
TestUtils.stubClient();
|
|
const client = MatrixClientPeg.safeGet();
|
|
client.hasLazyLoadMembersEnabled = () => false;
|
|
|
|
// Make room
|
|
const memberListRoom = createRoom(client);
|
|
expect(memberListRoom.roomId).toBeTruthy();
|
|
|
|
// Give the test an opportunity to make changes to room before first render
|
|
roomSetup?.(memberListRoom);
|
|
|
|
// Make users
|
|
const adminUsers = [];
|
|
const moderatorUsers = [];
|
|
const defaultUsers = [];
|
|
for (let i = 0; i < usersPerLevel; i++) {
|
|
const adminUser = new RoomMember(memberListRoom.roomId, `@admin${i}:localhost`);
|
|
adminUser.membership = KnownMembership.Join;
|
|
adminUser.powerLevel = 100;
|
|
adminUser.user = User.createUser(adminUser.userId, client);
|
|
adminUser.user.currentlyActive = true;
|
|
adminUser.user.presence = "online";
|
|
adminUser.user.lastPresenceTs = 1000;
|
|
adminUser.user.lastActiveAgo = 10;
|
|
adminUsers.push(adminUser);
|
|
|
|
const moderatorUser = new RoomMember(memberListRoom.roomId, `@moderator${i}:localhost`);
|
|
moderatorUser.membership = KnownMembership.Join;
|
|
moderatorUser.powerLevel = 50;
|
|
moderatorUser.user = User.createUser(moderatorUser.userId, client);
|
|
moderatorUser.user.currentlyActive = true;
|
|
moderatorUser.user.presence = "online";
|
|
moderatorUser.user.lastPresenceTs = 1000;
|
|
moderatorUser.user.lastActiveAgo = 10;
|
|
moderatorUsers.push(moderatorUser);
|
|
|
|
const defaultUser = new RoomMember(memberListRoom.roomId, `@default${i}:localhost`);
|
|
defaultUser.membership = KnownMembership.Join;
|
|
defaultUser.powerLevel = 0;
|
|
defaultUser.user = User.createUser(defaultUser.userId, client);
|
|
defaultUser.user.currentlyActive = true;
|
|
defaultUser.user.presence = "online";
|
|
defaultUser.user.lastPresenceTs = 1000;
|
|
defaultUser.user.lastActiveAgo = 10;
|
|
defaultUsers.push(defaultUser);
|
|
}
|
|
|
|
client.getRoom = (roomId) => {
|
|
if (roomId === memberListRoom.roomId) return memberListRoom;
|
|
else return null;
|
|
};
|
|
memberListRoom.currentState = {
|
|
members: {},
|
|
getMember: jest.fn(),
|
|
getStateEvents: ((eventType, stateKey) => (stateKey === undefined ? [] : null)) as RoomState["getStateEvents"], // ignore 3pid invites
|
|
} as unknown as RoomState;
|
|
for (const member of [...adminUsers, ...moderatorUsers, ...defaultUsers]) {
|
|
memberListRoom.currentState.members[member.userId] = member;
|
|
}
|
|
|
|
const context = new TestSdkContext();
|
|
context.client = client;
|
|
context.memberListStore.isPresenceEnabled = jest.fn().mockReturnValue(enablePresence);
|
|
const root = render(
|
|
<MatrixClientContext.Provider value={client}>
|
|
<SDKContext.Provider value={context}>
|
|
<MemberListView roomId={memberListRoom.roomId} onClose={() => {}} />
|
|
</SDKContext.Provider>
|
|
</MatrixClientContext.Provider>,
|
|
{
|
|
wrapper: ({ children }) => (
|
|
<VirtuosoMockContext.Provider value={{ viewportHeight: 600, itemHeight: 56 }}>
|
|
{children}
|
|
</VirtuosoMockContext.Provider>
|
|
),
|
|
},
|
|
);
|
|
await waitFor(async () => {
|
|
expect(root.container.querySelectorAll(".mx_MemberTileView")).toHaveLength(usersPerLevel * 3);
|
|
});
|
|
|
|
const reRender = createReRenderFunction(client, memberListRoom);
|
|
|
|
return {
|
|
client,
|
|
root,
|
|
memberListRoom,
|
|
adminUsers,
|
|
moderatorUsers,
|
|
defaultUsers,
|
|
reRender,
|
|
};
|
|
}
|
|
|
|
function createReRenderFunction(client: MatrixClient, memberListRoom: Room): Rendered["reRender"] {
|
|
return async function (): Promise<void> {
|
|
await act(async () => {
|
|
//@ts-ignore
|
|
client.emit(RoomStateEvent.Events, {
|
|
//@ts-ignore
|
|
getType: () => EventType.RoomThirdPartyInvite,
|
|
getRoomId: () => memberListRoom.roomId,
|
|
});
|
|
});
|
|
await new Promise((r) => setTimeout(r, 1000));
|
|
};
|
|
}
|