Skip to content

Restore mouse-wheel zoom on React match-results and encounter image viewers#1628

Open
JasonWildMe wants to merge 1 commit into
mainfrom
feature/wheel-zoom-standardization
Open

Restore mouse-wheel zoom on React match-results and encounter image viewers#1628
JasonWildMe wants to merge 1 commit into
mainfrom
feature/wheel-zoom-standardization

Conversation

@JasonWildMe

Copy link
Copy Markdown
Collaborator

Problem

A researcher reported she can no longer mouse-wheel to zoom on the React match-results page images and has to use the icon buttons. Confirmed: there is no wheel/onWheel listener anywhere in the React frontend — wheel-to-zoom existed in the older viewer and was never ported into the React rewrite.

Fix

Restore wheel-zoom and standardize it behind a small shared hook so the three (currently divergent) zoom/pan viewers can share one implementation.

  • New frontend/src/hooks/useWheelZoom.js — attaches a native, non-passive wheel listener to a target element. A native listener is required because React's synthetic onWheel is registered passively and cannot preventDefault(), so the page would scroll behind the zoom. The hook owns no zoom state: callers pass onZoom(direction) (+1 in / -1 out) and keep their own zoom/pan/clamp semantics. A latest-callback ref avoids re-subscribing the listener every render.
  • AnnotationOverlay.jsx (match-results viewer, the reported regression) — wheel zoom mirrors the existing zoomIn/zoomOut imperative handle (clampZoom + clampPan + zoomStep), gated on imageLoaded, attached to the existing outerContainerRef.
  • ImageModal.jsx (encounter-page modal) — wheel zoom matches its buttons (step 0.25, range 1..3); pan re-centers via the existing [zoom, safeIndex] effect. Listener attached to a new ref on #image-modal-image-container. Also moved the if (!assets…) return null guard below the hooks (it previously sat above all hooks — a latent Rules-of-Hooks violation) and normalized the assets prop to an array so the hooks are null-safe.

Behavior decisions

  • One zoom step per wheel notch, centered — matches what the icon buttons already do. Zoom-to-cursor is deliberately left as a future enhancement: the two viewers use different transformOrigin (top-left vs center) and pan models (clamped vs reset-on-zoom), so cursor-anchoring would reintroduce per-component divergence.
  • preventDefault() stops the page from scrolling while zooming. The listener is scoped to the image container only.
  • No change to the zoom buttons, imperative handle, pan-drag, annotation rendering, or zoom bounds.

Scope / follow-up

  • ImageGalleryModal.jsx is intentionally excluded. Its zoom/pan comes from the still-open Gallery View Zooming and Panning Feature #1596 (no zoom on main); adding wheel-zoom there now would conflict. It gets the same useWheelZoom hook in a quick follow-up once Gallery View Zooming and Panning Feature #1596 merges.
  • Trackpads emit many small wheel events, so wheel-zoom can feel fast on a trackpad; acceptable for restoring the mouse-wheel behavior and bounded by the zoom clamps. A delta-accumulation throttle is a possible future refinement.

Testing

  • Babel parse OK; ESLint clean (project flat config) on all three files; LF; git diff --check clean.
  • Design and implementation each reviewed by a second-opinion pass (caught and fixed the ImageModal hook-ordering + null-safety issues).
  • Manual (after build/deploy): on /react/match-results, wheel up/down over an image zooms in/out within bounds, the page doesn't scroll, annotation rects stay aligned, and the buttons still work. Same on the encounter-page image modal.

🤖 Generated with Claude Code

Wheel-to-zoom was lost in the React rewrite: there was no wheel handler anywhere
in the frontend, so users had to use the icon buttons to zoom match-results and
encounter images. This restores it and standardizes the behavior behind a small
shared hook.

- New hook frontend/src/hooks/useWheelZoom.js attaches a NATIVE, non-passive
  "wheel" listener to a target element (React's synthetic onWheel is passive and
  cannot preventDefault, so the page would scroll behind the zoom). It owns no
  zoom state: callers pass onZoom(direction) (+1 in / -1 out) and keep their own
  zoom/pan/clamp semantics. A latest-callback ref avoids re-subscribing each
  render.
- AnnotationOverlay.jsx (match-results viewer): wheel zoom mirrors the existing
  zoomIn/zoomOut imperative handle (clampZoom + clampPan + zoomStep), gated on
  imageLoaded, attached to the existing outerContainerRef.
- ImageModal.jsx (encounter modal): wheel zoom matches its buttons (step 0.25,
  range 1..3); pan re-centers via the existing [zoom, safeIndex] effect. Listener
  attached to a new ref on the #image-modal-image-container. Also moved the
  `if (!assets...) return null` guard below the hooks (it previously sat above all
  hooks, a latent Rules-of-Hooks violation) and normalized the assets prop to an
  array so the hooks are null-safe.

One wheel step per notch, centered (matches the buttons). Zoom-to-cursor is left
as a future enhancement since the two viewers use different transformOrigin and
pan models. ImageGalleryModal is intentionally not included here because its zoom
comes from the still-open PR #1596; it will get the same hook once that merges.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
JasonWildMe added a commit to Joey-Li0118/Wildbook that referenced this pull request Jun 20, 2026
Wire the shared useWheelZoom hook into the Gallery-View image modal so wheeling
zooms the image (matching the zoom-in/reset buttons: step 0.25, range 1..3; pan
re-centers via the existing [zoom, safeIndex] effect). The native non-passive
wheel listener is attached to the non-transformed #image-modal-image container.

The hook (frontend/src/hooks/useWheelZoom.js) is added here identical to the copy
in PR WildMeOrg#1628 (which applies it to AnnotationOverlay + ImageModal), so the gallery
viewer's wheel-zoom can be tested alongside the rest of this PR. Identical
same-path content merges cleanly with WildMeOrg#1628.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant