fix(embed): scroll freeze final layer — Chrome fires a queued pointerenter after pointercancel when dragging outside, permanently trapping hover pause
Sequence: drag past iframe edge -> pointercancel (clears press state) -> Chrome still delivers one pre-queued pointerenter (re-sets hover) -> no further boundary events ever reach the iframe -> hover pause stuck forever. Fix: ignore pointerenter within 500ms after a pointercancel (and while pressed); re-set hover on genuine in-frame release so post-click hover behavior is preserved. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -155,10 +155,6 @@ function simulateBurst(photoId: string, delta: number): void {
|
||||
}
|
||||
|
||||
defineExpose({ simulateBurst });
|
||||
// 临时探针(卡死排查,随后移除)
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as unknown as Record<string, unknown>).__embedDebug = { reasons: pauseReasons, userPaused };
|
||||
}
|
||||
|
||||
// 收起/隐藏/换绑/减少动效 → 清粒子(§5.3)。
|
||||
watch(activeRef, (value) => {
|
||||
@@ -205,6 +201,8 @@ let pointerMoved = 0;
|
||||
let lockedTarget: string | null = null;
|
||||
/** 拖动抑制一次 click。 */
|
||||
let suppressClick = false;
|
||||
/** 最近 pointercancel 时刻(enter 噪音过滤)。 */
|
||||
let lastPointerCancelTs = 0;
|
||||
/** 按住看门狗(滚动卡死修复):up 丢失(窗外释放)时 2s 强制恢复滚动。 */
|
||||
const POINTER_WATCHDOG_MS = 2000;
|
||||
let pointerWatchdog: ReturnType<typeof setTimeout> | null = null;
|
||||
@@ -273,19 +271,32 @@ function onPointerUp(event: PointerEvent): void {
|
||||
// 滚动停死到刷新;滚轮/键盘路径各有 500ms 定时清除,拖动漏了)。
|
||||
setReason('manual', false);
|
||||
setReason('pointer', false);
|
||||
// 窗外结束时(拖出窗外 Chrome 派发 pointercancel 而非 pointerleave——leave
|
||||
// 不会再来)hover 原因同样会永久残留,这里按坐标判定一并清除;窗内正常
|
||||
// 释放不清(点击后悬停继续暂停是合理行为)。
|
||||
const rect = viewportEl.value?.getBoundingClientRect();
|
||||
if (
|
||||
rect !== undefined &&
|
||||
(event.clientX < rect.left ||
|
||||
event.clientX > rect.right ||
|
||||
event.clientY < rect.top ||
|
||||
event.clientY > rect.bottom)
|
||||
) {
|
||||
// 窗外结束时 hover 原因同样会永久残留(拖出窗外 Chrome 派发 pointercancel
|
||||
// 而非 pointerleave;其合成坐标恒为 0,0——恰落在铺满 iframe 的 viewport
|
||||
// rect 内,坐标判定不可用)。pointercancel 本身即手势被打断(拖出/系统
|
||||
// 抢占),鼠标大概率已不在窗上:无条件清 hover。真实 pointerup 坐标可信,
|
||||
// 窗内释放不清(点击后悬停继续暂停是合理行为)。
|
||||
if (event.type === 'pointercancel') {
|
||||
lastPointerCancelTs = Date.now();
|
||||
hoverPointerIsMouse = false;
|
||||
setReason('hover', false);
|
||||
} else {
|
||||
const rect = viewportEl.value?.getBoundingClientRect();
|
||||
const outside =
|
||||
rect !== undefined &&
|
||||
(event.clientX < rect.left ||
|
||||
event.clientX > rect.right ||
|
||||
event.clientY < rect.top ||
|
||||
event.clientY > rect.bottom);
|
||||
if (outside) {
|
||||
hoverPointerIsMouse = false;
|
||||
setReason('hover', false);
|
||||
} else if (event.pointerType === 'mouse') {
|
||||
// 窗内真实释放:按住期间的 enter 被忽略后在此补设悬停(释放后鼠标
|
||||
// 仍在窗上 → 悬停暂停继续成立,与直接移入行为一致)。
|
||||
hoverPointerIsMouse = true;
|
||||
setReason('hover', true);
|
||||
}
|
||||
}
|
||||
scheduleResume();
|
||||
}
|
||||
@@ -351,6 +362,13 @@ function onVisibilityChange(): void {
|
||||
}
|
||||
function onPointerEnter(event: PointerEvent): void {
|
||||
// 悬停仅鼠标(效果示例 e.pointerType==='mouse'——触摸不粘滞)。
|
||||
// 按住期间 / pointercancel 后短窗内的 enter 是边界移交噪音(滚动卡死末层
|
||||
// 根因 2026-09-23:拖出窗外时 Chrome 先派发 pointercancel 清掉按住标志,
|
||||
// 再补发一个 cancel 前排队的 pointerenter——此后 iframe 不再收到任何
|
||||
// boundary 事件,此 enter 设置的 hover 会永久卡住滚动)。真实悬停意图只在
|
||||
// 无按住且非 cancel 余波时成立;释放后鼠标仍在窗上由 onPointerUp 补设。
|
||||
if (pointerDownY !== null) return;
|
||||
if (Date.now() - lastPointerCancelTs < 500) return;
|
||||
if (event.pointerType === 'mouse') {
|
||||
hoverPointerIsMouse = true;
|
||||
if (resumeTimer !== null) {
|
||||
|
||||
Reference in New Issue
Block a user