-
Notifications
You must be signed in to change notification settings - Fork 0
🎨 Palette: Accessibility improvements for interactive elements #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| ## 2026-06-06 - Interactive badges as spans | ||
| **Learning:** Found interactive badges (like `healthBadge` and `staleBadge`) built as `<span>` elements with `onclick` handlers, breaking keyboard navigation and screen reader semantics. There are also inputs and selects with no `aria-label` or `<label>` tag. | ||
| **Action:** Always check if an `onclick` is on a non-interactive element like a `div` or `span`. Convert them to `<button>` or add appropriate `role="button"` and `tabindex="0"`. Add missing `aria-label` attributes to form elements not directly tied to visible labels. |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -185,7 +185,7 @@ | |
| <button id="pillActive" onclick="toggleStatusPill('active')" class="px-2 py-1 rounded text-xs font-medium bg-accent text-primary">Active</button> | ||
| <div class="relative"> | ||
| <button id="pillDone" onclick="toggleStatusPill('done')" class="px-2 py-1 rounded text-xs font-medium bg-overlay text-secondary border border-strong">Done</button> | ||
| <select id="doneTimeBound" onchange="onDoneTimeBoundChange()" class="hidden absolute top-full right-0 mt-1 bg-overlay text-primary text-xs rounded px-1 py-0.5 border border-strong z-10"> | ||
| <select id="doneTimeBound" onchange="onDoneTimeBoundChange()" class="hidden absolute top-full right-0 mt-1 bg-overlay text-primary text-xs rounded px-1 py-0.5 border border-strong z-10" aria-label="Time bound for done issues"> | ||
| <option value="1">24 hours</option> | ||
| <option value="3">3 days</option> | ||
| <option value="7">7 days</option> | ||
|
|
@@ -217,7 +217,7 @@ | |
|
|
||
| <div class="flex items-center gap-3 text-xs text-secondary"> | ||
| <span id="refreshIndicator" class="opacity-0 text-accent">Refreshing...</span> | ||
| <span id="healthBadge" onclick="showHealthBreakdown()" class="cursor-pointer px-2 py-0.5 rounded text-xs font-bold" title="System Health Score — click for breakdown">--</span> | ||
| <button id="healthBadge" onclick="showHealthBreakdown()" class="cursor-pointer px-2 py-0.5 rounded text-xs font-bold" title="System Health Score — click for breakdown" style="min-height: unset">--</button> | ||
| <div class="relative"> | ||
| <button onclick="toggleSettingsMenu(event)" id="settingsGear" class="text-xs px-2 py-1 rounded bg-overlay bg-overlay-hover" title="Settings" aria-label="Settings menu">⚙</button> | ||
| <div id="settingsDropdown" class="hidden absolute right-0 top-full mt-1 rounded-lg shadow-xl text-xs z-50" style="background:var(--surface-base);border:1px solid var(--border-strong);min-width:160px"> | ||
|
|
@@ -340,7 +340,7 @@ | |
| <button id="btnCluster" onclick="switchKanbanMode('cluster')" class="px-2 py-0.5 rounded" title="Group issues by parent epic with progress bars">Cluster</button> | ||
| <button id="btnList" onclick="switchKanbanMode('list')" class="px-2 py-0.5 rounded" title="Table list view for large projects">List</button> | ||
| <span class="text-muted">|</span> | ||
| <select id="filterType" onchange="applyTypeFilter()" class="bg-overlay text-primary text-xs rounded px-2 py-0.5 border border-strong" title="Filter to one issue type and show its workflow states as columns"> | ||
| <select id="filterType" onchange="applyTypeFilter()" class="bg-overlay text-primary text-xs rounded px-2 py-0.5 border border-strong" title="Filter to one issue type and show its workflow states as columns" aria-label="Filter by issue type"> | ||
| <option value="">All types</option> | ||
| </select> | ||
| <span id="typeFilterPill" class="hidden text-xs px-2 py-0.5 rounded border text-accent" style="background:var(--accent-subtle);border-color:var(--accent)"> | ||
|
|
@@ -373,7 +373,7 @@ | |
| <div class="shrink-0 px-6 pt-6 pb-2"> | ||
| <div class="max-w-3xl mx-auto flex items-center gap-3"> | ||
| <span class="text-base font-semibold text-primary">Insights</span> | ||
| <select id="insightsDays" onchange="loadMetrics()" class="bg-overlay text-xs rounded px-2 py-1 border border-strong"> | ||
| <select id="insightsDays" onchange="loadMetrics()" class="bg-overlay text-xs rounded px-2 py-1 border border-strong" aria-label="Time range for insights"> | ||
| <option value="7">7 days</option> | ||
| <option value="30" selected>30 days</option> | ||
| <option value="90">90 days</option> | ||
|
|
@@ -394,7 +394,7 @@ | |
| <span class="text-base font-semibold text-primary">Files</span> | ||
| <div class="flex items-center gap-2"> | ||
| <input id="filesSearch" type="text" placeholder="Filter by path..." | ||
| class="bg-overlay text-primary text-xs rounded px-3 py-1 border border-strong w-64 focus:outline-none focus-accent"> | ||
| class="bg-overlay text-primary text-xs rounded px-3 py-1 border border-strong w-64 focus:outline-none focus-accent" aria-label="Filter files by path"> | ||
| <label class="flex items-center gap-1 text-xs text-secondary"> | ||
| <input type="checkbox" id="filesCriticalOnly" style="accent-color:var(--accent)"> Critical only | ||
| </label> | ||
|
|
@@ -468,7 +468,7 @@ <h2 class="text-base font-semibold text-primary m-0">Releases</h2> | |
| <span>Blocked: <b id="footBlocked" class="text-red-400">0</b></span> | ||
| <span>Deps: <b id="footDeps" class="text-primary">0</b></span> | ||
| <canvas id="sparkline" width="100" height="20" title="14-day throughput trend" class="opacity-70"></canvas> | ||
| <span id="staleBadge" class="hidden text-xs bg-red-900/50 text-red-400 px-2 py-0.5 rounded border border-red-800 cursor-pointer" onclick="showStaleIssues()" title="WIP issues with no updates for >2 hours. Click to view."></span> | ||
| <button id="staleBadge" class="hidden text-xs bg-red-900/50 text-red-400 px-2 py-0.5 rounded border border-red-800 cursor-pointer" onclick="showStaleIssues()" title="WIP issues with no updates for >2 hours. Click to view." style="min-height: unset"></button> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The inline Useful? React with 👍 / 👎. |
||
| <span class="ml-auto text-muted" id="footVersion"></span> | ||
| </footer> | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,7 @@ | |
|
|
||
| import { fetchCriticalPath } from "../api.js"; | ||
| import { CATEGORY_COLORS, state, THEME_COLORS } from "../state.js"; | ||
| import { resolveGraphScope, handleGhostClick } from "./graphSidebar.js"; | ||
| import { handleGhostClick, resolveGraphScope } from "./graphSidebar.js"; | ||
|
|
||
| // --- Callbacks for functions not yet available at import time --- | ||
|
|
||
|
|
@@ -319,7 +319,7 @@ export function renderGraph() { | |
| const filteredIds = new Set(filteredNodes.map((n) => n.id)); | ||
| const search = document.getElementById("filterSearch")?.value?.toLowerCase().trim() || ""; | ||
|
|
||
| let cyNodes = filteredNodes.map((n) => { | ||
| const cyNodes = filteredNodes.map((n) => { | ||
| const title = n.title || n.id; | ||
| const isGhost = ghostIds.has(n.id); | ||
| const matchesSearch = | ||
|
|
@@ -340,7 +340,7 @@ export function renderGraph() { | |
| }; | ||
| }); | ||
|
|
||
| let cyEdges = scopeEdges | ||
| const cyEdges = scopeEdges | ||
| .filter((e) => filteredIds.has(e.source) && filteredIds.has(e.target)) | ||
| .map((e) => ({ | ||
| data: { | ||
|
|
@@ -407,8 +407,7 @@ export function renderGraph() { | |
| state.cy.destroy(); | ||
|
|
||
| const canReusePositions = | ||
| cyNodes.length > 0 && | ||
| cyNodes.every((n) => Object.prototype.hasOwnProperty.call(previousPositions, n.data.id)); | ||
| cyNodes.length > 0 && cyNodes.every((n) => Object.hasOwn(previousPositions, n.data.id)); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This dashboard JS is served directly as browser modules without transpilation, and replacing Useful? React with 👍 / 👎. |
||
| const graphMinZoom = computeGraphMinZoom(cyNodes.length); | ||
| state.cy = cytoscape({ | ||
| container, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When this badge is reachable by keyboard, its accessible name comes from its text content, which is just
--initially and later only the numeric score fromcomputeHealthScore(). Screen reader users tabbing to it will hear something like “82 button” without the “system health” context or that it opens a breakdown; thetitleis not a reliable accessible name when the button has content. Add anaria-labelhere and update it alongside the score.Useful? React with 👍 / 👎.