Skip to content

Long tap does not work after drag-and-drop#2847

Merged
raineorshine merged 8 commits intocybersemics:mainfrom
karunkop:long-tap-bug
Mar 13, 2025
Merged

Long tap does not work after drag-and-drop#2847
raineorshine merged 8 commits intocybersemics:mainfrom
karunkop:long-tap-bug

Conversation

@karunkop
Copy link
Collaborator

@karunkop karunkop commented Mar 11, 2025

Problem

  • Long press functionality failed after drag operations
  • Global lock state remained active, blocking subsequent long presses
  • No reliable way for components to detect active long presses elsewhere
  • State management relied on global variables and DOM events

Solution

  • Created longPressStore.ts using ministore pattern for centralized state
  • Updated useLongPress hook to use store instead of globals
  • Modified drag hooks to notify store when drag operations begin/end
  • Implemented proper canceled flag handling for interrupted presses

Testing: Added end-to-end tests to verify long press interactions and drag-and-drop behavior.

issue: #2090

@karunkop karunkop added the bug Something isn't working label Mar 11, 2025
@karunkop karunkop added this to the 🧤 Drag & Drop II milestone Mar 11, 2025
@karunkop karunkop requested a review from raineorshine March 11, 2025 19:08
@karunkop karunkop self-assigned this Mar 11, 2025
@raineorshine
Copy link
Contributor

raineorshine commented Mar 11, 2025

Thanks for the PR!

Before I can evaluate the proposed solution, please provide a thorough description of the cause of the bug. Then, an explanation of why this solution is the best way to address it.

In particular, you will need to
justify the added state. Drag-and-drop and long press already have existing state management, so my assumption is that there is a bug in the existing code, not that more state is needed.

@karunkop
Copy link
Collaborator Author

Thanks for the PR!

Before I can evaluate the proposed solution, please provide a thorough description of the cause of the bug. Then, an explanation of why this solution is the best way to address it.

In particular, you will need to justify the added state. Drag-and-drop and long press already have existing state management, so my assumption is that there is a bug in the existing code, not that more state is needed.

Cause of the issue:

The issue with long press in drag and drop functionality is caused by a lock mechanism that's not being properly reset:

In useLongPress.ts, a global lock prevents multiple simultaneous long presses
When long press starts → lock = true
When long press ends normally → lock = false

Problem: During drag operations, the normal flow is interrupted.
endDrag function in useDragAndDropThought.tsx resets dragHold state but not the lock

Result: The lock remains true, blocking all subsequent long press attempts

This explains why we can't immediately long-press a thought after a drag operation. The solution is to reset the lock in the endDrag function. In short we are not adding new state but rather reseting a global lock via custom dispatching method.

@raineorshine
Copy link
Contributor

Thank you, I appreciate the explanation.

Problem: During drag operations, the normal flow is interrupted.
endDrag function in useDragAndDropThought.tsx resets dragHold state but not the lock

I'd like to drill down on this a bit more. You're saying that when a drag ends, useDragAndDropThought is not resetting the lock state. However, the lock is self contained within useLongPress, and that hook should have everything it needs to reset the lock. The lock is reset in stop, and stop is called on touchend and touchcancel. Thus the lock should be guaranteed to be reset when the touch ends. Why isn't this happening after drag-and-drop?

The way I see it, useDragAndDropThought shouldn't have to do anything special to reset the lock, since it's handled internally in useLongPress. Is the touchend or touchcancel event propagation being prevented before it gets to useLongPress? Or are the handlers possibly getting overwritten?

Hopefully I'm not missing something, but that's the question I have currently.

In short we are not adding new state but rather reseting a global lock via custom dispatching method.

You're right, sorry I mischaracterized it! It is a new dispatching method, not new state.

@karunkop
Copy link
Collaborator Author

Thank you, I appreciate the explanation.

Problem: During drag operations, the normal flow is interrupted.
endDrag function in useDragAndDropThought.tsx resets dragHold state but not the lock

I'd like to drill down on this a bit more. You're saying that when a drag ends, useDragAndDropThought is not resetting the lock state. However, the lock is self contained within useLongPress, and that hook should have everything it needs to reset the lock. The lock is reset in stop, and stop is called on touchend and touchcancel. Thus the lock should be guaranteed to be reset when the touch ends. Why isn't this happening after drag-and-drop?

The way I see it, useDragAndDropThought shouldn't have to do anything special to reset the lock, since it's handled internally in useLongPress. Is the touchend or touchcancel event propagation being prevented before it gets to useLongPress? Or are the handlers possibly getting overwritten?

Hopefully I'm not missing something, but that's the question I have currently.

In short we are not adding new state but rather reseting a global lock via custom dispatching method.

You're right, sorry I mischaracterized it! It is a new dispatching method, not new state.

Yes you are actually right about event propagation being prevented thing. I spent some time researching about the internals of react dnd and here's what i found out. React DnD intercepts touch/mouse events during drag, blocking useLongPress.stop from running. It captures touchend/mouseup and stops propagation, preventing the lock reset. As a result, lock remains true, blocking future long presses. So the only solution that came in find is to manually reset the lock when the drag process ends. Since we are maintaining a global lock in the useLongPress hook, we needed a way to expose it to reset the value from other hooks/files.

