Stop the hover magnifier sticking open after a drag.
`noMagnify` only suppressed rendering — each card went on tracking hover underneath it. During a drag the cursor outruns the lifted card (its transform lands a frame later), so the rows it passes take mouseenter, and React re-inserts those rows as the list reorders, so the matching mouseleave often never arrives. Every card left holding stale hover then popped its preview the moment the drag state cleared, at stale coordinates, and stayed there until that exact card was hovered and left again. noMagnify now means the magnifier is off, not hidden: the handlers come off and any tracked hover is dropped. Nothing can be left half-armed regardless of which boundary events the browser does or doesn't deliver. The drag also cleared its state 180ms after release — its own glide — while the neighbours it passed keep gliding for anim.ts's MOVE_MS of 240ms. A row sliding out from under a stationary cursor in that 60ms tail could re-arm the same stale hover, so the settle window now covers the whole tray coming to rest.
This commit is contained in:
@@ -15,6 +15,13 @@ interface Props {
|
||||
const DRAG_SLOP = 5
|
||||
// How long a finger must rest on a card before it picks it up.
|
||||
const HOLD_MS = 220
|
||||
// How long after release the drag counts as still settling. The dropped card's
|
||||
// own glide is 180ms (`.arrange-drag.is-dropping`), but the neighbours it passed
|
||||
// keep gliding for MOVE_MS (240ms, anim.ts) — and rows sliding out from under a
|
||||
// stationary cursor don't reliably fire `mouseleave`, which is how the hover
|
||||
// magnifier used to get stuck open after a drop. So hold the drag state, and
|
||||
// with it CardView's `noMagnify`, until every row has come to rest.
|
||||
const SETTLE_MS = 260
|
||||
|
||||
// A press that hasn't become a drag yet: which pointer, where it landed, and
|
||||
// (for touch) the pending hold timer that would lift the card.
|
||||
@@ -152,8 +159,8 @@ export function ArrangePhase({ view, you, send }: Props) {
|
||||
|
||||
function beginDrag(i: number, clientY: number) {
|
||||
clearPress()
|
||||
// Cancel a previous card's drop glide, so re-grabbing within its 180ms
|
||||
// doesn't have that timer tear down this drag's state mid-gesture.
|
||||
// Cancel a previous card's settle, so re-grabbing inside that window doesn't
|
||||
// have its timer tear down this drag's state mid-gesture.
|
||||
window.clearTimeout(dropTimer.current)
|
||||
dragIndex.current = i
|
||||
startIndex.current = i
|
||||
@@ -234,14 +241,15 @@ export function ArrangePhase({ view, you, send }: Props) {
|
||||
function endDrag() {
|
||||
if (dragIndex.current === null) return
|
||||
dragIndex.current = null
|
||||
// Glide the lifted card down into its resting slot, then clear drag state.
|
||||
// Glide the lifted card down into its resting slot, then — once the whole
|
||||
// tray has settled, not just this card — clear drag state.
|
||||
setDropping(true)
|
||||
setDragTranslate(0)
|
||||
dropTimer.current = window.setTimeout(() => {
|
||||
setDragId(null)
|
||||
setDropping(false)
|
||||
setDragging(false)
|
||||
}, 180)
|
||||
}, SETTLE_MS)
|
||||
}
|
||||
|
||||
// While a press is live, track the pointer on `window` so the gesture keeps
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useLayoutEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import type { Card } from '../types'
|
||||
import { artFor, artUrlFor } from '../petArt'
|
||||
@@ -222,8 +222,9 @@ interface Props {
|
||||
dead?: boolean
|
||||
// preview marks the floating magnified copy so it doesn't magnify itself.
|
||||
preview?: boolean
|
||||
// noMagnify suppresses the hover magnifier (e.g. while this card is being
|
||||
// dragged, so the frozen preview copy doesn't linger over the drag ghost).
|
||||
// noMagnify turns the hover magnifier off entirely — not just hidden but not
|
||||
// tracked, so nothing is left half-armed to spring open when it flips back
|
||||
// (e.g. while cards are being dragged and the rows slide under the cursor).
|
||||
noMagnify?: boolean
|
||||
}
|
||||
|
||||
@@ -245,6 +246,17 @@ export function CardView({
|
||||
// Hover magnifier: show a large floating copy beside the cursor. The
|
||||
// preview copy itself opts out so it can't recurse.
|
||||
const [hover, setHover] = useState<{ x: number; y: number } | null>(null)
|
||||
// When the magnifier is off, don't track hover at all — and forget anything
|
||||
// already tracked. Suppressing only the rendering isn't enough: a drag slides
|
||||
// rows around under the cursor, so the `mouseleave` that would normally clear
|
||||
// this frequently never reaches the card it belongs to (the cursor outruns the
|
||||
// lifted card, and React re-inserts the rows it passes). The stale hover then
|
||||
// sprang the preview open the instant the drag ended and stuck there until
|
||||
// that exact card was hovered and left again.
|
||||
const hoverOff = preview || !!noMagnify || !CAN_HOVER
|
||||
useEffect(() => {
|
||||
if (hoverOff) setHover(null)
|
||||
}, [hoverOff])
|
||||
const power = (card.power ?? 0) + bonus
|
||||
// Shrink long ability text to fit the (fixed-height) card, re-fitting when
|
||||
// the text or card size changes. Only the branch that renders attaches it.
|
||||
@@ -265,8 +277,10 @@ export function CardView({
|
||||
.join(' ')
|
||||
|
||||
// Place the magnified preview near the cursor, clamped into the viewport.
|
||||
// Checking `hoverOff` here as well as in the effect keeps the preview from
|
||||
// flashing for the one render between it turning on and the effect clearing.
|
||||
const previewEl =
|
||||
hover && !preview && !noMagnify ? (
|
||||
hover && !hoverOff ? (
|
||||
<CardMagnify card={card} x={hover.x} y={hover.y} bonus={bonus} damage={damage} dead={dead} />
|
||||
) : null
|
||||
|
||||
@@ -276,9 +290,9 @@ export function CardView({
|
||||
data-card-id={card.id || undefined}
|
||||
onClick={disabled ? undefined : onClick}
|
||||
role={onClick ? 'button' : undefined}
|
||||
onMouseEnter={preview || !CAN_HOVER ? undefined : (e) => setHover({ x: e.clientX, y: e.clientY })}
|
||||
onMouseMove={preview || !CAN_HOVER ? undefined : (e) => setHover({ x: e.clientX, y: e.clientY })}
|
||||
onMouseLeave={preview || !CAN_HOVER ? undefined : () => setHover(null)}
|
||||
onMouseEnter={hoverOff ? undefined : (e) => setHover({ x: e.clientX, y: e.clientY })}
|
||||
onMouseMove={hoverOff ? undefined : (e) => setHover({ x: e.clientX, y: e.clientY })}
|
||||
onMouseLeave={hoverOff ? undefined : () => setHover(null)}
|
||||
>
|
||||
{previewEl}
|
||||
{/* Upper illustration: a stylised landscape with the pet as a sticker and
|
||||
|
||||
Reference in New Issue
Block a user