Files

143 lines
4.6 KiB
TypeScript

import { css, cx } from '@emotion/css';
import { Global } from '@emotion/react';
import Slider from 'rc-slider';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { FieldConfigEditorProps, GrafanaTheme2, SliderFieldConfigSettings } from '@grafana/data';
import { useTheme2 } from '@grafana/ui';
import { getStyles } from '@grafana/ui/src/components/Slider/styles';
import { NumberInput } from './NumberInput';
type Props = FieldConfigEditorProps<number, SliderFieldConfigSettings>;
export const SliderValueEditor = ({ value, onChange, item }: Props) => {
// Input reference
const inputRef = useRef<HTMLSpanElement>(null);
// Settings
const { settings } = item;
const min = settings?.min || 0;
const max = settings?.max || 100;
const step = settings?.step;
const marks = settings?.marks || { [min]: min, [max]: max };
const included = settings?.included;
const ariaLabelForHandle = settings?.ariaLabelForHandle;
// Core slider specific parameters and state
const inputWidthDefault = 75;
const isHorizontal = true;
const theme = useTheme2();
const [sliderValue, setSliderValue] = useState<number>(value ?? min);
const [inputWidth, setInputWidth] = useState<number>(inputWidthDefault);
// Check for a difference between prop value and internal state
useEffect(() => {
if (value != null && value !== sliderValue) {
setSliderValue(value);
}
}, [value, sliderValue]);
// Using input font and expected maximum number of digits, set input width
useEffect(() => {
const inputElement = getComputedStyle(inputRef.current!);
const fontWeight = inputElement.getPropertyValue('font-weight') || 'normal';
const fontSize = inputElement.getPropertyValue('font-size') || '16px';
const fontFamily = inputElement.getPropertyValue('font-family') || 'Arial';
const wideNumericalCharacter = '0';
const marginDigits = 4; // extra digits to account for things like negative, exponential, and controls
const inputPadding = 8; // TODO: base this on input styling
const maxDigits =
Math.max((max + (step || 0)).toString().length, (max - (step || 0)).toString().length) + marginDigits;
const refString = wideNumericalCharacter.repeat(maxDigits);
const calculatedTextWidth = getTextWidth(refString, `${fontWeight} ${fontSize} ${fontFamily}`);
if (calculatedTextWidth) {
setInputWidth(calculatedTextWidth + inputPadding * 2);
}
}, [max, step]);
const onSliderChange = useCallback(
(v: number | number[]) => {
const value = typeof v === 'number' ? v : v[0];
setSliderValue(value);
if (onChange) {
onChange(value);
}
},
[setSliderValue, onChange]
);
const onSliderInputChange = useCallback(
(value?: number) => {
let v = value;
if (Number.isNaN(v) || !v) {
v = 0;
}
setSliderValue(v);
if (onChange) {
onChange(v);
}
},
[onChange]
);
// Styles
const styles = getStyles(theme, isHorizontal, Boolean(marks));
const stylesSlider = getStylesSlider(theme, inputWidth);
const sliderInputClassNames = !isHorizontal ? [styles.sliderInputVertical] : [];
return (
<div className={cx(styles.container, styles.slider)}>
{/** Slider tooltip's parent component is body and therefore we need Global component to do css overrides for it. */}
<Global styles={styles.slider} />
<label className={cx(styles.sliderInput, ...sliderInputClassNames)}>
<Slider
min={min}
max={max}
step={step}
defaultValue={value}
value={sliderValue}
onChange={onSliderChange}
vertical={!isHorizontal}
reverse={false}
ariaLabelForHandle={ariaLabelForHandle}
marks={marks}
included={included}
/>
<span className={stylesSlider.numberInputWrapper} ref={inputRef}>
<NumberInput value={sliderValue} onChange={onSliderInputChange} max={max} min={min} step={step} />
</span>
</label>
</div>
);
};
// Calculate width of string with given font
function getTextWidth(text: string, font: string): number | null {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (context) {
context.font = font;
const metrics = context.measureText(text);
return metrics.width;
}
return null;
}
const getStylesSlider = (theme: GrafanaTheme2, width: number) => {
return {
numberInputWrapper: css`
margin-left: 10px;
max-height: 32px;
max-width: ${width}px;
min-width: ${width}px;
overflow: visible;
width: 100%;
`,
};
};