Skip to content

fix(website): Improve accessibility of tabs and file upload widgets#1588

Merged
yamadashy merged 2 commits into
mainfrom
a11y/tabs-and-file-upload
May 21, 2026
Merged

fix(website): Improve accessibility of tabs and file upload widgets#1588
yamadashy merged 2 commits into
mainfrom
a11y/tabs-and-file-upload

Conversation

@yamadashy

Copy link
Copy Markdown
Owner

Summary

  • Add role="tablist" / role="tab" / aria-selected to the URL/Folder/ZIP mode tabs in TryIt.vue and the Result/File Selection tabs in TryItResult.vue.
  • Give each tab panel role="tabpanel" + aria-labelledby so assistive tech can pair the active tab with its content.
  • Make file and folder upload drop zones focusable: role="button", tabindex="0", Enter/Space activation, and an aria-label.
  • Add an aria-label to the inline clear (×) button for the selected file/folder.

Why

Tab triggers had no role or selected-state hints, so screen reader users could not tell which view was active. The upload drop zones were a div over a display:none input, which made the picker unreachable by keyboard and invisible to screen readers.

Checklist

  • Run node --run lint (website/client)
  • Run node --run docs:build (website/client)

Tab triggers in `TryIt.vue` (URL / Folder / ZIP) and `TryItResult.vue`
(Result / File Selection) now expose `role="tablist"`, `role="tab"`,
and `aria-selected`. Their panels carry `role="tabpanel"` plus
`aria-labelledby` so screen readers can pair the active tab with its
content.

File and folder upload drop zones in `TryItFileUpload.vue` and
`TryItFolderUpload.vue` were focusable only by mouse. They now act as
real buttons (`role="button"`, `tabindex="0"`, Enter/Space activation,
`aria-label`), and the inline clear buttons announce themselves via
`aria-label`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 35b00742-8bc6-4e55-8771-510ab65cb9e7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR enhances accessibility across the Try It UI by adding ARIA semantics to tab navigation and keyboard support to upload interactions. Four Vue components receive targeted improvements: tab lists and buttons gain role, aria-selected, and aria-controls attributes; tab panels are labeled with role="tabpanel" and aria-labelledby; and upload areas become keyboard-accessible through Enter/Space key handlers alongside button semantics.

Changes

Accessibility Improvements

Layer / File(s) Summary
Tab navigation ARIA semantics
website/client/components/Home/TryIt.vue, website/client/components/Home/TryItResult.vue
Input mode selection and result view tabs now include ARIA role="tablist" containers, individual tab buttons with role="tab", aria-selected state bindings, and aria-controls references, plus dedicated tab panels with role="tabpanel" and aria-labelledby linkage.
Upload area keyboard accessibility
website/client/components/Home/TryItFileUpload.vue, website/client/components/Home/TryItFolderUpload.vue
File and folder upload containers now support keyboard activation via Enter and Space keys with button semantics (role="button", tabindex="0", aria-label), and clear buttons include accessible labels while maintaining their click handlers.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • yamadashy/repomix#387: Both PRs touch the same mode/tab controls and folder-upload UI, with this PR layering ARIA and keyboard accessibility onto prior functionality.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: improving accessibility of tabs and file upload widgets in the website component.
Description check ✅ Passed The description provides a clear summary of changes, explains the rationale, lists what was verified, and includes completed checklist items matching the repository template.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch a11y/tabs-and-file-upload

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
website/client/components/Home/TryItFolderUpload.vue (1)

76-111: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Same keydown-bubbling issue as TryItFileUpload.vue.

