Skip to content

Catalog UI 5/5: keyboard and screen-reader accessibility pass (#19) - #36

Merged
cchacs merged 7 commits into
kilo/18-catalog-install-actionfrom
kilo/19-catalog-a11y
Oct 8, 2026
Merged

cchacs merged 7 commits into
kilo/18-catalog-install-actionfrom
kilo/19-catalog-a11y

Conversation

@cchacs

@cchacs cchacs commented Oct 7, 2026

Copy link
Copy Markdown

Part of #4. Closes #19 — Catalog UI 5/5.

Stacked on #35 (Catalog UI 4/5): this branch is based on kilo/18-catalog-install-action, so the diff here is only the accessibility pass. Merge #35 first, then retarget this PR to main (GitHub does that automatically once the base branch merges).

Changes

  • Visible focus states. The host page ships no :focus-visible styling, so keyboard travel through this dark panel had no visible position. The fragment now brings its own outline rule, scoped to #plugin-manager-panel so it cannot repaint focus rings elsewhere in the app.
  • Screen-reader labels and live regions (screen.html):
    • the install plan note (#pm-catalog-deps) is a labelled region (Installation plan) with aria-live="polite" — selection state, "Required by the selection: Core (required by Practice Alpha)", pruned entries, download size and the network requirement are announced as they change;
    • install outcome (#pm-catalog-msg), browse status (#pm-catalog-status), filter state (#pm-catalog-active) and download progress text are polite live regions;
    • the <progress> element and both placeholder-only inputs (catalog search, git URL) get accessible names; the install list is a labelled group.
  • Disabled entries expose their explanation to AT, not only visually (screen.js):
    • every disabled checkbox carries a title naming the reason and the control that undoes it (Unpin / Enable / Downgrade / "ships with the app");
    • incompatibility reasons render as a visible line with a stable id, and resolver-placed locks get their own line — "Locked into the plan: required by Practice Alpha. Untick Practice Alpha to remove it." — both wired to the checkbox via aria-describedby;
    • the lock line is rewritten on every resolve, so releasing the dependent clears the stale explanation instead of leaving it on an enabled box.
  • No structural keyboard changes were needed: every filter and selection control was already a native input/button in document order. Pinned that: no inline event handlers and no positive tabindex anywhere in the fragment or screen script.

Manual checklist (recorded per the issue)

Keyboard-only walkthrough (Tab / Shift+Tab / Space / Enter, no mouse):

  • Search field → each filter facet checkbox → Clear filters → install-list checkboxes → Install selected / Cancel → per-row lifecycle buttons (Update, Pin, Downgrade, Enable, Uninstall, Restore previous) are all reachable in a logical order, with no traps.
  • Space toggles checkboxes (including inside the row <label>), Enter activates every button.
  • A visible focus ring appears at every stop (new :focus-visible rule); disabled checkboxes are skipped by Tab as expected, with their reason exposed via aria-describedby/title when browsed.
  • Ticking a plugin announces the updated plan (live region): install set, required-by line, download size, network note.

Screen-reader pass:

  • Walked the accessibility tree control by control (roles/names/states/descriptions): search announces its label; facets announce legend + checkbox state; status announces "No plugins match these filters…"; each checkbox announces name/version plus its disabled reason or lock explanation; the plan region announces the resolved set on change; progress announces "Downloading … %"; install results announce per plugin.
  • Physical NVDA/VoiceOver session on a packaged build — not possible in this environment; the tree walk above plus the automated pins cover the wiring, but a hands-on pass before release is still recommended.

Verification

  • npm test — 311 passed, 6 skipped; the only failure is the JUCE-submodule suite, which a clean checkout cannot populate (pre-existing).
  • npm run typecheck — clean (no TS changes in this PR).
  • New tests bite-verified: removing the aria-describedby wiring, the lock-line text, or the :focus-visible rule each fails the corresponding test.

cchacs added 2 commits October 7, 2026 21:49
- Visible focus: a :focus-visible outline scoped to the plugin-manager panel,
  so keyboard travel has a position the dark theme cannot eat.
- Screen-reader labels: the install plan note is a labelled live region
  (selection state, required-by explanations, pruned entries, download size);
  install outcome, browse status, filter state and download progress are
  polite live regions; the progress bar and both placeholder-only inputs get
  accessible names; the install list is a labelled group.
- Disabled entries expose their reason to AT, not only visually: every
  disabled checkbox carries a title, incompatibility reasons and resolver
  locks are visible lines wired through aria-describedby, and a lock says
  who requires the plugin and how to release it.
- Tests: describedby/lock-line/title behaviour per row state, and static
  pins on the fragment's a11y markup (no inline handlers, no positive
  tabindex).
# Conflicts:
#	src/renderer/plugin-manager/screen.js

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Important

Two findings strike at the goals this pass sets for itself: the new live regions are populated while still display:none, so the first — and every re-revealed — announcement is dropped by NVDA, JAWS and VoiceOver; and the unconditional aria-describedby both re-announces text the wrapping <label> already puts in the accessible name and, per accname precedence, suppresses title as the description. Details inline.

Reviewed changes the full keyboard and screen-reader pass for the catalog UI — focus visibility, live-region wiring and disabled-state explanations across the fragment and screen script, with new pins in the view suite.

  • Visible focus — a #plugin-manager-panel-scoped :focus-visible outline rule, so keyboard travel through the dark panel has a visible position.
  • Live regions and labels (screen.html) — aria-live/role on the install-plan note, install outcome, browse status, filter state and download progress text; aria-label on <progress> and the git URL input; role="group" on the install list.
  • Disabled-reason exposure (screen.js) — title plus aria-describedby on every disabled checkbox; the incompatibility line gains a stable id; a new lock element rewritten on every resolve names who required the entry and how to release it.
  • Tests — four new a11y tests (disabled reason, lock explanation, static-reason titles, markup wiring, no inline handlers or positive tabindex) plus setAttribute stubs in both DOM shims.

ℹ️ Nitpicks

  • The map-shape comment at src/renderer/plugin-manager/screen.js:184 still documents {box, entry, staticDisabled, incompatible, incompatibleReason}; this PR changed the stored shape to staticReason and lockEl.
  • tests/renderer-html-escaping.test.js — this PR had to extend its DOM shim, but no ci.yml step runs that suite (only the npm test glob does, and no workflow runs npm test), so it never executes in CI. Pre-existing gap, worth closing while the file is touched.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using big-pickle (free) | 𝕏

Comment thread src/renderer/plugin-manager/screen.html Resolved
Comment thread src/renderer/plugin-manager/screen.js Resolved
Comment thread src/renderer/plugin-manager/screen.js Resolved

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ℹ️ The delta since my previous review is clean — no new findings. The two open findings from that review (live-region reveal timing and the aria-describedby/title interaction on the wrapping <label>) remain outstanding and are not repeated here.

Reviewed changes what changed since the prior pullfrog review (b417ce2 → e13e4cb) — the merge of the base branch's reworked resolver mirror, with this branch's accessibility edits re-applied on top:

  • Lock-line rendering moved into the reworked branches (mirrorPlanOntoRows) — the pruned and unlocked branches now clear the lock element and restore box.title to the static reason, and a lock line renders only for entries the adoption loop pulls into the selection; on-disk entries (bundled/held) are deliberately left without a "Locked into the plan" line.
  • Stale-lock-line comment resolved — the third inline comment from my previous review no longer applies: a visible lock line now implies its entry was adopted into the selection, so the empty-selection early return can no longer strand it.
  • Tests — the a11y tests are unchanged against this head and pass: node --test tests/plugin-catalog-view.test.js 48/48, node --test tests/renderer-html-escaping.test.js 7/7.

ℹ️ Nitpicks

  • The branch that suppresses the lock line for already-on-disk entries (src/renderer/plugin-manager/screen.js:572-578) has no test pinning it — the a11y suite only exercises the adoptable-dependency case, so a regression there would silently restore a "Locked into the plan" line over a checkbox that was never selectable.

Pullfrog  | Fix it ➔ | View workflow run | Using big-pickle (free) | 𝕏

@kilo-code-bot

kilo-code-bot Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (2 files)
  • src/renderer/plugin-manager/screen.html
  • src/renderer/plugin-manager/screen.js
Previous Review Summaries (2 snapshots, latest commit ee70357)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit ee70357)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (3 files)
  • src/renderer/plugin-manager/screen.html - 3 prior findings resolved (removed hidden from the three polite live regions so the first announcement lands on a registered, visible, empty region)
  • src/renderer/plugin-manager/screen.js - 2 prior findings resolved (aria-describedby now conditional on incompatible/locked rows only, restoring title as fallback; mirrorPlanOntoRows(null) added to the empty-selection early return so stale lock lines clear)
  • tests/plugin-catalog-view.test.js - 1 prior finding resolved (removeAttribute added to the test element mock so the new removal calls are supported)

Previous review (commit e13e4cb)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (4 files)
  • src/renderer/plugin-manager/screen.html
  • src/renderer/plugin-manager/screen.js
  • tests/plugin-catalog-view.test.js
  • tests/renderer-html-escaping.test.js

Note: reviewed against head e13e4cbb (merge of advanced base kilo/18-catalog-install-action). The base merge restructured mirrorPlanOntoRows (fixed-point dependency adoption, pruned-first ordering); the accessibility changes were re-applied cleanly on top and verified against that structure: disabled-row reasons (title + aria-describedby to stable reason lines), resolver lock lines with required-by names, live regions, accessible names for placeholder-only inputs and the progress bar, and a panel-scoped :focus-visible rule. Static pins in the new tests match the current implementation, including the lock-release scenario.


Reviewed by free · Input: 173.8K · Output: 6.7K · Cached: 103.7K

@pullfrog

pullfrog Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Reviewed review #5448998607 on PR #36. The two open findings from the previous review (live-region reveal timing and aria-describedby/title interaction) remain as noted in the follow-up review; the stale lock-line case is addressed by calling mirrorPlanOntoRows(null) when the selection empties. Changes committed and pushed. Renderer HTML escaping tests still pass (7/7). Typecheck on renderer files clean.

Task list (5/5 completed)
  • Checkout the PR branch via pullfrog_checkout_pr.
  • Fetch review comments via pullfrog_get_review_comments for PR 36 review 5448998607.
  • Read commentsPath file for full comment details and diff context.
  • For each comment, analyze intent and verify against code. Address if change is warranted.
  • Test, commit, push (if code changed), reply and resolve threads as appropriate.

Pullfrog  | View workflow run | via Pullfrog | Using big-pickle (free) | 𝕏

…sible, aria-describedby only for visible lines, stale lock cleared on empty selection

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Important

The aria-describedby scoping and the stale-lock clearing from the first review are fixed cleanly, but the live-region fix only covers the very first announcement. The plan note is re-hidden when the selection empties, and the progress text lives in a wrapper that is re-hidden between runs; both are then repopulated in the same task that unhides them again, so those announcements are still dropped.

Reviewed changes the ee70357 address-review delta — live-region visibility, aria-describedby scoping, and the empty-selection lock cleanup.

  • Live-region visibility — hidden dropped from #pm-catalog-deps and #pm-catalog-msg, so their first content lands in an already-rendered region.
  • aria-describedby scoping — set only where it points at real text (pm-row-reason-* for an incompatible row, pm-row-lock-* while locked), removed on release, so title regains its role as the fallback description.
  • Empty-selection cleanup — the empty branch now calls mirrorPlanOntoRows(null), clearing every lock line and lock-derived title.
  • Test shim — removeAttribute added to both DOM shims.

⚠️ The live-region fix only covers the first reveal

#pm-catalog-deps is re-hidden whenever the last plugin is unticked (screen.js:601), and #pm-catalog-progress-text lives inside #pm-catalog-progress-wrap, which is re-hidden after every install (screen.js:726). Both then get their text written in the same task that removes hidden again (screen.js:647-650, screen.js:197-201), so the next announcement is once more a populate-then-unhide reveal that NVDA, JAWS and VoiceOver drop. Only #pm-catalog-msg — never re-hidden — is fully fixed. The first-review thread on this stays open.

Technical details
# Live-region fix is partial: re-reveals still take the display:none path

## Affected sites
- src/renderer/plugin-manager/screen.js:600-606 — empty selection re-adds `hidden` to `#pm-catalog-deps` and clears it. The `mirrorPlanOntoRows(null)` addition here is correct and clears the rows.
- src/renderer/plugin-manager/screen.js:647-650 — for a non-empty selection the text is written before `hidden` is removed, so the first announcement after the region was hidden is a populate-then-unhide reveal.
- src/renderer/plugin-manager/screen.js:195-201 — `renderCatalogProgress` removes `hidden` from `#pm-catalog-progress-wrap` and writes `#pm-catalog-progress-text` in the same task; the wrapper is re-hidden after each install (screen.js:726), so the first progress update of every run is dropped.
- src/renderer/plugin-manager/screen.html:78,85 — the two regions involved.

## Required outcome
- Every announcement the install-plan and progress regions make must reach assistive tech, not just the first one after page load.

## Suggested approach
- Keep `#pm-catalog-deps` and `#pm-catalog-progress-text` rendered at all times (never re-add `hidden`); clear their text when there is nothing to say. Or unhide the region while empty, then write the text in a later task (`setTimeout(..., 0)`).

## Open questions for the human
- Is the progress bar's visual presence tied to the wrapper's `hidden`? If so, move the live-region text out of the wrapper so the bar can hide independently.

Pullfrog  | Fix it ➔ | View workflow run | Using big-pickle (free) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Important

The commit drops the last classList.remove('hidden') on the install-plan region but leaves both re-adding sites standing, so after the first successful install — or a resolver error — the plan note is stuck display:none and its live-region announcements are dead for the rest of the session. The progress-text move out of the wrapper likewise strands the last status line once the bar hides.

Reviewed changes what changed since the prior pullfrog review (ee70357 → d1d3923):

  • Progress text leaves the hidden wrapper (screen.html) — #pm-catalog-progress-text moved out of #pm-catalog-progress-wrap, so the live region is always rendered and only the bar is toggled.
  • Plan note stops re-hiding — and stops un-hiding (screen.js) — the hidden class toggles were removed from the empty-selection and lines/no-lines branches of updateDependencyInfo, and renderCatalogProgress gained a null guard on the progress-text write.

Tests still pass on this head: node --test tests/plugin-catalog-view.test.js 48/48, tests/renderer-html-escaping.test.js 7/7.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using big-pickle (free) | 𝕏

Comment thread src/renderer/plugin-manager/screen.js Resolved
Comment thread src/renderer/plugin-manager/screen.html Resolved

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes the single commit since the prior pullfrog review (d1d3923 → 8d347a6):

  • Removed the last two plan-note hides (screen.js) — the resolver-error catch and the post-install branch now clear #pm-catalog-deps instead of adding hidden, so the region stays rendered for the rest of the session and every later plan write is both visible and announced; the catch comment was reworded to match the new contract.
  • Cleared the progress text with the bar (screen.js) — the install finally now empties #pm-catalog-progress-text next to the wrapper hide, so a cancelled or failed run no longer strands a stale "downloading… 42%" line under a hidden bar.

Audited every write site for the five live regions (#pm-catalog-deps, #pm-catalog-msg, #pm-catalog-status, #pm-catalog-active, #pm-catalog-progress-text) against this head: none is populated while it or an ancestor is display:none, and no hidden toggle on them survives anywhere in screen.js or screen.html. Suites pass on this head: node --test tests/plugin-catalog-view.test.js 48/48, node --test tests/renderer-html-escaping.test.js 7/7.

Pullfrog  | View workflow run | Using big-pickle (free) | 𝕏

@cchacs
cchacs merged commit 41bd79c into kilo/18-catalog-install-action Oct 8, 2026
5 checks passed
@cchacs
cchacs deleted the kilo/19-catalog-a11y branch October 9, 2026 03:24
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