fix: let the guard option veto a blank pointerdown; buttons that click and drag (backport to master) - #3446
Open
kumilingus wants to merge 1 commit into
Open
Conversation
…k and drag Backport of clientIO#3438 (targets dev) onto master. Net change of the PR's 10 commits, applied to master's identical core/preset base. - joint-core: split guard() into guardExplicit() (event-level veto/allow) + guard() (event + target), so options.guard can veto a blank pointerdown; split FORM_CONTROL_TAG_NAMES vs PREVENT_INTERACTION_TAG_NAMES and match both across the target path (not just evt.target); drop OPTION. - joint-react: preset guards portaled overlay content via guardExplicit, lets a <button> in a node/magnet both click and drag (drops BUTTON from PREVENT_INTERACTION_TAG_NAMES, swallows the trailing click after a move). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Backport of #3438 onto
master(that PR targetsdev). Same net change, same 11 files — applied to master's identical core/preset base. The Storybook story resolves on master's Storybook 10 setup (getAPILink,@storybook/react-vite).Supersedes #3437 — carries its commit unchanged (credit to @samuelgja) and builds on it.
1. fix(joint-core):
options.guardcan veto a blank pointerdownpointerdownconsultedguard()only when the press hit a cell view, so nothing could suppress a blank interaction. That matters for DOM content rendered intopaper.el— an overlay, a popup, a toolbar — where the paper cannot be reasoned with from outside: its listeners are delegated onpaper.el, so astopPropagation()from content inside it always arrives after the paper has already reacted.Running the full
guard()there is not an option — it rejects any target that is not inside the paper's own SVG, so a ruler or gutter inpaper.elwould stop opening a blank interaction at all, and the whole gesture with it, since the document-level drag listeners are delegated frompointerdown.So
guard()is split:guardExplicit(evt, view)— decisions about this event: the right mouse button, theguardoption, anevt.data.guardedflag. Returnsboolean | undefined;undefinedmeans nothing decided. The third state is required becauseguarded: falseis an explicit allow that must beat the target tests.guard(evt, view)— unchanged:guardExplicit()first, then judge the target — tag name, view, whether it lies inside the paper's SVG.A press that hit no cell view consults only
guardExplicit(), so it opens a blank interaction exactly as before andoptions.guardcan now veto it.GUARDED_TAG_NAMESstill judges the target, so a<select>in an overlay is not treated differently — there is a test pinning that.guard()also returns a real boolean now:evt.data.guardedis caller-set and was only ever tested againstundefined, so anullor0used to propagate out of a method typedboolean.2. fix(joint-react): keep portaled content from driving the paper
<Paper>children are portaled intopaper.elbut render outside its SVG, and a press on one must not drive the paper. joint-core has to leave that press alone (a plaindia.Paperconsumer rendering intopaper.elrelies on it), so the strict behaviour lives in the joint-react preset, which overridesguardExplicit. It runs after the caller's ownoptions.guard.3. fix(joint-core, joint-react): a button inside a node can both click and drag
A
<button>in a node body or a magnet could only ever be clicked, so there was no way to drag a node by a button inside it, or to start a link from a button in a magnet — the natural gesture when the magnet is a row with a control in it.The block came from one flag answering two questions. Now two lists:
FORM_CONTROL_TAG_NAMESpreventDefault) → native click and focusPREVENT_INTERACTION_TAG_NAMESSame members by default, so core behaviour is unchanged. joint-react's preset drops
BUTTONfrom the second list only —FORM_CONTROL_TAG_NAMESis inherited untouched, so a button keeps its native default action and still takes focus on press. (Verified in Chrome:document.activeElementis the button after a click.)To keep one gesture from being both, the preset's
pointerupwithholds the next nativeclickonce the pointer has travelled pastclickThreshold. joint-core already withholds its ownpointerclickat that point; the browser does not, because press and release share a target whenever the node follows the pointer — exactly what happens when an element is dragged by a button inside it.4. fix(joint-core): tag lists are matched against the whole path
Both lists were tested against
evt.targetalone. The press target of<button><span>Save</span></button>is the SPAN, which is in neither list — so a button with an icon or a label span inside it behaved the opposite way round from a bare one: it lost its default action (no focus) and did start an element move. Confirmed ondevbefore fixing.hasTagNameInPath()walks from the target up to the cell view, so a press anywhere inside a control counts as a press on the control. Note this is a behaviour change for anyone who was (accidentally) dragging a node by markup nested inside one of these controls.Path matching also makes
OPTIONredundant — an<option>only exists inside a<select>, which is listed — so it is dropped from both lists. Covered by a test using<select multiple>, whose options render inline and do receive real presses.The lists stay exact
tagNamematches rather than becomingclosest()selectors, so all three tag-name options (including the pre-existingGUARDED_TAG_NAMES) keep one matching semantic, and the arrays do not quietly become CSS selector lists.Story (joint-react)
Examples → Buttons In Magnetsshows one control per list, both in a node body and inside a magnet:<button>— clicks and drags; its label sits in a<span>, so it also exercises the nested-target case<input>— keeps every gesture that stays inside it, selecting text rather than dragging<select>— guarded outright; the event never reaches the paperTyping (joint-core)
Paper.Options['guard']now declaresviewoptional. It has always been called without a view frompointerclick,pointerdblclick,mouseoverand the rest. Custom guards that dereferenceviewwill now fail to compile — that surfaces a latent bug rather than creating one.Tests
guardveto, the<select>bit-exactness case, the two-list split, and a press inside a<button>test:tsand lint cleanNote for reviewers: joint-react's jest resolves
@joint/coretopackages/joint-core/dist/joint.min.js, so that suite only exercises core changes after ayarn dist.🤖 Generated with Claude Code