diff --git a/CHANGELOG.md b/CHANGELOG.md index 43c4d6609fd..e2916eed5d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Added prepend and append to `EuiColorPicker` ([#2819](https://github.com/elastic/eui/pull/2819)) - Improved `EuiDescribedFormGroup` accessibility by avoiding duplicated output in screen readers ([#2783](https://github.com/elastic/eui/pull/2783)) - Added optional `key` attribute to `EuiContextMenu` items and relaxed `name` attribute to allow any React node ([#2817](https://github.com/elastic/eui/pull/2817)) +- Converted `EuiColorPicker` color conversion functions to `chroma-js` methods ([#2805](https://github.com/elastic/eui/pull/2805)) **Bug fixes** diff --git a/src/components/color_picker/color_picker.tsx b/src/components/color_picker/color_picker.tsx index eaa6df78d05..9004714d5ec 100644 --- a/src/components/color_picker/color_picker.tsx +++ b/src/components/color_picker/color_picker.tsx @@ -3,11 +3,13 @@ import React, { HTMLAttributes, ReactElement, cloneElement, + useCallback, useEffect, useRef, useState, } from 'react'; import classNames from 'classnames'; +import chroma, { ColorSpaces } from 'chroma-js'; import { CommonProps } from '../common'; @@ -15,7 +17,6 @@ import { EuiScreenReaderOnly } from '../accessibility'; import { EuiColorPickerSwatch } from './color_picker_swatch'; import { EuiFocusTrap } from '../focus_trap'; import { EuiFlexGroup, EuiFlexItem } from '../flex'; -// @ts-ignore import { EuiFieldText } from '../form/field_text'; import { EuiFormControlLayout, @@ -23,14 +24,7 @@ import { } from '../form/form_control_layout'; import { EuiI18n } from '../i18n'; import { EuiPopover } from '../popover'; -import { - HSV, - VISUALIZATION_COLORS, - keyCodes, - hexToHsv, - hsvToHex, - isValidHex, -} from '../../services'; +import { VISUALIZATION_COLORS, keyCodes } from '../../services'; import { EuiHue } from './hue'; import { EuiSaturation } from './saturation'; @@ -98,6 +92,13 @@ 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'); +}; + export const EuiColorPicker: FunctionComponent = ({ button, className, @@ -118,10 +119,13 @@ export const EuiColorPicker: FunctionComponent = ({ prepend, append, }) => { - const [isColorSelectorShown, setIsColorSelectorShown] = useState(false); - const [colorAsHsv, setColorAsHsv] = useState( - color ? hexToHsv(color) : hexToHsv('') + const getHsvFromColor = useCallback( + (): ColorSpaces['hsv'] => + color && chromaValid(color) ? chroma(color).hsv() : [0, 0, 0], + [color] ); + 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); @@ -129,14 +133,20 @@ export const EuiColorPicker: FunctionComponent = ({ 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]); + }; + useEffect(() => { - // Mimics `componentDidMount` and `componentDidUpdate` if (lastHex !== color) { // Only react to outside changes - const newColorAsHsv = color ? hexToHsv(color) : hexToHsv(''); - setColorAsHsv(newColorAsHsv); + const newColorAsHsv = getHsvFromColor(); + updateColorAsHsv(newColorAsHsv); } - }, [color, lastHex]); + }, [color, lastHex, getHsvFromColor]); const classes = classNames('euiColorPicker', className); const popoverClass = 'euiColorPicker__popoverAnchor'; @@ -236,30 +246,29 @@ export const EuiColorPicker: FunctionComponent = ({ const handleColorInput = (e: React.ChangeEvent) => { handleOnChange(e.target.value); - if (isValidHex(e.target.value)) { - setColorAsHsv(hexToHsv(e.target.value)); + if (chromaValid(e.target.value)) { + updateColorAsHsv(chroma(e.target.value).hsv()); } }; - const handleColorSelection = (color: HSV) => { - const { h } = colorAsHsv; - const hue = h ? h : 1; - const newHsv = { ...color, h: hue }; - handleOnChange(hsvToHex(newHsv)); - setColorAsHsv(newHsv); + const handleColorSelection = (color: ColorSpaces['hsv']) => { + const [h] = colorAsHsv; + const [, s, v] = color; + const newHsv: ColorSpaces['hsv'] = [h, s, v]; + handleOnChange(chroma.hsv(...newHsv).hex()); + updateColorAsHsv(newHsv); }; const handleHueSelection = (hue: number) => { - const { s, v } = colorAsHsv; - const satVal = s && v ? { s, v } : { s: 1, v: 1 }; - const newHsv = { ...satVal, h: hue }; - handleOnChange(hsvToHex(newHsv)); - setColorAsHsv(newHsv); + const [, s, v] = colorAsHsv; + const newHsv: ColorSpaces['hsv'] = [hue, s, v]; + handleOnChange(chroma.hsv(...newHsv).hex()); + updateColorAsHsv(newHsv); }; const handleSwatchSelection = (color: string) => { handleOnChange(color); - setColorAsHsv(hexToHsv(color)); + updateColorAsHsv(chroma(color).hsv()); handleFinalSelection(); }; @@ -277,7 +286,7 @@ export const EuiColorPicker: FunctionComponent = ({ /> @@ -318,7 +327,7 @@ export const EuiColorPicker: FunctionComponent = ({ 'data-test-subj': testSubjAnchor, }); } else { - const showColor = color && isValidHex(color); + const showColor = color && chromaValid(color); buttonOrInput = ( { test('accepts a color', () => { const component = render( diff --git a/src/components/color_picker/saturation.tsx b/src/components/color_picker/saturation.tsx index 30f3c7df243..026f9c8df78 100644 --- a/src/components/color_picker/saturation.tsx +++ b/src/components/color_picker/saturation.tsx @@ -7,10 +7,10 @@ import React, { useState, } from 'react'; import classNames from 'classnames'; +import { ColorSpaces } from 'chroma-js'; import { CommonProps } from '../common'; import { keyCodes } from '../../services'; -import { HSV } from '../../services/color'; import { isNil } from '../../services/predicate'; import { EuiScreenReaderOnly } from '../accessibility'; import { EuiI18n } from '../i18n'; @@ -25,8 +25,8 @@ export type SaturationClientRect = Pick< export type SaturationPosition = Pick; interface HTMLDivElementOverrides { - color?: HSV; - onChange: (color: HSV) => void; + color?: ColorSpaces['hsv']; + onChange: (color: ColorSpaces['hsv']) => void; } export type EuiSaturationProps = Omit< HTMLAttributes, @@ -41,7 +41,7 @@ export const EuiSaturation = forwardRef( ( { className, - color = { h: 1, s: 0, v: 0 }, + color = [1, 0, 0], 'data-test-subj': dataTestSubj = 'euiSaturation', hex, id, @@ -55,17 +55,14 @@ export const EuiSaturation = forwardRef( left: 0, top: 0, }); - const [lastColor, setlastColor] = useState({}); + const [lastColor, setlastColor] = useState([]); const boxRef = useRef(null); useEffect(() => { // Mimics `componentDidMount` and `componentDidUpdate` - const { s, v } = color; - if ( - !isNil(boxRef.current) && - Object.values(lastColor).join() !== Object.values(color).join() - ) { + const [, s, v] = color; + if (!isNil(boxRef.current) && lastColor.join() !== color.join()) { const { height, width } = boxRef.current.getBoundingClientRect(); setIndicator({ left: s * width, @@ -79,11 +76,11 @@ export const EuiSaturation = forwardRef( height, left, width, - }: SaturationClientRect) => { - const { h } = color; + }: SaturationClientRect): ColorSpaces['hsv'] => { + const [h] = color; const s = left / width; const v = 1 - top / height; - return { h, s, v }; + return [h, s, v]; }; const handleUpdate = (box: SaturationClientRect) => { @@ -164,7 +161,7 @@ export const EuiSaturation = forwardRef( className={classes} data-test-subj={dataTestSubj} style={{ - background: `hsl(${color.h}, 100%, 50%)`, + background: `hsl(${color[0]}, 100%, 50%)`, }} {...rest}>