From 85301f3cf463e60f2c4ebff71a07a08b30b0286c Mon Sep 17 00:00:00 2001 From: swithek <52840391+swithek@users.noreply.github.com> Date: Mon, 7 Sep 2026 23:12:30 +0200 Subject: [PATCH] improve a11y attributes --- e2e/tests/search.test.ts | 5 +-- web/CLAUDE.md | 4 +-- web/app/components/DocumentDeletionModal.vue | 25 +++++++------- web/app/components/NotificationBox.vue | 3 ++ web/app/components/SearchModal.nuxt.test.ts | 5 ++- web/app/components/SearchModal.vue | 5 +-- web/app/components/TagDeletionModal.vue | 25 +++++++------- .../components/editor/IconPickerProvider.vue | 4 ++- .../components/editor/NameEditor.nuxt.test.ts | 6 ++-- web/app/components/editor/NameEditor.vue | 16 +++++---- web/app/components/editor/ai/ChatBox.vue | 1 + .../blocks/figma/FigmaBlock.nuxt.test.ts | 10 +++--- .../editor/blocks/figma/FigmaBlock.vue | 13 +++++--- .../editor/blocks/file/FileBlock.nuxt.test.ts | 10 +++--- .../editor/blocks/file/FileBlock.vue | 9 +++-- .../blocks/image/ImageBlock.nuxt.test.ts | 12 +++---- .../editor/blocks/image/ImageBlock.vue | 11 ++++--- .../VisualizationTypeSelect.nuxt.test.ts | 2 +- .../config-modal/VisualizationTypeSelect.vue | 33 ++++++++++++------- .../split-documentation/BottomAction.vue | 1 + .../comments/CommentIndicator.nuxt.test.ts | 10 +++--- .../editor/comments/CommentIndicator.vue | 10 +++--- .../comments/CommentIndicatorContainer.vue | 4 +++ .../editor/drag-handle/BlockHandle.vue | 15 +++++++++ .../github-tracking/FileSelectInputItem.vue | 7 ++-- .../components/editor/link/LinkBubbleMenu.vue | 13 ++++++++ .../components/editor/link/LinkCreateMenu.vue | 2 ++ .../editor/slash/CommandButton.nuxt.test.ts | 14 ++++++-- .../components/editor/slash/CommandButton.vue | 5 +-- .../components/editor/slash/CommandList.vue | 2 +- .../AccountDeletionAction.nuxt.test.ts | 4 +-- .../settings/AccountDeletionAction.vue | 4 +-- .../DataSourceRemovalAction.nuxt.test.ts | 10 ++++-- .../settings/DataSourceRemovalAction.vue | 25 +++++++------- .../settings/DataSourceUpsertAction.vue | 4 +-- .../components/settings/EmailChangeAction.vue | 4 +-- .../settings/PasswordChangeAction.vue | 29 ++++++++-------- .../components/settings/ProfileSection.vue | 1 + .../settings/WorkspaceInvitationAction.vue | 4 +-- .../settings/WorkspaceMemberRemovalAction.vue | 25 +++++++------- .../components/settings/WorkspaceSection.vue | 1 + .../settings/WorkspaceURLChangeAction.vue | 4 +-- web/app/components/test-helpers.ts | 22 ++++++++++++- web/eslint.config.mjs | 19 +++++++++++ web/i18n/locales/en/editor.json | 7 +++- web/package.json | 1 + web/pnpm-lock.yaml | 25 ++++++++++++++ web/vitest.config.ts | 9 ----- 48 files changed, 316 insertions(+), 164 deletions(-) diff --git a/e2e/tests/search.test.ts b/e2e/tests/search.test.ts index d540117f..e9332539 100644 --- a/e2e/tests/search.test.ts +++ b/e2e/tests/search.test.ts @@ -83,8 +83,9 @@ test.describe("search", () => { await searchFor(dialog, "marmalade", result) // the text exists on the draft alone, so that is the branch the - // hit names and the one the link opens - await expect(result.getByTestId("search-result-branch")).toHaveText("draft") + // hit names and the one the link opens. The branch name is the last + // span of the result's meta line + await expect(result.locator("span:last-child")).toHaveText("draft") await result.click() await expect(page).toHaveURL(/New-Page-[a-z0-9]{20}\?branch=[a-z0-9]{20}/) diff --git a/web/CLAUDE.md b/web/CLAUDE.md index 601078e8..79148df2 100644 --- a/web/CLAUDE.md +++ b/web/CLAUDE.md @@ -117,13 +117,13 @@ Form validation (vee-validate) uses its own internal messages — see [README.md - **shadcn-vue** components live in [app/components/shadcn/ui/](app/components/shadcn/ui/) with prefix `ShadcnUi`. Configured in [components.json](components.json). - **Tailwind v4** via `@tailwindcss/vite`, theme in [app/assets/css/main.css](app/assets/css/main.css). - Icons via `@nuxt/icon` (CSS mode). The client bundle holds the icons the scanner finds in app code, plus — on the desktop build only — the document title icons listed by `selectableIconList()` in [app/utils/icon.ts](app/utils/icon.ts). The web build resolves those on demand through the server so they stay off the startup path. The picker itself does not go through ``: [modules/icon-picker-css.ts](modules/icon-picker-css.ts) generates one stylesheet with a rule per selectable icon, served as `virtual:icon-picker.css` and imported by the picker provider once the page's load event has fired. Custom SVGs live in [app/assets/custom-icons/](app/assets/custom-icons/) and are reachable under the `custom-icons:` prefix. -- **Open a11y bug: dialogs have no `DialogDescription`.** Every modal renders its description as a plain `

`, so the `aria-describedby` reka-ui puts on `DialogContent` points at nothing — screen readers get no description, and reka-ui warns once per mount. **This is scheduled to be fixed, not accepted.** The warning is suppressed in [vitest.config.ts](vitest.config.ts)'s `onConsoleLog` purely to keep test output readable in the meantime; that branch comes out with the fix, and the warning is still visible in `nuxt dev`. Note that routing the text through `ShadcnUiDialogDescription` is *not* a drop-in fix: the wrapper runs its class through `cn()`, and default tailwind-merge reads the custom `text-2sm` as a colour utility and drops it in favour of the wrapper's `text-sm`. Closing this needs either reka-ui's `DialogDescription` imported directly (no `cn()`) or `extendTailwindMerge` teaching `cn()` the custom text scale. +- **Every dialog describes itself.** reka-ui points `DialogContent`'s `aria-describedby` at a `DialogDescription`, so each dialog and sheet renders one — `ShadcnUiDialogDescription` (or the sheet variant) with `sr-only` when nothing visible plays that role, `as-child` around an `` when the visible description is interpolated. The settings action components render theirs inside the `ActionModal` around them, which is why their tests mount through `mountUnderDialogRoot`. A dialog missing one makes reka-ui warn once per mount in `nuxt dev` and in test output. ## Formatting & TS Prettier uses **tabs**, no semicolons, trailing commas — see [prettier.config.js](prettier.config.js). -ESLint ([eslint.config.mjs](eslint.config.mjs)) runs **type-aware**: `eslint.config.typescript.tsconfigPath` in `nuxt.config.ts` switches the generated Nuxt preset to the `*-type-checked` rule sets, on top of which the config adds `eslint:recommended` (the Nuxt preset ships no base JS rules), typescript-eslint **strict + stylistic**, and `@intlify/eslint-plugin-vue-i18n`. Consequences worth knowing: +ESLint ([eslint.config.mjs](eslint.config.mjs)) runs **type-aware**: `eslint.config.typescript.tsconfigPath` in `nuxt.config.ts` switches the generated Nuxt preset to the `*-type-checked` rule sets, on top of which the config adds `eslint:recommended` (the Nuxt preset ships no base JS rules), typescript-eslint **strict + stylistic**, `@intlify/eslint-plugin-vue-i18n`, and `eslint-plugin-vuejs-accessibility` (its recommended set; the shadcn wrappers are exempt from the label-association and static-element rules, since the association is made where they are used). Consequences worth knowing: - Linting builds a full TS program, so a cold `check-eslint` takes about a minute. Runs are cached (`node_modules/.cache/eslint`) — unchanged files diff --git a/web/app/components/DocumentDeletionModal.vue b/web/app/components/DocumentDeletionModal.vue index c25d5481..4d952715 100644 --- a/web/app/components/DocumentDeletionModal.vue +++ b/web/app/components/DocumentDeletionModal.vue @@ -57,18 +57,19 @@ async function deleteDocument() {

- - - + + + + +
diff --git a/web/app/components/SearchModal.nuxt.test.ts b/web/app/components/SearchModal.nuxt.test.ts index e198f1ee..b314e344 100644 --- a/web/app/components/SearchModal.nuxt.test.ts +++ b/web/app/components/SearchModal.nuxt.test.ts @@ -224,8 +224,11 @@ describe("", { concurrent: false }, () => { await search("r") + // a result's spans are its icon, its type and its branch name, in that + // order, and highlights are s — so the branch is the only span + // that closes its parent const branches = [ - ...document.body.querySelectorAll("[data-testid='search-result-branch']"), + ...document.body.querySelectorAll("a span:last-child"), ].map((el) => el.textContent.trim()) expect(branches).toEqual(["main", "draft"]) diff --git a/web/app/components/SearchModal.vue b/web/app/components/SearchModal.vue index 62b020fd..cff17348 100644 --- a/web/app/components/SearchModal.vue +++ b/web/app/components/SearchModal.vue @@ -209,10 +209,7 @@ function resultTypeText(type: string) { {{ resultTypeText(result.type) }} - + {{ result.branchName }}
diff --git a/web/app/components/TagDeletionModal.vue b/web/app/components/TagDeletionModal.vue index a73b91dd..171a6167 100644 --- a/web/app/components/TagDeletionModal.vue +++ b/web/app/components/TagDeletionModal.vue @@ -57,18 +57,19 @@ async function deleteTag() {
- - - + + + + +
("[data-name]") if (!cell) { return @@ -230,6 +230,8 @@ function handleEscape(e: KeyboardEvent) { @scroll="handleScroll" @mouseover="handleGridHover" @mouseleave="hideTooltip" + @focusin="handleGridHover" + @focusout="hideTooltip" > diff --git a/web/app/components/editor/NameEditor.nuxt.test.ts b/web/app/components/editor/NameEditor.nuxt.test.ts index 129aaee1..6642accb 100644 --- a/web/app/components/editor/NameEditor.nuxt.test.ts +++ b/web/app/components/editor/NameEditor.nuxt.test.ts @@ -125,10 +125,10 @@ async function openActionMenu(wrapper: VueWrapper) { await nextTick() } -// the icon picker comes first, then the review action and the menu that -// switches between the review actions +// the hook handle and the icon picker come first, then the review action +// and the menu that switches between the review actions function actionMenuTrigger(wrapper: VueWrapper) { - return wrapper.findAll("button")[2] + return wrapper.findAll("button")[3] } function hookHandle(wrapper: VueWrapper) { diff --git a/web/app/components/editor/NameEditor.vue b/web/app/components/editor/NameEditor.vue index da4b8b9d..32822b7d 100644 --- a/web/app/components/editor/NameEditor.vue +++ b/web/app/components/editor/NameEditor.vue @@ -393,20 +393,24 @@ async function executeReviewableAction() { @update:open="(v: boolean) => (hookMenuOpen = v)" > -
-
+ -
+
+ {{ $t("editor.hook-handle.screen-reader-hint") }} -
+ {