diff --git a/messages/de.json b/messages/de.json index 92a3748cf..18d6ef5f3 100644 --- a/messages/de.json +++ b/messages/de.json @@ -8621,7 +8621,8 @@ "uploading": "Wird hochgeladen…", "dismissFailed": "Fehlgeschlagenen Upload ausblenden", "processing": "Wird verarbeitet…", - "ready": "Bereit" + "ready": "Bereit", + "deleteKey": "Entf" }, "error": { "fileTooLarge": "Diese Datei ist größer als das Limit von {maxSize}.", diff --git a/messages/en.json b/messages/en.json index c109b2117..28e7a704f 100644 --- a/messages/en.json +++ b/messages/en.json @@ -8621,7 +8621,8 @@ "uploading": "Uploading…", "dismissFailed": "Dismiss failed upload", "processing": "Processing…", - "ready": "Ready" + "ready": "Ready", + "deleteKey": "Del" }, "error": { "fileTooLarge": "This file is larger than the {maxSize} limit.", diff --git a/messages/es.json b/messages/es.json index 09e33ddcf..cecad4c0e 100644 --- a/messages/es.json +++ b/messages/es.json @@ -8621,7 +8621,8 @@ "uploading": "Subiendo…", "dismissFailed": "Descartar subida fallida", "processing": "Procesando…", - "ready": "Listo" + "ready": "Listo", + "deleteKey": "Supr" }, "error": { "fileTooLarge": "Este archivo supera el límite de {maxSize}.", diff --git a/messages/fr.json b/messages/fr.json index ae2e08e34..bdce1e8ec 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -8621,7 +8621,8 @@ "uploading": "Téléversement…", "dismissFailed": "Ignorer le téléversement échoué", "processing": "Traitement en cours…", - "ready": "Prêt" + "ready": "Prêt", + "deleteKey": "Suppr" }, "error": { "fileTooLarge": "Ce fichier dépasse la limite de {maxSize}.", diff --git a/messages/it.json b/messages/it.json index 826a7cff7..20ff430fd 100644 --- a/messages/it.json +++ b/messages/it.json @@ -8621,7 +8621,8 @@ "uploading": "Caricamento…", "dismissFailed": "Ignora caricamento non riuscito", "processing": "Elaborazione…", - "ready": "Pronto" + "ready": "Pronto", + "deleteKey": "Canc" }, "error": { "fileTooLarge": "Questo file supera il limite di {maxSize}.", diff --git a/messages/pl.json b/messages/pl.json index 55b270397..b0b01785a 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -8621,7 +8621,8 @@ "uploading": "Przesyłanie…", "dismissFailed": "Odrzuć nieudane przesyłanie", "processing": "Przetwarzanie…", - "ready": "Gotowe" + "ready": "Gotowe", + "deleteKey": "Del" }, "error": { "fileTooLarge": "Ten plik przekracza limit {maxSize}.", diff --git a/src/components/documents/__tests__/document-card.test.tsx b/src/components/documents/__tests__/document-card.test.tsx index 5cb9a5e0b..6cece6b9a 100644 --- a/src/components/documents/__tests__/document-card.test.tsx +++ b/src/components/documents/__tests__/document-card.test.tsx @@ -65,6 +65,38 @@ describe("", () => { expect(html).not.toContain("Download only"); }); + it("announces and shows the Delete shortcut when the card is deletable", () => { + const html = render( + , + ); + // The keyboard contract used to live only in a code comment. It is now + // announced to assistive tech and drawn while the card holds focus. + expect(html).toContain('aria-keyshortcuts="Delete"'); + expect(html).toContain('data-slot="document-delete-hint"'); + expect(html).toContain("Del"); + }); + + it("offers no delete shortcut when the card carries no delete handler", () => { + const html = render( + , + ); + expect(html).not.toContain("aria-keyshortcuts"); + expect(html).not.toContain('data-slot="document-delete-hint"'); + }); + it("shows the download-only badge for attachment-class documents", () => { const html = render( { + it("deletes on Delete and selects on Space", () => { + expect(documentCardKeyAction("Delete")).toBe("delete"); + expect(documentCardKeyAction(" ")).toBe("select"); + }); + + it("does NOT delete on Backspace", () => { + // The reflex "go back" key on a card that takes focus by default, with + // no other delete control and only a transient undo toast behind it. + expect(documentCardKeyAction("Backspace")).toBeNull(); + }); + + it("ignores every other key", () => { + for (const key of ["Enter", "Escape", "a", "ArrowDown", "Del"]) { + expect(documentCardKeyAction(key)).toBeNull(); + } + }); +}); diff --git a/src/components/documents/document-card.tsx b/src/components/documents/document-card.tsx index ba9f94763..a1c05ffb1 100644 --- a/src/components/documents/document-card.tsx +++ b/src/components/documents/document-card.tsx @@ -29,6 +29,7 @@ import type { InboundDocumentDto } from "@/lib/validations/inbound-documents"; import { DOCUMENT_KIND_ICONS } from "./document-kind-meta"; import type { UploadQueueItem } from "./use-document-upload"; import { + documentCardKeyAction, documentDateKey, formatBytes, isDocumentProcessing, @@ -241,7 +242,15 @@ export function DocumentCard({ (the checkbox stays reachable via its own z-10). Keyboard contract (roving tabindex owned by the timeline): Enter opens (native click), Space toggles selection, Delete removes with undo. Touch: - press-and-hold selects instead of opening. */} + press-and-hold selects instead of opening. + + Backspace is deliberately NOT a delete key here. It is the reflex + "go back" key, the roving tabindex hands a card focus the moment + someone tabs into the grid, and a card carries no other delete + control — so the reflex press destroyed a medical document with + nothing but a transient undo toast to catch it. Delete alone, + announced through `aria-keyshortcuts` and shown as a `` chip + while the card holds focus. */}