diff --git a/.jules/palette.md b/.jules/palette.md index a34ea59b2..6150562b9 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-08-17 - 비동기 버튼 로딩 시 명시적 aria-busy 속성 적용 및 상태 복원 +**Learning:** 단독으로 작동하는 비동기 액션 버튼은 로딩 중일 때 시각적인 변화뿐만 아니라 스크린 리더 등 보조 기기에 정확한 상태를 전달하기 위해 동적으로 `aria-busy="true"` 속성을 부여해야 합니다. 또한, 로딩이 완료된 후 버튼 내부의 원래 DOM 구조를 안전하게 복원하기 위해 `innerHTML` 대신 `childNodes`를 저장해 두고 `replaceChildren`으로 복원하는 것이 권장됩니다. +**Action:** 비동기 네트워크 요청을 수행하는 버튼의 경우, 시작 시 `disabled` 설정과 함께 `aria-busy="true"`를 부여하고 원본 `childNodes`를 별도로 저장하여, 종료 시 상태 복원 및 `aria-busy` 속성을 제거(`removeAttribute`)하도록 일관되게 구현합니다. diff --git a/src/main/resources/static/assets/viewer/viewer.js b/src/main/resources/static/assets/viewer/viewer.js index a998675c7..ea3c59102 100644 --- a/src/main/resources/static/assets/viewer/viewer.js +++ b/src/main/resources/static/assets/viewer/viewer.js @@ -19,6 +19,7 @@ const el = { }; let pdfJsModulePromise; +let originalRetryBtnNodes = null; function getMetaContent(name) { const meta = document.querySelector(`meta[name="${name}"]`); @@ -55,6 +56,10 @@ function setLoading(message) { el.liveStatus.textContent = message; el.preview.setAttribute("aria-busy", "true"); el.retryBtn.disabled = true; + el.retryBtn.setAttribute("aria-busy", "true"); + if (!originalRetryBtnNodes) { + originalRetryBtnNodes = Array.from(el.retryBtn.childNodes); + } el.retryBtn.textContent = "Refreshing..."; } @@ -65,7 +70,10 @@ function showError(message) { el.preview.setAttribute("aria-busy", "false"); el.errorTitle.focus(); el.retryBtn.disabled = false; - el.retryBtn.textContent = "Refresh"; + el.retryBtn.removeAttribute("aria-busy"); + if (originalRetryBtnNodes) { + el.retryBtn.replaceChildren(...originalRetryBtnNodes); + } } function clearPreview() { @@ -274,7 +282,10 @@ async function poll(docId, abortSignal) { el.preview.setAttribute("aria-busy", "false"); el.liveStatus.textContent = "Ready."; el.retryBtn.disabled = false; - el.retryBtn.textContent = "Refresh"; + el.retryBtn.removeAttribute("aria-busy"); + if (originalRetryBtnNodes) { + el.retryBtn.replaceChildren(...originalRetryBtnNodes); + } } catch (_error) { if (abortSignal.aborted) { return;