Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 49 additions & 12 deletions src/components/ui/Lightbox.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,37 @@ import { X, ChevronLeft, ChevronRight } from "lucide-react";

const Lightbox = ({ images, currentIndex, onClose, onNavigate }) => {
const touchStartX = useRef(null);
const containerRef = useRef(null);

const canGoPrev = currentIndex > 0;
const canGoNext = currentIndex < images.length - 1;

useEffect(() => {
const enterFullscreen = async () => {
try {
if (document.fullscreenElement === null) {
await containerRef.current?.requestFullscreen();
}
} catch {
// Fullscreen not supported or denied - continue without it
}
};

enterFullscreen();

return () => {
if (document.fullscreenElement) {
document.exitFullscreen().catch(() => {});
}
};
}, []);

const navigate = useCallback(
(direction) => {
if (direction === "prev") {
onNavigate(currentIndex > 0 ? currentIndex - 1 : images.length - 1);
} else {
onNavigate(currentIndex < images.length - 1 ? currentIndex + 1 : 0);
if (direction === "prev" && currentIndex > 0) {
onNavigate(currentIndex - 1);
} else if (direction === "next" && currentIndex < images.length - 1) {
onNavigate(currentIndex + 1);
}
},
[currentIndex, images.length, onNavigate]
Expand Down Expand Up @@ -44,14 +68,17 @@ const Lightbox = ({ images, currentIndex, onClose, onNavigate }) => {
}, [navigate, onClose]);

return (
<div className="fixed inset-0 z-50 flex items-center justify-center animate-in fade-in duration-200">
<div
ref={containerRef}
className="fixed inset-0 z-50 flex items-center justify-center bg-black animate-in fade-in duration-200"
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
{/* Backdrop - semantic button element */}
<button
type="button"
className="absolute inset-0 bg-black/95 cursor-default"
className="absolute inset-0 bg-black cursor-default"
onClick={onClose}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
aria-label="Fechar lightbox"
/>

Expand All @@ -69,7 +96,12 @@ const Lightbox = ({ images, currentIndex, onClose, onNavigate }) => {
<button
type="button"
onClick={() => navigate("prev")}
className="absolute left-4 top-1/2 -translate-y-1/2 text-white/70 hover:text-white transition-colors z-10 p-2"
disabled={!canGoPrev}
className={`absolute left-4 top-1/2 -translate-y-1/2 transition-colors z-10 p-2 ${
canGoPrev
? "text-white/70 hover:text-white cursor-pointer"
: "text-white/20 cursor-not-allowed"
}`}
aria-label="Foto anterior"
>
<ChevronLeft className="size-10" />
Expand All @@ -79,21 +111,26 @@ const Lightbox = ({ images, currentIndex, onClose, onNavigate }) => {
<button
type="button"
onClick={() => navigate("next")}
className="absolute right-4 top-1/2 -translate-y-1/2 text-white/70 hover:text-white transition-colors z-10 p-2"
disabled={!canGoNext}
className={`absolute right-4 top-1/2 -translate-y-1/2 transition-colors z-10 p-2 ${
canGoNext
? "text-white/70 hover:text-white cursor-pointer"
: "text-white/20 cursor-not-allowed"
}`}
aria-label="Próxima foto"
>
<ChevronRight className="size-10" />
</button>

{/* Image container */}
<figure className="relative z-10 aspect-3/4 max-h-[90vh] max-w-[90vw] overflow-hidden rounded-lg animate-in zoom-in-95 duration-200 block m-0">
<figure className="relative z-10 max-h-[95vh] max-w-[95vw] overflow-hidden rounded-lg animate-in zoom-in-95 duration-200 m-0">
<img
src={images[currentIndex].image}
alt={
images[currentIndex].alt ||
`Projeto ${currentIndex + 1} de ${images.length}`
}
className="w-full h-full object-cover"
className="max-h-[95vh] max-w-[95vw] object-contain"
/>
</figure>
</div>
Expand Down
38 changes: 33 additions & 5 deletions src/components/ui/Lightbox.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,16 +84,32 @@ describe("Lightbox", () => {
expect(onNavigateMock).toHaveBeenCalledWith(2);
});

it("wraps to last image when navigating prev from first image", () => {
it("does not navigate when at first image and prev is clicked", () => {
renderLightbox(0);
fireEvent.click(screen.getByLabelText("Foto anterior"));
expect(onNavigateMock).toHaveBeenCalledWith(2); // Last image index
expect(onNavigateMock).not.toHaveBeenCalled();
});

it("wraps to first image when navigating next from last image", () => {
it("does not navigate when at last image and next is clicked", () => {
renderLightbox(2);
fireEvent.click(screen.getByLabelText("Próxima foto"));
expect(onNavigateMock).toHaveBeenCalledWith(0); // First image index
expect(onNavigateMock).not.toHaveBeenCalled();
});

it("disables prev button at first image", () => {
renderLightbox(0);
expect(screen.getByLabelText("Foto anterior")).toBeDisabled();
});

it("disables next button at last image", () => {
renderLightbox(2);
expect(screen.getByLabelText("Próxima foto")).toBeDisabled();
});

it("enables both buttons in the middle", () => {
renderLightbox(1);
expect(screen.getByLabelText("Foto anterior")).not.toBeDisabled();
expect(screen.getByLabelText("Próxima foto")).not.toBeDisabled();
});

describe("keyboard navigation", () => {
Expand All @@ -115,6 +131,18 @@ describe("Lightbox", () => {
expect(onNavigateMock).toHaveBeenCalledWith(2);
});

it("does not navigate prev on ArrowLeft at first image", () => {
renderLightbox(0);
fireEvent.keyDown(document, { key: "ArrowLeft" });
expect(onNavigateMock).not.toHaveBeenCalled();
});

it("does not navigate next on ArrowRight at last image", () => {
renderLightbox(2);
fireEvent.keyDown(document, { key: "ArrowRight" });
expect(onNavigateMock).not.toHaveBeenCalled();
});

it("does not respond to other keys", () => {
renderLightbox();
fireEvent.keyDown(document, { key: "Enter" });
Expand Down Expand Up @@ -180,7 +208,7 @@ describe("Lightbox", () => {
});

it("stops propagation when clicking navigation buttons", () => {
renderLightbox();
renderLightbox(1); // Use middle index so both buttons are enabled

// Click prev button - should not trigger backdrop close
fireEvent.click(screen.getByLabelText("Foto anterior"));
Expand Down