Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: RoomHeader keyboard navigability #31837

Merged
merged 11 commits into from
Feb 29, 2024
9 changes: 7 additions & 2 deletions apps/meteor/client/views/room/Header/ParentRoom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<HeaderTag onClick={handleClick}>
<HeaderTag
role='button'
tabIndex={0}
onKeyDown={(e) => (e.code === 'Space' || e.code === 'Enter') && handleRedirect()}
onClick={handleRedirect}
>
<HeaderTagIcon icon={icon} />
{roomCoordinator.getRoomName(room.t, room)}
</HeaderTag>
Expand Down
12 changes: 10 additions & 2 deletions apps/meteor/client/views/room/Header/ParentTeam.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};

Expand All @@ -58,7 +61,12 @@ const ParentTeam = ({ room }: { room: IRoom }): ReactElement | null => {
}

return (
<HeaderTag onClick={isTeamPublic || userBelongsToTeam ? redirectToMainRoom : undefined}>
<HeaderTag
role='button'
tabIndex={0}
onKeyDown={(e) => (e.code === 'Space' || e.code === 'Enter') && redirectToMainRoom()}
onClick={redirectToMainRoom}
>
<HeaderTagIcon icon={{ name: isTeamPublic ? 'team' : 'team-lock' }} />
{teamInfoData?.teamInfo.name}
</HeaderTag>
Expand Down
48 changes: 38 additions & 10 deletions apps/meteor/client/views/room/Header/RoomTitle.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<HeaderTitleButton
onKeyDown={(e: KeyboardEvent) => (e.code === 'Enter' || e.code === 'Space') && handleOpenRoomInfo()}
onClick={() => handleOpenRoomInfo()}
tabIndex={0}
role='button'
>
<HeaderIconWithRoom room={room} />
<HeaderTitle is='h1'>{room.name}</HeaderTitle>
</>
</HeaderTitleButton>
);
};

Expand Down
28 changes: 15 additions & 13 deletions apps/meteor/client/views/room/Header/icons/Favorite.tsx
Original file line number Diff line number Diff line change
@@ -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 && (
<HeaderState
title={favoriteLabel}
icon={favorite ? 'star-filled' : 'star'}
onClick={handleFavoriteClick}
color={favorite ? 'status-font-on-warning' : null}
tiny
/>
)
<HeaderState
title={favoriteLabel}
aria-live='assertive'
icon={favorite ? 'star-filled' : 'star'}
onClick={handleFavoriteClick}
color={favorite ? 'status-font-on-warning' : null}
tiny
/>
);
};

Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/client/views/room/body/LeaderBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const LeaderBar = ({ _id, name, username, visible, onAvatarClick, triggerProps }
className={[roomLeaderStyle, 'room-leader', !visible && 'animated-hidden'].filter(isTruthy)}
>
<Box display='flex' alignItems='center'>
<Box is='button' mie={4} onClick={handleAvatarClick} {...triggerProps}>
<Box mie={4} onClick={handleAvatarClick} {...triggerProps}>
<UserAvatar username={username} />
</Box>
<Box fontScale='p2' mi={4} display='flex' alignItems='center'>
Expand Down
79 changes: 65 additions & 14 deletions apps/meteor/tests/e2e/channel-management.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand All @@ -65,71 +67,120 @@ 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();

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();
await poHomeChannel.tabs.room.inputTopic.fill('hello-topic-edited');
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();
await poHomeChannel.tabs.room.inputAnnouncement.fill('hello-announcement-edited');
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();
await poHomeChannel.tabs.room.inputDescription.fill('hello-description-edited');
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 });
Expand All @@ -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');
Expand All @@ -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 });
Expand Down
5 changes: 3 additions & 2 deletions apps/meteor/tests/e2e/messaging.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 10 additions & 0 deletions apps/meteor/tests/e2e/team-management.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Box, Tag } from '@rocket.chat/fuselage';
import type { ComponentProps, FC } from 'react';

const HeaderTag: FC<ComponentProps<typeof Tag>> = ({ children, ...props }) => (
<Box mi={4} withTruncatedText minWidth='x64'>
<Box p={4} withTruncatedText minWidth='x64'>
<Tag medium {...props}>
{children}
</Tag>
Expand Down
Loading
Loading