The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/packages/grafana-ui/src/components/uPlot/plugins/KeyboardPlugin.tsx

162 lines
4.1 KiB

import { clamp } from 'lodash';
import React, { useLayoutEffect } from 'react';
import uPlot from 'uplot';
import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder';
interface KeyboardPluginProps {
config: UPlotConfigBuilder; // onkeypress, onkeyup, onkeydown (triggered by vizlayout handlers)
}
const PIXELS_PER_MS = 0.1 as const;
const SHIFT_MULTIPLIER = 2 as const;
const KNOWN_KEYS = new Set(['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'Shift', ' ']);
const initHook = (u: uPlot) => {
let vizLayoutViz: HTMLElement | null = u.root.closest('[tabindex]');
let pressedKeys = new Set<string>();
let dragStartX: number | null = null;
let keysLastHandledAt: number | null = null;
if (!vizLayoutViz) {
return;
}
const moveCursor = (dx: number, dy: number) => {
const { cursor } = u;
if (cursor.left === undefined || cursor.top === undefined) {
return;
}
const { width, height } = u.over.style;
const [maxX, maxY] = [Math.floor(parseFloat(width)), Math.floor(parseFloat(height))];
u.setCursor({
left: clamp(cursor.left + dx, 0, maxX),
top: clamp(cursor.top + dy, 0, maxY),
});
};
const handlePressedKeys = (time: number) => {
const nothingPressed = pressedKeys.size === 0;
if (nothingPressed || !u) {
keysLastHandledAt = null;
return;
}
const dt = time - (keysLastHandledAt ?? time);
const dx = dt * PIXELS_PER_MS;
let horValue = 0;
let vertValue = 0;
if (pressedKeys.has('ArrowUp')) {
vertValue -= dx;
}
if (pressedKeys.has('ArrowDown')) {
vertValue += dx;
}
if (pressedKeys.has('ArrowLeft')) {
horValue -= dx;
}
if (pressedKeys.has('ArrowRight')) {
horValue += dx;
}
if (pressedKeys.has('Shift')) {
horValue *= SHIFT_MULTIPLIER;
vertValue *= SHIFT_MULTIPLIER;
}
moveCursor(horValue, vertValue);
const { cursor } = u;
if (pressedKeys.has(' ') && cursor) {
const drawHeight = Number(u.over.style.height.slice(0, -2));
u.setSelect(
{
left: cursor.left! < dragStartX! ? cursor.left! : dragStartX!,
top: 0,
width: Math.abs(cursor.left! - (dragStartX ?? cursor.left!)),
height: drawHeight,
},
false
);
}
keysLastHandledAt = time;
window.requestAnimationFrame(handlePressedKeys);
};
vizLayoutViz.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
// Hide the cursor if the user tabs away
u.setCursor({ left: -5, top: -5 });
return;
}
if (!KNOWN_KEYS.has(e.key)) {
return;
}
e.preventDefault();
e.stopPropagation();
const newKey = !pressedKeys.has(e.key);
if (newKey) {
const initiateAnimationLoop = pressedKeys.size === 0;
pressedKeys.add(e.key);
dragStartX = e.key === ' ' && dragStartX === null ? u.cursor.left! : dragStartX;
if (initiateAnimationLoop) {
window.requestAnimationFrame(handlePressedKeys);
}
}
});
vizLayoutViz.addEventListener('keyup', (e) => {
if (!KNOWN_KEYS.has(e.key)) {
return;
}
pressedKeys.delete(e.key);
if (e.key === ' ') {
e.preventDefault();
e.stopPropagation();
// We do this so setSelect hooks get fired, zooming the plot
u.setSelect(u.select);
dragStartX = null;
}
});
vizLayoutViz.addEventListener('focus', (e) => {
// We only want to initialize the cursor if the user is using keyboard controls
if (!vizLayoutViz?.matches(':focus-visible')) {
return;
}
// Is there a more idiomatic way to do this?
const drawWidth = parseFloat(u.over.style.width);
const drawHeight = parseFloat(u.over.style.height);
u.setCursor({ left: drawWidth / 2, top: drawHeight / 2 });
});
vizLayoutViz.addEventListener('blur', (e) => {
keysLastHandledAt = null;
dragStartX = null;
pressedKeys.clear();
u.setSelect({ left: 0, top: 0, width: 0, height: 0 }, false);
});
};
/**
* @alpha
*/
export const KeyboardPlugin: React.FC<KeyboardPluginProps> = ({ config }) => {
useLayoutEffect(() => config.addHook('init', initHook), [config]);
return null;
};