fix(a11y): replace clickable divs with button elements in PdfViewer and AnnotationLayer #114
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
PdfViewer.svelteandAnnotationLayer.svelteuse<div onclick={...}>for interactive elements. These are invisible to keyboard navigation and screen readers — users who cannot use a mouse cannot interact with annotations.Fix
Replace all interactive
<div>elements with<button>, or add the minimum required attributes where a<button>would break layout:Affected Files
src/lib/components/PdfViewer.sveltesrc/lib/components/AnnotationLayer.svelte(if applicable)Acceptance Criteria
aria-label👨💻 Felix Brandt — Senior Fullstack Developer
Questions & Observations
<button>) is always preferred. A<button>gets keyboard support, focus management, and screen reader semantics for free. Only fall back to Option B (div + role="button") if there is a demonstrated layout breakage — and even then, challenge whether the layout constraint is real.<div>with a<button>in a PDF viewer context often causes styling issues because browsers apply default button styles (border, background, padding). The implementation must include an explicit CSS reset or Tailwind reset classes (appearance-none bg-transparent border-0 p-0 cursor-pointer).onkeydownhandler in Option B is incomplete — it only handlesEnterandSpacebut misseskeyupfor Space (standard button behaviour fires onkeyupfor space). If using Option A, this is handled automatically by the browser.Suggestions
@testing-library/svelte: render the component,getByRole('button', { name: /annotations/i }), thenuserEvent.keyboard('{Enter}')anduserEvent.keyboard(' ')— assert the toggle fires for both.aria-pressedtoggles correctly: after activation it should reflect the new state.onclickhandler itself doesn't change, so they should.🔒 Nora "NullX" Steiner — Application Security Engineer
Questions & Observations
<div>elements are occasionally used to bypass CSPscript-srcrestrictions via event handler injection — the attack surface is narrow but real. Semantic<button>elements are also harder to target with certain clickjacking techniques. This is a minor security co-benefit of the a11y fix.aria-labelmust not be user-controlled. The issue correctly usesm.pdf_toggle_annotations()(a Paraglide i18n function) for the label. Confirm this value is statically defined in the locale files and never derived from user input — a user-controlledaria-labelcould be a minor XSS vector if the value is ever rendered unsanitised elsewhere.Suggestions
axe-playwrighton the document detail page and confirm zero WCAG violations for keyboard accessibility.🧪 Sara Holt — QA Engineer & Test Strategist
Test Strategy
Component tests (
@testing-library/svelte):E2E (Playwright + axe):
Edge Cases to Cover
AnnotationLayer.sveltemust be checked — not just the toggle inPdfViewer.svelte🏗️ Markus Keller — Application Architect
Questions & Observations
<div role="button">with atabindexand anonkeydownhandler is three attributes doing the work that one<button>element does natively. Option B exists for documentation completeness, but in practice there is almost never a layout reason to prefer it. The comment in the issue is correct: challenge the layout constraint before reaching for Option B.Tabactually reaches the new<button>elements — the keyboard event handling in PDF.js can absorb keystrokes before they reach the DOM. This is a runtime concern, not a code concern.Suggestions
PdfViewer.svelteandAnnotationLayer.svelte. Don't sweep other components for similar patterns in the same PR — keep the diff reviewable.<div>elements elsewhere in the codebase, open a separate issue for them rather than bundling into this one.🎨 Leonie Voss — UI/UX Designer & Accessibility Strategist
Questions & Observations
<div>pattern is invisible to all of them.<div>is replaced with a<button>, the browser applies a default focus ring. However, this project likely has CSS resets (Tailwind'spreflightbase styles reset button appearance). Explicitly verify the focus state is visible after the change — check both the default browser ring and any custom focus styles in the existing CSS.outline: nonewithout a replacement is a WCAG 2.4.7 violation.aria-labellanguage. The annotation toggle label must be translated —m.pdf_toggle_annotations()is correct. Make sure all three locale files have a meaningful label, not just English. For German: "Anmerkungen ein-/ausblenden"; Spanish: "Mostrar/ocultar anotaciones".aria-pressedstate must update on toggle. Screen readers announce the pressed state to users —aria-pressed={showAnnotations}must be reactive and reflect the actual current state.Suggestions
min-w-[44px] min-h-[44px]if the natural size is smaller.🚀 Tobias Wendt — DevOps & Platform Engineer
No infrastructure concerns — this is a frontend component change with no deployment, config, or Docker impact.
One CI note: if axe-playwright is added to the E2E suite as part of this issue (which it should be), make sure the E2E job in the Gitea Actions workflow has the axe-playwright package installed. Check the
package.jsondevDependencies and the CI workflow's npm install step — no separate action needed, it installs with the rest of the dependencies.