diff --git a/packages/devextreme/js/__internal/viz/chart_components/zoom_and_pan.ts b/packages/devextreme/js/__internal/viz/chart_components/zoom_and_pan.ts index 67440eb16e50..42361753519e 100644 --- a/packages/devextreme/js/__internal/viz/chart_components/zoom_and_pan.ts +++ b/packages/devextreme/js/__internal/viz/chart_components/zoom_and_pan.ts @@ -39,6 +39,7 @@ const SCROLL_BAR_END_EVENT_NAME = `dxc-scroll-end${EVENTS_NS}`; const GESTURE_TIMEOUT = 300; const MIN_DRAG_DELTA = 5; +export const SCROLL_PREVENTION_TIMEOUT = 500; const _min = Math.min; const _max = Math.max; @@ -129,6 +130,7 @@ export default { init() { const chart = this; const renderer = this._renderer; + let lastWheelTimer: number | undefined; function getAxesCopy(zoomAndPan, actionField) { let axes = []; @@ -307,13 +309,23 @@ export default { return e.offset[coordField] - actionData.offset[coordField]; } - function preventDefaults(e) { + function setLastWheelTimer() { + clearTimeout(lastWheelTimer); + // eslint-disable-next-line no-restricted-globals + lastWheelTimer = setTimeout(() => { + lastWheelTimer = undefined; + }, SCROLL_PREVENTION_TIMEOUT) as unknown as number; + } + + function preventDefaults(e, stopChartHandler = true): void { if (e.cancelable !== false) { e.preventDefault(); e.stopPropagation(); } - chart._stopCurrentHandling(); + if (stopChartHandler) { + chart._stopCurrentHandling(); + } } const zoomAndPan = { @@ -536,12 +548,20 @@ export default { axesZoomed |= canZoom && zoomAxes(e, chart._argumentAxes, getRange, e.delta > 0, { coord: rotated ? coords.y : coords.x }, chart.getArgumentAxis()); } + const isPanningAvailable = targetAxes ? isAxisAvailablePanning(targetAxes) : zoomAndPan.panningVisualRangeEnabled(); + if (axesZoomed) { chart._requestChange(['VISUAL_RANGE']); - if (targetAxes && isAxisAvailablePanning(targetAxes) || !targetAxes && zoomAndPan.panningVisualRangeEnabled()) { + if (isPanningAvailable) { preventDefaults(e); // T249548 + setLastWheelTimer(); } } + + if ((!axesZoomed || !isPanningAvailable) && lastWheelTimer) { + preventDefaults(e, false); + setLastWheelTimer(); + } }, cleanup() { renderer.root.off(EVENTS_NS); diff --git a/packages/devextreme/testing/tests/DevExpress.viz.charts/zoomAndPan.tests.js b/packages/devextreme/testing/tests/DevExpress.viz.charts/zoomAndPan.tests.js index b3fcda5dcfbc..3a0258027f54 100644 --- a/packages/devextreme/testing/tests/DevExpress.viz.charts/zoomAndPan.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.viz.charts/zoomAndPan.tests.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import pointerMock from '../../helpers/pointerMock.js'; import 'viz/chart'; +import { SCROLL_PREVENTION_TIMEOUT } from '__internal/viz/chart_components/zoom_and_pan'; const CHART_SVG_SELECTOR = 'svg.dxc.dxc-chart'; const TOOLTIP_CLASS = 'dxc-tooltip'; @@ -3109,6 +3110,57 @@ QUnit.test('Default behavior - no prevent. On mouse wheel', function(assert) { assert.equal(this.trackerStopHandling.callCount, 0); }); +QUnit.test('On mouse wheel. Should prevent scroll page after max zoom level reached (T1314606)', function(assert) { + const preventDefault = sinon.spy(); + const stopPropagation = sinon.spy(); + const onZoomEnd = sinon.spy(); + const wholeRange = { startValue: 0, endValue: 5 }; + const chart = this.createChart({ + argumentAxis: { + visualRange: { + startValue: 0.1, + endValue: 4.9 + }, + wholeRange, + }, + zoomAndPan: { + argumentAxis: 'zoom', + allowMouseWheel: true + }, + onZoomEnd: onZoomEnd + }); + + const $root = $(chart._renderer.root.element); + + $root.trigger(new $.Event('dxmousewheel', { d: -10, pageX: 200, pageY: 250, preventDefault: preventDefault, stopPropagation: stopPropagation })); + + assert.deepEqual(onZoomEnd.getCall(0).args[0].range, wholeRange, 'chart zoomed out to wholeRange'); + assert.strictEqual(preventDefault.callCount, 1, 'after zoom e.preventDefault called'); + assert.strictEqual(stopPropagation.callCount, 1, 'after zoom e.stopPropagation called'); + assert.strictEqual(this.trackerStopHandling.callCount, 1, 'chart stopped wheel event handling'); + + this.clock.tick(SCROLL_PREVENTION_TIMEOUT / 2); + $root.trigger(new $.Event('dxmousewheel', { d: -10, pageX: 200, pageY: 250, preventDefault: preventDefault, stopPropagation: stopPropagation })); + + assert.equal(preventDefault.callCount, 2, 'e.preventDefault called'); + assert.equal(stopPropagation.callCount, 2, 'e.stopPropagation called'); + assert.equal(this.trackerStopHandling.callCount, 1, 'chart not passed event handling in SCROLL_PREVENTION_TIMEOUT window after zoom'); + + this.clock.tick(SCROLL_PREVENTION_TIMEOUT - 10); + $root.trigger(new $.Event('dxmousewheel', { d: -10, pageX: 200, pageY: 250, preventDefault: preventDefault, stopPropagation: stopPropagation })); + + assert.equal(preventDefault.callCount, 3, 'e.preventDefault called'); + assert.equal(stopPropagation.callCount, 3, 'e.stopPropagation called'); + assert.equal(this.trackerStopHandling.callCount, 1, 'chart not passed event handling in SCROLL_PREVENTION_TIMEOUT window after last wheel event on chart'); + + this.clock.tick(SCROLL_PREVENTION_TIMEOUT + 10); + $root.trigger(new $.Event('dxmousewheel', { d: -10, pageX: 200, pageY: 250, preventDefault: preventDefault, stopPropagation: stopPropagation })); + + assert.equal(preventDefault.callCount, 3, 'chart not prevents event handling after SCROLL_PREVENTION_TIMEOUT expires, e.preventDefault not called'); + assert.equal(stopPropagation.callCount, 3, 'chart not prevent event propagation after SCROLL_PREVENTION_TIMEOUT expires, e.stopPropagation not called'); + assert.equal(this.trackerStopHandling.callCount, 1, 'chart._stopCurrentHandling not called after SCROLL_PREVENTION_TIMEOUT window'); +}); + QUnit.test('On pinch zoom', function(assert) { const preventDefault = sinon.spy(); const stopPropagation = sinon.spy();