diff --git a/CHANGELOG.md b/CHANGELOG.md index c55b85bcab9..7d4a4021d33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - Removed `role` attribute from `EuiImage`([#3036](https://github.com/elastic/eui/pull/3036)) - Added `prepend` and `append` ability to `EuiComboBox` single selection only ([#3003](https://github.com/elastic/eui/pull/3003)) - Added `onColumnResize` prop to `EuiDataGrid` of type `EuiDataGridOnColumnResizeHandler` that gets called when column changes it's size ([#2963](https://github.com/elastic/eui/pull/2963)) +- Added RGB format support to `EuiColorPicker` and `EuiColorStops` ([#2850](https://github.com/elastic/eui/pull/2850)) +- Added alpha channel (opacity) support to `EuiColorPicker` and `EuiColorStops` ([#2850](https://github.com/elastic/eui/pull/2850)) **Bug Fixes** diff --git a/package.json b/package.json index 089cd60a01a..22d631f7cfe 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "test-staged" ], "dependencies": { - "@types/chroma-js": "^1.4.3", + "@types/chroma-js": "^2.0.0", "@types/enzyme": "^3.1.13", "@types/lodash": "^4.14.116", "@types/numeral": "^0.0.25", diff --git a/src-docs/src/views/color_picker/alpha.js b/src-docs/src/views/color_picker/alpha.js new file mode 100644 index 00000000000..801d973fd04 --- /dev/null +++ b/src-docs/src/views/color_picker/alpha.js @@ -0,0 +1,60 @@ +import React from 'react'; + +import { EuiColorPicker, EuiFormRow } from '../../../../src/components'; +import { useColorPicker } from './utils'; + +export const Alpha = () => { + const [color, setColor, errors] = useColorPicker('#D36086'); + const [color2, setColor2, errors2] = useColorPicker('211, 96, 134'); + + const customSwatches = [ + '#54B399', + '#6092C0', + '#D36086', + '#9170B8', + '#CA8EAE', + '#54B39940', + '#6092C040', + '#D3608640', + '#9170B840', + '#CA8EAE40', + ]; + + const customSwatches2 = [ + '211, 96, 134, 0.25', + '211, 96, 134, 0.5', + '211, 96, 134, 0.75', + '211, 96, 134', + ]; + + return ( + <> + + + + + + + + + ); +}; diff --git a/src-docs/src/views/color_picker/color_picker.js b/src-docs/src/views/color_picker/color_picker.js index 5fc8b2f548a..ba1f02a34d0 100644 --- a/src-docs/src/views/color_picker/color_picker.js +++ b/src-docs/src/views/color_picker/color_picker.js @@ -1,36 +1,13 @@ -import React, { Component } from 'react'; +import React from 'react'; import { EuiColorPicker, EuiFormRow } from '../../../../src/components'; -import { isValidHex } from '../../../../src/services'; - -export class ColorPicker extends Component { - constructor(props) { - super(props); - this.state = { - color: '#D36086', - }; - } - - handleChange = value => { - this.setState({ color: value }); - }; - - render() { - const hasErrors = !isValidHex(this.state.color) && this.state.color !== ''; - - let errors; - if (hasErrors) { - errors = ['Provide a valid hex value']; - } - - return ( - - - - ); - } -} +import { useColorPicker } from './utils'; + +export const ColorPicker = () => { + const [color, setColor, errors] = useColorPicker('#D36086'); + return ( + + + + ); +}; diff --git a/src-docs/src/views/color_picker/color_picker_example.js b/src-docs/src/views/color_picker/color_picker_example.js index 90f7425a6e8..07e65a97995 100644 --- a/src-docs/src/views/color_picker/color_picker_example.js +++ b/src-docs/src/views/color_picker/color_picker_example.js @@ -63,6 +63,28 @@ const colorPickerRangeSnippet = ` `; +import { Alpha } from './alpha'; +const alphaSource = require('!!raw-loader!./alpha'); +const alphaHtml = renderToHtml(Alpha); +const alphaSnippet = ``; + +import { Formats } from './formats'; +const formatsSource = require('!!raw-loader!./formats'); +const formatsHtml = renderToHtml(Formats); +const formatsSnippet = ``; + import { CustomSwatches } from './custom_swatches'; const customSwatchesSource = require('!!raw-loader!./custom_swatches'); const customSwatchesHtml = renderToHtml(CustomSwatches); @@ -243,13 +265,13 @@ export const ColorPickerExample = { selection.

- Direct text entry will only match hexadecimal (hex) colors, and - output values only return hex values. Spatial selection involves - HSV manipulaton, which is converted to hex. + Direct text entry will match hexadecimal (hex) and RGB(a) colors, + and output will return both hex and RGBa values. Spatial selection + involves HSV manipulaton, which is converted to hex.

Swatches allow consumers to predefine preferred or suggested - choices. The swatches must also be entered in hex format. + choices. The swatches must also be entered in hex or RGBa format.

@@ -329,6 +351,58 @@ export const ColorPickerExample = { snippet: colorPickerRangeSnippet, demo: , }, + { + title: 'Format selection', + source: [ + { + type: GuideSectionTypes.JS, + code: formatsSource, + }, + { + type: GuideSectionTypes.HTML, + code: formatsHtml, + }, + ], + text: ( + <> +

+ Format selection does not limit the format of text input + the picker will allow, but instead attempts to keep consistency + during HSV selection. By default, the color picker will + automatically use the last input value format. Notice in following + the examples how hue and saturation selection behave differently. +

+

+ Swatches will always show the "as-authored" color value, + as will the value provided via the color prop. +

+ + ), + snippet: formatsSnippet, + demo: , + }, + { + title: 'Alpha channel (opacity) selection', + source: [ + { + type: GuideSectionTypes.JS, + code: alphaSource, + }, + { + type: GuideSectionTypes.HTML, + code: alphaHtml, + }, + ], + text: ( +

+ To allow color opacity via alpha channel, set the{' '} + showAlpha prop to `true`. This will also display a + range slider allowing manual opacity updates. +

+ ), + snippet: alphaSnippet, + demo: , + }, { title: 'Custom color swatches', source: [ diff --git a/src-docs/src/views/color_picker/custom_button.js b/src-docs/src/views/color_picker/custom_button.js index 57625d7af47..b5b0601b867 100644 --- a/src-docs/src/views/color_picker/custom_button.js +++ b/src-docs/src/views/color_picker/custom_button.js @@ -1,4 +1,4 @@ -import React, { Component, Fragment } from 'react'; +import React, { Fragment, useState } from 'react'; import { EuiColorPicker, @@ -8,56 +8,42 @@ import { EuiSpacer, } from '../../../../src/components'; -import { isValidHex } from '../../../../src/services'; +import { useColorPicker } from './utils'; -export class CustomButton extends Component { - constructor(props) { - super(props); - this.state = { - color: null, - }; - } - - handleChange = value => { - this.setState({ color: value }); +export const CustomButton = () => { + const [color, setColor, errors] = useColorPicker(''); + const [selectedColor, setSelectedColor] = useState(color); + const handleColorChange = (text, { hex, isValid }) => { + setColor(text, { hex, isValid }); + setSelectedColor(hex); }; - - render() { - const hasErrors = !isValidHex(this.state.color) && this.state.color !== ''; - - let errors; - if (hasErrors) { - errors = ['Provide a valid hex value']; - } - - return ( - - - - } - /> - - + return ( + + - Color this badge - + } /> - - ); - } -} + + + + Color this badge + + } + /> + + ); +}; diff --git a/src-docs/src/views/color_picker/formats.js b/src-docs/src/views/color_picker/formats.js new file mode 100644 index 00000000000..1f568a28716 --- /dev/null +++ b/src-docs/src/views/color_picker/formats.js @@ -0,0 +1,38 @@ +import React from 'react'; + +import { EuiColorPicker, EuiFormRow } from '../../../../src/components'; +import { useColorPicker } from './utils'; + +export const Formats = () => { + const [color, setColor, errors] = useColorPicker('#D36086'); + const [color2, setColor2, errors2] = useColorPicker('#D36086'); + const [color3, setColor3, errors3] = useColorPicker('211, 96, 134'); + return ( + <> + + + + + + + + + + + ); +}; diff --git a/src-docs/src/views/color_picker/inline.js b/src-docs/src/views/color_picker/inline.js index 3c7f16b1706..789adea7a6c 100644 --- a/src-docs/src/views/color_picker/inline.js +++ b/src-docs/src/views/color_picker/inline.js @@ -1,30 +1,16 @@ -import React, { Component } from 'react'; +import React from 'react'; import { EuiColorPicker } from '../../../../src/components'; -import { isValidHex } from '../../../../src/services'; +import { useColorPicker } from './utils'; -export class Inline extends Component { - constructor(props) { - super(props); - this.state = { - color: '', - }; - } - - handleChange = value => { - this.setState({ color: value }); - }; - - render() { - const hasErrors = !isValidHex(this.state.color) && this.state.color !== ''; - - return ( - - ); - } -} +export const Inline = () => { + const [color, setColor, errors] = useColorPicker('#D36086'); + return ( + + ); +}; diff --git a/src-docs/src/views/color_picker/kitchen_sink.js b/src-docs/src/views/color_picker/kitchen_sink.js index 12c7fd944d0..211e71f59ca 100644 --- a/src-docs/src/views/color_picker/kitchen_sink.js +++ b/src-docs/src/views/color_picker/kitchen_sink.js @@ -21,7 +21,10 @@ export const KitchenSink = () => { {/* DisplayToggles wrapper for Docs only */} - + // https://www.paulirish.com/2009/random-hex-color-code-snippets/ @@ -32,11 +31,16 @@ export const useColorStop = (useRandomColor = false) => { }; export const useColorPicker = (initialColor = '') => { - const [color, setColor] = useState(initialColor); + const [color, setColorValue] = useState(initialColor); + const [isValid, setIsValid] = useState(true); + const setColor = (text, { isValid }) => { + setColorValue(text); + setIsValid(isValid); + }; const errors = useMemo(() => { - const hasErrors = !isValidHex(color) && color !== ''; - return hasErrors ? ['Provide a valid hex value'] : null; - }, [color]); + const hasErrors = !isValid; + return hasErrors ? ['Provide a valid color value'] : null; + }, [isValid]); return [color, setColor, errors]; }; diff --git a/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap b/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap index d3630070891..1f83e2b4ccb 100644 --- a/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap +++ b/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap @@ -14,7 +14,7 @@ exports[`renders EuiColorPicker 1`] = ` class="euiFormControlLayout__childrenWrapper" >
@@ -80,7 +79,7 @@ exports[`renders EuiColorPicker with a color swatch when color is defined 1`] = class="euiFormControlLayout__childrenWrapper" >
@@ -157,7 +155,6 @@ exports[`renders EuiColorPicker with an empty swatch when color is "" 1`] = ` autocomplete="off" class="euiFieldText euiColorPicker__input euiFieldText--withIcon" data-test-subj="colorPickerAnchor" - maxlength="7" placeholder="Transparent" type="text" value="" @@ -222,7 +219,6 @@ exports[`renders EuiColorPicker with an empty swatch when color is null 1`] = ` autocomplete="off" class="euiFieldText euiColorPicker__input euiFieldText--withIcon" data-test-subj="colorPickerAnchor" - maxlength="7" placeholder="Transparent" type="text" value="" @@ -281,7 +277,7 @@ exports[`renders a EuiColorPicker with a prepend and append 1`] = ` class="euiFormControlLayout__childrenWrapper" >
@@ -338,6 +333,71 @@ exports[`renders a EuiColorPicker with a prepend and append 1`] = `
`; +exports[`renders a EuiColorPicker with an alpha range selector 1`] = ` +
+
+
+
+
+
+
+ +
+ + +
+
+
+
+ + +
+
+
+
+`; + exports[`renders compressed EuiColorPicker 1`] = `
@@ -418,7 +477,7 @@ exports[`renders disabled EuiColorPicker 1`] = ` class="euiFormControlLayout__childrenWrapper" >
@@ -485,7 +543,7 @@ exports[`renders fullWidth EuiColorPicker 1`] = ` class="euiFormControlLayout__childrenWrapper" >
@@ -594,7 +651,7 @@ exports[`renders inline EuiColorPicker 1`] = `
@@ -624,7 +681,7 @@ exports[`renders inline EuiColorPicker 1`] = ` aria-label="Select #6092C0 as the color" class="euiColorPickerSwatch euiColorPicker__swatchSelect" role="option" - style="background:#6092C0" + style="background:rgb(96,146,192)" type="button" />
@@ -635,7 +692,7 @@ exports[`renders inline EuiColorPicker 1`] = ` aria-label="Select #D36086 as the color" class="euiColorPickerSwatch euiColorPicker__swatchSelect" role="option" - style="background:#D36086" + style="background:rgb(211,96,134)" type="button" />
@@ -646,7 +703,7 @@ exports[`renders inline EuiColorPicker 1`] = ` aria-label="Select #9170B8 as the color" class="euiColorPickerSwatch euiColorPicker__swatchSelect" role="option" - style="background:#9170B8" + style="background:rgb(145,112,184)" type="button" />
@@ -657,7 +714,7 @@ exports[`renders inline EuiColorPicker 1`] = ` aria-label="Select #CA8EAE as the color" class="euiColorPickerSwatch euiColorPicker__swatchSelect" role="option" - style="background:#CA8EAE" + style="background:rgb(202,142,174)" type="button" />
@@ -668,7 +725,7 @@ exports[`renders inline EuiColorPicker 1`] = ` aria-label="Select #D6BF57 as the color" class="euiColorPickerSwatch euiColorPicker__swatchSelect" role="option" - style="background:#D6BF57" + style="background:rgb(214,191,87)" type="button" />
@@ -679,7 +736,7 @@ exports[`renders inline EuiColorPicker 1`] = ` aria-label="Select #B9A888 as the color" class="euiColorPickerSwatch euiColorPicker__swatchSelect" role="option" - style="background:#B9A888" + style="background:rgb(185,168,136)" type="button" />
@@ -690,7 +747,7 @@ exports[`renders inline EuiColorPicker 1`] = ` aria-label="Select #DA8B45 as the color" class="euiColorPickerSwatch euiColorPicker__swatchSelect" role="option" - style="background:#DA8B45" + style="background:rgb(218,139,69)" type="button" />
@@ -701,7 +758,7 @@ exports[`renders inline EuiColorPicker 1`] = ` aria-label="Select #AA6556 as the color" class="euiColorPickerSwatch euiColorPicker__swatchSelect" role="option" - style="background:#AA6556" + style="background:rgb(170,101,86)" type="button" />
@@ -712,7 +769,7 @@ exports[`renders inline EuiColorPicker 1`] = ` aria-label="Select #E7664C as the color" class="euiColorPickerSwatch euiColorPicker__swatchSelect" role="option" - style="background:#E7664C" + style="background:rgb(231,102,76)" type="button" />
@@ -734,7 +791,7 @@ exports[`renders readOnly EuiColorPicker 1`] = ` class="euiFormControlLayout__childrenWrapper" >
{ expect(component).toMatchSnapshot(); }); +test('renders a EuiColorPicker with an alpha range selector', () => { + const component = render( + + ); + + expect(component).toMatchSnapshot(); +}); + test('renders EuiColorPicker with an empty swatch when color is null', () => { const colorPicker = render( @@ -193,7 +206,11 @@ test('Setting a new color calls onChange', () => { expect(inputs.length).toBe(1); inputs.simulate('change', event); expect(onChange).toBeCalled(); - expect(onChange).toBeCalledWith('#000000'); + expect(onChange).toBeCalledWith('#000000', { + hex: '#000000', + isValid: true, + rgba: [0, 0, 0, 1], + }); }); test('Clicking a swatch calls onChange', () => { @@ -206,7 +223,45 @@ test('Clicking a swatch calls onChange', () => { expect(swatches.length).toBe(VISUALIZATION_COLORS.length); swatches.first().simulate('click'); expect(onChange).toBeCalled(); - expect(onChange).toBeCalledWith(VISUALIZATION_COLORS[0]); + expect(onChange).toBeCalledWith(VISUALIZATION_COLORS[0], { + hex: '#54b399', + isValid: true, + rgba: [84, 179, 153, 1], + }); +}); + +test('Setting a new alpha value calls onChange', () => { + const colorPicker = mount( + + ); + + findTestSubject(colorPicker, 'colorPickerAnchor').simulate('click'); + // Slider + const alpha = findTestSubject(colorPicker, 'colorPickerAlpha'); + const event1 = { target: { value: '50' } }; + const range = alpha.first(); // input[type=range] + range.simulate('change', event1); + expect(onChange).toBeCalled(); + expect(onChange).toBeCalledWith('#ffeedd80', { + hex: '#ffeedd80', + isValid: true, + rgba: [255, 238, 221, 0.5], + }); + // Number input + const event2 = { target: { value: '25' } }; + const input = alpha.at(1); // input[type=number] + input.simulate('change', event2); + expect(onChange).toBeCalled(); + expect(onChange).toBeCalledWith('#ffeedd40', { + hex: '#ffeedd40', + isValid: true, + rgba: [255, 238, 221, 0.25], + }); }); test('default mode does redners child components', () => { diff --git a/src/components/color_picker/color_picker.tsx b/src/components/color_picker/color_picker.tsx index 825816d9171..4a8a19d9bfd 100644 --- a/src/components/color_picker/color_picker.tsx +++ b/src/components/color_picker/color_picker.tsx @@ -3,8 +3,8 @@ import React, { HTMLAttributes, ReactElement, cloneElement, - useCallback, useEffect, + useMemo, useRef, useState, } from 'react'; @@ -21,24 +21,49 @@ import { EuiFieldText, EuiFormControlLayout, EuiFormControlLayoutProps, + EuiRange, } from '../form'; import { EuiI18n } from '../i18n'; import { EuiPopover } from '../popover'; +import { EuiSpacer } from '../spacer'; import { VISUALIZATION_COLORS, keyCodes } from '../../services'; import { EuiHue } from './hue'; import { EuiSaturation } from './saturation'; +import { + getChromaColor, + parseColor, + HEX_FALLBACK, + HSV_FALLBACK, + RGB_FALLBACK, + RGB_JOIN, +} from './utils'; type EuiColorPickerDisplay = 'default' | 'inline'; type EuiColorPickerMode = 'default' | 'swatch' | 'picker'; +export interface EuiColorPickerOutput { + rgba: ColorSpaces['rgba']; + hex: string; + isValid: boolean; +} + interface HTMLDivElementOverrides { /** - * Hex string (3 or 6 character). Empty string will register as 'transparent' + * hex (string) + * RGB (as comma separated string) + * RGBa (as comma separated string) + * Empty string will register as 'transparent' */ color?: string | null; onBlur?: () => void; - onChange: (hex: string) => void; + /** + * text (string, as entered or selected) + * hex (8-digit hex if alpha < 1, otherwise 6-digit hex) + * RGBa (as array; values of NaN if color is invalid) + * isValid (boolean signifying if the input text is a valid color) + */ + onChange: (text: string, output: EuiColorPickerOutput) => void; onFocus?: () => void; } export interface EuiColorPickerProps @@ -86,6 +111,16 @@ export interface EuiColorPickerProps * `string` | `ReactElement` or an array of these */ append?: EuiFormControlLayoutProps['append']; + /** + * Whether to render the alpha channel (opacity) value range slider. + */ + showAlpha?: boolean; + /** + * Will format the text input in the provided format when possible (hue and saturation selection) + * Exceptions: Manual text input and swatches will display as-authored + * Default is to display the last format entered by the user + */ + format?: 'hex' | 'rgba'; } function isKeyboardEvent( @@ -94,11 +129,36 @@ function isKeyboardEvent( return typeof event === 'object' && 'keyCode' in event; } -const chromaValid = (color: string) => { - // Temporary function until `@types/chroma-js` allows the 2nd param. - // Consolidating the `ts-ignore`s to one location - // @ts-ignore - return chroma.valid(color, 'hex'); +const getOutput = ( + text: string | null, + showAlpha: boolean = false +): EuiColorPickerOutput => { + const color = getChromaColor(text, true); + let isValid = true; + if (!showAlpha && color !== null) { + isValid = color.alpha() === 1; + } + // Note that if a consumer has disallowed opacity, + // we still return the color with an alpha channel, but mark it as invalid + return color + ? { + rgba: color.rgba(), + hex: color.hex(), + isValid, + } + : { + rgba: RGB_FALLBACK, + hex: HEX_FALLBACK, + isValid: false, + }; +}; + +const getHsv = (hsv?: number[], fallback: number = 0) => { + // Chroma's passthrough (RGB) parsing determines that black/white/gray are hue-less and returns `NaN` + // For our purposes we can process `NaN` as `0` if necessary + if (!hsv) return HSV_FALLBACK; + const hue = isNaN(hsv[0]) ? fallback : hsv[0]; + return [hue, hsv[1], hsv[2]] as ColorSpaces['hsv']; }; export const EuiColorPicker: FunctionComponent = ({ @@ -120,36 +180,52 @@ export const EuiColorPicker: FunctionComponent = ({ popoverZIndex, prepend, append, + showAlpha = false, + format, }) => { - const getHsvFromColor = useCallback( - (): ColorSpaces['hsv'] => - color && chromaValid(color) ? chroma(color).hsv() : [0, 0, 0], - [color] - ); + const preferredFormat = useMemo(() => { + if (format) return format; + const parsed = parseColor(color); + return parsed != null && typeof parsed === 'object' ? 'rgba' : 'hex'; + }, [color, format]); + const chromaColor = useMemo(() => getChromaColor(color, showAlpha), [ + color, + showAlpha, + ]); + const [alphaRangeValue, setAlphaRangeValue] = useState('100'); + const alphaChannel = useMemo(() => { + return chromaColor ? chromaColor.alpha() : 1; + }, [chromaColor]); + + useEffect(() => { + const percent = (alphaChannel * 100).toFixed(); + setAlphaRangeValue(percent); + }, [alphaChannel]); + const [isColorSelectorShown, setIsColorSelectorShown] = useState(false); - const [colorAsHsv, setColorAsHsv] = useState(getHsvFromColor()); - const [lastHex, setLastHex] = useState(color); const [inputRef, setInputRef] = useState(null); // Ideally this is uses `useRef`, but `EuiFieldText` isn't ready for that const [popoverShouldOwnFocus, setPopoverShouldOwnFocus] = useState(false); + const prevColor = useRef(chromaColor ? chromaColor.rgba().join() : null); + const [colorAsHsv, setColorAsHsv] = useState( + chromaColor ? getHsv(chromaColor.hsv()) : HSV_FALLBACK + ); + const usableHsv: ColorSpaces['hsv'] = useMemo(() => { + if (chromaColor && chromaColor.rgba().join() !== prevColor.current) { + const [h, s, v] = chromaColor.hsv(); + const hue = isNaN(h) ? colorAsHsv[0] : h; + return [hue, s, v]; + } + return colorAsHsv; + }, [chromaColor, colorAsHsv]); + const satruationRef = useRef(null); const swatchRef = useRef(null); const updateColorAsHsv = ([h, s, v]: ColorSpaces['hsv']) => { - // Chroma's passthrough (RGB) parsing determines that black/white/gray are hue-less and returns `NaN` - // For our purposes we can process `NaN` as `0` - const hue = isNaN(h) ? 0 : h; - setColorAsHsv([hue, s, v]); + setColorAsHsv(getHsv([h, s, v], usableHsv[0])); }; - useEffect(() => { - if (lastHex !== color) { - // Only react to outside changes - const newColorAsHsv = getHsvFromColor(); - updateColorAsHsv(newColorAsHsv); - } - }, [color, lastHex, getHsvFromColor]); - const classes = classNames('euiColorPicker', className); const popoverClass = 'euiColorPicker__popoverAnchor'; const panelClasses = classNames('euiColorPicker__popoverPanel', { @@ -163,9 +239,12 @@ export const EuiColorPicker: FunctionComponent = ({ 'euiColorPicker__input--inGroup': prepend || append, }); - const handleOnChange = (hex: string) => { - setLastHex(hex); - onChange(hex); + const handleOnChange = (text: string) => { + const output = getOutput(text, showAlpha); + if (output.isValid) { + prevColor.current = output.rgba.join(); + } + onChange(text, output); }; const closeColorSelector = (shouldDelay = false) => { @@ -248,48 +327,89 @@ export const EuiColorPicker: FunctionComponent = ({ const handleColorInput = (e: React.ChangeEvent) => { handleOnChange(e.target.value); - if (chromaValid(e.target.value)) { - updateColorAsHsv(chroma(e.target.value).hsv()); + const newColor = getChromaColor(e.target.value, showAlpha); + if (newColor) { + updateColorAsHsv(newColor.hsv()); + } + }; + + const updateWithHsv = (hsv: ColorSpaces['hsv']) => { + const color = chroma.hsv(...hsv).alpha(alphaChannel); + let formatted; + if (preferredFormat === 'rgba') { + formatted = + alphaChannel < 1 + ? color.rgba().join(RGB_JOIN) + : color.rgb().join(RGB_JOIN); + } else { + formatted = color.hex(); } + handleOnChange(formatted); + updateColorAsHsv(hsv); }; const handleColorSelection = (color: ColorSpaces['hsv']) => { - const [h] = colorAsHsv; + const [h] = usableHsv; const [, s, v] = color; const newHsv: ColorSpaces['hsv'] = [h, s, v]; - handleOnChange(chroma.hsv(...newHsv).hex()); - updateColorAsHsv(newHsv); + updateWithHsv(newHsv); }; const handleHueSelection = (hue: number) => { - const [, s, v] = colorAsHsv; + const [, s, v] = usableHsv; const newHsv: ColorSpaces['hsv'] = [hue, s, v]; - handleOnChange(chroma.hsv(...newHsv).hex()); - updateColorAsHsv(newHsv); + updateWithHsv(newHsv); }; const handleSwatchSelection = (color: string) => { + const newColor = getChromaColor(color, showAlpha); handleOnChange(color); - updateColorAsHsv(chroma(color).hsv()); + if (newColor) { + updateColorAsHsv(newColor.hsv()); + } handleFinalSelection(); }; + const handleAlphaSelection = ( + e: + | React.ChangeEvent + | React.MouseEvent, + isValid: boolean + ) => { + const target = e.target as HTMLInputElement; + setAlphaRangeValue(target.value || ''); + if (isValid) { + const alpha = parseInt(target.value, 10) / 100; + const newColor = chromaColor ? chromaColor.alpha(alpha) : null; + const hex = newColor ? newColor.hex() : HEX_FALLBACK; + const rgba = newColor ? newColor.rgba() : RGB_FALLBACK; + let text; + if (preferredFormat === 'rgba') { + text = + alpha < 1 ? rgba.join(RGB_JOIN) : rgba.slice(0, 3).join(RGB_JOIN); + } else { + text = hex; + } + onChange(text, { hex, rgba, isValid: !!newColor }); + } + }; + const composite = ( {mode !== 'swatch' && (
@@ -317,6 +437,29 @@ export const EuiColorPicker: FunctionComponent = ({ ))} )} + {showAlpha && ( + + + + {(alphaLabel: string) => ( + + )} + + + )}
); @@ -329,7 +472,7 @@ export const EuiColorPicker: FunctionComponent = ({ 'data-test-subj': testSubjAnchor, }); } else { - const showColor = color && chromaValid(color); + const colorStyle = chromaColor ? chromaColor.css() : undefined; buttonOrInput = ( = ({ append={append}>
+ style={{ + color: colorStyle, + }}> = ({ className={inputClasses} onClick={handleInputActivity} onKeyDown={handleInputActivity} - value={color ? color.toUpperCase() : ''} + value={color ? color.toUpperCase() : HEX_FALLBACK} placeholder={!color ? 'Transparent' : undefined} id={id} onChange={handleColorInput} - maxLength={7} - icon={showColor ? 'swatchInput' : 'stopSlash'} + icon={chromaColor ? 'swatchInput' : 'stopSlash'} inputRef={setInputRef} isInvalid={isInvalid} compressed={compressed} diff --git a/src/components/color_picker/color_picker_swatch.tsx b/src/components/color_picker/color_picker_swatch.tsx index 85f3a8f60f5..0e6fddc4c1d 100644 --- a/src/components/color_picker/color_picker_swatch.tsx +++ b/src/components/color_picker/color_picker_swatch.tsx @@ -1,8 +1,10 @@ -import React, { ButtonHTMLAttributes, forwardRef } from 'react'; +import React, { ButtonHTMLAttributes, forwardRef, useMemo } from 'react'; import classNames from 'classnames'; import { CommonProps } from '../common'; +import { getChromaColor } from './utils'; + export type EuiColorPickerSwatchProps = CommonProps & Omit, 'color'> & { color?: string; @@ -13,12 +15,20 @@ export const EuiColorPickerSwatch = forwardRef< EuiColorPickerSwatchProps >(({ className, color, style, ...rest }, ref) => { const classes = classNames('euiColorPickerSwatch', className); + const chromaColor = useMemo(() => getChromaColor(color, true), [color]); + const background = useMemo( + () => (chromaColor ? chromaColor.css() : 'transparent'), + [chromaColor] + ); return (