fix(mobile): full-screen media viewers + touch-pan for zoomed images (M3)
Mobile-audit batch 3. All changes mobile-gated (@media <=750px) so desktop is unchanged. - ModalWide: fill the phone screen (100vw/100vh, no radius) at <=750px instead of floating as an 85vw card. This also full-screens the file/PDF viewer and the avatar-crop editor on mobile (they share ModalWide) — intended. - UserHero avatar viewer: new mobile-only ModalMobileFull class (no desktop effect) so it goes edge-to-edge on phones like the timeline lightbox. - usePan: add touch support (single-finger drag, cleaned up on touchend/cancel/unmount) alongside the unchanged mouse path, so a zoomed image can be panned on a phone. Wired into ImageViewer and the MediaGallery lightbox. Two review passes: mouse path byte-for-byte unchanged; desktop provably unaffected; touch is gated to zoom!=1 so a non-zoomed image never hijacks swipe/scroll. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+46
-1
@@ -1,4 +1,4 @@
|
||||
import { MouseEventHandler, useEffect, useRef, useState } from 'react';
|
||||
import { MouseEventHandler, TouchEventHandler, useEffect, useRef, useState } from 'react';
|
||||
|
||||
export type Pan = {
|
||||
translateX: number;
|
||||
@@ -21,6 +21,10 @@ export const usePan = (active: boolean) => {
|
||||
const attachedRef = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>(
|
||||
null,
|
||||
);
|
||||
const touchAttachedRef = useRef<{
|
||||
move: (e: TouchEvent) => void;
|
||||
end: (e: TouchEvent) => void;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setCursor(active ? 'grab' : 'initial');
|
||||
@@ -53,6 +57,40 @@ export const usePan = (active: boolean) => {
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
// Touch equivalent so a zoomed image can be dragged on a phone. Single-finger
|
||||
// only (ignore multi-touch / pinch); touch events carry no movementX/Y, so we
|
||||
// derive the delta from the previous touch position.
|
||||
const handleTouchStart: TouchEventHandler<HTMLElement> = (evt) => {
|
||||
if (!active || evt.touches.length !== 1) return;
|
||||
setCursor('grabbing');
|
||||
let lastX = evt.touches[0].clientX;
|
||||
let lastY = evt.touches[0].clientY;
|
||||
|
||||
const handleTouchMove = (e: TouchEvent) => {
|
||||
if (e.touches.length !== 1) return;
|
||||
e.preventDefault();
|
||||
const t = e.touches[0];
|
||||
const dx = t.clientX - lastX;
|
||||
const dy = t.clientY - lastY;
|
||||
lastX = t.clientX;
|
||||
lastY = t.clientY;
|
||||
setPan((p) => ({ translateX: p.translateX + dx, translateY: p.translateY + dy }));
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
setCursor('grab');
|
||||
document.removeEventListener('touchmove', handleTouchMove);
|
||||
document.removeEventListener('touchend', handleTouchEnd);
|
||||
document.removeEventListener('touchcancel', handleTouchEnd);
|
||||
touchAttachedRef.current = null;
|
||||
};
|
||||
|
||||
touchAttachedRef.current = { move: handleTouchMove, end: handleTouchEnd };
|
||||
document.addEventListener('touchmove', handleTouchMove, { passive: false });
|
||||
document.addEventListener('touchend', handleTouchEnd);
|
||||
document.addEventListener('touchcancel', handleTouchEnd);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) setPan(INITIAL_PAN);
|
||||
}, [active]);
|
||||
@@ -65,6 +103,12 @@ export const usePan = (active: boolean) => {
|
||||
document.removeEventListener('mouseup', attachedRef.current.up);
|
||||
attachedRef.current = null;
|
||||
}
|
||||
if (touchAttachedRef.current) {
|
||||
document.removeEventListener('touchmove', touchAttachedRef.current.move);
|
||||
document.removeEventListener('touchend', touchAttachedRef.current.end);
|
||||
document.removeEventListener('touchcancel', touchAttachedRef.current.end);
|
||||
touchAttachedRef.current = null;
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
@@ -73,5 +117,6 @@ export const usePan = (active: boolean) => {
|
||||
pan,
|
||||
cursor,
|
||||
onMouseDown: handleMouseDown,
|
||||
onTouchStart: handleTouchStart,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user