Skip to content

Commit ef2aad7

Browse files
authored
fix(context-menu): keep menu open after long-press release on touch (tldraw#8741)
In order to make the long-press context menu usable on touch devices, this PR fixes a bug where the menu opened on iOS but closed the moment the user lifted their finger (tldraw#8740). Radix's `ContextMenu` Trigger has its own 700ms touch long-press detection that is independent of tldraw's `long_press` event. When that timer fires, the menu opens while the same touch is still down. The pointerup that follows is read by the dismissable layer as an outside interaction and dismisses the menu before the user can choose anything. The fix swallows outside-interaction dismissals during a 500ms grace window after the menu opens, but only on coarse pointers — desktop right-click is untouched. ### Change type - [x] `bugfix` ### Test plan 1. On iOS Safari (or a touch-emulated browser), long-press the canvas in the examples app or on tldraw.com. 2. Confirm the context menu opens and stays open after the finger is lifted. 3. Tap a menu item; confirm the action runs and the menu closes. 4. Tap somewhere outside the menu (a fresh touch) and confirm the menu dismisses. 5. On desktop, right-click the canvas; confirm the menu opens, an item can be selected, and an outside click dismisses it normally. - [ ] Unit tests - [ ] End to end tests ### Release notes - Fix the iOS context menu closing immediately after a long-press release. ### Code changes | Section | LOC change | | --------- | ---------- | | Core code | +20 / -2 |
1 parent bf660a7 commit ef2aad7

1 file changed

Lines changed: 20 additions & 2 deletions

File tree

packages/tldraw/src/lib/ui/components/ContextMenu/DefaultContextMenu.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { preventDefault, useContainer, useEditor, useEditorComponents } from '@tldraw/editor'
22
import { ContextMenu as _ContextMenu } from 'radix-ui'
3-
import { ReactNode, memo, useCallback, useEffect } from 'react'
3+
import { ReactNode, memo, useCallback, useEffect, useRef } from 'react'
44
import { useMenuIsOpen } from '../../hooks/useMenuIsOpen'
55
import { useDirection, useTranslation } from '../../hooks/useTranslation/useTranslation'
66
import { TldrawUiMenuContextProvider } from '../primitives/menus/TldrawUiMenuContext'
@@ -44,6 +44,13 @@ export const DefaultContextMenu = memo(function DefaultContextMenu({
4444
}
4545
}, [editor, preventEscapeFromLosingShapeFocus])
4646

47+
// On touch devices, the same touch that triggers Radix's long-press open is still
48+
// down when the menu mounts. The release fires events the dismissable layer treats
49+
// as an outside interaction and closes the menu. We swallow dismissals during a
50+
// short grace window after open so the menu stays put until the user actually
51+
// interacts again.
52+
const suppressDismissUntilRef = useRef(0)
53+
4754
const cb = useCallback(
4855
(isOpen: boolean) => {
4956
const body = editor.getContainerDocument().body
@@ -64,8 +71,10 @@ export const DefaultContextMenu = memo(function DefaultContextMenu({
6471
capture: true,
6572
})
6673

67-
// Weird route: selecting locked shapes on long press
6874
if (editor.getInstanceState().isCoarsePointer) {
75+
suppressDismissUntilRef.current = Date.now() + 500
76+
77+
// Weird route: selecting locked shapes on long press
6978
const selectedShapes = editor.getSelectedShapes()
7079
const currentPagePoint = editor.inputs.getCurrentPagePoint()
7180

@@ -115,6 +124,15 @@ export const DefaultContextMenu = memo(function DefaultContextMenu({
115124
alignOffset={-4}
116125
collisionPadding={4}
117126
onContextMenu={preventDefault}
127+
onPointerDownOutside={(e) => {
128+
if (Date.now() < suppressDismissUntilRef.current) e.preventDefault()
129+
}}
130+
onInteractOutside={(e) => {
131+
if (Date.now() < suppressDismissUntilRef.current) e.preventDefault()
132+
}}
133+
onFocusOutside={(e) => {
134+
if (Date.now() < suppressDismissUntilRef.current) e.preventDefault()
135+
}}
118136
>
119137
<TldrawUiMenuContextProvider type="context-menu" sourceId="context-menu">
120138
{content}

0 commit comments

Comments
 (0)