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
26 changes: 24 additions & 2 deletions .github/workflows/github-pages-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
4 changes: 0 additions & 4 deletions .github/workflows/netlify-deploy.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
28 changes: 16 additions & 12 deletions src/hooks/useFullscreenGallery.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand Down
155 changes: 155 additions & 0 deletions src/hooks/useFullscreenGallery.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down