fix(a11y): restore native keyboard file selection

This commit is contained in:
cesnimda
2026-10-10 21:52:01 +02:00
parent 24c98e160f
commit 57054349ef
5 changed files with 26 additions and 14 deletions
@@ -144,6 +144,7 @@ export default function AddJobModal({ open, onClose, onCreated, initialUrl }: Pr
const [notes, setNotes] = useState("");
const [generateTailoredCv, setGenerateTailoredCv] = useState(false);
const [attachments, setAttachments] = useState<AttachmentBuckets>(() => emptyAttachmentBuckets());
const attachmentInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
setCompanies(cachedCompanies);
@@ -421,10 +422,10 @@ export default function AddJobModal({ open, onClose, onCreated, initialUrl }: Pr
<Typography sx={{ fontWeight: 800 }}>{label}</Typography>
<Typography variant="body2" sx={{ color: "text.secondary" }}>{helperText}</Typography>
</Box>
<Button component="label" variant="outlined" size="small" startIcon={<UploadFileOutlinedIcon />}>
<Button variant="outlined" size="small" startIcon={<UploadFileOutlinedIcon />} onClick={() => attachmentInputRef.current?.click()}>
{t("addJobModalChooseFiles")}
<input hidden type="file" multiple accept={ACCEPTED_DOCUMENT_TYPES} onChange={(e) => setFilesForBucket(bucket, e.target.files)} />
</Button>
<input ref={attachmentInputRef} hidden type="file" multiple accept={ACCEPTED_DOCUMENT_TYPES} onChange={(e) => setFilesForBucket(bucket, e.target.files)} />
</Box>
<Typography variant="caption" sx={{ display: "block", mt: 1, color: "text.secondary" }}>
{filesLabel(attachments[bucket])}
@@ -233,10 +233,10 @@ export default function Attachments({ jobId }: { jobId: number }) {
<Chip size="small" label={t("attachmentsMaxSize")} variant="outlined" />
</Box>
</Box>
<Button component="label" size="small" variant="outlined" startIcon={<CloudUploadOutlinedIcon />} disabled={uploading}>
<Button size="small" variant="outlined" startIcon={<CloudUploadOutlinedIcon />} disabled={uploading} onClick={() => fileInputRef.current?.click()}>
{uploading ? t("attachmentsUploading") : t("attachmentsUpload")}
<input ref={fileInputRef} type="file" multiple hidden onChange={upload} />
</Button>
<input ref={fileInputRef} type="file" multiple hidden onChange={upload} />
</Box>
<Box
@@ -1,4 +1,4 @@
import React, { useMemo, useState } from "react";
import React, { useMemo, useRef, useState } from "react";
import { Box, Button, Typography } from "@mui/material";
import { PageSection } from "../layout/PageLayout";
@@ -180,6 +180,8 @@ export default function ImportExportJobs() {
const { t } = useI18n();
const [importing, setImporting] = useState(false);
const [lastImportCount, setLastImportCount] = useState<number | null>(null);
const importInputRef = useRef<HTMLInputElement>(null);
const csvInputRef = useRef<HTMLInputElement>(null);
const exportAll = async (format: "json" | "csv") => {
try {
@@ -255,15 +257,15 @@ export default function ImportExportJobs() {
<Button variant="outlined" onClick={() => void exportAll("json")}>{t("exportJson")}</Button>
<Button variant="outlined" onClick={() => void exportAll("csv")}>{t("exportCsv")}</Button>
<Button variant="contained" component="label" disabled={importing}>
<Button variant="contained" disabled={importing} onClick={() => importInputRef.current?.click()}>
{importing ? t("profileUploading") : t("importJson")}
<input type="file" accept="application/json" hidden onChange={(e) => void onImportFile(e.target.files?.[0] ?? null)} />
</Button>
<input ref={importInputRef} type="file" accept="application/json" hidden onChange={(e) => void onImportFile(e.target.files?.[0] ?? null)} />
<Button variant="outlined" component="label">
<Button variant="outlined" onClick={() => csvInputRef.current?.click()}>
{t("convertCsvToImportJson")}
<input type="file" accept=".csv,text/csv" hidden onChange={(e) => void onConvertCsv(e.target.files?.[0] ?? null)} />
</Button>
<input ref={csvInputRef} type="file" accept=".csv,text/csv" hidden onChange={(e) => void onConvertCsv(e.target.files?.[0] ?? null)} />
{lastImportCount !== null && (
<Typography sx={{ ml: 1, color: "text.secondary" }}>
@@ -1,4 +1,5 @@
import React from "react";
import "@testing-library/jest-dom";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Attachments from "./components/Attachments";
@@ -23,11 +24,11 @@ async function expectNativeInputActivation(button: HTMLElement, input: HTMLInput
const activated = jest.fn();
input.addEventListener("click", activated);
act(() => button.focus());
await userEvent.keyboard("{Enter}");
await act(async () => { await userEvent.keyboard("{Enter}"); });
expect(activated).toHaveBeenCalledTimes(1);
await userEvent.keyboard(" ");
await act(async () => { await userEvent.keyboard(" "); });
expect(activated).toHaveBeenCalledTimes(2);
userEvent.click(button);
await act(async () => { userEvent.click(button); });
expect(activated).toHaveBeenCalledTimes(3);
expect(button).toHaveFocus();
}
@@ -85,7 +86,7 @@ test("attachment selection retains files and job ownership, disables activation
expect(body.getAll("files")).toEqual(files);
const activated = jest.fn();
input.addEventListener("click", activated);
userEvent.click(uploading);
act(() => uploading.click());
expect(activated).not.toHaveBeenCalled();
finishUpload({ data: {} });
await waitFor(() => expect(screen.getByRole("button", { name: "Upload" })).toBeEnabled());