From 7422f8bc5b99018b3b10a97eb3397d136b3c9c70 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:09:35 +0000 Subject: [PATCH 01/10] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=EC=99=B8?= =?UTF-8?q?=EB=B6=80=20=EB=A7=81=ED=81=AC=20=EC=83=88=20=ED=83=AD=20?= =?UTF-8?q?=EC=97=B4=EB=A6=BC=20=EC=8A=A4=ED=81=AC=EB=A6=B0=20=EB=A6=AC?= =?UTF-8?q?=EB=8D=94=20=EC=A0=91=EA=B7=BC=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/palette.md | 4 ++++ scanner/dashboard/index.html | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.jules/palette.md b/.jules/palette.md index ea004e2d..4ce77e97 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-15 - External Link Accessibility +**Learning:** Links opening in new tabs (`target="_blank"`) create a jarring context switch for screen reader users when they are not explicitly warned beforehand. +**Action:** When using `target="_blank"` for external links, explicitly warn screen reader users of the context switch by adding visually hidden text like `(opens in a new tab)` or updating the `aria-label`. diff --git a/scanner/dashboard/index.html b/scanner/dashboard/index.html index 132bc31b..720c6eba 100644 --- a/scanner/dashboard/index.html +++ b/scanner/dashboard/index.html @@ -281,7 +281,7 @@

Dashboard

