CI / Build & Quality Checks (push) Canceled after 1m33s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Secret scan (gitleaks) (push) Canceled after 0s
CI / Docker image build & smoke test (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s
- Skip link: following #main-content left focus on <body> (the <main> target wasn't focusable), so nothing was announced and the URL got a stray fragment. <main> is now tabIndex=-1 and the link focuses it. - Keyboard-shortcuts dialog (?): no role, and focus stayed in the timeline, so it opened silently. Now role="dialog" aria-modal, and focus moves into it (Escape still returns focus to where you were). - Reaction viewer (both the reaction context-menu path and "View Reactions"): same — role="dialog" aria-modal aria-label="Reactions", focus moves in. Keyboard-only checks (Playwright): Tab → skip link → Enter focuses <main>; Tab reaches the room list, Enter opens a room, typing lands in the composer, Enter sends (verified on the server); focus ring visible. "?" opens the dialog with focus inside, Escape returns to the same element, "?" in the composer stays text. Topic viewer, Search dialog and reaction viewer all return focus to their opener. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
66 lines
2.0 KiB
TypeScript
66 lines
2.0 KiB
TypeScript
import React, { ReactNode } from 'react';
|
|
import { Box, Line } from 'folds';
|
|
import { useAtom } from 'jotai';
|
|
import { bookmarksPanelAtom } from '../../state/bookmarksPanel';
|
|
import { BookmarksPanel } from '../../features/bookmarks/BookmarksPanel';
|
|
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
|
|
|
type ClientLayoutProps = {
|
|
nav: ReactNode;
|
|
children: ReactNode;
|
|
};
|
|
export function ClientLayout({ nav, children }: ClientLayoutProps) {
|
|
const [bookmarksOpen, setBookmarksOpen] = useAtom(bookmarksPanelAtom);
|
|
const screenSize = useScreenSizeContext();
|
|
|
|
return (
|
|
<>
|
|
<a
|
|
href="#main-content"
|
|
style={{
|
|
position: 'absolute',
|
|
top: '-40px',
|
|
left: 0,
|
|
padding: '8px 16px',
|
|
background: '#000',
|
|
color: '#fff',
|
|
zIndex: 9999,
|
|
borderRadius: '0 0 4px 0',
|
|
transition: 'top 0.1s',
|
|
}}
|
|
onFocus={(e) => {
|
|
(e.currentTarget as HTMLElement).style.top = '0';
|
|
}}
|
|
onBlur={(e) => {
|
|
(e.currentTarget as HTMLElement).style.top = '-40px';
|
|
}}
|
|
// #185 P1: following the fragment left focus on <body> (the <main> target
|
|
// wasn't focusable), so screen readers announced nothing and the URL got
|
|
// a stray #main-content. Move focus to <main> explicitly instead.
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
document.getElementById('main-content')?.focus();
|
|
}}
|
|
>
|
|
Skip to main content
|
|
</a>
|
|
<Box grow="Yes">
|
|
<Box shrink="No" as="nav" aria-label="Room navigation">
|
|
{nav}
|
|
</Box>
|
|
<Box grow="Yes" as="main" id="main-content" tabIndex={-1} style={{ outline: 'none' }}>
|
|
{children}
|
|
</Box>
|
|
{bookmarksOpen && (
|
|
<>
|
|
{screenSize === ScreenSize.Desktop && (
|
|
<Line variant="Background" direction="Vertical" size="300" />
|
|
)}
|
|
<BookmarksPanel onClose={() => setBookmarksOpen(false)} />
|
|
</>
|
|
)}
|
|
</Box>
|
|
</>
|
|
);
|
|
}
|