fix: detect the {x, y} overload by key presence (#812) - #816
Conversation
$.fn.contextMenu() decided between the {x, y} positioning overload and a
menu definition by checking that both values were not undefined. An event
without a pointer position - a keyboard or synthetic one - yields
{x: undefined, y: undefined}, which then fell through to the plain-object
branch, was treated as a menu definition and threw "No selector specified".
The overload is now recognised by key presence. Coordinates that are not
usable numbers, including a half-filled pair, fall back to the
element-relative default position instead of throwing or positioning at NaN.
The default position() no longer bails out on a falsy x/y either, so an
explicit {x: 0, y: 0} lands at the page origin.
Also documents that {x, y} are page coordinates.
Fixes #812
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b6fb3d90a3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // See https://github.com/swisnl/jQuery-contextMenu/issues/812 | ||
| function isCoordinateOperation(operation) { | ||
| return !!operation && typeof operation === 'object' && | ||
| 'x' in operation && 'y' in operation; |
There was a problem hiding this comment.
Recognize single-key coordinate operations
When callers actually omit one coordinate, such as .contextMenu({x: 123}), this predicate returns false because it requires both keys. The argument then enters the plain-object menu-definition branch and ultimately throws No selector specified, rather than using determinePosition as the new documentation and changelog promise. The half-filled test does not catch this because it supplies a y key whose value is undefined; treat the presence of either coordinate key as the positioning overload so genuinely missing properties also fall back.
Useful? React with 👍 / 👎.
Review follow-up. {x: 123} with the y key genuinely absent still fell
through to the menu-definition branch and threw "No selector specified",
while {x: 123, y: undefined} did not. Either key now selects the
positioning overload, with a menu definition still winning whenever the
object also carries a definition-only key (items, selector, build), so an
options object with an x/y of its own keeps being registered as a menu.
Also pins the neighbouring call shapes - no argument, 'hide', 'destroy',
true/false, a definition object - as unchanged, plus negative coordinates.
|
Thanks, addressed in 1da51e9. Recognize single-key coordinate operations (P2) - agreed and fixed. Because that widens what counts as a positioning call, I paired it with a guard in the other direction: an object that also carries a definition-only key ( New tests cover both: an omitted 92 unit tests and 36 acceptance tests green locally; CI was green on the first commit across jQuery 1.12.4 / 2.2.4 / 3.7.1 / 4.0.0. |
Closes #812
The bug
$.fn.contextMenu(operation)decided between the{x, y}positioning overload and a menu definition with:An event that carries no pointer position (a keyboard-originated or synthetic one) has
pageX/pageYundefined, so$(el).contextMenu({x: e.pageX, y: e.pageY})produced{x: undefined, y: undefined}. That failed the test above, fell through to the$.isPlainObject()branch, was treated as a menu definition and reached$.contextMenu('create', ...), which threwNo selector specifiedfor what was clearly a positioning call.The fix
Detection is now by key presence rather than by value: an object carrying an
xor aykey is a positioning call regardless of what those values are. A menu definition still wins whenever the object also carries a definition-only key (items,selectororbuild), so an options object that happens to have anx/yof its own cannot be misrouted. The plugin has nox/yoption, so that guard is belt and braces, but it keeps the widened detection strictly non-breaking.For the values themselves I chose the graceful fallback, not a thrown error: when the pair is not a usable coordinate pair, the menu is shown at the element-relative default position, exactly as
$(el).contextMenu()with no argument does. Reasoning:position()already routes todeterminePosition()when it has no coordinates, and$(el).contextMenu()already means "position relative to the element". The fallback just leavespageX/pageYunset on the triggered event and reuses that path, so there is one behaviour for "no coordinates" instead of two.layerClickfakeClickguard, thee.dataguard in the contextmenu handler). It throws for structurally impossible input, such as a create without a selector, which this is not.Details:
{x: 123, y: undefined}) and a half-omitted one ({x: 123}) fall back too, rather than computing an offset ofNaNor throwing.0is treated as a real coordinate everywhere. The defaultposition()used to bail out todeterminePosition()on!x && !y, which also caught the perfectly valid page origin. It now checks for actual numbers, with the'maintain'branch moved ahead of that check.{x: '123', y: '456'}, e.g. read off a data attribute) still work, since the positioning arithmetic always accepted them. They are coerced before going on the event, soe.pageXis always a real number.Docs
documentation/docs/plugin-commands.mdnow states thatxandyare page coordinates, the same space asevent.pageX/event.pageYand scroll inclusive, not viewport coordinates and not relative to the trigger, with a short conversion snippet for viewport-based sources such asgetBoundingClientRect(), a canvas or a map library. It also documents the fallback. See #726 for a worked example of why this matters.Backwards compatibility
No currently working call shape changes.
test/unit/issue-812-xy-overload.test.jspins the ones that share this dispatch chain: no argument,'hide','destroy',false/true(disable/enable), a plain definition object, a definition object that also carriesx/y, and{x: 123, y: 456}reachingposition()unchanged. Everything else this PR touches used to throw or to misposition, so the only behaviour changes are crash-to-working ones. Nothing new is thrown.The one deliberate behaviour change is
{x: 0, y: 0}: it used to be caught by the!x && !ybail-out and positioned relative to the element, and now lands at the page origin as documented. The existingopen contextMenu at 0,0test still passes.Tests
New
test/unit/issue-812-xy-overload.test.js: undefined coordinates no longer throw and fall back, an omittedxorykey falls back, finite and negative coordinates are forwarded unchanged, numeric strings arrive as numbers,{x: 0, y: 0}is honoured by both the routing and the defaultposition(), a half-filled pair falls back without producingNaNcss, non-numeric values fall back, a definition object carryingx/ystill registers a menu, and the call shapes listed above are unchanged.Full unit suite green (92 tests) and the 36 acceptance tests pass, since positioning is exercised there.
dist/is intentionally untouched;test/integration/htmlis gitignored, so the docs edit brings no generated HTML with it.