Skip to content

Commit 21aec95

Browse files
RicterZclaude
andcommitted
fix: 彻底修复视图切换后的 ghost click 问题
原有 blockClicks + pointer-events-none 方案有两个缺陷: 1. 屏蔽时长 300ms 不够:iOS 合成 click 最晚 ~500ms 才到, 在 t=150+300=450ms 解除后仍可能被触发 2. pointer-events-none 只阻止新的命中测试,无法拦截已合成的事件 修复方案: - 新增 blockClicksRef(与 blockClicks state 同步),供事件监听器无闭包延迟地读取 - 在 sidebar 最外层 div 加 onClickCapture,在捕获阶段用 ref 判断并 stopPropagation + preventDefault,彻底阻断已合成的 ghost click 传播 - 将视图切换的屏蔽时长从 300ms 延长至 600ms,覆盖浏览器合成延迟的最坏情况 - 统一用 setBlockClicksSync 同时更新 state 和 ref Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 739b0d9 commit 21aec95

1 file changed

Lines changed: 21 additions & 8 deletions

File tree

src/components/sidebar/left-sidebar.tsx

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,11 @@ export const LeftSidebar = ({ onFlyTo, addMarkerEnabled, onToggleAddMarker }: Le
271271
// 视图切换动画:向左滑出,从右滑入
272272
const [slideState, setSlideState] = useState<'idle' | 'exit' | 'enter'>('idle')
273273
const [blockClicks, setBlockClicks] = useState(false) // 防幽灵 click
274+
const blockClicksRef = useRef(false) // ref 供捕获监听器同步读取(state 有闭包延迟)
275+
const setBlockClicksSync = useCallback((val: boolean) => {
276+
blockClicksRef.current = val
277+
setBlockClicks(val)
278+
}, [])
274279
const [displayMode, setDisplayMode] = useState(activeView.mode)
275280
const [displayTripId, setDisplayTripId] = useState(activeView.tripId)
276281
const [displayDayId, setDisplayDayId] = useState(activeView.dayId)
@@ -290,7 +295,7 @@ export const LeftSidebar = ({ onFlyTo, addMarkerEnabled, onToggleAddMarker }: Le
290295

291296
// 1. 当前内容淡出(150ms)
292297
setSlideState('exit')
293-
setBlockClicks(true)
298+
setBlockClicksSync(true)
294299
cancelConfirmDelete()
295300

296301
// 2. 内容切换 + 新内容淡入
@@ -302,7 +307,7 @@ export const LeftSidebar = ({ onFlyTo, addMarkerEnabled, onToggleAddMarker }: Le
302307
requestAnimationFrame(() => {
303308
requestAnimationFrame(() => setSlideState('idle'))
304309
})
305-
setTimeout(() => setBlockClicks(false), 300)
310+
setTimeout(() => setBlockClicksSync(false), 600)
306311
}, 150)
307312

308313
return () => { clearTimeout(t) }
@@ -458,8 +463,8 @@ export const LeftSidebar = ({ onFlyTo, addMarkerEnabled, onToggleAddMarker }: Le
458463
setActiveDragId(null)
459464

460465
// 拖拽结束后短暂屏蔽点击,防止 touchend 合成的 ghost click 触发按钮
461-
setBlockClicks(true)
462-
setTimeout(() => setBlockClicks(false), 300)
466+
setBlockClicksSync(true)
467+
setTimeout(() => setBlockClicksSync(false), 300)
463468

464469
if (!over || active.id === over.id) return
465470
if (!currentDay || !activeView.tripId || !activeView.dayId) return
@@ -677,7 +682,7 @@ export const LeftSidebar = ({ onFlyTo, addMarkerEnabled, onToggleAddMarker }: Le
677682
{/* Back button */}
678683
{displayMode === 'trip' && (
679684
<button
680-
onClick={() => { setBlockClicks(true); setActiveView('overview', null, null) }}
685+
onClick={() => { setBlockClicksSync(true); setActiveView('overview', null, null) }}
681686
className="mr-1 p-1 rounded-lg text-gray-500 hover:bg-white/80 hover:text-blue-600 transition-colors"
682687
title="返回全览"
683688
>
@@ -688,7 +693,7 @@ export const LeftSidebar = ({ onFlyTo, addMarkerEnabled, onToggleAddMarker }: Le
688693
)}
689694
{displayMode === 'day' && (
690695
<button
691-
onClick={() => { setBlockClicks(true); setActiveView('trip', activeView.tripId, null) }}
696+
onClick={() => { setBlockClicksSync(true); setActiveView('trip', activeView.tripId, null) }}
692697
className="mr-1 p-1 rounded-lg text-gray-500 hover:bg-white/80 hover:text-blue-600 transition-colors"
693698
title="返回旅行"
694699
>
@@ -851,7 +856,7 @@ export const LeftSidebar = ({ onFlyTo, addMarkerEnabled, onToggleAddMarker }: Le
851856
return (
852857
<div key={trip.id} className="border border-gray-200 rounded-xl bg-white overflow-hidden mb-2">
853858
<button
854-
onClick={() => { setBlockClicks(true); setActiveView('trip', trip.id, null) }}
859+
onClick={() => { setBlockClicksSync(true); setActiveView('trip', trip.id, null) }}
855860
className="w-full flex items-center gap-3 px-3 py-3 hover:bg-blue-50 transition-colors text-left"
856861
>
857862
<div className="w-9 h-9 bg-blue-100 rounded-full flex items-center justify-center text-lg flex-shrink-0">{trip.emoji ?? '✈️'}</div>
@@ -985,7 +990,7 @@ export const LeftSidebar = ({ onFlyTo, addMarkerEnabled, onToggleAddMarker }: Le
985990
return (
986991
<React.Fragment key={day.id}>
987992
<button
988-
onClick={() => { setBlockClicks(true); setActiveView('day', activeView.tripId, day.id) }}
993+
onClick={() => { setBlockClicksSync(true); setActiveView('day', activeView.tripId, day.id) }}
989994
className="w-full flex items-center gap-3 px-3 py-3 border border-gray-200 rounded-xl bg-white hover:border-blue-300 hover:bg-blue-50 transition-colors text-left"
990995
>
991996
<div className="w-9 h-9 bg-blue-100 rounded-full flex items-center justify-center text-sm font-bold text-blue-600 flex-shrink-0">
@@ -1290,6 +1295,14 @@ export const LeftSidebar = ({ onFlyTo, addMarkerEnabled, onToggleAddMarker }: Le
12901295
!leftSidebar.isOpen ? 'max-lg:-translate-x-full max-lg:transition-transform max-lg:duration-200' : 'max-lg:animate-slide-in-left',
12911296
)}
12921297
style={{ paddingTop: 'env(safe-area-inset-top)', paddingBottom: 'env(safe-area-inset-bottom, 0px)' }}
1298+
onClickCapture={(e) => {
1299+
// 在捕获阶段拦截 ghost click:CSS pointer-events-none 无法阻止已合成的事件,
1300+
// 用 ref(非 state)同步判断,stopPropagation + preventDefault 双重阻断
1301+
if (blockClicksRef.current) {
1302+
e.stopPropagation()
1303+
e.preventDefault()
1304+
}
1305+
}}
12931306
>
12941307
{renderHeader()}
12951308

0 commit comments

Comments
 (0)