[Regression on closed #366] all six route-level loading.tsx skeletons have lost their ARIA live region — route changes are silent for screen readers

Aperta
#842 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
3/5
Tempo stimato
1-2 giorni
Idoneità per principianti
72/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
next.js, react, tailwindcss, typescript

Direzione di ricerca

Read the six route boundaries at src/app/loading.tsx, src/app/dashboard/loading.tsx, src/app/issues/loading.tsx, src/app/issues/[id]/loading.tsx, src/app/milestones/loading.tsx, and src/app/reputation/[handle]/loading.tsx. Compare their accessibility pattern with src/app/auth/callback/page.tsx and check the loading-boundary test file mentioned for #667. Done when each boundary announces loading accessibly, supports reduced motion, and has test coverage for role="status".

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

accessibility bug

Location

All six route-level loading boundaries, none of which carries a live region:

  • src/app/loading.tsx:5
  • src/app/dashboard/loading.tsx:5
  • src/app/issues/loading.tsx:3
  • src/app/issues/[id]/loading.tsx
  • src/app/milestones/loading.tsx
  • src/app/reputation/[handle]/loading.tsx

Problem

Each file returns a purely visual skeleton — a container with animate-pulse and a grid of grey placeholder <div>s — and nothing else:

export default function Loading() {
  return (
    <div className="mx-auto max-w-6xl px-6 py-12 animate-pulse">
      <div className="h-8 w-48 rounded-lg bg-slate-200 dark:bg-slate-800" />
      ...

grep -rn 'role=\|aria-' src/app/**/loading.tsx returns zero matches across all six files. There is no role="status", no aria-live, no aria-busy, and no visually-hidden text naming what is loading. Nothing higher in the tree supplies one either: src/app/layout.tsx wraps {children} in a bare <main id="main-content"> with no live region, and Next.js renders a loading.tsx as the Suspense fallback in that same slot.

This is precisely what closed issue #366 was about — "None of the five route-level loading.tsx skeleton screens have an ARIA live-region/status role" — and the fix is no longer present in any of the files, including the sixth one added since. For contrast, the pattern is still correct elsewhere in the app: src/app/auth/callback/page.tsx:15 and src/app/auth/callback/CallbackClient.tsx:154 both use <p role="status" aria-live="polite">, so the convention exists and these six files diverge from it.

Why it matters

Client-side navigation in the App Router does not reload the document, so a screen reader gets no page-load announcement. The skeleton is the only indication that anything is happening, and it is announced as nothing at all — a sighted user sees a shimmer, a screen-reader user hears silence between activating a link and the new page's content appearing. On /issues/[id] and /reputation/[handle], where the fetch can take a full second against a live backend, the user has no way to tell whether their activation registered, and is likely to activate the link again.

animate-pulse also runs unconditionally, so the skeleton ignores prefers-reduced-motion — a smaller instance of what closed issue #258 covered app-wide.

Reproduction

  1. npm run dev, then enable VoiceOver (macOS) or NVDA (Windows).
  2. From /issues, activate a bounty card link.
  3. The skeleton from src/app/issues/[id]/loading.tsx renders. Nothing is announced; the screen reader stays silent until the resolved page content lands and focus happens to be moved there.
  4. curl -s http://localhost:3000/issues | grep -c 'role="status"' against a slow backend returns 0 for the skeleton markup.

Suggested fix

Give each skeleton a polite live region with an accessible name, and mark the region busy while it is up. For example, in each loading.tsx:

<div
  role="status"
  aria-live="polite"
  aria-busy="true"
  className="mx-auto max-w-6xl px-6 py-12 animate-pulse motion-reduce:animate-none"
>
  <span className="sr-only">Loading bounties…</span>
  {/* existing placeholder divs, with aria-hidden="true" on the wrapper that holds them */}

Because all six files repeat the same structure, the cleanest version is a single shared <RouteSkeleton label="…"> component in src/components/ui/ that owns the live region, the sr-only label and the motion-reduce:animate-none class, with each loading.tsx passing only its own label and placeholder shape. That also stops the next new route from reintroducing the gap.

Add an assertion to the loading-boundary test file (added for closed #667) that every loading.tsx default export renders an element with role="status", so this cannot silently regress a third time.

Lingua principale
TypeScript
Stelle
1
Fork
94
Merge medio
1h 2m
PR unite (30g)
25

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di MergeFi/frontend

Tutte le issue di MergeFi/frontend

Issue simili

Altre issue su TypeScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.