Fix bug in drag and drop.
This commit is contained in:
@@ -33,6 +33,9 @@ export function ArrangePhase({ view, you, send }: Props) {
|
|||||||
// One entry per card row, in current play order, so a pointer drag can find
|
// One entry per card row, in current play order, so a pointer drag can find
|
||||||
// which slot the finger/cursor is currently over by hit-testing rects.
|
// which slot the finger/cursor is currently over by hit-testing rects.
|
||||||
const cardRefs = useRef<(HTMLDivElement | null)[]>([])
|
const cardRefs = useRef<(HTMLDivElement | null)[]>([])
|
||||||
|
// Holds the latest `onDragMove` so the window listener (attached once per
|
||||||
|
// drag) always calls the fresh closure — see the drag effect below.
|
||||||
|
const moveRef = useRef<(clientY: number) => void>(() => {})
|
||||||
const locked = you.ready
|
const locked = you.ready
|
||||||
|
|
||||||
// The arrow buttons reorder `order` and the cards glide to their new slot.
|
// The arrow buttons reorder `order` and the cards glide to their new slot.
|
||||||
@@ -65,10 +68,16 @@ export function ArrangePhase({ view, you, send }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Drag-to-reorder via pointer events, driven by the grip handle. Native HTML5
|
// Drag-to-reorder via pointer events, driven by the grip handle. Native HTML5
|
||||||
// drag doesn't fire on touch, so we use pointer events (mouse + touch alike)
|
// drag doesn't fire on touch, so we use pointer events (mouse + touch alike).
|
||||||
// and capture the pointer on the handle so the drag keeps tracking even when
|
// `touch-action: none` on the handle stops the browser from scrolling the
|
||||||
// the finger/cursor leaves the handle. `touch-action: none` on the handle
|
// page mid-drag.
|
||||||
// stops the browser from scrolling the page mid-drag.
|
//
|
||||||
|
// We do NOT rely on `setPointerCapture` here: the captured handle lives inside
|
||||||
|
// the keyed row that reorders mid-drag, and React moves that DOM node
|
||||||
|
// (`insertBefore`) as the list changes — which makes browsers drop the active
|
||||||
|
// pointer capture, freezing the drag until the user re-grabs. Instead we
|
||||||
|
// listen on `window` for the drag's lifetime (see the effect below), so
|
||||||
|
// reordering the rows can never interrupt the gesture.
|
||||||
//
|
//
|
||||||
// The lifted card follows the finger while the list reorders live beneath it.
|
// The lifted card follows the finger while the list reorders live beneath it.
|
||||||
// The translate is applied to an inner wrapper, not the `.arrange-card` box
|
// The translate is applied to an inner wrapper, not the `.arrange-card` box
|
||||||
@@ -86,10 +95,9 @@ export function ArrangePhase({ view, you, send }: Props) {
|
|||||||
setDragTranslate(0)
|
setDragTranslate(0)
|
||||||
setDropping(false)
|
setDropping(false)
|
||||||
setDragging(true)
|
setDragging(true)
|
||||||
e.currentTarget.setPointerCapture(e.pointerId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragMove(e: React.PointerEvent<HTMLElement>) {
|
function onDragMove(clientY: number) {
|
||||||
if (dragIndex.current === null) return
|
if (dragIndex.current === null) return
|
||||||
// Find the slot the pointer has crossed into by hit-testing the *other*
|
// Find the slot the pointer has crossed into by hit-testing the *other*
|
||||||
// rows' midpoints (the lifted row's own box stays in its natural slot).
|
// rows' midpoints (the lifted row's own box stays in its natural slot).
|
||||||
@@ -100,8 +108,8 @@ export function ArrangePhase({ view, you, send }: Props) {
|
|||||||
if (!el) continue
|
if (!el) continue
|
||||||
const r = el.getBoundingClientRect()
|
const r = el.getBoundingClientRect()
|
||||||
const mid = r.top + r.height / 2
|
const mid = r.top + r.height / 2
|
||||||
if (j < dragIndex.current && e.clientY < mid) target = Math.min(target, j)
|
if (j < dragIndex.current && clientY < mid) target = Math.min(target, j)
|
||||||
else if (j > dragIndex.current && e.clientY > mid) target = Math.max(target, j)
|
else if (j > dragIndex.current && clientY > mid) target = Math.max(target, j)
|
||||||
}
|
}
|
||||||
if (target !== dragIndex.current) {
|
if (target !== dragIndex.current) {
|
||||||
move(dragIndex.current, target)
|
move(dragIndex.current, target)
|
||||||
@@ -110,8 +118,11 @@ export function ArrangePhase({ view, you, send }: Props) {
|
|||||||
// Follow the finger from where the drag began, minus how far the card's own
|
// Follow the finger from where the drag began, minus how far the card's own
|
||||||
// slot has since shifted — keeping it pinned under the finger.
|
// slot has since shifted — keeping it pinned under the finger.
|
||||||
const shift = (dragIndex.current - startIndex.current) * stride.current
|
const shift = (dragIndex.current - startIndex.current) * stride.current
|
||||||
setDragTranslate(e.clientY - grabY.current - shift)
|
setDragTranslate(clientY - grabY.current - shift)
|
||||||
}
|
}
|
||||||
|
// Keep the window listener pointed at the current-render closure (fresh
|
||||||
|
// `order`/refs) without re-attaching the listener on every reorder.
|
||||||
|
moveRef.current = onDragMove
|
||||||
|
|
||||||
function endDrag() {
|
function endDrag() {
|
||||||
if (dragIndex.current === null) return
|
if (dragIndex.current === null) return
|
||||||
@@ -126,6 +137,25 @@ export function ArrangePhase({ view, you, send }: Props) {
|
|||||||
}, 180)
|
}, 180)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// While a drag is active, track the pointer on `window` so the gesture keeps
|
||||||
|
// running even as the list reorders under the finger (the handle's own
|
||||||
|
// pointer capture would be lost when React moves its row). Attached once per
|
||||||
|
// drag; `moveRef` keeps it calling the latest closure.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dragging) return
|
||||||
|
const onMove = (e: PointerEvent) => moveRef.current(e.clientY)
|
||||||
|
const onUp = () => endDrag()
|
||||||
|
window.addEventListener('pointermove', onMove)
|
||||||
|
window.addEventListener('pointerup', onUp)
|
||||||
|
window.addEventListener('pointercancel', onUp)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('pointermove', onMove)
|
||||||
|
window.removeEventListener('pointerup', onUp)
|
||||||
|
window.removeEventListener('pointercancel', onUp)
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [dragging])
|
||||||
|
|
||||||
// Which pets do the foods land on? Foods buff the next pet later in play order
|
// Which pets do the foods land on? Foods buff the next pet later in play order
|
||||||
// (i.e. the next pet below them in the list). Compute buff per card for preview.
|
// (i.e. the next pet below them in the list). Compute buff per card for preview.
|
||||||
const bonuses = new Map<string, number>()
|
const bonuses = new Map<string, number>()
|
||||||
@@ -196,9 +226,6 @@ export function ArrangePhase({ view, you, send }: Props) {
|
|||||||
aria-label="drag to reorder"
|
aria-label="drag to reorder"
|
||||||
title="Drag to reorder"
|
title="Drag to reorder"
|
||||||
onPointerDown={(e) => startDrag(e, i)}
|
onPointerDown={(e) => startDrag(e, i)}
|
||||||
onPointerMove={onDragMove}
|
|
||||||
onPointerUp={endDrag}
|
|
||||||
onPointerCancel={endDrag}
|
|
||||||
>
|
>
|
||||||
⠿
|
⠿
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user