diff --git a/package.json b/package.json index 7169afdc6e..9741734d06 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts index 995c37d358..98f75d54e1 100644 --- a/playwright/e2e/crypto/crypto.spec.ts +++ b/playwright/e2e/crypto/crypto.spec.ts @@ -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 diff --git a/playwright/e2e/crypto/dehydration.spec.ts b/playwright/e2e/crypto/dehydration.spec.ts index 13da99ccad..eb9efde4ee 100644 --- a/playwright/e2e/crypto/dehydration.spec.ts +++ b/playwright/e2e/crypto/dehydration.spec.ts @@ -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(); diff --git a/playwright/e2e/lazy-loading/lazy-loading.spec.ts b/playwright/e2e/lazy-loading/lazy-loading.spec.ts index 8b81589813..c04bcb8c64 100644 --- a/playwright/e2e/lazy-loading/lazy-loading.spec.ts +++ b/playwright/e2e/lazy-loading/lazy-loading.spec.ts @@ -80,7 +80,7 @@ test.describe("Lazy Loading", () => { async function openMemberlist(page: Page): Promise { 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 { diff --git a/playwright/e2e/read-receipts/index.ts b/playwright/e2e/read-receipts/index.ts index 4dd0450fb9..484df2251d 100644 --- a/playwright/e2e/read-receipts/index.ts +++ b/playwright/e2e/read-receipts/index.ts @@ -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(); } /** diff --git a/playwright/e2e/right-panel/right-panel.spec.ts b/playwright/e2e/right-panel/right-panel.spec.ts index 4f578748d6..e323a4b24f 100644 --- a/playwright/e2e/right-panel/right-panel.spec.ts +++ b/playwright/e2e/right-panel/right-panel.spec.ts @@ -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); }); }); diff --git a/playwright/e2e/spaces/threads-activity-centre/index.ts b/playwright/e2e/spaces/threads-activity-centre/index.ts index 8bafe2e804..8b013c44bb 100644 --- a/playwright/e2e/spaces/threads-activity-centre/index.ts +++ b/playwright/e2e/spaces/threads-activity-centre/index.ts @@ -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(); } /** diff --git a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts index 7d0b694ef5..66a3bc58e5 100644 --- a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts +++ b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts @@ -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(); }); }); diff --git a/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png b/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png index 4211f82b7a..2c6160f2a1 100644 Binary files a/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png and b/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png index 0443552daa..614533956b 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png index dd6303036b..51f365f353 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png differ diff --git a/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png b/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png index 7fbf4be835..943cc9dfc8 100644 Binary files a/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png and b/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png differ diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 043e7b7658..327b86da08 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -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"; diff --git a/res/css/views/right_panel/_RightPanelTabs.pcss b/res/css/views/right_panel/_RightPanelTabs.pcss new file mode 100644 index 0000000000..afaae6c657 --- /dev/null +++ b/res/css/views/right_panel/_RightPanelTabs.pcss @@ -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; + } +} diff --git a/res/css/views/right_panel/_RoomSummaryCard.pcss b/res/css/views/right_panel/_RoomSummaryCard.pcss index e3ed7b261b..549eb69ee4 100644 --- a/res/css/views/right_panel/_RoomSummaryCard.pcss +++ b/res/css/views/right_panel/_RoomSummaryCard.pcss @@ -235,7 +235,7 @@ limitations under the License. } .mx_RoomSummaryCard_header { - padding: 15px 12px; + padding: 24px 12px 15px; } .mx_RoomSummaryCard_search { diff --git a/res/css/views/rooms/_MemberList.pcss b/res/css/views/rooms/_MemberList.pcss index 086a60810f..6e2e5a43a4 100644 --- a/res/css/views/rooms/_MemberList.pcss +++ b/res/css/views/rooms/_MemberList.pcss @@ -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; diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 638011e9dc..bc80692459 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -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 { { card = ( { return ( ); diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 74c4f91627..a0555abbf7 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1287,7 +1287,7 @@ export class RoomView extends React.Component { ]); } } else { - RightPanelStore.instance.showOrHidePanel(RightPanelPhases.RoomMemberList); + RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomMemberList); } break; case Action.View3pidInvite: diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index d1e8360174..1fca77c27e 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -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 = ({ 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 ( = ({ roomId, onClose, permalinkCreator }) => }} > = ({ roomId, onClose, permalinkCreator }) => empty={!hasThreads} /> } + id="thread-panel" className="mx_ThreadPanel" + ariaLabelledBy="thread-panel-tab" + role="tabpanel" onClose={onClose} withoutScrollContainer={true} ref={card} diff --git a/src/components/views/right_panel/BaseCard.tsx b/src/components/views/right_panel/BaseCard.tsx index 2afae0bc28..bb07426a11 100644 --- a/src/components/views/right_panel/BaseCard.tsx +++ b/src/components/views/right_panel/BaseCard.tsx @@ -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 = forwardRef( onClose, onBack, className, + id, + ariaLabelledBy, + role, + hideHeaderButtons, header, footer, withoutScrollContainer, @@ -100,13 +108,31 @@ const BaseCard: React.FC = forwardRef( children = {children}; } + let headerButtons: React.ReactElement | undefined; + if (!hideHeaderButtons) { + headerButtons = ( + <> + {backButton} + {closeButton} + + ); + } + + const shouldRenderHeader = header || !hideHeaderButtons; + return ( -
- {header !== null && ( +
+ {shouldRenderHeader && (
- {backButton} - {closeButton} + {headerButtons}
{header}
)} diff --git a/src/components/views/right_panel/LegacyRoomHeaderButtons.tsx b/src/components/views/right_panel/LegacyRoomHeaderButtons.tsx index 672a288656..207c97ec7b 100644 --- a/src/components/views/right_panel/LegacyRoomHeaderButtons.tsx +++ b/src/components/views/right_panel/LegacyRoomHeaderButtons.tsx @@ -214,27 +214,27 @@ export default class LegacyRoomHeaderButtons extends HeaderButtons { 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 => { diff --git a/src/components/views/right_panel/RightPanelTabs.tsx b/src/components/views/right_panel/RightPanelTabs.tsx new file mode 100644 index 0000000000..fc2eeb17fa --- /dev/null +++ b/src/components/views/right_panel/RightPanelTabs.tsx @@ -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 = ({ phase }): JSX.Element | null => { + const threadsTabRef = useRef(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 ( + + { + RightPanelStore.instance.pushCard({ phase: RightPanelPhases.RoomSummary }, true); + }} + active={phase === RightPanelPhases.RoomSummary} + > + {_t("right_panel|info")} + + ) => { + RightPanelStore.instance.pushCard({ phase: RightPanelPhases.RoomMemberList }, true); + PosthogTrackers.trackInteraction("WebRightPanelRoomInfoPeopleButton", ev); + }} + active={phase === RightPanelPhases.RoomMemberList} + > + {_t("common|people")} + + { + RightPanelStore.instance.pushCard({ phase: RightPanelPhases.ThreadPanel }, true); + }} + active={phase === RightPanelPhases.ThreadPanel} + ref={threadsTabRef} + > + {_t("common|threads")} + + + ); +}; diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx index 2a1359e89d..dbc8743944 100644 --- a/src/components/views/right_panel/RoomSummaryCard.tsx +++ b/src/components/views/right_panel/RoomSummaryCard.tsx @@ -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> = ({ room }): JSX.Element | null const RoomSummaryCard: React.FC = ({ room, permalinkCreator, - onClose, onSearchChange, onSearchCancel, focusRoomSearch, @@ -416,11 +413,6 @@ const RoomSummaryCard: React.FC = ({ }); }; - 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 = ({ const isFavorite = roomTags.includes(DefaultTagID.Favourite); return ( - + = ({ /> )} - {header} @@ -589,13 +581,6 @@ const RoomSummaryCard: React.FC = ({ - {!isVideoRoom && ( <> diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 9d1de1f13d..5f9830f5d6 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -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 = ({ user, room, onClose, phase = RightPanelPha ); + return ( : undefined} + header={createSpaceScopeHeader(room)} onClose={onClose} closeLabel={closeLabel} cardState={cardState} diff --git a/src/components/views/rooms/MemberList.tsx b/src/components/views/rooms/MemberList.tsx index 828f9691da..130daf50bd 100644 --- a/src/components/views/rooms/MemberList.tsx +++ b/src/components/views/rooms/MemberList.tsx @@ -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 { public render(): React.ReactNode { if (this.state.loading) { return ( - + ); @@ -415,12 +423,14 @@ export default class MemberList extends React.Component { /> ); - const scopeHeader = room ? : undefined; - return ( {scopeHeader}} + ariaLabelledBy="memberlist-panel-tab" + role="tabpanel" + header={createSpaceScopeHeader(room)} + hideHeaderButtons={this.props.hideHeaderButtons} footer={footer} onClose={this.props.onClose} > diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 6f4620b637..19b368cd18 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -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({ )} + + { + evt.stopPropagation(); + RightPanelStore.instance.showOrHidePanel(RightPanelPhases.RoomSummary); + }} + aria-label={_t("right_panel|room_summary_card|title")} + > + + + ; + 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 ( : undefined; - return ( - + {/* same as userinfo name style */} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index afb381f6a1..eaf5784af9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -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 Pin to stick them here.", diff --git a/src/stores/right-panel/RightPanelStore.ts b/src/stores/right-panel/RightPanelStore.ts index 0cffdb423c..69ac46806d 100644 --- a/src/stores/right-panel/RightPanelStore.ts +++ b/src/stores/right-panel/RightPanelStore.ts @@ -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): void { + public showOrHidePhase(phase: RightPanelPhases, cardState?: Partial): void { if (this.currentCard.phase == phase && !cardState && this.isOpen) { this.togglePanel(null); } else { diff --git a/src/stores/right-panel/action-handlers/View3pidInvite.ts b/src/stores/right-panel/action-handlers/View3pidInvite.ts index be61fd5745..e1d516de7a 100644 --- a/src/stores/right-panel/action-handlers/View3pidInvite.ts +++ b/src/stores/right-panel/action-handlers/View3pidInvite.ts @@ -32,6 +32,6 @@ export const onView3pidInvite = (payload: ActionPayload, rightPanelStore: RightP state: { memberInfoEvent: payload.event }, }); } else { - rightPanelStore.showOrHidePanel(RightPanelPhases.RoomMemberList); + rightPanelStore.showOrHidePhase(RightPanelPhases.RoomMemberList); } }; diff --git a/test/components/structures/ThreadPanel-test.tsx b/test/components/structures/ThreadPanel-test.tsx index 4f66379a3d..19122e4cce 100644 --- a/test/components/structures/ThreadPanel-test.tsx +++ b/test/components/structures/ThreadPanel-test.tsx @@ -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( - - - - - , - ); - - // 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", () => { diff --git a/test/components/views/right_panel/RightPanelTabs-test.tsx b/test/components/views/right_panel/RightPanelTabs-test.tsx new file mode 100644 index 0000000000..dae7b1a79a --- /dev/null +++ b/test/components/views/right_panel/RightPanelTabs-test.tsx @@ -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("", () => { + it("Component renders the correct tabs", () => { + const { container, getByRole } = render(); + 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(); + 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(); + expect(container).toBeEmptyDOMElement(); + }); + + it("onClick behaviors work as expected", () => { + const spy = jest.spyOn(RightPanelStore.instance, "pushCard"); + const { getByRole } = render(); + + // 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(); + dis.dispatch({ action: Action.FocusThreadsPanel }, true); + expect(getByRole("tab", { name: "Threads" })).toHaveFocus(); + }); +}); diff --git a/test/components/views/right_panel/RoomSummaryCard-test.tsx b/test/components/views/right_panel/RoomSummaryCard-test.tsx index f90144a3ba..1ddea76382 100644 --- a/test/components/views/right_panel/RoomSummaryCard-test.tsx +++ b/test/components/views/right_panel/RoomSummaryCard-test.tsx @@ -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("", () => { @@ -212,7 +210,6 @@ describe("", () => { @@ -270,18 +267,6 @@ describe("", () => { 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"); diff --git a/test/components/views/right_panel/__snapshots__/RightPanelTabs-test.tsx.snap b/test/components/views/right_panel/__snapshots__/RightPanelTabs-test.tsx.snap new file mode 100644 index 0000000000..36c3ccf6b5 --- /dev/null +++ b/test/components/views/right_panel/__snapshots__/RightPanelTabs-test.tsx.snap @@ -0,0 +1,119 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` Component renders the correct tabs 1`] = ` +
+ +
+`; + +exports[` Correct tab is active 1`] = ` +
+ +
+`; diff --git a/test/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap b/test/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap index 13d038adca..8f8322d44d 100644 --- a/test/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap +++ b/test/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap @@ -3,7 +3,10 @@ exports[` has button to edit topic when expanded 1`] = `
has button to edit topic when expanded 1`] = `
-
-
+ />
@@ -244,36 +239,6 @@ exports[` has button to edit topic when expanded 1`] = ` data-orientation="horizontal" role="separator" /> - +