Skip to content
1 change: 1 addition & 0 deletions draftlogs/8152_fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Use consistent cursors across all plot types and modes [[#8152](https://github.com/plotly/plotly.js/pull/8152)]
23 changes: 23 additions & 0 deletions src/components/dragelement/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,26 @@ exports.selectingOrDrawing = function(dragmode) {
exports.rectMode(dragmode)
);
};

exports.effectiveDragmode = (dragmode, { shiftKey } = {}) => {
if(!shiftKey || !dragmode || exports.selectingOrDrawing(dragmode)) return dragmode;
return dragmode === 'pan' ? 'zoom' : 'pan';
};

exports.dragCursor = (dragmode, modifiers) =>
exports.effectiveDragmode(dragmode, modifiers) === 'pan' ? 'move' : 'crosshair';

const ACTION_BY_MODIFIER = { shiftKey: 'rotate', ctrlKey: 'pan', altKey: 'zoom' };
const CURSOR_BY_ACTION = { rotate: 'default', pan: 'move', zoom: 'crosshair' };

exports.dragCursor3d = (cameraMode, modifiers = {}) => {
if(!cameraMode) return 'default';

const heldActions = Object.keys(ACTION_BY_MODIFIER)
.filter((key) => modifiers[key])
.map((key) => ACTION_BY_MODIFIER[key]);
if(heldActions.length > 1) return 'default';

const modeAction = cameraMode === 'pan' || cameraMode === 'zoom' ? cameraMode : 'rotate';
return CURSOR_BY_ACTION[heldActions[0] || modeAction];
};
44 changes: 44 additions & 0 deletions src/components/dragelement/modifier_keys.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use strict';

const NO_MODIFIERS = { shiftKey: false, ctrlKey: false, altKey: false };

const isEditable = (el) =>
!!el && (el.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(el.tagName));

exports.update = function(gd, { shiftKey, ctrlKey, altKey }) {
if(!gd._fullLayout) return;

const prev = gd._modifierKeys || NO_MODIFIERS;
if(prev.shiftKey === !!shiftKey && prev.ctrlKey === !!ctrlKey && prev.altKey === !!altKey) return;
gd._modifierKeys = { shiftKey: !!shiftKey, ctrlKey: !!ctrlKey, altKey: !!altKey };

const modules = gd._fullLayout._basePlotModules;
for(let i = 0; i < modules.length; i++) {
if(modules[i].updateCursor) modules[i].updateCursor(gd);
}
};

exports.track = function(gd) {
if(gd._untrackModifierKeys) return;

const onKey = (e) => {
if(!isEditable(e.target)) exports.update(gd, e);
};
const onBlur = () => exports.update(gd, NO_MODIFIERS);

window.addEventListener('keydown', onKey);
window.addEventListener('keyup', onKey);
window.addEventListener('blur', onBlur);

gd._untrackModifierKeys = function() {
window.removeEventListener('keydown', onKey);
window.removeEventListener('keyup', onKey);
window.removeEventListener('blur', onBlur);
delete gd._untrackModifierKeys;
delete gd._modifierKeys;
};
};

exports.untrack = function(gd) {
if(gd._untrackModifierKeys) gd._untrackModifierKeys();
};
5 changes: 5 additions & 0 deletions src/components/dragelement/unhover.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
'use strict';

var d3 = require('@plotly/d3');

var Events = require('../../lib/events');
var overrideCursor = require('../../lib/override_cursor');
var throttle = require('../../lib/throttle');
var getGraphDiv = require('../../lib/dom').getGraphDiv;

Expand Down Expand Up @@ -45,6 +48,8 @@ unhover.raw = function raw(gd, evt) {
var oldhoverdata = gd._hoverdata;

if(!evt) evt = {};
if(evt.target && evt.target.tagName) overrideCursor(d3.select(evt.target), '');

if(evt.target && !gd._dragged &&
Events.triggerHandler(gd, 'plotly_beforehover', evt) === false) {
return false;
Expand Down
2 changes: 2 additions & 0 deletions src/plot_api/plot_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ var cartesianLayoutAttributes = require('../plots/cartesian/layout_attributes');
var Drawing = require('../components/drawing');
var Color = require('../components/color');
var initInteractions = require('../plots/cartesian/graph_interact').initInteractions;
var modifierKeys = require('../components/dragelement/modifier_keys');
var xmlnsNamespaces = require('../constants/xmlns_namespaces');
var clearOutline = require('../components/selections').clearOutline;

Expand Down Expand Up @@ -394,6 +395,7 @@ function _doPlot(gd, data, layout, config) {
subroutines.drawData,
subroutines.finalDraw,
initInteractions,
modifierKeys.track,
Plots.rehover,
Plots.redrag,
Plots.reselect,
Expand Down
10 changes: 3 additions & 7 deletions src/plots/cartesian/dragbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ var dragElement = require('../../components/dragelement');
var helpers = require('../../components/dragelement/helpers');
var selectingOrDrawing = helpers.selectingOrDrawing;
var freeMode = helpers.freeMode;
var effectiveDragmode = helpers.effectiveDragmode;

var FROM_TL = require('../../constants/alignment').FROM_TL;
var clearGlCanvases = require('../../lib/clear_gl_canvases');
Expand Down Expand Up @@ -168,12 +169,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
if(isMainDrag) {
// main dragger handles all drag modes, and changes
// to pan (or to zoom if it already is pan) on shift
if(e.shiftKey) {
if(dragModeNow === 'pan') dragModeNow = 'zoom';
else if(!selectingOrDrawing(dragModeNow)) dragModeNow = 'pan';
} else if(e.ctrlKey) {
dragModeNow = 'pan';
}
dragModeNow = effectiveDragmode(dragModeNow, e);
} else {
// all other draggers just pan
dragModeNow = 'pan';
Expand Down Expand Up @@ -1153,7 +1149,7 @@ function dZoom(d) {
}

function getDragCursor(nsew, dragmode, isMainDrag) {
if(!nsew) return 'pointer';
if(!nsew) return 'default';
if(nsew === 'nsew') {
// in this case here, clear cursor and
// use the cursor style set on <g .draglayer>
Expand Down
11 changes: 10 additions & 1 deletion src/plots/cartesian/graph_interact.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ var d3 = require('@plotly/d3');
var Fx = require('../../components/fx');
var dragElement = require('../../components/dragelement');
var setCursor = require('../../lib/setcursor');
var dragCursor = require('../../components/dragelement/helpers').dragCursor;
var modifierKeys = require('../../components/dragelement/modifier_keys');

var makeDragBox = require('./dragbox').makeDragBox;
var DRAGGERSIZE = require('./constants').DRAGGERSIZE;
Expand Down Expand Up @@ -47,6 +49,8 @@ exports.initInteractions = function initInteractions(gd) {
xa._length, ya._length, 'ns', 'ew');

maindrag.onmousemove = function(evt) {
modifierKeys.update(gd, evt);

// This is on `gd._fullLayout`, *not* fullLayout because the reference
// changes by the time this is called again.
gd._fullLayout._rehover = function() {
Expand Down Expand Up @@ -128,6 +132,7 @@ exports.initInteractions = function initInteractions(gd) {
var hoverLayer = fullLayout._hoverlayer.node();

hoverLayer.onmousemove = function(evt) {
modifierKeys.update(gd, evt);
evt.target = gd._fullLayout._lasthover;
Fx.hover(gd, evt, fullLayout._hoversubplot);
};
Expand All @@ -151,7 +156,11 @@ exports.initInteractions = function initInteractions(gd) {
// Note that changing the axis configuration and/or the fixedrange attribute
// should trigger a full initInteractions.
exports.updateFx = function(gd) {
exports.updateCursor(gd);
};

exports.updateCursor = function(gd) {
var fullLayout = gd._fullLayout;
var cursor = fullLayout.dragmode === 'pan' ? 'move' : 'crosshair';
var cursor = dragCursor(fullLayout.dragmode, gd._modifierKeys);
setCursor(fullLayout._draggers, cursor);
};
1 change: 1 addition & 0 deletions src/plots/cartesian/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -761,3 +761,4 @@ exports.toSVG = function(gd) {
};

exports.updateFx = require('./graph_interact').updateFx;
exports.updateCursor = require('./graph_interact').updateCursor;
4 changes: 4 additions & 0 deletions src/plots/geo/geo.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ var Plots = require('../plots');
var Axes = require('../cartesian/axes');
const { concatExtremes, getAutoRange, makePadFn } = require('../cartesian/autorange');
var dragElement = require('../../components/dragelement');
var dragCursor = require('../../components/dragelement/helpers').dragCursor;
var setCursor = require('../../lib/setcursor');
var prepSelect = require('../../components/selections').prepSelect;
var clearOutline = require('../../components/selections').clearOutline;
var selectOnClick = require('../../components/selections').selectOnClick;
Expand Down Expand Up @@ -506,6 +508,8 @@ proto.updateFx = function (fullLayout, geoLayout) {
}
};

setCursor(bgRect, dragMode === false ? '' : dragCursor(dragMode));

if (dragMode === 'pan') {
bgRect.node().onmousedown = null;
const zoom = createGeoZoom(_this, geoLayout);
Expand Down
34 changes: 26 additions & 8 deletions src/plots/geo/zoom.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,29 @@
var d3 = require('@plotly/d3');
var Lib = require('../../lib');
var Registry = require('../../registry');
var dragElement = require('../../components/dragelement');

var radians = Math.PI / 180;
var degrees = 180 / Math.PI;
var zoomstartStyle = { cursor: 'pointer' };
var zoomendStyle = { cursor: 'auto' };
var zoomstartStyle = { cursor: 'move' };
var zoomendStyle = { cursor: null };

function startDragStyle(geo, el) {
d3.select(el).style(zoomstartStyle);

var sourceEvent = d3.event.sourceEvent;
var isRightClick = sourceEvent && (sourceEvent.buttons === 2 || sourceEvent.ctrlKey);
if(sourceEvent && sourceEvent.type === 'mousedown' && !isRightClick) {
geo._dragCover = dragElement.coverSlip();
geo._dragCover.style.cursor = zoomstartStyle.cursor;
}
}

function endDragStyle(geo, el) {
d3.select(el).style(zoomendStyle);
Lib.removeElement(geo._dragCover);
geo._dragCover = null;
}

function createGeoZoom(geo, geoLayout) {
var projection = geo.projection;
Expand Down Expand Up @@ -74,7 +92,7 @@ function zoomScoped(geo, projection) {
var zoom = initZoom(geo, projection);

function handleZoomstart() {
d3.select(this).style(zoomstartStyle);
startDragStyle(geo, this);
}

function handleZoom() {
Expand All @@ -97,7 +115,7 @@ function zoomScoped(geo, projection) {
}

function handleZoomend() {
d3.select(this).style(zoomendStyle);
endDragStyle(geo, this);
sync(geo, projection, syncCb);
}

Expand Down Expand Up @@ -127,7 +145,7 @@ function zoomNonClipped(geo, projection) {
}

function handleZoomstart() {
d3.select(this).style(zoomstartStyle);
startDragStyle(geo, this);

// Fallback to bbox center when there's no source event
// (e.g. synthetic zoom.event dispatched on initial render
Expand Down Expand Up @@ -176,7 +194,7 @@ function zoomNonClipped(geo, projection) {
}

function handleZoomend() {
d3.select(this).style(zoomendStyle);
endDragStyle(geo, this);
if (didZoom) sync(geo, projection, syncCb);
}

Expand Down Expand Up @@ -206,7 +224,7 @@ function zoomClipped(geo, projection) {
var zoomPoint;

zoom.on('zoomstart', function () {
d3.select(this).style(zoomstartStyle);
startDragStyle(geo, this);

// Fallback to bbox center when there's no source event
// (e.g. synthetic zoom.event dispatched on initial render
Expand Down Expand Up @@ -262,7 +280,7 @@ function zoomClipped(geo, projection) {
zoomstarted(event.of(this, arguments));
})
.on('zoomend', function () {
d3.select(this).style(zoomendStyle);
endDragStyle(geo, this);
zoomOn.call(zoom, 'zoom', null);
zoomended(event.of(this, arguments));
sync(geo, projection, syncCb);
Expand Down
9 changes: 9 additions & 0 deletions src/plots/gl3d/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,15 @@ exports.cleanId = function cleanId(id) {
return SCENE + sceneNum;
};

exports.updateCursor = function(gd) {
var fullLayout = gd._fullLayout;
var subplotIds = fullLayout._subplots[GL3D];

for(var i = 0; i < subplotIds.length; i++) {
fullLayout[subplotIds[i]]._scene.updateCursor();
}
};

exports.updateFx = function(gd) {
var fullLayout = gd._fullLayout;
var subplotIds = fullLayout._subplots[GL3D];
Expand Down
18 changes: 18 additions & 0 deletions src/plots/gl3d/scene.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ var createPlot = glPlot3d.createScene;

var getContext = require('webgl-context');
var passiveSupported = require('has-passive-events');
var d3 = require('@plotly/d3');

var Registry = require('../../registry');
var Lib = require('../../lib');
Expand All @@ -16,6 +17,9 @@ var Fx = require('../../components/fx');

var str2RGBAarray = require('../../lib/str2rgbarray');
var showNoWebGlMsg = require('../../lib/show_no_webgl_msg');
var setCursor = require('../../lib/setcursor');
var dragCursor3d = require('../../components/dragelement/helpers').dragCursor3d;
var modifierKeys = require('../../components/dragelement/modifier_keys');

var project = require('./project');
var createAxesOptions = require('./layout/convert');
Expand Down Expand Up @@ -265,6 +269,10 @@ proto.initializeGLPlot = function() {
}
}, passiveSupported ? {passive: false} : false);

scene.glplot.canvas.addEventListener('mousemove', function(e) {
modifierKeys.update(scene.graphDiv, e);
});

scene.glplot.canvas.addEventListener('mousemove', function() {
if(scene.fullSceneLayout.dragmode === false) return;
if(scene.camera.mouseListener.buttons === 0) return;
Expand Down Expand Up @@ -1052,10 +1060,20 @@ proto.updateFx = function(dragmode, hovermode) {
}
}

scene.updateCursor();

// to put dragmode and hovermode on the same grounds from relayout
scene.fullSceneLayout.hovermode = hovermode;
};

proto.updateCursor = function() {
var scene = this;
if(scene.staticMode || !scene.camera) return;

var cursor = dragCursor3d(scene.camera.keyBindingMode, scene.graphDiv._modifierKeys);
setCursor(d3.select(scene.container), cursor);
};

function flipPixels(pixels, w, h) {
for(var i = 0, q = h - 1; i < q; ++i, --q) {
for(var j = 0; j < w; ++j) {
Expand Down
3 changes: 3 additions & 0 deletions src/plots/plots.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var BADNUM = require('../constants/numerical').BADNUM;

var axisIDs = require('./cartesian/axis_ids');
var clearOutline = require('../components/shapes/handle_outline').clearOutline;
var modifierKeys = require('../components/dragelement/modifier_keys');
var scatterAttrs = require('../traces/scatter/layout_attributes');

var animationAttrs = require('./animation_attributes');
Expand Down Expand Up @@ -1470,6 +1471,8 @@ plots.purge = function(gd) {
// remove responsive handler
Lib.clearResponsive(gd);

modifierKeys.untrack(gd);

// Cancel the pending redraw for web fonts
delete gd._fontLoadToken;

Expand Down
3 changes: 2 additions & 1 deletion src/plots/ternary/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

var Ternary = require('./ternary');
var dragCursor = require('../../components/dragelement/helpers').dragCursor;

var getSubplotCalcData = require('../../plots/get_data').getSubplotCalcData;
var counterRegex = require('../../lib').counterRegex;
Expand Down Expand Up @@ -80,5 +81,5 @@ exports.updateFx = function(gd) {
var fullLayout = gd._fullLayout;
fullLayout._ternarylayer
.selectAll('g.toplevel')
.style('cursor', fullLayout.dragmode === 'pan' ? 'move' : 'crosshair');
.style('cursor', dragCursor(fullLayout.dragmode));
};
2 changes: 1 addition & 1 deletion src/plots/ternary/ternary.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ proto.makeFramework = function(fullLayout) {
proto.updateFx = function(fullLayout) {
fullLayout._ternarylayer
.selectAll('g.toplevel')
.style('cursor', fullLayout.dragmode === 'pan' ? 'move' : 'crosshair');
.style('cursor', dragHelpers.dragCursor(fullLayout.dragmode));
};

proto.updateLayers = function(ternaryLayout) {
Expand Down
Loading
Loading