Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,7 @@
## 2025-02-18 - Search Input Escape Key
**Learning:** Users who heavily rely on keyboard navigation (and power users) experience friction when forced to backspace manually or switch to the mouse to click a "Clear" button after filtering a list.
**Action:** Always provide an `Escape` key listener on search inputs to instantly clear the query and re-render the view, matching native OS text field behavior.

## 2024-08-12 - Proxy native file inputs for consistent UX
**Learning:** In single-page applications, browsers natively suppress `change` events on `<input type="file">` if the user selects the same file path sequentially. Additionally, native file inputs have inconsistent cross-browser styling.
**Action:** Visually hide the native `<input type="file">` (using `sr-only`, `tabindex="-1"`, and `aria-hidden="true"`) and use a styled proxy `<button>` with an explicit click event listener to trigger the hidden file input. This allows complete styling control and fixes same-file selection issues when combined with clearing the input value after `change`.
4 changes: 3 additions & 1 deletion scanner/dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,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" id="header-browse" class="tag" style="margin-left:12px; cursor:pointer; color:var(--text); font-size: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"></main>
Expand Down Expand Up @@ -320,6 +321,7 @@ <h1>Dashboard</h1>
}

const fileInput = document.getElementById('file');
document.getElementById('header-browse').addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', () => {
const selectedFile = fileInput.files?.[0];
fileInput.value = '';
Expand Down
12 changes: 11 additions & 1 deletion tests/test_dashboard_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,23 @@ 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 "aria-label=\"Search findings\"" in html
assert "aria-label=\"Filter by severity\"" in html
assert "tr.addEventListener('keydown'" in html
assert "e.key === 'Enter' || e.key === ' '" in html


def test_dashboard_upload_proxy_preserves_accessible_file_selection_contract():
"""The styled upload control must remain a real button wired to the hidden input."""
html = dashboard_index_path().read_text(encoding="utf-8")

assert '<button type="button" id="header-browse"' in html
assert 'id="file" accept="application/json,.json" class="sr-only"' in html
assert 'tabindex="-1" aria-hidden="true"' in html
assert "document.getElementById('header-browse').addEventListener('click'" in html
assert "fileInput.value = '';" in html


def test_dashboard_escapes_severity_in_innerhtml():
"""Severity chips must go through esc() โ€” findings JSON is untrusted input."""
html = dashboard_index_path().read_text(encoding="utf-8")
Expand Down
Loading