From ff4485dc1090f9d5db7e195fdb1b1cf8493650fe Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Mon, 27 Jul 2026 21:32:20 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EB=B0=8F=20=EB=A7=81=ED=81=AC=EC=97=90=20=EB=AA=85?= =?UTF-8?q?=EC=8B=9C=EC=A0=81=20ARIA=20=EB=9D=BC=EB=B2=A8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 데모 히스토리 테이블(`demo.js`)의 액션 버튼("Details", "Status JSON") 및 링크("Open viewer")에 문서 파일명을 포함한 명시적인 ARIA 라벨을 부여하여 스크린 리더 사용자의 접근성을 향상시켰습니다. - 비동기 로딩 상태 전환 시 원본 ARIA 라벨이 유실되지 않도록 보존 및 복원 처리를 추가했습니다. - CHANGELOG 및 관련 UX 저널(`.jules/palette.md`)을 갱신하였습니다. --- .jules/palette.md | 4 +++ CHANGELOG.md | 3 +++ .../resources/static/assets/viewer/demo.js | 25 +++++++++++++++---- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/.jules/palette.md b/.jules/palette.md index a34ea59b..ef8e6559 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -13,3 +13,7 @@ ## 2026-07-13 - Async Table Actions UX **Learning:** Adding explicit loading and disabled states to table action buttons that invoke asynchronous processes helps prevent redundant API calls and visually assures the user that their request is being handled. **Action:** Consistently apply `disabled` state and `Loading...` text changes to inline table action buttons linked to async workflows, and carefully preserve underlying DOM structures with `Array.from(btn.childNodes)` during the loading cycle to avoid rendering regressions. + +## 2026-07-27 - Dynamic ARIA Labels for Iterative Table UI +**Learning:** Repetitive inline table action buttons and links (such as "Details" and "Open viewer") fail basic accessibility when they share the exact same visible label but point to different document contexts, creating confusion for screen reader users. +**Action:** Always provide dynamically generated `aria-label` attributes to repetitive action buttons and links by appending row-specific identifiers (e.g., `Details for [Filename]`) and preserve the dynamically injected label even across temporary async state transformations. diff --git a/CHANGELOG.md b/CHANGELOG.md index e8e8c980..9442554a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [Unreleased] +- 접근성: 데모 페이지 내 액션 버튼 및 링크에 context-specific ARIA label 추가 (`demo.js`의 `createLink`, `createActionButton` 변경) + ## [Unreleased] ### Added - **UI UX 개선**: 'Details' 버튼 클릭 시, 작업 상세 정보 로드 중에 사용자가 명시적인 로딩 상태를 확인할 수 있도록 'Loading...' 텍스트와 비활성화 상태를 표시하도록 추가했습니다. diff --git a/src/main/resources/static/assets/viewer/demo.js b/src/main/resources/static/assets/viewer/demo.js index 51466a8b..6d211e29 100644 --- a/src/main/resources/static/assets/viewer/demo.js +++ b/src/main/resources/static/assets/viewer/demo.js @@ -81,13 +81,16 @@ function updateJob(jobId, patch, { refreshKpisAfterUpdate = true } = {}) { } } -function createLink(href, label) { +function createLink(href, label, ariaLabel) { const link = document.createElement("a"); link.href = href; link.textContent = label; link.className = "table-link"; link.target = "_blank"; link.rel = "noopener noreferrer"; + if (ariaLabel) { + link.setAttribute("aria-label", ariaLabel); + } return link; } @@ -110,12 +113,15 @@ async function openJsonDocument(url, title) { : "Unable to load JSON evidence with the current tenant claim."; } -function createActionButton(label, onClick) { +function createActionButton(label, onClick, ariaLabel) { const button = document.createElement("button"); button.type = "button"; button.textContent = label; button.className = "btn btn-secondary btn-compact"; button.addEventListener("click", onClick); + if (ariaLabel) { + button.setAttribute("aria-label", ariaLabel); + } return button; } @@ -143,23 +149,32 @@ function renderHistory(history = loadHistory()) { submittedCell.textContent = job.submittedAt || ""; actionsCell.className = "table-actions"; + const fileDesc = job.fileName || "Document"; + if (job.statusUrl) { actionsCell.appendChild(createActionButton("Details", (e) => { const btn = e.currentTarget; const initialChildren = Array.from(btn.childNodes); btn.disabled = true; + const currentAriaLabel = btn.getAttribute("aria-label"); + if (currentAriaLabel) { + btn.setAttribute("aria-label", `Loading details for ${fileDesc}`); + } btn.textContent = "Loading..."; openJobDetail(job).finally(() => { btn.replaceChildren(...initialChildren); + if (currentAriaLabel) { + btn.setAttribute("aria-label", currentAriaLabel); + } btn.disabled = false; }); - })); + }, `Details for ${fileDesc}`)); actionsCell.appendChild(createActionButton("Status JSON", () => { void openJsonDocument(job.statusUrl, "Clearfolio status JSON"); - })); + }, `Status JSON for ${fileDesc}`)); } if (job.jobId) { - actionsCell.appendChild(createLink(`/viewer/${encodeURIComponent(job.jobId)}`, "Open viewer")); + actionsCell.appendChild(createLink(`/viewer/${encodeURIComponent(job.jobId)}`, "Open viewer", `Open viewer for ${fileDesc}`)); } row.append(fileCell, statusCell, submittedCell, actionsCell);