fix(webapp): basic accessibility improvements - #1236
Conversation
Accessible names: the mobile navigation toggle and the create button in the CRUD lists are icon-only and were exposed to assistive technology without a name. Two dialog close controls were bare <v-icon> elements carrying a click handler, so they were neither focusable nor operable by keyboard; they now use the same button pattern as the close control in TOTPVerifyDialog. Headings: page titles rendered as <div>, so views such as the login page had no heading at all and screen reader users had nothing to navigate by. Toolbar titles now render as <h1> through Vuetify's tag prop, and the step titles on the domain setup page as <h2>. Neither changes appearance. Progress bar: it appears on every request and, while active, was exposed to assistive technology as an unnamed progressbar, which is the reported irritation. It is decorative, so it is now hidden from the accessibility tree; the visual behaviour is unchanged. Follows the review on desec-io#890 by adding accessible names only where there is no visible text, rather than duplicating text the accessible name is already computed from. Closes desec-io#472
e100fbf to
b9f4a8f
Compare
| <v-btn | ||
| icon | ||
| variant="text" | ||
| aria-label="Close dialog" | ||
| @click.stop="close" | ||
| > | ||
| <v-icon :icon="mdiClose" /> | ||
| </v-btn> |
There was a problem hiding this comment.
I now realize this also happens for other dialog (like the 2FA setup email notice from TOTPVerifyDialog.vue). It's not required for this PR, but if you have a solution idea, that'd be appreciated!
There was a problem hiding this comment.
Had a look at this one. It reproduces only when creating a domain: the create dialog closes as the setup dialog opens, so the element that had focus disappears, and Vuetify moves focus to the first focusable child, which is the close button. Opening the same dialog from the info icon on a domain row doesn't do it.
I tried some obvious one-liners, making the card itself focusable via tabindex or autofocus. Both stop the close button getting focus, but focus then lands on the + button behind the dialog, because the closing create dialog restores focus to its own activator. From there, tabbing walks straight into the table behind the overlay. That seems like a worse trade than the focus ring.
So maybe best to leave it as is here, and treating initial focus in dialogs as its own issue, including TOTPVerifyDialog since it has the same shape.

Closes #472
This picks up the three items in #472. It builds on #890 by @xyhhx, which got a review but went quiet and no longer merges after the Vuetify 3 migration. Following that review, accessible names are added only where there is no visible text, rather than duplicating text the accessible name is already computed from — a
<v-btn aria-label="Log In">Log In</v-btn>gains nothing and can hurt voice control.Accessible names
App.vuehad no name (it shows as a barebuttonwith animgin the accessibility tree). It now hasaria-labelplusaria-expanded, since it toggles the drawer.headlines.create, so it reads as "Create New Domain" rather than nothing.<v-icon>elements carrying a click handler. Those are not focusable and not exposed as controls at all, so the dialogs could not be closed by keyboard via that affordance. They now use the samev-btn icon variant="text" aria-label="Close dialog"pattern already used by the close control inTOTPVerifyDialog.vue.Worth noting what is not here: the show/hide password toggles get Vuetify's generated name, "Password appended action". That is poor but not missing, and fixing it properly means replacing five
@click:append-innerhandlers with labelled buttons in theappend-innerslot. Happy to do it in a follow-up if you want it.Headings
The page titles were
<v-toolbar-title>, which renders a<div>. The login page, for example, had no heading element at all, so there was nothing to navigate by. Vuetify'stagprop fixes this with no visual change:<h1>; the two on the donate page become<h2>, since that page already has an<h1><h2>/custom-setup/{domain}went from zero headings to:This is what @robin24 suggested in the issue thread.
Progress bar
@nils-wisiol asked why it was irritating and couldn't see it visually. The reason isn't visual.
VProgressLinearrendersaria-hidden="false"whenever it is active, so on every single request an unnamedprogressbarenters the accessibility tree and leaves again. It carries no information a screen reader user can act on — the outcome is announced by the result or the error alert — so it is marked decorative.That keeps the bar exactly as it is for sighted users, rather than removing it as was floated. If you'd rather it went away entirely, or want a spinner instead as @peterthomassen suggested, say so and I'll change it.
Verification
Checked in a browser against the accessibility tree and the DOM, not just by reading source:
H1 Log In, nav button named witharia-expandedwork_countdriven directly: onmainit isaria-hidden="false"while working; with this branch it stays"true". Visual state (height: 0, no--activeclass) is identical on both, so this changes assistive-technology exposure and nothing elsenpm run buildandnpm test(4 passed) passTwo caveats I'd rather state than paper over. The CRUD create/destroy dialogs and
DomainSetupDialogare behind login, so I verified those by source and by matching the existingTOTPVerifyDialogpattern, not visually. And where a heading replaced an inline<span>inside av-card-title(which isdisplay: block), I keptd-inlineon the heading so the layout is byte-identical rather than adding flex — semantics change, appearance does not.Unrelated, noticed while working:
npm run lintfails onmaintoo, because the globsrc/**/*.{vue,js,json}matchessrc/modules/qrcode.vue, which is a directory. Left alone here.