mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Grafana/UI: Add ColorPickerInput component (#52222)
* ColorPickerInput: Setup componnet * ColorPickerInput: Add onChange callback * ColorPickerInput: Conver ColorInput to functional component * ColorPickerInput: Extend Input props * ColorPickerInput: Upate value prop * ColorPickerInput: Allow customising color format for onChange * ColorPickerInput: Add docs * ColorPickerInput: Update docs * ColorPickerInput: Memoize debounced callback * ColorPickerInput: Add tests and use theme for spacing * Apply styles from SpectrumPalette * Cleanup
This commit is contained in:
parent
9a72ebcd99
commit
ba543343c4
@ -10,6 +10,7 @@ import {
|
||||
getContrastRatio,
|
||||
getLuminance,
|
||||
lighten,
|
||||
asRgbString,
|
||||
} from './colorManipulator';
|
||||
|
||||
describe('utils/colorManipulator', () => {
|
||||
@ -415,4 +416,11 @@ describe('utils/colorManipulator', () => {
|
||||
expect(lighten('color(display-p3 1 0 0)', 0)).toEqual('color(display-p3 1 0 0)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('asRgbString', () => {
|
||||
it('should convert hex color to rgb', () => {
|
||||
expect(asRgbString('#FFFFFF')).toEqual('rgb(255, 255, 255)');
|
||||
expect(asRgbString('#000000')).toEqual('rgb(0, 0, 0)');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -81,6 +81,17 @@ export function asHexString(color: string): string {
|
||||
return tColor.getAlpha() === 1 ? tColor.toHexString() : tColor.toHex8String();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a color to rgb string
|
||||
*/
|
||||
export function asRgbString(color: string) {
|
||||
if (color.startsWith('rgb')) {
|
||||
return color;
|
||||
}
|
||||
|
||||
return tinycolor(color).toRgbString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a color from hsl format to rgb format.
|
||||
* @param color - HSL color values
|
||||
|
@ -1,87 +1,62 @@
|
||||
import { cx, css } from '@emotion/css';
|
||||
import { debounce } from 'lodash';
|
||||
import React from 'react';
|
||||
import React, { forwardRef, useState, useEffect, useMemo } from 'react';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../themes';
|
||||
import { Input } from '../Input/Input';
|
||||
import { Input, Props as InputProps } from '../Input/Input';
|
||||
|
||||
import { ColorPickerProps } from './ColorPickerPopover';
|
||||
|
||||
interface ColorInputState {
|
||||
previousColor: string;
|
||||
value: string;
|
||||
}
|
||||
interface ColorInputProps extends ColorPickerProps, Omit<InputProps, 'color' | 'onChange'> {}
|
||||
|
||||
interface ColorInputProps extends ColorPickerProps {
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
}
|
||||
const ColorInput = forwardRef<HTMLInputElement, ColorInputProps>(({ color, onChange, ...inputProps }, ref) => {
|
||||
const [value, setValue] = useState(color);
|
||||
const [previousColor, setPreviousColor] = useState(color);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const updateColor = useMemo(() => debounce(onChange, 100), []);
|
||||
|
||||
class ColorInput extends React.PureComponent<ColorInputProps, ColorInputState> {
|
||||
constructor(props: ColorInputProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
previousColor: props.color,
|
||||
value: props.color,
|
||||
};
|
||||
|
||||
this.updateColor = debounce(this.updateColor, 100);
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: ColorPickerProps, state: ColorInputState) {
|
||||
const newColor = tinycolor(props.color);
|
||||
if (newColor.isValid() && props.color !== state.previousColor) {
|
||||
return {
|
||||
...state,
|
||||
previousColor: props.color,
|
||||
value: newColor.toString(),
|
||||
};
|
||||
useEffect(() => {
|
||||
const newColor = tinycolor(color);
|
||||
if (newColor.isValid() && color !== previousColor) {
|
||||
setValue(newColor.toString());
|
||||
setPreviousColor(color);
|
||||
}
|
||||
}, [color, previousColor]);
|
||||
|
||||
return state;
|
||||
}
|
||||
updateColor = (color: string) => {
|
||||
this.props.onChange(color);
|
||||
};
|
||||
|
||||
onChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
const onChangeColor = (event: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
const newColor = tinycolor(event.currentTarget.value);
|
||||
|
||||
this.setState({
|
||||
value: event.currentTarget.value,
|
||||
});
|
||||
setValue(event.currentTarget.value);
|
||||
|
||||
if (newColor.isValid()) {
|
||||
this.updateColor(newColor.toString());
|
||||
updateColor(newColor.toString());
|
||||
}
|
||||
};
|
||||
|
||||
onBlur = () => {
|
||||
const newColor = tinycolor(this.state.value);
|
||||
const onBlur = () => {
|
||||
const newColor = tinycolor(value);
|
||||
|
||||
if (!newColor.isValid()) {
|
||||
this.setState({
|
||||
value: this.props.color,
|
||||
});
|
||||
setValue(color);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { value } = this.state;
|
||||
return (
|
||||
<Input
|
||||
className={this.props.className}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
onBlur={this.onBlur}
|
||||
addonBefore={<ColorPreview color={this.props.color} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Input
|
||||
{...inputProps}
|
||||
value={value}
|
||||
onChange={onChangeColor}
|
||||
onBlur={onBlur}
|
||||
addonBefore={<ColorPreview color={color} />}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
ColorInput.displayName = 'ColorInput';
|
||||
|
||||
export default ColorInput;
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Meta, Props } from '@storybook/addon-docs/blocks';
|
||||
import { ColorPicker } from './ColorPicker';
|
||||
import { ColorPickerInput } from './ColorPickerInput';
|
||||
|
||||
<Meta title="Pickers and Editors/ColorPicker/ColorPicker" />
|
||||
|
||||
@ -11,4 +12,10 @@ The `Popover` is a tabbed view where you can switch between `Palettes`. The `Nam
|
||||
|
||||
The `Pickers` are single circular color fields that show the currently picked color. On click, they open the `Popover`.
|
||||
|
||||
## ColorPickerInput
|
||||
|
||||
Color picker component, modified to be used inside forms. Supports all usual input props. Allows manually typing in color value as well as selecting it from the popover.
|
||||
|
||||
The format in which the color is returned to the `onChange` callback can be customised via `returnColorAs` prop.
|
||||
|
||||
<Props of={ColorPicker} />
|
||||
|
@ -9,12 +9,13 @@ import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
|
||||
|
||||
import mdx from './ColorPicker.mdx';
|
||||
import { ColorPickerInput, ColorPickerInputProps } from './ColorPickerInput';
|
||||
import { ColorPickerProps } from './ColorPickerPopover';
|
||||
|
||||
export default {
|
||||
title: 'Pickers and Editors/ColorPicker',
|
||||
component: ColorPicker,
|
||||
subcomponents: { SeriesColorPicker },
|
||||
subcomponents: { SeriesColorPicker, ColorPickerInput },
|
||||
decorators: [withCenteredStory],
|
||||
parameters: {
|
||||
docs: {
|
||||
@ -74,3 +75,13 @@ export const SeriesPicker: Story<ColorPickerProps> = ({ enableNamedColors }) =>
|
||||
</UseState>
|
||||
);
|
||||
};
|
||||
|
||||
export const Input: Story<ColorPickerInputProps> = () => {
|
||||
return (
|
||||
<UseState initialState="#ffffff">
|
||||
{(value, onChange) => {
|
||||
return <ColorPickerInput value={value} onChange={onChange} />;
|
||||
}}
|
||||
</UseState>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,81 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { useState, forwardRef } from 'react';
|
||||
import { RgbaStringColorPicker } from 'react-colorful';
|
||||
import { useThrottleFn } from 'react-use';
|
||||
|
||||
import { colorManipulator, GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { useStyles2, useTheme2 } from '../../themes';
|
||||
import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper';
|
||||
import { Props as InputProps } from '../Input/Input';
|
||||
|
||||
import ColorInput from './ColorInput';
|
||||
import { getStyles as getPaletteStyles } from './SpectrumPalette';
|
||||
|
||||
export interface ColorPickerInputProps extends Omit<InputProps, 'value' | 'onChange'> {
|
||||
value?: string;
|
||||
onChange: (color: string) => void;
|
||||
/** Format for returning the color in onChange callback, defaults to 'rgb' */
|
||||
returnColorAs?: 'rgb' | 'hex';
|
||||
}
|
||||
|
||||
export const ColorPickerInput = forwardRef<HTMLInputElement, ColorPickerInputProps>(
|
||||
({ value = '', onChange, returnColorAs = 'rgb', ...inputProps }, ref) => {
|
||||
const [currentColor, setColor] = useState(value);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const theme = useTheme2();
|
||||
const styles = useStyles2(getStyles);
|
||||
const paletteStyles = useStyles2(getPaletteStyles);
|
||||
|
||||
useThrottleFn(
|
||||
(c) => {
|
||||
const color = theme.visualization.getColorByName(c);
|
||||
if (returnColorAs === 'rgb') {
|
||||
onChange(colorManipulator.asRgbString(color));
|
||||
} else {
|
||||
onChange(colorManipulator.asHexString(color));
|
||||
}
|
||||
},
|
||||
500,
|
||||
[currentColor]
|
||||
);
|
||||
|
||||
return (
|
||||
<ClickOutsideWrapper onClick={() => setIsOpen(false)}>
|
||||
<div className={styles.wrapper}>
|
||||
{isOpen && (
|
||||
<RgbaStringColorPicker
|
||||
color={currentColor}
|
||||
onChange={setColor}
|
||||
className={cx(paletteStyles.root, styles.picker)}
|
||||
/>
|
||||
)}
|
||||
<div onClick={() => setIsOpen(true)}>
|
||||
<ColorInput {...inputProps} theme={theme} color={currentColor} onChange={setColor} ref={ref} />
|
||||
</div>
|
||||
</div>
|
||||
</ClickOutsideWrapper>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ColorPickerInput.displayName = 'ColorPickerInput';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
wrapper: css`
|
||||
position: relative;
|
||||
`,
|
||||
picker: css`
|
||||
&.react-colorful {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 11;
|
||||
bottom: 36px;
|
||||
}
|
||||
`,
|
||||
inner: css`
|
||||
position: absolute;
|
||||
`,
|
||||
};
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { RgbaStringColorPicker } from 'react-colorful';
|
||||
import { useThrottleFn } from 'react-use';
|
||||
@ -37,13 +37,13 @@ const SpectrumPalette: React.FunctionComponent<SpectrumPaletteProps> = ({ color,
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<RgbaStringColorPicker className={cx(styles.root)} color={rgbaString} onChange={setColor} />
|
||||
<RgbaStringColorPicker className={styles.root} color={rgbaString} onChange={setColor} />
|
||||
<ColorInput theme={theme} color={rgbaString} onChange={setColor} className={styles.colorInput} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
export const getStyles = (theme: GrafanaTheme2) => ({
|
||||
wrapper: css`
|
||||
flex-grow: 1;
|
||||
`,
|
||||
|
@ -24,6 +24,7 @@ export { ButtonCascader } from './ButtonCascader/ButtonCascader';
|
||||
|
||||
export { LoadingPlaceholder, LoadingPlaceholderProps } from './LoadingPlaceholder/LoadingPlaceholder';
|
||||
export { ColorPicker, SeriesColorPicker } from './ColorPicker/ColorPicker';
|
||||
export { ColorPickerInput } from './ColorPicker/ColorPickerInput';
|
||||
export { SeriesColorPickerPopover, SeriesColorPickerPopoverWithTheme } from './ColorPicker/SeriesColorPickerPopover';
|
||||
export { EmptySearchResult } from './EmptySearchResult/EmptySearchResult';
|
||||
export { UnitPicker } from './UnitPicker/UnitPicker';
|
||||
|
Loading…
Reference in New Issue
Block a user