diff --git a/.jules/palette.md b/.jules/palette.md index a34ea59b..b628ab45 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. + +## 2024-07-25 - 비동기 액션 버튼의 접근성 개선 +**Learning:** 비동기 작업을 트리거하는 버튼에 aria-busy="true" 속성을 명시적으로 부여하면 스크린 리더와 같은 보조 기기 사용자에게 현재 작업이 진행 중임을 명확히 알릴 수 있습니다. +**Action:** 비동기 작업을 시작할 때 버튼의 aria-busy를 true로 설정하고 작업 완료 후 제거하여 접근성을 향상시켜야 합니다. diff --git a/src/main/resources/static/assets/viewer/demo.js b/src/main/resources/static/assets/viewer/demo.js index 51466a8b..dce390a7 100644 --- a/src/main/resources/static/assets/viewer/demo.js +++ b/src/main/resources/static/assets/viewer/demo.js @@ -148,10 +148,12 @@ function renderHistory(history = loadHistory()) { const btn = e.currentTarget; const initialChildren = Array.from(btn.childNodes); btn.disabled = true; + btn.setAttribute("aria-busy", "true"); btn.textContent = "Loading..."; openJobDetail(job).finally(() => { btn.replaceChildren(...initialChildren); btn.disabled = false; + btn.removeAttribute("aria-busy"); }); })); actionsCell.appendChild(createActionButton("Status JSON", () => { @@ -299,6 +301,7 @@ async function retryActiveJob() { const jobId = activeJobDetail.jobId; const initialChildren = Array.from(el.retryJobBtn.childNodes); el.retryJobBtn.disabled = true; + el.retryJobBtn.setAttribute("aria-busy", "true"); el.retryJobBtn.textContent = "Retrying..."; setStatus("Requesting operator retry..."); @@ -340,6 +343,7 @@ async function retryActiveJob() { } finally { el.retryJobBtn.replaceChildren(...initialChildren); el.retryJobBtn.disabled = false; + el.retryJobBtn.removeAttribute("aria-busy"); } } @@ -414,6 +418,7 @@ async function refreshKpis() { async function refreshKpiEvidence() { const initialChildren = Array.from(el.refreshEvidenceBtn.childNodes); el.refreshEvidenceBtn.disabled = true; + el.refreshEvidenceBtn.setAttribute("aria-busy", "true"); el.refreshEvidenceBtn.textContent = "Refreshing..."; try { @@ -429,12 +434,14 @@ async function refreshKpiEvidence() { } finally { el.refreshEvidenceBtn.replaceChildren(...initialChildren); el.refreshEvidenceBtn.disabled = false; + el.refreshEvidenceBtn.removeAttribute("aria-busy"); } } async function loadDemoData() { const initialChildren = Array.from(el.loadDemoDataBtn.childNodes); el.loadDemoDataBtn.disabled = true; + el.loadDemoDataBtn.setAttribute("aria-busy", "true"); el.loadDemoDataBtn.textContent = "Loading..."; setStatus("Loading seeded buyer-demo story..."); @@ -460,6 +467,7 @@ async function loadDemoData() { } finally { el.loadDemoDataBtn.replaceChildren(...initialChildren); el.loadDemoDataBtn.disabled = false; + el.loadDemoDataBtn.removeAttribute("aria-busy"); } } @@ -507,6 +515,7 @@ async function submitDocument(event) { const initialChildren = Array.from(el.submitBtn.childNodes); el.submitBtn.disabled = true; + el.submitBtn.setAttribute("aria-busy", "true"); el.submitBtn.textContent = "Submitting..."; setStatus("Submitting document..."); @@ -552,6 +561,7 @@ async function submitDocument(event) { } finally { el.submitBtn.replaceChildren(...initialChildren); el.submitBtn.disabled = false; + el.submitBtn.removeAttribute("aria-busy"); } }