From 78e8187fb3ff4394ad1b9d7b4ce30cad6fd42b2e Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Wed, 12 Feb 2020 11:24:39 -0600 Subject: [PATCH 01/29] alpha range --- package.json | 2 +- .../color_picker/color_picker_example.js | 28 +++++++ .../color_picker/_color_picker.scss | 10 ++- src/components/color_picker/color_picker.tsx | 76 ++++++++++++++++--- src/components/form/range/_range_wrapper.scss | 6 ++ yarn.lock | 8 +- 6 files changed, 113 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 6bbc40b9f68..45279cfbf1d 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,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/color_picker_example.js b/src-docs/src/views/color_picker/color_picker_example.js index 90f7425a6e8..df1f6aefa55 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,18 @@ const colorPickerRangeSnippet = ` `; +import { Alpha } from './alpha'; +const alphaSource = require('!!raw-loader!./alpha'); +const alphaHtml = renderToHtml(Alpha); +const alphaSnippet = ``; + import { CustomSwatches } from './custom_swatches'; const customSwatchesSource = require('!!raw-loader!./custom_swatches'); const customSwatchesHtml = renderToHtml(CustomSwatches); @@ -329,6 +341,22 @@ export const ColorPickerExample = { snippet: colorPickerRangeSnippet, demo: , }, + { + title: 'Alpha channel (opacity) selection', + source: [ + { + type: GuideSectionTypes.JS, + code: alphaSource, + }, + { + type: GuideSectionTypes.HTML, + code: alphaHtml, + }, + ], + //text:

alpha

, + snippet: alphaSnippet, + demo: , + }, { title: 'Custom color swatches', source: [ diff --git a/src/components/color_picker/_color_picker.scss b/src/components/color_picker/_color_picker.scss index bf4734dfbe1..92032669a43 100644 --- a/src/components/color_picker/_color_picker.scss +++ b/src/components/color_picker/_color_picker.scss @@ -14,8 +14,6 @@ } } - - // Adds a stroke color for the swatchInput icon. Unlike most EuiIcons it has a stroke in the SVG .euiSwatchInput__stroke { fill: none; @@ -38,4 +36,10 @@ height: $euiFormControlLayoutGroupInputCompressedHeight !important; border-radius: 0; } -} \ No newline at end of file +} + +.euiColorPicker__alphaRange { + .euiRangeInput { + min-width: 0; + } +} diff --git a/src/components/color_picker/color_picker.tsx b/src/components/color_picker/color_picker.tsx index 9004714d5ec..bee71fbdd19 100644 --- a/src/components/color_picker/color_picker.tsx +++ b/src/components/color_picker/color_picker.tsx @@ -18,12 +18,14 @@ import { EuiColorPickerSwatch } from './color_picker_swatch'; import { EuiFocusTrap } from '../focus_trap'; import { EuiFlexGroup, EuiFlexItem } from '../flex'; import { EuiFieldText } from '../form/field_text'; +import { EuiRange } from '../form/range'; import { EuiFormControlLayout, EuiFormControlLayoutProps, } from '../form/form_control_layout'; import { EuiI18n } from '../i18n'; import { EuiPopover } from '../popover'; +import { EuiSpacer } from '../spacer'; import { VISUALIZATION_COLORS, keyCodes } from '../../services'; import { EuiHue } from './hue'; @@ -38,7 +40,7 @@ interface HTMLDivElementOverrides { */ color?: string | null; onBlur?: () => void; - onChange: (hex: string) => void; + onChange: (hex: string, rgba?: number[]) => void; onFocus?: () => void; } export interface EuiColorPickerProps @@ -84,6 +86,14 @@ export interface EuiColorPickerProps * Creates an input group with element(s) coming after input. It only shows when the `display` is set to `default`. */ append?: EuiFormControlLayoutProps['append']; + /** + * Alpha channel (opacity) value. Scale of 0-1. + */ + alpha?: number; + /** + * Whether to render the alpha channel (opacity) value range slider. + */ + showAlpha?: boolean; } function isKeyboardEvent( @@ -93,9 +103,6 @@ function isKeyboardEvent( } 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'); }; @@ -118,15 +125,23 @@ export const EuiColorPicker: FunctionComponent = ({ popoverZIndex, prepend, append, + alpha = 1, + showAlpha = false, }) => { + const getRgb = (hex?: string | null): ColorSpaces['rgb'] => + hex && chromaValid(hex) ? chroma(hex).rgb() : [NaN, NaN, NaN]; const getHsvFromColor = useCallback( (): ColorSpaces['hsv'] => color && chromaValid(color) ? chroma(color).hsv() : [0, 0, 0], [color] ); + const getRgbFromColor = useCallback((): ColorSpaces['rgb'] => { + return getRgb(color); + }, [color]); const [isColorSelectorShown, setIsColorSelectorShown] = useState(false); const [colorAsHsv, setColorAsHsv] = useState(getHsvFromColor()); const [lastHex, setLastHex] = useState(color); + const [lastRgb, setLastRgb] = useState(getRgbFromColor()); const [inputRef, setInputRef] = useState(null); // Ideally this is uses `useRef`, but `EuiFieldText` isn't ready for that const [popoverShouldOwnFocus, setPopoverShouldOwnFocus] = useState(false); @@ -143,10 +158,10 @@ export const EuiColorPicker: FunctionComponent = ({ useEffect(() => { if (lastHex !== color) { // Only react to outside changes - const newColorAsHsv = getHsvFromColor(); - updateColorAsHsv(newColorAsHsv); + updateColorAsHsv(getHsvFromColor()); + setLastRgb(getRgbFromColor()); } - }, [color, lastHex, getHsvFromColor]); + }, [color, lastHex, getHsvFromColor, getRgbFromColor]); const classes = classNames('euiColorPicker', className); const popoverClass = 'euiColorPicker__popoverAnchor'; @@ -163,7 +178,9 @@ export const EuiColorPicker: FunctionComponent = ({ const handleOnChange = (hex: string) => { setLastHex(hex); - onChange(hex); + const rgb = getRgb(hex); + setLastRgb(rgb); + onChange(hex, [...rgb, alpha]); }; const closeColorSelector = (shouldDelay = false) => { @@ -273,6 +290,20 @@ export const EuiColorPicker: FunctionComponent = ({ handleFinalSelection(); }; + const handleAlphaChange = ( + e: + | React.ChangeEvent + | React.MouseEvent, + isValid: boolean + ) => { + if (isValid) { + const target = e.target as HTMLInputElement; + const alpha = parseInt(target.value, 10) / 100; + const hex = lastHex || ''; + onChange(hex, [...lastRgb, alpha]); + } + }; + const composite = ( {mode !== 'swatch' && ( @@ -315,6 +346,28 @@ export const EuiColorPicker: FunctionComponent = ({ ))} )} + {showAlpha && ( + + + + {(alphaLabel: string) => ( + + )} + + + )} ); @@ -346,7 +399,12 @@ export const EuiColorPicker: FunctionComponent = ({ append={append}>
+ style={{ + color: + showColor && color + ? `rgba(${lastRgb[0]},${lastRgb[1]},${lastRgb[2]},${alpha})` + : undefined, + }}> .euiFormControlLayout { /* 1 */ width: auto; + + &.euiFormControlLayout--group { + flex-shrink: 0; /* 2 */ + } } } diff --git a/yarn.lock b/yarn.lock index 11a61621c8d..ded2204bdf9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1207,10 +1207,10 @@ resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.8.tgz#5702f74f78b73e13f1eb1bd435c2c9de61a250d4" integrity sha512-LzF540VOFabhS2TR2yYFz2Mu/fTfkA+5AwYddtJbOJGwnYrr2e7fHadT7/Z3jNGJJdCRlO3ySxmW26NgRdwhNA== -"@types/chroma-js@^1.4.3": - version "1.4.3" - resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-1.4.3.tgz#4456e5cb46885a4952324e55a4b6d4064904790c" - integrity sha512-m33zg9cRLtuaUSzlbMrr7iLIKNzrD4+M6Unt5+9mCu4BhR5NwnRjVKblINCwzcBXooukIgld8DtEncP8qpvbNg== +"@types/chroma-js@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-2.0.0.tgz#b0fc98c8625d963f14e8138e0a7961103303ab22" + integrity sha512-iomunXsXjDxhm2y1OeJt8NwmgC7RyNkPAOddlYVGsbGoX8+1jYt84SG4/tf6RWcwzROLx1kPXPE95by1s+ebIg== "@types/classnames@^2.2.6": version "2.2.6" From 00f84a6ff4bb28cee4bed1bf23130ae27355905a Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Wed, 12 Feb 2020 11:58:11 -0600 Subject: [PATCH 02/29] alpha tests --- .../__snapshots__/color_picker.test.tsx.snap | 166 +++++++++++++++++- .../color_picker/color_picker.test.tsx | 58 +++++- src/components/color_picker/color_picker.tsx | 1 + 3 files changed, 216 insertions(+), 9 deletions(-) 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 c2a754b8a5c..c5f0650463e 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" >
`; +exports[`renders a EuiColorPicker with an alpha range selector 1`] = ` +
+
+
+
+
+
+
+ +
+ + + +
+
+
+
+
+ + + +
+
+
+
+
+`; + +exports[`renders a EuiColorPicker with an alpha value provided 1`] = ` +
+
+
+
+
+
+
+ +
+ + + +
+
+
+
+
+ + + +
+
+
+
+
+`; + exports[`renders compressed EuiColorPicker 1`] = `
{ expect(component).toMatchSnapshot(); }); +test('renders a EuiColorPicker with an alpha value provided', () => { + const component = render( + + ); + + 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 +220,7 @@ 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', [0, 0, 0, 1]); }); test('Clicking a swatch calls onChange', () => { @@ -206,7 +233,34 @@ 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], [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('#ffeedd', [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('#ffeedd', [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 bee71fbdd19..e5e6b37c7f0 100644 --- a/src/components/color_picker/color_picker.tsx +++ b/src/components/color_picker/color_picker.tsx @@ -355,6 +355,7 @@ export const EuiColorPicker: FunctionComponent = ({ {(alphaLabel: string) => ( Date: Wed, 12 Feb 2020 12:18:47 -0600 Subject: [PATCH 03/29] docs --- src-docs/src/views/color_picker/alpha.js | 44 +++++++++++++++++++ .../color_picker/color_picker_example.js | 17 ++++++- 2 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 src-docs/src/views/color_picker/alpha.js 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..a532c2e4444 --- /dev/null +++ b/src-docs/src/views/color_picker/alpha.js @@ -0,0 +1,44 @@ +import React, { useState } from 'react'; + +import { EuiColorPicker, EuiFormRow } from '../../../../src/components'; +import { useColorPicker } from './utils'; + +export const Alpha = () => { + const [color, setColor, errors] = useColorPicker('#D36086'); + const [alpha, setAlpha] = useState(1); + + const handleChange = (hex, [, , , a]) => { + setColor(hex); + setAlpha(a); + }; + + return ( + <> + + + + + + setColor(hex)} + color={color} + alpha={0.25} + showAlpha={false} + isInvalid={!!errors} + /> + + + ); +}; 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 df1f6aefa55..9120b6b36e5 100644 --- a/src-docs/src/views/color_picker/color_picker_example.js +++ b/src-docs/src/views/color_picker/color_picker_example.js @@ -67,6 +67,13 @@ import { Alpha } from './alpha'; const alphaSource = require('!!raw-loader!./alpha'); const alphaHtml = renderToHtml(Alpha); const alphaSnippet = ``; +const alphaSnippetShow = `alpha

, - snippet: alphaSnippet, + text: ( +

+ Set the alpha prop to designate a starting opacity + value (decimal range, 0 to 1). To allow user updates to the color + opacity, set the showAlpha prop to `true`. +

+ ), + snippet: [alphaSnippet, alphaSnippetShow], demo: , }, { From 6af0bed7e6048ee7a60d1f0ce1ee80a51e98d6ce Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Fri, 14 Feb 2020 14:40:09 -0600 Subject: [PATCH 04/29] wip: rgba support --- src-docs/src/views/color_picker/alpha.js | 42 ++--- .../src/views/color_picker/color_picker.js | 21 ++- .../__snapshots__/color_picker.test.tsx.snap | 88 +---------- .../__snapshots__/hue.test.tsx.snap | 6 +- .../color_picker/color_picker.test.tsx | 19 +-- src/components/color_picker/color_picker.tsx | 147 +++++++++++------- src/components/color_picker/hue.tsx | 2 +- 7 files changed, 126 insertions(+), 199 deletions(-) diff --git a/src-docs/src/views/color_picker/alpha.js b/src-docs/src/views/color_picker/alpha.js index a532c2e4444..e9c50c0052f 100644 --- a/src-docs/src/views/color_picker/alpha.js +++ b/src-docs/src/views/color_picker/alpha.js @@ -1,44 +1,26 @@ -import React, { useState } from 'react'; +import React from 'react'; import { EuiColorPicker, EuiFormRow } from '../../../../src/components'; import { useColorPicker } from './utils'; export const Alpha = () => { const [color, setColor, errors] = useColorPicker('#D36086'); - const [alpha, setAlpha] = useState(1); - const handleChange = (hex, [, , , a]) => { + const handleChange = hex => { setColor(hex); - setAlpha(a); }; return ( - <> - + - - - - - setColor(hex)} - color={color} - alpha={0.25} - showAlpha={false} - isInvalid={!!errors} - /> - - + /> + ); }; diff --git a/src-docs/src/views/color_picker/color_picker.js b/src-docs/src/views/color_picker/color_picker.js index 5fc8b2f548a..9aa5e1b3fa4 100644 --- a/src-docs/src/views/color_picker/color_picker.js +++ b/src-docs/src/views/color_picker/color_picker.js @@ -24,13 +24,20 @@ export class ColorPicker extends Component { } return ( - - - + <> + + + + + ); } } 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 c5f0650463e..2ce61d12e31 100644 --- a/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap +++ b/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap @@ -27,7 +27,6 @@ exports[`renders EuiColorPicker 1`] = ` autocomplete="off" class="euiFieldText euiColorPicker__input euiFieldText--withIcon" data-test-subj="colorPickerAnchor" - maxlength="7" type="text" value="#FFEEDD" /> @@ -103,7 +102,6 @@ exports[`renders EuiColorPicker with a color swatch when color is defined 1`] = autocomplete="off" class="euiFieldText euiColorPicker__input euiFieldText--withIcon" data-test-subj="colorPickerAnchor" - maxlength="7" type="text" value="#FFFFFF" /> @@ -177,7 +175,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="" @@ -252,7 +249,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="" @@ -334,7 +330,6 @@ exports[`renders a EuiColorPicker with a prepend and append 1`] = ` autocomplete="off" class="euiFieldText euiColorPicker__input euiColorPicker__input--inGroup euiFieldText--withIcon" data-test-subj="colorPickerAnchor" - maxlength="7" type="text" value="#FFEEDD" /> @@ -415,83 +410,6 @@ exports[`renders a EuiColorPicker with an alpha range selector 1`] = ` autocomplete="off" class="euiFieldText euiColorPicker__input euiFieldText--withIcon" data-test-subj="colorPickerAnchor" - maxlength="7" - type="text" - value="#FFEEDD" - /> -
- - - -
-
-
-
-
- - - -
-
-
-
-
-`; - -exports[`renders a EuiColorPicker with an alpha value provided 1`] = ` -
-
-
-
-
-
-
- @@ -567,7 +485,6 @@ exports[`renders compressed EuiColorPicker 1`] = ` autocomplete="off" class="euiFieldText euiColorPicker__input euiFieldText--withIcon euiFieldText--compressed" data-test-subj="colorPickerAnchor" - maxlength="7" type="text" value="#FFEEDD" /> @@ -644,7 +561,6 @@ exports[`renders disabled EuiColorPicker 1`] = ` class="euiFieldText euiColorPicker__input euiFieldText--withIcon" data-test-subj="colorPickerAnchor" disabled="" - maxlength="7" type="text" value="#FFEEDD" /> @@ -720,7 +636,6 @@ exports[`renders fullWidth EuiColorPicker 1`] = ` autocomplete="off" class="euiFieldText euiColorPicker__input euiFieldText--withIcon euiFieldText--fullWidth" data-test-subj="colorPickerAnchor" - maxlength="7" type="text" value="#FFEEDD" /> @@ -826,7 +741,7 @@ exports[`renders inline EuiColorPicker 1`] = ` { expect(component).toMatchSnapshot(); }); -test('renders a EuiColorPicker with an alpha value provided', () => { - const component = render( - - ); - - expect(component).toMatchSnapshot(); -}); - test('renders a EuiColorPicker with an alpha range selector', () => { const component = render( @@ -241,7 +227,6 @@ test('Setting a new alpha value calls onChange', () => { @@ -254,13 +239,13 @@ test('Setting a new alpha value calls onChange', () => { const range = alpha.first(); // input[type=range] range.simulate('change', event1); expect(onChange).toBeCalled(); - expect(onChange).toBeCalledWith('#ffeedd', [255, 238, 221, 0.5]); + expect(onChange).toBeCalledWith('#ffeedd80', [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('#ffeedd', [255, 238, 221, 0.25]); + expect(onChange).toBeCalledWith('#ffeedd40', [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 e5e6b37c7f0..4fdb2fe623d 100644 --- a/src/components/color_picker/color_picker.tsx +++ b/src/components/color_picker/color_picker.tsx @@ -3,8 +3,7 @@ import React, { HTMLAttributes, ReactElement, cloneElement, - useCallback, - useEffect, + useMemo, useRef, useState, } from 'react'; @@ -36,10 +35,17 @@ type EuiColorPickerMode = 'default' | 'swatch' | 'picker'; 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; + /** + * hex (8-digit hex if alpha < 1) + * RGBa (as array; values of NaN if color is invalid) + */ onChange: (hex: string, rgba?: number[]) => void; onFocus?: () => void; } @@ -96,16 +102,55 @@ export interface EuiColorPickerProps showAlpha?: boolean; } +const HEX_FALLBACK = ''; +const HSV_FALLBACK: ColorSpaces['hsv'] = [0, 0, 0]; +const RGB_FALLBACK: ColorSpaces['rgb'] = [NaN, NaN, NaN]; + function isKeyboardEvent( event: React.MouseEvent | React.KeyboardEvent ): event is React.KeyboardEvent { return typeof event === 'object' && 'keyCode' in event; } -const chromaValid = (color: string) => { +const chromaValid = (color: string | number[]) => { + if (typeof color === 'object') { + return chroma.valid(color, 'rgb') || chroma.valid(color, 'rgba'); + } return chroma.valid(color, 'hex'); }; +const getRgba = (hex?: string | null): number[] => + hex && chromaValid(hex) ? chroma(hex).rgba() : [...RGB_FALLBACK, 1]; + +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']; +}; + +const parseColor = (input?: string | null) => { + let parsed: string | number[]; + if (!input) return null; + if (input.indexOf(',') > 0) { + const rgb = input + .trim() + .split(',') + .filter(n => n !== '') + .map(Number); + parsed = rgb.length > 2 && rgb.length < 5 ? rgb : HEX_FALLBACK; + } else { + parsed = input; + } + + if (chromaValid(parsed)) { + // type guard for the function overload + return typeof parsed === 'object' ? chroma(parsed) : chroma(parsed); + } + return null; +}; + export const EuiColorPicker: FunctionComponent = ({ button, className, @@ -125,44 +170,34 @@ export const EuiColorPicker: FunctionComponent = ({ popoverZIndex, prepend, append, - alpha = 1, showAlpha = false, }) => { - const getRgb = (hex?: string | null): ColorSpaces['rgb'] => - hex && chromaValid(hex) ? chroma(hex).rgb() : [NaN, NaN, NaN]; - const getHsvFromColor = useCallback( - (): ColorSpaces['hsv'] => - color && chromaValid(color) ? chroma(color).hsv() : [0, 0, 0], - [color] - ); - const getRgbFromColor = useCallback((): ColorSpaces['rgb'] => { - return getRgb(color); - }, [color]); + const chromaColor = useMemo(() => parseColor(color), [color]); + const [isColorSelectorShown, setIsColorSelectorShown] = useState(false); - const [colorAsHsv, setColorAsHsv] = useState(getHsvFromColor()); - const [lastHex, setLastHex] = useState(color); - const [lastRgb, setLastRgb] = useState(getRgbFromColor()); 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.hex() : null); + const [colorAsHsv, setColorAsHsv] = useState( + chromaColor ? getHsv(chromaColor.hsv()) : HSV_FALLBACK + ); + const usableHsv: ColorSpaces['hsv'] = useMemo(() => { + if (chromaColor && chromaColor.hex() !== 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 - updateColorAsHsv(getHsvFromColor()); - setLastRgb(getRgbFromColor()); - } - }, [color, lastHex, getHsvFromColor, getRgbFromColor]); - const classes = classNames('euiColorPicker', className); const popoverClass = 'euiColorPicker__popoverAnchor'; const panelClasses = classNames('euiColorPicker__popoverPanel', { @@ -177,10 +212,11 @@ export const EuiColorPicker: FunctionComponent = ({ }); const handleOnChange = (hex: string) => { - setLastHex(hex); - const rgb = getRgb(hex); - setLastRgb(rgb); - onChange(hex, [...rgb, alpha]); + const rgba = getRgba(hex); + if (chromaValid(hex)) { + prevColor.current = hex; + } + onChange(hex, rgba); }; const closeColorSelector = (shouldDelay = false) => { @@ -263,13 +299,14 @@ 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 = parseColor(e.target.value); + if (newColor) { + updateColorAsHsv(newColor.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()); @@ -277,7 +314,7 @@ export const EuiColorPicker: FunctionComponent = ({ }; 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); @@ -290,7 +327,7 @@ export const EuiColorPicker: FunctionComponent = ({ handleFinalSelection(); }; - const handleAlphaChange = ( + const handleAlphaSelection = ( e: | React.ChangeEvent | React.MouseEvent, @@ -299,8 +336,9 @@ export const EuiColorPicker: FunctionComponent = ({ if (isValid) { const target = e.target as HTMLInputElement; const alpha = parseInt(target.value, 10) / 100; - const hex = lastHex || ''; - onChange(hex, [...lastRgb, alpha]); + const hex = chromaColor ? chromaColor.alpha(alpha).hex() : HEX_FALLBACK; + const rgb = chromaColor ? chromaColor.rgb() : RGB_FALLBACK; + onChange(hex, [...rgb, alpha]); } }; @@ -310,15 +348,15 @@ export const EuiColorPicker: FunctionComponent = ({
@@ -360,9 +398,11 @@ export const EuiColorPicker: FunctionComponent = ({ showInput={true} max={100} min={0} - value={(alpha * 100).toFixed()} + value={( + (chromaColor ? chromaColor.alpha() : 1) * 100 + ).toFixed()} append="%" - onChange={handleAlphaChange} + onChange={handleAlphaSelection} aria-label={alphaLabel} /> )} @@ -381,7 +421,8 @@ export const EuiColorPicker: FunctionComponent = ({ 'data-test-subj': testSubjAnchor, }); } else { - const showColor = color && chromaValid(color); + const showColor = chromaColor !== null; + const rgba = chromaColor ? chromaColor.rgba() : null; buttonOrInput = ( = ({
= ({ 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'} inputRef={setInputRef} isInvalid={isInvalid} diff --git a/src/components/color_picker/hue.tsx b/src/components/color_picker/hue.tsx index 93b5a9ad2d1..309da6b18db 100644 --- a/src/components/color_picker/hue.tsx +++ b/src/components/color_picker/hue.tsx @@ -9,7 +9,7 @@ import { CommonProps } from '../common'; import { EuiScreenReaderOnly } from '../accessibility'; import { EuiI18n } from '../i18n'; -const HUE_RANGE = 360; +const HUE_RANGE = 359; export type EuiHueProps = Omit< InputHTMLAttributes, From 3a9534039a84a2ecd51501be032f6eef6129bbf3 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Wed, 19 Feb 2020 12:20:59 -0600 Subject: [PATCH 05/29] maintain alpha value --- src-docs/src/views/color_picker/alpha.js | 14 ++++++++++++ src-docs/src/views/color_picker/utils.js | 4 ++-- src/components/color_picker/color_picker.tsx | 23 +++++++++++++------- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src-docs/src/views/color_picker/alpha.js b/src-docs/src/views/color_picker/alpha.js index e9c50c0052f..6f1f6527c19 100644 --- a/src-docs/src/views/color_picker/alpha.js +++ b/src-docs/src/views/color_picker/alpha.js @@ -10,6 +10,19 @@ export const Alpha = () => { setColor(hex); }; + const customSwatches = [ + '#54B399', + '#6092C0', + '#D36086', + '#9170B8', + '#CA8EAE', + '#54B39940', + '#6092C040', + '#D3608640', + '#9170B840', + '#CA8EAE40', + ]; + return ( { color={color} showAlpha={true} isInvalid={!!errors} + swatches={customSwatches} /> ); diff --git a/src-docs/src/views/color_picker/utils.js b/src-docs/src/views/color_picker/utils.js index 6bb640ec1a5..351de716e10 100644 --- a/src-docs/src/views/color_picker/utils.js +++ b/src-docs/src/views/color_picker/utils.js @@ -1,5 +1,5 @@ import { useMemo, useState } from 'react'; -import { isValidHex } from '../../../../src/services'; +import chroma from 'chroma-js'; const generateRandomColor = () => // https://www.paulirish.com/2009/random-hex-color-code-snippets/ @@ -34,7 +34,7 @@ export const useColorStop = (useRandomColor = false) => { export const useColorPicker = (initialColor = '') => { const [color, setColor] = useState(initialColor); const errors = useMemo(() => { - const hasErrors = !isValidHex(color) && color !== ''; + const hasErrors = !chroma.valid(color, 'hex') && color !== ''; return hasErrors ? ['Provide a valid hex value'] : null; }, [color]); diff --git a/src/components/color_picker/color_picker.tsx b/src/components/color_picker/color_picker.tsx index 4fdb2fe623d..68e249b223e 100644 --- a/src/components/color_picker/color_picker.tsx +++ b/src/components/color_picker/color_picker.tsx @@ -92,10 +92,6 @@ export interface EuiColorPickerProps * Creates an input group with element(s) coming after input. It only shows when the `display` is set to `default`. */ append?: EuiFormControlLayoutProps['append']; - /** - * Alpha channel (opacity) value. Scale of 0-1. - */ - alpha?: number; /** * Whether to render the alpha channel (opacity) value range slider. */ @@ -173,6 +169,9 @@ export const EuiColorPicker: FunctionComponent = ({ showAlpha = false, }) => { const chromaColor = useMemo(() => parseColor(color), [color]); + const alpha = useMemo(() => (chromaColor ? chromaColor.alpha() : 1), [ + chromaColor, + ]); const [isColorSelectorShown, setIsColorSelectorShown] = useState(false); const [inputRef, setInputRef] = useState(null); // Ideally this is uses `useRef`, but `EuiFieldText` isn't ready for that @@ -305,19 +304,27 @@ export const EuiColorPicker: FunctionComponent = ({ } }; + const updateWithHsv = (hsv: ColorSpaces['hsv']) => { + handleOnChange( + chroma + .hsv(...hsv) + .alpha(alpha) + .hex() + ); + updateColorAsHsv(hsv); + }; + const handleColorSelection = (color: ColorSpaces['hsv']) => { 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] = usableHsv; const newHsv: ColorSpaces['hsv'] = [hue, s, v]; - handleOnChange(chroma.hsv(...newHsv).hex()); - updateColorAsHsv(newHsv); + updateWithHsv(newHsv); }; const handleSwatchSelection = (color: string) => { From 9900dd9ad69d594b47418bf4668aa67192e1ba15 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Wed, 19 Feb 2020 13:51:44 -0600 Subject: [PATCH 06/29] prevent track visibility; clean up --- .../src/views/color_picker/color_picker.js | 21 +++++++------------ .../color_picker/_color_picker.scss | 5 +++++ .../color_stops/_color_stops.scss | 12 +++++++++++ .../color_stops/color_stop_thumb.tsx | 16 ++++++-------- .../color_picker/color_stops/utils.ts | 5 +++-- 5 files changed, 33 insertions(+), 26 deletions(-) diff --git a/src-docs/src/views/color_picker/color_picker.js b/src-docs/src/views/color_picker/color_picker.js index 9aa5e1b3fa4..5fc8b2f548a 100644 --- a/src-docs/src/views/color_picker/color_picker.js +++ b/src-docs/src/views/color_picker/color_picker.js @@ -24,20 +24,13 @@ export class ColorPicker extends Component { } return ( - <> - - - - - + + + ); } } diff --git a/src/components/color_picker/_color_picker.scss b/src/components/color_picker/_color_picker.scss index 92032669a43..4177b966962 100644 --- a/src/components/color_picker/_color_picker.scss +++ b/src/components/color_picker/_color_picker.scss @@ -11,6 +11,11 @@ &[class*='--compressed'] { @include euiFormControlWithIcon($isIconOptional: false, $side: 'right', $compressed: true); } + + + .euiFormControlLayoutIcons { + // Override :disabled state, which obscures the selected color + color: inherit; + } } } diff --git a/src/components/color_picker/color_stops/_color_stops.scss b/src/components/color_picker/color_stops/_color_stops.scss index 55ea1dc7bf8..00cb76c54eb 100644 --- a/src/components/color_picker/color_stops/_color_stops.scss +++ b/src/components/color_picker/color_stops/_color_stops.scss @@ -55,6 +55,18 @@ position: absolute; width: 100%; height: 100%; + + &:before { + content: ''; + display: block; + position: absolute; + left: 1px; + right: 1px; + top: 50%; + height: $euiRangeHighlightHeight; + margin-top: $euiRangeHighlightHeight * -.5; + background: $euiColorEmptyShade; + } } .euiColorStopThumb.euiRangeThumb:not(:disabled) { diff --git a/src/components/color_picker/color_stops/color_stop_thumb.tsx b/src/components/color_picker/color_stops/color_stop_thumb.tsx index 35628b55366..3e2a81d0dbd 100644 --- a/src/components/color_picker/color_stops/color_stop_thumb.tsx +++ b/src/components/color_picker/color_stops/color_stop_thumb.tsx @@ -20,8 +20,9 @@ import { keyCodes } from '../../../services'; import { EuiButtonIcon } from '../../button'; import { EuiColorPicker, EuiColorPickerProps } from '../color_picker'; import { EuiFlexGroup, EuiFlexItem } from '../../flex'; -// @ts-ignore -import { EuiFieldNumber, EuiFieldText, EuiFormRow } from '../../form'; +import { EuiFieldNumber } from '../../form/field_number'; +import { EuiFieldText } from '../../form/field_text'; +import { EuiFormRow } from '../../form/form_row'; import { EuiI18n } from '../../i18n'; import { EuiRangeThumb } from '../../form/range/range_thumb'; import { EuiPopover } from '../../popover'; @@ -310,15 +311,14 @@ export const EuiColorStopThumb: FunctionComponent = ({ = ({ - + @@ -374,7 +371,6 @@ export const EuiColorStopThumb: FunctionComponent = ({ { - return !isValidHex(color) || color === ''; + return !chroma.valid(color, 'hex') || color === ''; }; export const isStopInvalid = (stop: ColorStop['stop']) => { From 52c6aeec7768ed371b21ccebcedef97543a1e843 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Wed, 19 Feb 2020 17:27:14 -0600 Subject: [PATCH 07/29] display toggles --- src-docs/src/views/color_picker/kitchen_sink.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 */} - + Date: Thu, 20 Feb 2020 10:49:01 -0600 Subject: [PATCH 08/29] docs validation --- .../src/views/color_picker/color_picker.js | 45 +++-------- .../src/views/color_picker/custom_button.js | 79 +++++++------------ src-docs/src/views/color_picker/inline.js | 40 +++------- 3 files changed, 54 insertions(+), 110 deletions(-) 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/custom_button.js b/src-docs/src/views/color_picker/custom_button.js index 57625d7af47..d75a05b60f2 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 } from 'react'; import { EuiColorPicker, @@ -8,56 +8,37 @@ 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 }); - }; - - render() { - const hasErrors = !isValidHex(this.state.color) && this.state.color !== ''; - - let errors; - if (hasErrors) { - errors = ['Provide a valid hex value']; - } - - return ( - - - - } - /> - - +export const CustomButton = () => { + const [color, setColor, errors] = useColorPicker(''); + return ( + + - Color this badge - + } /> - - ); - } -} + + + + Color this badge + + } + /> + + ); +}; 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 ( + + ); +}; From 3320fac1f550e94623f6365ddaee546ef4469a08 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Thu, 20 Feb 2020 11:44:30 -0600 Subject: [PATCH 09/29] use rgba for swatches --- .../__snapshots__/color_picker.test.tsx.snap | 20 +++++++++---------- src/components/color_picker/color_picker.tsx | 8 ++------ .../color_picker/color_picker_swatch.tsx | 12 +++++++++-- 3 files changed, 22 insertions(+), 18 deletions(-) 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 2ce61d12e31..09ae20b3f55 100644 --- a/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap +++ b/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap @@ -760,7 +760,7 @@ exports[`renders inline EuiColorPicker 1`] = ` aria-label="Select #54B399 as the color" class="euiColorPickerSwatch euiColorPicker__swatchSelect" role="option" - style="background:#54B399" + style="background:rgba(84,179,153,1)" type="button" />
@@ -771,7 +771,7 @@ exports[`renders inline EuiColorPicker 1`] = ` aria-label="Select #6092C0 as the color" class="euiColorPickerSwatch euiColorPicker__swatchSelect" role="option" - style="background:#6092C0" + style="background:rgba(96,146,192,1)" type="button" />
@@ -782,7 +782,7 @@ exports[`renders inline EuiColorPicker 1`] = ` aria-label="Select #D36086 as the color" class="euiColorPickerSwatch euiColorPicker__swatchSelect" role="option" - style="background:#D36086" + style="background:rgba(211,96,134,1)" type="button" />
@@ -793,7 +793,7 @@ exports[`renders inline EuiColorPicker 1`] = ` aria-label="Select #9170B8 as the color" class="euiColorPickerSwatch euiColorPicker__swatchSelect" role="option" - style="background:#9170B8" + style="background:rgba(145,112,184,1)" type="button" />
@@ -804,7 +804,7 @@ exports[`renders inline EuiColorPicker 1`] = ` aria-label="Select #CA8EAE as the color" class="euiColorPickerSwatch euiColorPicker__swatchSelect" role="option" - style="background:#CA8EAE" + style="background:rgba(202,142,174,1)" type="button" />
@@ -815,7 +815,7 @@ exports[`renders inline EuiColorPicker 1`] = ` aria-label="Select #D6BF57 as the color" class="euiColorPickerSwatch euiColorPicker__swatchSelect" role="option" - style="background:#D6BF57" + style="background:rgba(214,191,87,1)" type="button" />
@@ -826,7 +826,7 @@ exports[`renders inline EuiColorPicker 1`] = ` aria-label="Select #B9A888 as the color" class="euiColorPickerSwatch euiColorPicker__swatchSelect" role="option" - style="background:#B9A888" + style="background:rgba(185,168,136,1)" type="button" />
@@ -837,7 +837,7 @@ exports[`renders inline EuiColorPicker 1`] = ` aria-label="Select #DA8B45 as the color" class="euiColorPickerSwatch euiColorPicker__swatchSelect" role="option" - style="background:#DA8B45" + style="background:rgba(218,139,69,1)" type="button" />
@@ -848,7 +848,7 @@ exports[`renders inline EuiColorPicker 1`] = ` aria-label="Select #AA6556 as the color" class="euiColorPickerSwatch euiColorPicker__swatchSelect" role="option" - style="background:#AA6556" + style="background:rgba(170,101,86,1)" type="button" />
@@ -859,7 +859,7 @@ exports[`renders inline EuiColorPicker 1`] = ` aria-label="Select #E7664C as the color" class="euiColorPickerSwatch euiColorPicker__swatchSelect" role="option" - style="background:#E7664C" + style="background:rgba(231,102,76,1)" type="button" />
diff --git a/src/components/color_picker/color_picker.tsx b/src/components/color_picker/color_picker.tsx index 68e249b223e..30f96ff8b84 100644 --- a/src/components/color_picker/color_picker.tsx +++ b/src/components/color_picker/color_picker.tsx @@ -405,9 +405,7 @@ export const EuiColorPicker: FunctionComponent = ({ showInput={true} max={100} min={0} - value={( - (chromaColor ? chromaColor.alpha() : 1) * 100 - ).toFixed()} + value={(alpha * 100).toFixed()} append="%" onChange={handleAlphaSelection} aria-label={alphaLabel} @@ -449,9 +447,7 @@ export const EuiColorPicker: FunctionComponent = ({
(({ className, color, style, ...rest }, ref) => { const classes = classNames('euiColorPickerSwatch', className); + const rgba = useMemo( + () => (color && chroma.valid(color) ? chroma(color).rgba() : null), + [color] + ); return (