From 2f1a976350174e0749b039b208186d77fd207597 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 1 Oct 2026 04:57:05 +0000 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=A0=91=EA=B7=BC?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20UX=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 네이티브 파일 입력(input type="file")을 aria-hidden="true" 등을 통해 접근성 트리에서 제거 - 사용자와 스크린 리더 친화적인 "Upload findings" 프록시 버튼(button) 추가 - 클릭 이벤트 리스너를 통한 안전한 파일 선택 동작 구현 - 관련 테스트 코드 수정 및 UX 저널(palette.md) 업데이트 --- .jules/palette.md | 4 ++++ scanner/dashboard/index.html | 8 +++++++- tests/test_dashboard_core.py | 3 ++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.jules/palette.md b/.jules/palette.md index ea004e2d2..a8d9f7365 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. + +## 2026-08-16 - Accessible Native File Input Proxy +**Learning:** `aria-hidden="true"`는 파일 입력 요소를 접근성 트리에서 제거합니다. 따라서 네이티브 `` 대신 명시적인 버튼을 노출하고 프록시로 동작하도록 하면 접근성이 향상됩니다. +**Action:** 앞으로 네이티브 파일 입력을 숨길 때 이 방식을 사용합니다. diff --git a/scanner/dashboard/index.html b/scanner/dashboard/index.html index 132bc31b2..4c29cc663 100644 --- a/scanner/dashboard/index.html +++ b/scanner/dashboard/index.html @@ -93,7 +93,8 @@ AppGuardrail no findings loaded - + +

@@ -388,6 +389,11 @@

Dashboard

