From 0bc0517ddf937f0b81034ead16fc9b2f39104caf Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 10 Sep 2026 14:25:59 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Add=20explicit=20inte?= =?UTF-8?q?raction=20blocking=20for=20async=20loading=20states?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/palette.md | 4 + patch.diff | 20 +++ scanner/dashboard/console.html | 5 +- scanner/dashboard/console.html.orig | 186 ++++++++++++++++++++++++++++ test_ui.py | 12 ++ 5 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 patch.diff create mode 100644 scanner/dashboard/console.html.orig create mode 100644 test_ui.py diff --git a/.jules/palette.md b/.jules/palette.md index ea004e2d..6deffaa5 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -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-11-20 - Enforcing Interaction Blocking on Non-Button Loading States +**Learning:** Adding visual cues (like opacity) and `pointer-events: none` to elements with `aria-busy="true"` is insufficient for full accessibility, as keyboard interactions (`Enter`/`Space`) are not blocked. This allows users to trigger duplicate async requests if they use a keyboard. +**Action:** Always combine CSS visual blocking (`pointer-events: none`) with explicit JavaScript event guards (`if(el.getAttribute("aria-busy")==="true")return;`) in both `onclick` and `keydown` listeners for non-native interactive elements (like `tr[role="button"]`) during loading states. diff --git a/patch.diff b/patch.diff new file mode 100644 index 00000000..beffa678 --- /dev/null +++ b/patch.diff @@ -0,0 +1,20 @@ +--- scanner/dashboard/console.html ++++ scanner/dashboard/console.html +@@ -46,6 +46,7 @@ + .err{color:var(--crit);font-weight:600} + code{background:var(--bg);padding:1px 5px;border-radius:4px} + .hidden{display:none} ++ [aria-busy="true"]{pointer-events:none;opacity:0.6} + + + +@@ -135,8 +136,8 @@ + ${s.total}${pill(s.deploy_blocking,"var(--crit)")}${pill(s.new_blocking,"var(--high)")}`).join("")||'No scans. POST to /api/v1/scans from CI.'; + document.querySelectorAll("tr.scan").forEach(tr=>{ +- tr.onclick=()=>detail(tr.dataset.id,tr); +- tr.addEventListener('keydown', e => { if(e.key === 'Enter' || e.key === ' ') { e.preventDefault(); detail(tr.dataset.id,tr); } }); ++ tr.onclick=()=>{ if(tr.getAttribute("aria-busy")==="true")return; detail(tr.dataset.id,tr); }; ++ tr.addEventListener('keydown', e => { if(e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if(tr.getAttribute("aria-busy")==="true")return; detail(tr.dataset.id,tr); } }); + }); + }catch(e){ $("#msg").classList.remove("hidden");$("#app").classList.add("hidden"); + $("#msg").innerHTML=`${esc(e.message)}`; } diff --git a/scanner/dashboard/console.html b/scanner/dashboard/console.html index 7ec262af..02339016 100644 --- a/scanner/dashboard/console.html +++ b/scanner/dashboard/console.html @@ -46,6 +46,7 @@ .err{color:var(--crit);font-weight:600} code{background:var(--bg);padding:1px 5px;border-radius:4px} .hidden{display:none} + [aria-busy="true"]{pointer-events:none;opacity:0.6} @@ -135,8 +136,8 @@

AppGuardrail Console

${esc(s.created_at)}${esc(s.repo||"—")}${esc((s.commit||"—").slice(0,10))} ${s.total}${pill(s.deploy_blocking,"var(--crit)")}${pill(s.new_blocking,"var(--high)")}`).join("")||'No scans. POST to /api/v1/scans from CI.'; document.querySelectorAll("tr.scan").forEach(tr=>{ - tr.onclick=()=>detail(tr.dataset.id,tr); - tr.addEventListener('keydown', e => { if(e.key === 'Enter' || e.key === ' ') { e.preventDefault(); detail(tr.dataset.id,tr); } }); + tr.onclick=()=>{ if(tr.getAttribute("aria-busy")==="true")return; detail(tr.dataset.id,tr); }; + tr.addEventListener('keydown', e => { if(e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if(tr.getAttribute("aria-busy")==="true")return; detail(tr.dataset.id,tr); } }); }); }catch(e){ $("#msg").classList.remove("hidden");$("#app").classList.add("hidden"); $("#msg").innerHTML=`${esc(e.message)}`; } diff --git a/scanner/dashboard/console.html.orig b/scanner/dashboard/console.html.orig new file mode 100644 index 00000000..7ec262af --- /dev/null +++ b/scanner/dashboard/console.html.orig @@ -0,0 +1,186 @@ + + + + + +AppGuardrail Console + + + + +
+ +

AppGuardrail Console

+ + + + + +
+
+ + +
+ + + diff --git a/test_ui.py b/test_ui.py new file mode 100644 index 00000000..fe91a90a --- /dev/null +++ b/test_ui.py @@ -0,0 +1,12 @@ +from playwright.sync_api import sync_playwright + +def run(): + with sync_playwright() as p: + browser = p.chromium.launch() + page = browser.new_page() + page.goto("file:///app/scanner/dashboard/console.html") + page.wait_for_timeout(1000) + print("Page title:", page.title()) + browser.close() + +run()