Skip to content

Commit

Permalink
feat(dashboards): Allow cancelling a zoom selection (#76626)
Browse files Browse the repository at this point in the history
Closes #44723. Pressing the
"Escape" key while making a drag selection on a chart will cancel the
selection! Makes it possible to change your mind halfway through.
  • Loading branch information
gggritso authored Aug 28, 2024
1 parent f115699 commit 4d5f8cf
Showing 1 changed file with 64 additions and 0 deletions.
64 changes: 64 additions & 0 deletions static/app/components/charts/chartZoom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {Component} from 'react';
import type {InjectedRouter} from 'react-router';
import type {
DataZoomComponentOption,
ECharts,
InsideDataZoomComponentOption,
ToolboxComponentOption,
XAXisComponentOption,
Expand Down Expand Up @@ -100,6 +101,15 @@ class ChartZoom extends Component<Props> {
this.saveCurrentPeriod(this.props);
}

componentWillUnmount(): void {
document.body.removeEventListener('keydown', this.handleKeyDown);
document.body.removeEventListener('mouseup', this.handleMouseUp);
this.$chart?.removeEventListener('mousedown', this.handleMouseDown);
}

chart?: ECharts;
$chart?: HTMLDivElement;
isCancellingZoom?: boolean;
history: Period[];
currentPeriod?: Period;
zooming: (() => void) | null = null;
Expand Down Expand Up @@ -186,6 +196,35 @@ class ChartZoom extends Component<Props> {
*/
handleChartReady = chart => {
this.props.onChartReady?.(chart);

// The DOM element is also available via chart._dom but TypeScript hates that, since
// _dom is technically private. Instead, use `querySelector` to get the element
this.chart = chart;
this.$chart = document.querySelector(
`div[_echarts_instance_="${chart.id}"]`
) as HTMLDivElement;

this.$chart.addEventListener('mousedown', this.handleMouseDown);
};

handleKeyDown = evt => {
if (!this.chart) {
return;
}

// This handler only exists if mouse down was caught inside the chart.
// Therefore, no need to check any other state.
if (evt.key === 'Escape') {
evt.stopPropagation();
// Mark the component as currently cancelling a zoom selection. This allows
// us to prevent "restore" handlers from running
this.isCancellingZoom = true;

// "restore" removes the current chart zoom selection
this.chart.dispatchAction({
type: 'restore',
});
}
};

/**
Expand All @@ -194,6 +233,15 @@ class ChartZoom extends Component<Props> {
* Updates URL state to reflect initial params
*/
handleZoomRestore = (evt, chart) => {
if (this.isCancellingZoom) {
// If this restore is caused by a zoom cancel, do not run handlers!
// The regular handler restores to the earliest point in the zoom history
// and we do not want that. We want to cancel the selection and do nothing
// else. Reset `isCancellingZoom` here in case the dispatch was async
this.isCancellingZoom = false;
return;
}

if (!this.history.length) {
return;
}
Expand All @@ -206,6 +254,22 @@ class ChartZoom extends Component<Props> {
this.props.onRestore?.(evt, chart);
};

handleMouseDown = () => {
// Register `mouseup` and `keydown` listeners on mouse down
// This ensures that there is only one live listener at a time
// regardless of how many charts are rendered. NOTE: It's
// important to set `useCapture: true` in the `"keydown"` handler
// otherwise the Escape will close whatever modal or panel the
// chart is in. Those elements register their handlers _earlier_.
document.body.addEventListener('mouseup', this.handleMouseUp);
document.body.addEventListener('keydown', this.handleKeyDown, true);
};

handleMouseUp = () => {
document.body.removeEventListener('mouseup', this.handleMouseUp);
document.body.removeEventListener('keydown', this.handleKeyDown, true);
};

handleDataZoom = (evt, chart) => {
const model = chart.getModel();
const {startValue, endValue} = model._payload.batch[0];
Expand Down

0 comments on commit 4d5f8cf

Please sign in to comment.