- useHabitDrag: onDrop 经 ref 桥接实时闭包 —— bind/bindHandle 空依赖 useCallback 固化了首次渲染(groups=[] 数据未加载)的落位函数, 松手 静默早退, 顺序永远不变 - 把手路径挂非 passive touchstart preventDefault: 规范上禁止浏览器对 整个触摸的默认处理 (含滚动), 与 touch-action:none 双保险 - 把手触区放大 (36px 高), 降低按到行体触发不可靠长按路径的概率
This commit is contained in:
@@ -18,6 +18,12 @@ export default function useHabitDrag({ onDrop }) {
|
||||
const st = useRef({ timer: null, active: false, startX: 0, startY: 0, item: null, over: null })
|
||||
const lastEnd = useRef(0)
|
||||
|
||||
// onDrop 取"最新一次渲染"的闭包: bind/bindHandle 是空依赖 useCallback,
|
||||
// 若直接捕获 onDrop, 会固化首次渲染(数据未加载, groups=[])的落位函数,
|
||||
// 松手时静默早退 —— 顺序永远不变。用 ref 桥接到实时实现。
|
||||
const onDropRef = useRef(onDrop)
|
||||
onDropRef.current = onDrop
|
||||
|
||||
const cleanup = useCallback(() => {
|
||||
if (st.current.timer) { clearTimeout(st.current.timer); st.current.timer = null }
|
||||
st.current.active = false
|
||||
@@ -61,17 +67,21 @@ export default function useHabitDrag({ onDrop }) {
|
||||
const s = st.current
|
||||
if (s.active && s.item) {
|
||||
lastEnd.current = Date.now()
|
||||
if (s.over && s.over.id !== s.item.id) onDrop?.(s.item, s.over.id, s.over.after)
|
||||
if (s.over && s.over.id !== s.item.id) onDropRef.current?.(s.item, s.over.id, s.over.after)
|
||||
}
|
||||
cleanup()
|
||||
}
|
||||
|
||||
// 指针监听装配: instant=true 时按下即激活 (把手路径), 否则长按 400ms 激活
|
||||
// 指针监听装配: instant=true 时按下即激活 (把手路径), 否则长按 400ms 激活。
|
||||
// 把手路径额外挂非 passive touchstart preventDefault —— 规范上这会禁止
|
||||
// 浏览器对"整个触摸"的一切默认处理 (含滚动), 比 touch-action:none 更强,
|
||||
// 双保险防滚动抢占取消指针流。
|
||||
const armListeners = (ev, instant, item) => {
|
||||
try { ev.currentTarget.setPointerCapture(ev.pointerId) } catch { /* ignore */ }
|
||||
const el = ev.currentTarget
|
||||
const move = (e2) => onMove(e2)
|
||||
const tm = (e2) => { if (st.current.active) e2.preventDefault() }
|
||||
const ts = (e2) => e2.preventDefault()
|
||||
const up = () => { onUp(); detach() }
|
||||
const cancel = () => {
|
||||
if (st.current.active) lastEnd.current = Date.now()
|
||||
@@ -83,11 +93,13 @@ export default function useHabitDrag({ onDrop }) {
|
||||
el.removeEventListener('pointerup', up)
|
||||
el.removeEventListener('pointercancel', cancel)
|
||||
el.removeEventListener('touchmove', tm)
|
||||
el.removeEventListener('touchstart', ts)
|
||||
}
|
||||
el.addEventListener('pointermove', move)
|
||||
el.addEventListener('pointerup', up)
|
||||
el.addEventListener('pointercancel', cancel)
|
||||
el.addEventListener('touchmove', tm, { passive: false })
|
||||
if (instant) el.addEventListener('touchstart', ts, { passive: false })
|
||||
|
||||
if (instant) {
|
||||
st.current.active = true
|
||||
|
||||
@@ -59,12 +59,15 @@ export default function useRowDrag({ columns, onDrop }) {
|
||||
cleanup()
|
||||
}
|
||||
|
||||
// 指针监听装配 (capture + 非 passive touchmove: 激活后尽力拦截滚动接管)
|
||||
const armListeners = (ev) => {
|
||||
// 指针监听装配 (capture + 非 passive touchmove: 激活后尽力拦截滚动接管;
|
||||
// instant=true 把手路径再挂 touchstart preventDefault, 规范上禁止浏览器对
|
||||
// 整个触摸的默认处理, 彻底杜绝滚动抢占)
|
||||
const armListeners = (ev, instant = false) => {
|
||||
try { ev.currentTarget.setPointerCapture(ev.pointerId) } catch { /* ignore */ }
|
||||
const el = ev.currentTarget
|
||||
const move = (e2) => onMove(e2)
|
||||
const tm = (e2) => { if (st.current.active) e2.preventDefault() }
|
||||
const ts = (e2) => e2.preventDefault()
|
||||
const up = () => { onUp(); detach() }
|
||||
const cancel = () => {
|
||||
if (st.current.active) lastEnd.current = Date.now()
|
||||
@@ -76,11 +79,13 @@ export default function useRowDrag({ columns, onDrop }) {
|
||||
el.removeEventListener('pointerup', up)
|
||||
el.removeEventListener('pointercancel', cancel)
|
||||
el.removeEventListener('touchmove', tm)
|
||||
el.removeEventListener('touchstart', ts)
|
||||
}
|
||||
el.addEventListener('pointermove', move)
|
||||
el.addEventListener('pointerup', up)
|
||||
el.addEventListener('pointercancel', cancel)
|
||||
el.addEventListener('touchmove', tm, { passive: false })
|
||||
if (instant) el.addEventListener('touchstart', ts, { passive: false })
|
||||
}
|
||||
|
||||
/** 绑定到列表行: 行元素需设置 touch-action: pan-y (长按路径, 可滚场景不可靠) */
|
||||
@@ -115,7 +120,7 @@ export default function useRowDrag({ columns, onDrop }) {
|
||||
st.current.active = true
|
||||
if (navigator.vibrate) navigator.vibrate(15)
|
||||
setDrag({ task, fromIdx, toIdx: fromIdx })
|
||||
armListeners(ev)
|
||||
armListeners(ev, true)
|
||||
},
|
||||
onContextMenu: (ev) => ev.preventDefault(),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -682,7 +682,9 @@ body.me-safe-top .app-safe-top { height: 0; }
|
||||
指针流不会被滚动抢占取消, 是 Android WebView 上唯一可靠的拖动入口 */
|
||||
.grip {
|
||||
touch-action: none; -webkit-user-select: none; user-select: none;
|
||||
color: #c2c7cf; font-size: 14px; line-height: 1; padding: 8px 2px;
|
||||
color: #c2c7cf; font-size: 14px; line-height: 1;
|
||||
padding: 14px 6px; margin: -8px 0; min-width: 26px; min-height: 36px;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
cursor: grab; flex-shrink: 0; align-self: center;
|
||||
}
|
||||
.grip:active { color: var(--color-primary); }
|
||||
|
||||
Reference in New Issue
Block a user