Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
## 2026-07-23 - 테이블 반복 액션 버튼 접근성 개선
**Learning:** 데이터 테이블 내에서 반복적으로 사용되는 동일한 텍스트의 액션 버튼(예: "Details", "Status JSON")은 스크린 리더 사용자에게 맥락 없는 정보만을 전달하여 접근성을 크게 저하시킨다는 것을 확인했습니다.
**Action:** `aria-label`을 동적으로 생성하여(예: `Details for [파일명]`) 각 버튼의 고유한 컨텍스트를 명확히 제공함으로써 시각 장애인 사용자가 각 행의 액션을 정확히 구별하고 탐색할 수 있도록 접근성을 개선해야 합니다.
19 changes: 14 additions & 5 deletions src/main/resources/static/assets/viewer/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,11 @@ function updateJob(jobId, patch, { refreshKpisAfterUpdate = true } = {}) {
}
}

function createLink(href, label) {
function createLink(href, label, ariaLabel) {
const link = document.createElement("a");
if (ariaLabel) {
link.setAttribute("aria-label", ariaLabel);
}
link.href = href;
link.textContent = label;
link.className = "table-link";
Expand Down Expand Up @@ -110,8 +113,11 @@ 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");
if (ariaLabel) {
button.setAttribute("aria-label", ariaLabel);
}
button.type = "button";
button.textContent = label;
button.className = "btn btn-secondary btn-compact";
Expand Down Expand Up @@ -144,6 +150,7 @@ function renderHistory(history = loadHistory()) {
actionsCell.className = "table-actions";

if (job.statusUrl) {
const detailLabel = `Details for ${fileCell.textContent}`;
actionsCell.appendChild(createActionButton("Details", (e) => {
const btn = e.currentTarget;
const initialChildren = Array.from(btn.childNodes);
Expand All @@ -153,13 +160,15 @@ function renderHistory(history = loadHistory()) {
btn.replaceChildren(...initialChildren);
btn.disabled = false;
});
}));
}, detailLabel));
const jsonLabel = `Status JSON for ${fileCell.textContent}`;
actionsCell.appendChild(createActionButton("Status JSON", () => {
void openJsonDocument(job.statusUrl, "Clearfolio status JSON");
}));
}, jsonLabel));
}
if (job.jobId) {
actionsCell.appendChild(createLink(`/viewer/${encodeURIComponent(job.jobId)}`, "Open viewer"));
const viewerLabel = `Open viewer for ${fileCell.textContent}`;
actionsCell.appendChild(createLink(`/viewer/${encodeURIComponent(job.jobId)}`, "Open viewer", viewerLabel));
}

row.append(fileCell, statusCell, submittedCell, actionsCell);
Expand Down
Loading