mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Slider: Enforce numeric constraints and styling within the text input (#50905)
* Add NumberInput to core slider * Change opacity interpretation To be consistent with other layers, and CSS, opacity of 0 is fully transparent and an opacity of 1 is fully opaque. * Add state management for slider. * Ensure number input step matches slider * Style input width based on expected digits * Improve styling for number input validation error
This commit is contained in:
parent
1976edaffc
commit
6dc119ef56
@ -126,7 +126,12 @@ export class NumberInput extends PureComponent<Props, State> {
|
|||||||
range = `> ${max}`;
|
range = `> ${max}`;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Field invalid={inputCorrected} error={`Value out of range ${range} `}>
|
<Field
|
||||||
|
invalid={inputCorrected}
|
||||||
|
error={`Out of range ${range}`}
|
||||||
|
validationMessageHorizontalOverflow={true}
|
||||||
|
style={{ direction: 'rtl' }}
|
||||||
|
>
|
||||||
{this.renderInput()}
|
{this.renderInput()}
|
||||||
</Field>
|
</Field>
|
||||||
);
|
);
|
||||||
|
@ -1,26 +1,144 @@
|
|||||||
import React from 'react';
|
import { css, cx } from '@emotion/css';
|
||||||
|
import { Global } from '@emotion/react';
|
||||||
|
import SliderComponent from 'rc-slider';
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { FieldConfigEditorProps, SliderFieldConfigSettings } from '@grafana/data';
|
import { FieldConfigEditorProps, GrafanaTheme2, SliderFieldConfigSettings } from '@grafana/data';
|
||||||
import { Slider } from '@grafana/ui';
|
import { useTheme2 } from '@grafana/ui';
|
||||||
|
import { getStyles } from '@grafana/ui/src/components/Slider/styles';
|
||||||
|
|
||||||
|
import { NumberInput } from './NumberInput';
|
||||||
|
|
||||||
export const SliderValueEditor: React.FC<FieldConfigEditorProps<number, SliderFieldConfigSettings>> = ({
|
export const SliderValueEditor: React.FC<FieldConfigEditorProps<number, SliderFieldConfigSettings>> = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
item,
|
item,
|
||||||
}) => {
|
}) => {
|
||||||
|
// Input reference
|
||||||
|
const inputRef = useRef<HTMLSpanElement>(null);
|
||||||
|
|
||||||
|
// Settings
|
||||||
const { settings } = item;
|
const { settings } = item;
|
||||||
const initialValue = typeof value === 'number' ? value : typeof value === 'string' ? +value : 0;
|
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 SliderWithTooltip = SliderComponent;
|
||||||
|
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) => {
|
||||||
|
setSliderValue(v);
|
||||||
|
|
||||||
|
if (onChange) {
|
||||||
|
onChange(v);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[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 (
|
return (
|
||||||
<Slider
|
<div className={cx(styles.container, styles.slider)}>
|
||||||
value={initialValue}
|
{/** Slider tooltip's parent component is body and therefore we need Global component to do css overrides for it. */}
|
||||||
min={settings?.min || 0}
|
<Global styles={styles.slider} />
|
||||||
max={settings?.max || 100}
|
<label className={cx(styles.sliderInput, ...sliderInputClassNames)}>
|
||||||
step={settings?.step}
|
<SliderWithTooltip
|
||||||
marks={settings?.marks}
|
min={min}
|
||||||
included={settings?.included}
|
max={max}
|
||||||
onChange={onChange}
|
step={step}
|
||||||
ariaLabelForHandle={settings?.ariaLabelForHandle}
|
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%;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -515,7 +515,7 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
const handler = await item.create(map, options, this.props.eventBus, config.theme2);
|
const handler = await item.create(map, options, this.props.eventBus, config.theme2);
|
||||||
const layer = handler.init();
|
const layer = handler.init();
|
||||||
if (options.opacity != null) {
|
if (options.opacity != null) {
|
||||||
layer.setOpacity(1 - options.opacity);
|
layer.setOpacity(options.opacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.name) {
|
if (!options.name) {
|
||||||
|
Loading…
Reference in New Issue
Block a user