Skip to content

Commit

Permalink
feat: Skip to main content shortcut and useDocumentTitle (#30680)
Browse files Browse the repository at this point in the history
Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com>
  • Loading branch information
dougfabris and ggazzo authored Dec 5, 2023
1 parent 0681c45 commit dd5fd6d
Show file tree
Hide file tree
Showing 26 changed files with 249 additions and 40 deletions.
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;
33 changes: 33 additions & 0 deletions apps/meteor/client/views/root/MainLayout/AccessibilityShortcut.tsx
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

0 comments on commit dd5fd6d

Please sign in to comment.