= ({
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 (
diff --git a/src/components/color_picker/color_stops/__snapshots__/color_stop_thumb.test.tsx.snap b/src/components/color_picker/color_stops/__snapshots__/color_stop_thumb.test.tsx.snap
index 0fc97c4c5d2..301b018d599 100644
--- a/src/components/color_picker/color_stops/__snapshots__/color_stop_thumb.test.tsx.snap
+++ b/src/components/color_picker/color_stops/__snapshots__/color_stop_thumb.test.tsx.snap
@@ -17,7 +17,7 @@ exports[`renders EuiColorStopThumb 1`] = `
class="euiRangeThumb euiColorStopThumb"
data-test-subj="euiColorStopThumb"
role="slider"
- style="background:#FF0000"
+ style="background:rgb(255,0,0)"
tabindex="-1"
title="Click to edit, drag to reposition"
type="button"
@@ -44,7 +44,7 @@ exports[`renders disabled EuiColorStopThumb 1`] = `
data-test-subj="euiColorStopThumb"
disabled=""
role="slider"
- style="background:#FF0000"
+ style="background:rgb(255,0,0)"
tabindex="-1"
title="Click to edit, drag to reposition"
type="button"
@@ -70,7 +70,7 @@ exports[`renders picker-only EuiColorStopThumb 1`] = `
class="euiRangeThumb euiColorStopThumb"
data-test-subj="euiColorStopThumb"
role="slider"
- style="background:#FF0000"
+ style="background:rgb(255,0,0)"
tabindex="-1"
title="Click to edit, drag to reposition"
type="button"
@@ -96,7 +96,7 @@ exports[`renders readOnly EuiColorStopThumb 1`] = `
class="euiRangeThumb euiColorStopThumb"
data-test-subj="euiColorStopThumb"
role="slider"
- style="background:#FF0000"
+ style="background:rgb(255,0,0)"
tabindex="-1"
title="Click to edit, drag to reposition"
type="button"
@@ -122,7 +122,7 @@ exports[`renders swatch-only EuiColorStopThumb 1`] = `
class="euiRangeThumb euiColorStopThumb"
data-test-subj="euiColorStopThumb"
role="slider"
- style="background:#FF0000"
+ style="background:rgb(255,0,0)"
tabindex="-1"
title="Click to edit, drag to reposition"
type="button"
diff --git a/src/components/color_picker/color_stops/__snapshots__/color_stops.test.tsx.snap b/src/components/color_picker/color_stops/__snapshots__/color_stops.test.tsx.snap
index 890de038a7e..3b60b39b4c7 100644
--- a/src/components/color_picker/color_stops/__snapshots__/color_stops.test.tsx.snap
+++ b/src/components/color_picker/color_stops/__snapshots__/color_stops.test.tsx.snap
@@ -20,7 +20,7 @@ exports[`renders EuiColorStops 1`] = `
>
= ({
isRangeMax = false,
parentRef,
colorPickerMode,
+ colorPickerShowAlpha,
colorPickerSwatches,
disabled,
readOnly,
@@ -83,8 +86,14 @@ export const EuiColorStopThumb: FunctionComponent = ({
'data-index': dataIndex,
'aria-valuetext': ariaValueText,
}) => {
+ const background = useMemo(() => {
+ const chromaColor = getChromaColor(color, colorPickerShowAlpha);
+ return chromaColor ? chromaColor.css() : undefined;
+ }, [color, colorPickerShowAlpha]);
const [hasFocus, setHasFocus] = useState(isPopoverOpen);
- const [colorIsInvalid, setColorIsInvalid] = useState(isColorInvalid(color));
+ const [colorIsInvalid, setColorIsInvalid] = useState(
+ isColorInvalid(color, colorPickerShowAlpha)
+ );
const [stopIsInvalid, setStopIsInvalid] = useState(isStopInvalid(stop));
const [numberInputRef, setNumberInputRef] = useState();
const popoverRef = useRef(null);
@@ -123,7 +132,7 @@ export const EuiColorStopThumb: FunctionComponent = ({
const setHasFocusFalse = () => setHasFocus(false);
const handleColorChange = (value: ColorStop['color']) => {
- setColorIsInvalid(isColorInvalid(value));
+ setColorIsInvalid(isColorInvalid(value, colorPickerShowAlpha));
onChange({ stop, color: value });
};
@@ -278,7 +287,7 @@ export const EuiColorStopThumb: FunctionComponent = ({
className="euiColorStopThumb"
tabIndex={-1}
style={{
- background: color,
+ background,
}}
disabled={disabled}
/>
@@ -353,6 +362,7 @@ export const EuiColorStopThumb: FunctionComponent = ({
mode={colorPickerMode}
swatches={colorPickerSwatches}
display="inline"
+ showAlpha={colorPickerShowAlpha}
/>
)}
@@ -364,7 +374,7 @@ export const EuiColorStopThumb: FunctionComponent = ({
'euiColorStopThumb.hexLabel',
'euiColorStopThumb.hexErrorMessage',
]}
- defaults={['Hex color', 'Invalid hex value']}>
+ defaults={['Color', 'Invalid color value']}>
{([hexLabel, hexErrorMessage]: React.ReactChild[]) => (
= ({
label,
stopType = 'gradient',
swatches,
+ showAlpha = false,
}) => {
const sortedStops = useMemo(() => sortStops(colorStops), [colorStops]);
const rangeMax: number = useMemo(() => {
@@ -172,9 +175,9 @@ export const EuiColorStops: FunctionComponent = ({
const handleOnChange = useCallback(
(colorStops: ColorStop[]) => {
- onChange(colorStops, isInvalid(colorStops));
+ onChange(colorStops, isInvalid(colorStops, showAlpha));
},
- [onChange]
+ [onChange, showAlpha]
);
const onFocusStop = useCallback(
@@ -360,6 +363,7 @@ export const EuiColorStops: FunctionComponent = ({
onFocus={() => setFocusedStopIndex(index)}
parentRef={wrapperRef}
colorPickerMode={mode}
+ colorPickerShowAlpha={showAlpha}
colorPickerSwatches={swatches}
disabled={disabled}
readOnly={readOnly}
@@ -387,6 +391,7 @@ export const EuiColorStops: FunctionComponent = ({
rangeMax,
rangeMin,
readOnly,
+ showAlpha,
sortedStops,
swatches,
wrapperRef,
@@ -396,12 +401,14 @@ export const EuiColorStops: FunctionComponent = ({
? sortedStops.map(({ stop }) => getPositionFromStopFn(stop))
: [];
const gradientStop = (colorStop: ColorStop, index: number) => {
+ const color = getChromaColor(colorStop.color, showAlpha);
+ const rgba = color ? color.css() : 'currentColor';
if (index === 0) {
- return `currentColor, currentColor ${positions[index]}%, ${
- colorStop.color
- } ${positions[index]}%`;
+ return `currentColor, currentColor ${positions[index]}%, ${rgba} ${
+ positions[index]
+ }%`;
}
- return `${colorStop.color} ${positions[index]}%`;
+ return `${rgba} ${positions[index]}%`;
};
const fixedStop = (colorStop: ColorStop, index: number) => {
if (index === sortedStops.length - 1) {
diff --git a/src/components/color_picker/color_stops/utils.ts b/src/components/color_picker/color_stops/utils.ts
index 9b475257b42..d2ec41e22d6 100644
--- a/src/components/color_picker/color_stops/utils.ts
+++ b/src/components/color_picker/color_stops/utils.ts
@@ -1,5 +1,5 @@
-import { getEventPosition } from '../utils';
-import { isValidHex, DEFAULT_VISUALIZATION_COLOR } from '../../../services';
+import { getEventPosition, getChromaColor } from '../utils';
+import { DEFAULT_VISUALIZATION_COLOR } from '../../../services';
import { ColorStop } from './color_stop_thumb';
const EUI_THUMB_SIZE = 16; // Same as $euiRangeThumbHeight & $euiRangeThumbWidth
@@ -60,17 +60,23 @@ export const addStop = (
];
};
-export const isColorInvalid = (color: string) => {
- return !isValidHex(color) || color === '';
+export const isColorInvalid = (color: string, showAlpha: boolean = false) => {
+ return getChromaColor(color, showAlpha) == null || color === '';
};
export const isStopInvalid = (stop: ColorStop['stop']) => {
return stop == null || isNaN(stop);
};
-export const isInvalid = (colorStops: ColorStop[]) => {
+export const isInvalid = (
+ colorStops: ColorStop[],
+ showAlpha: boolean = false
+) => {
return colorStops.some(colorStop => {
- return isColorInvalid(colorStop.color) || isStopInvalid(colorStop.stop);
+ return (
+ isColorInvalid(colorStop.color, showAlpha) ||
+ isStopInvalid(colorStop.stop)
+ );
});
};
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,
diff --git a/src/components/color_picker/utils.test.ts b/src/components/color_picker/utils.test.ts
new file mode 100644
index 00000000000..95a1359509c
--- /dev/null
+++ b/src/components/color_picker/utils.test.ts
@@ -0,0 +1,144 @@
+import { chromaValid, getChromaColor, parseColor } from './utils';
+
+describe('parseColor', () => {
+ test('hex-like', () => {
+ expect(parseColor('#')).toBe('#');
+ expect(parseColor('#0')).toBe('#0');
+ expect(parseColor('#00')).toBe('#00');
+ expect(parseColor('#000')).toBe('#000');
+ expect(parseColor('#0000')).toBe('#0000');
+ expect(parseColor('#00000')).toBe('#00000');
+ expect(parseColor('#000000')).toBe('#000000');
+ expect(parseColor('#0000000')).toBe('#0000000');
+ expect(parseColor('#00000000')).toBe('#00000000');
+ expect(parseColor('#000000000')).toBe('#000000000');
+
+ expect(parseColor('000')).toBe('000');
+ expect(parseColor('000000')).toBe('000000');
+
+ expect(parseColor('#JKJ')).toBe('#JKJ');
+ expect(parseColor('#JKJKJK')).toBe('#JKJKJK');
+ expect(parseColor('#JKJKJK00')).toBe('#JKJKJK00');
+ });
+ test('comma separated', () => {
+ expect(parseColor('0,')).toBe('');
+ expect(parseColor('0,0')).toBe('');
+ expect(parseColor('0,0,')).toBe('');
+ expect(parseColor('0,0,0')).toEqual([0, 0, 0]);
+ expect(parseColor('0,0,0,')).toEqual([0, 0, 0]);
+ expect(parseColor('0,0,0,0')).toEqual([0, 0, 0, 0]);
+ expect(parseColor('0,0,0,0,')).toEqual([0, 0, 0, 0]);
+ expect(parseColor('0,0,0,0,0')).toBe('');
+
+ expect(parseColor('0, 0, 0, 0')).toEqual([0, 0, 0, 0]);
+ });
+ test('color names', () => {
+ expect(parseColor('red')).toBe('red');
+ });
+ test('nonsensical', () => {
+ expect(parseColor('test')).toBe('test');
+ });
+ test('null or empty', () => {
+ expect(parseColor(null)).toBe(null);
+ expect(parseColor('')).toBe(null);
+ });
+});
+
+describe('chromaValid', () => {
+ test('hex-like', () => {
+ expect(chromaValid('#')).toBe(false);
+ expect(chromaValid('#0')).toBe(false);
+ expect(chromaValid('#00')).toBe(false);
+ expect(chromaValid('#000')).toBe(true);
+ expect(chromaValid('#0000')).toBe(false);
+ expect(chromaValid('#00000')).toBe(false);
+ expect(chromaValid('#000000')).toBe(true);
+ expect(chromaValid('#0000000')).toBe(false);
+ expect(chromaValid('#00000000')).toBe(true);
+ expect(chromaValid('#000000000')).toBe(false);
+
+ expect(chromaValid('000')).toBe(true);
+ expect(chromaValid('000000')).toBe(true);
+ expect(chromaValid('00000000')).toBe(true);
+
+ expect(chromaValid('#JKJ')).toBe(false);
+ expect(chromaValid('#JKJKJK')).toBe(false);
+ expect(chromaValid('#JKJKJK00')).toBe(false);
+ });
+ test('comma separated', () => {
+ expect(chromaValid('0,')).toBe(false);
+ expect(chromaValid('0,0')).toBe(false);
+ expect(chromaValid('0,0,')).toBe(false);
+ expect(chromaValid('0,0,0')).toBe(true);
+ expect(chromaValid('0,0,0,')).toBe(true);
+ expect(chromaValid('0,0,0,0')).toBe(true);
+ expect(chromaValid('0,0,0,0,')).toBe(true);
+ expect(chromaValid('0,0,0,0,0')).toBe(false);
+
+ expect(chromaValid('0, 0, 0, 0')).toBe(true);
+
+ expect(chromaValid([0, 0, 0])).toBe(true);
+ expect(chromaValid([0, 0, 0, 0])).toBe(true);
+ });
+ test('color names', () => {
+ expect(chromaValid('red')).toBe(false);
+ });
+ test('nonsensical', () => {
+ expect(chromaValid('test')).toBe(false);
+ });
+ test('empty', () => {
+ expect(chromaValid('')).toBe(false);
+ });
+});
+
+// chroma-js does not expose its `Color` class, so using `.toBeInstanceOf` is not possible.
+// It also adds custom methods to returned arrays and objects making equality checks difficult.
+// Thus, using resulting class methods (rgba()) to check return value
+describe('getChromaColor', () => {
+ test('hex-like', () => {
+ expect(getChromaColor('#')).toBe(null);
+ expect(getChromaColor('#0')).toBe(null);
+ expect(getChromaColor('#00')).toBe(null);
+ expect(getChromaColor('#000')!.rgba()).toEqual([0, 0, 0, 1]);
+ expect(getChromaColor('#0000')).toBe(null);
+ expect(getChromaColor('#00000')).toBe(null);
+ expect(getChromaColor('#000000')!.rgba()).toEqual([0, 0, 0, 1]);
+ expect(getChromaColor('#0000000')).toBe(null);
+ expect(getChromaColor('#00000000')).toBe(null);
+ expect(getChromaColor('#00000000', true)!.rgba()).toEqual([0, 0, 0, 0]);
+ expect(getChromaColor('#000000000')).toBe(null);
+
+ expect(getChromaColor('000')!.rgba()).toEqual([0, 0, 0, 1]);
+ expect(getChromaColor('000000')!.rgba()).toEqual([0, 0, 0, 1]);
+
+ expect(getChromaColor('00000000')).toBe(null);
+ expect(getChromaColor('00000000', true)!.rgba()).toEqual([0, 0, 0, 0]);
+
+ expect(getChromaColor('#JKJ')).toBe(null);
+ expect(getChromaColor('#JKJKJK')).toBe(null);
+ expect(getChromaColor('#JKJKJK00')).toBe(null);
+ });
+ test('comma separated', () => {
+ expect(getChromaColor('0,')).toBe(null);
+ expect(getChromaColor('0,0')).toBe(null);
+ expect(getChromaColor('0,0,')).toBe(null);
+ expect(getChromaColor('0,0,0')!.rgba()).toEqual([0, 0, 0, 1]);
+ expect(getChromaColor('0,0,0,')!.rgba()).toEqual([0, 0, 0, 1]);
+ expect(getChromaColor('0,0,0,0')).toBe(null);
+ expect(getChromaColor('0,0,0,0', true)!.rgba()).toEqual([0, 0, 0, 0]);
+ expect(getChromaColor('0,0,0,0,')).toBe(null);
+ expect(getChromaColor('0,0,0,0,0')).toBe(null);
+
+ expect(getChromaColor('0, 0, 0, 0')).toBe(null);
+ expect(getChromaColor('0, 0, 0, 0', true)!.rgba()).toEqual([0, 0, 0, 0]);
+ });
+ test('color names', () => {
+ expect(getChromaColor('red')).toBe(null);
+ });
+ test('nonsensical', () => {
+ expect(getChromaColor('test')).toBe(null);
+ });
+ test('empty', () => {
+ expect(getChromaColor('')).toBe(null);
+ });
+});
diff --git a/src/components/color_picker/utils.ts b/src/components/color_picker/utils.ts
index cc326471e59..3d805072665 100644
--- a/src/components/color_picker/utils.ts
+++ b/src/components/color_picker/utils.ts
@@ -1,4 +1,5 @@
import { MouseEvent as ReactMouseEvent, TouchEvent, useEffect } from 'react';
+import chroma, { ColorSpaces } from 'chroma-js';
export const getEventPosition = (
location: { x: number; y: number },
@@ -80,3 +81,59 @@ export function useMouseMove(
return [handleMouseDown, handleInteraction];
}
+
+export const HEX_FALLBACK = '';
+export const HSV_FALLBACK: ColorSpaces['hsv'] = [0, 0, 0];
+export const RGB_FALLBACK: ColorSpaces['rgba'] = [NaN, NaN, NaN, 1];
+export const RGB_JOIN = ', ';
+
+// Given a string, this attempts to return a format that can be consumed by chroma-js
+export const parseColor = (input?: string | null) => {
+ let parsed: string | number[];
+ if (!input) return null;
+ if (input.indexOf(',') > 0) {
+ if (!/^[\s,.0-9]*$/.test(input)) {
+ return null;
+ }
+ const rgb = input
+ .trim()
+ .split(',')
+ .filter(n => n !== '')
+ .map(Number);
+ parsed = rgb.length > 2 && rgb.length < 5 ? rgb : HEX_FALLBACK;
+ } else {
+ parsed = input;
+ }
+ return parsed;
+};
+
+// Returns whether the given input will return a valid chroma-js object when designated as one of
+// the acceptable formats: hex, rgb, rgba
+export const chromaValid = (color: string | number[]) => {
+ let parsed: string | number[] | null = color;
+ if (typeof color === 'string') {
+ parsed = parseColor(color);
+ }
+
+ if (!parsed) return false;
+
+ if (typeof parsed === 'object') {
+ return chroma.valid(parsed, 'rgb') || chroma.valid(parsed, 'rgba');
+ }
+ return chroma.valid(color, 'hex');
+};
+
+// Given an input and opacity configuration, this returns a valid chroma-js object
+export const getChromaColor = (input?: string | null, allowOpacity = false) => {
+ const parsed = parseColor(input);
+ if (parsed && chromaValid(parsed)) {
+ // type guard for the function overload
+ const chromaColor =
+ typeof parsed === 'object' ? chroma(parsed) : chroma(parsed);
+ if (!allowOpacity && chromaColor.alpha() < 1) {
+ return null;
+ }
+ return chromaColor;
+ }
+ return null;
+};
diff --git a/src/components/form/range/_range_wrapper.scss b/src/components/form/range/_range_wrapper.scss
index 71b65977b19..d393d314d0d 100644
--- a/src/components/form/range/_range_wrapper.scss
+++ b/src/components/form/range/_range_wrapper.scss
@@ -1,6 +1,8 @@
/*
* 1. There's no way to target the layout of the extra input, so we must
* use the descendant selector to allow the width to shrink.
+ *
+ * 2. Prevent the prepend/append label from extending outside the parent element
*/
.euiRangeWrapper {
@@ -10,5 +12,9 @@
> .euiFormControlLayout { /* 1 */
width: auto;
+
+ &.euiFormControlLayout--group {
+ flex-shrink: 0; /* 2 */
+ }
}
}
diff --git a/yarn.lock b/yarn.lock
index 33aa9452898..0ae4cfaaf25 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1170,10 +1170,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"