slides: press and hold opens the same menu on a phone - #276
Conversation
aee8df4 to
98522b0
Compare
The editor had no context menu at all, so a right-click on the canvas produced the browser's own — Back, Reload, View source — which is precisely the wrong set of verbs for a slide, and the one gesture every user of every other editor tries first. Three menus, chosen by what was actually clicked. An element offers Edit text, Cut, Copy, Duplicate, Bring to front, Send to back, Group / Ungroup and Delete. A slide thumbnail offers New slide, Duplicate slide and Delete slide. The canvas beside the slide offers Paste and the slide's own verbs. Aiming at something outside the selection selects it first, the way every editor does, so the verbs always describe what they are about to act on. The browser's menu is deliberately left where it is the better one: form fields, links, and text mid-edit, where the system carries spelling, dictation, look-up and a real paste. That last case needs the question asked on the PRESS — a right-click commits the edit before contextmenu is dispatched — and answered by geometry, because Moveable's control box sits on top of the caret and swallows the target. ctxmenu.ts renders and dismisses; it knows nothing about slides. Paste shares the text path with the paste EVENT rather than growing a second copy of it, which is why wirePaste is split.
A touch screen has no right mouse button, and iOS fires no contextmenu event for an ordinary element — a long press there raises the system callout instead. So the menu the previous commit added would have been mouse-only, and Duplicate, Group, the z-order and Delete would have had no route on a phone at all. The gesture is recognised by hand and routed into the SAME openContextMenuAt() the right-click uses, so the two can never drift apart in what they offer or in what they refuse. Three things this has to get right, because the listener sits on the surface Moveable drags elements on: - movement cancels it, so a drag or a pan is never stolen; - a second finger cancels it, so a pinch is never stolen; - the release is swallowed, because the finger comes up ON the menu that just appeared beneath it and would otherwise fire its first row. iOS's callout bubble is suppressed on the canvas and the slide list so the two gestures do not race — except over text being edited, where the system bubble carries paste, dictation and spelling and is the better menu of the two.
Carries @7jameslondon's review of nyblnet#271 across to this gesture, which had the same shape: a pointer handler that runs for a mouse and filters itself back out by pointerType, plus a window-level listener swallowing pointerup/mouseup/click for 500ms afterwards. Touch events cannot fire for a mouse, and cancelling the touchend stops the browser generating the tap that ends the press rather than racing it. swallowTapAfterLongPress and TAP_AFTER_PRESS_MS go away. The release still cannot fire the row that opened under the finger — that is now a property of the cancelled touchend rather than of a timed suppression window, and the test that asserts it is unchanged.
f90517a to
d36e06e
Compare
|
@nyblnet — review at your convenience; this is the touch half of #275. iOS fires no Three things it had to get right, since the listener sits on the surface Moveable drags on: movement cancels it, a second finger cancels it, and the release is cancelled so it can't fire the row that appeared under the finger. There's a test asserting nothing is duplicated or deleted by the lift. The second commit carries @7jameslondon's review from #271 across to this gesture — touch events instead of pointer events, cancelling at source instead of a timed suppression window. Stacked on #275. One thing worth a real device: whether |
The problem
A touch screen has no right mouse button. And iOS fires no
contextmenueventfor an ordinary element — a long press there raises the system callout ("Copy
/ Look Up / Share"), not a menu.
So the context menu in #275 would have shipped mouse-only, and on a phone
Duplicate, Group, Bring to front, Send to back and Delete would have had no
route at all — none of them appear in the phone toolbar, which folds down to
☰ · + · undo · Format · Save · ⋯.
Measured before this change: a 900ms press on a canvas element produced
contextmenuevents: 0.The fix
Hold for 500ms — what iOS itself uses for the callout, so it matches the muscle
memory already on the device — and the same menu opens. Element, slide
thumbnail, or the canvas beside the slide, exactly as with a right-click.
The gesture is recognised by hand and routed into the same
openContextMenuAt()the right-click uses, so the two can never drift apart inwhat they offer or in what they refuse.
The three things this had to get right
This listener sits on the surface Moveable drags elements on, so a careless
version is worse than no menu:
the finger, so the lift that ends the press would land as a click on the row
now sitting at that point — the first item would fire itself. Cancelling the
touchendstops the browser generating that tap at all. There is a testasserting nothing is duplicated or deleted by the release.
iOS's own callout bubble is suppressed on the canvas and the slide list so the
two gestures don't race:
The exemption matters: over text being edited, the system bubble carries
paste, dictation and spelling, and is the better menu of the two.
.ed-rootalready sets
user-select: none, so nothing else changes.Before / after
Same gesture both sides: press and hold an element, then a slide thumbnail.
How this was verified
iPhone emulation (390×664, DPR 3,
hasTouch) with real CDP touch dispatch —genuine press / move / lift sequences, not synthetic clicks:
Gates:
No new user-facing strings (the menu's strings landed with #275), so no i18n
changes here. No format or kernel change; nothing under
sync/.Update after review
@7jameslondon's review on #271 applies to this gesture as well, so the second
commit here carries it across. The recogniser was a
pointerdownhandler thatruns for a mouse too and filtered itself back out by
pointerType, plus awindow-level listener swallowing
pointerup/mouseup/clickfor 500ms afterthe menu opened. It is now
touchstart/touchmove/touchend, which cannotfire for a mouse at all, and the release is stopped by cancelling the
touchendrather than by a timed suppression window — soswallowTapAfterLongPressandTAP_AFTER_PRESS_MSare gone. The test thatasserts the release cannot fire the row beneath it is unchanged and still
passes.
Caveat: verified under Chromium's iPhone emulation, not on physical iOS
hardware. The two places real Safari could still differ are worth a device
check: whether
-webkit-touch-callout: nonefully suppresses the callout on along press over rendered slide text, and whether iOS's own haptic/selection
behaviour interferes with the 500ms threshold. The recogniser itself is pointer
events and timers, which behave identically.
Changelog entry — not in the diff, on purpose
CHANGELOG.mdis this repo's named conflict magnet (docs/PARALLEL-WORK.md§3). With several of these open at once, and[Unreleased]being emptied every time a release is cut, a changelog hunk made every one of them conflict on that file and nothing else — twice over. They carry noCHANGELOG.mdchange so they stay mergeable; here is the entry to drop in at release time, or I'll add it back in whatever form you prefer.