diff --git a/.jules/palette.md b/.jules/palette.md
index 4075499..d36f05a 100644
--- a/.jules/palette.md
+++ b/.jules/palette.md
@@ -9,3 +9,7 @@
## 2024-08-05 - Dynamic Rendering Cursor Position
**Learning:** When recreating input elements dynamically during client-side rendering (e.g. updating `innerHTML` after a search keystroke), re-focusing the element isn't enough. Simply placing the cursor at the end of the value disrupts users who are editing text in the middle of a string.
**Action:** Always capture `e.target.selectionStart` and `e.target.selectionEnd` before the DOM is replaced, and use `el.setSelectionRange(start, end)` after the element is re-rendered to maintain a seamless typing experience.
+
+## 2024-09-06 - Table Row Button Accessibility
+**Learning:** When using `role="button"` on a `
` element, screen readers will announce the element as a button. However, the generic `aria-label="View details for finding"` overrides the entire row's text content, making it impossible for users to know *which* finding they are activating. Furthermore, dynamic filtering needs `aria-live="polite"` on the result count so users know their search or filter changed the number of results.
+**Action:** Always provide unique context in `aria-label` attributes on rows or buttons (e.g., incorporating the file name or ID). Always wrap filter result counts in `aria-live="polite"` regions.
diff --git a/scanner/dashboard/index.html b/scanner/dashboard/index.html
index 398ea8e..af239ea 100644
--- a/scanner/dashboard/index.html
+++ b/scanner/dashboard/index.html
@@ -154,7 +154,7 @@ No findings loaded
const rows = filtered.map(({f,i})=>{
const s = String(f.severity||'INFO').toUpperCase();
const block = isDeployBlocking(f);
- return `
+ return `
| ${s} |
${esc(f.message)} |
${esc(f.file)}:${esc(f.line)} |
@@ -163,9 +163,13 @@ No findings loaded
`;
}).join('');
+ const subText = filtered.length !== ALL.length
+ ? `Showing ${filtered.length} of ${ALL.length} findings · ${blocking} deploy-blocking (gate ${blocking?'active':'clear'})`
+ : `${ALL.length} findings · ${blocking} deploy-blocking (gate ${blocking?'active':'clear'})`;
+
app.innerHTML = `
Dashboard
- ${ALL.length} findings · ${blocking} deploy-blocking (gate ${blocking?'active':'clear'})
+ ${subText}
${cards}
Deploy-blocking
${blocking}
@@ -181,7 +185,7 @@ Dashboard
| Severity | Finding | File | Category | Status |
- ${rows||'| No findings match the filter. |
'}
+ ${rows||'| No findings match your criteria. Try adjusting your search or severity filter. |
'}
`;
diff --git a/tests/test_dashboard_core.py b/tests/test_dashboard_core.py
index 4759263..aad4325 100644
--- a/tests/test_dashboard_core.py
+++ b/tests/test_dashboard_core.py
@@ -48,7 +48,7 @@ def test_dashboard_rows_are_keyboard_accessible():
html = dashboard_index_path().read_text(encoding="utf-8")
assert 'tabindex="0" role="button"' in html
- assert 'aria-label="View details for finding"' in html
+ assert 'aria-label="View details: ${esc(f.rule_id)} in ${esc(f.file)}"' in html
assert "tbody tr:focus-visible" in html
assert "aria-label=\"Upload findings file\"" in html
assert "aria-label=\"Search findings\"" in html