Skip to content

Commit 7a83481

Browse files
fix: Space and Enter keyboard support for zooming and CI improve for main to dev sync (#66)
* ci: skip staging deploy for sync commits from bot * feat: add Enter and Space keyboard support for zoom cycling in fullscreen * remove unnecessary comments * test: add tests for Enter/Space zoom and wheel zoom in fullscreen
1 parent 234533b commit 7a83481

4 files changed

Lines changed: 195 additions & 18 deletions

File tree

.github/workflows/github-pages-deploy.yml

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,33 @@ concurrency:
1111
cancel-in-progress: true
1212

1313
jobs:
14+
# Skip deployment for sync commits from the bot
15+
check:
16+
runs-on: ubuntu-latest
17+
outputs:
18+
should_deploy: ${{ steps.check.outputs.should_deploy }}
19+
steps:
20+
- name: Check if should deploy
21+
id: check
22+
env:
23+
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
24+
run: |
25+
if [[ "$COMMIT_MESSAGE" == "chore: sync from main" ]]; then
26+
echo "Skipping deployment for sync commit"
27+
echo "should_deploy=false" >> $GITHUB_OUTPUT
28+
else
29+
echo "should_deploy=true" >> $GITHUB_OUTPUT
30+
fi
31+
1432
test:
33+
needs: [check]
34+
if: needs.check.outputs.should_deploy == 'true'
1535
uses: ./.github/workflows/test.yml
1636

1737
build:
1838
runs-on: ubuntu-latest
19-
needs: [test]
39+
needs: [check, test]
40+
if: needs.check.outputs.should_deploy == 'true'
2041
permissions:
2142
contents: read
2243
steps:
@@ -50,7 +71,8 @@ jobs:
5071
name: github-pages
5172
url: ${{ steps.deployment.outputs.page_url }}
5273
runs-on: ubuntu-latest
53-
needs: build
74+
needs: [check, build]
75+
if: needs.check.outputs.should_deploy == 'true'
5476
permissions:
5577
pages: write
5678
id-token: write

.github/workflows/netlify-deploy.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
# Production deployment workflow
2-
# Triggered on push to main or manual dispatch
3-
# Runs tests and lint before deploying to Netlify
4-
51
name: Production → Netlify
62
on:
73
push:

src/hooks/useFullscreenGallery.js

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -94,18 +94,6 @@ export const useFullscreenGallery = (images, carouselApi) => {
9494
};
9595
}, [carouselApi, currentIndex]);
9696

