Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,6 @@
## 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-08-13 - Add Context Switch Warning for External Links
**Learning:** Screen reader users need to be informed when links open in a new tab, as unexpected context switching can be disorienting. Simply adding `target="_blank"` is not enough.
**Action:** Always add visually hidden text or an explicit `aria-label` like `(opens in a new tab)` for links with `target="_blank"`.
2 changes: 1 addition & 1 deletion scanner/dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ <h1>Dashboard</h1>
function openDetail(f){
lastFocus = document.activeElement;
const s = String(f.severity||'INFO').toUpperCase();
const refs = (f.references||[]).map(r=>`<a href="${esc(safeUrl(r))}" target="_blank" rel="noopener">${esc(r)}</a>`).join('<br>');
const refs = (f.references||[]).map(r=>`<a href="${esc(safeUrl(r))}" target="_blank" rel="noopener" aria-label="${esc(r)} (opens in a new tab)">${esc(r)}</a>`).join('<br>');
const owasp = (f.owasp||[]).join(', ');
const cwe = (f.cwe||[]).join(', ');
const d = document.getElementById('detail');
Expand Down
28 changes: 28 additions & 0 deletions tests/test_dashboard_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,3 +302,31 @@ def test_dashboard_search_escape_clears_input():
assert "e.key === 'Escape'" in html
assert "query = '';" in html
assert "render();" in html

class _LinkAttributeParser(HTMLParser):
def __init__(self):
super().__init__()
self.links = []

def handle_starttag(self, tag, attrs):
if tag == "a":
self.links.append(dict(attrs))

def test_dashboard_external_links_warn_screen_readers():
"""External links opening in a new tab should inform screen readers."""
html_content = dashboard_index_path().read_text(encoding="utf-8")
detail_markup = re.search(
r"const refs = \(f\.references\|\|\[\]\)\.map\(r=>`(.*?)`\)\.join\('<br>'\);",
html_content,
flags=re.DOTALL,
)
assert detail_markup is not None

parser = _LinkAttributeParser()
parser.feed(detail_markup.group(1))

assert any(
attributes.get("target") == "_blank"
and attributes.get("aria-label") == "${esc(r)} (opens in a new tab)"
for attributes in parser.links
)
Loading