Skip to content

Commit fc6ffa3

Browse files
committed
Consolidate event handling to use the pointer API
1 parent dcc825c commit fc6ffa3

File tree

2 files changed

+134
-129
lines changed

2 files changed

+134
-129
lines changed

ui/v2.5/src/hooks/Lightbox/Lightbox.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,14 +139,15 @@ const LightboxCarousel = forwardRef(function (
139139
}
140140

141141
function handleReleaseCarousel(
142-
event: React.TouchEvent,
143-
swipeDuration: number
142+
event: React.PointerEvent,
143+
swipeDuration: number,
144+
cancelled: boolean
144145
) {
145146
const cappedDuration = Math.max(50, Math.min(500, swipeDuration)) / 1000;
146147
const adjustedShift = carouselShift / (2 * cappedDuration);
147-
if (adjustedShift < -window.innerWidth / 2) {
148+
if (!cancelled && adjustedShift < -window.innerWidth / 2) {
148149
handleRight();
149-
} else if (adjustedShift > window.innerWidth / 2) {
150+
} else if (!cancelled && adjustedShift > window.innerWidth / 2) {
150151
handleLeft();
151152
}
152153
setCarouselShift(0);

ui/v2.5/src/hooks/Lightbox/LightboxImage.tsx

Lines changed: 129 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,11 @@ interface IProps {
103103
onLeft: () => void;
104104
onRight: () => void;
105105
moveCarousel: (v: number) => void;
106-
releaseCarousel: (ev: React.TouchEvent, swipeDuration: number) => void;
106+
releaseCarousel: (
107+
ev: React.PointerEvent,
108+
swipeDuration: number,
109+
cancelled: boolean
110+
) => void;
107111
isVideo: boolean;
108112
}
109113

@@ -130,7 +134,6 @@ export const LightboxImage: React.FC<IProps> = ({
130134
isVideo,
131135
}) => {
132136
const [defaultZoom, setDefaultZoom] = useState<number | null>(null);
133-
const [moving, setMoving] = useState(false);
134137
const [positionX, setPositionX] = useState(0);
135138
const [positionY, setPositionY] = useState(0);
136139
const [imageWidth, setImageWidth] = useState(width);
@@ -139,10 +142,12 @@ export const LightboxImage: React.FC<IProps> = ({
139142
const [containerRef, { width: boxWidth, height: boxHeight }] =
140143
useContainerDimensions();
141144

142-
const mouseDownEvent = useRef<MouseEvent>();
143145
const resetPositionRef = useRef(resetPosition);
144146

145-
const startPoints = useRef<number[]>([0, 0]);
147+
// Panning and swipe navigation are tracked in startPoint. Pinch zoom is
148+
// tracked in prevDiff. They are undefined if no action of that type is in
149+
// progress.
150+
const startPoint = useRef<number[] | undefined>();
146151
const startTime = useRef<number>(0);
147152
const pointerCache = useRef<React.PointerEvent[]>([]);
148153
const prevDiff = useRef<number | undefined>();
@@ -450,135 +455,81 @@ export const LightboxImage: React.FC<IProps> = ({
450455
debouncedScrollReset();
451456
}
452457

453-
function onImageMouseOver(ev: React.MouseEvent) {
454-
if (!moving) return;
455-
456-
if (!ev.buttons) {
457-
setMoving(false);
458-
return;
459-
}
460-
461-
const deltaX = ev.pageX - startPoints.current[0];
462-
const deltaY = ev.pageY - startPoints.current[1];
463-
startPoints.current = [ev.pageX, ev.pageY];
464-
465-
const newPositionX = Math.max(
466-
panBounds.minX,
467-
Math.min(panBounds.maxX, positionX + deltaX)
468-
);
469-
const newPositionY = Math.max(
470-
panBounds.minY,
471-
Math.min(panBounds.maxY, positionY + deltaY)
458+
function onPointerDown(ev: React.PointerEvent) {
459+
// replace pointer event with the same id, if applicable
460+
pointerCache.current = pointerCache.current.filter(
461+
(e) => e.pointerId !== ev.pointerId
472462
);
473463

474-
setPositionX(newPositionX);
475-
setPositionY(newPositionY);
476-
}
464+
pointerCache.current.push(ev);
465+
prevDiff.current = undefined;
477466

478-
function onImageMouseDown(ev: React.MouseEvent) {
479-
startPoints.current = [ev.pageX, ev.pageY];
480467
startTime.current = ev.timeStamp;
481-
setMoving(true);
482-
483-
mouseDownEvent.current = ev.nativeEvent;
484-
}
485-
486-
function onImageMouseUp(ev: React.MouseEvent) {
487-
if (ev.button !== 0) return;
488-
489-
if (
490-
!mouseDownEvent.current ||
491-
ev.timeStamp - mouseDownEvent.current.timeStamp > 200
468+
if (pointerCache.current.length === 1) {
469+
startPoint.current = [ev.clientX, ev.clientY];
470+
} else if (
471+
pointerCache.current.length === 2 &&
472+
startPoint.current !== undefined
492473
) {
493-
// not a click - ignore
494-
return;
474+
const centerX = Math.abs(ev.clientX + startPoint.current[0]) / 2;
475+
const centerY = Math.abs(ev.clientY + startPoint.current[1]) / 2;
476+
startPoint.current = [centerX, centerY];
495477
}
478+
}
496479

497-
// must be a click
498-
if (
499-
ev.pageX !== startPoints.current[0] ||
500-
ev.pageY !== startPoints.current[1]
501-
) {
502-
return;
503-
}
480+
function onPointerUp(ev: React.PointerEvent) {
481+
let found = false;
504482

505-
if (ev.nativeEvent.offsetX >= (ev.target as HTMLElement).offsetWidth / 2) {
506-
onRight();
507-
} else {
508-
onLeft();
483+
for (let i = 0; i < pointerCache.current.length; i++) {
484+
if (pointerCache.current[i].pointerId === ev.pointerId) {
485+
pointerCache.current.splice(i, 1);
486+
found = true;
487+
break;
488+
}
509489
}
510-
}
511490

512-
const onTouchStart = useCallback(
513-
(ev: TouchEvent) => {
514-
ev.preventDefault();
515-
if (ev.touches.length === 1) {
516-
startPoints.current = [ev.touches[0].pageX, ev.touches[0].pageY];
517-
startTime.current = ev.timeStamp;
518-
setMoving(true);
491+
if (!found || pointerCache.current.length !== 0) {
492+
if (pointerCache.current.length === 1) {
493+
// If we are transitioning from pinch zoom to pan, reset this
494+
// so we don't pan relative to the old center point.
495+
startPoint.current = [
496+
pointerCache.current[0].clientX,
497+
pointerCache.current[0].clientY,
498+
];
519499
}
520-
},
521-
[startPoints, startTime, setMoving]
522-
);
523-
524-
useEffect(() => {
525-
const container = containerRef.current;
526-
if (!container) {
527500
return;
528501
}
529-
container.addEventListener("touchstart", onTouchStart);
530-
return () => {
531-
container.removeEventListener("touchstart", onTouchStart);
532-
};
533-
}, [containerRef, onTouchStart]);
534-
535-
function onTouchMove(ev: React.TouchEvent) {
536-
if (!moving) return;
537502

538-
if (ev.touches.length === 1) {
539-
const deltaX = ev.touches[0].pageX - startPoints.current[0];
540-
const deltaY = ev.touches[0].pageY - startPoints.current[1];
541-
startPoints.current = [ev.touches[0].pageX, ev.touches[0].pageY];
503+
if (ev.pointerType === "touch" && startPoint.current !== null) {
504+
// Swipe navigation
505+
releaseCarousel(ev, ev.timeStamp - startTime.current, false);
506+
}
542507

543-
if (panBounds.minX != panBounds.maxX) {
544-
const newPositionX = Math.max(
545-
panBounds.minX,
546-
Math.min(panBounds.maxX, positionX + deltaX)
547-
);
548-
const newPositionY = Math.max(
549-
panBounds.minY,
550-
Math.min(panBounds.maxY, positionY + deltaY)
551-
);
508+
if (
509+
ev.button === 0 &&
510+
ev.timeStamp - startTime.current <= 200 &&
511+
startPoint.current !== undefined &&
512+
ev.clientX === startPoint.current[0] &&
513+
ev.clientY === startPoint.current[1]
514+
) {
515+
// Click or tap navigation
552516

553-
setPositionX(newPositionX);
554-
setPositionY(newPositionY);
517+
if (ev.clientX >= window.innerWidth / 2) {
518+
onRight();
555519
} else {
556-
moveCarousel(deltaX);
520+
onLeft();
557521
}
558522
}
559523
}
560524

561-
function onTouchEnd(ev: React.TouchEvent) {
562-
if (ev.changedTouches.length === 1 && ev.touches.length === 0) {
563-
releaseCarousel(ev, ev.timeStamp - startTime.current);
564-
}
565-
}
566-
567-
function onPointerDown(ev: React.PointerEvent) {
568-
// replace pointer event with the same id, if applicable
569-
pointerCache.current = pointerCache.current.filter(
570-
(e) => e.pointerId !== ev.pointerId
571-
);
572-
573-
pointerCache.current.push(ev);
574-
prevDiff.current = undefined;
575-
}
576-
577-
function onPointerUp(ev: React.PointerEvent) {
525+
function onPointerCancel(ev: React.PointerEvent) {
578526
for (let i = 0; i < pointerCache.current.length; i++) {
579527
if (pointerCache.current[i].pointerId === ev.pointerId) {
580528
pointerCache.current.splice(i, 1);
581-
break;
529+
if (ev.pointerType === "touch" && pointerCache.current.length === 0) {
530+
releaseCarousel(ev, ev.timeStamp - startTime.current, true);
531+
}
532+
return;
582533
}
583534
}
584535
}
@@ -588,19 +539,25 @@ export const LightboxImage: React.FC<IProps> = ({
588539
const cachedIndex = pointerCache.current.findIndex(
589540
(c) => c.pointerId === ev.pointerId
590541
);
591-
if (cachedIndex !== -1) {
592-
pointerCache.current[cachedIndex] = ev;
593-
}
594542

595-
if (defaultZoom === null) return;
543+
if (cachedIndex === -1 || defaultZoom === null) return;
596544

597-
// compare the difference between the two pointers
598-
if (pointerCache.current.length === 2) {
545+
pointerCache.current[cachedIndex] = ev;
546+
547+
if (pointerCache.current.length === 2 && startPoint.current !== undefined) {
548+
// Pinch zoom
549+
550+
// compare the difference between the two pointers
599551
const ev1 = pointerCache.current[0];
600552
const ev2 = pointerCache.current[1];
601553
const diffX = Math.abs(ev1.clientX - ev2.clientX);
602554
const diffY = Math.abs(ev1.clientY - ev2.clientY);
603555
const diff = Math.sqrt(diffX ** 2 + diffY ** 2);
556+
const centerX = Math.abs(ev1.clientX + ev2.clientX) / 2;
557+
const centerY = Math.abs(ev1.clientY + ev2.clientY) / 2;
558+
const deltaX = centerX - startPoint.current[0];
559+
const deltaY = centerY - startPoint.current[1];
560+
startPoint.current = [centerX, centerY];
604561

605562
if (prevDiff.current !== undefined) {
606563
const diffDiff = diff - prevDiff.current;
@@ -609,11 +566,62 @@ export const LightboxImage: React.FC<IProps> = ({
609566
let newZoom = diffDiff > 0 ? zoom * factor : zoom / factor;
610567
setZoom(newZoom);
611568
const bounds = calcPanBounds(defaultZoom * newZoom);
612-
setPositionX(Math.max(bounds.minX, Math.min(bounds.maxX, positionX)));
613-
setPositionY(Math.max(bounds.minY, Math.min(bounds.maxY, positionY)));
569+
570+
const newPositionX = Math.max(
571+
bounds.minX,
572+
Math.min(
573+
bounds.maxX,
574+
(diffDiff > 0 ? positionX * factor : positionX / factor) + deltaX
575+
)
576+
);
577+
const newPositionY = Math.max(
578+
bounds.minY,
579+
Math.min(
580+
bounds.maxY,
581+
(diffDiff > 0 ? positionY * factor : positionY / factor) + deltaY
582+
)
583+
);
584+
585+
setPositionX(newPositionX);
586+
setPositionY(newPositionY);
614587
}
615588

616589
prevDiff.current = diff;
590+
} else if (
591+
pointerCache.current.length === 1 &&
592+
startPoint.current !== undefined &&
593+
pointerCache.current[0].pointerType === "touch" &&
594+
panBounds.minX === panBounds.maxX
595+
) {
596+
// Swipe navigation (touch only, and only when panning is not possible)
597+
const deltaX = ev.clientX - startPoint.current[0];
598+
startPoint.current = [ev.clientX, ev.clientY];
599+
moveCarousel(deltaX);
600+
} else if (
601+
pointerCache.current.length === 1 &&
602+
startPoint.current !== undefined
603+
) {
604+
// Panning
605+
606+
if (!ev.buttons) {
607+
return;
608+
}
609+
610+
const deltaX = ev.clientX - startPoint.current[0];
611+
const deltaY = ev.clientY - startPoint.current[1];
612+
startPoint.current = [ev.clientX, ev.clientY];
613+
614+
const newPositionX = Math.max(
615+
panBounds.minX,
616+
Math.min(panBounds.maxX, positionX + deltaX)
617+
);
618+
const newPositionY = Math.max(
619+
panBounds.minY,
620+
Math.min(panBounds.maxY, positionY + deltaY)
621+
);
622+
623+
setPositionX(newPositionX);
624+
setPositionY(newPositionY);
617625
}
618626
}
619627

@@ -627,8 +635,10 @@ export const LightboxImage: React.FC<IProps> = ({
627635
})}
628636
style={{ touchAction: "none" }}
629637
onWheel={(e) => onContainerScroll(e)}
630-
onTouchMove={onTouchMove}
631-
onTouchEnd={onTouchEnd}
638+
onPointerDown={onPointerDown}
639+
onPointerMove={onPointerMove}
640+
onPointerUp={onPointerUp}
641+
onPointerCancel={onPointerCancel}
632642
>
633643
{defaultZoom ? (
634644
<picture>
@@ -648,12 +658,6 @@ export const LightboxImage: React.FC<IProps> = ({
648658
alt=""
649659
draggable={false}
650660
onWheel={current ? (e) => onImageScroll(e) : undefined}
651-
onMouseDown={onImageMouseDown}
652-
onMouseUp={onImageMouseUp}
653-
onMouseMove={onImageMouseOver}
654-
onPointerDown={onPointerDown}
655-
onPointerUp={onPointerUp}
656-
onPointerMove={onPointerMove}
657661
/>
658662
</picture>
659663
) : undefined}

0 commit comments

Comments
 (0)