fix(a11y): restore native keyboard file selection
This commit is contained in:
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user