nicolium: add skip links

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2026-03-17 18:27:30 +01:00
parent 8ff61c422e
commit 3957b40572
5 changed files with 62 additions and 11 deletions

View File

@ -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>
)}

View File

@ -139,6 +139,7 @@ const Main: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ children, classN
},
className,
)}
tabIndex={-1}
>
{children}
</main>

View File

@ -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,

View File

@ -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",

View File

@ -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;