Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/kind-beers-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/ui-client": minor
"@rocket.chat/web-ui-registration": minor
---

feat: Skip to main content shortcut and useDocumentTitle
8 changes: 3 additions & 5 deletions apps/meteor/client/components/Page/PageHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Box, IconButton } from '@rocket.chat/fuselage';
import { useAutoFocus } from '@rocket.chat/fuselage-hooks';
import { HeaderToolbox } from '@rocket.chat/ui-client';
import { HeaderToolbox, useDocumentTitle } from '@rocket.chat/ui-client';
import { useLayout, useTranslation } from '@rocket.chat/ui-contexts';
import type { FC, ComponentProps, ReactNode } from 'react';
import React, { useContext } from 'react';
Expand All @@ -18,12 +17,11 @@ const PageHeader: FC<PageHeaderProps> = ({ children = undefined, title, onClickB
const t = useTranslation();
const [border] = useContext(PageContext);
const { isMobile } = useLayout();
const headerAutoFocus = useAutoFocus();

useDocumentTitle(typeof title === 'string' ? title : undefined);

return (
<Box
tabIndex={-1}
ref={headerAutoFocus}
is='header'
borderBlockEndWidth='default'
minHeight='x64'
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/client/sidebar/Item/Condensed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const Condensed: FC<CondensedProps> = ({ icon, title = '', avatar, actions, href
{badges && <Sidebar.Item.Badge>{badges}</Sidebar.Item.Badge>}
{menu && (
<Sidebar.Item.Menu {...handleMenuEvent}>
{menuVisibility ? menu() : <IconButton mini rcx-sidebar-item__menu icon='kebab' />}
{menuVisibility ? menu() : <IconButton tabIndex={-1} aria-hidden mini rcx-sidebar-item__menu icon='kebab' />}
</Sidebar.Item.Menu>
)}
</Sidebar.Item.Content>
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/client/sidebar/Item/Extended.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const Extended: VFC<ExtendedProps> = ({
};

return (
<Sidebar.Item aria-selected={selected} selected={selected} highlighted={unread} {...props} {...({ href } as any)} clickable={!!href}>
<Sidebar.Item selected={selected} highlighted={unread} {...props} {...({ href } as any)} clickable={!!href}>
{avatar && <Sidebar.Item.Avatar>{avatar}</Sidebar.Item.Avatar>}
<Sidebar.Item.Content>
<Sidebar.Item.Content>
Expand All @@ -72,7 +72,7 @@ const Extended: VFC<ExtendedProps> = ({
<Sidebar.Item.Badge>{badges}</Sidebar.Item.Badge>
{menu && (
<Sidebar.Item.Menu {...handleMenuEvent}>
{menuVisibility ? menu() : <IconButton mini rcx-sidebar-item__menu icon='kebab' />}
{menuVisibility ? menu() : <IconButton tabIndex={-1} aria-hidden mini rcx-sidebar-item__menu icon='kebab' />}
</Sidebar.Item.Menu>
)}
</Sidebar.Item.Wrapper>
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/client/sidebar/Item/Medium.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const Medium: VFC<MediumProps> = ({ icon, title = '', avatar, actions, href, bad
{badges && <Sidebar.Item.Badge>{badges}</Sidebar.Item.Badge>}
{menu && (
<Sidebar.Item.Menu {...handleMenuEvent}>
{menuVisibility ? menu() : <IconButton mini rcx-sidebar-item__menu icon='kebab' />}
{menuVisibility ? menu() : <IconButton tabIndex={-1} aria-hidden mini rcx-sidebar-item__menu icon='kebab' />}
</Sidebar.Item.Menu>
)}
</Sidebar.Item.Content>
Expand Down
5 changes: 0 additions & 5 deletions apps/meteor/client/startup/unread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { Session } from 'meteor/session';
import { Tracker } from 'meteor/tracker';

import { ChatSubscription, ChatRoom } from '../../app/models/client';
import { settings } from '../../app/settings/client';
import { getUserPreference } from '../../app/utils/client';
import { fireGlobalEvent } from '../lib/utils/fireGlobalEvent';

Expand Down Expand Up @@ -78,13 +77,9 @@ Meteor.startup(() => {
const updateFavicon = manageFavicon();

Tracker.autorun(() => {
const siteName = settings.get('Site_Name') ?? '';

const unread = Session.get('unread');
fireGlobalEvent('unread-changed', unread);

updateFavicon(unread);

document.title = unread === '' ? siteName : `(${unread}) ${siteName}`;
});
});
2 changes: 1 addition & 1 deletion apps/meteor/client/views/directory/DirectoryPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ const DirectoryPage = (): ReactElement => {
)}
</Tabs>
<Page.Content>
{tab === 'users' && <UsersTab />}
{tab === 'channels' && <ChannelsTab />}
{tab === 'users' && <UsersTab />}
{tab === 'teams' && <TeamsTab />}
{federationEnabled && tab === 'external' && <UsersTab workspace='external' />}
</Page.Content>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ const ChannelsTable = () => {

return (
<>
<FilterByText autoFocus placeholder={t('Search_Channels')} onChange={({ text }): void => setText(text)} />
<FilterByText placeholder={t('Search_Channels')} onChange={({ text }): void => setText(text)} />
{isLoading && (
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const TeamsTable = () => {

return (
<>
<FilterByText placeholder={t('Teams_Search_teams')} autoFocus onChange={({ text }): void => setText(text)} />
<FilterByText placeholder={t('Teams_Search_teams')} onChange={({ text }): void => setText(text)} />
{isLoading && (
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const UsersTable = ({ workspace = 'local' }): ReactElement => {

return (
<>
<FilterByText autoFocus placeholder={t('Search_Users')} onChange={({ text }): void => setText(text)} />
<FilterByText placeholder={t('Search_Users')} onChange={({ text }): void => setText(text)} />
{isLoading && (
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>
Expand Down
12 changes: 5 additions & 7 deletions apps/meteor/client/views/home/cards/CustomContentCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ const CustomContentCard = (): ReactElement | null => {

const { data } = useIsEnterprise();
const isAdmin = useRole('admin');
const customContentBody = String(useSetting('Layout_Home_Body'));
const customContentBody = useSetting<string>('Layout_Home_Body');
const isCustomContentBodyEmpty = customContentBody === '';
const isCustomContentVisible = Boolean(useSetting('Layout_Home_Custom_Block_Visible'));
const isCustomContentOnly = Boolean(useSetting('Layout_Custom_Body_Only'));
const isCustomContentVisible = useSetting<boolean>('Layout_Home_Custom_Block_Visible');
const isCustomContentOnly = useSetting<boolean>('Layout_Custom_Body_Only');

const settingsRoute = useRoute('admin-settings');

Expand Down Expand Up @@ -55,14 +55,12 @@ const CustomContentCard = (): ReactElement | null => {
return (
<Card data-qa-id='homepage-custom-card'>
<Box display='flex' mbe={12}>
<Tag role='status' aria-label={willNotShowCustomContent ? t('Not_Visible_To_Workspace') : t('Visible_To_Workspace')}>
<Tag>
<Icon mie={4} name={willNotShowCustomContent ? 'eye-off' : 'eye'} size='x12' />
{willNotShowCustomContent ? t('Not_Visible_To_Workspace') : t('Visible_To_Workspace')}
</Tag>
</Box>
<Box mb={8} role='status' aria-label={isCustomContentBodyEmpty ? t('Homepage_Custom_Content_Default_Message') : customContentBody}>
{isCustomContentBodyEmpty ? t('Homepage_Custom_Content_Default_Message') : <CustomHomepageContent />}
</Box>
<Box mb={8}>{isCustomContentBodyEmpty ? t('Homepage_Custom_Content_Default_Message') : <CustomHomepageContent />}</Box>
<CardFooterWrapper>
<CardFooter>
<Button onClick={() => settingsRoute.push({ group: 'Layout' })} title={t('Layout_Home_Page_Content')}>
Expand Down
18 changes: 11 additions & 7 deletions apps/meteor/client/views/room/Header/RoomTitle.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { HeaderTitle } from '@rocket.chat/ui-client';
import { HeaderTitle, useDocumentTitle } from '@rocket.chat/ui-client';
import type { ReactElement } from 'react';
import React from 'react';

Expand All @@ -9,11 +9,15 @@ type RoomTitleProps = {
room: IRoom;
};

const RoomTitle = ({ room }: RoomTitleProps): ReactElement => (
<>
<HeaderIconWithRoom room={room} />
<HeaderTitle is='h1'>{room.name}</HeaderTitle>
</>
);
const RoomTitle = ({ room }: RoomTitleProps): ReactElement => {
useDocumentTitle(room.name, false);

return (
<>
<HeaderIconWithRoom room={room} />
<HeaderTitle is='h1'>{room.name}</HeaderTitle>
</>
);
};

export default RoomTitle;
7 changes: 6 additions & 1 deletion apps/meteor/client/views/root/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useSyncExternalStore } from 'use-sync-external-store/shim';
import { useAnalytics } from '../../../app/analytics/client/loadScript';
import { useAnalyticsEventTracking } from '../../hooks/useAnalyticsEventTracking';
import { appLayout } from '../../lib/appLayout';
import DocumentTitleWrapper from './DocumentTitleWrapper';
import PageLoading from './PageLoading';
import { useEscapeKeyStroke } from './hooks/useEscapeKeyStroke';
import { useGoogleTagManager } from './hooks/useGoogleTagManager';
Expand All @@ -26,7 +27,11 @@ const AppLayout = () => {

const layout = useSyncExternalStore(appLayout.subscribe, appLayout.getSnapshot);

return <Suspense fallback={<PageLoading />}>{layout}</Suspense>;
return (
<Suspense fallback={<PageLoading />}>
<DocumentTitleWrapper>{layout}</DocumentTitleWrapper>
</Suspense>
);
};

export default AppLayout;
55 changes: 55 additions & 0 deletions apps/meteor/client/views/root/DocumentTitleWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { css } from '@rocket.chat/css-in-js';
import { Box } from '@rocket.chat/fuselage';
import { useDocumentTitle } from '@rocket.chat/ui-client';
import { useSetting } from '@rocket.chat/ui-contexts';
import type { FC } from 'react';
import React, { useEffect, useCallback } from 'react';

import { useUnreadMessages } from './hooks/useUnreadMessages';

const useRouteTitleFocus = () => {
return useCallback((node: HTMLElement | null) => {
if (!node) {
return;
}

node.focus();
}, []);
};

const DocumentTitleWrapper: FC = ({ children }) => {
useDocumentTitle(useSetting<string>('Site_Name') || '', false);
const { title, key } = useDocumentTitle(useUnreadMessages(), false);

const refocusRef = useRouteTitleFocus();

useEffect(() => {
document.title = title;
}, [title]);

return (
<>
<Box
tabIndex={-1}
ref={refocusRef}
key={key}
className={css`
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
`}
>
{title}
</Box>
{children}
</>
);
};

export default DocumentTitleWrapper;
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { css } from '@rocket.chat/css-in-js';
import { Button } from '@rocket.chat/fuselage';
import { useRouter, useTranslation } from '@rocket.chat/ui-contexts';
import React from 'react';

const AccessibilityShortcut = () => {
const t = useTranslation();
const router = useRouter();
const currentRoutePath = router.getLocationPathname();

const customButtonClass = css`
position: absolute;
top: 2px;
left: 2px;
z-index: 99;
&:not(:focus) {
width: 1px;
height: 1px;
padding: 0;
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
border: 0;
}
`;

return (
<Button className={customButtonClass} is='a' href={`${currentRoutePath}#main-content`} primary>
{t('Skip_to_main_content')}
</Button>
);
};

export default AccessibilityShortcut;
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { ReactElement, ReactNode } from 'react';
import React, { useEffect, useRef } from 'react';

import Sidebar from '../../../sidebar';
import AccessibilityShortcut from './AccessibilityShortcut';

const LayoutWithSidebar = ({ children }: { children: ReactNode }): ReactElement => {
const { isEmbedded: embeddedLayout } = useLayout();
Expand Down Expand Up @@ -46,10 +47,14 @@ const LayoutWithSidebar = ({ children }: { children: ReactNode }): ReactElement
className={[embeddedLayout ? 'embedded-view' : undefined, 'menu-nav'].filter(Boolean).join(' ')}
aria-hidden={Boolean(modal)}
>
<AccessibilityShortcut />
<PaletteStyleTag />
<SidebarPaletteStyleTag />
{!removeSidenav && <Sidebar />}
<main className={['rc-old', 'main-content', readReceiptsEnabled ? 'read-receipts-enabled' : undefined].filter(Boolean).join(' ')}>
<main
id='main-content'
className={['rc-old', 'main-content', readReceiptsEnabled ? 'read-receipts-enabled' : undefined].filter(Boolean).join(' ')}
>
{children}
</main>
</Box>
Expand Down
14 changes: 14 additions & 0 deletions apps/meteor/client/views/root/hooks/useUnreadMessages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useSession, useTranslation } from '@rocket.chat/ui-contexts';

export const useUnreadMessages = (): string | undefined => {
const t = useTranslation();
const unreadMessages = useSession('unread');

return (() => {
if (unreadMessages === '') {
return undefined;
}

return t('unread_messages_counter', { count: unreadMessages });
})();
};
1 change: 1 addition & 0 deletions apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -4775,6 +4775,7 @@
"Size": "Size",
"Skin_tone": "Skin tone",
"Skip": "Skip",
"Skip_to_main_content": "Skip to main content",
"SLA_Policy": "SLA Policy",
"SLA_Policies": "SLA Policies",
"SLA_removed": "SLA removed",
Expand Down
12 changes: 6 additions & 6 deletions apps/meteor/tests/e2e/homepage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ test.describe.serial('homepage', () => {
test('visibility and button functionality in custom body with empty custom content', async () => {
await test.step('expect default value in custom body', async () => {
await expect(
adminPage.locator('role=status[name="Admins may insert content html to be rendered in this white space."]'),
adminPage.locator('div >> text="Admins may insert content html to be rendered in this white space."'),
).toBeVisible();
});

Expand All @@ -60,7 +60,7 @@ test.describe.serial('homepage', () => {
});

await test.step('expect visibility tag to show "not visible"', async () => {
await expect(adminPage.locator('role=status[name="Not visible to workspace"]')).toBeVisible();
await expect(adminPage.locator('span >> text="Not visible to workspace"')).toBeVisible();
});
});
});
Expand All @@ -72,7 +72,7 @@ test.describe.serial('homepage', () => {

test('visibility and button functionality in custom body with custom content', async () => {
await test.step('expect custom body to be visible', async () => {
await expect(adminPage.locator('role=status[name="Hello admin"]')).toBeVisible();
await expect(adminPage.locator('div >> text="Hello admin"')).toBeVisible();
});

await test.step('expect correct state for card buttons', async () => {
Expand Down Expand Up @@ -101,7 +101,7 @@ test.describe.serial('homepage', () => {
});

await test.step('expect visibility tag to show "visible to workspace"', async () => {
await expect(adminPage.locator('role=status[name="Visible to workspace"]')).toBeVisible();
await expect(adminPage.locator('span >> text="Visible to workspace"')).toBeVisible();
});
});
});
Expand Down Expand Up @@ -188,7 +188,7 @@ test.describe.serial('homepage', () => {
});

test('expect custom body to be visible', async () => {
await expect(regularUserPage.locator('role=status[name="Hello"]')).toBeVisible();
await expect(regularUserPage.locator('div >> text="Hello"')).toBeVisible();
});

test.describe('enterprise edition', () => {
Expand All @@ -208,7 +208,7 @@ test.describe.serial('homepage', () => {
});

await test.step('expect custom body to be visible', async () => {
await expect(regularUserPage.locator('role=status[name="Hello"]')).toBeVisible();
await expect(regularUserPage.locator('div >> text="Hello"')).toBeVisible();
});
});
});
Expand Down
Loading