ARTICLE AD BOX
I'm building a drag-to-reorder interaction for horizontal card layout inside a JUCE plugin's WebBrowserComponent (backed by WKWebView on macOS). The card moves correctly during drag but the release event (pointerup, mouseup) never fires.
What works:
Knob drag: canvas.setPointerCapture(id) + canvas.addEventListener('pointermove', ...) + canvas.addEventListener('pointerup', ...) → fully functional
Slider drag: same pattern on a <div> → fully functional
What doesn't work:
Node card horizontal drag: identical pattern on a card <div> inside an overflow-x: auto scroll container → pointermove fires (card translates), but pointerup and mouseup never fire on either the element OR document function attachDragReorder(card, getNodeIdx) { const` handle = card.querySelector('.drag-handle');` let` dragging = false, fromIdx = -1, lastDropIdx = -1, capturedId = null, startX = 0;` const` finish = (commit) => {` if` (!dragging) return;` dragging `= false;` document.`removeEventListener('pointerup', onDocUp, { capture: true });` document.`removeEventListener('pointercancel', onDocCancel, { capture: true });` document.`removeEventListener('mouseup', onDocMouseUp);` try` { card.releasePointerCapture(capturedId); } catch (_) {}` // ... commit drop if commit=true }; const` onDocUp = (e) => { if (e.pointerId === capturedId) finish(true); };` const` onDocCancel = (e) => { if (e.pointerId === capturedId) finish(false); };` const` onDocMouseUp = () => finish(true);` handle.`addEventListener('pointerdown', (e) => {` if` (e.button !== 0) return;` capturedId `= e.pointerId;` card.`setPointerCapture(capturedId);` startX `= e.clientX;` dragging `= true;` card.classList.`add('dragging');` document.`addEventListener('pointerup', onDocUp, { capture: true });` document.`addEventListener('pointercancel', onDocCancel, { capture: true });` document.`addEventListener('mouseup', onDocMouseUp);` }); card.`addEventListener('pointermove', (e) => {` if` (!dragging) return;` card.style.transform `` = `translateX(${e.clientX - startX}px)`; `` // ... update drop indicator }); card.`addEventListener('pointerup', () => finish(true));` card.`addEventListener('pointercancel', () => finish(false));`}
CSS:
.node { touch-action: none; }.drag-handle { touch-action: none; cursor: grab; }
Observations:
Removing e.preventDefault() from pointerdown did not fix it
Moving setPointerCapture from the handle <span> to the card <div> fixed pointermove firing but not pointerup
Adding touch-action: none to the card did not fix pointerup
Document-level pointerup/mouseup listeners with { capture: true } do not fire either
The drag is INSIDE an overflow-x: auto scroll container — suspect this causes pointercancel to fire and consume the event before release
My hypothesis: WKWebView fires pointercancel when a pointer-captured element applies transform: translateX() and the pointer leaves the element's original visual bounds, which causes dragging to be reset to false before mouseup fires.
Question: Is there a known-working pattern for drag-to-reorder inside a WKWebView scroll container? Should I abandon pointer events entirely and use mousedown/mousemove/mouseup on document?
Environment: macOS, JUCE 8.x WebBrowserComponent (WKWebView), Safari WebKit 18.x
