Skip to content

Commit

Permalink
Use chroma-js color methods in EuiColorPicker (#2805)
Browse files Browse the repository at this point in the history
* use chroma for all color calc

* use chroma.valid

* move chromaValid

* CL
  • Loading branch information
thompsongl authored Feb 4, 2020
1 parent c99dcec commit 02b6069
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 47 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,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**

Expand Down
73 changes: 41 additions & 32 deletions src/components/color_picker/color_picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,28 @@ 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';

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,
EuiFormControlLayoutProps,
} 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';
Expand Down Expand Up @@ -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<EuiColorPickerProps> = ({
button,
className,
Expand All @@ -118,25 +119,34 @@ export const EuiColorPicker: FunctionComponent<EuiColorPickerProps> = ({
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<HTMLInputElement | null>(null); // Ideally this is uses `useRef`, but `EuiFieldText` isn't ready for that
const [popoverShouldOwnFocus, setPopoverShouldOwnFocus] = useState(false);

const satruationRef = useRef<HTMLDivElement>(null);
const swatchRef = useRef<HTMLButtonElement>(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';
Expand Down Expand Up @@ -236,30 +246,29 @@ export const EuiColorPicker: FunctionComponent<EuiColorPickerProps> = ({

const handleColorInput = (e: React.ChangeEvent<HTMLInputElement>) => {
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();
};
Expand All @@ -277,7 +286,7 @@ export const EuiColorPicker: FunctionComponent<EuiColorPickerProps> = ({
/>
<EuiHue
id={id}
hue={typeof colorAsHsv === 'object' ? colorAsHsv.h : undefined}
hue={typeof colorAsHsv === 'object' ? colorAsHsv[0] : undefined}
hex={color || undefined}
onChange={handleHueSelection}
/>
Expand Down Expand Up @@ -318,7 +327,7 @@ export const EuiColorPicker: FunctionComponent<EuiColorPickerProps> = ({
'data-test-subj': testSubjAnchor,
});
} else {
const showColor = color && isValidHex(color);
const showColor = color && chromaValid(color);
buttonOrInput = (
<EuiFormControlLayout
icon={
Expand Down
2 changes: 1 addition & 1 deletion src/components/color_picker/saturation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe('EuiHue', () => {
test('accepts a color', () => {
const component = render(
<EuiSaturation
color={{ h: 180, s: 1, v: 0.5 }}
color={[180, 1, 0.5]}
onChange={onChange}
{...requiredProps}
/>
Expand Down
25 changes: 11 additions & 14 deletions src/components/color_picker/saturation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -25,8 +25,8 @@ export type SaturationClientRect = Pick<
export type SaturationPosition = Pick<SaturationClientRect, 'left' | 'top'>;

interface HTMLDivElementOverrides {
color?: HSV;
onChange: (color: HSV) => void;
color?: ColorSpaces['hsv'];
onChange: (color: ColorSpaces['hsv']) => void;
}
export type EuiSaturationProps = Omit<
HTMLAttributes<HTMLDivElement>,
Expand All @@ -41,7 +41,7 @@ export const EuiSaturation = forwardRef<HTMLDivElement, EuiSaturationProps>(
(
{
className,
color = { h: 1, s: 0, v: 0 },
color = [1, 0, 0],
'data-test-subj': dataTestSubj = 'euiSaturation',
hex,
id,
Expand All @@ -55,17 +55,14 @@ export const EuiSaturation = forwardRef<HTMLDivElement, EuiSaturationProps>(
left: 0,
top: 0,
});
const [lastColor, setlastColor] = useState<HSV | {}>({});
const [lastColor, setlastColor] = useState<ColorSpaces['hsv'] | []>([]);

const boxRef = useRef<HTMLDivElement>(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,
Expand All @@ -79,11 +76,11 @@ export const EuiSaturation = forwardRef<HTMLDivElement, EuiSaturationProps>(
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) => {
Expand Down Expand Up @@ -164,7 +161,7 @@ export const EuiSaturation = forwardRef<HTMLDivElement, EuiSaturationProps>(
className={classes}
data-test-subj={dataTestSubj}
style={{
background: `hsl(${color.h}, 100%, 50%)`,
background: `hsl(${color[0]}, 100%, 50%)`,
}}
{...rest}>
<EuiScreenReaderOnly>
Expand Down

0 comments on commit 02b6069

Please sign in to comment.