Pressing Enter/Space while focus is on the inner clear button (lines 106–111) will clear the selection and then re-trigger the folder picker because the keydown event bubbles up to the outer role="button" container that listens for @keydown.enter.prevent / @keydown.space.prevent. Apply the same fix here (either .stop on the clear button's keydown, or .self on the container's keydown handlers). See the matching comment on TryItFileUpload.vue for the diff.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@website/client/components/Home/TryItFolderUpload.vue` around lines 76 - 111,
The clear button's keydown events are bubbling to the outer upload-container
which also listens for `@keydown.enter.prevent` and `@keydown.space.prevent`,
causing the folder picker to re-open after clearing; update the clear-button
element to stop propagation of keydown for Enter/Space (e.g., add keydown
handlers with .stop for Enter and Space) so invoking clearFolder via the clear
button doesn't bubble to triggerFileInput on the container (alternatively you
can make the container keydown handlers use .self); modify the clear button near
the Selected: {{ selectedFolder }} block to intercept keydown events and call
clearFolder without letting them propagate.
website/client/components/Home/TryIt.vue (1)

5-36: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Incomplete tablist pattern — input panels are missing role="tabpanel" and labelling.

The tab buttons declare role="tab" but the corresponding input area (.input-field, lines 38–60) that they switch between has no role="tabpanel", no id, no aria-labelledby, and the tabs have no aria-controls pointing at it. Per the WAI-ARIA tabs pattern, each tab must be associated with a tabpanel; otherwise assistive tech announces "tab" state but cannot find/describe the controlled region, which partially defeats the goal of this PR. TryItResult.vue in this same PR does this correctly and can serve as the reference.

Additionally, since the three panels are rendered conditionally (v-if/v-else-if) into a single container, the simplest fix is to give each input component (or a wrapper) a stable id per mode and add aria-controls on each tab, plus role="tabpanel" + aria-labelledby on the rendered panel.

♻️ Sketch of the fix
       <button
         type="button"
         role="tab"
+        id="tab-url"
+        aria-controls="tabpanel-url"
         aria-label="Remote repository URL"
         :aria-selected="mode === 'url'"
         ...
       >
       <!-- repeat id/aria-controls for folder and file tabs -->

-      <div class="input-field">
+      <div
+        class="input-field"
+        role="tabpanel"
+        :id="`tabpanel-${mode}`"
+        :aria-labelledby="`tab-${mode}`"
+      >

Optional follow-up: consider arrow-key navigation between tabs (roving tabindex) per the ARIA APG tabs pattern for full keyboard parity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@website/client/components/Home/TryIt.vue` around lines 5 - 36, The tablist
lacks proper tabpanel associations: update the three tab buttons in the
.tab-container (the buttons that call setMode('url'|'folder'|'file') and read
mode) to include aria-controls pointing to stable IDs (e.g. input-panel-url /
input-panel-folder / input-panel-file), and give each rendered input area (the
.input-field wrapper or the component instance shown via v-if / v-else-if)
role="tabpanel", an id matching the button's aria-controls, and aria-labelledby
pointing back at the corresponding tab button id (assign predictable ids to the
buttons, e.g. tab-url/tab-folder/tab-file). Use the existing mode value to
compute/assign these IDs so the correct tab and panel are associated when
switching.
website/client/components/Home/TryItFileUpload.vue (1)

66-101: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keydown on the inner clear button bubbles to the outer drop zone and re-opens the file picker.

The outer container has role="button" with @keydown.enter.prevent="triggerFileInput" and @keydown.space.prevent="triggerFileInput". The inner .clear-button (lines 96–101) uses @click.stop to block the click bubble, but the keydown event is not stopped. When focus is on the clear button and the user presses Enter or Space, the browser fires click on the clear button (clearing the file) and the keydown bubbles up to the container, which immediately calls triggerFileInput() and opens the file picker — the opposite of what the user intended.

Also worth noting: nesting a real <button> (and an <input>) inside an element with role="button" is an invalid ARIA nesting (interactive descendants of a button role), which some screen readers handle inconsistently. A cleaner long-term fix is to make the drop zone a non-button element with explicit click/keydown handling, or scope the keyboard activation to only fire when the event target is the container itself.

🛡️ Minimal fix: stop keydown propagation on the clear button
             <button
               type="button"
               class="clear-button"
               aria-label="Clear selected file"
               `@click.stop`="clearFile"
+              `@keydown.enter.stop`
+              `@keydown.space.stop`
             >×</button>

Or, on the outer container, ignore activation when the event originated from a descendant:

