diff --git a/.github/workflows/github-pages-deploy.yml b/.github/workflows/github-pages-deploy.yml index 401cc57..f49119f 100644 --- a/.github/workflows/github-pages-deploy.yml +++ b/.github/workflows/github-pages-deploy.yml @@ -11,12 +11,33 @@ concurrency: cancel-in-progress: true jobs: + # Skip deployment for sync commits from the bot + check: + runs-on: ubuntu-latest + outputs: + should_deploy: ${{ steps.check.outputs.should_deploy }} + steps: + - name: Check if should deploy + id: check + env: + COMMIT_MESSAGE: ${{ github.event.head_commit.message }} + run: | + if [[ "$COMMIT_MESSAGE" == "chore: sync from main" ]]; then + echo "Skipping deployment for sync commit" + echo "should_deploy=false" >> $GITHUB_OUTPUT + else + echo "should_deploy=true" >> $GITHUB_OUTPUT + fi + test: + needs: [check] + if: needs.check.outputs.should_deploy == 'true' uses: ./.github/workflows/test.yml build: runs-on: ubuntu-latest - needs: [test] + needs: [check, test] + if: needs.check.outputs.should_deploy == 'true' permissions: contents: read steps: @@ -50,7 +71,8 @@ jobs: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest - needs: build + needs: [check, build] + if: needs.check.outputs.should_deploy == 'true' permissions: pages: write id-token: write diff --git a/.github/workflows/netlify-deploy.yml b/.github/workflows/netlify-deploy.yml index ac0d66d..580a4e2 100644 --- a/.github/workflows/netlify-deploy.yml +++ b/.github/workflows/netlify-deploy.yml @@ -1,7 +1,3 @@ -# Production deployment workflow -# Triggered on push to main or manual dispatch -# Runs tests and lint before deploying to Netlify - name: Production → Netlify on: push: diff --git a/src/hooks/useFullscreenGallery.js b/src/hooks/useFullscreenGallery.js index 760e103..2ceed2a 100644 --- a/src/hooks/useFullscreenGallery.js +++ b/src/hooks/useFullscreenGallery.js @@ -94,18 +94,6 @@ export const useFullscreenGallery = (images, carouselApi) => { }; }, [carouselApi, currentIndex]); - // Keyboard navigation - useEffect(() => { - if (!isFullscreen) return; - - const handleKey = (e) => { - if (e.key === "ArrowLeft") goToPrev(); - else if (e.key === "ArrowRight") goToNext(); - }; - document.addEventListener("keydown", handleKey); - return () => document.removeEventListener("keydown", handleKey); - }, [isFullscreen, goToPrev, goToNext]); - // Cycle to next zoom level const cycleZoom = useCallback(() => { const currentLevelIndex = ZOOM_LEVELS.findIndex((z) => scale <= z); @@ -119,6 +107,22 @@ export const useFullscreenGallery = (images, carouselApi) => { if (newScale === 1) setPosition({ x: 0, y: 0 }); }, [scale]); + // Keyboard navigation + useEffect(() => { + if (!isFullscreen) return; + + const handleKey = (e) => { + if (e.key === "ArrowLeft") goToPrev(); + else if (e.key === "ArrowRight") goToNext(); + else if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + cycleZoom(); + } + }; + document.addEventListener("keydown", handleKey); + return () => document.removeEventListener("keydown", handleKey); + }, [isFullscreen, goToPrev, goToNext, cycleZoom]); + // Wheel zoom - use native event listener with passive: false to allow preventDefault useEffect(() => { const container = containerRef.current; diff --git a/src/hooks/useFullscreenGallery.test.js b/src/hooks/useFullscreenGallery.test.js index 89d5806..3b807ec 100644 --- a/src/hooks/useFullscreenGallery.test.js +++ b/src/hooks/useFullscreenGallery.test.js @@ -524,6 +524,161 @@ describe("useFullscreenGallery", () => { expect(result.current.currentIndex).toBe(1); }); + it("handles Enter key for zoom cycling in fullscreen", async () => { + const { result } = renderHook(() => + useFullscreenGallery(mockImages, mockCarouselApi) + ); + + await setupFullscreen(result); + + expect(result.current.scale).toBe(1); + + // Press Enter to cycle zoom + await act(async () => { + const event = new KeyboardEvent("keydown", { key: "Enter" }); + document.dispatchEvent(event); + }); + + expect(result.current.scale).toBe(1.5); + }); + + it("handles Space key for zoom cycling in fullscreen", async () => { + const { result } = renderHook(() => + useFullscreenGallery(mockImages, mockCarouselApi) + ); + + await setupFullscreen(result); + + expect(result.current.scale).toBe(1); + + // Press Space to cycle zoom + await act(async () => { + const event = new KeyboardEvent("keydown", { key: " " }); + document.dispatchEvent(event); + }); + + expect(result.current.scale).toBe(1.5); + }); + + it("handles wheel zoom in fullscreen", async () => { + const { result } = renderHook(() => + useFullscreenGallery(mockImages, mockCarouselApi) + ); + + const mockContainer = document.createElement("div"); + mockContainer.requestFullscreen = vi.fn().mockResolvedValue(); + mockContainer.addEventListener = vi.fn(); + mockContainer.removeEventListener = vi.fn(); + result.current.containerRef.current = mockContainer; + + await act(async () => { + result.current.open(0); + }); + + // Simulate fullscreen being active + Object.defineProperty(document, "fullscreenElement", { + value: mockContainer, + writable: true, + configurable: true, + }); + + await act(async () => { + document.dispatchEvent(new Event("fullscreenchange")); + }); + + // The wheel handler should have been added + expect(mockContainer.addEventListener).toHaveBeenCalledWith( + "wheel", + expect.any(Function), + { passive: false } + ); + }); + + it("handles wheel zoom up (zoom in)", async () => { + const { result } = renderHook(() => + useFullscreenGallery(mockImages, mockCarouselApi) + ); + + const mockContainer = document.createElement("div"); + mockContainer.requestFullscreen = vi.fn().mockResolvedValue(); + + let wheelHandler; + mockContainer.addEventListener = vi.fn((event, handler) => { + if (event === "wheel") wheelHandler = handler; + }); + mockContainer.removeEventListener = vi.fn(); + result.current.containerRef.current = mockContainer; + + await act(async () => { + result.current.open(0); + }); + + Object.defineProperty(document, "fullscreenElement", { + value: mockContainer, + writable: true, + configurable: true, + }); + + await act(async () => { + document.dispatchEvent(new Event("fullscreenchange")); + }); + + // Simulate wheel scroll up (zoom in) + await act(async () => { + wheelHandler({ deltaY: -100, preventDefault: vi.fn() }); + }); + + expect(result.current.scale).toBeGreaterThan(1); + }); + + it("handles wheel zoom down (zoom out) and resets position at scale 1", async () => { + const { result } = renderHook(() => + useFullscreenGallery(mockImages, mockCarouselApi) + ); + + const mockContainer = document.createElement("div"); + mockContainer.requestFullscreen = vi.fn().mockResolvedValue(); + + let wheelHandler; + mockContainer.addEventListener = vi.fn((event, handler) => { + if (event === "wheel") wheelHandler = handler; + }); + mockContainer.removeEventListener = vi.fn(); + result.current.containerRef.current = mockContainer; + + await act(async () => { + result.current.open(0); + }); + + Object.defineProperty(document, "fullscreenElement", { + value: mockContainer, + writable: true, + configurable: true, + }); + + await act(async () => { + document.dispatchEvent(new Event("fullscreenchange")); + }); + + // First zoom in + await act(async () => { + wheelHandler({ deltaY: -100, preventDefault: vi.fn() }); + }); + + expect(result.current.scale).toBeGreaterThan(1); + + // Then zoom out back to 1 + await act(async () => { + wheelHandler({ deltaY: 100, preventDefault: vi.fn() }); + wheelHandler({ deltaY: 100, preventDefault: vi.fn() }); + wheelHandler({ deltaY: 100, preventDefault: vi.fn() }); + wheelHandler({ deltaY: 100, preventDefault: vi.fn() }); + }); + + expect(result.current.scale).toBe(1); + expect(result.current.position).toEqual({ x: 0, y: 0 }); + }); + it("handles mouse down correctly when zoomed", async () => { const { result } = renderHook(() => useFullscreenGallery(mockImages, mockCarouselApi)