From 57054349ef7816b84a31c29a8c847a734ab22479 Mon Sep 17 00:00:00 2001 From: cesnimda Date: Sat, 10 Oct 2026 21:52:01 +0200 Subject: [PATCH] fix(a11y): restore native keyboard file selection --- .../phase-5/p5-15/p5-15-04/file-picker/README.md | 10 +++++++++- job-tracker-ui/src/components/AddJobModal.tsx | 5 +++-- job-tracker-ui/src/components/Attachments.tsx | 4 ++-- job-tracker-ui/src/components/ImportExportJobs.tsx | 12 +++++++----- job-tracker-ui/src/file-picker-activation.test.tsx | 9 +++++---- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/docs/redesign/phase-5/p5-15/p5-15-04/file-picker/README.md b/docs/redesign/phase-5/p5-15/p5-15-04/file-picker/README.md index b248e2e..063ef43 100644 --- a/docs/redesign/phase-5/p5-15/p5-15-04/file-picker/README.md +++ b/docs/redesign/phase-5/p5-15/p5-15-04/file-picker/README.md @@ -6,6 +6,14 @@ From `f55c761ac229a69357a1c5b68a06e837cfa31ec0`, the unchanged native audit agai The label-only MUI buttons have no click handler. MUI keyboard activation invokes a nonnative button's handler; it does not emulate the browser's label-to-input default activation. The same pattern exists in Add Job's four attachment buckets. It is not an API, permissions or file-validation failure. -`src/file-picker-activation.test.tsx` adds eight real-component cases: seven keyboard/input activation boundaries (all fail with expected one input click/actual zero), plus file/Job association, uploading disabled state and post-upload selection reset. The latter also exposes that a label is not a native disabled button. Earlier test-fixture label/step mistakes were corrected before recording the final RED; they are not application defects. +`src/file-picker-activation.test.tsx` adds eight real-component cases: seven keyboard/input activation boundaries fail with expected one input click/actual zero; an eighth covers file/Job association, uploading disabled state and post-upload selection reset. Its initial matcher setup failed and is not application RED evidence. Fixture label/step and matcher setup mistakes were corrected; they are not application defects. Affected contracts: PRES-W08 attachments and PRES-J10 import/export; Add Job same-pattern caller is covered without changing its domain workflow. Whole groups remain FAIL at this reproduction checkpoint. No implementation correction or fresh full-suite claim is made here. Production remains NO-GO; human screen-reader/device acceptance remains incomplete. + +## Repair successor — GO for the bounded functional repair + +The three audited controls and Add Job's shared bucket control now use native buttons that click their existing referenced file inputs. Inputs remain hidden siblings, preventing bubbled input clicks from reactivating the button. Existing accepted-file constraints, multiple selection, bucket ownership, API payloads, upload progress/reset and import/conversion implementations are retained. There is no API/schema/storage change or extra dependency. + +Fresh rebuilt application: the original native chooser case passes **all six Enter/Space checks and three pointer checks**, without writing file data. The eight real-component cases pass, including distinct Add Job buckets and the two-file Job association/disabled-upload/reset behaviour. Complete frontend **498/498 across83 suites**, +8 from490; TypeScript, lint and production export build pass. Fresh backend1,033, focusedAI108, revision5/5 and sidecar49 also pass; those unchanged backend contracts remain independent owner/data proof. The suite's other documented browser failures are not claimed fixed by this repair. + +PRES-W08/J10 may move FAIL→PARTIAL after this verified boundary is reconciled; whole-group human/provider/long-tail requirements remain. Actual native chooser activation is stronger than a mocked picker success; upload/domain ownership additionally has backend evidence. Human screen-reader and physical-touch acceptance are still incomplete. Production remains NO-GO. diff --git a/job-tracker-ui/src/components/AddJobModal.tsx b/job-tracker-ui/src/components/AddJobModal.tsx index d2a9020..a4c6c96 100644 --- a/job-tracker-ui/src/components/AddJobModal.tsx +++ b/job-tracker-ui/src/components/AddJobModal.tsx @@ -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(() => emptyAttachmentBuckets()); + const attachmentInputRef = useRef(null); useEffect(() => { setCompanies(cachedCompanies); @@ -421,10 +422,10 @@ export default function AddJobModal({ open, onClose, onCreated, initialUrl }: Pr {label} {helperText} - + setFilesForBucket(bucket, e.target.files)} /> {filesLabel(attachments[bucket])} diff --git a/job-tracker-ui/src/components/Attachments.tsx b/job-tracker-ui/src/components/Attachments.tsx index 90d351e..3c322c7 100644 --- a/job-tracker-ui/src/components/Attachments.tsx +++ b/job-tracker-ui/src/components/Attachments.tsx @@ -233,10 +233,10 @@ export default function Attachments({ jobId }: { jobId: number }) { - + (null); + const importInputRef = useRef(null); + const csvInputRef = useRef(null); const exportAll = async (format: "json" | "csv") => { try { @@ -255,15 +257,15 @@ export default function ImportExportJobs() { - + void onImportFile(e.target.files?.[0] ?? null)} /> - + void onConvertCsv(e.target.files?.[0] ?? null)} /> {lastImportCount !== null && ( diff --git a/job-tracker-ui/src/file-picker-activation.test.tsx b/job-tracker-ui/src/file-picker-activation.test.tsx index 26bf270..b987de8 100644 --- a/job-tracker-ui/src/file-picker-activation.test.tsx +++ b/job-tracker-ui/src/file-picker-activation.test.tsx @@ -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());