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 - Handle aria-label on loading states
**Learning:** Screen readers prioritize `aria-label` over text content. When dynamically updating a button's text content (like changing to "Loading..."), failing to also update its `aria-label` causes screen readers to miss the critical loading feedback and announce the original label instead.
**Action:** When temporarily changing a button to a loading state, store its original `aria-label` (if any), update the `aria-label` to match the loading text, and restore the original label after the async operation completes.
54 changes: 48 additions & 6 deletions src/main/resources/static/assets/viewer/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,13 @@ 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;
if (ariaLabel) {
link.setAttribute("aria-label", ariaLabel);
}
link.className = "table-link";
link.target = "_blank";
link.rel = "noopener noreferrer";
Expand All @@ -110,10 +113,13 @@ 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;
if (ariaLabel) {
button.setAttribute("aria-label", ariaLabel);
}
button.className = "btn btn-secondary btn-compact";
button.addEventListener("click", onClick);
return button;
Expand All @@ -138,7 +144,8 @@ function renderHistory(history = loadHistory()) {
const submittedCell = document.createElement("td");
const actionsCell = document.createElement("td");

fileCell.textContent = job.fileName || "Document";
const fileName = job.fileName || "Document";
fileCell.textContent = fileName;
statusCell.textContent = job.status || "SUBMITTED";
submittedCell.textContent = job.submittedAt || "";
actionsCell.className = "table-actions";
Expand All @@ -147,19 +154,26 @@ function renderHistory(history = loadHistory()) {
actionsCell.appendChild(createActionButton("Details", (e) => {
const btn = e.currentTarget;
const initialChildren = Array.from(btn.childNodes);
const originalAriaLabel = btn.getAttribute("aria-label");
btn.disabled = true;
btn.textContent = "Loading...";
if (originalAriaLabel) {
btn.setAttribute("aria-label", "Loading...");
}
openJobDetail(job).finally(() => {
btn.replaceChildren(...initialChildren);
btn.disabled = false;
if (originalAriaLabel) {
btn.setAttribute("aria-label", originalAriaLabel);
}
});
}));
}, `Details for ${fileName}`));
actionsCell.appendChild(createActionButton("Status JSON", () => {
void openJsonDocument(job.statusUrl, "Clearfolio status JSON");
}));
}, `Status JSON for ${fileName}`));
}
if (job.jobId) {
actionsCell.appendChild(createLink(`/viewer/${encodeURIComponent(job.jobId)}`, "Open viewer"));
actionsCell.appendChild(createLink(`/viewer/${encodeURIComponent(job.jobId)}`, "Open viewer", `Open viewer for ${fileName}`));
}

