@@ -162,9 +163,6 @@ export function TaskDocumentsTab({
* Task detail image artifacts must be viewable in-place from the task modal. Keep the expand target image-only so document, audio, video, and generic cards retain their current non-lightbox behavior without empty controls.
*/
const [lightboxArtifact, setLightboxArtifact] = useState
(null);
- const lightboxDialogRef = useRef(null);
- const lightboxCloseRef = useRef(null);
- const lightboxReturnFocusRef = useRef(null);
const loadedTaskIdRef = useRef(taskId);
const documentKeysRef = useRef>(new Set());
const { artifacts, loading: artifactsLoading, error: artifactsError } = useArtifacts({ projectId, taskId });
@@ -381,79 +379,8 @@ export function TaskDocumentsTab({
setEditContent("");
}
- const handleExpandArtifactImage = useCallback((artifact: ArtifactWithTask) => {
- lightboxReturnFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
- setLightboxArtifact(artifact);
- }, []);
-
- const handleCloseLightbox = useCallback(() => {
- setLightboxArtifact(null);
- lightboxReturnFocusRef.current?.focus();
- lightboxReturnFocusRef.current = null;
- }, []);
-
- useEffect(() => {
- if (!lightboxArtifact) {
- return;
- }
-
- const previousOverflow = document.body.style.overflow;
- document.body.style.overflow = "hidden";
- lightboxCloseRef.current?.focus();
-
- const handleKeyDown = (event: globalThis.KeyboardEvent) => {
- if (event.key === "Escape") {
- event.preventDefault();
- handleCloseLightbox();
- return;
- }
-
- if (event.key !== "Tab") {
- return;
- }
-
- /*
- * FNXC:ArtifactRegistry 2026-06-29-17:08:
- * The artifact preview declares an aria-modal dialog, so keyboard focus must stay inside the lightbox until Escape, overlay click, or the close button dismisses it. Cycle Tab/Shift+Tab over current focusable controls instead of letting focus escape into the task-detail modal behind the overlay.
- */
- const dialog = lightboxDialogRef.current;
- const focusableElements = Array.from(dialog?.querySelectorAll(
- 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])',
- ) ?? []).filter((element) => element.getAttribute("aria-hidden") !== "true");
-
- if (!dialog || focusableElements.length === 0) {
- event.preventDefault();
- return;
- }
-
- const firstElement = focusableElements[0];
- const lastElement = focusableElements[focusableElements.length - 1];
- const activeElement = document.activeElement;
-
- if (event.shiftKey && activeElement === firstElement) {
- event.preventDefault();
- lastElement.focus();
- } else if (!event.shiftKey && activeElement === lastElement) {
- event.preventDefault();
- firstElement.focus();
- } else if (!dialog.contains(activeElement)) {
- event.preventDefault();
- firstElement.focus();
- }
- };
-
- document.addEventListener("keydown", handleKeyDown);
- return () => {
- document.body.style.overflow = previousOverflow;
- document.removeEventListener("keydown", handleKeyDown);
- };
- }, [handleCloseLightbox, lightboxArtifact]);
-
- const handleLightboxOverlayClick = useCallback((event: MouseEvent) => {
- if (event.target === event.currentTarget) {
- handleCloseLightbox();
- }
- }, [handleCloseLightbox]);
+ const handleExpandArtifactImage = useCallback((artifact: ArtifactWithTask) => setLightboxArtifact(artifact), []);
+ const handleCloseLightbox = useCallback(() => setLightboxArtifact(null), []);
if (loading || artifactsLoading) {
return (
@@ -749,32 +676,7 @@ export function TaskDocumentsTab({
)}
- {lightboxArtifact && (
-
-
event.stopPropagation()}>
-
-
{lightboxArtifact.title || t("documents.untitledArtifact", "Untitled artifact")}
-
-
-
-

-
-
-
- )}
+ {lightboxArtifact && }
);
}
diff --git a/packages/dashboard/app/components/__tests__/ArtifactImageViewer.test.tsx b/packages/dashboard/app/components/__tests__/ArtifactImageViewer.test.tsx
new file mode 100644
index 0000000000..cc98c78041
--- /dev/null
+++ b/packages/dashboard/app/components/__tests__/ArtifactImageViewer.test.tsx
@@ -0,0 +1,48 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import type { ReactNode } from "react";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { ArtifactImageViewer } from "../ArtifactImageViewer";
+import { useArtifactImageBlob } from "../../hooks/useArtifactImageBlob";
+
+vi.mock("../../hooks/useArtifactImageBlob", () => ({ useArtifactImageBlob: vi.fn() }));
+vi.mock("../FloatingWindow", () => ({
+ FloatingWindow: ({ children, ariaLabel }: { children: ReactNode; ariaLabel?: string }) =>