nicolium: add skip links
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
@ -24,11 +24,12 @@ const FormGroup: React.FC<IFormGroup> = (props) => {
|
||||
|
||||
let firstChild;
|
||||
if (React.isValidElement(inputChildren[0])) {
|
||||
firstChild = React.cloneElement(
|
||||
inputChildren[0],
|
||||
firstChild = React.cloneElement(inputChildren[0], {
|
||||
// @ts-expect-error
|
||||
{ id: formFieldId },
|
||||
);
|
||||
id: formFieldId,
|
||||
'aria-invalid': hasError,
|
||||
'aria-describedby': hasError ? `error-${formFieldId}` : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
@ -53,7 +54,11 @@ const FormGroup: React.FC<IFormGroup> = (props) => {
|
||||
|
||||
{hasError && (
|
||||
<div>
|
||||
<p data-testid='form-group-error' className='⁂-form-group__error'>
|
||||
<p
|
||||
id={`error-${formFieldId}`}
|
||||
data-testid='form-group-error'
|
||||
className='⁂-form-group__error'
|
||||
>
|
||||
{errors.join(', ')}
|
||||
</p>
|
||||
</div>
|
||||
@ -96,7 +101,11 @@ const FormGroup: React.FC<IFormGroup> = (props) => {
|
||||
{inputChildren.filter((_, i) => i !== 0)}
|
||||
|
||||
{hasError && (
|
||||
<p data-testid='form-group-error' className='⁂-form-group__error'>
|
||||
<p
|
||||
id={`error-${formFieldId}`}
|
||||
data-testid='form-group-error'
|
||||
className='⁂-form-group__error'
|
||||
>
|
||||
{errors.join(', ')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@ -139,6 +139,7 @@ const Main: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ children, classN
|
||||
},
|
||||
className,
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
|
||||
@ -2,6 +2,7 @@ import { Outlet, useNavigate } from '@tanstack/react-router';
|
||||
import clsx from 'clsx';
|
||||
import React, { Suspense, useEffect, useRef } from 'react';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { register as registerPushNotifications } from '@/actions/push-notifications/registerer';
|
||||
import SidebarNavigation from '@/components/navigation/sidebar-navigation';
|
||||
@ -22,11 +23,8 @@ import { usePrefetchNotifications } from '@/queries/notifications/use-notificati
|
||||
import { useFilters } from '@/queries/settings/use-filters';
|
||||
import { scheduledStatusesQueryOptions } from '@/queries/statuses/scheduled-statuses';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useInstance } from '@/stores/instance';
|
||||
import { useInstanceStore } from '@/stores/instance';
|
||||
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
||||
// Without this it ends up in ~8 very commonly used bundles.
|
||||
import '@/components/statuses/status';
|
||||
import { useInstance, useInstanceStore } from '@/stores/instance';
|
||||
import { useModalsActions } from '@/stores/modals';
|
||||
import { useShoutboxSubscription } from '@/stores/shoutbox';
|
||||
import { useIsDropdownMenuOpen } from '@/stores/ui';
|
||||
import { useIsStandalone } from '@/utils/state';
|
||||
@ -39,6 +37,9 @@ import {
|
||||
StatusHoverCard,
|
||||
} from './util/async-components';
|
||||
import GlobalHotkeys from './util/global-hotkeys';
|
||||
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
||||
// Without this it ends up in ~8 very commonly used bundles.
|
||||
import '@/components/statuses/status';
|
||||
|
||||
const UI: React.FC = React.memo(() => {
|
||||
const navigate = useNavigate();
|
||||
@ -50,6 +51,7 @@ const UI: React.FC = React.memo(() => {
|
||||
const vapidKey =
|
||||
useAuthStore((state) => state.app?.vapid_key) ?? instance.configuration.vapid.public_key;
|
||||
const client = useClient();
|
||||
const { openModal } = useModalsActions();
|
||||
|
||||
const isDropdownMenuOpen = useIsDropdownMenuOpen();
|
||||
const standalone = useIsStandalone();
|
||||
@ -84,6 +86,14 @@ const UI: React.FC = React.memo(() => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleSkipToContent = () => {
|
||||
document.querySelector('main')?.focus();
|
||||
};
|
||||
|
||||
const handleOpenHotkeysModal = () => {
|
||||
openModal('HOTKEYS');
|
||||
};
|
||||
|
||||
/** Load initial data when a user is logged in */
|
||||
const loadAccountData = () => {
|
||||
if (!account) return;
|
||||
@ -150,6 +160,17 @@ const UI: React.FC = React.memo(() => {
|
||||
return (
|
||||
<GlobalHotkeys node={node}>
|
||||
<div ref={node} style={style}>
|
||||
<div className='⁂-skip-links'>
|
||||
<button onClick={handleSkipToContent}>
|
||||
<FormattedMessage id='skip_links.skip_to_content' defaultMessage='Skip to content' />
|
||||
</button>
|
||||
<button onClick={handleOpenHotkeysModal}>
|
||||
<FormattedMessage
|
||||
id='navigation.keyboard_shortcuts'
|
||||
defaultMessage='Keyboard shortcuts'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className={clsx('⁂-dragging-area', {
|
||||
'⁂-dragging-area--dragging': isDragging,
|
||||
|
||||
@ -1836,6 +1836,7 @@
|
||||
"signup_panel.subtitle": "Sign up now to discuss what's happening.",
|
||||
"signup_panel.title": "New to {site_title}?",
|
||||
"site_preview.preview": "Preview",
|
||||
"skip_links.skip_to_content": "Skip to content",
|
||||
"status.add_known_language": "Do not auto-translate posts in {language}.",
|
||||
"status.admin_account": "Moderate @{name}",
|
||||
"status.admin_status": "Open this post in the moderation interface",
|
||||
|
||||
@ -623,6 +623,25 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.⁂-skip-links {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: -4rem;
|
||||
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
|
||||
padding: 0.5rem;
|
||||
|
||||
&:focus-within {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
@include mixins.button($theme: secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.⁂-dragging-area {
|
||||
pointer-events: none;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user