function openDetail(f){ lastFocus = document.activeElement; const s = String(f.severity||'INFO').toUpperCase(); - const refs = (f.references||[]).map(r=>`${esc(r)}`).join('
'); + const refs = (f.references||[]).map(r=>`${esc(r)} (opens in a new tab)`).join('
'); const owasp = (f.owasp||[]).join(', '); const cwe = (f.cwe||[]).join(', '); const d = document.getElementById('detail'); From 1f27ea9cfe55eb4f8746241e68e014cd3bbfb122 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:12:58 +0000 Subject: [PATCH 02/10] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=EC=99=B8?= =?UTF-8?q?=EB=B6=80=20=EB=A7=81=ED=81=AC=20=EC=83=88=20=ED=83=AD=20?= =?UTF-8?q?=EC=97=B4=EB=A6=BC=20=EC=8A=A4=ED=81=AC=EB=A6=B0=20=EB=A6=AC?= =?UTF-8?q?=EB=8D=94=20=EC=A0=91=EA=B7=BC=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From e149094a021b8a43f623cfe926bc5f8f6d62ebd5 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:15:34 +0000 Subject: [PATCH 03/10] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=EC=99=B8?= =?UTF-8?q?=EB=B6=80=20=EB=A7=81=ED=81=AC=20=EC=83=88=20=ED=83=AD=20?= =?UTF-8?q?=EC=97=B4=EB=A6=BC=20=EC=8A=A4=ED=81=AC=EB=A6=B0=20=EB=A6=AC?= =?UTF-8?q?=EB=8D=94=20=EC=A0=91=EA=B7=BC=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 5a2cdf47d2a5b330ee527bb86b3ead0a90b70d1d Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:22:14 +0000 Subject: [PATCH 04/10] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=EC=99=B8?= =?UTF-8?q?=EB=B6=80=20=EB=A7=81=ED=81=AC=20=EC=83=88=20=ED=83=AD=20?= =?UTF-8?q?=EC=97=B4=EB=A6=BC=20=EC=8A=A4=ED=81=AC=EB=A6=B0=20=EB=A6=AC?= =?UTF-8?q?=EB=8D=94=20=EC=A0=91=EA=B7=BC=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 2a5da4369c1531f23cb5b81141acf5084a1b7147 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:28:02 +0000 Subject: [PATCH 05/10] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=EC=99=B8?= =?UTF-8?q?=EB=B6=80=20=EB=A7=81=ED=81=AC=20=EC=83=88=20=ED=83=AD=20?= =?UTF-8?q?=EC=97=B4=EB=A6=BC=20=EC=8A=A4=ED=81=AC=EB=A6=B0=20=EB=A6=AC?= =?UTF-8?q?=EB=8D=94=20=EC=A0=91=EA=B7=BC=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From c304a821c309c4dc46e72bcc7e52595e51c52005 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:38:37 +0000 Subject: [PATCH 06/10] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=EC=99=B8?= =?UTF-8?q?=EB=B6=80=20=EB=A7=81=ED=81=AC=20=EC=83=88=20=ED=83=AD=20?= =?UTF-8?q?=EC=97=B4=EB=A6=BC=20=EC=8A=A4=ED=81=AC=EB=A6=B0=20=EB=A6=AC?= =?UTF-8?q?=EB=8D=94=20=EC=A0=91=EA=B7=BC=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 02d18540d97186e9fe398e88fcc59d2022bcd694 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:52:21 +0000 Subject: [PATCH 07/10] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=EC=99=B8?= =?UTF-8?q?=EB=B6=80=20=EB=A7=81=ED=81=AC=20=EC=83=88=20=ED=83=AD=20?= =?UTF-8?q?=EC=97=B4=EB=A6=BC=20=EC=8A=A4=ED=81=AC=EB=A6=B0=20=EB=A6=AC?= =?UTF-8?q?=EB=8D=94=20=EC=A0=91=EA=B7=BC=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/palette.md | 6 +++--- scanner/dashboard/index.html | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.jules/palette.md b/.jules/palette.md index 4ce77e97..7c809f27 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -82,6 +82,6 @@ **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-15 - External Link Accessibility -**Learning:** Links opening in new tabs (`target="_blank"`) create a jarring context switch for screen reader users when they are not explicitly warned beforehand. -**Action:** When using `target="_blank"` for external links, explicitly warn screen reader users of the context switch by adding visually hidden text like `(opens in a new tab)` or updating the `aria-label`. +## 2026-08-15 - 외부 링크 스크린 리더 접근성 (External Link Accessibility) +**Learning:** 새 탭에서 열리는 링크(`target="_blank"`)는 사전에 안내되지 않으면 스크린 리더 사용자에게 갑작스러운 컨텍스트 전환을 일으켜 혼란을 줍니다. +**Action:** `target="_blank"`를 사용하는 외부 링크에는 항상 `(새 탭에서 열림)`과 같은 스크린 리더용 숨김 텍스트를 추가하거나 `aria-label`을 업데이트하여 컨텍스트 전환을 명확히 안내해야 합니다. diff --git a/scanner/dashboard/index.html b/scanner/dashboard/index.html index 720c6eba..13f3ad01 100644 --- a/scanner/dashboard/index.html +++ b/scanner/dashboard/index.html @@ -281,7 +281,7 @@

Dashboard

function openDetail(f){ lastFocus = document.activeElement; const s = String(f.severity||'INFO').toUpperCase(); - const refs = (f.references||[]).map(r=>`${esc(r)} (opens in a new tab)`).join('
'); + const refs = (f.references||[]).map(r=>`${esc(r)} (새 탭에서 열림)`).join('
'); const owasp = (f.owasp||[]).join(', '); const cwe = (f.cwe||[]).join(', '); const d = document.getElementById('detail'); From c0f724dd01978d284f48244a376837146f64686a Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 9 Sep 2026 20:02:25 +0000 Subject: [PATCH 08/10] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=EC=99=B8?= =?UTF-8?q?=EB=B6=80=20=EB=A7=81=ED=81=AC=20=EC=83=88=20=ED=83=AD=20?= =?UTF-8?q?=EC=97=B4=EB=A6=BC=20=EC=8A=A4=ED=81=AC=EB=A6=B0=20=EB=A6=AC?= =?UTF-8?q?=EB=8D=94=20=EC=A0=91=EA=B7=BC=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 8c5663783a2f4ef4afb29aaa78b90f468e5826d6 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 9 Sep 2026 22:34:09 +0000 Subject: [PATCH 09/10] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=EC=99=B8?= =?UTF-8?q?=EB=B6=80=20=EB=A7=81=ED=81=AC=20=EC=83=88=20=ED=83=AD=20?= =?UTF-8?q?=EC=97=B4=EB=A6=BC=20=EC=8A=A4=ED=81=AC=EB=A6=B0=20=EB=A6=AC?= =?UTF-8?q?=EB=8D=94=20=EC=A0=91=EA=B7=BC=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scanner/dashboard/index.html | 4 +++- tests/test_dashboard_core.py | 45 ++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/scanner/dashboard/index.html b/scanner/dashboard/index.html index 13f3ad01..0a75766e 100644 --- a/scanner/dashboard/index.html +++ b/scanner/dashboard/index.html @@ -96,6 +96,7 @@

+opens in a new tab
@@ -281,7 +282,8 @@

Dashboard

function openDetail(f){ lastFocus = document.activeElement; const s = String(f.severity||'INFO').toUpperCase(); - const refs = (f.references||[]).map(r=>`${esc(r)} (새 탭에서 열림)`).join('
'); + const extIcon = ``; + const refs = (f.references||[]).map(r=>`${esc(r)}${extIcon}`).join('
'); const owasp = (f.owasp||[]).join(', '); const cwe = (f.cwe||[]).join(', '); const d = document.getElementById('detail'); diff --git a/tests/test_dashboard_core.py b/tests/test_dashboard_core.py index 75a5809f..f6e9bac7 100644 --- a/tests/test_dashboard_core.py +++ b/tests/test_dashboard_core.py @@ -302,3 +302,48 @@ def test_dashboard_search_escape_clears_input(): assert "e.key === 'Escape'" in html assert "query = '';" in html assert "render();" in html + +def test_dashboard_external_links_have_accessible_visual_indicator(): + """External links opening in new tabs must have both an assistive warning and a visual indicator (WCAG G201).""" + from html.parser import HTMLParser + import re + + html = dashboard_index_path().read_text(encoding="utf-8") + + # Verify the global description element exists + assert 'opens in a new tab' in html + + refs_markup = re.search( + r"const refs\s*=\s*\(f\.references\|\|\[\]\)\.map\(r=>`(?P.*?)`\)\.join\('
'\);", + html, + flags=re.DOTALL, + ) + assert refs_markup is not None + + class _LinkAttributeParser(HTMLParser): + def __init__(self) -> None: + super().__init__() + self.links = [] + self.svgs = [] + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: + if tag == "a": + self.links.append(dict(attrs)) + if tag == "svg": + self.svgs.append(dict(attrs)) + + parser = _LinkAttributeParser() + # Inject a dummy href to make the template string parseable HTML + test_html = refs_markup.group("markup").replace('${esc(safeUrl(r))}', 'https://example.com').replace('${esc(r)}', 'Link').replace('${extIcon}', '') + parser.feed(test_html) + + assert len(parser.links) == 1 + link = parser.links[0] + assert link.get("target") == "_blank" + assert link.get("rel") == "noopener" + assert link.get("aria-describedby") == "ext-link-desc" + + assert len(parser.svgs) >= 1 + svg = parser.svgs[0] + assert svg.get("aria-hidden") == "true" + assert svg.get("focusable") == "false" From 95c25d01c70f32fb644c640d4d27d795ea9b0979 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 05:05:55 +0900 Subject: [PATCH 10/10] docs(ui): remove unverified global external-link doctrine --- .jules/palette.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.jules/palette.md b/.jules/palette.md index 7c809f27..ea004e2d 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -81,7 +81,3 @@ ## 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-15 - 외부 링크 스크린 리더 접근성 (External Link Accessibility) -**Learning:** 새 탭에서 열리는 링크(`target="_blank"`)는 사전에 안내되지 않으면 스크린 리더 사용자에게 갑작스러운 컨텍스트 전환을 일으켜 혼란을 줍니다. -**Action:** `target="_blank"`를 사용하는 외부 링크에는 항상 `(새 탭에서 열림)`과 같은 스크린 리더용 숨김 텍스트를 추가하거나 `aria-label`을 업데이트하여 컨텍스트 전환을 명확히 안내해야 합니다.