Skip to content
4 changes: 4 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,7 @@
## 2026-08-12 - Skip to Content Accessibility
**Learning:** Screen reader and keyboard-only users experience significant friction when forced to navigate through repetitive header controls on every page load.
**Action:** Keep a visible-on-focus skip link as the first interactive element, target a programmatically focusable main container, and give the focused link a high-contrast outline.

## 2026-10-02 - Proxy Button Accessibility
**Learning:** Native `<input type="file">` elements are hard to style consistently. While replacing them with a custom button proxy improves visual design, developers often forget to make the hidden input inaccessible (`class="sr-only" tabindex="-1" aria-hidden="true"`) to screen readers, which leads to double announcements or confusing navigation.
**Action:** When replacing a native file input with a proxy button in UI components, always hide the native input explicitly using `aria-hidden="true"`, `class="sr-only"`, and `tabindex="-1"`. The proxy button should have standard keyboard support and trigger the hidden input dynamically. Ensure UI contract tests are updated to verify the new accessible structure instead of just looking for `aria-label` on the hidden input.
5 changes: 4 additions & 1 deletion scanner/dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@
<span class="logo"></span><span class="brand">AppGuardrail</span>
<span class="spacer"></span>
<span class="meta" id="src">no findings loaded</span>
<input type="file" id="file" accept="application/json,.json" aria-label="Upload findings file" style="margin-left:12px">
<button type="button" class="primary-action" id="header-browse" style="margin-left:12px">Upload findings</button>
<input type="file" id="file" accept="application/json,.json" class="sr-only" tabindex="-1" aria-hidden="true">
</header>
<p id="findings-summary" class="sr-only" role="status" aria-live="polite" aria-atomic="true"></p>
<main id="app" tabindex="-1"></main>
Expand Down Expand Up @@ -325,6 +326,8 @@ <h1>Dashboard</h1>
}

const fileInput = document.getElementById('file');
const headerBrowse = document.getElementById('header-browse');
if(headerBrowse) headerBrowse.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', () => {
const selectedFile = fileInput.files?.[0];
fileInput.value = '';
Expand Down
4 changes: 3 additions & 1 deletion tests/test_dashboard_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ def test_dashboard_rows_are_keyboard_accessible():
assert 'tabindex="0" role="button"' in html
assert 'title="View details for finding"' in html
assert "tbody tr:focus-visible" in html
assert "aria-label=\"Upload findings file\"" in html
assert 'class="sr-only" tabindex="-1" aria-hidden="true"' in html
assert 'id="header-browse"' in html
assert "headerBrowse.addEventListener('click', () => fileInput.click());" in html
assert "aria-label=\"Search findings\"" in html
assert "aria-label=\"Filter by severity\"" in html
assert "tr.addEventListener('keydown'" in html
Expand Down
Loading