if(isDeployBlocking(f)!==expect) console.error('selftest FAIL', f, 'expected', expect); } })(); + +(function() { + const btn = document.getElementById('upload-proxy'); + if(btn) btn.addEventListener('click', () => document.getElementById('file').click()); +})(); \ No newline at end of file diff --git a/tests/test_dashboard_core.py b/tests/test_dashboard_core.py index 75a5809f8..9c9e1b82e 100644 --- a/tests/test_dashboard_core.py +++ b/tests/test_dashboard_core.py @@ -77,7 +77,8 @@ 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 'id="upload-proxy"' in html + assert "Upload findings" in html assert "aria-label=\"Search findings\"" in html assert "aria-label=\"Filter by severity\"" in html assert "tr.addEventListener('keydown'" in html From ad07916b9969326f4e0cf2e67b88541515be0e41 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 1 Oct 2026 05:04:28 +0000 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=A0=91=EA=B7=BC?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20UX=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 네이티브 파일 입력(input type="file")을 aria-hidden="true" 등을 통해 접근성 트리에서 제거 - 사용자와 스크린 리더 친화적인 "Upload findings" 프록시 버튼(button) 추가 - 클릭 이벤트 리스너를 통한 안전한 파일 선택 동작 구현 - 관련 테스트 코드 수정 및 UX 저널(palette.md) 업데이트 From 75b34f877a5d7f09da0009f02084d32d9b7945c6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 3 Oct 2026 12:03:37 +0900 Subject: [PATCH 3/5] test(dashboard): execute upload proxy contract --- tests/test_dashboard_core.py | 80 ++++++++++++++++++++++++++++++++---- 1 file changed, 72 insertions(+), 8 deletions(-) diff --git a/tests/test_dashboard_core.py b/tests/test_dashboard_core.py index 9c9e1b82e..020050d84 100644 --- a/tests/test_dashboard_core.py +++ b/tests/test_dashboard_core.py @@ -3,6 +3,7 @@ import json import json as _json import re +import subprocess import threading import urllib.error import urllib.request @@ -16,18 +17,22 @@ make_dashboard_server, render_tokens_css) -class _ButtonAttributeParser(HTMLParser): - """Collect attributes from every dashboard button element.""" +class _DashboardControlParser(HTMLParser): + """Collect attributes from dashboard buttons and inputs.""" def __init__(self): - """Initialize an empty button-attribute collection.""" + """Initialize empty dashboard-control collections.""" super().__init__() self.buttons = [] + self.inputs = [] def handle_starttag(self, tag, attrs): - """Record one button's attributes while ignoring other elements.""" + """Record button and input attributes while ignoring other elements.""" + attributes = dict(attrs) if tag == "button": - self.buttons.append(dict(attrs)) + self.buttons.append(attributes) + elif tag == "input": + self.inputs.append(attributes) def _serve(server): @@ -77,14 +82,73 @@ 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 'id="upload-proxy"' in html - assert "Upload findings" 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_hides_input_and_delegates_click(): + """The visible upload button must own the native input's complete contract.""" + html = dashboard_index_path().read_text(encoding="utf-8") + parser = _DashboardControlParser() + parser.feed(html) + + file_input = next( + attributes for attributes in parser.inputs if attributes.get("id") == "file" + ) + proxy_button = next( + attributes + for attributes in parser.buttons + if attributes.get("id") == "upload-proxy" + ) + + assert file_input.get("type") == "file" + assert "sr-only" in file_input.get("class", "").split() + assert file_input.get("tabindex") == "-1" + assert file_input.get("aria-hidden") == "true" + assert proxy_button.get("type") == "button" + assert "Upload findings" in html + + bridge_match = re.search( + r"\(function\(\) \{\s*" + r"const btn = document\.getElementById\('upload-proxy'\);" + r".*?\}\)\(\);", + html, + flags=re.DOTALL, + ) + assert bridge_match is not None + + node_program = """ +let delegatedClicks = 0; +let clickHandler = null; +const uploadInput = {click() { delegatedClicks += 1; }}; +const proxyButton = { + addEventListener(eventName, handler) { + if (eventName === 'click') clickHandler = handler; + } +}; +const document = { + getElementById(elementId) { + if (elementId === 'upload-proxy') return proxyButton; + if (elementId === 'file') return uploadInput; + return null; + } +}; +""" + bridge_match.group(0) + """ +if (typeof clickHandler !== 'function') throw new Error('click handler missing'); +clickHandler(); +if (delegatedClicks !== 1) throw new Error('file input click not delegated'); +""" + result = subprocess.run( + ["node", "-e", node_program], + check=False, + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stderr + + def test_dashboard_severity_cards_are_accessible_filter_toggles(): """Severity cards must preserve their complete pointer and keyboard contract.""" html = dashboard_index_path().read_text(encoding="utf-8") @@ -286,7 +350,7 @@ def test_dashboard_dialog_close_button_has_tooltip(): ) assert detail_markup is not None - parser = _ButtonAttributeParser() + parser = _DashboardControlParser() parser.feed(detail_markup.group("markup")) assert any( From 54e49b84cb132965584a6e99a4b9b42c88af2ab8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 3 Oct 2026 16:58:14 +0900 Subject: [PATCH 4/5] test(dashboard): require upload proxy action token --- tests/test_dashboard_core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_dashboard_core.py b/tests/test_dashboard_core.py index 020050d84..b05a15a74 100644 --- a/tests/test_dashboard_core.py +++ b/tests/test_dashboard_core.py @@ -108,6 +108,7 @@ def test_dashboard_upload_proxy_hides_input_and_delegates_click(): assert file_input.get("tabindex") == "-1" assert file_input.get("aria-hidden") == "true" assert proxy_button.get("type") == "button" + assert "primary-action" in proxy_button.get("class", "").split() assert "Upload findings" in html bridge_match = re.search( From 1f620b2b4aac9d3a46239aee53719f017c789875 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 3 Oct 2026 16:58:28 +0900 Subject: [PATCH 5/5] fix(dashboard): reuse accessible upload action token --- scanner/dashboard/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scanner/dashboard/index.html b/scanner/dashboard/index.html index 4c29cc663..87f6db751 100644 --- a/scanner/dashboard/index.html +++ b/scanner/dashboard/index.html @@ -94,7 +94,7 @@ no findings loaded - +