From ba543343c4ddd529b4988e31ae8c69b9b44fe3e2 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Tue, 19 Jul 2022 08:09:09 +0300 Subject: [PATCH] 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 --- .../src/themes/colorManipulator.test.ts | 8 ++ .../src/themes/colorManipulator.ts | 11 +++ .../src/components/ColorPicker/ColorInput.tsx | 91 +++++++------------ .../components/ColorPicker/ColorPicker.mdx | 7 ++ .../ColorPicker/ColorPicker.story.tsx | 13 ++- .../ColorPicker/ColorPickerInput.tsx | 81 +++++++++++++++++ .../ColorPicker/SpectrumPalette.tsx | 6 +- packages/grafana-ui/src/components/index.ts | 1 + 8 files changed, 156 insertions(+), 62 deletions(-) create mode 100644 packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.tsx diff --git a/packages/grafana-data/src/themes/colorManipulator.test.ts b/packages/grafana-data/src/themes/colorManipulator.test.ts index f4ca0652065..4f65f50c4d5 100644 --- a/packages/grafana-data/src/themes/colorManipulator.test.ts +++ b/packages/grafana-data/src/themes/colorManipulator.test.ts @@ -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)'); + }); + }); }); diff --git a/packages/grafana-data/src/themes/colorManipulator.ts b/packages/grafana-data/src/themes/colorManipulator.ts index b74c745e075..4bc4f8c3f3d 100644 --- a/packages/grafana-data/src/themes/colorManipulator.ts +++ b/packages/grafana-data/src/themes/colorManipulator.ts @@ -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 diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorInput.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorInput.tsx index 5877ba3f959..354ee19bf9b 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorInput.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorInput.tsx @@ -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 {} -interface ColorInputProps extends ColorPickerProps { - style?: React.CSSProperties; - className?: string; -} +const ColorInput = forwardRef(({ 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 { - 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) => { + const onChangeColor = (event: React.SyntheticEvent) => { 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 ( - } - /> - ); - } -} + return ( + } + ref={ref} + /> + ); +}); + +ColorInput.displayName = 'ColorInput'; export default ColorInput; diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.mdx b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.mdx index 6f1b631c526..cb45dbedc20 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.mdx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.mdx @@ -1,5 +1,6 @@ import { Meta, Props } from '@storybook/addon-docs/blocks'; import { ColorPicker } from './ColorPicker'; +import { ColorPickerInput } from './ColorPickerInput'; @@ -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. + diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx index 200154f0ff0..d582bbcd2fd 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx @@ -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 = ({ enableNamedColors }) => ); }; + +export const Input: Story = () => { + return ( + + {(value, onChange) => { + return ; + }} + + ); +}; diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.tsx new file mode 100644 index 00000000000..fd26ab1c8c5 --- /dev/null +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.tsx @@ -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 { + value?: string; + onChange: (color: string) => void; + /** Format for returning the color in onChange callback, defaults to 'rgb' */ + returnColorAs?: 'rgb' | 'hex'; +} + +export const ColorPickerInput = forwardRef( + ({ 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 ( + setIsOpen(false)}> +
+ {isOpen && ( + + )} +
setIsOpen(true)}> + +
+
+
+ ); + } +); + +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; + `, + }; +}; diff --git a/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx b/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx index 7006a1e8636..f477c3d41b2 100644 --- a/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx @@ -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 = ({ color, return (
- +
); }; -const getStyles = (theme: GrafanaTheme2) => ({ +export const getStyles = (theme: GrafanaTheme2) => ({ wrapper: css` flex-grow: 1; `, diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 7d5605ebc2c..bf6931b8a3d 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -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';