Refactor button and link attributes for accessibility
Refactor attributes for buttons and links to use aria-disabled and remove busy attributes. Update comments for clarity and maintainability.
This commit is contained in:
parent
5a0f024852
commit
f3a40eadbb
1 changed files with 15 additions and 30 deletions
|
|
@ -25,7 +25,6 @@ function ResultsArticle({
|
||||||
files: Filename[];
|
files: Filename[];
|
||||||
outputPath: string;
|
outputPath: string;
|
||||||
}) {
|
}) {
|
||||||
// If the DB schema includes jobs.num_files use it; otherwise fall back to files.length
|
|
||||||
const maxFiles =
|
const maxFiles =
|
||||||
typeof job.num_files === "number" && Number.isFinite(job.num_files)
|
typeof job.num_files === "number" && Number.isFinite(job.num_files)
|
||||||
? Number(job.num_files)
|
? Number(job.num_files)
|
||||||
|
|
@ -38,7 +37,6 @@ function ResultsArticle({
|
||||||
const isDone = maxFiles > 0 ? doneFiles >= maxFiles : true;
|
const isDone = maxFiles > 0 ? doneFiles >= maxFiles : true;
|
||||||
|
|
||||||
const disabledLinkClass = "pointer-events-none opacity-50";
|
const disabledLinkClass = "pointer-events-none opacity-50";
|
||||||
const busyAttrs = { disabled: true, "aria-busy": "true" } as const;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article class="article">
|
<article class="article">
|
||||||
|
|
@ -51,7 +49,7 @@ function ResultsArticle({
|
||||||
!isDone ? disabledLinkClass : ""
|
!isDone ? disabledLinkClass : ""
|
||||||
}`}
|
}`}
|
||||||
href={`${WEBROOT}/delete/${job.id}`}
|
href={`${WEBROOT}/delete/${job.id}`}
|
||||||
{...(!isDone ? busyAttrs : {})}
|
aria-disabled={!isDone ? "true" : undefined}
|
||||||
>
|
>
|
||||||
<DeleteIcon /> <p>Delete</p>
|
<DeleteIcon /> <p>Delete</p>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -62,7 +60,7 @@ function ResultsArticle({
|
||||||
}`}
|
}`}
|
||||||
href={`${WEBROOT}/archive/${job.id}`}
|
href={`${WEBROOT}/archive/${job.id}`}
|
||||||
download
|
download
|
||||||
{...(!isDone ? busyAttrs : {})}
|
aria-disabled={!isDone ? "true" : undefined}
|
||||||
>
|
>
|
||||||
<DownloadIcon /> <p>Tar</p>
|
<DownloadIcon /> <p>Tar</p>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -71,7 +69,8 @@ function ResultsArticle({
|
||||||
id="cxDownloadAll"
|
id="cxDownloadAll"
|
||||||
type="button"
|
type="button"
|
||||||
class="flex btn-primary flex-row gap-2 text-contrast"
|
class="flex btn-primary flex-row gap-2 text-contrast"
|
||||||
{...(!isDone ? busyAttrs : {})}
|
disabled={!isDone ? "true" : undefined}
|
||||||
|
aria-busy={!isDone ? "true" : undefined}
|
||||||
>
|
>
|
||||||
<DownloadIcon /> <p>All</p>
|
<DownloadIcon /> <p>All</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -215,7 +214,7 @@ function ResultsArticle({
|
||||||
<button
|
<button
|
||||||
id="cxShareSubmit"
|
id="cxShareSubmit"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-primary flex items-center gap-2"
|
class="flex btn-primary items-center gap-2"
|
||||||
>
|
>
|
||||||
Send
|
Send
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -253,11 +252,6 @@ function ResultsArticle({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* IMPORTANT FIX:
|
|
||||||
* Results HTML is re-rendered via /progress/:jobId and replaces the article + modal.
|
|
||||||
* So we MUST NOT keep stale DOM references. We re-query elements on each action.
|
|
||||||
*/
|
|
||||||
const shareJs = `
|
const shareJs = `
|
||||||
(function () {
|
(function () {
|
||||||
const WEBROOT = ${JSON.stringify(WEBROOT)};
|
const WEBROOT = ${JSON.stringify(WEBROOT)};
|
||||||
|
|
@ -316,13 +310,11 @@ const shareJs = `
|
||||||
currentFileName = null;
|
currentFileName = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delegated: always works even after /progress replaces the article
|
|
||||||
document.addEventListener("click", (e) => {
|
document.addEventListener("click", (e) => {
|
||||||
const t = e.target;
|
const t = e.target;
|
||||||
const btn = t && (t.closest ? t.closest('[data-share="true"]') : null);
|
const btn = t && (t.closest ? t.closest('[data-share="true"]') : null);
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
|
|
||||||
// kill old handlers (e.g. alert() from older results.js)
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (e.stopImmediatePropagation) e.stopImmediatePropagation();
|
if (e.stopImmediatePropagation) e.stopImmediatePropagation();
|
||||||
|
|
@ -332,7 +324,6 @@ const shareJs = `
|
||||||
openModal(jobId, fileName);
|
openModal(jobId, fileName);
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
// Delegated close (because modal DOM is replaced during progress polling)
|
|
||||||
document.addEventListener("click", (e) => {
|
document.addEventListener("click", (e) => {
|
||||||
const id = e.target && e.target.id;
|
const id = e.target && e.target.id;
|
||||||
if (id === "cxShareClose" || id === "cxShareCancel") {
|
if (id === "cxShareClose" || id === "cxShareCancel") {
|
||||||
|
|
@ -340,7 +331,6 @@ const shareJs = `
|
||||||
closeModal();
|
closeModal();
|
||||||
}
|
}
|
||||||
if (id === "cxShareModal") {
|
if (id === "cxShareModal") {
|
||||||
// click outside dialog closes
|
|
||||||
closeModal();
|
closeModal();
|
||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
|
@ -350,7 +340,6 @@ const shareJs = `
|
||||||
if (e.key === "Escape" && r.modal && !r.modal.classList.contains("hidden")) closeModal();
|
if (e.key === "Escape" && r.modal && !r.modal.classList.contains("hidden")) closeModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delegated copy
|
|
||||||
document.addEventListener("click", async (e) => {
|
document.addEventListener("click", async (e) => {
|
||||||
const t = e.target;
|
const t = e.target;
|
||||||
if (!t || t.id !== "cxShareCopy") return;
|
if (!t || t.id !== "cxShareCopy") return;
|
||||||
|
|
@ -369,7 +358,6 @@ const shareJs = `
|
||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
// Delegated submit
|
|
||||||
document.addEventListener("click", async (e) => {
|
document.addEventListener("click", async (e) => {
|
||||||
const t = e.target;
|
const t = e.target;
|
||||||
if (!t || t.id !== "cxShareSubmit") return;
|
if (!t || t.id !== "cxShareSubmit") return;
|
||||||
|
|
@ -413,13 +401,13 @@ const shareJs = `
|
||||||
}
|
}
|
||||||
|
|
||||||
const url =
|
const url =
|
||||||
json?.share_url ||
|
(json && typeof json === "object" && json.share_url) ||
|
||||||
json?.share_link ||
|
(json && typeof json === "object" && json.share_link) ||
|
||||||
json?.data?.url ||
|
(json && typeof json === "object" && json.data && json.data.url) ||
|
||||||
json?.data?.share?.url ||
|
(json && typeof json === "object" && json.data && json.data.share && json.data.share.url) ||
|
||||||
null;
|
null;
|
||||||
|
|
||||||
if (url) {
|
if (url && typeof url === "string") {
|
||||||
r.linkEl.value = url;
|
r.linkEl.value = url;
|
||||||
r.linkBlock.classList.remove("hidden");
|
r.linkBlock.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
|
|
@ -436,7 +424,6 @@ const shareJs = `
|
||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
// Download All: delegated, because button is replaced during progress polling
|
|
||||||
document.addEventListener("click", (e) => {
|
document.addEventListener("click", (e) => {
|
||||||
const t = e.target;
|
const t = e.target;
|
||||||
const btn = t && (t.closest ? t.closest("#cxDownloadAll") : null);
|
const btn = t && (t.closest ? t.closest("#cxDownloadAll") : null);
|
||||||
|
|
@ -458,7 +445,6 @@ const shareJs = `
|
||||||
|
|
||||||
export const results = new Elysia()
|
export const results = new Elysia()
|
||||||
.use(userService)
|
.use(userService)
|
||||||
|
|
||||||
.get(
|
.get(
|
||||||
"/results-share.js",
|
"/results-share.js",
|
||||||
() =>
|
() =>
|
||||||
|
|
@ -470,7 +456,6 @@ export const results = new Elysia()
|
||||||
}),
|
}),
|
||||||
{ auth: true },
|
{ auth: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
.get(
|
.get(
|
||||||
"/results/:jobId",
|
"/results/:jobId",
|
||||||
async ({ params, set, cookie: { job_id }, user }) => {
|
async ({ params, set, cookie: { job_id }, user }) => {
|
||||||
|
|
@ -506,10 +491,10 @@ export const results = new Elysia()
|
||||||
<ResultsArticle job={job} files={files} outputPath={outputPath} />
|
<ResultsArticle job={job} files={files} outputPath={outputPath} />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* keep existing file */}
|
{/* existing file */}
|
||||||
<script src={`${WEBROOT}/results.js`} defer />
|
<script src={`${WEBROOT}/results.js`} defer />
|
||||||
|
|
||||||
{/* our override must also load */}
|
{/* override handlers */}
|
||||||
<script src={`${WEBROOT}/results-share.js`} defer />
|
<script src={`${WEBROOT}/results-share.js`} defer />
|
||||||
</>
|
</>
|
||||||
</BaseHtml>
|
</BaseHtml>
|
||||||
|
|
@ -517,7 +502,6 @@ export const results = new Elysia()
|
||||||
},
|
},
|
||||||
{ auth: true },
|
{ auth: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
.post(
|
.post(
|
||||||
"/progress/:jobId",
|
"/progress/:jobId",
|
||||||
async ({ set, params, cookie: { job_id }, user }) => {
|
async ({ set, params, cookie: { job_id }, user }) => {
|
||||||
|
|
@ -544,7 +528,6 @@ export const results = new Elysia()
|
||||||
},
|
},
|
||||||
{ auth: true },
|
{ auth: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
.post(
|
.post(
|
||||||
"/share-to-erugo/:jobId",
|
"/share-to-erugo/:jobId",
|
||||||
async ({ params, body, user, set }) => {
|
async ({ params, body, user, set }) => {
|
||||||
|
|
@ -559,7 +542,9 @@ export const results = new Elysia()
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = db
|
const file = db
|
||||||
.query("SELECT * FROM file_names WHERE job_id = ? AND output_file_name = ?")
|
.query(
|
||||||
|
"SELECT * FROM file_names WHERE job_id = ? AND output_file_name = ?",
|
||||||
|
)
|
||||||
.as(Filename)
|
.as(Filename)
|
||||||
.get(params.jobId, body.fileName);
|
.get(params.jobId, body.fileName);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue