feat: Skip to main content shortcut and `useDocumentTitle` (#30680)
Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com>pull/31161/head
parent
0681c455fc
commit
dd5fd6d2c8
@ -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 |
||||
@ -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; |
||||
@ -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; |
||||
@ -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 }); |
||||
})(); |
||||
}; |
||||
@ -0,0 +1,26 @@ |
||||
import { renderHook } from '@testing-library/react-hooks'; |
||||
|
||||
import { useDocumentTitle } from './useDocumentTitle'; |
||||
|
||||
const DEFAULT_TITLE = 'Default Title'; |
||||
const EXAMPLE_TITLE = 'Example Title'; |
||||
|
||||
it('should return the default title', () => { |
||||
const { result } = renderHook(() => useDocumentTitle(DEFAULT_TITLE)); |
||||
|
||||
expect(result.current.title).toBe(DEFAULT_TITLE); |
||||
}); |
||||
|
||||
it('should return the default title and empty key value if refocus param is false', () => { |
||||
const { result } = renderHook(() => useDocumentTitle(DEFAULT_TITLE, false)); |
||||
|
||||
expect(result.current.title).toBe(DEFAULT_TITLE); |
||||
expect(result.current.key).toBe(''); |
||||
}); |
||||
|
||||
it('should return the default title and the example title concatenated', () => { |
||||
renderHook(() => useDocumentTitle(DEFAULT_TITLE)); |
||||
const { result } = renderHook(() => useDocumentTitle(EXAMPLE_TITLE)); |
||||
|
||||
expect(result.current.title).toBe(`${EXAMPLE_TITLE} - ${DEFAULT_TITLE}`); |
||||
}); |
||||
@ -0,0 +1,54 @@ |
||||
import { Emitter } from '@rocket.chat/emitter'; |
||||
import { useCallback, useEffect } from 'react'; |
||||
import { useSyncExternalStore } from 'use-sync-external-store/shim'; |
||||
|
||||
const ee = new Emitter<{ |
||||
change: void; |
||||
}>(); |
||||
|
||||
const titles = new Set<{ |
||||
title?: string; |
||||
refocus?: boolean; |
||||
}>(); |
||||
|
||||
const useReactiveDocumentTitle = (): string => |
||||
useSyncExternalStore( |
||||
useCallback((callback) => ee.on('change', callback), []), |
||||
(): string => |
||||
Array.from(titles) |
||||
.reverse() |
||||
.map(({ title }) => title) |
||||
.join(' - '), |
||||
); |
||||
|
||||
const useReactiveDocumentTitleKey = (): string => |
||||
useSyncExternalStore( |
||||
useCallback((callback) => ee.on('change', callback), []), |
||||
(): string => |
||||
Array.from(titles) |
||||
.filter(({ refocus }) => refocus) |
||||
.map(({ title }) => title) |
||||
.join(' - '), |
||||
); |
||||
|
||||
export const useDocumentTitle = (documentTitle?: string, refocus = true) => { |
||||
useEffect(() => { |
||||
const titleObj = { |
||||
title: documentTitle, |
||||
refocus, |
||||
}; |
||||
|
||||
if (titleObj.title) { |
||||
titles.add(titleObj); |
||||
} |
||||
|
||||
ee.emit('change'); |
||||
|
||||
return () => { |
||||
titles.delete(titleObj); |
||||
ee.emit('change'); |
||||
}; |
||||
}, [documentTitle, refocus]); |
||||
|
||||
return { title: useReactiveDocumentTitle(), key: useReactiveDocumentTitleKey() }; |
||||
}; |
||||
@ -1,3 +1,4 @@ |
||||
export * from './components'; |
||||
export * from './hooks/useFeaturePreview'; |
||||
export * from './hooks/useFeaturePreviewList'; |
||||
export * from './hooks/useDocumentTitle'; |
||||
|
||||
Loading…
Reference in new issue