row.append(fileCell, statusCell, submittedCell, actionsCell);
Expand Down Expand Up @@ -298,8 +312,12 @@ async function retryActiveJob() {

const jobId = activeJobDetail.jobId;
const initialChildren = Array.from(el.retryJobBtn.childNodes);
const originalAriaLabel = el.retryJobBtn.getAttribute("aria-label");
el.retryJobBtn.disabled = true;
el.retryJobBtn.textContent = "Retrying...";
if (originalAriaLabel) {
el.retryJobBtn.setAttribute("aria-label", "Retrying...");
}
setStatus("Requesting operator retry...");

try {
Expand Down Expand Up @@ -340,6 +358,9 @@ async function retryActiveJob() {
} finally {
el.retryJobBtn.replaceChildren(...initialChildren);
el.retryJobBtn.disabled = false;
if (originalAriaLabel) {
el.retryJobBtn.setAttribute("aria-label", originalAriaLabel);
}
}
}

Expand Down Expand Up @@ -413,8 +434,12 @@ async function refreshKpis() {

async function refreshKpiEvidence() {
const initialChildren = Array.from(el.refreshEvidenceBtn.childNodes);
const originalAriaLabel = el.refreshEvidenceBtn.getAttribute("aria-label");
el.refreshEvidenceBtn.disabled = true;
el.refreshEvidenceBtn.textContent = "Refreshing...";
if (originalAriaLabel) {
el.refreshEvidenceBtn.setAttribute("aria-label", "Refreshing...");
}

try {
const { res, data } = await fetchJson(KPI_EXPORTS_ENDPOINT);
Expand All @@ -429,13 +454,20 @@ async function refreshKpiEvidence() {
} finally {
el.refreshEvidenceBtn.replaceChildren(...initialChildren);
el.refreshEvidenceBtn.disabled = false;
if (originalAriaLabel) {
el.refreshEvidenceBtn.setAttribute("aria-label", originalAriaLabel);
}
}
}

async function loadDemoData() {
const initialChildren = Array.from(el.loadDemoDataBtn.childNodes);
const originalAriaLabel = el.loadDemoDataBtn.getAttribute("aria-label");
el.loadDemoDataBtn.disabled = true;
el.loadDemoDataBtn.textContent = "Loading...";
if (originalAriaLabel) {
el.loadDemoDataBtn.setAttribute("aria-label", "Loading...");
}
setStatus("Loading seeded buyer-demo story...");

try {
Expand All @@ -460,6 +492,9 @@ async function loadDemoData() {
} finally {
el.loadDemoDataBtn.replaceChildren(...initialChildren);
el.loadDemoDataBtn.disabled = false;
if (originalAriaLabel) {
el.loadDemoDataBtn.setAttribute("aria-label", originalAriaLabel);
}
}
}

Expand Down Expand Up @@ -506,8 +541,12 @@ async function submitDocument(event) {
}

const initialChildren = Array.from(el.submitBtn.childNodes);
const originalAriaLabel = el.submitBtn.getAttribute("aria-label");
el.submitBtn.disabled = true;
el.submitBtn.textContent = "Submitting...";
if (originalAriaLabel) {
el.submitBtn.setAttribute("aria-label", "Submitting...");
}
setStatus("Submitting document...");

try {
Expand Down Expand Up @@ -552,6 +591,9 @@ async function submitDocument(event) {
} finally {
el.submitBtn.replaceChildren(...initialChildren);
el.submitBtn.disabled = false;
if (originalAriaLabel) {
el.submitBtn.setAttribute("aria-label", originalAriaLabel);
}
}
}

Expand Down
17 changes: 17 additions & 0 deletions src/main/resources/static/assets/viewer/viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,13 @@ function setLoading(message) {
el.error.hidden = true;
el.liveStatus.textContent = message;
el.preview.setAttribute("aria-busy", "true");
const originalAriaLabel = el.retryBtn.getAttribute("aria-label");
el.retryBtn.dataset.originalAriaLabel = originalAriaLabel || "";
el.retryBtn.disabled = true;
el.retryBtn.textContent = "Refreshing...";
if (originalAriaLabel) {
el.retryBtn.setAttribute("aria-label", "Refreshing...");
}
Comment on lines +53 to +59
}

function showError(message) {
Expand All @@ -62,6 +67,12 @@ function showError(message) {
el.errorTitle.focus();
el.retryBtn.disabled = false;
el.retryBtn.textContent = "Refresh";
const originalAriaLabel = el.retryBtn.dataset.originalAriaLabel;
if (originalAriaLabel) {
el.retryBtn.setAttribute("aria-label", originalAriaLabel);
} else {
el.retryBtn.removeAttribute("aria-label");
}
}

function clearPreview() {
Expand Down Expand Up @@ -211,6 +222,12 @@ async function poll(docId, abortSignal) {
el.liveStatus.textContent = "Ready.";
el.retryBtn.disabled = false;
el.retryBtn.textContent = "Refresh";
const originalAriaLabel = el.retryBtn.dataset.originalAriaLabel;
if (originalAriaLabel) {
el.retryBtn.setAttribute("aria-label", originalAriaLabel);
} else {
el.retryBtn.removeAttribute("aria-label");
}

clearPreview();
const path = bootstrap.data.previewResourcePath;
Expand Down
Loading