* 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
121 lines
4.2 KiB
TypeScript
121 lines
4.2 KiB
TypeScript
/*
|
|
Copyright 2024 New Vector Ltd.
|
|
|
|
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 { Form } from "@vector-im/compound-web";
|
|
import React, { type JSX, useCallback } from "react";
|
|
|
|
import { Flex } from "../../../../shared-components/utils/Flex";
|
|
import {
|
|
type MemberWithSeparator,
|
|
SEPARATOR,
|
|
useMemberListViewModel,
|
|
} from "../../../viewmodels/memberlist/MemberListViewModel";
|
|
import { RoomMemberTileView } from "./tiles/RoomMemberTileView";
|
|
import { ThreePidInviteTileView } from "./tiles/ThreePidInviteTileView";
|
|
import { MemberListHeaderView } from "./MemberListHeaderView";
|
|
import BaseCard from "../../right_panel/BaseCard";
|
|
import { _t } from "../../../../languageHandler";
|
|
import { type ListContext, ListView } from "../../../utils/ListView";
|
|
|
|
interface IProps {
|
|
roomId: string;
|
|
onClose: () => void;
|
|
}
|
|
|
|
const MemberListView: React.FC<IProps> = (props: IProps) => {
|
|
const vm = useMemberListViewModel(props.roomId);
|
|
const { isPresenceEnabled, onClickMember, memberCount } = vm;
|
|
|
|
const getItemKey = useCallback((item: MemberWithSeparator): string => {
|
|
if (item === SEPARATOR) {
|
|
return "separator";
|
|
} else if (item.member) {
|
|
return `member-${item.member.userId}`;
|
|
} else {
|
|
return `threePidInvite-${item.threePidInvite.event.getContent().public_key}`;
|
|
}
|
|
}, []);
|
|
|
|
const getItemComponent = useCallback(
|
|
(index: number, item: MemberWithSeparator, context: ListContext<any>): JSX.Element => {
|
|
const itemKey = getItemKey(item);
|
|
const isRovingItem = itemKey === context.tabIndexKey;
|
|
const focused = isRovingItem && context.focused;
|
|
if (item === SEPARATOR) {
|
|
return <hr className="mx_MemberListView_separator" />;
|
|
} else if (item.member) {
|
|
return (
|
|
<RoomMemberTileView
|
|
member={item.member}
|
|
showPresence={isPresenceEnabled}
|
|
focused={focused}
|
|
tabIndex={isRovingItem ? 0 : -1}
|
|
index={index}
|
|
memberCount={memberCount}
|
|
/>
|
|
);
|
|
} else {
|
|
return (
|
|
<ThreePidInviteTileView
|
|
threePidInvite={item.threePidInvite}
|
|
focused={focused}
|
|
tabIndex={isRovingItem ? 0 : -1}
|
|
memberIndex={index - 1} // Adjust as invites are below the separator
|
|
memberCount={memberCount}
|
|
/>
|
|
);
|
|
}
|
|
},
|
|
[isPresenceEnabled, getItemKey, memberCount],
|
|
);
|
|
|
|
const handleSelectItem = useCallback(
|
|
(item: MemberWithSeparator): void => {
|
|
if (item !== SEPARATOR) {
|
|
if (item.member) {
|
|
onClickMember(item.member);
|
|
} else {
|
|
onClickMember(item.threePidInvite);
|
|
}
|
|
}
|
|
},
|
|
[onClickMember],
|
|
);
|
|
|
|
const isItemFocusable = useCallback((item: MemberWithSeparator): boolean => {
|
|
return item !== SEPARATOR;
|
|
}, []);
|
|
|
|
return (
|
|
<BaseCard
|
|
id="memberlist-panel"
|
|
className="mx_MemberListView"
|
|
ariaLabelledBy="memberlist-panel-tab"
|
|
role="tabpanel"
|
|
header={_t("common|people")}
|
|
onClose={props.onClose}
|
|
>
|
|
<Flex align="stretch" direction="column" className="mx_MemberListView_container">
|
|
<Form.Root onSubmit={(e) => e.preventDefault()}>
|
|
<MemberListHeaderView vm={vm} />
|
|
</Form.Root>
|
|
<ListView
|
|
items={vm.members}
|
|
onSelectItem={handleSelectItem}
|
|
getItemComponent={getItemComponent}
|
|
getItemKey={getItemKey}
|
|
isItemFocusable={isItemFocusable}
|
|
role="listbox"
|
|
aria-label={_t("member_list|list_title")}
|
|
/>
|
|
</Flex>
|
|
</BaseCard>
|
|
);
|
|
};
|
|
|
|
export default MemberListView;
|