-      `@keydown.enter.prevent`="triggerFileInput"
-      `@keydown.space.prevent`="triggerFileInput"
+      `@keydown.enter.self.prevent`="triggerFileInput"
+      `@keydown.space.self.prevent`="triggerFileInput"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@website/client/components/Home/TryItFileUpload.vue` around lines 66 - 101,
The Enter/Space keydown on the inner clear button bubbles to the outer drop zone
and triggers triggerFileInput; fix by preventing keydown propagation on the
clear button (add a keydown handler on the element with class "clear-button"
that stops propagation and prevents default for Enter/Space) or, alternatively,
make the outer handler (the element with class "upload-container") only activate
when the event.target === event.currentTarget in its keydown handlers; update
handlers around triggerFileInput, clearFile, and the clear-button to implement
one of these approaches and ensure accessibility semantics remain correct.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@website/client/components/Home/TryIt.vue`:
- Around line 5-36: The tablist lacks proper tabpanel associations: update the
three tab buttons in the .tab-container (the buttons that call
setMode('url'|'folder'|'file') and read mode) to include aria-controls pointing
to stable IDs (e.g. input-panel-url / input-panel-folder / input-panel-file),
and give each rendered input area (the .input-field wrapper or the component
instance shown via v-if / v-else-if) role="tabpanel", an id matching the
button's aria-controls, and aria-labelledby pointing back at the corresponding
tab button id (assign predictable ids to the buttons, e.g.
tab-url/tab-folder/tab-file). Use the existing mode value to compute/assign
these IDs so the correct tab and panel are associated when switching.

In `@website/client/components/Home/TryItFileUpload.vue`:
- Around line 66-101: The Enter/Space keydown on the inner clear button bubbles
to the outer drop zone and triggers triggerFileInput; fix by preventing keydown
propagation on the clear button (add a keydown handler on the element with class
"clear-button" that stops propagation and prevents default for Enter/Space) or,
alternatively, make the outer handler (the element with class
"upload-container") only activate when the event.target === event.currentTarget
in its keydown handlers; update handlers around triggerFileInput, clearFile, and
the clear-button to implement one of these approaches and ensure accessibility
semantics remain correct.

In `@website/client/components/Home/TryItFolderUpload.vue`:
- Around line 76-111: The clear button's keydown events are bubbling to the
outer upload-container which also listens for `@keydown.enter.prevent` and
`@keydown.space.prevent`, causing the folder picker to re-open after clearing;
update the clear-button element to stop propagation of keydown for Enter/Space
(e.g., add keydown handlers with .stop for Enter and Space) so invoking
clearFolder via the clear button doesn't bubble to triggerFileInput on the
container (alternatively you can make the container keydown handlers use .self);
modify the clear button near the Selected: {{ selectedFolder }} block to
intercept keydown events and call clearFolder without letting them propagate.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3d42f420-00b8-438c-b766-99124e38fe10

📥 Commits

Reviewing files that changed from the base of the PR and between eb945d9 and d60b100.

📒 Files selected for processing (4)
  • website/client/components/Home/TryIt.vue
  • website/client/components/Home/TryItFileUpload.vue
  • website/client/components/Home/TryItFolderUpload.vue
  • website/client/components/Home/TryItResult.vue

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request improves the accessibility of the 'Try It' components by adding ARIA roles, labels, and keyboard event handlers. Feedback highlights that the tab implementation in TryIt.vue is incomplete compared to TryItResult.vue, missing proper aria-controls and tabpanel associations. Furthermore, the file and folder upload components need @keydown.stop on their clear buttons to prevent event bubbling to the parent container, and the nested interactive control structure should be addressed to comply with accessibility standards.

Comment thread website/client/components/Home/TryItFileUpload.vue
Comment thread website/client/components/Home/TryItFolderUpload.vue
Comment thread website/client/components/Home/TryIt.vue
@claude

claude Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Review Summary

Solid accessibility improvement — adds the right ARIA semantics for tabs/tabpanels and makes the drop zones keyboard-operable. Net positive. A few follow-ups worth considering before/after merge.

Details

Recommended

1. Add a visible focus indicator (WCAG 2.4.7)

