Skip to content

Commit 970366f

Browse files
fix(webapp): let keyboard users skip repeated navigation (#1620)
1 parent 38ff5ba commit 970366f

10 files changed

Lines changed: 176 additions & 12 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"hephaestus": patch
3+
---
4+
5+
Keyboard users can skip repeated navigation and move directly to the page's main content. The documentation now explains the current accessibility assessment and how to report barriers.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
title: Accessibility audit plan
3+
description: Scope and evidence requirements for evaluating the web application against WCAG 2.2 AA.
4+
---
5+
6+
# Accessibility audit plan
7+
8+
The audit follows [WCAG-EM](https://www.w3.org/WAI/test-evaluate/conformance/wcag-em/). Automated
9+
checks supplement rather than replace manual evaluation.
10+
11+
## Scope
12+
13+
- **Standard:** [WCAG 2.2](https://www.w3.org/TR/WCAG22/), Levels A and AA
14+
- **Application:** the Hephaestus SPA, including public, authentication, loading, empty, error, and
15+
permission-denied states
16+
- **Complete processes:** authentication, workspace creation, developer feedback, mentor, settings,
17+
workspace administration, and instance administration
18+
- **Outside the application scope:** linked external sites and identity-provider pages
19+
20+
Hephaestus controls and presentation around imported content remain in scope even when the content
21+
comes from a third party.
22+
23+
## Evaluation matrix
24+
25+
| Surface or process | Representative routes and states | Manual status |
26+
| --- | --- | --- |
27+
| Public and legal | `/`, `/about`, `/imprint`, `/privacy`, not found | Not evaluated |
28+
| Authentication | `/login`, workspace login, callback and error | Not evaluated |
29+
| Workspace creation | provider selection, GitHub, GitLab, validation and errors | Not evaluated |
30+
| Workspace home | dashboard, teams, achievements and user profiles | Not evaluated |
31+
| Developer feedback | reviews, trace, observations, findings, delivery and targets | Not evaluated |
32+
| Mentor | thread list, greeting, transcript, composer and copilot | Not evaluated |
33+
| Personal settings | settings, integrations and destructive actions | Not evaluated |
34+
| Workspace administration | members, practices, review operations, models, usage and integrations | Not evaluated |
35+
| Instance administration | users, workspaces, audit, catalogue, providers, models and usage | Not evaluated |
36+
37+
For each row, record the tested revision and deployment, route or process, state, applicable WCAG
38+
criteria, result, tester, date, and exact operating-system, browser, and assistive-technology versions.
39+
The manual matrix must include keyboard-only operation, NVDA with Firefox on Windows, and VoiceOver
40+
with Safari on macOS. Link every failure to an issue containing its reproduction steps, user impact,
41+
WCAG criterion, owner, and target release.
42+
43+
## Automated checks
44+
45+
`bun run --filter webapp test:storybook` runs the maintained Storybook states in Chromium and treats
46+
axe violations as errors. Record the revision, browser version, command result, and retained report
47+
with each audit. Automated results do not establish WCAG conformance.
48+
49+
Until every matrix row is evaluated and each failure is fixed or linked, the public accessibility
50+
statement must make no conformance claim.

docs/docusaurus.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,10 @@ const config: Config = {
215215
label: "Release Notes",
216216
href: "https://github.com/ls1intum/Hephaestus/releases",
217217
},
218+
{
219+
label: "Accessibility",
220+
to: "/user/accessibility",
221+
},
218222
{
219223
label: "Open Hephaestus",
220224
href: "https://hephaestus.aet.cit.tum.de",

docs/sidebars.contributor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const sidebars: SidebarsConfig = {
1515
"local-verification",
1616
"testing",
1717
"e2e-testing",
18+
"accessibility-audit-plan",
1819
"security-mutation-testing",
1920
"coding-guidelines",
2021
"api-error-handling",

docs/sidebars.user.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const sidebars: SidebarsConfig = {
1919
label: "Optional features",
2020
items: ["leaderboard", "achievements"],
2121
},
22+
{ type: "doc", id: "accessibility", label: "Accessibility" },
2223
],
2324
};
2425

docs/user/accessibility.mdx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
title: Accessibility
3+
description: The accessibility status, testing approach, and support channels for Hephaestus.
4+
---
5+
6+
# Accessibility statement
7+
8+
**Last reviewed: 30 August 2026**
9+
10+
Hephaestus aims to be usable by people who navigate with a keyboard, magnify content, use speech
11+
input, or use a screen reader.
12+
13+
## Conformance status
14+
15+
Conformance of the Hephaestus web application with
16+
[WCAG 2.2 Level AA](https://www.w3.org/TR/WCAG22/) **has not yet been established**. Automated checks
17+
pass for the maintained Storybook states, but the required keyboard and assistive-technology review
18+
of the integrated application is incomplete. We therefore make no WCAG conformance claim yet.
19+
20+
This statement covers the Hephaestus single-page application, including its presentation of imported
21+
content. Linked external sites and identity-provider pages are outside its scope.
22+
23+
## Current evidence
24+
25+
The current assessment uses automated component checks in Chromium and source analysis. Integrated
26+
keyboard and screen-reader evaluation is incomplete, so these results do not establish WCAG
27+
conformance. The scope and evidence requirements are in the
28+
[accessibility audit plan](/contributor/accessibility-audit-plan).
29+
30+
## Known limitations
31+
32+
The integrated keyboard, NVDA, and VoiceOver evaluations are incomplete, so accessibility barriers
33+
may remain.
34+
35+
## Feedback and contact
36+
37+
If an accessibility problem prevents you from using Hephaestus, open an
38+
[accessibility issue](https://github.com/ls1intum/Hephaestus/issues/new?labels=webapp&title=%5BA11y%5D%20).
39+
Include the page, what you were trying to do, browser, assistive technology, and version when
40+
possible. Do not include private workspace data in a public report.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { expect, userEvent } from "storybook/test";
3+
4+
import { SkipToContent } from "./SkipToContent";
5+
6+
const meta = {
7+
title: "Common/Skip to content",
8+
component: SkipToContent,
9+
parameters: { layout: "fullscreen" },
10+
tags: ["autodocs"],
11+
} satisfies Meta<typeof SkipToContent>;
12+
13+
export default meta;
14+
type Story = StoryObj<typeof meta>;
15+
16+
export const KeyboardNavigation: Story = {
17+
render: () => (
18+
<>
19+
<SkipToContent />
20+
<main id="main-content" tabIndex={-1} className="p-6">
21+
<h1>Main content</h1>
22+
<button type="button">First action</button>
23+
</main>
24+
</>
25+
),
26+
play: async ({ canvas }) => {
27+
await userEvent.tab();
28+
const skipLink = canvas.getByRole("link", { name: "Skip to main content" });
29+
await expect(skipLink).toHaveFocus();
30+
await expect(skipLink).toBeVisible();
31+
32+
await userEvent.keyboard("{Enter}");
33+
await expect(canvas.getByRole("main")).toHaveFocus();
34+
35+
await userEvent.tab();
36+
await expect(canvas.getByRole("button", { name: "First action" })).toHaveFocus();
37+
},
38+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export function SkipToContent() {
2+
return (
3+
<a
4+
href="#main-content"
5+
className="sr-only fixed left-4 top-4 z-[100] rounded-md bg-background px-4 py-2 text-sm font-medium text-foreground shadow-lg ring-2 ring-ring focus:not-sr-only"
6+
onClick={(event) => {
7+
const target = event.currentTarget.ownerDocument.getElementById(
8+
event.currentTarget.hash.slice(1),
9+
);
10+
if (!target) return;
11+
event.preventDefault();
12+
target.focus();
13+
}}
14+
>
15+
Skip to main content
16+
</a>
17+
);
18+
}

webapp/src/components/ui/sidebar.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,13 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
284284
);
285285
}
286286

287-
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
287+
/**
288+
* ⚠️ Diverges from the shadcn registry: the inset is layout-only. Route layouts own the main
289+
* landmark so global footers can remain outside it.
290+
*/
291+
function SidebarInset({ className, ...props }: React.ComponentProps<"div">) {
288292
return (
289-
<main
293+
<div
290294
data-slot="sidebar-inset"
291295
className={cn(
292296
"bg-background md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2 relative flex w-full flex-1 flex-col",

webapp/src/routes/__root.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { CookieConsentBanner } from "@/components/consent/CookieConsentBanner";
2222
import Footer from "@/components/core/Footer";
2323
import Header from "@/components/core/Header";
2424
import { AppSidebar, type SidebarContext } from "@/components/core/sidebar/AppSidebar";
25+
import { SkipToContent } from "@/components/core/SkipToContent";
2526
import { StandardPageSurface } from "@/components/core/StandardPageSurface";
2627
import { Chat } from "@/components/mentor/Chat";
2728
import { Copilot } from "@/components/mentor/Copilot";
@@ -78,9 +79,10 @@ function RootLayout() {
7879
return (
7980
<>
8081
<HeadContent />
82+
<SkipToContent />
8183
<CookieConsentBanner />
8284
<ProviderColorScope>
83-
<main>
85+
<main id="main-content" tabIndex={-1}>
8486
<Outlet />
8587
</main>
8688
</ProviderColorScope>
@@ -92,6 +94,7 @@ function RootLayout() {
9294
return (
9395
<>
9496
<HeadContent />
97+
<SkipToContent />
9598
{/* Rendered early so keyboard/AT users reach the consent region before the app chrome. */}
9699
<CookieConsentBanner />
97100
<ImpersonationBanner />
@@ -103,7 +106,7 @@ function RootLayout() {
103106
style={{ marginRight: "var(--right-sidebar-width, 0)" }}
104107
>
105108
<HeaderContainer />
106-
<div className="flex min-h-0 flex-1 flex-col">
109+
<main id="main-content" tabIndex={-1} className="flex min-h-0 flex-1 flex-col">
107110
{surface === "standard" ? (
108111
<StandardPageSurface className="flex-1">
109112
<Outlet />
@@ -117,13 +120,13 @@ function RootLayout() {
117120
<Outlet />
118121
</div>
119122
)}
120-
{surface !== "fullscreen" && (
121-
<Footer
122-
buildInfo={environment.buildInfo}
123-
isProduction={environment.deployment.isProduction}
124-
/>
125-
)}
126-
</div>
123+
</main>
124+
{surface !== "fullscreen" && (
125+
<Footer
126+
buildInfo={environment.buildInfo}
127+
isProduction={environment.deployment.isProduction}
128+
/>
129+
)}
127130
</SidebarInset>
128131
</SidebarProvider>
129132
</ProviderColorScope>
@@ -141,7 +144,7 @@ export const Route = createRootRouteWithContext<MyRouterContext>()({
141144
component: RootLayout,
142145
notFoundComponent: () => (
143146
<div className="mx-auto flex w-full max-w-2xl flex-col items-center justify-center py-16 text-center">
144-
<h2 className="text-3xl font-bold mb-4">Page Not Found</h2>
147+
<h1 className="text-3xl font-bold mb-4">Page Not Found</h1>
145148
<p className="text-muted-foreground mb-8">
146149
The page you're looking for doesn't exist or you don't have permission to view it.
147150
</p>

0 commit comments

Comments
 (0)