diff --git a/apps/meteor/client/views/room/Header/ParentRoom.tsx b/apps/meteor/client/views/room/Header/ParentRoom.tsx index 00b181f47df5..3d598cce4c26 100644 --- a/apps/meteor/client/views/room/Header/ParentRoom.tsx +++ b/apps/meteor/client/views/room/Header/ParentRoom.tsx @@ -13,10 +13,15 @@ type ParentRoomProps = { const ParentRoom = ({ room }: ParentRoomProps): ReactElement => { const icon = useRoomIcon(room); - const handleClick = (): void => roomCoordinator.openRouteLink(room.t, { rid: room._id, ...room }); + const handleRedirect = (): void => roomCoordinator.openRouteLink(room.t, { rid: room._id, ...room }); return ( - + (e.code === 'Space' || e.code === 'Enter') && handleRedirect()} + onClick={handleRedirect} + > {roomCoordinator.getRoomName(room.t, room)} diff --git a/apps/meteor/client/views/room/Header/ParentTeam.tsx b/apps/meteor/client/views/room/Header/ParentTeam.tsx index 2f1f15327c79..33ef98bbe81b 100644 --- a/apps/meteor/client/views/room/Header/ParentTeam.tsx +++ b/apps/meteor/client/views/room/Header/ParentTeam.tsx @@ -41,11 +41,14 @@ const ParentTeam = ({ room }: { room: IRoom }): ReactElement | null => { const redirectToMainRoom = (): void => { const rid = teamInfoData?.teamInfo.roomId; - if (!rid) { return; } + if (!(isTeamPublic || userBelongsToTeam)) { + return; + } + goToRoomById(rid); }; @@ -58,7 +61,12 @@ const ParentTeam = ({ room }: { room: IRoom }): ReactElement | null => { } return ( - + (e.code === 'Space' || e.code === 'Enter') && redirectToMainRoom()} + onClick={redirectToMainRoom} + > {teamInfoData?.teamInfo.name} diff --git a/apps/meteor/client/views/room/Header/RoomTitle.tsx b/apps/meteor/client/views/room/Header/RoomTitle.tsx index 13b628ab3823..4d81d077c154 100644 --- a/apps/meteor/client/views/room/Header/RoomTitle.tsx +++ b/apps/meteor/client/views/room/Header/RoomTitle.tsx @@ -1,22 +1,50 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import { HeaderTitle, useDocumentTitle } from '@rocket.chat/ui-client'; -import type { ReactElement } from 'react'; +import { isTeamRoom, type IRoom } from '@rocket.chat/core-typings'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { HeaderTitle, HeaderTitleButton, useDocumentTitle } from '@rocket.chat/ui-client'; +import type { KeyboardEvent, ReactElement } from 'react'; import React from 'react'; +import { useRoomToolbox } from '../contexts/RoomToolboxContext'; import HeaderIconWithRoom from './HeaderIconWithRoom'; -type RoomTitleProps = { - room: IRoom; -}; - -const RoomTitle = ({ room }: RoomTitleProps): ReactElement => { +const RoomTitle = ({ room }: { room: IRoom }): ReactElement => { useDocumentTitle(room.name, false); + const { openTab } = useRoomToolbox(); + + const handleOpenRoomInfo = useEffectEvent(() => { + if (isTeamRoom(room)) { + return openTab('team-info'); + } + + switch (room.t) { + case 'l': + openTab('room-info'); + break; + + case 'v': + openTab('voip-room-info'); + break; + + case 'd': + (room.uids?.length ?? 0) > 2 ? openTab('user-info-group') : openTab('user-info'); + break; + + default: + openTab('channel-settings'); + break; + } + }); return ( - <> + (e.code === 'Enter' || e.code === 'Space') && handleOpenRoomInfo()} + onClick={() => handleOpenRoomInfo()} + tabIndex={0} + role='button' + > {room.name} - + ); }; diff --git a/apps/meteor/client/views/room/Header/icons/Favorite.tsx b/apps/meteor/client/views/room/Header/icons/Favorite.tsx index f66af3443ed8..4a99a7a0411e 100644 --- a/apps/meteor/client/views/room/Header/icons/Favorite.tsx +++ b/apps/meteor/client/views/room/Header/icons/Favorite.tsx @@ -1,39 +1,41 @@ import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { HeaderState } from '@rocket.chat/ui-client'; import { useSetting, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; import React, { memo } from 'react'; import { useUserIsSubscribed } from '../../contexts/RoomContext'; -const Favorite = ({ room: { _id, f: favorite = false, t: type } }: { room: IRoom & { f?: ISubscription['f'] } }) => { +const Favorite = ({ room: { _id, f: favorite = false, t: type, name } }: { room: IRoom & { f?: ISubscription['f'] } }) => { const t = useTranslation(); const subscribed = useUserIsSubscribed(); const isFavoritesEnabled = useSetting('Favorite_Rooms') && ['c', 'p', 'd', 't'].includes(type); const toggleFavorite = useMethod('toggleFavorite'); - const handleFavoriteClick = useMutableCallback(() => { + + const handleFavoriteClick = useEffectEvent(() => { if (!isFavoritesEnabled) { return; } + toggleFavorite(_id, !favorite); }); - const favoriteLabel = favorite ? t('Unfavorite') : t('Favorite'); + + const favoriteLabel = favorite ? `${t('Unfavorite')} ${name}` : `${t('Favorite')} ${name}`; if (!subscribed || !isFavoritesEnabled) { return null; } return ( - isFavoritesEnabled && ( - - ) + ); }; diff --git a/apps/meteor/client/views/room/body/LeaderBar.tsx b/apps/meteor/client/views/room/body/LeaderBar.tsx index 4cf5266b89d8..bb0ba305633a 100644 --- a/apps/meteor/client/views/room/body/LeaderBar.tsx +++ b/apps/meteor/client/views/room/body/LeaderBar.tsx @@ -66,7 +66,7 @@ const LeaderBar = ({ _id, name, username, visible, onAvatarClick, triggerProps } className={[roomLeaderStyle, 'room-leader', !visible && 'animated-hidden'].filter(isTruthy)} > - + diff --git a/apps/meteor/tests/e2e/channel-management.spec.ts b/apps/meteor/tests/e2e/channel-management.spec.ts index 96d50bae6151..f76752085771 100644 --- a/apps/meteor/tests/e2e/channel-management.spec.ts +++ b/apps/meteor/tests/e2e/channel-management.spec.ts @@ -11,6 +11,7 @@ test.use({ storageState: Users.admin.state }); test.describe.serial('channel-management', () => { let poHomeChannel: HomeChannel; let targetChannel: string; + let discussionName: string; test.beforeAll(async ({ api }) => { targetChannel = await createTargetChannel(api); @@ -56,7 +57,8 @@ test.describe.serial('channel-management', () => { await expect(page.getByRole('button', { name: 'Start call' })).toBeFocused(); }); - test('expect add "user1" to "targetChannel"', async () => { + // FIXME: bad assertion + test('should add "user1" to "targetChannel"', async () => { await poHomeChannel.sidenav.openChat(targetChannel); await poHomeChannel.tabs.btnTabMembers.click(); await poHomeChannel.tabs.members.showAllUsers(); @@ -65,7 +67,8 @@ test.describe.serial('channel-management', () => { await expect(poHomeChannel.toastSuccess).toBeVisible(); }); - test('expect create invite to the room', async () => { + // FIXME: bad assertion + test('should create invite to the room', async () => { await poHomeChannel.sidenav.openChat(targetChannel); await poHomeChannel.tabs.btnTabMembers.click(); await poHomeChannel.tabs.members.inviteUser(); @@ -73,28 +76,29 @@ test.describe.serial('channel-management', () => { await expect(poHomeChannel.toastSuccess).toBeVisible(); }); - test('expect mute "user1"', async () => { + test.fixme('should mute "user1"', async () => { await poHomeChannel.sidenav.openChat(targetChannel); await poHomeChannel.tabs.btnTabMembers.click(); await poHomeChannel.tabs.members.showAllUsers(); await poHomeChannel.tabs.members.muteUser('user1'); }); - test('expect set "user1" as owner', async () => { + test.fixme('should set "user1" as owner', async () => { await poHomeChannel.sidenav.openChat(targetChannel); await poHomeChannel.tabs.btnTabMembers.click(); await poHomeChannel.tabs.members.showAllUsers(); await poHomeChannel.tabs.members.setUserAsOwner('user1'); }); - test('expect set "user1" as moderator', async () => { + + test.fixme('should set "user1" as moderator', async () => { await poHomeChannel.sidenav.openChat(targetChannel); await poHomeChannel.tabs.btnTabMembers.click(); await poHomeChannel.tabs.members.showAllUsers(); await poHomeChannel.tabs.members.setUserAsModerator('user1'); }); - test('expect edit topic of "targetChannel"', async () => { + test.fixme('should edit topic of "targetChannel"', async () => { await poHomeChannel.sidenav.openChat(targetChannel); await poHomeChannel.tabs.btnRoomInfo.click(); await poHomeChannel.tabs.room.btnEdit.click(); @@ -102,7 +106,7 @@ test.describe.serial('channel-management', () => { await poHomeChannel.tabs.room.btnSave.click(); }); - test('expect edit announcement of "targetChannel"', async () => { + test.fixme('should edit announcement of "targetChannel"', async () => { await poHomeChannel.sidenav.openChat(targetChannel); await poHomeChannel.tabs.btnRoomInfo.click(); await poHomeChannel.tabs.room.btnEdit.click(); @@ -110,7 +114,7 @@ test.describe.serial('channel-management', () => { await poHomeChannel.tabs.room.btnSave.click(); }); - test('expect edit description of "targetChannel"', async () => { + test.fixme('should edit description of "targetChannel"', async () => { await poHomeChannel.sidenav.openChat(targetChannel); await poHomeChannel.tabs.btnRoomInfo.click(); await poHomeChannel.tabs.room.btnEdit.click(); @@ -118,18 +122,65 @@ test.describe.serial('channel-management', () => { await poHomeChannel.tabs.room.btnSave.click(); }); - test('expect edit name of "targetChannel"', async ({ page }) => { + test('should edit name of "targetChannel"', async ({ page }) => { await poHomeChannel.sidenav.openChat(targetChannel); await poHomeChannel.tabs.btnRoomInfo.click(); await poHomeChannel.tabs.room.btnEdit.click(); await poHomeChannel.tabs.room.inputName.fill(`NAME-EDITED-${targetChannel}`); await poHomeChannel.tabs.room.btnSave.click(); - await poHomeChannel.sidenav.openChat(`NAME-EDITED-${targetChannel}`); - await expect(page).toHaveURL(`/channel/NAME-EDITED-${targetChannel}`); + targetChannel = `NAME-EDITED-${targetChannel}`; + await poHomeChannel.sidenav.openChat(targetChannel); + + await expect(page).toHaveURL(`/channel/${targetChannel}`); + }); + + test('should truncate the room name for small screens', async ({ page }) => { + const hugeName = faker.string.alpha(100); + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.tabs.btnRoomInfo.click(); + await poHomeChannel.tabs.room.btnEdit.click(); + await poHomeChannel.tabs.room.inputName.fill(hugeName); + await poHomeChannel.tabs.room.btnSave.click(); + targetChannel = hugeName; + + await page.setViewportSize({ width: 640, height: 460 }); + await expect(page.getByRole('heading', { name: hugeName })).toHaveCSS('width', '423px'); + }); + + test('should info contextualbar when clicking on roomName', async ({ page }) => { + await poHomeChannel.sidenav.openChat(targetChannel); + await page.getByRole('button', { name: targetChannel }).first().focus(); + await page.keyboard.press('Space'); + await page.getByRole('complementary').waitFor(); + + await expect(page.getByRole('complementary')).toBeVisible(); + }); + + test('should create a discussion using the message composer', async ({ page }) => { + discussionName = faker.string.uuid(); + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.content.btnMenuMoreActions.click(); + await page.getByRole('menuitem', { name: 'Discussion' }).click(); + await page.getByRole('textbox', { name: 'Discussion name' }).fill(discussionName); + await page.getByRole('button', { name: 'Create' }).click(); + + await expect(page.getByRole('heading', { name: discussionName })).toBeVisible(); + }); + + test('should access targetTeam through discussion header', async ({ page }) => { + await poHomeChannel.sidenav.openChat(targetChannel); + await page.locator('[data-qa-type="message"]', { hasText: discussionName }).locator('button').first().click(); + await page.getByRole('button', { name: discussionName }).first().focus(); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Space'); + + await expect(page).toHaveURL(`/channel/${targetChannel}`); }); - test.skip('expect edit notification preferences of "targetChannel"', async () => { + // FIXME: bad assertion + test.fixme('should edit notification preferences of "targetChannel"', async () => { await poHomeChannel.sidenav.openChat(targetChannel); await poHomeChannel.tabs.kebab.click({ force: true }); await poHomeChannel.tabs.btnNotificationPreferences.click({ force: true }); @@ -140,7 +191,7 @@ test.describe.serial('channel-management', () => { }); let regularUserPage: Page; - test('expect "readOnlyChannel" to show join button', async ({ browser }) => { + test('should "readOnlyChannel" show join button', async ({ browser }) => { const channelName = faker.string.uuid(); await poHomeChannel.sidenav.openNewByLabel('Channel'); @@ -160,7 +211,7 @@ test.describe.serial('channel-management', () => { await regularUserPage.close(); }); - test.skip('expect all notification preferences of "targetChannel" to be "Mentions"', async () => { + test.fixme('should all notification preferences of "targetChannel" to be "Mentions"', async () => { await poHomeChannel.sidenav.openChat(targetChannel); await poHomeChannel.tabs.kebab.click({ force: true }); await poHomeChannel.tabs.btnNotificationPreferences.click({ force: true }); diff --git a/apps/meteor/tests/e2e/messaging.spec.ts b/apps/meteor/tests/e2e/messaging.spec.ts index 4fa248e72183..ab31dcba2729 100644 --- a/apps/meteor/tests/e2e/messaging.spec.ts +++ b/apps/meteor/tests/e2e/messaging.spec.ts @@ -38,13 +38,14 @@ test.describe.serial('Messaging', () => { await page.keyboard.press('ArrowDown'); await expect(page.locator('[data-qa-type="message"]:has-text("msg1")')).toBeFocused(); - // move focus to the favorite icon + // move focus to the room title await page.keyboard.press('Shift+Tab'); - await expect(poHomeChannel.roomHeaderFavoriteBtn).toBeFocused(); + await expect(page.getByRole('button', { name: targetChannel }).first()).toBeFocused(); // refocus on the first typed message await page.keyboard.press('Tab'); await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); await expect(page.locator('[data-qa-type="message"]:has-text("msg1")')).toBeFocused(); // move focus to the message toolbar diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 2c739ae6667d..5435985fedb3 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -153,7 +153,7 @@ export class HomeContent { } get btnMenuMoreActions() { - return this.page.locator('[data-qa-id="menu-more-actions"]'); + return this.page.getByRole('button', { name: 'More actions' }); } get userCard(): Locator { diff --git a/apps/meteor/tests/e2e/team-management.spec.ts b/apps/meteor/tests/e2e/team-management.spec.ts index 338f5c5eb0ef..11609ec51ed6 100644 --- a/apps/meteor/tests/e2e/team-management.spec.ts +++ b/apps/meteor/tests/e2e/team-management.spec.ts @@ -89,4 +89,14 @@ test.describe.serial('teams-management', () => { await poHomeTeam.tabs.channels.btnAdd.click(); await expect(page.locator('//main//aside >> li')).toContainText(targetChannel); }); + + test('should access team channel through "targetTeam" header', async ({ page }) => { + await poHomeTeam.sidenav.openChat(targetChannel); + await page.getByRole('button', { name: targetChannel }).first().focus(); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Space'); + + await expect(page).toHaveURL(`/group/${targetTeam}`); + }); }); diff --git a/packages/ui-client/src/components/Header/HeaderTag/HeaderTag.tsx b/packages/ui-client/src/components/Header/HeaderTag/HeaderTag.tsx index c921f2b93525..44661229707c 100644 --- a/packages/ui-client/src/components/Header/HeaderTag/HeaderTag.tsx +++ b/packages/ui-client/src/components/Header/HeaderTag/HeaderTag.tsx @@ -2,7 +2,7 @@ import { Box, Tag } from '@rocket.chat/fuselage'; import type { ComponentProps, FC } from 'react'; const HeaderTag: FC> = ({ children, ...props }) => ( - + {children} diff --git a/packages/ui-client/src/components/Header/HeaderTitleButton.tsx b/packages/ui-client/src/components/Header/HeaderTitleButton.tsx new file mode 100644 index 000000000000..c27f20ac9e09 --- /dev/null +++ b/packages/ui-client/src/components/Header/HeaderTitleButton.tsx @@ -0,0 +1,25 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Box, Palette } from '@rocket.chat/fuselage'; +import type { ComponentProps } from 'react'; + +const HeaderTitleButton = ({ className, ...props }: { className?: string } & ComponentProps) => { + const customClass = css` + border-width: 1px; + border-style: solid; + border-color: transparent; + + &:hover { + cursor: pointer; + background-color: ${Palette.surface['surface-hover']}; + } + &:focus.focus-visible { + outline: 0; + box-shadow: 0 0 0 2px ${Palette.stroke['stroke-extra-light-highlight']}; + border-color: ${Palette.stroke['stroke-highlight']}; + } + `; + + return ; +}; + +export default HeaderTitleButton; diff --git a/packages/ui-client/src/components/Header/index.ts b/packages/ui-client/src/components/Header/index.ts index b8d62fb777f9..00e2c0ab17dc 100644 --- a/packages/ui-client/src/components/Header/index.ts +++ b/packages/ui-client/src/components/Header/index.ts @@ -8,4 +8,5 @@ export { default as HeaderState } from './HeaderState'; export { default as HeaderSubtitle } from './HeaderSubtitle'; export * from './HeaderTag'; export { default as HeaderTitle } from './HeaderTitle'; +export { default as HeaderTitleButton } from './HeaderTitleButton'; export * from './HeaderToolbar';