From 243f1fc64b1c1fa5ab19100736b085d8fc2ca426 Mon Sep 17 00:00:00 2001 From: Jesse David Peterson Date: Mon, 10 Nov 2025 11:26:00 -0400 Subject: [PATCH] Timeseries: Change mouse cursors to indicate active x-axis and y-axis zoom interactions (#113465) * feat(panel-zoom): change mouse cursor when zooming x-axis or y-axis * test(panel-zoom): browser test mouse cursor change interactions * fix(mouse-cursor-styles): no need for important --- .../timeseries-mouse-zoom-interaction.spec.ts | 93 +++++++++++++++++++ .../uPlot/plugins/TooltipPlugin2.tsx | 24 +++++ .../src/themes/GlobalStyles/uPlot.ts | 4 + 3 files changed, 121 insertions(+) create mode 100644 e2e-playwright/panels-suite/timeseries-mouse-zoom-interaction.spec.ts diff --git a/e2e-playwright/panels-suite/timeseries-mouse-zoom-interaction.spec.ts b/e2e-playwright/panels-suite/timeseries-mouse-zoom-interaction.spec.ts new file mode 100644 index 00000000000..c881151f656 --- /dev/null +++ b/e2e-playwright/panels-suite/timeseries-mouse-zoom-interaction.spec.ts @@ -0,0 +1,93 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_UID = 'TkZXxlNG3'; + +test.describe('Panels test: Timeseries zoom interaction', { tag: ['@panels', '@timeseries'] }, () => { + test('shows zoom cursor during x-axis drag-to-zoom interaction', async ({ gotoDashboardPage, page }) => { + await gotoDashboardPage({ + uid: DASHBOARD_UID, + }); + + const uplotOverlay = page.locator('.u-over').first(); + await expect(uplotOverlay, 'uplot overlay is visible').toBeVisible(); + + const bbox = await uplotOverlay.boundingBox(); + expect(bbox, 'uplot overlay has dimensions').toBeDefined(); + + await expect(uplotOverlay, 'no zoom cursor initially').not.toHaveClass(/zoom-drag/); + + await page.mouse.move(bbox!.x + 100, bbox!.y + 50); + await page.mouse.down(); + + await expect(uplotOverlay, 'zoom cursor appears on mousedown').toHaveClass(/zoom-drag/); + + await page.mouse.move(bbox!.x + 300, bbox!.y + 50); + + await expect(uplotOverlay, 'zoom cursor persists during drag').toHaveClass(/zoom-drag/); + + await page.mouse.up(); + + await expect(uplotOverlay, 'zoom cursor removed after mouseup').not.toHaveClass(/zoom-drag/); + }); + + test('shows zoom cursor during y-axis drag-to-zoom interaction with Shift key', async ({ + gotoDashboardPage, + page, + }) => { + await gotoDashboardPage({ + uid: DASHBOARD_UID, + }); + + const uplotOverlay = page.locator('.u-over').first(); + await expect(uplotOverlay, 'uplot overlay is visible').toBeVisible(); + + const bbox = await uplotOverlay.boundingBox(); + expect(bbox, 'uplot overlay has dimensions').toBeDefined(); + + await expect(uplotOverlay, 'no zoom cursor initially').not.toHaveClass(/zoom-drag/); + + await page.keyboard.down('Shift'); + await page.mouse.move(bbox!.x + 100, bbox!.y + 50); + await page.mouse.down(); + + await expect(uplotOverlay, 'zoom cursor appears on mousedown with Shift').toHaveClass(/zoom-drag/); + + await page.mouse.move(bbox!.x + 100, bbox!.y + 150); + + await expect(uplotOverlay, 'zoom cursor persists during drag with Shift').toHaveClass(/zoom-drag/); + + await page.mouse.up(); + await page.keyboard.up('Shift'); + + await expect(uplotOverlay, 'zoom cursor removed after mouseup').not.toHaveClass(/zoom-drag/); + }); + + test('does not show zoom cursor when modifier keys are pressed', async ({ gotoDashboardPage, page }) => { + await gotoDashboardPage({ + uid: DASHBOARD_UID, + }); + + const uplotOverlay = page.locator('.u-over').first(); + await expect(uplotOverlay, 'uplot overlay is visible').toBeVisible(); + + const bbox = await uplotOverlay.boundingBox(); + + await page.keyboard.down('Control'); + await page.mouse.move(bbox!.x + 100, bbox!.y + 50); + await page.mouse.down(); + + await expect(uplotOverlay, 'no zoom cursor with Ctrl key').not.toHaveClass(/zoom-drag/); + + await page.mouse.up(); + await page.keyboard.up('Control'); + + await page.keyboard.down('Meta'); + await page.mouse.move(bbox!.x + 100, bbox!.y + 50); + await page.mouse.down(); + + await expect(uplotOverlay, 'no zoom cursor with Meta key').not.toHaveClass(/zoom-drag/); + + await page.mouse.up(); + await page.keyboard.up('Meta'); + }); +}); diff --git a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx index 17147e8661b..606839aacad 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx @@ -340,6 +340,30 @@ export const TooltipPlugin2 = ({ ); } + // add zoom-in cursor during drag-to-zoom interaction + if (queryZoom != null || clientZoom) { + u.over.addEventListener( + 'mousedown', + (e) => { + if (!maybeZoomAction(e)) { + return; + } + + if (e.button === 0) { + u.over.classList.add('zoom-drag'); + + let onUp = () => { + u.over.classList.remove('zoom-drag'); + document.removeEventListener('mouseup', onUp, true); + }; + + document.addEventListener('mouseup', onUp, true); + } + }, + true + ); + } + // this handles pinning, 0-width range selection, and one-click u.over.addEventListener('click', (e) => { if (e.target === u.over) { diff --git a/packages/grafana-ui/src/themes/GlobalStyles/uPlot.ts b/packages/grafana-ui/src/themes/GlobalStyles/uPlot.ts index 3637999adee..ae0e066bbfc 100644 --- a/packages/grafana-ui/src/themes/GlobalStyles/uPlot.ts +++ b/packages/grafana-ui/src/themes/GlobalStyles/uPlot.ts @@ -12,6 +12,10 @@ export function getUplotStyles(theme: GrafanaTheme2) { background: 'rgba(120, 120, 130, 0.2)', }, + '.u-over.zoom-drag': { + cursor: 'zoom-in', + }, + '.u-hz .u-cursor-x, .u-vt .u-cursor-y': { borderRight: '1px dashed rgba(120, 120, 130, 0.5)', },