@raineorshine
Copy link
Contributor

Okay, that explains it. react-dnd breaks useLongPress by stopping the propagation of the touchend event.

From a complexity perspective this is unfortunate, as it forces us to tightly couple useLongPress with the drag-and-drop logic. I guess it's not totally unreasonable, as a long press on a thought is expected to change seamlessly into drag-and-drop on touchmove. But it's too bad that useLongPress will no longer be self-contained.

Now that we have identified the root cause, note that the problem is bigger than just lock. If the touchend event is captured, then useLongPress will never fire its onLongPressEnd handler. The consumers of onLongPressEnd expect onLongPressEnd to always be called (with canceled set appropriately). We should trigger onLongPressEnd when drag-and-drop hijacks the touch events to avoid other problems.

A few other points:

  • Resetting the lock and triggering onLongPressEnd({ canceled: true }) should probably occur on beginDrag, not endDrag.
  • This will need to occur everywhere that uses react-dnd's useDrag, not just useDragAndDropThought.
  • Firing a custom DOM event is a bit unusual for a React-Redux project. Instead, let's stick with dispatching actions to the Redux store or using a ministore. I believe the ministore is new since you worked on the project last. It's a great way to manage reactive state independently from the Redux store, so changes do not trigger selector recalculations everywhere. It is type safe, composable, and reactive. For this case I would recommend ministore.

@karunkop
Copy link
Collaborator Author

Okay, that explains it. react-dnd breaks useLongPress by stopping the propagation of the touchend event.

From a complexity perspective this is unfortunate, as it forces us to tightly couple useLongPress with the drag-and-drop logic. I guess it's not totally unreasonable, as a long press on a thought is expected to change seamlessly into drag-and-drop on touchmove. But it's too bad that useLongPress will no longer be self-contained.

Now that we have identified the root cause, note that the problem is bigger than just lock. If the touchend event is captured, then useLongPress will never fire its onLongPressEnd handler. The consumers of onLongPressEnd expect onLongPressEnd to always be called (with canceled set appropriately). We should trigger onLongPressEnd when drag-and-drop hijacks the touch events to avoid other problems.

A few other points:

  • Resetting the lock and triggering onLongPressEnd({ canceled: true }) should probably occur on beginDrag, not endDrag.
  • This will need to occur everywhere that uses react-dnd's useDrag, not just useDragAndDropThought.
  • Firing a custom DOM event is a bit unusual for a React-Redux project. Instead, let's stick with dispatching actions to the Redux store or using a ministore. I believe the ministore is new since you worked on the project last. It's a great way to manage reactive state independently from the Redux store, so changes do not trigger selector recalculations everywhere. It is type safe, composable, and reactive. For this case I would recommend ministore.

Changes Made As Per Discussion

  • Created a centralized longPressStore using the ministore pattern
  • Removed global lock variable and related event listeners
  • Properly tracked long press state across drag operations
  • Updated all drag sources (useDragAndDropThought, useDragDropFavorites, useDragAndDropToolbarButton) to notify the store when drags start
  • Fixed the canceled flag state in useLongPress to properly handle normal long press completion

Copy link
Contributor

@raineorshine raineorshine left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm... I'm not sure how we got to this point. My first comments were about avoiding additional state. You reassured me by pointing out that you were just introducing a new dispatch method to change the long press lock from a different module. Now we have longPressStore.isLocked, longPressStore.isLongPressing, onLongPressEndRef, and wasLongPressingRef (and that's not including the existing globals.longpressing). This is a lot of state for a long press hook. I think you need to rethink your approach and find a lighter solution.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test says that it shows the alert and quick drop panels, but they don't appear in the snapshot.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved

@karunkop
Copy link
Collaborator Author

Hmmm... I'm not sure how we got to this point. My first comments were about avoiding additional state. You reassured me by pointing out that you were just introducing a new dispatch method to change the long press lock from a different module. Now we have longPressStore.isLocked, longPressStore.isLongPressing, onLongPressEndRef, and wasLongPressingRef (and that's not including the existing globals.longpressing). This is a lot of state for a long press hook. I think you need to rethink your approach and find a lighter solution.

I have refactored the ministore pattern to opt in for single state variable that controls the locking behavior while long press

Copy link
Contributor

@raineorshine raineorshine left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks much better! Thanks.

I think it may be possible to consolidate globals.longpressing, but that is out of scope for this issue. It will be good to do it in a separate PR anyway for more granular regression tracking.

@raineorshine raineorshine merged commit 25e64c7 into cybersemics:main Mar 13, 2025
3 checks passed
ethan-james pushed a commit to ethan-james/em that referenced this pull request Mar 21, 2025
@raineorshine raineorshine modified the milestones: 🧤 Drag & Drop II, 🧤 Drag & Dropp Jun 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants