From e062592f32223671b2aa4acc0426fcbf4a255fed Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 15 Jul 2026 21:21:11 +0000 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EB=B9=84=EB=8F=99=EA=B8=B0=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20=EB=B2=84=ED=8A=BC=EC=9D=98=20=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=EC=84=B1=20=EB=B0=8F=20=EC=83=81=ED=83=9C=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 비동기 로딩을 유발하는 버튼들에 `aria-busy` 상태 추가 - 동적 테이블 생성 시 컨텍스트가 포함된 `aria-label`을 전달하여 스크린 리더 환경 개선 - `.jules/palette.md` 접근성 개선 저널 항목 추가 --- .jules/palette.md | 3 +++ .../resources/static/assets/viewer/demo.js | 26 +++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/.jules/palette.md b/.jules/palette.md index a34ea59..ef56ddf 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -13,3 +13,6 @@ ## 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. +## 2024-05-24 - 동적 테이블 및 비동기 작업의 접근성 향상 +**Learning:** 동적으로 생성되는 테이블 내 액션 버튼(예: Details, Status JSON)은 텍스트만으로 스크린 리더에서 맥락을 파악하기 어려우므로 동적 `aria-label`(예: `Details for [파일명]`)을 부여해야 혼동을 방지할 수 있음을 확인했습니다. 또한, 비동기 작업 시 버튼이 단순히 비활성화(disabled)되는 것 외에도 `aria-busy="true"`를 적용해야 보조 기술이 로딩 상태를 명확히 인지함을 재확인했습니다. +**Action:** 향후 리스트나 테이블 안에서 반복되는 액션 요소나 비동기 요청을 수행하는 요소 구현 시, 동적인 식별 정보를 포함한 `aria-label`과 `aria-busy` 상태 토글을 필수로 적용하겠습니다. diff --git a/src/main/resources/static/assets/viewer/demo.js b/src/main/resources/static/assets/viewer/demo.js index 51466a8..9420922 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; } @@ -149,17 +155,19 @@ function renderHistory(history = loadHistory()) { const initialChildren = Array.from(btn.childNodes); btn.disabled = true; btn.textContent = "Loading..."; + btn.setAttribute("aria-busy", "true"); openJobDetail(job).finally(() => { btn.replaceChildren(...initialChildren); btn.disabled = false; + btn.removeAttribute("aria-busy"); }); - })); + }, `Details for ${job.fileName || "Document"}`)); actionsCell.appendChild(createActionButton("Status JSON", () => { void openJsonDocument(job.statusUrl, "Clearfolio status JSON"); - })); + }, `Status JSON for ${job.fileName || "Document"}`)); } if (job.jobId) { - actionsCell.appendChild(createLink(`/viewer/${encodeURIComponent(job.jobId)}`, "Open viewer")); + actionsCell.appendChild(createLink(`/viewer/${encodeURIComponent(job.jobId)}`, "Open viewer", `Open viewer for ${job.fileName || "Document"}`)); } row.append(fileCell, statusCell, submittedCell, actionsCell); @@ -300,6 +308,7 @@ async function retryActiveJob() { const initialChildren = Array.from(el.retryJobBtn.childNodes); el.retryJobBtn.disabled = true; el.retryJobBtn.textContent = "Retrying..."; + el.retryJobBtn.setAttribute("aria-busy", "true"); setStatus("Requesting operator retry..."); try { @@ -340,6 +349,7 @@ async function retryActiveJob() { } finally { el.retryJobBtn.replaceChildren(...initialChildren); el.retryJobBtn.disabled = false; + el.retryJobBtn.removeAttribute("aria-busy"); } } @@ -415,6 +425,7 @@ async function refreshKpiEvidence() { const initialChildren = Array.from(el.refreshEvidenceBtn.childNodes); el.refreshEvidenceBtn.disabled = true; el.refreshEvidenceBtn.textContent = "Refreshing..."; + el.refreshEvidenceBtn.setAttribute("aria-busy", "true"); try { const { res, data } = await fetchJson(KPI_EXPORTS_ENDPOINT); @@ -429,6 +440,7 @@ async function refreshKpiEvidence() { } finally { el.refreshEvidenceBtn.replaceChildren(...initialChildren); el.refreshEvidenceBtn.disabled = false; + el.refreshEvidenceBtn.removeAttribute("aria-busy"); } } @@ -436,6 +448,7 @@ async function loadDemoData() { const initialChildren = Array.from(el.loadDemoDataBtn.childNodes); el.loadDemoDataBtn.disabled = true; el.loadDemoDataBtn.textContent = "Loading..."; + el.loadDemoDataBtn.setAttribute("aria-busy", "true"); setStatus("Loading seeded buyer-demo story..."); try { @@ -460,6 +473,7 @@ async function loadDemoData() { } finally { el.loadDemoDataBtn.replaceChildren(...initialChildren); el.loadDemoDataBtn.disabled = false; + el.loadDemoDataBtn.removeAttribute("aria-busy"); } } @@ -508,6 +522,7 @@ async function submitDocument(event) { const initialChildren = Array.from(el.submitBtn.childNodes); el.submitBtn.disabled = true; el.submitBtn.textContent = "Submitting..."; + el.submitBtn.setAttribute("aria-busy", "true"); setStatus("Submitting document..."); try { @@ -552,6 +567,7 @@ async function submitDocument(event) { } finally { el.submitBtn.replaceChildren(...initialChildren); el.submitBtn.disabled = false; + el.submitBtn.removeAttribute("aria-busy"); } } From 2654902b14de31ff13208d19b22b0168b5ef4dfb Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 15 Jul 2026 22:09:27 +0000 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=EB=B9=84=EB=8F=99=EA=B8=B0=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20=EB=B2=84=ED=8A=BC=EC=9D=98=20=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=EC=84=B1=20=EB=B0=8F=20=EC=83=81=ED=83=9C=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 비동기 로딩을 유발하는 버튼들에 \`aria-busy\` 상태 추가 - 동적 테이블 생성 시 컨텍스트가 포함된 \`aria-label\`을 전달하여 스크린 리더 환경 개선 - \`.jules/palette.md\` 접근성 개선 저널 항목 추가 From 5c66f430098399e56fc501a1bf9fe224cc81a0ea Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 15 Jul 2026 22:43:44 +0000 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=EB=B9=84=EB=8F=99=EA=B8=B0=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20=EB=B2=84=ED=8A=BC=EC=9D=98=20=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=EC=84=B1=20=EB=B0=8F=20=EC=83=81=ED=83=9C=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 비동기 로딩을 유발하는 버튼들에 \`aria-busy\` 상태 추가 - 동적 테이블 생성 시 컨텍스트가 포함된 \`aria-label\`을 전달하여 스크린 리더 환경 개선 - \`.jules/palette.md\` 접근성 개선 저널 항목 추가 From 6309a68e49e59e26719c90c52b2373a6e2ff6e53 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 15 Jul 2026 23:17:27 +0000 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20=EB=B9=84=EB=8F=99=EA=B8=B0=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20=EB=B2=84=ED=8A=BC=EC=9D=98=20=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=EC=84=B1=20=EB=B0=8F=20=EC=83=81=ED=83=9C=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 비동기 로딩을 유발하는 버튼들에 \`aria-busy\` 상태 추가 - 동적 테이블 생성 시 컨텍스트가 포함된 \`aria-label\`을 전달하여 스크린 리더 환경 개선 - \`.jules/palette.md\` 접근성 개선 저널 항목 추가