Skip to content

Commit f6d9822

Browse files
jeremyfeltclaude
andcommitted
Fix erratic mouse behavior in the swipe (slider) compare view
The before/after slider had two classic pointer bugs: - The screenshots are normal <img> elements, draggable by default, and pointerdown never called preventDefault. Mousedown-then-move started a native image drag instead of moving the divider. - That native drag fires pointercancel, not pointerup, and only pointerup was handled — so `dragging` never reset and the divider then tracked the cursor on plain hover with no button held. Apply the standard guards used by image-comparison sliders: - preventDefault on pointerdown; respond to the primary button only. - End the drag on pointerup AND pointercancel/lostpointercapture, and release the pointer capture, so an interrupted gesture can't get stuck. - Make the images non-draggable and non-interactive (draggable="false", pointer-events:none, user-select/-webkit-user-drag:none) so the stage is always the pointer target and can't be hijacked by a native image drag. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 411681d commit f6d9822

2 files changed

Lines changed: 38 additions & 4 deletions

File tree

templates/assets/app.js

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,7 +1133,13 @@
11331133
function buildSwipe(stage, img) {
11341134
var pos = 0.5;
11351135
var layer = h('div', { class: 'layer' }, [
1136-
h('img', { src: img.control, alt: 'Baseline screenshot' }),
1136+
h('img', {
1137+
src: img.control,
1138+
alt: 'Baseline screenshot',
1139+
// String 'false', not boolean: the h() helper drops false-valued
1140+
// props, and draggable is an enumerated attribute anyway.
1141+
draggable: 'false',
1142+
}),
11371143
]);
11381144
var divider = h(
11391145
'div',
@@ -1158,7 +1164,11 @@
11581164
};
11591165
var dragging = false;
11601166
var shot = h('div', { class: 'shot swipe-stage' }, [
1161-
h('img', { src: img.capture, alt: 'Current screenshot' }),
1167+
h('img', {
1168+
src: img.capture,
1169+
alt: 'Current screenshot',
1170+
draggable: 'false',
1171+
}),
11621172
layer,
11631173
divider,
11641174
h('span', { class: 'taglabel l', text: 'baseline' }),
@@ -1168,7 +1178,23 @@
11681178
var rect = shot.getBoundingClientRect();
11691179
return clamp01((e.clientX - rect.left) / rect.width);
11701180
};
1181+
var endDrag = function (e) {
1182+
dragging = false;
1183+
// Release capture so the stage stops receiving pointer events once
1184+
// the gesture is over.
1185+
if (e && shot.hasPointerCapture(e.pointerId)) {
1186+
shot.releasePointerCapture(e.pointerId);
1187+
}
1188+
};
11711189
shot.addEventListener('pointerdown', function (e) {
1190+
// Primary button / touch / pen only — ignore right- and middle-click.
1191+
if (e.button !== 0) {
1192+
return;
1193+
}
1194+
// Suppress the browser's native image drag and text selection, which
1195+
// would otherwise hijack the gesture and fire pointercancel instead
1196+
// of pointerup, leaving the divider stuck to the cursor.
1197+
e.preventDefault();
11721198
dragging = true;
11731199
shot.setPointerCapture(e.pointerId);
11741200
setPos(posFromEvent(e));
@@ -1178,7 +1204,11 @@
11781204
setPos(posFromEvent(e));
11791205
}
11801206
});
1181-
shot.addEventListener('pointerup', function () {
1207+
// End on up and on cancel/lost-capture, so an interrupted gesture can
1208+
// never leave dragging stuck on.
1209+
shot.addEventListener('pointerup', endDrag);
1210+
shot.addEventListener('pointercancel', endDrag);
1211+
shot.addEventListener('lostpointercapture', function () {
11821212
dragging = false;
11831213
});
11841214
stage.appendChild(shot);

templates/assets/reglance.css

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,11 @@ kbd {
379379
.taglabel.l { left: 10px; }
380380
.taglabel.r { right: 10px; }
381381

382-
.swipe-stage { cursor: col-resize; touch-action: none; }
382+
.swipe-stage { cursor: col-resize; touch-action: none; user-select: none; }
383+
/* The images are never the pointer target: the stage handles the whole
384+
gesture, and a non-draggable, non-selectable image can't start a native
385+
drag that would hijack it. */
386+
.swipe-stage img { pointer-events: none; -webkit-user-drag: none; user-select: none; }
383387
.divider { position: absolute; top: 0; bottom: 0; width: 0; z-index: 5; pointer-events: none; }
384388
.divider::before {
385389
content: '';

0 commit comments

Comments
 (0)