The drop zones now receive keyboard focus (tabindex=\"0\"), and the tab buttons are reachable via Tab — but none of the four files define :focus / :focus-visible styles, so keyboard users land on these elements without any visual cue. This is the most impactful gap in the PR.

Suggested additions:

```css
/* TryIt.vue */
.tab-container button:focus-visible {
outline: 2px solid var(--vp-c-brand-1);
outline-offset: -2px;
}

/* TryItFileUpload.vue + TryItFolderUpload.vue */
.upload-container:focus-visible {
outline: 2px solid var(--vp-c-brand-1);
outline-offset: 2px;
}

/* TryItResult.vue */
.tab-button:focus-visible {
outline: 2px solid var(--vp-c-brand-1);
outline-offset: -2px;
}
```

2. Tab keyboard navigation (WAI-ARIA APG)

The WAI-ARIA Authoring Practices tab pattern expects Left/Right arrow navigation between tabs plus a roving `tabindex` (active tab `tabindex="0"`, inactive tabs `tabindex="-1"`). Right now all three tabs are in the tab sequence and arrow keys do nothing. Reasonable to defer to a follow-up PR — calling it out so it's tracked.

Nit

3. Static IDs in TryItResult.vue

`id="tab-result"` / `id="tabpanel-result"` etc. are fine as long as the component mounts at most once per page (currently true). If `TryItResult` ever renders multiple times, the IDs would collide and break `aria-labelledby` / `aria-controls`. Easy to make instance-unique later with `useId()` (Vue 3.5+) or a local counter.

Not blocking

  • The `aria-label` on the icon-only tab buttons (`URL` / `Folder` / `ZIP`) correctly provides accessible names since the buttons have no visible text — this is the right pattern.
  • No security, performance, or test-coverage concerns. The website client doesn't have a Vue component test harness, so unit tests aren't expected for this layer; manual verification with a screen reader + keyboard-only navigation is the appropriate check.

@github-actions

github-actions Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

⚡ Performance Benchmark

Latest commit:ed7c2b8 fix(website): Address PR review feedback for a11y improvements
Status:✅ Benchmark complete!
Ubuntu:0.67s (±0.01s) → 0.67s (±0.02s) · -0.00s (-0.1%)
macOS:0.43s (±0.07s) → 0.43s (±0.06s) · +0.01s (+1.2%)
Windows:0.89s (±0.01s) → 0.89s (±0.01s) · +0.00s (+0.1%)
Details
  • Packing the repomix repository with node bin/repomix.cjs
  • Warmup: 2 runs (discarded), interleaved execution
  • Measurement: 20 runs / 30 on macOS (median ± IQR)
  • Workflow run
History

d60b100 fix(website): Improve accessibility of tabs and file upload widgets

Ubuntu:0.72s (±0.03s) → 0.72s (±0.04s) · -0.00s (-0.3%)
macOS:0.59s (±0.12s) → 0.59s (±0.15s) · -0.00s (-0.7%)
Windows:0.95s (±0.02s) → 0.95s (±0.02s) · -0.00s (-0.1%)

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented May 21, 2026

Copy link
Copy Markdown

Deploying repomix with  Cloudflare Pages  Cloudflare Pages

Latest commit: ed7c2b8
Status:⚡️  Build in progress...

View logs

- Scope upload-zone keyboard activation to the container itself via
  `@keydown.enter.self.prevent` / `@keydown.space.self.prevent`. The
  previous handlers fired even when the inner clear button was
  focused, re-opening the picker after a clear.
- Add tabpanel semantics to the URL/Folder/ZIP input area in
  `TryIt.vue` so the mode tabs now point at a single `tabpanel-input`
  region with `aria-labelledby` derived from the active mode, mirroring
  the existing pattern in `TryItResult.vue`.
- Add `:focus-visible` outlines to the tab buttons and upload drop
  zones in all four files so keyboard users get a clear focus
  indicator (WCAG 2.4.7).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@yamadashy yamadashy merged commit 5f70439 into main May 21, 2026
7 of 8 checks passed
@yamadashy yamadashy deleted the a11y/tabs-and-file-upload branch May 21, 2026 15:25
@claude

claude Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Post-merge review notes

Thanks for landing the a11y improvements! Since this is already merged, sharing findings as follow-up suggestions rather than blockers. Six reviewers (code-quality, security, performance, test-coverage, conventions, holistic a11y) looked at the diff — no security or performance concerns. A few items worth a follow-up:

Worth a follow-up

1. Tabs are missing arrow-key navigation (P1, a11y)TryIt.vue:5-42 and TryItResult.vue:78-103 declare role="tablist" / role="tab" but only handle @click. WAI-ARIA APG's tabs pattern requires Left/Right (and Home/End) arrow navigation with a roving tabindex (active tab tabindex="0", inactive -1). Right now every tab is in the Tab order and arrows do nothing, which is contrary to what screen-reader users are taught to expect from role=tab. This is the largest remaining gap relative to the PR title.

2. Interactive descendants nested inside role=\"button\" (P1, a11y)TryItFileUpload.vue:66-108 and TryItFolderUpload.vue:76-118 give the outer drop zone role=\"button\" while it still contains a real <button class=\"clear-button\"> (and the hidden <input>). ARIA 1.2 disallows focusable/interactive descendants in role=button; assistive tech may flatten the inner button or announce nested "button, button". A cleaner structure would be a labeled drop region (role=\"region\" or no role) with a sibling "Browse" button — or just keep keyboard activation but drop the role=\"button\" on the wrapper.

3. No live-region announcement on selection/error (P2, a11y) — When selectedFile / errorMessage flips in either upload component, the <p> content swaps silently. SR users hear nothing unless they re-traverse. Wrapping the status <p> with aria-live=\"polite\" aria-atomic=\"true\" (and role=\"alert\" for the error variant) would close the loop.

4. Inconsistent tabpanel-id strategy within the same PRTryIt.vue:45 uses one shared id=\"tabpanel-input\" with dynamic :aria-labelledby=\"tab-\${mode}\", while TryItResult.vue:107,115 uses per-panel ids (tabpanel-result / tabpanel-files). The single-id approach works today only because the three inputs are mutually exclusive via v-if, but it's fragile — a future switch to v-show would give two tabs the same aria-controls target. Aligning on the per-panel-id pattern (matching TryItResult.vue) is more robust.

Nits / smaller items
  • Component duplication. TryItFileUpload.vue and TryItFolderUpload.vue are now ~95% identical (same drop-zone markup, same role=\"button\"/tabindex/@keydown block, same .clear-button, same :focus-visible style). This PR added another duplicated block to each side. Extracting a shared <UploadDropZone> wrapper (or composable returning the keyboard handlers) would make the next a11y change touch one file instead of two.
  • Focus-ring contrast on the active tab. :focus-visible uses outline: 2px solid var(--vp-c-brand-1); outline-offset: -2px — on the active tab the background is already var(--vp-c-brand-1), so the focus ring is brand-on-brand. Consider a contrasting color (e.g., var(--vp-c-bg)) when the tab is .active.
  • Selected-file text inside the role=button wrapper. "Selected: foo.zip" lives inside the wrapper that has aria-label=\"Upload ZIP file\". Some screen readers won't read the wrapper's child text when it's exposed as a button. Either include the filename in a dynamic aria-label (e.g., "Upload ZIP file, currently selected: foo.zip") or move the status text out of the button.
  • Drop affordance not in aria-label. aria-label=\"Upload ZIP file\" doesn't convey that drag-and-drop is supported. Extending the label (or using aria-describedby pointing at the placeholder text) tells AT users about the drop interaction too.
  • Tests. CLAUDE.md asks for unit tests on new features, but website/client/ has no Vitest / @vue/test-utils setup today — adding a single test would require scaffolding the harness first. If interested, candidates would be: "clicking a mode tab updates aria-selected", "Enter/Space on the drop zone calls triggerFileInput", "aria-labelledby on the tabpanel matches the active tab id".

Already verified clean

  • Security: no new XSS sinks; :aria-labelledby interpolates a typed InputMode union, no untrusted input in any ARIA value, file-validation (10 MB cap, ZIP check) untouched. Adding type=\"button\" to the clear ✕ is actually a defensive improvement against accidental form submit.
  • Performance: :focus-visible outlines don't trigger layout; the .self keydown modifier compiles to one event.target !== event.currentTarget check; no new inline-arrow allocations.
  • CodeRabbit's earlier feedback (incomplete tablist pattern, keydown bubbling from clear button) was fully addressed in the merged version via role=\"tabpanel\" + aria-labelledby and the .self modifier on the container's keydown handlers — nice.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant