Add tabs to the right panel (#12672)
* Create new method for header button behaviour With the introduction of tabs, the behaviour of the header buttons is changed as follows: - Close any right panel if open - Open the correct right panel if no panel was open before The old method (and behaviour) is retained as showOrHidePhase. * Implement tabs in the right panel There are three tabs: Info, People and Threads * Remove unwanted code from RoomSummaryCard - Remove the menu item for opening the memberlist since that is now taken of by the tabs. - Remove the close button * Remove code for focusing close button from tac item See https://github.com/matrix-org/matrix-react-sdk/pull/12410 There's no longer a close button to focus so we instead focus the thread tab. This is done in RightPaneltabs.tsx so we just need to remove this code. * Introduce a room info icon to the header This was previously present in the legacy room header but not in the new header. * BaseCard changes - Adds id, ariaLabelledBy and role props to implement tab accessibility. - Adds hideHeaderButtons prop to hide header buttons (think back and close buttons). - Change confusing header rendering code: header is not rendered ONLY when no header is passed AND hideHeaderButtons is true. * Refactor repeated code into function Created a new function createSpaceScopeHeader which returns the component if the room is a space room. Previously this code was duplicated in every component that uses SpaceScopeHeader component. * Pass BaseCard attributes and use helper function Actually using the code from the last two commits * Add, update and remove tests/screenshots/snapshots * Fix distance between search bar and tabs * Update compound * Update screenshots/snapshots
|
@ -78,7 +78,7 @@
|
|||
"@sentry/browser": "^8.0.0",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@vector-im/compound-design-tokens": "^1.2.0",
|
||||
"@vector-im/compound-web": "^5.1.2",
|
||||
"@vector-im/compound-web": "^5.2.3",
|
||||
"@zxcvbn-ts/core": "^3.0.4",
|
||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||
"@zxcvbn-ts/language-en": "^3.0.2",
|
||||
|
|
|
@ -103,7 +103,7 @@ const verify = async (page: Page, bob: Bot) => {
|
|||
const bobsVerificationRequestPromise = waitForVerificationRequest(bob);
|
||||
|
||||
const roomInfo = await openRoomInfo(page);
|
||||
await roomInfo.getByRole("menuitem", { name: "People" }).click();
|
||||
await page.locator(".mx_RightPanelTabs").getByText("People").click();
|
||||
await roomInfo.getByText("Bob").click();
|
||||
await roomInfo.getByRole("button", { name: "Verify" }).click();
|
||||
await roomInfo.getByRole("button", { name: "Start Verification" }).click();
|
||||
|
@ -279,7 +279,7 @@ test.describe("Cryptography", function () {
|
|||
|
||||
// Assert that verified icon is rendered
|
||||
await page.getByRole("button", { name: "Room members" }).click();
|
||||
await page.getByRole("button", { name: "Room information" }).click();
|
||||
await page.locator(".mx_RightPanelTabs").getByText("Info").click();
|
||||
await expect(page.locator('.mx_RoomSummaryCard_badges [data-kind="success"]')).toContainText("Encrypted");
|
||||
|
||||
// Take a snapshot of RoomSummaryCard with a verified E2EE icon
|
||||
|
|
|
@ -102,7 +102,7 @@ test.describe("Dehydration", () => {
|
|||
|
||||
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
||||
|
||||
await page.getByRole("menuitem", { name: "People" }).click();
|
||||
await page.locator(".mx_RightPanelTabs").getByText("People").click();
|
||||
await expect(page.locator(".mx_MemberList")).toBeVisible();
|
||||
|
||||
await getMemberTileByName(page, NAME).click();
|
||||
|
|
|
@ -80,7 +80,7 @@ test.describe("Lazy Loading", () => {
|
|||
|
||||
async function openMemberlist(page: Page): Promise<void> {
|
||||
await page.locator(".mx_LegacyRoomHeader").getByRole("button", { name: "Room info" }).click();
|
||||
await page.locator(".mx_RoomSummaryCard").getByRole("menuitem", { name: "People" }).click(); // \d represents the number of the room members
|
||||
await page.locator(".mx_RightPanelTabs").getByText("People").click();
|
||||
}
|
||||
|
||||
function getMemberInMemberlist(page: Page, name: string): Locator {
|
||||
|
|
|
@ -399,11 +399,10 @@ class Helpers {
|
|||
}
|
||||
|
||||
/**
|
||||
* Close the threads panel. (Actually, close any right panel, but for these
|
||||
* tests we only open the threads panel.)
|
||||
* Close the threads panel.
|
||||
*/
|
||||
async closeThreadsPanel() {
|
||||
await this.page.locator(".mx_RightPanel").getByLabel("Close").click();
|
||||
await this.page.locator(".mx_LegacyRoomHeader").getByLabel("Threads").click();
|
||||
await expect(this.page.locator(".mx_RightPanel")).not.toBeVisible();
|
||||
}
|
||||
|
||||
|
@ -411,7 +410,7 @@ class Helpers {
|
|||
* Return to the list of threads, given we are viewing a single thread.
|
||||
*/
|
||||
async backToThreadsList() {
|
||||
await this.page.locator(".mx_RightPanel").getByLabel("Threads").click();
|
||||
await this.page.locator(".mx_LegacyRoomHeader").getByLabel("Threads").click();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -113,7 +113,7 @@ test.describe("RightPanel", () => {
|
|||
test("should handle viewing room member", async ({ page, app }) => {
|
||||
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
||||
|
||||
await page.getByRole("menuitem", { name: "People" }).click();
|
||||
await page.locator(".mx_RightPanelTabs").getByText("People").click();
|
||||
await expect(page.locator(".mx_MemberList")).toBeVisible();
|
||||
|
||||
await getMemberTileByName(page, NAME).click();
|
||||
|
@ -123,7 +123,7 @@ test.describe("RightPanel", () => {
|
|||
await page.getByRole("button", { name: "Room members" }).click();
|
||||
await expect(page.locator(".mx_MemberList")).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Room information" }).click();
|
||||
await page.locator(".mx_RightPanelTabs").getByText("Info").click();
|
||||
await checkRoomSummaryCard(page, ROOM_NAME);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -337,12 +337,10 @@ export class Helpers {
|
|||
}
|
||||
|
||||
/**
|
||||
* Assert that the thread panel is focused (actually the 'close' button, specifically)
|
||||
* Assert that the thread tab is focused
|
||||
*/
|
||||
assertThreadPanelFocused() {
|
||||
return expect(
|
||||
this.page.locator(".mx_ThreadPanel").locator(".mx_BaseCard_header").getByLabel("Close"),
|
||||
).toBeFocused();
|
||||
assertThreadTabFocused() {
|
||||
return expect(this.page.locator("#thread-panel-tab")).toBeFocused();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -161,17 +161,12 @@ test.describe("Threads Activity Centre", () => {
|
|||
await util.assertNoTacIndicator();
|
||||
});
|
||||
|
||||
test("should focus the thread panel close button when clicking an item in the TAC", async ({
|
||||
room1,
|
||||
room2,
|
||||
util,
|
||||
msg,
|
||||
}) => {
|
||||
test("should focus the thread tab when clicking an item in the TAC", async ({ room1, room2, util, msg }) => {
|
||||
await util.receiveMessages(room1, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
|
||||
|
||||
await util.openTac();
|
||||
await util.clickRoomInTac(room1.name);
|
||||
|
||||
await util.assertThreadPanelFocused();
|
||||
await util.assertThreadTabFocused();
|
||||
});
|
||||
});
|
||||
|
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 29 KiB |
|
@ -261,6 +261,7 @@
|
|||
@import "./views/right_panel/_BaseCard.pcss";
|
||||
@import "./views/right_panel/_EncryptionInfo.pcss";
|
||||
@import "./views/right_panel/_PinnedMessagesCard.pcss";
|
||||
@import "./views/right_panel/_RightPanelTabs.pcss";
|
||||
@import "./views/right_panel/_RoomSummaryCard.pcss";
|
||||
@import "./views/right_panel/_ThreadPanel.pcss";
|
||||
@import "./views/right_panel/_TimelineCard.pcss";
|
||||
|
|
25
res/css/views/right_panel/_RightPanelTabs.pcss
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_RightPanelTabs {
|
||||
margin: 0;
|
||||
height: 64px;
|
||||
box-sizing: border-box;
|
||||
|
||||
ul {
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
|
@ -235,7 +235,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_RoomSummaryCard_header {
|
||||
padding: 15px 12px;
|
||||
padding: 24px 12px 15px;
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_search {
|
||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
margin-top: 24px;
|
||||
|
||||
.mx_Spinner {
|
||||
flex: 1 0 auto;
|
||||
|
|
|
@ -42,6 +42,7 @@ import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
|||
import { IRightPanelCard, IRightPanelCardState } from "../../stores/right-panel/RightPanelStoreIPanelState";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { XOR } from "../../@types/common";
|
||||
import { RightPanelTabs } from "../views/right_panel/RightPanelTabs";
|
||||
|
||||
interface BaseProps {
|
||||
overwriteCard?: IRightPanelCard; // used to display a custom card and ignoring the RightPanelStore (used for UserView)
|
||||
|
@ -171,6 +172,7 @@ export default class RightPanel extends React.Component<Props, IState> {
|
|||
<MemberList
|
||||
roomId={roomId}
|
||||
key={roomId}
|
||||
hideHeaderButtons
|
||||
onClose={this.onClose}
|
||||
searchQuery={this.state.searchQuery}
|
||||
onSearchQueryChanged={this.onSearchQueryChanged}
|
||||
|
@ -294,7 +296,6 @@ export default class RightPanel extends React.Component<Props, IState> {
|
|||
card = (
|
||||
<RoomSummaryCard
|
||||
room={this.props.room}
|
||||
onClose={this.onClose}
|
||||
// whenever RightPanel is passed a room it is passed a permalinkcreator
|
||||
permalinkCreator={this.props.permalinkCreator!}
|
||||
onSearchChange={this.props.onSearchChange}
|
||||
|
@ -314,6 +315,7 @@ export default class RightPanel extends React.Component<Props, IState> {
|
|||
|
||||
return (
|
||||
<aside className="mx_RightPanel" id="mx_RightPanel">
|
||||
{phase && <RightPanelTabs phase={phase} />}
|
||||
{card}
|
||||
</aside>
|
||||
);
|
||||
|
|
|
@ -1287,7 +1287,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
]);
|
||||
}
|
||||
} else {
|
||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.RoomMemberList);
|
||||
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomMemberList);
|
||||
}
|
||||
break;
|
||||
case Action.View3pidInvite:
|
||||
|
|
|
@ -37,9 +37,6 @@ import { ButtonEvent } from "../views/elements/AccessibleButton";
|
|||
import Spinner from "../views/elements/Spinner";
|
||||
import Heading from "../views/typography/Heading";
|
||||
import { clearRoomNotification } from "../../utils/notifications";
|
||||
import { useDispatcher } from "../../hooks/useDispatcher";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
|
@ -259,14 +256,6 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
|
|||
}
|
||||
}, [timelineSet, timelinePanel]);
|
||||
|
||||
useDispatcher(dis, (payload) => {
|
||||
// This actually foucses the close button on the threads panel, as its the only interactive element,
|
||||
// but at least it puts the user in the right area of the app.
|
||||
if (payload.action === Action.FocusThreadsPanel) {
|
||||
closeButonRef.current?.focus();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<RoomContext.Provider
|
||||
value={{
|
||||
|
@ -277,6 +266,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
|
|||
}}
|
||||
>
|
||||
<BaseCard
|
||||
hideHeaderButtons
|
||||
header={
|
||||
<ThreadPanelHeader
|
||||
filterOption={filterOption}
|
||||
|
@ -284,7 +274,10 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
|
|||
empty={!hasThreads}
|
||||
/>
|
||||
}
|
||||
id="thread-panel"
|
||||
className="mx_ThreadPanel"
|
||||
ariaLabelledBy="thread-panel-tab"
|
||||
role="tabpanel"
|
||||
onClose={onClose}
|
||||
withoutScrollContainer={true}
|
||||
ref={card}
|
||||
|
|
|
@ -26,8 +26,12 @@ import { CardContext } from "./context";
|
|||
|
||||
interface IProps {
|
||||
header?: ReactNode | null;
|
||||
hideHeaderButtons?: boolean;
|
||||
footer?: ReactNode;
|
||||
className?: string;
|
||||
id?: string;
|
||||
role?: "tabpanel";
|
||||
ariaLabelledBy?: string;
|
||||
withoutScrollContainer?: boolean;
|
||||
closeLabel?: string;
|
||||
onClose?(ev: ButtonEvent): void;
|
||||
|
@ -62,6 +66,10 @@ const BaseCard: React.FC<IProps> = forwardRef<HTMLDivElement, IProps>(
|
|||
onClose,
|
||||
onBack,
|
||||
className,
|
||||
id,
|
||||
ariaLabelledBy,
|
||||
role,
|
||||
hideHeaderButtons,
|
||||
header,
|
||||
footer,
|
||||
withoutScrollContainer,
|
||||
|
@ -100,13 +108,31 @@ const BaseCard: React.FC<IProps> = forwardRef<HTMLDivElement, IProps>(
|
|||
children = <AutoHideScrollbar>{children}</AutoHideScrollbar>;
|
||||
}
|
||||
|
||||
return (
|
||||
<CardContext.Provider value={{ isCard: true }}>
|
||||
<div className={classNames("mx_BaseCard", className)} ref={ref} onKeyDown={onKeyDown}>
|
||||
{header !== null && (
|
||||
<div className="mx_BaseCard_header">
|
||||
let headerButtons: React.ReactElement | undefined;
|
||||
if (!hideHeaderButtons) {
|
||||
headerButtons = (
|
||||
<>
|
||||
{backButton}
|
||||
{closeButton}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const shouldRenderHeader = header || !hideHeaderButtons;
|
||||
|
||||
return (
|
||||
<CardContext.Provider value={{ isCard: true }}>
|
||||
<div
|
||||
id={id}
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
role={role}
|
||||
className={classNames("mx_BaseCard", className)}
|
||||
ref={ref}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
{shouldRenderHeader && (
|
||||
<div className="mx_BaseCard_header">
|
||||
{headerButtons}
|
||||
<div className="mx_BaseCard_headerProp">{header}</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -214,27 +214,27 @@ export default class LegacyRoomHeaderButtons extends HeaderButtons<IProps> {
|
|||
const currentPhase = RightPanelStore.instance.currentCard.phase;
|
||||
if (currentPhase && ROOM_INFO_PHASES.includes(currentPhase)) {
|
||||
if (this.state.phase === currentPhase) {
|
||||
RightPanelStore.instance.showOrHidePanel(currentPhase);
|
||||
RightPanelStore.instance.showOrHidePhase(currentPhase);
|
||||
} else {
|
||||
RightPanelStore.instance.showOrHidePanel(currentPhase, RightPanelStore.instance.currentCard.state);
|
||||
RightPanelStore.instance.showOrHidePhase(currentPhase, RightPanelStore.instance.currentCard.state);
|
||||
}
|
||||
} else {
|
||||
// This toggles for us, if needed
|
||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.RoomSummary);
|
||||
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomSummary);
|
||||
}
|
||||
};
|
||||
|
||||
private onNotificationsClicked = (): void => {
|
||||
// This toggles for us, if needed
|
||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.NotificationPanel);
|
||||
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.NotificationPanel);
|
||||
};
|
||||
|
||||
private onPinnedMessagesClicked = (): void => {
|
||||
// This toggles for us, if needed
|
||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.PinnedMessages);
|
||||
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.PinnedMessages);
|
||||
};
|
||||
private onTimelineCardClicked = (): void => {
|
||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.Timeline);
|
||||
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.Timeline);
|
||||
};
|
||||
|
||||
private onThreadsPanelClicked = (ev: ButtonEvent): void => {
|
||||
|
|
86
src/components/views/right_panel/RightPanelTabs.tsx
Normal file
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useRef } from "react";
|
||||
import { NavBar, NavItem } from "@vector-im/compound-web";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import { useDispatcher } from "../../../hooks/useDispatcher";
|
||||
import dispatcher from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
|
||||
function shouldShowTabsForPhase(phase?: RightPanelPhases): boolean {
|
||||
const tabs = [RightPanelPhases.RoomSummary, RightPanelPhases.RoomMemberList, RightPanelPhases.ThreadPanel];
|
||||
return !!phase && tabs.includes(phase);
|
||||
}
|
||||
|
||||
type Props = {
|
||||
phase: RightPanelPhases;
|
||||
};
|
||||
|
||||
export const RightPanelTabs: React.FC<Props> = ({ phase }): JSX.Element | null => {
|
||||
const threadsTabRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
useDispatcher(dispatcher, (payload) => {
|
||||
// This actually focuses the threads tab, as its the only interactive element,
|
||||
// but at least it puts the user in the right area of the app.
|
||||
if (payload.action === Action.FocusThreadsPanel) {
|
||||
threadsTabRef.current?.focus();
|
||||
}
|
||||
});
|
||||
|
||||
if (!shouldShowTabsForPhase(phase)) return null;
|
||||
|
||||
return (
|
||||
<NavBar className="mx_RightPanelTabs" aria-label="right panel" role="tablist">
|
||||
<NavItem
|
||||
aria-controls="room-summary-panel"
|
||||
id="room-summary-panel-tab"
|
||||
onClick={() => {
|
||||
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.RoomSummary }, true);
|
||||
}}
|
||||
active={phase === RightPanelPhases.RoomSummary}
|
||||
>
|
||||
{_t("right_panel|info")}
|
||||
</NavItem>
|
||||
<NavItem
|
||||
aria-controls="memberlist-panel"
|
||||
id="memberlist-panel-tab"
|
||||
onClick={(ev: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.RoomMemberList }, true);
|
||||
PosthogTrackers.trackInteraction("WebRightPanelRoomInfoPeopleButton", ev);
|
||||
}}
|
||||
active={phase === RightPanelPhases.RoomMemberList}
|
||||
>
|
||||
{_t("common|people")}
|
||||
</NavItem>
|
||||
<NavItem
|
||||
aria-controls="thread-panel"
|
||||
id="thread-panel-tab"
|
||||
onClick={() => {
|
||||
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.ThreadPanel }, true);
|
||||
}}
|
||||
active={phase === RightPanelPhases.ThreadPanel}
|
||||
ref={threadsTabRef}
|
||||
>
|
||||
{_t("common|threads")}
|
||||
</NavItem>
|
||||
</NavBar>
|
||||
);
|
||||
};
|
|
@ -39,7 +39,6 @@ import {
|
|||
} from "@vector-im/compound-web";
|
||||
import { Icon as FavouriteIcon } from "@vector-im/compound-design-tokens/icons/favourite.svg";
|
||||
import { Icon as UserAddIcon } from "@vector-im/compound-design-tokens/icons/user-add.svg";
|
||||
import { Icon as UserProfileSolidIcon } from "@vector-im/compound-design-tokens/icons/user-profile-solid.svg";
|
||||
import { Icon as LinkIcon } from "@vector-im/compound-design-tokens/icons/link.svg";
|
||||
import { Icon as SettingsIcon } from "@vector-im/compound-design-tokens/icons/settings.svg";
|
||||
import { Icon as ExportArchiveIcon } from "@vector-im/compound-design-tokens/icons/export-archive.svg";
|
||||
|
@ -106,7 +105,6 @@ import { useTransition } from "../../../hooks/useTransition";
|
|||
interface IProps {
|
||||
room: Room;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
onClose(): void;
|
||||
onSearchChange?: (e: ChangeEvent) => void;
|
||||
onSearchCancel?: () => void;
|
||||
focusRoomSearch?: boolean;
|
||||
|
@ -382,7 +380,6 @@ const RoomTopic: React.FC<Pick<IProps, "room">> = ({ room }): JSX.Element | null
|
|||
const RoomSummaryCard: React.FC<IProps> = ({
|
||||
room,
|
||||
permalinkCreator,
|
||||
onClose,
|
||||
onSearchChange,
|
||||
onSearchCancel,
|
||||
focusRoomSearch,
|
||||
|
@ -416,11 +413,6 @@ const RoomSummaryCard: React.FC<IProps> = ({
|
|||
});
|
||||
};
|
||||
|
||||
const onRoomMembersClick = (ev: Event): void => {
|
||||
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.RoomMemberList }, true);
|
||||
PosthogTrackers.trackInteraction("WebRightPanelRoomInfoPeopleButton", ev);
|
||||
};
|
||||
|
||||
const isRoomEncrypted = useIsEncrypted(cli, room);
|
||||
const roomContext = useContext(RoomContext);
|
||||
const e2eStatus = roomContext.e2eStatus;
|
||||
|
@ -532,7 +524,13 @@ const RoomSummaryCard: React.FC<IProps> = ({
|
|||
const isFavorite = roomTags.includes(DefaultTagID.Favourite);
|
||||
|
||||
return (
|
||||
<BaseCard header={null} className="mx_RoomSummaryCard" onClose={onClose}>
|
||||
<BaseCard
|
||||
hideHeaderButtons
|
||||
id="room-summary-panel"
|
||||
className="mx_RoomSummaryCard"
|
||||
ariaLabelledBy="room-summary-panel-tab"
|
||||
role="tabpanel"
|
||||
>
|
||||
<Flex
|
||||
as="header"
|
||||
className="mx_RoomSummaryCard_header"
|
||||
|
@ -558,12 +556,6 @@ const RoomSummaryCard: React.FC<IProps> = ({
|
|||
/>
|
||||
</Form.Root>
|
||||
)}
|
||||
<AccessibleButton
|
||||
data-testid="base-card-close-button"
|
||||
className="mx_BaseCard_close"
|
||||
onClick={onClose}
|
||||
title={_t("action|close")}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
{header}
|
||||
|
@ -589,13 +581,6 @@ const RoomSummaryCard: React.FC<IProps> = ({
|
|||
<MenuItem Icon={SettingsIcon} label={_t("common|settings")} onSelect={onRoomSettingsClick} />
|
||||
|
||||
<Separator />
|
||||
<MenuItem
|
||||
// this icon matches the legacy implementation
|
||||
// and is a short term solution until legacy room header is removed
|
||||
Icon={UserProfileSolidIcon}
|
||||
label={_t("common|people")}
|
||||
onSelect={onRoomMembersClick}
|
||||
/>
|
||||
{!isVideoRoom && (
|
||||
<>
|
||||
<MenuItem Icon={FilesIcon} label={_t("right_panel|files_button")} onSelect={onRoomFilesClick} />
|
||||
|
|
|
@ -80,7 +80,7 @@ import { DirectoryMember, startDmOnFirstMessage } from "../../../utils/direct-me
|
|||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
import { asyncSome } from "../../../utils/arrays";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
import { SpaceScopeHeader } from "../rooms/SpaceScopeHeader";
|
||||
import { createSpaceScopeHeader } from "../rooms/SpaceScopeHeader";
|
||||
|
||||
export interface IDevice extends Device {
|
||||
ambiguous?: boolean;
|
||||
|
@ -1774,10 +1774,11 @@ const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPha
|
|||
<UserInfoHeader member={member} e2eStatus={e2eStatus} roomId={room?.roomId} />
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseCard
|
||||
className={classes.join(" ")}
|
||||
header={room ? <SpaceScopeHeader room={room} /> : undefined}
|
||||
header={createSpaceScopeHeader(room)}
|
||||
onClose={onClose}
|
||||
closeLabel={closeLabel}
|
||||
cardState={cardState}
|
||||
|
|
|
@ -55,7 +55,7 @@ import { SDKContext } from "../../../contexts/SDKContext";
|
|||
import { canInviteTo } from "../../../utils/room/canInviteTo";
|
||||
import { inviteToRoom } from "../../../utils/room/inviteToRoom";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { SpaceScopeHeader } from "./SpaceScopeHeader";
|
||||
import { createSpaceScopeHeader } from "./SpaceScopeHeader";
|
||||
|
||||
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||
const INITIAL_LOAD_NUM_INVITED = 5;
|
||||
|
@ -64,6 +64,7 @@ const SHOW_MORE_INCREMENT = 100;
|
|||
interface IProps {
|
||||
roomId: string;
|
||||
searchQuery: string;
|
||||
hideHeaderButtons?: boolean;
|
||||
onClose(): void;
|
||||
onSearchQueryChanged: (query: string) => void;
|
||||
}
|
||||
|
@ -358,7 +359,14 @@ export default class MemberList extends React.Component<IProps, IState> {
|
|||
public render(): React.ReactNode {
|
||||
if (this.state.loading) {
|
||||
return (
|
||||
<BaseCard className="mx_MemberList" onClose={this.props.onClose}>
|
||||
<BaseCard
|
||||
id="memberlist-panel"
|
||||
className="mx_MemberList"
|
||||
ariaLabelledBy="memberlist-panel-tab"
|
||||
role="tabpanel"
|
||||
hideHeaderButtons={this.props.hideHeaderButtons}
|
||||
onClose={this.props.onClose}
|
||||
>
|
||||
<Spinner />
|
||||
</BaseCard>
|
||||
);
|
||||
|
@ -415,12 +423,14 @@ export default class MemberList extends React.Component<IProps, IState> {
|
|||
/>
|
||||
);
|
||||
|
||||
const scopeHeader = room ? <SpaceScopeHeader room={room} /> : undefined;
|
||||
|
||||
return (
|
||||
<BaseCard
|
||||
id="memberlist-panel"
|
||||
className="mx_MemberList"
|
||||
header={<React.Fragment>{scopeHeader}</React.Fragment>}
|
||||
ariaLabelledBy="memberlist-panel-tab"
|
||||
role="tabpanel"
|
||||
header={createSpaceScopeHeader(room)}
|
||||
hideHeaderButtons={this.props.hideHeaderButtons}
|
||||
footer={footer}
|
||||
onClose={this.props.onClose}
|
||||
>
|
||||
|
|
|
@ -20,6 +20,7 @@ import { Icon as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/v
|
|||
import { Icon as VoiceCallIcon } from "@vector-im/compound-design-tokens/icons/voice-call.svg";
|
||||
import { Icon as CloseCallIcon } from "@vector-im/compound-design-tokens/icons/close.svg";
|
||||
import { Icon as ThreadsIcon } from "@vector-im/compound-design-tokens/icons/threads-solid.svg";
|
||||
import { Icon as RoomInfoIcon } from "@vector-im/compound-design-tokens/icons/info-solid.svg";
|
||||
import { Icon as NotificationsIcon } from "@vector-im/compound-design-tokens/icons/notifications-solid.svg";
|
||||
import { Icon as VerifiedIcon } from "@vector-im/compound-design-tokens/icons/verified.svg";
|
||||
import { Icon as ErrorIcon } from "@vector-im/compound-design-tokens/icons/error.svg";
|
||||
|
@ -336,6 +337,17 @@ export default function RoomHeader({
|
|||
</>
|
||||
)}
|
||||
|
||||
<Tooltip label={_t("right_panel|room_summary_card|title")}>
|
||||
<IconButton
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.RoomSummary);
|
||||
}}
|
||||
aria-label={_t("right_panel|room_summary_card|title")}
|
||||
>
|
||||
<RoomInfoIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip label={_t("common|threads")}>
|
||||
<IconButton
|
||||
indicator={notificationLevelToIndicator(threadNotifications)}
|
||||
|
|
|
@ -21,18 +21,23 @@ import { Text } from "@vector-im/compound-web";
|
|||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import { useRoomName } from "../../../hooks/useRoomName";
|
||||
|
||||
/**
|
||||
* Returns a space scope header if needed
|
||||
* @param room The room object
|
||||
* @returns rendered component if the room is a space room, otherwise returns null
|
||||
*/
|
||||
export function createSpaceScopeHeader(room?: Room | null): React.JSX.Element | null {
|
||||
if (room?.isSpaceRoom()) return <SpaceScopeHeader room={room} />;
|
||||
else return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope header used to decorate right panels that are scoped to a space.
|
||||
* When room is not a space renders nothing.
|
||||
* Otherwise renders room avatar and name.
|
||||
* It renders room avatar and name.
|
||||
*/
|
||||
export const SpaceScopeHeader: React.FC<{ room: Room }> = ({ room }) => {
|
||||
const roomName = useRoomName(room);
|
||||
|
||||
if (!room.isSpaceRoom()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
as="div"
|
||||
|
|
|
@ -28,7 +28,7 @@ import { Action } from "../../../dispatcher/actions";
|
|||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import BaseCard from "../right_panel/BaseCard";
|
||||
import { Flex } from "../../utils/Flex";
|
||||
import { SpaceScopeHeader } from "./SpaceScopeHeader";
|
||||
import { createSpaceScopeHeader } from "./SpaceScopeHeader";
|
||||
|
||||
interface IProps {
|
||||
event: MatrixEvent;
|
||||
|
@ -133,10 +133,8 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
|
|||
);
|
||||
}
|
||||
|
||||
const scopeHeader: JSX.Element | undefined = this.room ? <SpaceScopeHeader room={this.room} /> : undefined;
|
||||
|
||||
return (
|
||||
<BaseCard header={scopeHeader} onClose={this.props.onClose}>
|
||||
<BaseCard header={createSpaceScopeHeader(this.room)} onClose={this.props.onClose}>
|
||||
<Flex className="mx_ThirdPartyMemberInfo" direction="column" gap="var(--cpd-space-4x)">
|
||||
<Flex direction="column" as="section" justify="start" gap="var(--cpd-space-2x)">
|
||||
{/* same as userinfo name style */}
|
||||
|
|
|
@ -1833,6 +1833,7 @@
|
|||
"edit_integrations": "Edit widgets, bridges & bots",
|
||||
"export_chat_button": "Export chat",
|
||||
"files_button": "Files",
|
||||
"info": "Info",
|
||||
"pinned_messages": {
|
||||
"empty": "Nothing pinned, yet",
|
||||
"explainer": "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.",
|
||||
|
|
|
@ -236,6 +236,23 @@ export default class RightPanelStore extends ReadyWatchingStore {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the right panel is open, it is closed.
|
||||
* If the right panel is closed, it is opened with `phase`.
|
||||
*
|
||||
* This is different from showOrHidePhase which only closes the panel
|
||||
* if the panel was already showing the phase passed as argument.
|
||||
* @see showOrHidePhase
|
||||
* @param phase The right panel phase.
|
||||
*/
|
||||
public showOrHidePanel(phase: RightPanelPhases): void {
|
||||
if (!this.isOpen) {
|
||||
this.setCard({ phase });
|
||||
} else {
|
||||
this.togglePanel(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to show a right panel phase.
|
||||
* If the UI is already showing that phase, the right panel will be hidden.
|
||||
|
@ -245,7 +262,7 @@ export default class RightPanelStore extends ReadyWatchingStore {
|
|||
* @param phase The right panel phase.
|
||||
* @param cardState The state within the phase.
|
||||
*/
|
||||
public showOrHidePanel(phase: RightPanelPhases, cardState?: Partial<IRightPanelCardState>): void {
|
||||
public showOrHidePhase(phase: RightPanelPhases, cardState?: Partial<IRightPanelCardState>): void {
|
||||
if (this.currentCard.phase == phase && !cardState && this.isOpen) {
|
||||
this.togglePanel(null);
|
||||
} else {
|
||||
|
|
|
@ -32,6 +32,6 @@ export const onView3pidInvite = (payload: ActionPayload, rightPanelStore: RightP
|
|||
state: { memberInfoEvent: payload.event },
|
||||
});
|
||||
} else {
|
||||
rightPanelStore.showOrHidePanel(RightPanelPhases.RoomMemberList);
|
||||
rightPanelStore.showOrHidePhase(RightPanelPhases.RoomMemberList);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -36,8 +36,6 @@ import ResizeNotifier from "../../../src/utils/ResizeNotifier";
|
|||
import { createTestClient, getRoomContext, mkRoom, mockPlatformPeg, stubClient } from "../../test-utils";
|
||||
import { mkThread } from "../../test-utils/threads";
|
||||
import { IRoomState } from "../../../src/components/structures/RoomView";
|
||||
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../src/dispatcher/actions";
|
||||
|
||||
jest.mock("../../../src/utils/Feedback");
|
||||
|
||||
|
@ -148,43 +146,6 @@ describe("ThreadPanel", () => {
|
|||
fireEvent.click(getByRole(container, "button", { name: "Mark all as read" }));
|
||||
await waitFor(() => expect(mockClient.sendReadReceipt).not.toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it("focuses the close button on FocusThreadsPanel dispatch", () => {
|
||||
const ROOM_ID = "!roomId:example.org";
|
||||
|
||||
stubClient();
|
||||
mockPlatformPeg();
|
||||
const mockClient = mocked(MatrixClientPeg.safeGet());
|
||||
|
||||
const room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<RoomContext.Provider
|
||||
value={getRoomContext(room, {
|
||||
canSendMessages: true,
|
||||
})}
|
||||
>
|
||||
<ThreadPanel
|
||||
roomId={ROOM_ID}
|
||||
onClose={jest.fn()}
|
||||
resizeNotifier={new ResizeNotifier()}
|
||||
permalinkCreator={new RoomPermalinkCreator(room)}
|
||||
/>
|
||||
</RoomContext.Provider>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
// Unfocus it first so we know it's not just focused by coincidence
|
||||
screen.getByTestId("base-card-close-button").blur();
|
||||
expect(screen.getByTestId("base-card-close-button")).not.toHaveFocus();
|
||||
|
||||
defaultDispatcher.dispatch({ action: Action.FocusThreadsPanel }, true);
|
||||
|
||||
expect(screen.getByTestId("base-card-close-button")).toHaveFocus();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Filtering", () => {
|
||||
|
|
72
test/components/views/right_panel/RightPanelTabs-test.tsx
Normal file
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, fireEvent } from "@testing-library/react";
|
||||
|
||||
import dis from "../../../../src/dispatcher/dispatcher";
|
||||
import RightPanelStore from "../../../../src/stores/right-panel/RightPanelStore";
|
||||
import { RightPanelPhases } from "../../../../src/stores/right-panel/RightPanelStorePhases";
|
||||
import { RightPanelTabs } from "../../../../src/components/views/right_panel/RightPanelTabs";
|
||||
import { Action } from "../../../../src/dispatcher/actions";
|
||||
|
||||
describe("<RightPanelTabs />", () => {
|
||||
it("Component renders the correct tabs", () => {
|
||||
const { container, getByRole } = render(<RightPanelTabs phase={RightPanelPhases.RoomSummary} />);
|
||||
expect(container).toMatchSnapshot();
|
||||
|
||||
// We expect Info, People and Threads as tabs
|
||||
expect(getByRole("tab", { name: "Info" })).toBeDefined();
|
||||
expect(getByRole("tab", { name: "People" })).toBeDefined();
|
||||
expect(getByRole("tab", { name: "Threads" })).toBeDefined();
|
||||
});
|
||||
|
||||
it("Correct tab is active", () => {
|
||||
const { container } = render(<RightPanelTabs phase={RightPanelPhases.RoomMemberList} />);
|
||||
expect(container).toMatchSnapshot();
|
||||
// Assert that the active tab is Info
|
||||
expect(container.querySelectorAll("[aria-selected='true'").length).toEqual(1);
|
||||
expect(container.querySelector("[aria-selected='true'")).toHaveAccessibleName("People");
|
||||
});
|
||||
|
||||
it("Renders nothing for some phases, eg: FilePanel", () => {
|
||||
const { container } = render(<RightPanelTabs phase={RightPanelPhases.FilePanel} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it("onClick behaviors work as expected", () => {
|
||||
const spy = jest.spyOn(RightPanelStore.instance, "pushCard");
|
||||
const { getByRole } = render(<RightPanelTabs phase={RightPanelPhases.RoomSummary} />);
|
||||
|
||||
// Info -> People
|
||||
fireEvent.click(getByRole("tab", { name: "People" }));
|
||||
expect(spy).toHaveBeenLastCalledWith({ phase: RightPanelPhases.RoomMemberList }, true);
|
||||
|
||||
// People -> Threads
|
||||
fireEvent.click(getByRole("tab", { name: "Threads" }));
|
||||
expect(spy).toHaveBeenLastCalledWith({ phase: RightPanelPhases.ThreadPanel }, true);
|
||||
|
||||
// Threads -> Info
|
||||
fireEvent.click(getByRole("tab", { name: "Info" }));
|
||||
expect(spy).toHaveBeenLastCalledWith({ phase: RightPanelPhases.RoomSummary }, true);
|
||||
});
|
||||
|
||||
it("Threads tab is focused on action", () => {
|
||||
const { getByRole } = render(<RightPanelTabs phase={RightPanelPhases.ThreadPanel} />);
|
||||
dis.dispatch({ action: Action.FocusThreadsPanel }, true);
|
||||
expect(getByRole("tab", { name: "Threads" })).toHaveFocus();
|
||||
});
|
||||
});
|
|
@ -35,7 +35,6 @@ import { flushPromises, getMockClientWithEventEmitter, mockClientMethodsUser } f
|
|||
import { PollHistoryDialog } from "../../../../src/components/views/dialogs/PollHistoryDialog";
|
||||
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks";
|
||||
import { _t } from "../../../../src/languageHandler";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
import { tagRoom } from "../../../../src/utils/room/tagRoom";
|
||||
import { DefaultTagID } from "../../../../src/stores/room-list/models";
|
||||
import { Action } from "../../../../src/dispatcher/actions";
|
||||
|
@ -195,7 +194,6 @@ describe("<RoomSummaryCard />", () => {
|
|||
<RoomSummaryCard
|
||||
room={room}
|
||||
permalinkCreator={new RoomPermalinkCreator(room)}
|
||||
onClose={jest.fn()}
|
||||
onSearchChange={onSearchChange}
|
||||
focusRoomSearch={true}
|
||||
/>
|
||||
|
@ -212,7 +210,6 @@ describe("<RoomSummaryCard />", () => {
|
|||
<RoomSummaryCard
|
||||
room={room}
|
||||
permalinkCreator={new RoomPermalinkCreator(room)}
|
||||
onClose={jest.fn()}
|
||||
onSearchChange={onSearchChange}
|
||||
/>
|
||||
</RoomContext.Provider>
|
||||
|
@ -270,18 +267,6 @@ describe("<RoomSummaryCard />", () => {
|
|||
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: "open_room_settings" });
|
||||
});
|
||||
|
||||
it("renders room members options when new room UI is not enabled", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
||||
const { getByText } = getComponent();
|
||||
|
||||
fireEvent.click(getByText(_t("common|people")));
|
||||
|
||||
expect(RightPanelStore.instance.pushCard).toHaveBeenCalledWith(
|
||||
{ phase: RightPanelPhases.RoomMemberList },
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
describe("pinning", () => {
|
||||
it("renders pins options when pinning feature is enabled", () => {
|
||||
mocked(settingsHooks.useFeatureEnabled).mockImplementation((feature) => feature === "feature_pinning");
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<RightPanelTabs /> Component renders the correct tabs 1`] = `
|
||||
<div>
|
||||
<nav
|
||||
class="mx_RightPanelTabs _nav-bar_135dy_16"
|
||||
role="presentation"
|
||||
>
|
||||
<ul
|
||||
aria-label="right panel"
|
||||
class="_nav-bar-items_135dy_22"
|
||||
role="tablist"
|
||||
>
|
||||
<li
|
||||
class="_nav-tab_135dy_33"
|
||||
data-current="true"
|
||||
role="presentation"
|
||||
>
|
||||
<button
|
||||
aria-controls="room-summary-panel"
|
||||
aria-selected="true"
|
||||
class="_nav-item_135dy_55"
|
||||
id="room-summary-panel-tab"
|
||||
role="tab"
|
||||
>
|
||||
Info
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
class="_nav-tab_135dy_33"
|
||||
role="presentation"
|
||||
>
|
||||
<button
|
||||
aria-controls="memberlist-panel"
|
||||
aria-selected="false"
|
||||
class="_nav-item_135dy_55"
|
||||
id="memberlist-panel-tab"
|
||||
role="tab"
|
||||
>
|
||||
People
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
class="_nav-tab_135dy_33"
|
||||
role="presentation"
|
||||
>
|
||||
<button
|
||||
aria-controls="thread-panel"
|
||||
aria-selected="false"
|
||||
class="_nav-item_135dy_55"
|
||||
id="thread-panel-tab"
|
||||
role="tab"
|
||||
>
|
||||
Threads
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<RightPanelTabs /> Correct tab is active 1`] = `
|
||||
<div>
|
||||
<nav
|
||||
class="mx_RightPanelTabs _nav-bar_135dy_16"
|
||||
role="presentation"
|
||||
>
|
||||
<ul
|
||||
aria-label="right panel"
|
||||
class="_nav-bar-items_135dy_22"
|
||||
role="tablist"
|
||||
>
|
||||
<li
|
||||
class="_nav-tab_135dy_33"
|
||||
role="presentation"
|
||||
>
|
||||
<button
|
||||
aria-controls="room-summary-panel"
|
||||
aria-selected="false"
|
||||
class="_nav-item_135dy_55"
|
||||
id="room-summary-panel-tab"
|
||||
role="tab"
|
||||
>
|
||||
Info
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
class="_nav-tab_135dy_33"
|
||||
data-current="true"
|
||||
role="presentation"
|
||||
>
|
||||
<button
|
||||
aria-controls="memberlist-panel"
|
||||
aria-selected="true"
|
||||
class="_nav-item_135dy_55"
|
||||
id="memberlist-panel-tab"
|
||||
role="tab"
|
||||
>
|
||||
People
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
class="_nav-tab_135dy_33"
|
||||
role="presentation"
|
||||
>
|
||||
<button
|
||||
aria-controls="thread-panel"
|
||||
aria-selected="false"
|
||||
class="_nav-item_135dy_55"
|
||||
id="thread-panel-tab"
|
||||
role="tab"
|
||||
>
|
||||
Threads
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
`;
|
|
@ -3,7 +3,10 @@
|
|||
exports[`<RoomSummaryCard /> has button to edit topic when expanded 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-labelledby="room-summary-panel-tab"
|
||||
class="mx_BaseCard mx_RoomSummaryCard"
|
||||
id="room-summary-panel"
|
||||
role="tabpanel"
|
||||
>
|
||||
<div
|
||||
class="mx_AutoHideScrollbar"
|
||||
|
@ -12,15 +15,7 @@ exports[`<RoomSummaryCard /> has button to edit topic when expanded 1`] = `
|
|||
<header
|
||||
class="mx_Flex mx_RoomSummaryCard_header"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
>
|
||||
<div
|
||||
aria-label="Close"
|
||||
class="mx_AccessibleButton mx_BaseCard_close"
|
||||
data-testid="base-card-close-button"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</header>
|
||||
<header
|
||||
class="mx_RoomSummaryCard_container"
|
||||
>
|
||||
|
@ -244,36 +239,6 @@ exports[`<RoomSummaryCard /> has button to edit topic when expanded 1`] = `
|
|||
data-orientation="horizontal"
|
||||
role="separator"
|
||||
/>
|
||||
<button
|
||||
class="_item_1gwvj_17 _interactive_1gwvj_36"
|
||||
data-kind="primary"
|
||||
role="menuitem"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="_icon_1gwvj_44"
|
||||
height="24"
|
||||
width="24"
|
||||
/>
|
||||
<span
|
||||
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_1gwvj_53"
|
||||
>
|
||||
People
|
||||
</span>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_nav-hint_1gwvj_60"
|
||||
fill="currentColor"
|
||||
height="24"
|
||||
viewBox="8 0 8 24"
|
||||
width="8"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.7 17.3a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l3.9-3.9-3.9-3.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.6 4.6c.1.1.17.208.213.325.041.117.062.242.062.375s-.02.258-.063.375a.876.876 0 0 1-.212.325l-4.6 4.6a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="_item_1gwvj_17 _interactive_1gwvj_36"
|
||||
data-kind="primary"
|
||||
|
@ -423,7 +388,10 @@ exports[`<RoomSummaryCard /> has button to edit topic when expanded 1`] = `
|
|||
exports[`<RoomSummaryCard /> renders the room summary 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-labelledby="room-summary-panel-tab"
|
||||
class="mx_BaseCard mx_RoomSummaryCard"
|
||||
id="room-summary-panel"
|
||||
role="tabpanel"
|
||||
>
|
||||
<div
|
||||
class="mx_AutoHideScrollbar"
|
||||
|
@ -432,15 +400,7 @@ exports[`<RoomSummaryCard /> renders the room summary 1`] = `
|
|||
<header
|
||||
class="mx_Flex mx_RoomSummaryCard_header"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
>
|
||||
<div
|
||||
aria-label="Close"
|
||||
class="mx_AccessibleButton mx_BaseCard_close"
|
||||
data-testid="base-card-close-button"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</header>
|
||||
<header
|
||||
class="mx_RoomSummaryCard_container"
|
||||
>
|
||||
|
@ -637,36 +597,6 @@ exports[`<RoomSummaryCard /> renders the room summary 1`] = `
|
|||
data-orientation="horizontal"
|
||||
role="separator"
|
||||
/>
|
||||
<button
|
||||
class="_item_1gwvj_17 _interactive_1gwvj_36"
|
||||
data-kind="primary"
|
||||
role="menuitem"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="_icon_1gwvj_44"
|
||||
height="24"
|
||||
width="24"
|
||||
/>
|
||||
<span
|
||||
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_1gwvj_53"
|
||||
>
|
||||
People
|
||||
</span>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_nav-hint_1gwvj_60"
|
||||
fill="currentColor"
|
||||
height="24"
|
||||
viewBox="8 0 8 24"
|
||||
width="8"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.7 17.3a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l3.9-3.9-3.9-3.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.6 4.6c.1.1.17.208.213.325.041.117.062.242.062.375s-.02.258-.063.375a.876.876 0 0 1-.212.325l-4.6 4.6a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="_item_1gwvj_17 _interactive_1gwvj_36"
|
||||
data-kind="primary"
|
||||
|
@ -816,7 +746,10 @@ exports[`<RoomSummaryCard /> renders the room summary 1`] = `
|
|||
exports[`<RoomSummaryCard /> renders the room topic in the summary 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-labelledby="room-summary-panel-tab"
|
||||
class="mx_BaseCard mx_RoomSummaryCard"
|
||||
id="room-summary-panel"
|
||||
role="tabpanel"
|
||||
>
|
||||
<div
|
||||
class="mx_AutoHideScrollbar"
|
||||
|
@ -825,15 +758,7 @@ exports[`<RoomSummaryCard /> renders the room topic in the summary 1`] = `
|
|||
<header
|
||||
class="mx_Flex mx_RoomSummaryCard_header"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x);"
|
||||
>
|
||||
<div
|
||||
aria-label="Close"
|
||||
class="mx_AccessibleButton mx_BaseCard_close"
|
||||
data-testid="base-card-close-button"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</header>
|
||||
<header
|
||||
class="mx_RoomSummaryCard_container"
|
||||
>
|
||||
|
@ -1041,36 +966,6 @@ exports[`<RoomSummaryCard /> renders the room topic in the summary 1`] = `
|
|||
data-orientation="horizontal"
|
||||
role="separator"
|
||||
/>
|
||||
<button
|
||||
class="_item_1gwvj_17 _interactive_1gwvj_36"
|
||||
data-kind="primary"
|
||||
role="menuitem"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="_icon_1gwvj_44"
|
||||
height="24"
|
||||
width="24"
|
||||
/>
|
||||
<span
|
||||
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_1gwvj_53"
|
||||
>
|
||||
People
|
||||
</span>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_nav-hint_1gwvj_60"
|
||||
fill="currentColor"
|
||||
height="24"
|
||||
viewBox="8 0 8 24"
|
||||
width="8"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.7 17.3a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l3.9-3.9-3.9-3.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.6 4.6c.1.1.17.208.213.325.041.117.062.242.062.375s-.02.258-.063.375a.876.876 0 0 1-.212.325l-4.6 4.6a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="_item_1gwvj_17 _interactive_1gwvj_36"
|
||||
data-kind="primary"
|
||||
|
|
|
@ -216,6 +216,13 @@ describe("RoomHeader", () => {
|
|||
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomMemberList });
|
||||
});
|
||||
|
||||
it("has room info icon that opens the room info panel", async () => {
|
||||
const { getAllByRole } = render(<RoomHeader room={room} />, getWrapper());
|
||||
const infoButton = getAllByRole("button", { name: "Room info" })[1];
|
||||
fireEvent.click(infoButton);
|
||||
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomSummary });
|
||||
});
|
||||
|
||||
it("opens the thread panel", async () => {
|
||||
const { container } = render(<RoomHeader room={room} />, getWrapper());
|
||||
|
||||
|
|
|
@ -73,6 +73,20 @@ exports[`RoomHeader does not show the face pile for DMs 1`] = `
|
|||
<div />
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
class="_icon-button_rijzz_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<div />
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
class="_icon-button_rijzz_17"
|
||||
|
|
|
@ -225,4 +225,22 @@ describe("RightPanelStore", () => {
|
|||
await viewRoom("!1:example.org");
|
||||
expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.RoomMemberList);
|
||||
});
|
||||
|
||||
it("showOrHidePhase works as expected", async () => {
|
||||
await viewRoom("!1:example.org");
|
||||
|
||||
// Open the memberlist panel
|
||||
store.showOrHidePanel(RightPanelPhases.RoomMemberList);
|
||||
expect(store.isOpenForRoom("!1:example.org")).toEqual(true);
|
||||
expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.RoomMemberList);
|
||||
|
||||
// showOrHide with RoomSummary should now close the panel
|
||||
store.showOrHidePanel(RightPanelPhases.RoomSummary);
|
||||
expect(store.isOpenForRoom("!1:example.org")).toEqual(false);
|
||||
|
||||
// showOrHide with RoomSummary should now open the panel
|
||||
store.showOrHidePanel(RightPanelPhases.RoomSummary);
|
||||
expect(store.isOpenForRoom("!1:example.org")).toEqual(true);
|
||||
expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.RoomSummary);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,7 +28,7 @@ describe("onView3pidInvite()", () => {
|
|||
beforeEach(() => {
|
||||
rightPanelStore = {
|
||||
pushCard: jest.fn(),
|
||||
showOrHidePanel: jest.fn(),
|
||||
showOrHidePhase: jest.fn(),
|
||||
} as unknown as MockedObject<RightPanelStore>;
|
||||
});
|
||||
|
||||
|
@ -38,7 +38,7 @@ describe("onView3pidInvite()", () => {
|
|||
};
|
||||
onView3pidInvite(payload, rightPanelStore);
|
||||
|
||||
expect(rightPanelStore.showOrHidePanel).toHaveBeenCalledWith(RightPanelPhases.RoomMemberList);
|
||||
expect(rightPanelStore.showOrHidePhase).toHaveBeenCalledWith(RightPanelPhases.RoomMemberList);
|
||||
expect(rightPanelStore.pushCard).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
@ -49,7 +49,7 @@ describe("onView3pidInvite()", () => {
|
|||
};
|
||||
onView3pidInvite(payload, rightPanelStore);
|
||||
|
||||
expect(rightPanelStore.showOrHidePanel).not.toHaveBeenCalled();
|
||||
expect(rightPanelStore.showOrHidePhase).not.toHaveBeenCalled();
|
||||
expect(rightPanelStore.pushCard).toHaveBeenCalledWith({
|
||||
phase: RightPanelPhases.Room3pidMemberInfo,
|
||||
state: { memberInfoEvent: payload.event },
|
||||
|
|
45
yarn.lock
|
@ -3040,10 +3040,10 @@
|
|||
dependencies:
|
||||
svg2vectordrawable "^2.9.1"
|
||||
|
||||
"@vector-im/compound-web@^5.1.2":
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-5.1.2.tgz#2ee3d859819e153c898770c058a4277c0b8ef3b8"
|
||||
integrity sha512-p7ide2JRblCkcSMPNakkWjK9GxA8boMQCEgXCT7Dp+owhONf2QsYpyRzlW+tPZ3DULd+h4nqWRova4uSeZtBbA==
|
||||
"@vector-im/compound-web@^5.2.3":
|
||||
version "5.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-5.2.3.tgz#feab8ae7623cfaa243b9be69325e1696bfa1a09c"
|
||||
integrity sha512-KU5vAgNIFBzRHfCRK5dGAhxjrfkrUXeOYzDUNc2QjEnqGaUR3RM4c53sw0Ga1oHbOeAWoUGId+ptH3ewPdUTAQ==
|
||||
dependencies:
|
||||
"@floating-ui/react" "^0.26.9"
|
||||
"@floating-ui/react-dom" "^2.0.8"
|
||||
|
@ -3054,6 +3054,7 @@
|
|||
"@radix-ui/react-slot" "^1.0.2"
|
||||
"@radix-ui/react-tooltip" "^1.0.6"
|
||||
classnames "^2.3.2"
|
||||
ts-xor "^1.3.0"
|
||||
vaul "^0.7.0"
|
||||
|
||||
"@zxcvbn-ts/core@^3.0.4":
|
||||
|
@ -8475,16 +8476,7 @@ string-length@^4.0.1:
|
|||
char-regex "^1.0.2"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
|
@ -8571,14 +8563,7 @@ string_decoder@~1.1.1:
|
|||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
|
@ -8921,6 +8906,11 @@ ts-node@^10.9.1:
|
|||
v8-compile-cache-lib "^3.0.1"
|
||||
yn "3.1.1"
|
||||
|
||||
ts-xor@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-xor/-/ts-xor-1.3.0.tgz#3e59f24f0321f9f10f350e0cee3b534b89a2c70b"
|
||||
integrity sha512-RLXVjliCzc1gfKQFLRpfeD0rrWmjnSTgj7+RFhoq3KRkUYa8LE/TIidYOzM5h+IdFBDSjjSgk9Lto9sdMfDFEA==
|
||||
|
||||
tsconfig-paths@^3.15.0:
|
||||
version "3.15.0"
|
||||
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4"
|
||||
|
@ -9372,7 +9362,7 @@ which@^2.0.1:
|
|||
dependencies:
|
||||
isexe "^2.0.0"
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
|
@ -9390,15 +9380,6 @@ wrap-ansi@^6.2.0:
|
|||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||
|
|