Skip to content
Draft
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 @@ -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.

## 2024-09-03 - Added Proxy Button for File Input in Header
**Learning:** In zero-build dashboard (scanner/dashboard/index.html), the native <input type="file"> does not support firing the change event when a user attempts to select the same file path sequentially. Additionally, hiding the file input and exposing a proxy <button> allows users to have a much larger and better-styled click target.
**Action:** Visually hide the native <input type="file"> with the hidden attribute and create a proxy <button> that triggers the file input. Remove aria-label from the native input when using a proxy button to adhere to 'label-in-name' principles and update UI contract tests appropriately.
4 changes: 3 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" id="header-browse" class="primary-action" style="margin-left:12px">Upload findings file</button>
<input type="file" id="file" accept="application/json,.json" hidden>
</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,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
2 changes: 1 addition & 1 deletion tests/test_dashboard_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ 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 ">Upload findings file</button>" 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
38 changes: 38 additions & 0 deletions tests/test_dashboard_upload_proxy_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Contracts for the dashboard file-upload proxy control."""

from __future__ import annotations

import re
from pathlib import Path


DASHBOARD = Path("scanner/dashboard/index.html")


def _header_upload_button(html: str) -> str:
"""Return the shipped header upload button tag."""
match = re.search(r'<button\b[^>]*\bid="header-browse"[^>]*>', html)
assert match is not None, "header upload proxy button must exist"
return match.group(0)


def test_header_upload_proxy_preserves_primary_action_touch_target() -> None:
"""The header proxy must not override the shared 44px primary-action target."""
html = DASHBOARD.read_text(encoding="utf-8")
button = _header_upload_button(html)

assert 'class="primary-action"' in button
assert "min-height:auto" not in button.replace(" ", "")
assert "padding:6px 12px" not in button


def test_header_upload_proxy_keeps_visible_name_and_native_file_boundary() -> None:
"""The visible button owns the name while the hidden native input owns selection."""
html = DASHBOARD.read_text(encoding="utf-8")

assert ">Upload findings file</button>" in html
assert '<input type="file" id="file" accept="application/json,.json" hidden>' in html
assert "document.getElementById('header-browse').addEventListener('click', () => fileInput.click())" in html
# Same-file re-selection is enabled by clearing the native input after handling it;
# it is not a property of the proxy button itself.
assert "fileInput.value = '';" in html
Loading