97-
// Keyboard navigation
98-
useEffect(() => {
99-
if (!isFullscreen) return;
100-
101-
const handleKey = (e) => {
102-
if (e.key === "ArrowLeft") goToPrev();
103-
else if (e.key === "ArrowRight") goToNext();
104-
};
105-
document.addEventListener("keydown", handleKey);
106-
return () => document.removeEventListener("keydown", handleKey);
107-
}, [isFullscreen, goToPrev, goToNext]);
108-
10997
// Cycle to next zoom level
11098
const cycleZoom = useCallback(() => {
11199
const currentLevelIndex = ZOOM_LEVELS.findIndex((z) => scale <= z);
@@ -119,6 +107,22 @@ export const useFullscreenGallery = (images, carouselApi) => {
119107
if (newScale === 1) setPosition({ x: 0, y: 0 });
120108
}, [scale]);
121109

110+
// Keyboard navigation
111+
useEffect(() => {
112+
if (!isFullscreen) return;
113+
114+
const handleKey = (e) => {
115+
if (e.key === "ArrowLeft") goToPrev();
116+
else if (e.key === "ArrowRight") goToNext();
117+
else if (e.key === "Enter" || e.key === " ") {
118+
e.preventDefault();
119+
cycleZoom();
120+
}
121+
};
122+
document.addEventListener("keydown", handleKey);
123+
return () => document.removeEventListener("keydown", handleKey);
124+
}, [isFullscreen, goToPrev, goToNext, cycleZoom]);
125+
122126
// Wheel zoom - use native event listener with passive: false to allow preventDefault
123127
useEffect(() => {
124128
const container = containerRef.current;

src/hooks/useFullscreenGallery.test.js

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,161 @@ describe("useFullscreenGallery", () => {
524524
expect(result.current.currentIndex).toBe(1);
525525
});
526526

527+
it("handles Enter key for zoom cycling in fullscreen", async () => {
528+
const { result } = renderHook(() =>
529+
useFullscreenGallery(mockImages, mockCarouselApi)
530+
);
531+
532+
await setupFullscreen(result);
533+
534+
expect(result.current.scale).toBe(1);
535+
536+
// Press Enter to cycle zoom
537+
await act(async () => {
538+
const event = new KeyboardEvent("keydown", { key: "Enter" });
539+
document.dispatchEvent(event);
540+
});
541+
542+
expect(result.current.scale).toBe(1.5);
543+
});
544+
545+
it("handles Space key for zoom cycling in fullscreen", async () => {
546+
const { result } = renderHook(() =>
547+
useFullscreenGallery(mockImages, mockCarouselApi)
548+
);
549+
550+
await setupFullscreen(result);
551+
552+
expect(result.current.scale).toBe(1);
553+
554+
// Press Space to cycle zoom
555+
await act(async () => {
556+
const event = new KeyboardEvent("keydown", { key: " " });
557+
document.dispatchEvent(event);
558+
});
559+
560+
expect(result.current.scale).toBe(1.5);
561+
});
562+
563+
it("handles wheel zoom in fullscreen", async () => {
564+
const { result } = renderHook(() =>
565+
useFullscreenGallery(mockImages, mockCarouselApi)
566+
);
567+
568+
const mockContainer = document.createElement("div");
569+
mockContainer.requestFullscreen = vi.fn().mockResolvedValue();
570+
mockContainer.addEventListener = vi.fn();
571+
mockContainer.removeEventListener = vi.fn();
572+
result.current.containerRef.current = mockContainer;
573+
574+
await act(async () => {
575+
result.current.open(0);
576+
});
577+
578+
// Simulate fullscreen being active
579+
Object.defineProperty(document, "fullscreenElement", {
580+
value: mockContainer,
581+
writable: true,
582+
configurable: true,
583+
});
584+
585+
await act(async () => {
586+
document.dispatchEvent(new Event("fullscreenchange"));
587+
});
588+
589+
// The wheel handler should have been added
590+
expect(mockContainer.addEventListener).toHaveBeenCalledWith(
591+
"wheel",
592+
expect.any(Function),
593+
{ passive: false }
594+
);
595+
});
596+
597+
it("handles wheel zoom up (zoom in)", async () => {
598+
const { result } = renderHook(() =>
599+
useFullscreenGallery(mockImages, mockCarouselApi)
600+
);
601+
602+
const mockContainer = document.createElement("div");
603+
mockContainer.requestFullscreen = vi.fn().mockResolvedValue();
604+
605+
let wheelHandler;
606+
mockContainer.addEventListener = vi.fn((event, handler) => {
607+
if (event === "wheel") wheelHandler = handler;
608+
});
609+
mockContainer.removeEventListener = vi.fn();
610+
result.current.containerRef.current = mockContainer;
611+
612+
await act(async () => {
613+
result.current.open(0);
614+
});
615+
616+
Object.defineProperty(document, "fullscreenElement", {
617+
value: mockContainer,
618+
writable: true,
619+
configurable: true,
620+
});
621+
622+
await act(async () => {
623+
document.dispatchEvent(new Event("fullscreenchange"));
624+
});
625+
626+
// Simulate wheel scroll up (zoom in)
627+
await act(async () => {
628+
wheelHandler({ deltaY: -100, preventDefault: vi.fn() });
629+
});
630+
631+
expect(result.current.scale).toBeGreaterThan(1);
632+
});
633+
634+
it("handles wheel zoom down (zoom out) and resets position at scale 1", async () => {
635+
const { result } = renderHook(() =>
636+
useFullscreenGallery(mockImages, mockCarouselApi)
637+
);
638+
639+
const mockContainer = document.createElement("div");
640+
mockContainer.requestFullscreen = vi.fn().mockResolvedValue();
641+
642+
let wheelHandler;
643+
mockContainer.addEventListener = vi.fn((event, handler) => {
644+
if (event === "wheel") wheelHandler = handler;
645+
});
646+
mockContainer.removeEventListener = vi.fn();
647+
result.current.containerRef.current = mockContainer;
648+
649+
await act(async () => {
650+
result.current.open(0);
651+
});
652+
653+
Object.defineProperty(document, "fullscreenElement", {
654+
value: mockContainer,
655+
writable: true,
656+
configurable: true,
657+
});
658+
659+
await act(async () => {
660+
document.dispatchEvent(new Event("fullscreenchange"));
661+
});
662+
663+
// First zoom in
664+
await act(async () => {
665+
wheelHandler({ deltaY: -100, preventDefault: vi.fn() });
666+
});
667+
668+
expect(result.current.scale).toBeGreaterThan(1);
669+
670+
// Then zoom out back to 1
671+
await act(async () => {
672+
wheelHandler({ deltaY: 100, preventDefault: vi.fn() });
673+
wheelHandler({ deltaY: 100, preventDefault: vi.fn() });
674+
wheelHandler({ deltaY: 100, preventDefault: vi.fn() });
675+
wheelHandler({ deltaY: 100, preventDefault: vi.fn() });
676+
});
677+
678+
expect(result.current.scale).toBe(1);
679+
expect(result.current.position).toEqual({ x: 0, y: 0 });
680+
});
681+
527682
it("handles mouse down correctly when zoomed", async () => {
528683
const { result } = renderHook(() =>
529684
useFullscreenGallery(mockImages, mockCarouselApi)

0 commit comments

Comments
 (0)