From 1e0616264071bb124b7a7be18d913568c9cad939 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Fri, 20 Sep 2019 16:58:31 -0500 Subject: [PATCH 01/29] WIP --- .../color_picker/color_picker_example.js | 40 ++- .../src/views/color_picker/color_stops.js | 105 ++++++++ .../color_picker/_color_picker.scss | 3 +- src/components/color_picker/_index.scss | 1 + src/components/color_picker/_variables.scss | 1 + src/components/color_picker/color_picker.tsx | 4 - .../color_stops/_color_stops.scss | 24 ++ .../color_picker/color_stops/_index.scss | 1 + .../color_stops/color_stop_thumb.tsx | 227 ++++++++++++++++++ .../color_picker/color_stops/color_stops.tsx | 211 ++++++++++++++++ .../color_picker/color_stops/index.ts | 1 + .../color_picker/color_stops/utils.test.ts | 103 ++++++++ .../color_picker/color_stops/utils.ts | 63 +++++ src/components/color_picker/index.ts | 3 +- src/components/color_picker/saturation.tsx | 43 +--- src/components/color_picker/utils.ts | 49 ++++ src/components/form/range/range_thumb.tsx | 67 ++++-- src/components/form/range/range_wrapper.tsx | 41 ++-- src/components/index.js | 1 + 19 files changed, 900 insertions(+), 88 deletions(-) create mode 100644 src-docs/src/views/color_picker/color_stops.js create mode 100644 src/components/color_picker/color_stops/_color_stops.scss create mode 100644 src/components/color_picker/color_stops/_index.scss create mode 100644 src/components/color_picker/color_stops/color_stop_thumb.tsx create mode 100644 src/components/color_picker/color_stops/color_stops.tsx create mode 100644 src/components/color_picker/color_stops/index.ts create mode 100644 src/components/color_picker/color_stops/utils.test.ts create mode 100644 src/components/color_picker/color_stops/utils.ts 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 25006528e42..814372217ff 100644 --- a/src-docs/src/views/color_picker/color_picker_example.js +++ b/src-docs/src/views/color_picker/color_picker_example.js @@ -4,7 +4,12 @@ import { renderToHtml } from '../../services'; import { GuideSectionTypes } from '../../components'; -import { EuiCode, EuiColorPicker, EuiText } from '../../../../src/components'; +import { + EuiCode, + EuiColorPicker, + EuiColorStops, + EuiText, +} from '../../../../src/components'; import { ColorPicker } from './color_picker'; const colorPickerSource = require('!!raw-loader!./color_picker'); @@ -121,6 +126,15 @@ const kitchenSinkSnippet = ` `; +import { ColorStops } from './color_stops'; +const colorStopsSource = require('!!raw-loader!./color_stops'); +const colorStopsHtml = renderToHtml(ColorStops); +const colorStopsSnippet = ` +`; + export const ColorPickerExample = { title: 'Color Picker', intro: ( @@ -284,5 +298,29 @@ export const ColorPickerExample = { snippet: kitchenSinkSnippet, demo: , }, + { + title: 'Color stops', + source: [ + { + type: GuideSectionTypes.JS, + code: colorStopsSource, + }, + { + type: GuideSectionTypes.HTML, + code: colorStopsHtml, + }, + ], + props: { EuiColorStops }, + text: ( +

+ Use EuiColorStops to define color stops for data + driven styling. Stops are numbers in strictly ascending order. The + range is from the given stop number (inclusive) to the next stop + number (exclusive). +

+ ), + snippet: colorStopsSnippet, + demo: , + }, ], }; diff --git a/src-docs/src/views/color_picker/color_stops.js b/src-docs/src/views/color_picker/color_stops.js new file mode 100644 index 00000000000..c01245bcfb7 --- /dev/null +++ b/src-docs/src/views/color_picker/color_stops.js @@ -0,0 +1,105 @@ +import React, { useState } from 'react'; + +import { EuiColorStops, EuiFormRow } from '../../../../src/components'; + +export const ColorStops = () => { + const [colorStops, setColorStops] = useState([ + { + stop: 0, + color: '#ff0000', + }, + { + stop: 25, + color: '#FFFF00', + }, + { + stop: 35, + color: '#008000', + }, + ]); + + const handleChange = colorStops => { + setColorStops(colorStops); + }; + + const [extendedColorStops, setExtendedColorStops] = useState([ + { + stop: 100, + color: '#ff0000', + }, + { + stop: 250, + color: '#FFFF00', + }, + { + stop: 350, + color: '#008000', + }, + ]); + + const handleExtendedChange = colorStops => { + setExtendedColorStops(colorStops); + }; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/components/color_picker/_color_picker.scss b/src/components/color_picker/_color_picker.scss index 700b95c9780..b2521831ac1 100644 --- a/src/components/color_picker/_color_picker.scss +++ b/src/components/color_picker/_color_picker.scss @@ -2,8 +2,7 @@ .euiColorPicker { position: relative; - // 5 columns of swatches + margins + border - width: ($euiSizeL * 5) + ($euiSizeS * 4); + width: $euiColorPickerWidth; } diff --git a/src/components/color_picker/_index.scss b/src/components/color_picker/_index.scss index af6e9fd99cc..b42d746aef0 100644 --- a/src/components/color_picker/_index.scss +++ b/src/components/color_picker/_index.scss @@ -3,3 +3,4 @@ @import 'color_picker_swatch'; @import 'hue'; @import 'saturation'; +@import 'color_stops/index'; diff --git a/src/components/color_picker/_variables.scss b/src/components/color_picker/_variables.scss index 08cd1445f7b..3bc15347438 100644 --- a/src/components/color_picker/_variables.scss +++ b/src/components/color_picker/_variables.scss @@ -3,3 +3,4 @@ $euiColorPickerValueRange1: rgba(255, 255, 255, 0); $euiColorPickerSaturationRange0: rgba(0, 0, 0, 1); $euiColorPickerSaturationRange1: rgba(0, 0, 0, 0); $euiColorPickerIndicatorSize: $euiSizeM; +$euiColorPickerWidth: ($euiSizeL * 5) + ($euiSizeS * 4); // 5 columns of swatches + margins + border diff --git a/src/components/color_picker/color_picker.tsx b/src/components/color_picker/color_picker.tsx index d60163f471e..070f9262057 100644 --- a/src/components/color_picker/color_picker.tsx +++ b/src/components/color_picker/color_picker.tsx @@ -65,10 +65,6 @@ export interface EuiColorPickerProps * Custom validation flag */ isInvalid?: boolean; - /** - * Renders inline, without an input element or popover - */ - inline?: boolean; /** * Choose between swatches with gradient picker (default), swatches only, or gradient picker only. */ diff --git a/src/components/color_picker/color_stops/_color_stops.scss b/src/components/color_picker/color_stops/_color_stops.scss new file mode 100644 index 00000000000..b44e6cd1de5 --- /dev/null +++ b/src/components/color_picker/color_stops/_color_stops.scss @@ -0,0 +1,24 @@ +.euiColorStops { + &:focus { + outline: 2px solid $euiFocusRingColor; + } +} + +.euiColorStop { + width: $euiColorPickerWidth; +} + +.euiColorStopPopover.euiPopover { + position: absolute; + top: 50%; + margin-top: -8px; + margin-left: -8px; +} + +.euiColorStopThumb.euiRangeThumb { + position: static; + top: auto; + margin-top: 0; + pointer-events: auto; + cursor: pointer; +} diff --git a/src/components/color_picker/color_stops/_index.scss b/src/components/color_picker/color_stops/_index.scss new file mode 100644 index 00000000000..9338d35b24a --- /dev/null +++ b/src/components/color_picker/color_stops/_index.scss @@ -0,0 +1 @@ +@import 'color_stops'; diff --git a/src/components/color_picker/color_stops/color_stop_thumb.tsx b/src/components/color_picker/color_stops/color_stop_thumb.tsx new file mode 100644 index 00000000000..c091df75146 --- /dev/null +++ b/src/components/color_picker/color_stops/color_stop_thumb.tsx @@ -0,0 +1,227 @@ +import React, { + FunctionComponent, + RefObject, + useEffect, + useRef, + useState, +} from 'react'; + +import { CommonProps } from '../../common'; +import { isColorInvalid, isStopInvalid } from './utils'; +import { getEventPosition, useMouseMove } from '../utils'; + +import { EuiButtonIcon } from '../../button'; +import { EuiColorPicker, EuiColorPickerProps } from '../color_picker'; +import { EuiFlexGroup, EuiFlexItem } from '../../flex'; +// @ts-ignore +import { EuiFieldNumber, EuiFieldText, EuiFormRow } from '../../form'; +import { EuiRangeThumb } from '../../form/range/range_thumb'; +import { EuiPopover } from '../../popover'; +import { EuiSpacer } from '../../spacer'; + +export interface ColorStop { + stop: number; + color: string; +} + +interface EuiColorStopThumbProps extends CommonProps, ColorStop { + id?: string; + onChange: (colorStop: ColorStop) => void; + onFocus?: () => void; + onRemove?: () => void; + globalMin: number; + globalMax: number; + min: number; + max: number; + parentRef: RefObject; + colorPickerMode: EuiColorPickerProps['mode']; + colorPickerSwatches?: EuiColorPickerProps['swatches']; +} + +export const EuiColorStopThumb: FunctionComponent = ({ + id, + stop, + color, + onChange, + onFocus, + onRemove, + globalMin, + globalMax, + min, + max, + parentRef, + colorPickerMode, + colorPickerSwatches, +}) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [colorIsInvalid, setColorIsInvalid] = useState(isColorInvalid(color)); + const [stopIsInvalid, setStopIsInvalid] = useState(isStopInvalid(stop)); + const [numberInputRef, setNumberInputRef] = useState(); + const popoverRef = useRef(null); + + useEffect(() => { + if (isPopoverOpen && popoverRef && popoverRef.current) { + popoverRef.current.positionPopoverFixed(); + } + }, [stop]); + + const openPopover = () => setIsPopoverOpen(true); + + const closePopover = () => setIsPopoverOpen(false); + + const handleColorChange = (value: ColorStop['color']) => { + setColorIsInvalid(isColorInvalid(value)); + propagateChange({ stop, color: value }); + }; + + const handleStopChange = ( + value: ColorStop['stop'], + isDrag: boolean = false + ) => { + const willBeInvalid = value > max || value < min; + + if (isDrag && willBeInvalid) { + if (value > max) { + value = max; + } + if (value < min) { + value = min; + } + } + setStopIsInvalid(isStopInvalid(value)); + propagateChange({ stop: value, color }); + }; + + const handleStopInputChange = (value: ColorStop['stop']) => { + const willBeInvalid = value > globalMax || value < globalMin; + + if (willBeInvalid) { + if (value > globalMax) { + value = globalMax; + } + if (value < globalMin) { + value = globalMin; + } + } + setStopIsInvalid(isStopInvalid(value)); + propagateChange({ stop: value, color }); + }; + + const propagateChange = (newColor: ColorStop) => { + onChange(newColor); + }; + + const handleChange = ( + location: { x: number; y: number }, + isFirstInteraction?: boolean + ) => { + if (isFirstInteraction) return; + if (parentRef == null || parentRef.current == null) { + return; + } + const box = getEventPosition(location, parentRef.current); + const newStop = Math.round((box.left / box.width) * 100); + handleStopChange(newStop, true); + }; + + const [handleMouseDown, handleInteraction] = useMouseMove( + handleChange + ); + + return ( + + }> +
+ + + + ) => + handleStopInputChange(parseFloat(e.target.value)) + } + /> + + + + + + + + + + + {colorPickerMode !== 'swatch' && ( + + + + ) => + handleColorChange(e.target.value) + } + /> + + + )} +
+
+ ); +}; diff --git a/src/components/color_picker/color_stops/color_stops.tsx b/src/components/color_picker/color_stops/color_stops.tsx new file mode 100644 index 00000000000..559eed67537 --- /dev/null +++ b/src/components/color_picker/color_stops/color_stops.tsx @@ -0,0 +1,211 @@ +import React, { FunctionComponent, useRef, useState } from 'react'; +import classNames from 'classnames'; + +import { CommonProps } from '../../common'; +import { keyCodes } from '../../../services'; +import { EuiColorStopThumb, ColorStop } from './color_stop_thumb'; +import { DEFAULT_COLOR, addStop, removeStop, isInvalid } from './utils'; + +import { EuiColorPickerProps } from '../'; +import { EuiRangeWrapper } from '../../form/range/range_wrapper'; + +interface EuiColorStopsProps extends CommonProps { + colorStops?: ColorStop[]; + onChange: (stops?: ColorStop[], isInvalid?: boolean) => void; + fullWidth?: boolean; + className?: string; + max: number; + min: number; + stopType?: 'fixed' | 'gradient'; + mode?: EuiColorPickerProps['mode']; + swatches?: EuiColorPickerProps['swatches']; +} + +export const EuiColorStops: FunctionComponent = ({ + max, + min, + mode = 'default', + colorStops = [{ stop: 0, color: DEFAULT_COLOR }], + onChange, + fullWidth, + className, + stopType = 'gradient', + swatches, +}) => { + const [hasFocus, setHasFocus] = useState(false); + const [focusedStop, setFocusedStop] = useState(null); + const wrapperRef = useRef(null); + const classes = classNames('euiColorStops', className); + + const handleOnChange = (colorStops: ColorStop[]) => { + onChange(colorStops, isInvalid(colorStops)); + }; + + const handleStopChange = (stop: ColorStop, id: number) => { + const newColorStops = [...colorStops]; + newColorStops.splice(id, 1, stop); + handleOnChange(newColorStops); + }; + + const onFocusStop = (index: number) => { + let toFocus; + if (wrapperRef) { + if (wrapperRef.current != null) { + toFocus = wrapperRef.current.querySelector( + `#stop_${index}` + ); + } + } + if (toFocus) { + setHasFocus(false); + setFocusedStop(index); + toFocus.focus(); + } + }; + + const onAdd = (index: number = colorStops.length - 1) => { + const newColorStops = addStop(colorStops, index); + + handleOnChange(newColorStops); + }; + + const onRemove = (index: number) => { + const newColorStops = removeStop(colorStops, index); + + setFocusedStop(null); + if (wrapperRef.current) { + wrapperRef.current.focus(); + } + handleOnChange(newColorStops); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + const target = e.target as HTMLElement; + switch (e.keyCode) { + case keyCodes.TAB: + break; + + case keyCodes.ENTER: + if (!hasFocus) return; + onAdd(); + break; + + case keyCodes.BACKSPACE: + if (hasFocus || focusedStop == null) return; + const index = sortedStops[focusedStop].id; + onRemove(index); + break; + + case keyCodes.UP: + if (target === wrapperRef.current || target.id.indexOf('stop_') > -1) { + e.preventDefault(); + if (focusedStop == null) { + onFocusStop(0); + } else { + const next = + focusedStop === sortedStops.length - 1 + ? focusedStop + : focusedStop + 1; + onFocusStop(next); + } + } + break; + + case keyCodes.DOWN: + if (target === wrapperRef.current || target.id.indexOf('stop_') > -1) { + e.preventDefault(); + if (focusedStop == null) { + onFocusStop(0); + } else { + const next = focusedStop === 0 ? focusedStop : focusedStop - 1; + onFocusStop(next); + } + } + break; + } + }; + + const sortedStops = colorStops + .map((el, index) => { + return { ...el, id: index }; + }) + .sort((a, b) => a.stop - b.stop); + + const thumbs = sortedStops.map((colorStop, index) => ( + 1 ? () => onRemove(colorStop.id) : undefined + } + onChange={stop => handleStopChange(stop, colorStop.id)} + onFocus={() => setFocusedStop(index)} + parentRef={wrapperRef} + colorPickerMode={mode} + colorPickerSwatches={swatches} + /> + )); + + const positions = sortedStops.map(colorStop => + Math.round(((colorStop.stop - min) / (max - min)) * 100) + ); + const gradientStops = (colorStop: ColorStop, index: number) => { + return `${colorStop.color} ${positions[index]}%`; + }; + const fixedStops = (colorStop: ColorStop, index: number) => { + if (index === 0) { + return `${colorStop.color}, ${colorStop.color} ${positions[index + 1]}%`; + } else if (index === sortedStops.length - 1) { + return `${colorStop.color} ${positions[index]}%`; + } else { + return `${colorStop.color} ${positions[index]}%, ${colorStop.color} ${ + positions[index + 1] + }%`; + } + }; + const linearGradient = sortedStops.map( + stopType === 'gradient' ? gradientStops : fixedStops + ); + const background = + sortedStops.length > 1 + ? `linear-gradient(to right,${linearGradient})` + : sortedStops[0].color; + + return ( + console.log(e)} + onFocus={e => { + if (e.target === wrapperRef.current) { + setHasFocus(true); + } + }} + onBlur={() => setHasFocus(false)}> + {/* TODO: Use euiRangeHighlight / euiRangeTrack */} +
+
+
+
+ {thumbs} +
+ + ); +}; diff --git a/src/components/color_picker/color_stops/index.ts b/src/components/color_picker/color_stops/index.ts new file mode 100644 index 00000000000..80a8c6f1895 --- /dev/null +++ b/src/components/color_picker/color_stops/index.ts @@ -0,0 +1 @@ +export { EuiColorStops } from './color_stops'; diff --git a/src/components/color_picker/color_stops/utils.test.ts b/src/components/color_picker/color_stops/utils.test.ts new file mode 100644 index 00000000000..7f7bf2b18d8 --- /dev/null +++ b/src/components/color_picker/color_stops/utils.test.ts @@ -0,0 +1,103 @@ +import { addStop, removeStop, isInvalid } from './utils'; + +const colorStops = [ + { stop: 0, color: '#FF0000' }, + { stop: 25, color: '#00FF00' }, + { stop: 35, color: '#0000FF' }, +]; + +describe('isInvalid', () => { + test('Should not mark valid colorStops as invalid', () => { + expect(isInvalid(colorStops)).toBe(false); + }); + + test('Should mark colorStops missing color as invalid', () => { + const colorStops = [{ stop: 0, color: '' }]; + expect(isInvalid(colorStops)).toBe(true); + }); + + test('Should mark colorStops with invalid color as invalid', () => { + const colorStops = [{ stop: 0, color: 'not color' }]; + expect(isInvalid(colorStops)).toBe(true); + }); + + test('Should mark colorStops missing stop as invalid', () => { + const colorStops = [{ stop: '', color: '#FF0000' }]; + // Intentionally wrong + // @ts-ignore + expect(isInvalid(colorStops)).toBe(true); + }); + + test('Should mark colorStops with invalid stop as invalid', () => { + const colorStops = [{ stop: 'I am not a number', color: '#FF0000' }]; + // Intentionally wrong + // @ts-ignore + expect(isInvalid(colorStops)).toBe(true); + }); + + test('Should mark colorStops with descending stops as invalid', () => { + const colorStops = [ + { stop: 10, color: '#FF0000' }, + { stop: 0, color: '#00FF00' }, + ]; + expect(isInvalid(colorStops)).toBe(true); + }); +}); + +describe('addStop', () => { + test('Should add row when there is only a single row', () => { + const colorStops = [{ stop: 0, color: '#FF0000' }]; + expect(addStop(colorStops, 0)).toEqual([ + { stop: 0, color: '#FF0000' }, + { stop: 1, color: '#FF0000' }, + ]); + }); + + describe('to middle of list', () => { + test('Should add row after first item', () => { + expect(addStop(colorStops, 0)).toEqual([ + { stop: 0, color: '#FF0000' }, + { stop: 12.5, color: '#FF0000' }, + { stop: 25, color: '#00FF00' }, + { stop: 35, color: '#0000FF' }, + ]); + }); + + test('Should add row after second item', () => { + expect(addStop(colorStops, 1)).toEqual([ + { stop: 0, color: '#FF0000' }, + { stop: 25, color: '#00FF00' }, + { stop: 30, color: '#FF0000' }, + { stop: 35, color: '#0000FF' }, + ]); + }); + }); + + test('Should add row to end of list', () => { + expect(addStop(colorStops, 2)).toEqual([ + { stop: 0, color: '#FF0000' }, + { stop: 25, color: '#00FF00' }, + { stop: 35, color: '#0000FF' }, + { stop: 45, color: '#FF0000' }, + ]); + }); +}); + +describe('removeStop', () => { + test('Should not remove last row', () => { + const colorStops = [{ stop: 0, color: '#FF0000' }]; + expect(removeStop(colorStops, 0)).toEqual(colorStops); + }); + + test('Should remove row at index', () => { + const colorStops = [ + { stop: 0, color: '#FF0000' }, + { stop: 25, color: '#00FF00' }, + { stop: 35, color: '#0000FF' }, + ]; + expect(removeStop(colorStops, 1)).toEqual([ + { stop: 0, color: '#FF0000' }, + { stop: 35, color: '#0000FF' }, + ]); + }); +}); diff --git a/src/components/color_picker/color_stops/utils.ts b/src/components/color_picker/color_stops/utils.ts new file mode 100644 index 00000000000..ff63dabad6f --- /dev/null +++ b/src/components/color_picker/color_stops/utils.ts @@ -0,0 +1,63 @@ +import { isValidHex } from '../../../services'; +import { ColorStop } from './color_stop_thumb'; + +export const DEFAULT_COLOR = '#FF0000'; + +export const removeStop = (colorStops: ColorStop[], index: number) => { + if (colorStops.length === 1) { + return colorStops; + } + + return [...colorStops.slice(0, index), ...colorStops.slice(index + 1)]; +}; + +export const addStop = (colorStops: ColorStop[], index: number) => { + const currentStop = colorStops[index].stop; + let delta = 1; + if (index === colorStops.length - 1) { + // Adding stop to end of list. + if (index !== 0) { + const prevStop = colorStops[index - 1].stop; + delta = currentStop - prevStop; + } + } else { + // Adding stop in middle of list. + const nextStop = colorStops[index + 1].stop; + delta = (nextStop - currentStop) / 2; + } + + const newStop = { + stop: currentStop + delta, + color: DEFAULT_COLOR, + }; + return [ + ...colorStops.slice(0, index + 1), + newStop, + ...colorStops.slice(index + 1), + ]; +}; + +export const isColorInvalid = (color: string) => { + return !isValidHex(color) || color === ''; +}; + +export const isStopInvalid = (stop: ColorStop['stop']) => { + return stop == null || isNaN(stop); +}; + +export const isInvalid = (colorStops: ColorStop[]) => { + return colorStops.some((colorStop, index) => { + // expect stops to be in ascending order + let isDescending = false; + if (index !== 0) { + const prevStop = colorStops[index - 1].stop; + isDescending = prevStop >= colorStop.stop; + } + + return ( + isColorInvalid(colorStop.color) || + isStopInvalid(colorStop.stop) || + isDescending + ); + }); +}; diff --git a/src/components/color_picker/index.ts b/src/components/color_picker/index.ts index 382690c914e..102816c0855 100644 --- a/src/components/color_picker/index.ts +++ b/src/components/color_picker/index.ts @@ -1,4 +1,5 @@ -export { EuiColorPicker } from './color_picker'; +export { EuiColorPicker, EuiColorPickerProps } from './color_picker'; export { EuiColorPickerSwatch } from './color_picker_swatch'; export { EuiHue } from './hue'; export { EuiSaturation } from './saturation'; +export { EuiColorStops } from './color_stops'; diff --git a/src/components/color_picker/saturation.tsx b/src/components/color_picker/saturation.tsx index d96fe3e0073..1bff224ee7d 100644 --- a/src/components/color_picker/saturation.tsx +++ b/src/components/color_picker/saturation.tsx @@ -1,8 +1,6 @@ import React, { HTMLAttributes, KeyboardEvent, - MouseEvent as ReactMouseEvent, - TouchEvent, forwardRef, useEffect, useRef, @@ -17,13 +15,7 @@ import { isNil } from '../../services/predicate'; import { EuiScreenReaderOnly } from '../accessibility'; import { EuiI18n } from '../i18n'; -import { getEventPosition, throttle } from './utils'; - -function isMouseEvent( - event: ReactMouseEvent | TouchEvent -): event is ReactMouseEvent { - return typeof event === 'object' && 'pageX' in event && 'pageY' in event; -} +import { getEventPosition, useMouseMove } from './utils'; export type SaturationClientRect = Pick< ClientRect, @@ -82,11 +74,6 @@ export const EuiSaturation = forwardRef( } }, [color]); - useEffect(() => { - // Mimic `componentWillUnmount` - return unbindEventListeners; - }, []); - const calculateColor = ({ top, height, @@ -113,30 +100,10 @@ export const EuiSaturation = forwardRef( const box = getEventPosition(location, boxRef.current); handleUpdate(box); }; - const handleInteraction = ( - e: ReactMouseEvent | TouchEvent - ) => { - if (e && boxRef.current) { - const x = isMouseEvent(e) ? e.pageX : e.touches[0].pageX; - const y = isMouseEvent(e) ? e.pageY : e.touches[0].pageY; - handleChange({ x, y }); - } - }; - const handleMouseMove = throttle((e: MouseEvent) => { - handleChange({ x: e.pageX, y: e.pageY }); - }); - const handleMouseUp = () => { - unbindEventListeners(); - }; - const handleMouseDown = (e: ReactMouseEvent) => { - handleInteraction(e); - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); - }; - const unbindEventListeners = () => { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); - }; + const [handleMouseDown, handleInteraction] = useMouseMove( + handleChange, + boxRef.current + ); const handleKeyDown = (e: KeyboardEvent) => { if (isNil(boxRef) || isNil(boxRef.current)) { return; diff --git a/src/components/color_picker/utils.ts b/src/components/color_picker/utils.ts index 7716d1a8dc0..c47c72047fe 100644 --- a/src/components/color_picker/utils.ts +++ b/src/components/color_picker/utils.ts @@ -1,3 +1,5 @@ +import { MouseEvent as ReactMouseEvent, TouchEvent, useEffect } from 'react'; + export const getEventPosition = ( location: { x: number; y: number }, container: HTMLElement @@ -31,3 +33,50 @@ export const throttle = (fn: (...args: any[]) => void, wait = 50) => { } }; }; + +export function isMouseEvent( + event: ReactMouseEvent | TouchEvent +): event is ReactMouseEvent { + return typeof event === 'object' && 'pageX' in event && 'pageY' in event; +} + +export function useMouseMove( + handleChange: ( + location: { x: number; y: number }, + isFirstInteraction?: boolean + ) => void, + interactionConditional: any = true +): [ + (e: ReactMouseEvent) => void, + (e: ReactMouseEvent | TouchEvent, isFirstInteraction?: boolean) => void +] { + useEffect(() => { + return unbindEventListeners; + }, []); + const handleInteraction = ( + e: ReactMouseEvent | TouchEvent, + isFirstInteraction?: boolean + ) => { + if (e) { + if (interactionConditional) { + const x = isMouseEvent(e) ? e.pageX : e.touches[0].pageX; + const y = isMouseEvent(e) ? e.pageY : e.touches[0].pageY; + handleChange({ x, y }, isFirstInteraction); + } + } + }; + const handleMouseMove = throttle((e: ReactMouseEvent) => { + handleChange({ x: e.pageX, y: e.pageY }, false); + }); + const handleMouseDown = (e: ReactMouseEvent) => { + handleInteraction(e, true); + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', unbindEventListeners); + }; + const unbindEventListeners = () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', unbindEventListeners); + }; + + return [handleMouseDown, handleInteraction]; +} diff --git a/src/components/form/range/range_thumb.tsx b/src/components/form/range/range_thumb.tsx index 1bcfa532644..8e850f39ef6 100644 --- a/src/components/form/range/range_thumb.tsx +++ b/src/components/form/range/range_thumb.tsx @@ -1,40 +1,61 @@ import React, { FunctionComponent, HTMLAttributes } from 'react'; import classNames from 'classnames'; -import { CommonProps } from '../../common'; +import { CommonProps, ExclusiveUnion, Omit } from '../../common'; -export type EuiRangeThumbProps = HTMLAttributes & - CommonProps & { - min: number; - max: number; - value?: number | string; - disabled?: boolean; - showInput?: boolean; - showTicks?: boolean; - }; +interface BaseProps extends CommonProps { + min: number; + max: number; + value?: number | string; + disabled?: boolean; + showInput?: boolean; + showTicks?: boolean; + thumbRef?: (node: HTMLButtonElement | null) => void; +} + +interface ButtonLike extends BaseProps, HTMLAttributes {} +interface DivLike + extends BaseProps, + Omit, 'onClick'> {} + +export type EuiRangeThumbProps = ExclusiveUnion; export const EuiRangeThumb: FunctionComponent = ({ + className, min, max, value, disabled, showInput, showTicks, + onClick, + tabIndex, ...rest }) => { - const classes = classNames('euiRangeThumb', { - 'euiRangeThumb--hasTicks': showTicks, - }); - return ( -
} /> + ) : ( +
} /> ); }; diff --git a/src/components/form/range/range_wrapper.tsx b/src/components/form/range/range_wrapper.tsx index 8179bab0239..16bc0a9fc7f 100644 --- a/src/components/form/range/range_wrapper.tsx +++ b/src/components/form/range/range_wrapper.tsx @@ -1,26 +1,29 @@ -import React, { FunctionComponent } from 'react'; +import React, { HTMLAttributes, forwardRef } from 'react'; import classNames from 'classnames'; +import { CommonProps } from '../../common'; -export interface EuiRangeWrapperProps { - className?: string; +export interface EuiRangeWrapperProps + extends CommonProps, + HTMLAttributes { fullWidth?: boolean; compressed?: boolean; } -export const EuiRangeWrapper: FunctionComponent = ({ - children, - className, - fullWidth, - compressed, -}) => { - const classes = classNames( - 'euiRangeWrapper', - { - 'euiRangeWrapper--fullWidth': fullWidth, - 'euiRangeWrapper--compressed': compressed, - }, - className - ); +export const EuiRangeWrapper = forwardRef( + ({ children, className, fullWidth, compressed, ...rest }, ref) => { + const classes = classNames( + 'euiRangeWrapper', + { + 'euiRangeWrapper--fullWidth': fullWidth, + 'euiRangeWrapper--compressed': compressed, + }, + className + ); - return
{children}
; -}; + return ( +
+ {children} +
+ ); + } +); diff --git a/src/components/index.js b/src/components/index.js index 5d93ff1ab84..7474571ad50 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -29,6 +29,7 @@ export { EuiCodeEditor } from './code_editor'; export { EuiColorPicker, EuiColorPickerSwatch, + EuiColorStops, EuiHue, EuiSaturation, } from './color_picker'; From 317c77d11b6f7071d0720a663256c37f7dd8edd4 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Mon, 23 Sep 2019 08:51:41 -0500 Subject: [PATCH 02/29] WIP: scaling clean up --- .../color_stops/_color_stops.scss | 1 - .../color_stops/color_stop_thumb.tsx | 21 ++++------ .../color_picker/color_stops/color_stops.tsx | 42 ++++++++++++------- .../color_picker/color_stops/utils.ts | 6 +++ 4 files changed, 41 insertions(+), 29 deletions(-) diff --git a/src/components/color_picker/color_stops/_color_stops.scss b/src/components/color_picker/color_stops/_color_stops.scss index b44e6cd1de5..9c7b99bfb9b 100644 --- a/src/components/color_picker/color_stops/_color_stops.scss +++ b/src/components/color_picker/color_stops/_color_stops.scss @@ -12,7 +12,6 @@ position: absolute; top: 50%; margin-top: -8px; - margin-left: -8px; } .euiColorStopThumb.euiRangeThumb { 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 c091df75146..d26768235b8 100644 --- a/src/components/color_picker/color_stops/color_stop_thumb.tsx +++ b/src/components/color_picker/color_stops/color_stop_thumb.tsx @@ -1,13 +1,7 @@ -import React, { - FunctionComponent, - RefObject, - useEffect, - useRef, - useState, -} from 'react'; +import React, { FunctionComponent, useEffect, useRef, useState } from 'react'; import { CommonProps } from '../../common'; -import { isColorInvalid, isStopInvalid } from './utils'; +import { isColorInvalid, isStopInvalid, calculateScale } from './utils'; import { getEventPosition, useMouseMove } from '../utils'; import { EuiButtonIcon } from '../../button'; @@ -33,7 +27,7 @@ interface EuiColorStopThumbProps extends CommonProps, ColorStop { globalMax: number; min: number; max: number; - parentRef: RefObject; + parentRef?: HTMLDivElement | null; colorPickerMode: EuiColorPickerProps['mode']; colorPickerSwatches?: EuiColorPickerProps['swatches']; } @@ -116,10 +110,10 @@ export const EuiColorStopThumb: FunctionComponent = ({ isFirstInteraction?: boolean ) => { if (isFirstInteraction) return; - if (parentRef == null || parentRef.current == null) { + if (parentRef == null) { return; } - const box = getEventPosition(location, parentRef.current); + const box = getEventPosition(location, parentRef); const newStop = Math.round((box.left / box.width) * 100); handleStopChange(newStop, true); }; @@ -139,7 +133,8 @@ export const EuiColorStopThumb: FunctionComponent = ({ initialFocus={numberInputRef} style={{ left: `${Math.round( - ((stop - globalMin) / (globalMax - globalMin)) * 100 + ((stop - globalMin) / (globalMax - globalMin)) * + calculateScale(parentRef ? parentRef.clientWidth : 100) )}%`, }} button={ @@ -161,7 +156,7 @@ export const EuiColorStopThumb: FunctionComponent = ({ /> }>
- + = ({ }) => { const [hasFocus, setHasFocus] = useState(false); const [focusedStop, setFocusedStop] = useState(null); - const wrapperRef = useRef(null); + const [wrapperRef, setWrapperRef] = useState< + HTMLDivElement | null | undefined + >(null); const classes = classNames('euiColorStops', className); const handleOnChange = (colorStops: ColorStop[]) => { @@ -50,10 +58,8 @@ export const EuiColorStops: FunctionComponent = ({ const onFocusStop = (index: number) => { let toFocus; if (wrapperRef) { - if (wrapperRef.current != null) { - toFocus = wrapperRef.current.querySelector( - `#stop_${index}` - ); + if (wrapperRef != null) { + toFocus = wrapperRef.querySelector(`#stop_${index}`); } } if (toFocus) { @@ -73,8 +79,8 @@ export const EuiColorStops: FunctionComponent = ({ const newColorStops = removeStop(colorStops, index); setFocusedStop(null); - if (wrapperRef.current) { - wrapperRef.current.focus(); + if (wrapperRef) { + wrapperRef.focus(); } handleOnChange(newColorStops); }; @@ -97,7 +103,7 @@ export const EuiColorStops: FunctionComponent = ({ break; case keyCodes.UP: - if (target === wrapperRef.current || target.id.indexOf('stop_') > -1) { + if (target === wrapperRef || target.id.indexOf('stop_') > -1) { e.preventDefault(); if (focusedStop == null) { onFocusStop(0); @@ -112,7 +118,7 @@ export const EuiColorStops: FunctionComponent = ({ break; case keyCodes.DOWN: - if (target === wrapperRef.current || target.id.indexOf('stop_') > -1) { + if (target === wrapperRef || target.id.indexOf('stop_') > -1) { e.preventDefault(); if (focusedStop == null) { onFocusStop(0); @@ -127,7 +133,10 @@ export const EuiColorStops: FunctionComponent = ({ const sortedStops = colorStops .map((el, index) => { - return { ...el, id: index }; + return { + ...el, + id: index, + }; }) .sort((a, b) => a.stop - b.stop); @@ -155,7 +164,10 @@ export const EuiColorStops: FunctionComponent = ({ )); const positions = sortedStops.map(colorStop => - Math.round(((colorStop.stop - min) / (max - min)) * 100) + Math.round( + ((colorStop.stop - min) / (max - min)) * + calculateScale(wrapperRef ? wrapperRef.clientWidth : 100) + ) ); const gradientStops = (colorStop: ColorStop, index: number) => { return `${colorStop.color} ${positions[index]}%`; @@ -181,14 +193,14 @@ export const EuiColorStops: FunctionComponent = ({ return ( console.log(e)} onFocus={e => { - if (e.target === wrapperRef.current) { + if (e.target === wrapperRef) { setHasFocus(true); } }} diff --git a/src/components/color_picker/color_stops/utils.ts b/src/components/color_picker/color_stops/utils.ts index ff63dabad6f..97d37f5005a 100644 --- a/src/components/color_picker/color_stops/utils.ts +++ b/src/components/color_picker/color_stops/utils.ts @@ -61,3 +61,9 @@ export const isInvalid = (colorStops: ColorStop[]) => { ); }); }; + +export const calculateScale = (trackWidth: number) => { + const EUI_THUMB_SIZE = 16; + const thumbToTrackRatio = EUI_THUMB_SIZE / trackWidth; + return (1 - thumbToTrackRatio) * 100; +}; From 68c21ef0c198e0be55fe5d20d1a8605c71954e0b Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Mon, 23 Sep 2019 09:34:13 -0500 Subject: [PATCH 03/29] reverse up/down keys --- src/components/color_picker/color_stops/color_stops.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/color_picker/color_stops/color_stops.tsx b/src/components/color_picker/color_stops/color_stops.tsx index 3d27b9bb652..57b3cccbb9f 100644 --- a/src/components/color_picker/color_stops/color_stops.tsx +++ b/src/components/color_picker/color_stops/color_stops.tsx @@ -102,7 +102,7 @@ export const EuiColorStops: FunctionComponent = ({ onRemove(index); break; - case keyCodes.UP: + case keyCodes.DOWN: if (target === wrapperRef || target.id.indexOf('stop_') > -1) { e.preventDefault(); if (focusedStop == null) { @@ -117,7 +117,7 @@ export const EuiColorStops: FunctionComponent = ({ } break; - case keyCodes.DOWN: + case keyCodes.UP: if (target === wrapperRef || target.id.indexOf('stop_') > -1) { e.preventDefault(); if (focusedStop == null) { From efce43803b00117f81f24c4e51f732da6351ecf2 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Mon, 23 Sep 2019 09:34:45 -0500 Subject: [PATCH 04/29] add left/right movement --- .../color_stops/color_stop_thumb.tsx | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) 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 d26768235b8..0cf6f4d169d 100644 --- a/src/components/color_picker/color_stops/color_stop_thumb.tsx +++ b/src/components/color_picker/color_stops/color_stop_thumb.tsx @@ -3,6 +3,7 @@ import React, { FunctionComponent, useEffect, useRef, useState } from 'react'; import { CommonProps } from '../../common'; import { isColorInvalid, isStopInvalid, calculateScale } from './utils'; import { getEventPosition, useMouseMove } from '../utils'; +import { keyCodes } from '../../../services'; import { EuiButtonIcon } from '../../button'; import { EuiColorPicker, EuiColorPickerProps } from '../color_picker'; @@ -70,11 +71,11 @@ export const EuiColorStopThumb: FunctionComponent = ({ const handleStopChange = ( value: ColorStop['stop'], - isDrag: boolean = false + shouldRespectBoundaries: boolean = false ) => { const willBeInvalid = value > max || value < min; - if (isDrag && willBeInvalid) { + if (shouldRespectBoundaries && willBeInvalid) { if (value > max) { value = max; } @@ -118,6 +119,20 @@ export const EuiColorStopThumb: FunctionComponent = ({ handleStopChange(newStop, true); }; + const handleKeyDown = (e: React.KeyboardEvent) => { + switch (e.keyCode) { + case keyCodes.LEFT: + e.preventDefault(); + handleStopChange(stop - 1, true); + break; + + case keyCodes.RIGHT: + e.preventDefault(); + handleStopChange(stop + 1, true); + break; + } + }; + const [handleMouseDown, handleInteraction] = useMouseMove( handleChange ); @@ -145,6 +160,7 @@ export const EuiColorStopThumb: FunctionComponent = ({ value={stop} onClick={openPopover} onFocus={onFocus} + onKeyDown={handleKeyDown} onMouseDown={handleMouseDown} onTouchStart={handleInteraction} onTouchMove={handleInteraction} From 7dbf922adf3ddcf753261495a7b00e295b052758 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Mon, 23 Sep 2019 10:01:05 -0500 Subject: [PATCH 05/29] update test --- src/components/color_picker/color_stops/utils.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/color_picker/color_stops/utils.test.ts b/src/components/color_picker/color_stops/utils.test.ts index 7f7bf2b18d8..0463cfde21f 100644 --- a/src/components/color_picker/color_stops/utils.test.ts +++ b/src/components/color_picker/color_stops/utils.test.ts @@ -22,7 +22,7 @@ describe('isInvalid', () => { }); test('Should mark colorStops missing stop as invalid', () => { - const colorStops = [{ stop: '', color: '#FF0000' }]; + const colorStops = [{ stop: null, color: '#FF0000' }]; // Intentionally wrong // @ts-ignore expect(isInvalid(colorStops)).toBe(true); From 6a10f689b4f1854937967c4f1559900efce4316d Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Mon, 23 Sep 2019 13:31:22 -0500 Subject: [PATCH 06/29] doubleClick addStop; clean up; tests removal --- .../src/views/color_picker/color_stops.js | 2 +- .../color_stops/color_stop_thumb.tsx | 4 ++- .../color_picker/color_stops/color_stops.tsx | 36 ++++++++++++++----- .../color_picker/color_stops/utils.test.ts | 8 ----- .../color_picker/color_stops/utils.ts | 26 +++++++------- 5 files changed, 44 insertions(+), 32 deletions(-) diff --git a/src-docs/src/views/color_picker/color_stops.js b/src-docs/src/views/color_picker/color_stops.js index c01245bcfb7..e32b9160c2d 100644 --- a/src-docs/src/views/color_picker/color_stops.js +++ b/src-docs/src/views/color_picker/color_stops.js @@ -13,7 +13,7 @@ export const ColorStops = () => { color: '#FFFF00', }, { - stop: 35, + stop: 45, color: '#008000', }, ]); 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 0cf6f4d169d..77f9f2001aa 100644 --- a/src/components/color_picker/color_stops/color_stop_thumb.tsx +++ b/src/components/color_picker/color_stops/color_stop_thumb.tsx @@ -115,7 +115,9 @@ export const EuiColorStopThumb: FunctionComponent = ({ return; } const box = getEventPosition(location, parentRef); - const newStop = Math.round((box.left / box.width) * 100); + const newStop = Math.round( + (box.left / box.width) * (globalMax - globalMin) + globalMin + ); handleStopChange(newStop, true); }; diff --git a/src/components/color_picker/color_stops/color_stops.tsx b/src/components/color_picker/color_stops/color_stops.tsx index 57b3cccbb9f..620b93346a5 100644 --- a/src/components/color_picker/color_stops/color_stops.tsx +++ b/src/components/color_picker/color_stops/color_stops.tsx @@ -7,12 +7,14 @@ import { EuiColorStopThumb, ColorStop } from './color_stop_thumb'; import { DEFAULT_COLOR, addStop, + addDefinedStop, removeStop, isInvalid, calculateScale, } from './utils'; import { EuiColorPickerProps } from '../'; +import { getEventPosition } from '../utils'; import { EuiRangeWrapper } from '../../form/range/range_wrapper'; interface EuiColorStopsProps extends CommonProps { @@ -27,6 +29,15 @@ interface EuiColorStopsProps extends CommonProps { swatches?: EuiColorPickerProps['swatches']; } +// Becuase of how the thumbs are rendered in the popover, using ref results in an infinite loop. +// We'll instead use old fashioned namespaced DOM selectors to get references +const STOP_ATTR = 'stop_'; + +function isTargetAThumb(target: HTMLElement | EventTarget) { + const element = target as HTMLElement; + return element.id.indexOf(STOP_ATTR) > -1; +} + export const EuiColorStops: FunctionComponent = ({ max, min, @@ -59,7 +70,9 @@ export const EuiColorStops: FunctionComponent = ({ let toFocus; if (wrapperRef) { if (wrapperRef != null) { - toFocus = wrapperRef.querySelector(`#stop_${index}`); + toFocus = wrapperRef.querySelector( + `#${STOP_ATTR}${index}` + ); } } if (toFocus) { @@ -85,12 +98,17 @@ export const EuiColorStops: FunctionComponent = ({ handleOnChange(newColorStops); }; + const handleDoubleClick = (e: React.MouseEvent) => { + if (isTargetAThumb(e.target)) return; + const box = getEventPosition({ x: e.pageX, y: e.pageY }, wrapperRef!); // event happens on `wrapperRef` element, so it must exist + const newStop = Math.round((box.left / box.width) * 100); + const newColorStops = addDefinedStop(colorStops, newStop); + + handleOnChange(newColorStops); + }; + const handleKeyDown = (e: React.KeyboardEvent) => { - const target = e.target as HTMLElement; switch (e.keyCode) { - case keyCodes.TAB: - break; - case keyCodes.ENTER: if (!hasFocus) return; onAdd(); @@ -103,7 +121,7 @@ export const EuiColorStops: FunctionComponent = ({ break; case keyCodes.DOWN: - if (target === wrapperRef || target.id.indexOf('stop_') > -1) { + if (e.target === wrapperRef || isTargetAThumb(e.target)) { e.preventDefault(); if (focusedStop == null) { onFocusStop(0); @@ -118,7 +136,7 @@ export const EuiColorStops: FunctionComponent = ({ break; case keyCodes.UP: - if (target === wrapperRef || target.id.indexOf('stop_') > -1) { + if (e.target === wrapperRef || isTargetAThumb(e.target)) { e.preventDefault(); if (focusedStop == null) { onFocusStop(0); @@ -142,7 +160,7 @@ export const EuiColorStops: FunctionComponent = ({ const thumbs = sortedStops.map((colorStop, index) => ( = ({ fullWidth={fullWidth} tabIndex={0} onKeyDown={handleKeyDown} - onClick={e => console.log(e)} + onDoubleClick={handleDoubleClick} onFocus={e => { if (e.target === wrapperRef) { setHasFocus(true); diff --git a/src/components/color_picker/color_stops/utils.test.ts b/src/components/color_picker/color_stops/utils.test.ts index 0463cfde21f..0d1b22d7b3c 100644 --- a/src/components/color_picker/color_stops/utils.test.ts +++ b/src/components/color_picker/color_stops/utils.test.ts @@ -34,14 +34,6 @@ describe('isInvalid', () => { // @ts-ignore expect(isInvalid(colorStops)).toBe(true); }); - - test('Should mark colorStops with descending stops as invalid', () => { - const colorStops = [ - { stop: 10, color: '#FF0000' }, - { stop: 0, color: '#00FF00' }, - ]; - expect(isInvalid(colorStops)).toBe(true); - }); }); describe('addStop', () => { diff --git a/src/components/color_picker/color_stops/utils.ts b/src/components/color_picker/color_stops/utils.ts index 97d37f5005a..fe9c2cfbde6 100644 --- a/src/components/color_picker/color_stops/utils.ts +++ b/src/components/color_picker/color_stops/utils.ts @@ -11,6 +11,17 @@ export const removeStop = (colorStops: ColorStop[], index: number) => { return [...colorStops.slice(0, index), ...colorStops.slice(index + 1)]; }; +export const addDefinedStop = ( + colorStops: ColorStop[], + stop: ColorStop['stop'] +) => { + const newStop = { + stop, + color: DEFAULT_COLOR, + }; + return [...colorStops, newStop]; +}; + export const addStop = (colorStops: ColorStop[], index: number) => { const currentStop = colorStops[index].stop; let delta = 1; @@ -46,19 +57,8 @@ export const isStopInvalid = (stop: ColorStop['stop']) => { }; export const isInvalid = (colorStops: ColorStop[]) => { - return colorStops.some((colorStop, index) => { - // expect stops to be in ascending order - let isDescending = false; - if (index !== 0) { - const prevStop = colorStops[index - 1].stop; - isDescending = prevStop >= colorStop.stop; - } - - return ( - isColorInvalid(colorStop.color) || - isStopInvalid(colorStop.stop) || - isDescending - ); + return colorStops.some(colorStop => { + return isColorInvalid(colorStop.color) || isStopInvalid(colorStop.stop); }); }; From 44e74ee6165630aebedff731f9e37762ced54740 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Mon, 23 Sep 2019 13:50:54 -0500 Subject: [PATCH 07/29] use more range components directly --- .../color_picker/color_stops/color_stops.tsx | 23 +++++++++---------- src/components/form/range/range_highlight.tsx | 3 +++ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/components/color_picker/color_stops/color_stops.tsx b/src/components/color_picker/color_stops/color_stops.tsx index 620b93346a5..612678a6e51 100644 --- a/src/components/color_picker/color_stops/color_stops.tsx +++ b/src/components/color_picker/color_stops/color_stops.tsx @@ -15,6 +15,8 @@ import { import { EuiColorPickerProps } from '../'; import { getEventPosition } from '../utils'; +import { EuiRangeHighlight } from '../../form/range/range_highlight'; +import { EuiRangeTrack } from '../../form/range/range_track'; import { EuiRangeWrapper } from '../../form/range/range_wrapper'; interface EuiColorStopsProps extends CommonProps { @@ -223,19 +225,16 @@ export const EuiColorStops: FunctionComponent = ({ } }} onBlur={() => setHasFocus(false)}> - {/* TODO: Use euiRangeHighlight / euiRangeTrack */} -
-
-
-
+ + {thumbs} -
+ ); }; diff --git a/src/components/form/range/range_highlight.tsx b/src/components/form/range/range_highlight.tsx index a93839f3d4e..2bfa2f0958a 100644 --- a/src/components/form/range/range_highlight.tsx +++ b/src/components/form/range/range_highlight.tsx @@ -2,6 +2,7 @@ import React, { FunctionComponent } from 'react'; import classNames from 'classnames'; export interface EuiRangeHighlightProps { + color?: string; compressed?: boolean; hasFocus?: boolean; showTicks?: boolean; @@ -19,12 +20,14 @@ export const EuiRangeHighlight: FunctionComponent = ({ max, min, compressed, + color, }) => { // Calculate the width the range based on value // const rangeWidth = (value - min) / (max - min); const leftPosition = (lowerValue - min) / (max - min); const rangeWidth = (upperValue - lowerValue) / (max - min); const rangeWidthStyle = { + background: color, marginLeft: `${leftPosition * 100}%`, width: `${rangeWidth * 100}%`, }; From 90c338ca803ec0066a39144394da28671ec4e946 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Mon, 23 Sep 2019 14:45:06 -0500 Subject: [PATCH 08/29] Firefox cleanup --- .../color_picker/color_stops/_color_stops.scss | 16 ++++++++++++---- .../color_stops/color_stop_thumb.tsx | 1 + 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/components/color_picker/color_stops/_color_stops.scss b/src/components/color_picker/color_stops/_color_stops.scss index 9c7b99bfb9b..ba92954509b 100644 --- a/src/components/color_picker/color_stops/_color_stops.scss +++ b/src/components/color_picker/color_stops/_color_stops.scss @@ -1,3 +1,5 @@ +@import '../../form/range/_variables'; + .euiColorStops { &:focus { outline: 2px solid $euiFocusRingColor; @@ -11,13 +13,19 @@ .euiColorStopPopover.euiPopover { position: absolute; top: 50%; - margin-top: -8px; + margin-top: $euiRangeThumbHeight * -.5; + width: $euiRangeThumbWidth; + height: $euiRangeThumbHeight; +} + +.euiColorStopPopover__anchor { + position: absolute; + width: 100%; + height: 100%; } .euiColorStopThumb.euiRangeThumb { - position: static; - top: auto; + top: 0; margin-top: 0; pointer-events: auto; - cursor: pointer; } 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 77f9f2001aa..b32c02b1b2b 100644 --- a/src/components/color_picker/color_stops/color_stop_thumb.tsx +++ b/src/components/color_picker/color_stops/color_stop_thumb.tsx @@ -143,6 +143,7 @@ export const EuiColorStopThumb: FunctionComponent = ({ Date: Mon, 23 Sep 2019 15:38:35 -0500 Subject: [PATCH 09/29] i18n; refactor --- .../color_stops/color_stop_thumb.tsx | 124 +++++++++++------- .../color_picker/color_stops/color_stops.tsx | 44 ++++--- 2 files changed, 97 insertions(+), 71 deletions(-) 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 b32c02b1b2b..4584b49a8c3 100644 --- a/src/components/color_picker/color_stops/color_stop_thumb.tsx +++ b/src/components/color_picker/color_stops/color_stop_thumb.tsx @@ -10,6 +10,7 @@ import { EuiColorPicker, EuiColorPickerProps } from '../color_picker'; import { EuiFlexGroup, EuiFlexItem } from '../../flex'; // @ts-ignore import { EuiFieldNumber, EuiFieldText, EuiFormRow } from '../../form'; +import { EuiI18n } from '../../i18n'; import { EuiRangeThumb } from '../../form/range/range_thumb'; import { EuiPopover } from '../../popover'; import { EuiSpacer } from '../../spacer'; @@ -66,7 +67,7 @@ export const EuiColorStopThumb: FunctionComponent = ({ const handleColorChange = (value: ColorStop['color']) => { setColorIsInvalid(isColorInvalid(value)); - propagateChange({ stop, color: value }); + onChange({ stop, color: value }); }; const handleStopChange = ( @@ -84,7 +85,7 @@ export const EuiColorStopThumb: FunctionComponent = ({ } } setStopIsInvalid(isStopInvalid(value)); - propagateChange({ stop: value, color }); + onChange({ stop: value, color }); }; const handleStopInputChange = (value: ColorStop['stop']) => { @@ -99,18 +100,14 @@ export const EuiColorStopThumb: FunctionComponent = ({ } } setStopIsInvalid(isStopInvalid(value)); - propagateChange({ stop: value, color }); + onChange({ stop: value, color }); }; - const propagateChange = (newColor: ColorStop) => { - onChange(newColor); - }; - - const handleChange = ( + const handlePointerChange = ( location: { x: number; y: number }, isFirstInteraction?: boolean ) => { - if (isFirstInteraction) return; + if (isFirstInteraction) return; // Prevents change on the inital MouseDown event if (parentRef == null) { return; } @@ -136,7 +133,7 @@ export const EuiColorStopThumb: FunctionComponent = ({ }; const [handleMouseDown, handleInteraction] = useMouseMove( - handleChange + handlePointerChange ); return ( @@ -177,34 +174,52 @@ export const EuiColorStopThumb: FunctionComponent = ({
- - ) => - handleStopInputChange(parseFloat(e.target.value)) - } - /> - + + {([stopLabel, stopErrorMessage]: React.ReactChild[]) => ( + + ) => + handleStopInputChange(parseFloat(e.target.value)) + } + /> + + )} + - - + + + {(removeLabel: string) => ( + + )} + @@ -219,20 +234,29 @@ export const EuiColorStopThumb: FunctionComponent = ({ {colorPickerMode !== 'swatch' && ( - - ) => - handleColorChange(e.target.value) - } - /> - + + {([hexLabel, hexErrorMessage]: React.ReactChild[]) => ( + + ) => + handleColorChange(e.target.value) + } + /> + + )} + )}
diff --git a/src/components/color_picker/color_stops/color_stops.tsx b/src/components/color_picker/color_stops/color_stops.tsx index 612678a6e51..e3a7a9b4923 100644 --- a/src/components/color_picker/color_stops/color_stops.tsx +++ b/src/components/color_picker/color_stops/color_stops.tsx @@ -33,7 +33,7 @@ interface EuiColorStopsProps extends CommonProps { // Becuase of how the thumbs are rendered in the popover, using ref results in an infinite loop. // We'll instead use old fashioned namespaced DOM selectors to get references -const STOP_ATTR = 'stop_'; +const STOP_ATTR = 'euiColorStop_'; function isTargetAThumb(target: HTMLElement | EventTarget) { const element = target as HTMLElement; @@ -52,7 +52,7 @@ export const EuiColorStops: FunctionComponent = ({ swatches, }) => { const [hasFocus, setHasFocus] = useState(false); - const [focusedStop, setFocusedStop] = useState(null); + const [focusedStopIndex, setFocusedStopIndex] = useState(null); const [wrapperRef, setWrapperRef] = useState< HTMLDivElement | null | undefined >(null); @@ -79,7 +79,7 @@ export const EuiColorStops: FunctionComponent = ({ } if (toFocus) { setHasFocus(false); - setFocusedStop(index); + setFocusedStopIndex(index); toFocus.focus(); } }; @@ -93,7 +93,7 @@ export const EuiColorStops: FunctionComponent = ({ const onRemove = (index: number) => { const newColorStops = removeStop(colorStops, index); - setFocusedStop(null); + setFocusedStopIndex(null); if (wrapperRef) { wrapperRef.focus(); } @@ -117,21 +117,21 @@ export const EuiColorStops: FunctionComponent = ({ break; case keyCodes.BACKSPACE: - if (hasFocus || focusedStop == null) return; - const index = sortedStops[focusedStop].id; + if (hasFocus || focusedStopIndex == null) return; + const index = sortedStops[focusedStopIndex].id; onRemove(index); break; case keyCodes.DOWN: if (e.target === wrapperRef || isTargetAThumb(e.target)) { e.preventDefault(); - if (focusedStop == null) { + if (focusedStopIndex == null) { onFocusStop(0); } else { const next = - focusedStop === sortedStops.length - 1 - ? focusedStop - : focusedStop + 1; + focusedStopIndex === sortedStops.length - 1 + ? focusedStopIndex + : focusedStopIndex + 1; onFocusStop(next); } } @@ -140,10 +140,11 @@ export const EuiColorStops: FunctionComponent = ({ case keyCodes.UP: if (e.target === wrapperRef || isTargetAThumb(e.target)) { e.preventDefault(); - if (focusedStop == null) { + if (focusedStopIndex == null) { onFocusStop(0); } else { - const next = focusedStop === 0 ? focusedStop : focusedStop - 1; + const next = + focusedStopIndex === 0 ? focusedStopIndex : focusedStopIndex - 1; onFocusStop(next); } } @@ -176,7 +177,7 @@ export const EuiColorStops: FunctionComponent = ({ sortedStops.length > 1 ? () => onRemove(colorStop.id) : undefined } onChange={stop => handleStopChange(stop, colorStop.id)} - onFocus={() => setFocusedStop(index)} + onFocus={() => setFocusedStopIndex(index)} parentRef={wrapperRef} colorPickerMode={mode} colorPickerSwatches={swatches} @@ -189,22 +190,23 @@ export const EuiColorStops: FunctionComponent = ({ calculateScale(wrapperRef ? wrapperRef.clientWidth : 100) ) ); - const gradientStops = (colorStop: ColorStop, index: number) => { + const gradientStop = (colorStop: ColorStop, index: number) => { return `${colorStop.color} ${positions[index]}%`; }; - const fixedStops = (colorStop: ColorStop, index: number) => { + const fixedStop = (colorStop: ColorStop, index: number) => { if (index === 0) { - return `${colorStop.color}, ${colorStop.color} ${positions[index + 1]}%`; + return `${colorStop.color}, ${gradientStop(colorStop, index + 1)}`; } else if (index === sortedStops.length - 1) { - return `${colorStop.color} ${positions[index]}%`; + return gradientStop(colorStop, index); } else { - return `${colorStop.color} ${positions[index]}%, ${colorStop.color} ${ - positions[index + 1] - }%`; + return `${gradientStop(colorStop, index)}, ${gradientStop( + colorStop, + index + 1 + )}`; } }; const linearGradient = sortedStops.map( - stopType === 'gradient' ? gradientStops : fixedStops + stopType === 'gradient' ? gradientStop : fixedStop ); const background = sortedStops.length > 1 From 83fc325526a813b24a576e0356807cc6f0710fa3 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Tue, 24 Sep 2019 13:52:00 -0500 Subject: [PATCH 10/29] WIP: add new stop via click --- .../src/views/color_picker/color_stops.js | 16 +++++++ .../color_stops/_color_stops.scss | 33 ++++++++++++- .../color_picker/color_stops/color_stops.tsx | 47 +++++++++++++++---- .../color_picker/color_stops/utils.ts | 13 +++-- src/components/form/range/range_highlight.tsx | 4 +- 5 files changed, 99 insertions(+), 14 deletions(-) diff --git a/src-docs/src/views/color_picker/color_stops.js b/src-docs/src/views/color_picker/color_stops.js index e32b9160c2d..a2120b7263d 100644 --- a/src-docs/src/views/color_picker/color_stops.js +++ b/src-docs/src/views/color_picker/color_stops.js @@ -3,6 +3,11 @@ import React, { useState } from 'react'; import { EuiColorStops, EuiFormRow } from '../../../../src/components'; export const ColorStops = () => { + const generateRandomColor = () => + // https://www.paulirish.com/2009/random-hex-color-code-snippets/ + `#${Math.floor(Math.random() * 16777215).toString(16)}`; + + const [addColor, setAddColor] = useState(generateRandomColor()); const [colorStops, setColorStops] = useState([ { stop: 0, @@ -20,6 +25,7 @@ export const ColorStops = () => { const handleChange = colorStops => { setColorStops(colorStops); + setAddColor(generateRandomColor()); }; const [extendedColorStops, setExtendedColorStops] = useState([ @@ -52,6 +58,16 @@ export const ColorStops = () => { /> + + + + void; fullWidth?: boolean; @@ -41,10 +42,11 @@ function isTargetAThumb(target: HTMLElement | EventTarget) { } export const EuiColorStops: FunctionComponent = ({ + addColor = DEFAULT_COLOR, max, min, mode = 'default', - colorStops = [{ stop: 0, color: DEFAULT_COLOR }], + colorStops = [{ stop: 0, color: addColor }], onChange, fullWidth, className, @@ -53,9 +55,9 @@ export const EuiColorStops: FunctionComponent = ({ }) => { const [hasFocus, setHasFocus] = useState(false); const [focusedStopIndex, setFocusedStopIndex] = useState(null); - const [wrapperRef, setWrapperRef] = useState< - HTMLDivElement | null | undefined - >(null); + const [wrapperRef, setWrapperRef] = useState(null); + const [addTargetPosition, setAddTargetPosition] = useState(0); + const [isHoverDisabled, setIsHoverDisabled] = useState(false); const classes = classNames('euiColorStops', className); const handleOnChange = (colorStops: ColorStop[]) => { @@ -85,7 +87,7 @@ export const EuiColorStops: FunctionComponent = ({ }; const onAdd = (index: number = colorStops.length - 1) => { - const newColorStops = addStop(colorStops, index); + const newColorStops = addStop(colorStops, index, addColor); handleOnChange(newColorStops); }; @@ -100,11 +102,25 @@ export const EuiColorStops: FunctionComponent = ({ handleOnChange(newColorStops); }; + const handleAddHover = (e: React.MouseEvent) => { + // reuse + const box = getEventPosition({ x: e.pageX, y: e.pageY }, wrapperRef!); // event happens on `wrapperRef` element, so it must exist + const stop = Math.round((box.left / box.width) * (max - min) + min); + + //reuse + const position = Math.round( + ((stop - min) / (max - min)) * + calculateScale(wrapperRef ? wrapperRef.clientWidth : 100) + ); + + setAddTargetPosition(position); + }; + const handleDoubleClick = (e: React.MouseEvent) => { if (isTargetAThumb(e.target)) return; const box = getEventPosition({ x: e.pageX, y: e.pageY }, wrapperRef!); // event happens on `wrapperRef` element, so it must exist - const newStop = Math.round((box.left / box.width) * 100); - const newColorStops = addDefinedStop(colorStops, newStop); + const newStop = Math.round((box.left / box.width) * (max - min) + min); + const newColorStops = addDefinedStop(colorStops, newStop, addColor); handleOnChange(newColorStops); }; @@ -219,8 +235,10 @@ export const EuiColorStops: FunctionComponent = ({ className={classes} fullWidth={fullWidth} tabIndex={0} + onMouseDown={() => setIsHoverDisabled(true)} + onMouseUp={() => setIsHoverDisabled(false)} + onMouseLeave={() => setIsHoverDisabled(false)} onKeyDown={handleKeyDown} - onDoubleClick={handleDoubleClick} onFocus={e => { if (e.target === wrapperRef) { setHasFocus(true); @@ -235,6 +253,19 @@ export const EuiColorStops: FunctionComponent = ({ upperValue={max} color={background} /> +
+
+
{thumbs} diff --git a/src/components/color_picker/color_stops/utils.ts b/src/components/color_picker/color_stops/utils.ts index fe9c2cfbde6..f95fadc7c12 100644 --- a/src/components/color_picker/color_stops/utils.ts +++ b/src/components/color_picker/color_stops/utils.ts @@ -13,16 +13,21 @@ export const removeStop = (colorStops: ColorStop[], index: number) => { export const addDefinedStop = ( colorStops: ColorStop[], - stop: ColorStop['stop'] + stop: ColorStop['stop'], + color: ColorStop['color'] = DEFAULT_COLOR ) => { const newStop = { stop, - color: DEFAULT_COLOR, + color, }; return [...colorStops, newStop]; }; -export const addStop = (colorStops: ColorStop[], index: number) => { +export const addStop = ( + colorStops: ColorStop[], + index: number, + color: ColorStop['color'] = DEFAULT_COLOR +) => { const currentStop = colorStops[index].stop; let delta = 1; if (index === colorStops.length - 1) { @@ -39,7 +44,7 @@ export const addStop = (colorStops: ColorStop[], index: number) => { const newStop = { stop: currentStop + delta, - color: DEFAULT_COLOR, + color, }; return [ ...colorStops.slice(0, index + 1), diff --git a/src/components/form/range/range_highlight.tsx b/src/components/form/range/range_highlight.tsx index 2bfa2f0958a..4ffbe74b6f5 100644 --- a/src/components/form/range/range_highlight.tsx +++ b/src/components/form/range/range_highlight.tsx @@ -10,6 +10,7 @@ export interface EuiRangeHighlightProps { upperValue: number; max: number; min: number; + onClick?: (e: React.MouseEvent) => void; } export const EuiRangeHighlight: FunctionComponent = ({ @@ -21,6 +22,7 @@ export const EuiRangeHighlight: FunctionComponent = ({ min, compressed, color, + onClick, }) => { // Calculate the width the range based on value // const rangeWidth = (value - min) / (max - min); @@ -42,7 +44,7 @@ export const EuiRangeHighlight: FunctionComponent = ({ }); return ( -
+
); From 3b7783189bdc83778ea89f4eb3d81ba317c515a0 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Tue, 24 Sep 2019 15:28:52 -0500 Subject: [PATCH 11/29] clean up --- .../color_stops/_color_stops.scss | 2 +- .../color_stops/color_stop_thumb.tsx | 29 ++++++++---- .../color_picker/color_stops/color_stops.tsx | 46 +++++++++---------- .../color_picker/color_stops/utils.ts | 22 +++++++++ 4 files changed, 64 insertions(+), 35 deletions(-) diff --git a/src/components/color_picker/color_stops/_color_stops.scss b/src/components/color_picker/color_stops/_color_stops.scss index f1328e9c3fc..f3d1b395d0a 100644 --- a/src/components/color_picker/color_stops/_color_stops.scss +++ b/src/components/color_picker/color_stops/_color_stops.scss @@ -15,7 +15,7 @@ height: $euiRangeThumbHeight; margin-top: $euiRangeThumbHeight * -.5; - &:hover:not(.isDisabled) { + &:hover:not(.euiColorStops__addContainer-isDisabled) { // TODO: copy? default? none? cursor: copy; 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 4584b49a8c3..4d243582a95 100644 --- a/src/components/color_picker/color_stops/color_stop_thumb.tsx +++ b/src/components/color_picker/color_stops/color_stop_thumb.tsx @@ -1,8 +1,13 @@ import React, { FunctionComponent, useEffect, useRef, useState } from 'react'; import { CommonProps } from '../../common'; -import { isColorInvalid, isStopInvalid, calculateScale } from './utils'; -import { getEventPosition, useMouseMove } from '../utils'; +import { + getPositionFromStop, + getStopFromMouseLocation, + isColorInvalid, + isStopInvalid, +} from './utils'; +import { useMouseMove } from '../utils'; import { keyCodes } from '../../../services'; import { EuiButtonIcon } from '../../button'; @@ -61,6 +66,16 @@ export const EuiColorStopThumb: FunctionComponent = ({ } }, [stop]); + const getStopFromMouseLocationFn = (location: { x: number; y: number }) => { + // Guards against `null` ref in useage + return getStopFromMouseLocation(location, parentRef!, globalMin, globalMax); + }; + + const getPositionFromStopFn = (stop: ColorStop['stop']) => { + // Guards against `null` ref in useage + return getPositionFromStop(stop, parentRef!, globalMin, globalMax); + }; + const openPopover = () => setIsPopoverOpen(true); const closePopover = () => setIsPopoverOpen(false); @@ -111,10 +126,7 @@ export const EuiColorStopThumb: FunctionComponent = ({ if (parentRef == null) { return; } - const box = getEventPosition(location, parentRef); - const newStop = Math.round( - (box.left / box.width) * (globalMax - globalMin) + globalMin - ); + const newStop = getStopFromMouseLocationFn(location); handleStopChange(newStop, true); }; @@ -147,10 +159,7 @@ export const EuiColorStopThumb: FunctionComponent = ({ ownFocus={true} initialFocus={numberInputRef} style={{ - left: `${Math.round( - ((stop - globalMin) / (globalMax - globalMin)) * - calculateScale(parentRef ? parentRef.clientWidth : 100) - )}%`, + left: `${getPositionFromStopFn(stop)}%`, }} button={ = ({ const [isHoverDisabled, setIsHoverDisabled] = useState(false); const classes = classNames('euiColorStops', className); + const getStopFromMouseLocationFn = (location: { x: number; y: number }) => { + // Guards against `null` ref in useage + return getStopFromMouseLocation(location, wrapperRef!, min, max); + }; + + const getPositionFromStopFn = (stop: ColorStop['stop']) => { + // Guards against `null` ref in useage + return getPositionFromStop(stop, wrapperRef!, min, max); + }; + const handleOnChange = (colorStops: ColorStop[]) => { onChange(colorStops, isInvalid(colorStops)); }; @@ -103,23 +113,16 @@ export const EuiColorStops: FunctionComponent = ({ }; const handleAddHover = (e: React.MouseEvent) => { - // reuse - const box = getEventPosition({ x: e.pageX, y: e.pageY }, wrapperRef!); // event happens on `wrapperRef` element, so it must exist - const stop = Math.round((box.left / box.width) * (max - min) + min); - - //reuse - const position = Math.round( - ((stop - min) / (max - min)) * - calculateScale(wrapperRef ? wrapperRef.clientWidth : 100) - ); + if (!wrapperRef) return; + const stop = getStopFromMouseLocationFn({ x: e.pageX, y: e.pageY }); + const position = getPositionFromStopFn(stop); setAddTargetPosition(position); }; - const handleDoubleClick = (e: React.MouseEvent) => { - if (isTargetAThumb(e.target)) return; - const box = getEventPosition({ x: e.pageX, y: e.pageY }, wrapperRef!); // event happens on `wrapperRef` element, so it must exist - const newStop = Math.round((box.left / box.width) * (max - min) + min); + const handleAddClick = (e: React.MouseEvent) => { + if (isTargetAThumb(e.target) || !wrapperRef) return; + const newStop = getStopFromMouseLocationFn({ x: e.pageX, y: e.pageY }); const newColorStops = addDefinedStop(colorStops, newStop, addColor); handleOnChange(newColorStops); @@ -200,12 +203,7 @@ export const EuiColorStops: FunctionComponent = ({ /> )); - const positions = sortedStops.map(colorStop => - Math.round( - ((colorStop.stop - min) / (max - min)) * - calculateScale(wrapperRef ? wrapperRef.clientWidth : 100) - ) - ); + const positions = sortedStops.map(({ stop }) => getPositionFromStopFn(stop)); const gradientStop = (colorStop: ColorStop, index: number) => { return `${colorStop.color} ${positions[index]}%`; }; @@ -255,9 +253,9 @@ export const EuiColorStops: FunctionComponent = ({ />
{ const thumbToTrackRatio = EUI_THUMB_SIZE / trackWidth; return (1 - thumbToTrackRatio) * 100; }; + +export const getStopFromMouseLocation = ( + location: { x: number; y: number }, + ref: HTMLDivElement, + min: number, + max: number +) => { + const box = getEventPosition(location, ref); + return Math.round((box.left / box.width) * (max - min) + min); +}; + +export const getPositionFromStop = ( + stop: ColorStop['stop'], + ref: HTMLDivElement, + min: number, + max: number +) => { + return Math.round( + ((stop - min) / (max - min)) * calculateScale(ref ? ref.clientWidth : 100) + ); +}; From 006ba4e66804b109be6bd234165551ed5fe96555 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Wed, 25 Sep 2019 13:05:37 -0500 Subject: [PATCH 12/29] better keyboard interaction --- .../color_stops/color_stop_thumb.tsx | 15 +++- .../color_picker/color_stops/color_stops.tsx | 77 ++++++++++++------- .../color_picker/color_stops/utils.test.ts | 46 +++++------ .../color_picker/color_stops/utils.ts | 34 ++++---- 4 files changed, 102 insertions(+), 70 deletions(-) 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 4d243582a95..e25a3e7502d 100644 --- a/src/components/color_picker/color_stops/color_stop_thumb.tsx +++ b/src/components/color_picker/color_stops/color_stop_thumb.tsx @@ -67,12 +67,12 @@ export const EuiColorStopThumb: FunctionComponent = ({ }, [stop]); const getStopFromMouseLocationFn = (location: { x: number; y: number }) => { - // Guards against `null` ref in useage + // Guards against `null` ref in usage return getStopFromMouseLocation(location, parentRef!, globalMin, globalMax); }; const getPositionFromStopFn = (stop: ColorStop['stop']) => { - // Guards against `null` ref in useage + // Guards against `null` ref in usage return getPositionFromStop(stop, parentRef!, globalMin, globalMax); }; @@ -80,6 +80,13 @@ export const EuiColorStopThumb: FunctionComponent = ({ const closePopover = () => setIsPopoverOpen(false); + const handleOnRemove = () => { + if (onRemove) { + closePopover(); + onRemove(); + } + }; + const handleColorChange = (value: ColorStop['color']) => { setColorIsInvalid(isColorInvalid(value)); onChange({ stop, color: value }); @@ -156,7 +163,7 @@ export const EuiColorStopThumb: FunctionComponent = ({ panelPaddingSize="m" isOpen={isPopoverOpen} closePopover={closePopover} - ownFocus={true} + ownFocus={isPopoverOpen} initialFocus={numberInputRef} style={{ left: `${getPositionFromStopFn(stop)}%`, @@ -225,7 +232,7 @@ export const EuiColorStopThumb: FunctionComponent = ({ aria-label={removeLabel} title={removeLabel} disabled={!onRemove} - onClick={onRemove} + onClick={handleOnRemove} /> )} diff --git a/src/components/color_picker/color_stops/color_stops.tsx b/src/components/color_picker/color_stops/color_stops.tsx index 945df4939e7..0a1dcfc9e20 100644 --- a/src/components/color_picker/color_stops/color_stops.tsx +++ b/src/components/color_picker/color_stops/color_stops.tsx @@ -1,4 +1,4 @@ -import React, { FunctionComponent, useState } from 'react'; +import React, { FunctionComponent, useEffect, useState } from 'react'; import classNames from 'classnames'; import { CommonProps } from '../../common'; @@ -41,6 +41,17 @@ function isTargetAThumb(target: HTMLElement | EventTarget) { return element.id.indexOf(STOP_ATTR) > -1; } +function sortStops(colorStops: ColorStop[]) { + return colorStops + .map((el, index) => { + return { + ...el, + id: index, + }; + }) + .sort((a, b) => a.stop - b.stop); +} + export const EuiColorStops: FunctionComponent = ({ addColor = DEFAULT_COLOR, max, @@ -53,20 +64,37 @@ export const EuiColorStops: FunctionComponent = ({ stopType = 'gradient', swatches, }) => { + const [sortedStops, setSortedStops] = useState(sortStops(colorStops)); const [hasFocus, setHasFocus] = useState(false); const [focusedStopIndex, setFocusedStopIndex] = useState(null); const [wrapperRef, setWrapperRef] = useState(null); const [addTargetPosition, setAddTargetPosition] = useState(0); const [isHoverDisabled, setIsHoverDisabled] = useState(false); + const [focusStopOnUpdate, setFocusStopOnUpdate] = useState( + null + ); + + useEffect(() => { + setSortedStops(sortStops(colorStops)); + }, [colorStops]); + + useEffect(() => { + if (focusStopOnUpdate !== null) { + const toFocus = sortedStops.map(el => el.stop).indexOf(focusStopOnUpdate); + onFocusStop(toFocus); + setFocusStopOnUpdate(null); + } + }, [sortedStops]); + const classes = classNames('euiColorStops', className); const getStopFromMouseLocationFn = (location: { x: number; y: number }) => { - // Guards against `null` ref in useage + // Guards against `null` ref in usage return getStopFromMouseLocation(location, wrapperRef!, min, max); }; const getPositionFromStopFn = (stop: ColorStop['stop']) => { - // Guards against `null` ref in useage + // Guards against `null` ref in usage return getPositionFromStop(stop, wrapperRef!, min, max); }; @@ -81,14 +109,10 @@ export const EuiColorStops: FunctionComponent = ({ }; const onFocusStop = (index: number) => { - let toFocus; - if (wrapperRef) { - if (wrapperRef != null) { - toFocus = wrapperRef.querySelector( - `#${STOP_ATTR}${index}` - ); - } - } + if (!wrapperRef) return; + const toFocus = wrapperRef.querySelector( + `#${STOP_ATTR}${index}` + ); if (toFocus) { setHasFocus(false); setFocusedStopIndex(index); @@ -96,19 +120,24 @@ export const EuiColorStops: FunctionComponent = ({ } }; - const onAdd = (index: number = colorStops.length - 1) => { - const newColorStops = addStop(colorStops, index, addColor); + const onFocusWrapper = () => { + setFocusedStopIndex(null); + if (wrapperRef) { + wrapperRef.focus(); + } + }; + + const onAdd = () => { + const newColorStops = addStop(colorStops, addColor, max); + setFocusStopOnUpdate(newColorStops[colorStops.length].stop); handleOnChange(newColorStops); }; const onRemove = (index: number) => { const newColorStops = removeStop(colorStops, index); - setFocusedStopIndex(null); - if (wrapperRef) { - wrapperRef.focus(); - } + onFocusWrapper(); handleOnChange(newColorStops); }; @@ -125,11 +154,16 @@ export const EuiColorStops: FunctionComponent = ({ const newStop = getStopFromMouseLocationFn({ x: e.pageX, y: e.pageY }); const newColorStops = addDefinedStop(colorStops, newStop, addColor); + setFocusStopOnUpdate(newStop); handleOnChange(newColorStops); }; const handleKeyDown = (e: React.KeyboardEvent) => { switch (e.keyCode) { + case keyCodes.ESCAPE: + onFocusWrapper(); + break; + case keyCodes.ENTER: if (!hasFocus) return; onAdd(); @@ -171,15 +205,6 @@ export const EuiColorStops: FunctionComponent = ({ } }; - const sortedStops = colorStops - .map((el, index) => { - return { - ...el, - id: index, - }; - }) - .sort((a, b) => a.stop - b.stop); - const thumbs = sortedStops.map((colorStop, index) => ( { }); describe('addStop', () => { - test('Should add row when there is only a single row', () => { + test('Should add stop when there is only a single stop', () => { const colorStops = [{ stop: 0, color: '#FF0000' }]; - expect(addStop(colorStops, 0)).toEqual([ + expect(addStop(colorStops, '#FF0000', 100)).toEqual([ { stop: 0, color: '#FF0000' }, { stop: 1, color: '#FF0000' }, ]); }); - describe('to middle of list', () => { - test('Should add row after first item', () => { - expect(addStop(colorStops, 0)).toEqual([ - { stop: 0, color: '#FF0000' }, - { stop: 12.5, color: '#FF0000' }, - { stop: 25, color: '#00FF00' }, - { stop: 35, color: '#0000FF' }, - ]); - }); - - test('Should add row after second item', () => { - expect(addStop(colorStops, 1)).toEqual([ - { stop: 0, color: '#FF0000' }, - { stop: 25, color: '#00FF00' }, - { stop: 30, color: '#FF0000' }, - { stop: 35, color: '#0000FF' }, - ]); - }); - }); - - test('Should add row to end of list', () => { - expect(addStop(colorStops, 2)).toEqual([ + test('Should add stop to end of list', () => { + expect(addStop(colorStops, '#FF0000', 100)).toEqual([ { stop: 0, color: '#FF0000' }, { stop: 25, color: '#00FF00' }, { stop: 35, color: '#0000FF' }, { stop: 45, color: '#FF0000' }, ]); }); + + test('Should add stop below the max if max is taken', () => { + expect( + addStop( + [{ stop: 0, color: '#FF0000' }, { stop: 100, color: '#FF0000' }], + '#FF0000', + 100 + ) + ).toEqual([ + { stop: 0, color: '#FF0000' }, + { stop: 100, color: '#FF0000' }, + { stop: 99, color: '#FF0000' }, + ]); + }); }); describe('removeStop', () => { - test('Should not remove last row', () => { + test('Should not remove only stop', () => { const colorStops = [{ stop: 0, color: '#FF0000' }]; expect(removeStop(colorStops, 0)).toEqual(colorStops); }); - test('Should remove row at index', () => { + test('Should remove stop at index', () => { const colorStops = [ { stop: 0, color: '#FF0000' }, { stop: 25, color: '#00FF00' }, diff --git a/src/components/color_picker/color_stops/utils.ts b/src/components/color_picker/color_stops/utils.ts index 852edd5aa8c..1e473641cc0 100644 --- a/src/components/color_picker/color_stops/utils.ts +++ b/src/components/color_picker/color_stops/utils.ts @@ -26,25 +26,31 @@ export const addDefinedStop = ( export const addStop = ( colorStops: ColorStop[], - index: number, - color: ColorStop['color'] = DEFAULT_COLOR + color: ColorStop['color'] = DEFAULT_COLOR, + max: number ) => { - const currentStop = colorStops[index].stop; + const index = colorStops.length - 1; + const stops = colorStops.map(el => el.stop); + const currentStop = stops[index]; let delta = 1; - if (index === colorStops.length - 1) { - // Adding stop to end of list. - if (index !== 0) { - const prevStop = colorStops[index - 1].stop; - delta = currentStop - prevStop; - } - } else { - // Adding stop in middle of list. - const nextStop = colorStops[index + 1].stop; - delta = (nextStop - currentStop) / 2; + if (index !== 0) { + const prevStop = stops[index - 1]; + delta = currentStop - prevStop; + } + + let stop = currentStop + delta; + + if (stop > max) { + stop = max; + } + + // We've reached the max, so start working backwards + while (stops.indexOf(stop) > -1) { + stop--; } const newStop = { - stop: currentStop + delta, + stop, color, }; return [ From d188f7dbaf63dbcd7c972600b5746f286c993c63 Mon Sep 17 00:00:00 2001 From: Ryan Keairns Date: Wed, 25 Sep 2019 14:33:44 -0500 Subject: [PATCH 13/29] add stop styles --- .../color_picker/color_stops/_color_stops.scss | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/color_picker/color_stops/_color_stops.scss b/src/components/color_picker/color_stops/_color_stops.scss index f3d1b395d0a..61a19522b45 100644 --- a/src/components/color_picker/color_stops/_color_stops.scss +++ b/src/components/color_picker/color_stops/_color_stops.scss @@ -16,11 +16,10 @@ margin-top: $euiRangeThumbHeight * -.5; &:hover:not(.euiColorStops__addContainer-isDisabled) { - // TODO: copy? default? none? - cursor: copy; + cursor: pointer; .euiColorStops__addTarget { - display: block; + opacity: .7; } } } @@ -28,13 +27,14 @@ .euiColorStops__addTarget { @include euiCustomControl($type: 'round'); @include euiRangeThumbStyle; - display: none; position: absolute; top: 0; height: $euiRangeThumbHeight; width: $euiRangeThumbHeight; background-color: $euiColorLightestShade; pointer-events: none; + opacity: 0; + transition: opacity $euiAnimSpeedFast; } .euiColorStop { @@ -59,4 +59,9 @@ top: 0; margin-top: 0; pointer-events: auto; + cursor: grab; + + &:active { + cursor: grabbing; + } } From a1ce7cfbca7b314793a0731402d9b5926faf11b8 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Wed, 25 Sep 2019 15:39:42 -0500 Subject: [PATCH 14/29] empty set; backspace fix; drag handle fix --- .../color_picker/color_picker_example.js | 4 +-- .../src/views/color_picker/color_stops.js | 34 ++++++++++++++++++- .../color_stops/_color_stops.scss | 4 +++ .../color_picker/color_stops/color_stops.tsx | 24 ++++++++----- .../color_picker/color_stops/utils.ts | 8 ++--- 5 files changed, 57 insertions(+), 17 deletions(-) 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 814372217ff..15ab99b6535 100644 --- a/src-docs/src/views/color_picker/color_picker_example.js +++ b/src-docs/src/views/color_picker/color_picker_example.js @@ -314,8 +314,8 @@ export const ColorPickerExample = { text: (

Use EuiColorStops to define color stops for data - driven styling. Stops are numbers in strictly ascending order. The - range is from the given stop number (inclusive) to the next stop + driven styling. Stops are numbers within the provided range. The color + segment spans from the given stop number (inclusive) to the next stop number (exclusive).

), diff --git a/src-docs/src/views/color_picker/color_stops.js b/src-docs/src/views/color_picker/color_stops.js index a2120b7263d..0f03fcf6eca 100644 --- a/src-docs/src/views/color_picker/color_stops.js +++ b/src-docs/src/views/color_picker/color_stops.js @@ -47,17 +47,49 @@ export const ColorStops = () => { setExtendedColorStops(colorStops); }; + const [emptyColorStops, setEmptyColorStops] = useState([]); + + const handleEmptyChange = colorStops => { + setEmptyColorStops(colorStops); + }; + + const changeProps = () => { + setColorStops([ + { + stop: 0, + color: '#ff0000', + }, + { + stop: 25, + color: '#FFFF00', + }, + { + stop: 45, + color: '#008000', + }, + ]); + }; + return ( + + + + - void; fullWidth?: boolean; className?: string; @@ -53,11 +52,11 @@ function sortStops(colorStops: ColorStop[]) { } export const EuiColorStops: FunctionComponent = ({ - addColor = DEFAULT_COLOR, + addColor = DEFAULT_VISUALIZATION_COLOR, max, min, mode = 'default', - colorStops = [{ stop: 0, color: addColor }], + colorStops, onChange, fullWidth, className, @@ -86,7 +85,11 @@ export const EuiColorStops: FunctionComponent = ({ } }, [sortedStops]); - const classes = classNames('euiColorStops', className); + const classes = classNames( + 'euiColorStops', + { 'euiColorStops-isDragging': isHoverDisabled }, + className + ); const getStopFromMouseLocationFn = (location: { x: number; y: number }) => { // Guards against `null` ref in usage @@ -171,8 +174,10 @@ export const EuiColorStops: FunctionComponent = ({ case keyCodes.BACKSPACE: if (hasFocus || focusedStopIndex == null) return; - const index = sortedStops[focusedStopIndex].id; - onRemove(index); + if (isTargetAThumb(e.target)) { + const index = sortedStops[focusedStopIndex].id; + onRemove(index); + } break; case keyCodes.DOWN: @@ -247,10 +252,11 @@ export const EuiColorStops: FunctionComponent = ({ const linearGradient = sortedStops.map( stopType === 'gradient' ? gradientStop : fixedStop ); + const singleColor = sortedStops[0] ? sortedStops[0].color : undefined; const background = sortedStops.length > 1 ? `linear-gradient(to right,${linearGradient})` - : sortedStops[0].color; + : singleColor; return ( { if (colorStops.length === 1) { return colorStops; @@ -15,7 +13,7 @@ export const removeStop = (colorStops: ColorStop[], index: number) => { export const addDefinedStop = ( colorStops: ColorStop[], stop: ColorStop['stop'], - color: ColorStop['color'] = DEFAULT_COLOR + color: ColorStop['color'] = DEFAULT_VISUALIZATION_COLOR ) => { const newStop = { stop, @@ -26,7 +24,7 @@ export const addDefinedStop = ( export const addStop = ( colorStops: ColorStop[], - color: ColorStop['color'] = DEFAULT_COLOR, + color: ColorStop['color'] = DEFAULT_VISUALIZATION_COLOR, max: number ) => { const index = colorStops.length - 1; From f51cf46bfcead73ad7f42e1925a2a4d375e937ea Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Wed, 25 Sep 2019 17:16:26 -0500 Subject: [PATCH 15/29] readOnly and disabled --- .../src/views/color_picker/color_stops.js | 23 ++++++--- .../color_stops/_color_stops.scss | 6 +-- .../color_stops/color_stop_thumb.tsx | 39 ++++++++++----- .../color_picker/color_stops/color_stops.tsx | 47 ++++++++++++++----- src/components/form/range/range_thumb.tsx | 1 + 5 files changed, 82 insertions(+), 34 deletions(-) diff --git a/src-docs/src/views/color_picker/color_stops.js b/src-docs/src/views/color_picker/color_stops.js index 0f03fcf6eca..bd23cd428a1 100644 --- a/src-docs/src/views/color_picker/color_stops.js +++ b/src-docs/src/views/color_picker/color_stops.js @@ -1,6 +1,11 @@ import React, { useState } from 'react'; -import { EuiColorStops, EuiFormRow } from '../../../../src/components'; +import { DisplayToggles } from '../form_controls/display_toggles'; +import { + EuiColorStops, + EuiFormRow, + EuiSpacer, +} from '../../../../src/components'; export const ColorStops = () => { const generateRandomColor = () => @@ -99,7 +104,6 @@ export const ColorStops = () => { addColor={addColor} /> - { max={400} /> - { mode="swatch" /> - { mode="picker" /> - { swatches={['#333', '#666', '#999', '#CCC']} /> - { stopType="fixed" /> + + + + + + ); }; diff --git a/src/components/color_picker/color_stops/_color_stops.scss b/src/components/color_picker/color_stops/_color_stops.scss index 603144e8e19..ea0dcb026d4 100644 --- a/src/components/color_picker/color_stops/_color_stops.scss +++ b/src/components/color_picker/color_stops/_color_stops.scss @@ -1,6 +1,6 @@ @import '../../form/range/_variables'; -.euiColorStops { +.euiColorStops:not(.euiColorStops-isDisabled) { &:focus { outline: 2px solid $euiFocusRingColor; } @@ -55,7 +55,7 @@ height: 100%; } -.euiColorStopThumb.euiRangeThumb { +.euiColorStopThumb.euiRangeThumb:not(:disabled) { top: 0; margin-top: 0; pointer-events: auto; @@ -66,6 +66,6 @@ } } -.euiColorStops.euiColorStops-isDragging { +.euiColorStops.euiColorStops-isDragging:not(.euiColorStops-isDisabled):not(.euiColorStops-isReadOnly) { cursor: grabbing; } 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 e25a3e7502d..0ae5b85326a 100644 --- a/src/components/color_picker/color_stops/color_stop_thumb.tsx +++ b/src/components/color_picker/color_stops/color_stop_thumb.tsx @@ -37,6 +37,8 @@ interface EuiColorStopThumbProps extends CommonProps, ColorStop { parentRef?: HTMLDivElement | null; colorPickerMode: EuiColorPickerProps['mode']; colorPickerSwatches?: EuiColorPickerProps['swatches']; + disabled?: boolean; + readOnly?: boolean; } export const EuiColorStopThumb: FunctionComponent = ({ @@ -53,6 +55,8 @@ export const EuiColorStopThumb: FunctionComponent = ({ parentRef, colorPickerMode, colorPickerSwatches, + disabled, + readOnly, }) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [colorIsInvalid, setColorIsInvalid] = useState(isColorInvalid(color)); @@ -176,15 +180,16 @@ export const EuiColorStopThumb: FunctionComponent = ({ value={stop} onClick={openPopover} onFocus={onFocus} - onKeyDown={handleKeyDown} - onMouseDown={handleMouseDown} - onTouchStart={handleInteraction} - onTouchMove={handleInteraction} + onKeyDown={readOnly ? undefined : handleKeyDown} + onMouseDown={readOnly ? undefined : handleMouseDown} + onTouchStart={readOnly ? undefined : handleInteraction} + onTouchMove={readOnly ? undefined : handleInteraction} className="euiColorStopThumb" tabIndex={-1} style={{ background: color, }} + disabled={disabled} /> }>
@@ -200,11 +205,13 @@ export const EuiColorStopThumb: FunctionComponent = ({ = ({ color="danger" aria-label={removeLabel} title={removeLabel} - disabled={!onRemove} + disabled={!onRemove || readOnly} onClick={handleOnRemove} /> )} @@ -239,14 +246,18 @@ export const EuiColorStopThumb: FunctionComponent = ({ - - + {!readOnly && ( + + + + + )} {colorPickerMode !== 'swatch' && ( @@ -260,10 +271,12 @@ export const EuiColorStopThumb: FunctionComponent = ({ ) => diff --git a/src/components/color_picker/color_stops/color_stops.tsx b/src/components/color_picker/color_stops/color_stops.tsx index 5bf3dafce86..3b6b4c87038 100644 --- a/src/components/color_picker/color_stops/color_stops.tsx +++ b/src/components/color_picker/color_stops/color_stops.tsx @@ -23,6 +23,10 @@ interface EuiColorStopsProps extends CommonProps { colorStops: ColorStop[]; onChange: (stops?: ColorStop[], isInvalid?: boolean) => void; fullWidth?: boolean; + disabled?: boolean; + readOnly?: boolean; + invalid?: boolean; + compressed?: boolean; className?: string; max: number; min: number; @@ -58,6 +62,10 @@ export const EuiColorStops: FunctionComponent = ({ mode = 'default', colorStops, onChange, + disabled, + readOnly, + // invalid, + compressed, fullWidth, className, stopType = 'gradient', @@ -85,9 +93,15 @@ export const EuiColorStops: FunctionComponent = ({ } }, [sortedStops]); + const isNotInteractive = disabled || readOnly; + const classes = classNames( 'euiColorStops', - { 'euiColorStops-isDragging': isHoverDisabled }, + { + 'euiColorStops-isDragging': isHoverDisabled, + 'euiColorStops-isDisabled': disabled, + 'euiColorStops-isReadOnly': readOnly, + }, className ); @@ -112,7 +126,7 @@ export const EuiColorStops: FunctionComponent = ({ }; const onFocusStop = (index: number) => { - if (!wrapperRef) return; + if (disabled || !wrapperRef) return; const toFocus = wrapperRef.querySelector( `#${STOP_ATTR}${index}` ); @@ -145,7 +159,7 @@ export const EuiColorStops: FunctionComponent = ({ }; const handleAddHover = (e: React.MouseEvent) => { - if (!wrapperRef) return; + if (isNotInteractive || !wrapperRef) return; const stop = getStopFromMouseLocationFn({ x: e.pageX, y: e.pageY }); const position = getPositionFromStopFn(stop); @@ -153,7 +167,7 @@ export const EuiColorStops: FunctionComponent = ({ }; const handleAddClick = (e: React.MouseEvent) => { - if (isTargetAThumb(e.target) || !wrapperRef) return; + if (isNotInteractive || isTargetAThumb(e.target) || !wrapperRef) return; const newStop = getStopFromMouseLocationFn({ x: e.pageX, y: e.pageY }); const newColorStops = addDefinedStop(colorStops, newStop, addColor); @@ -162,18 +176,19 @@ export const EuiColorStops: FunctionComponent = ({ }; const handleKeyDown = (e: React.KeyboardEvent) => { + if (disabled) return; switch (e.keyCode) { case keyCodes.ESCAPE: onFocusWrapper(); break; case keyCodes.ENTER: - if (!hasFocus) return; + if (readOnly || !hasFocus) return; onAdd(); break; case keyCodes.BACKSPACE: - if (hasFocus || focusedStopIndex == null) return; + if (readOnly || hasFocus || focusedStopIndex == null) return; if (isTargetAThumb(e.target)) { const index = sortedStops[focusedStopIndex].id; onRemove(index); @@ -230,6 +245,8 @@ export const EuiColorStops: FunctionComponent = ({ parentRef={wrapperRef} colorPickerMode={mode} colorPickerSwatches={swatches} + disabled={disabled} + readOnly={readOnly} /> )); @@ -263,10 +280,10 @@ export const EuiColorStops: FunctionComponent = ({ ref={setWrapperRef} className={classes} fullWidth={fullWidth} - tabIndex={0} - onMouseDown={() => setIsHoverDisabled(true)} - onMouseUp={() => setIsHoverDisabled(false)} - onMouseLeave={() => setIsHoverDisabled(false)} + tabIndex={disabled ? -1 : 0} + onMouseDown={() => !disabled && setIsHoverDisabled(true)} + onMouseUp={() => !disabled && setIsHoverDisabled(false)} + onMouseLeave={() => !disabled && setIsHoverDisabled(false)} onKeyDown={handleKeyDown} onFocus={e => { if (e.target === wrapperRef) { @@ -274,17 +291,23 @@ export const EuiColorStops: FunctionComponent = ({ } }} onBlur={() => setHasFocus(false)}> - +
diff --git a/src/components/form/range/range_thumb.tsx b/src/components/form/range/range_thumb.tsx index 8e850f39ef6..459ce70b681 100644 --- a/src/components/form/range/range_thumb.tsx +++ b/src/components/form/range/range_thumb.tsx @@ -52,6 +52,7 @@ export const EuiRangeThumb: FunctionComponent = ({ { { { { { { { { = ({ compressed, fullWidth, className, + label, stopType = 'gradient', swatches, }) => { @@ -298,8 +300,13 @@ export const EuiColorStops: FunctionComponent = ({

From d078fc837a113f8d9dbd6c14fc8479c8f0ca7b77 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Mon, 30 Sep 2019 13:54:41 -0500 Subject: [PATCH 22/29] misc feeback --- .../color_stops/color_stop_thumb.tsx | 23 +++++++-------- .../color_picker/color_stops/color_stops.tsx | 29 +++++++++---------- .../color_picker/color_stops/index.ts | 2 +- .../color_picker/color_stops/utils.ts | 12 ++++++-- src/components/form/range/range_highlight.tsx | 6 ++-- src/components/form/range/range_thumb.tsx | 1 - 6 files changed, 37 insertions(+), 36 deletions(-) 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 ad040c2dd74..8e1e56bf2f2 100644 --- a/src/components/color_picker/color_stops/color_stop_thumb.tsx +++ b/src/components/color_picker/color_stops/color_stop_thumb.tsx @@ -33,7 +33,6 @@ export interface ColorStop { } interface EuiColorStopThumbProps extends CommonProps, ColorStop { - id?: string; onChange: (colorStop: ColorStop) => void; onFocus?: () => void; onRemove?: () => void; @@ -46,11 +45,11 @@ interface EuiColorStopThumbProps extends CommonProps, ColorStop { colorPickerSwatches?: EuiColorPickerProps['swatches']; disabled?: boolean; readOnly?: boolean; + 'data-index'?: string; 'aria-valuetext'?: string; } export const EuiColorStopThumb: FunctionComponent = ({ - id, stop, color, onChange, @@ -65,6 +64,7 @@ export const EuiColorStopThumb: FunctionComponent = ({ colorPickerSwatches, disabled, readOnly, + 'data-index': dataIndex, 'aria-valuetext': ariaValueText, }) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); @@ -80,12 +80,12 @@ export const EuiColorStopThumb: FunctionComponent = ({ }, [stop]); const getStopFromMouseLocationFn = (location: { x: number; y: number }) => { - // Guards against `null` ref in usage + // Guard against `null` ref in usage return getStopFromMouseLocation(location, parentRef!, globalMin, globalMax); }; const getPositionFromStopFn = (stop: ColorStop['stop']) => { - // Guards against `null` ref in usage + // Guard against `null` ref in usage return getPositionFromStop(stop, parentRef!, globalMin, globalMax); }; @@ -105,13 +105,10 @@ export const EuiColorStopThumb: FunctionComponent = ({ onChange({ stop, color: value }); }; - const handleStopChange = ( - value: ColorStop['stop'], - shouldRespectBoundaries: boolean = false - ) => { + const handleStopChange = (value: ColorStop['stop']) => { const willBeInvalid = value > max || value < min; - if (shouldRespectBoundaries && willBeInvalid) { + if (willBeInvalid) { if (value > max) { value = max; } @@ -147,19 +144,19 @@ export const EuiColorStopThumb: FunctionComponent = ({ return; } const newStop = getStopFromMouseLocationFn(location); - handleStopChange(newStop, true); + handleStopChange(newStop); }; const handleKeyDown = (e: React.KeyboardEvent) => { switch (e.keyCode) { case keyCodes.LEFT: e.preventDefault(); - handleStopChange(stop - 1, true); + handleStopChange(stop - 1); break; case keyCodes.RIGHT: e.preventDefault(); - handleStopChange(stop + 1, true); + handleStopChange(stop + 1); break; } }; @@ -196,7 +193,7 @@ export const EuiColorStopThumb: FunctionComponent = ({ const title = buttonTitle as string; return ( void; @@ -38,13 +38,14 @@ interface EuiColorStopsProps extends CommonProps { swatches?: EuiColorPickerProps['swatches']; } -// Becuase of how the thumbs are rendered in the popover, using ref results in an infinite loop. +// Because of how the thumbs are rendered in the popover, using ref results in an infinite loop. // We'll instead use old fashioned namespaced DOM selectors to get references const STOP_ATTR = 'euiColorStop_'; function isTargetAThumb(target: HTMLElement | EventTarget) { const element = target as HTMLElement; - return element.id.indexOf(STOP_ATTR) > -1; + const attr = element.getAttribute('data-index'); + return attr && attr.indexOf(STOP_ATTR) > -1; } function sortStops(colorStops: ColorStop[]) { @@ -74,7 +75,7 @@ export const EuiColorStops: FunctionComponent = ({ stopType = 'gradient', swatches, }) => { - const [sortedStops, setSortedStops] = useState(sortStops(colorStops)); + const sortedStops = useMemo(() => sortStops(colorStops), [colorStops]); const [hasFocus, setHasFocus] = useState(false); const [focusedStopIndex, setFocusedStopIndex] = useState(null); const [wrapperRef, setWrapperRef] = useState(null); @@ -84,10 +85,6 @@ export const EuiColorStops: FunctionComponent = ({ null ); - useEffect(() => { - setSortedStops(sortStops(colorStops)); - }, [colorStops]); - useEffect(() => { if (focusStopOnUpdate !== null) { const toFocus = sortedStops.map(el => el.stop).indexOf(focusStopOnUpdate); @@ -109,12 +106,12 @@ export const EuiColorStops: FunctionComponent = ({ ); const getStopFromMouseLocationFn = (location: { x: number; y: number }) => { - // Guards against `null` ref in usage + // Guard against `null` ref in usage return getStopFromMouseLocation(location, wrapperRef!, min, max); }; const getPositionFromStopFn = (stop: ColorStop['stop']) => { - // Guards against `null` ref in usage + // Guard against `null` ref in usage return getPositionFromStop(stop, wrapperRef!, min, max); }; @@ -131,7 +128,7 @@ export const EuiColorStops: FunctionComponent = ({ const onFocusStop = (index: number) => { if (disabled || !wrapperRef) return; const toFocus = wrapperRef.querySelector( - `#${STOP_ATTR}${index}` + `[data-index=${STOP_ATTR}${index}]` ); if (toFocus) { setHasFocus(false); @@ -230,7 +227,7 @@ export const EuiColorStops: FunctionComponent = ({ const thumbs = sortedStops.map((colorStop, index) => ( = ({ /> )); - const positions = sortedStops.map(({ stop }) => getPositionFromStopFn(stop)); + const positions = wrapperRef + ? sortedStops.map(({ stop }) => getPositionFromStopFn(stop)) + : []; const gradientStop = (colorStop: ColorStop, index: number) => { return `${colorStop.color} ${positions[index]}%`; }; @@ -320,7 +319,7 @@ export const EuiColorStops: FunctionComponent = ({ max={max} lowerValue={min} upperValue={max} - color={background} + background={background} compressed={compressed} />
{ if (colorStops.length === 1) { return colorStops; @@ -73,7 +75,6 @@ export const isInvalid = (colorStops: ColorStop[]) => { }; export const calculateScale = (trackWidth: number) => { - const EUI_THUMB_SIZE = 16; const thumbToTrackRatio = EUI_THUMB_SIZE / trackWidth; return (1 - thumbToTrackRatio) * 100; }; @@ -94,7 +95,12 @@ export const getPositionFromStop = ( min: number, max: number ) => { - return Math.round( - ((stop - min) / (max - min)) * calculateScale(ref ? ref.clientWidth : 100) + // For wide implementations, integer percentages can be visually off. + // Use 1 decimal place for more accuracy + return parseFloat( + ( + ((stop - min) / (max - min)) * + calculateScale(ref ? ref.clientWidth : 100) + ).toFixed(1) ); }; diff --git a/src/components/form/range/range_highlight.tsx b/src/components/form/range/range_highlight.tsx index 4ffbe74b6f5..e4eea407dd5 100644 --- a/src/components/form/range/range_highlight.tsx +++ b/src/components/form/range/range_highlight.tsx @@ -2,7 +2,7 @@ import React, { FunctionComponent } from 'react'; import classNames from 'classnames'; export interface EuiRangeHighlightProps { - color?: string; + background?: string; compressed?: boolean; hasFocus?: boolean; showTicks?: boolean; @@ -21,7 +21,7 @@ export const EuiRangeHighlight: FunctionComponent = ({ max, min, compressed, - color, + background, onClick, }) => { // Calculate the width the range based on value @@ -29,7 +29,7 @@ export const EuiRangeHighlight: FunctionComponent = ({ const leftPosition = (lowerValue - min) / (max - min); const rangeWidth = (upperValue - lowerValue) / (max - min); const rangeWidthStyle = { - background: color, + background, marginLeft: `${leftPosition * 100}%`, width: `${rangeWidth * 100}%`, }; diff --git a/src/components/form/range/range_thumb.tsx b/src/components/form/range/range_thumb.tsx index 459ce70b681..e1b7c0196ff 100644 --- a/src/components/form/range/range_thumb.tsx +++ b/src/components/form/range/range_thumb.tsx @@ -10,7 +10,6 @@ interface BaseProps extends CommonProps { disabled?: boolean; showInput?: boolean; showTicks?: boolean; - thumbRef?: (node: HTMLButtonElement | null) => void; } interface ButtonLike extends BaseProps, HTMLAttributes {} From 32c720945428e4857822d25f72877f8582c30cf8 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Mon, 30 Sep 2019 14:28:11 -0500 Subject: [PATCH 23/29] update zIndex for active thumb --- .../color_stops/_color_stops.scss | 4 +++ .../color_stops/color_stop_thumb.tsx | 26 +++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/components/color_picker/color_stops/_color_stops.scss b/src/components/color_picker/color_stops/_color_stops.scss index 2ab7b5c3654..99fa4333d28 100644 --- a/src/components/color_picker/color_stops/_color_stops.scss +++ b/src/components/color_picker/color_stops/_color_stops.scss @@ -49,6 +49,10 @@ margin-top: $euiRangeThumbHeight * -.5; } +.euiColorStopPopover-hasFocus { + z-index: 1; +} + .euiColorStopPopover__anchor { position: absolute; width: 100%; 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 8e1e56bf2f2..2c1cb0eff3d 100644 --- a/src/components/color_picker/color_stops/color_stop_thumb.tsx +++ b/src/components/color_picker/color_stops/color_stop_thumb.tsx @@ -5,6 +5,7 @@ import React, { useRef, useState, } from 'react'; +import classNames from 'classnames'; import { CommonProps } from '../../common'; import { @@ -33,6 +34,7 @@ export interface ColorStop { } interface EuiColorStopThumbProps extends CommonProps, ColorStop { + className?: string; onChange: (colorStop: ColorStop) => void; onFocus?: () => void; onRemove?: () => void; @@ -50,6 +52,7 @@ interface EuiColorStopThumbProps extends CommonProps, ColorStop { } export const EuiColorStopThumb: FunctionComponent = ({ + className, stop, color, onChange, @@ -68,6 +71,7 @@ export const EuiColorStopThumb: FunctionComponent = ({ 'aria-valuetext': ariaValueText, }) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [hasFocus, setHasFocus] = useState(false); const [colorIsInvalid, setColorIsInvalid] = useState(isColorInvalid(color)); const [stopIsInvalid, setStopIsInvalid] = useState(isStopInvalid(stop)); const [numberInputRef, setNumberInputRef] = useState(); @@ -100,6 +104,13 @@ export const EuiColorStopThumb: FunctionComponent = ({ } }; + const handleFocus = () => { + setHasFocus(true); + if (onFocus) { + onFocus(); + } + }; + const handleColorChange = (value: ColorStop['color']) => { setColorIsInvalid(isColorInvalid(value)); onChange({ stop, color: value }); @@ -165,10 +176,18 @@ export const EuiColorStopThumb: FunctionComponent = ({ handlePointerChange ); + const classes = classNames( + 'euiColorStopPopover', + { + 'euiColorStopPopover-hasFocus': hasFocus || isPopoverOpen, + }, + className + ); + return ( = ({ max={max} value={stop} onClick={openPopover} - onFocus={onFocus} + onFocus={handleFocus} + onBlur={() => setHasFocus(false)} + onMouseOver={() => setHasFocus(true)} + onMouseOut={() => setHasFocus(false)} onKeyDown={readOnly ? undefined : handleKeyDown} onMouseDown={readOnly ? undefined : handleMouseDown} onTouchStart={readOnly ? undefined : handleInteraction} From 020a44b758556f0c0061c418e90aa0ccf16a519c Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Tue, 1 Oct 2019 14:23:26 -0500 Subject: [PATCH 24/29] docs refactor --- .../color_picker/color_picker_example.js | 232 +++++++++++++----- .../src/views/color_picker/color_stops.js | 101 +------- src-docs/src/views/color_picker/containers.js | 181 +++++++------- .../src/views/color_picker/custom_swatches.js | 62 ++--- .../src/views/color_picker/kitchen_sink.js | 48 ++-- src-docs/src/views/color_picker/modes.js | 95 +++---- src-docs/src/views/color_picker/utils.js | 42 ++++ 7 files changed, 422 insertions(+), 339 deletions(-) create mode 100644 src-docs/src/views/color_picker/utils.js 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 15ab99b6535..5cf265e2988 100644 --- a/src-docs/src/views/color_picker/color_picker_example.js +++ b/src-docs/src/views/color_picker/color_picker_example.js @@ -8,6 +8,7 @@ import { EuiCode, EuiColorPicker, EuiColorStops, + EuiSpacer, EuiText, } from '../../../../src/components'; @@ -22,6 +23,36 @@ const colorPickerSnippet = ` `; +import { ColorStops } from './color_stops'; +const colorStopsSource = require('!!raw-loader!./color_stops'); +const colorStopsHtml = renderToHtml(ColorStops); +const colorStopsSnippetStandard = ``; + +const colorStopsSnippetAdd = ``; + +const colorStopsSnippetFixed = ` +`; + import { CustomSwatches } from './custom_swatches'; const customSwatchesSource = require('!!raw-loader!./custom_swatches'); const customSwatchesHtml = renderToHtml(CustomSwatches); @@ -36,6 +67,20 @@ const customSwatchesSnippet = ``; + +const stopCustomSwatchesSnippet = ` `; @@ -88,6 +133,26 @@ const modesPickerSnippet = `// Gradient map only mode="picker" /> `; +const stopModesSwatchSnippet = `// Swatches only + +`; +const stopModesPickerSnippet = `// Gradient map only + +`; import { Inline } from './inline'; const inlineSource = require('!!raw-loader!./inline'); @@ -125,39 +190,60 @@ const kitchenSinkSnippet = ` `; - -import { ColorStops } from './color_stops'; -const colorStopsSource = require('!!raw-loader!./color_stops'); -const colorStopsHtml = renderToHtml(ColorStops); -const colorStopsSnippet = ` `; export const ColorPickerExample = { - title: 'Color Picker', + title: 'Color Selection', intro: (

- Color input component allowing for multiple methods of entry and - 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. -

-

- Swatches allow consumers to predefine preferred or suggested choices. - The swatches must also be entered in hex format. + Two components exist to aid color selection:{' '} + EuiColorPicker and EuiColorStops + .

+
), sections: [ { + title: 'Color picker', + text: ( + + +

+ Color input component allowing for multiple methods of entry and + 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. +

+

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

+
+
+ ), source: [ { type: GuideSectionTypes.JS, @@ -172,6 +258,38 @@ export const ColorPickerExample = { snippet: colorPickerSnippet, demo: , }, + { + title: 'Color stops', + text: ( + + +

+ Use EuiColorStops to define color stops for + data driven styling. Stops are numbers within the provided range. + The color segment spans from the given stop number (inclusive) to + the next stop number (exclusive). +

+
+
+ ), + source: [ + { + type: GuideSectionTypes.JS, + code: colorStopsSource, + }, + { + type: GuideSectionTypes.HTML, + code: colorStopsHtml, + }, + ], + props: { EuiColorStops }, + snippet: [ + colorStopsSnippetStandard, + colorStopsSnippetAdd, + colorStopsSnippetFixed, + ], + demo: , + }, { title: 'Custom color swatches', source: [ @@ -191,54 +309,59 @@ export const ColorPickerExample = { the swatches prop.

), - snippet: customSwatchesSnippet, + snippet: [customSwatchesSnippet, stopCustomSwatchesSnippet], demo: , }, { - title: 'Custom button', + title: 'Limited selection modes', source: [ { type: GuideSectionTypes.JS, - code: customButtonSource, + code: modesSource, }, { type: GuideSectionTypes.HTML, - code: customButtonHtml, + code: modesHtml, }, ], text: (

- You can optionally use a custom button as the trigger for selection - using the button prop. Please remember to add - accessibility to this component, using proper button markup and aria - labeling. + By default, both swatch selection and the gradient color map will be + rendered. Use the mode prop to pass `swatch` for + swatch-only selection, or pass `picker` for gradient map and hue + slider selection without swatches.

), - snippet: [customButtonSnippet, customBadgeSnippet], - demo: , + snippet: [ + modesSwatchSnippet, + modesPickerSnippet, + stopModesSwatchSnippet, + stopModesPickerSnippet, + ], + demo: , }, { - title: 'Limited selection modes', + title: 'Custom button', source: [ { type: GuideSectionTypes.JS, - code: modesSource, + code: customButtonSource, }, { type: GuideSectionTypes.HTML, - code: modesHtml, + code: customButtonHtml, }, ], text: (

- By default, both swatch selection and the gradient color map will be - rendered. Use the mode prop to pass `swatch` for - swatch-only selection, or pass `picker` for gradient map and hue - slider selection without swatches. + Available only in EuiColorPicker. You can + optionally use a custom button as the trigger for selection using the{' '} + button prop. Please remember to add accessibility + to this component, using proper button markup and aria labeling.

), - snippet: [modesSwatchSnippet, modesPickerSnippet], - demo: , + snippet: [customButtonSnippet, customBadgeSnippet], + demo: , }, { title: 'Inline', @@ -254,8 +377,9 @@ export const ColorPickerExample = { ], text: (

- Set the display prop to `inline` to display the - color picker without an input or popover. Note that the{' '} + Available only in EuiColorPicker. Set the{' '} + display prop to `inline` to display the color + picker without an input or popover. Note that the{' '} button prop will be ignored in this case.

), @@ -276,15 +400,15 @@ export const ColorPickerExample = { ], text: (

- Demonstrating that EuiColorPicker can exist in - portal containers and that its popover position works in nested + Demonstrating that both color selection components can exist in portal + containers and that their popover positioning works in nested contexts.

), demo: , }, { - title: 'Kitchen sink', + title: 'Option toggling', source: [ { type: GuideSectionTypes.JS, @@ -295,32 +419,8 @@ export const ColorPickerExample = { code: kitchenSinkHtml, }, ], - snippet: kitchenSinkSnippet, + snippet: [kitchenSinkSnippet, stopKitchenSinkSnippet], demo: , }, - { - title: 'Color stops', - source: [ - { - type: GuideSectionTypes.JS, - code: colorStopsSource, - }, - { - type: GuideSectionTypes.HTML, - code: colorStopsHtml, - }, - ], - props: { EuiColorStops }, - text: ( -

- Use EuiColorStops to define color stops for data - driven styling. Stops are numbers within the provided range. The color - segment spans from the given stop number (inclusive) to the next stop - number (exclusive). -

- ), - snippet: colorStopsSnippet, - demo: , - }, ], }; diff --git a/src-docs/src/views/color_picker/color_stops.js b/src-docs/src/views/color_picker/color_stops.js index 153349f747d..e92f36a631f 100644 --- a/src-docs/src/views/color_picker/color_stops.js +++ b/src-docs/src/views/color_picker/color_stops.js @@ -1,37 +1,11 @@ import React, { useState } from 'react'; -import { DisplayToggles } from '../form_controls/display_toggles'; -import { - EuiColorStops, - EuiFormRow, - EuiSpacer, -} from '../../../../src/components'; +import { EuiColorStops, EuiFormRow } from '../../../../src/components'; -export const ColorStops = () => { - const generateRandomColor = () => - // https://www.paulirish.com/2009/random-hex-color-code-snippets/ - `#${Math.floor(Math.random() * 16777215).toString(16)}`; +import { useColorStop } from './utils'; - const [addColor, setAddColor] = useState(generateRandomColor()); - const [colorStops, setColorStops] = useState([ - { - stop: 20, - color: '#00B3A4', - }, - { - stop: 50, - color: '#DB1374', - }, - { - stop: 65, - color: '#490092', - }, - ]); - - const handleChange = colorStops => { - setColorStops(colorStops); - setAddColor(generateRandomColor()); - }; +export const ColorStops = () => { + const [colorStops, setColorStops, addColor] = useColorStop(true); const [extendedColorStops, setExtendedColorStops] = useState([ { @@ -58,26 +32,8 @@ export const ColorStops = () => { setEmptyColorStops(colorStops); }; - const changeProps = () => { - setColorStops([ - { - stop: 0, - color: '#ff0000', - }, - { - stop: 25, - color: '#FFFF00', - }, - { - stop: 45, - color: '#008000', - }, - ]); - }; - return ( - { { max={400} /> - - - - - - - - - - - - - - - ); }; diff --git a/src-docs/src/views/color_picker/containers.js b/src-docs/src/views/color_picker/containers.js index ad38d3c1e01..7f6923ea65d 100644 --- a/src-docs/src/views/color_picker/containers.js +++ b/src-docs/src/views/color_picker/containers.js @@ -1,7 +1,8 @@ -import React, { Component, Fragment } from 'react'; +import React, { Fragment, useState } from 'react'; import { EuiColorPicker, + EuiColorStops, EuiButton, EuiPopover, EuiFormRow, @@ -13,104 +14,104 @@ import { EuiSpacer, } from '../../../../src/components'; -export default class extends Component { - constructor(props) { - super(props); +import { useColorPicker, useColorStop } from './utils'; - this.state = { - color: '#FFF', - isModalVisible: false, - isPopoverOpen: false, - }; - } - - closeModal = () => { - this.setState({ isModalVisible: false }); - }; +export default () => { + const [color, setColor] = useColorPicker('#FFF'); + const [colorStops, setColorStops] = useColorStop(); + const [isModalVisible, setIsModalVisible] = useState(false); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); - showModal = () => { - this.setState({ isModalVisible: true }); + const closeModal = () => { + setIsModalVisible(false); }; - togglePopover = () => { - this.setState(prevState => ({ - isPopoverOpen: !prevState.isPopoverOpen, - })); + const showModal = () => { + setIsModalVisible(true); }; - closePopover = () => { - this.setState({ - isPopoverOpen: false, - }); + const togglePopover = () => { + setIsPopoverOpen(!isPopoverOpen); }; - onChange = color => { - this.setState({ - color, - }); + const closePopover = () => { + setIsPopoverOpen(false); }; - render() { - const { color, isModalVisible, isPopoverOpen } = this.state; - - const colorPicker = ( - - ); - - const button = ( - - Open popover - - ); - - let modal; - - if (isModalVisible) { - modal = ( - - - - Color picker in a modal - - - - {colorPicker} - - - - ); - } - - return ( - - - {colorPicker} - - - - -
- {colorPicker} -
-
-
- - - - Show modal - - {modal} -
+ const colorPicker = ; + + const stops = ( + + ); + + const button = ( + + Open popover + + ); + + let modal; + + if (isModalVisible) { + modal = ( + + + + Color picker in a modal + + + + {colorPicker} + + {stops} + + + ); } -} + + return ( + + + {colorPicker} + + + + + + {stops} + + + + +
+ {colorPicker} + + {stops} +
+
+
+ + + + Show modal + + {modal} +
+ ); +}; diff --git a/src-docs/src/views/color_picker/custom_swatches.js b/src-docs/src/views/color_picker/custom_swatches.js index 789a5225aa8..b8256d60b04 100644 --- a/src-docs/src/views/color_picker/custom_swatches.js +++ b/src-docs/src/views/color_picker/custom_swatches.js @@ -1,39 +1,43 @@ -import React, { Component } from 'react'; +import React from 'react'; -import { EuiColorPicker, EuiFormRow } from '../../../../src/components'; -import { isValidHex } from '../../../../src/services'; +import { + EuiColorPicker, + EuiColorStops, + EuiFormRow, + EuiSpacer, +} from '../../../../src/components'; -export class CustomSwatches extends Component { - constructor(props) { - super(props); - this.state = { - color: '', - }; - } +import { useColorPicker, useColorStop } from './utils'; - handleChange = value => { - this.setState({ color: value }); - }; +export const CustomSwatches = () => { + const [color, setColor, errors] = useColorPicker(); + const [colorStops, setColorStops] = useColorStop(); - render() { - const hasErrors = !isValidHex(this.state.color) && this.state.color !== ''; + const customSwatches = ['#333', '#666', '#999', '#CCC']; - let errors; - if (hasErrors) { - errors = ['Provide a valid hex value']; - } + return ( + + + + - const customSwatches = ['#333', '#666', '#999', '#CCC']; + - return ( - - + - ); - } -} + + ); +}; diff --git a/src-docs/src/views/color_picker/kitchen_sink.js b/src-docs/src/views/color_picker/kitchen_sink.js index 3366f7617ad..258af45a6c7 100644 --- a/src-docs/src/views/color_picker/kitchen_sink.js +++ b/src-docs/src/views/color_picker/kitchen_sink.js @@ -1,26 +1,36 @@ -import React, { Component } from 'react'; +import React from 'react'; -import { EuiColorPicker } from '../../../../src/components'; +import { + EuiColorPicker, + EuiColorStops, + EuiSpacer, +} from '../../../../src/components'; import { DisplayToggles } from '../form_controls/display_toggles'; -export class KitchenSink extends Component { - constructor(props) { - super(props); - this.state = { - color: '', - }; - } +import { useColorPicker, useColorStop } from './utils'; - handleChange = value => { - this.setState({ color: value }); - }; +export const KitchenSink = () => { + const [color, setColor] = useColorPicker('#DB1374'); + const [colorStops, setColorStops, addStop] = useColorStop(true); - render() { - return ( - /* DisplayToggles wrapper for Docs only */ + return ( + + {/* DisplayToggles wrapper for Docs only */} - + - ); - } -} + + {/* DisplayToggles wrapper for Docs only */} + + + + + ); +}; diff --git a/src-docs/src/views/color_picker/modes.js b/src-docs/src/views/color_picker/modes.js index 17ec18086d1..762950fb9a1 100644 --- a/src-docs/src/views/color_picker/modes.js +++ b/src-docs/src/views/color_picker/modes.js @@ -1,47 +1,60 @@ -import React, { Component } from 'react'; +import React from 'react'; -import { EuiColorPicker, EuiFormRow } from '../../../../src/components'; -import { isValidHex } from '../../../../src/services'; +import { + EuiColorPicker, + EuiColorStops, + EuiFormRow, + EuiSpacer, +} from '../../../../src/components'; -export class Modes extends Component { - constructor(props) { - super(props); - this.state = { - color: '#DB1374', - }; - } +import { useColorPicker, useColorStop } from './utils'; - handleChange = value => { - this.setState({ color: value }); - }; +export const Modes = () => { + const [color, setColor, errors] = useColorPicker('#DB1374'); + const [colorStops, setColorStops] = useColorStop(); - render() { - const hasErrors = !isValidHex(this.state.color) && this.state.color !== ''; + return ( + + + + + + + - let errors; - if (hasErrors) { - errors = ['Provide a valid hex value']; - } + - return ( - - - - - - - - - ); - } -} + + + + + + + + + ); +}; diff --git a/src-docs/src/views/color_picker/utils.js b/src-docs/src/views/color_picker/utils.js new file mode 100644 index 00000000000..d375427c3d9 --- /dev/null +++ b/src-docs/src/views/color_picker/utils.js @@ -0,0 +1,42 @@ +import { useMemo, useState } from 'react'; +import { isValidHex } from '../../../../src/services'; + +const generateRandomColor = () => + // https://www.paulirish.com/2009/random-hex-color-code-snippets/ + `#${Math.floor(Math.random() * 16777215).toString(16)}`; + +export const useColorStop = (useRandomColor = false) => { + const [addColor, setAddColor] = useState(generateRandomColor()); + const [colorStops, setColorStops] = useState([ + { + stop: 20, + color: '#00B3A4', + }, + { + stop: 50, + color: '#DB1374', + }, + { + stop: 65, + color: '#490092', + }, + ]); + + const updateColorStops = colorStops => { + setColorStops(colorStops); + if (useRandomColor) { + setAddColor(generateRandomColor()); + } + }; + return [colorStops, updateColorStops, addColor]; +}; + +export const useColorPicker = (initialColor = '') => { + const [color, setColor] = useState(initialColor); + const errors = useMemo(() => { + const hasErrors = !isValidHex(color) && color !== ''; + return hasErrors ? ['Provide a valid hex value'] : null; + }, [color]); + + return [color, setColor, errors]; +}; From 90f6cf4c6f838a8d3a75fd16ba5c02c050bec8f7 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Tue, 1 Oct 2019 15:14:34 -0500 Subject: [PATCH 25/29] util; basic snapshot tests --- .../__snapshots__/color_stops.test.tsx.snap | 674 ++++++++++++++++++ .../color_stops/color_stops.test.tsx | 122 ++++ .../color_picker/color_stops/utils.test.ts | 19 +- 3 files changed, 814 insertions(+), 1 deletion(-) create mode 100644 src/components/color_picker/color_stops/__snapshots__/color_stops.test.tsx.snap create mode 100644 src/components/color_picker/color_stops/color_stops.test.tsx 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 new file mode 100644 index 00000000000..3400fa11833 --- /dev/null +++ b/src/components/color_picker/color_stops/__snapshots__/color_stops.test.tsx.snap @@ -0,0 +1,674 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders EuiColorStops 1`] = ` +
+

+ Test: Color stop picker. Each stop consists of a number and corresponding color value. Use the Down and Up arrow keys to select individual stops. Press the Enter key to create a new stop. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`renders compressed EuiColorStops 1`] = ` +
+

+ Test: Color stop picker. Each stop consists of a number and corresponding color value. Use the Down and Up arrow keys to select individual stops. Press the Enter key to create a new stop. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`renders disabled EuiColorStops 1`] = ` +
+

+ Test: Disabled. Color stop picker. Each stop consists of a number and corresponding color value. Use the Down and Up arrow keys to select individual stops. Press the Enter key to create a new stop. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`renders empty EuiColorStops 1`] = ` +
+

+ Test: Color stop picker. Each stop consists of a number and corresponding color value. Use the Down and Up arrow keys to select individual stops. Press the Enter key to create a new stop. +

+
+
+
+
+
+
+
+
+
+`; + +exports[`renders fixed stop EuiColorStops 1`] = ` +
+

+ Test: Color stop picker. Each stop consists of a number and corresponding color value. Use the Down and Up arrow keys to select individual stops. Press the Enter key to create a new stop. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`renders fullWidth EuiColorStops 1`] = ` +
+

+ Test: Color stop picker. Each stop consists of a number and corresponding color value. Use the Down and Up arrow keys to select individual stops. Press the Enter key to create a new stop. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`renders readOnly EuiColorStops 1`] = ` +
+

+ Test: Read-only. Color stop picker. Each stop consists of a number and corresponding color value. Use the Down and Up arrow keys to select individual stops. Press the Enter key to create a new stop. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; diff --git a/src/components/color_picker/color_stops/color_stops.test.tsx b/src/components/color_picker/color_stops/color_stops.test.tsx new file mode 100644 index 00000000000..794d7165121 --- /dev/null +++ b/src/components/color_picker/color_stops/color_stops.test.tsx @@ -0,0 +1,122 @@ +import React from 'react'; +import { render } from 'enzyme'; + +import { EuiColorStops } from './color_stops'; + +import { requiredProps } from '../../../test'; + +jest.mock('../../portal', () => ({ + // @ts-ignore + EuiPortal: ({ children }) => children, +})); + +const onChange = jest.fn(); + +const colorStopsArray = [ + { stop: 0, color: '#FF0000' }, + { stop: 25, color: '#00FF00' }, + { stop: 35, color: '#0000FF' }, +]; + +test('renders EuiColorStops', () => { + const colorStops = render( + + ); + expect(colorStops).toMatchSnapshot(); +}); + +test('renders compressed EuiColorStops', () => { + const colorStops = render( + + ); + expect(colorStops).toMatchSnapshot(); +}); + +test('renders readOnly EuiColorStops', () => { + const colorStops = render( + + ); + expect(colorStops).toMatchSnapshot(); +}); + +test('renders fullWidth EuiColorStops', () => { + const colorStops = render( + + ); + expect(colorStops).toMatchSnapshot(); +}); + +test('renders disabled EuiColorStops', () => { + const colorStops = render( + + ); + expect(colorStops).toMatchSnapshot(); +}); + +test('renders fixed stop EuiColorStops', () => { + const colorStops = render( + + ); + expect(colorStops).toMatchSnapshot(); +}); + +test('renders empty EuiColorStops', () => { + const colorStops = render( + + ); + expect(colorStops).toMatchSnapshot(); +}); diff --git a/src/components/color_picker/color_stops/utils.test.ts b/src/components/color_picker/color_stops/utils.test.ts index d46a6eddede..1b59f2953d1 100644 --- a/src/components/color_picker/color_stops/utils.test.ts +++ b/src/components/color_picker/color_stops/utils.test.ts @@ -1,4 +1,4 @@ -import { addStop, removeStop, isInvalid } from './utils'; +import { addStop, addDefinedStop, removeStop, isInvalid } from './utils'; const colorStops = [ { stop: 0, color: '#FF0000' }, @@ -69,6 +69,23 @@ describe('addStop', () => { }); }); +describe('addDefinedStop', () => { + const colorStops = [{ stop: 0, color: '#FF0000' }]; + test('Should add stop', () => { + expect(addDefinedStop(colorStops, 1)).toEqual([ + { stop: 0, color: '#FF0000' }, + { stop: 1, color: '#3185FC' }, + ]); + }); + + test('Should add stop with a specified color', () => { + expect(addDefinedStop(colorStops, 1, '#FFFFFF')).toEqual([ + { stop: 0, color: '#FF0000' }, + { stop: 1, color: '#FFFFFF' }, + ]); + }); +}); + describe('removeStop', () => { test('Should not remove only stop', () => { const colorStops = [{ stop: 0, color: '#FF0000' }]; From 7259d1a8b787288d4c91817c40fc47f5f1216c57 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Tue, 1 Oct 2019 16:35:04 -0500 Subject: [PATCH 26/29] color stops tests --- .../__snapshots__/color_stops.test.tsx.snap | 32 ++ .../color_stops/color_stop_thumb.tsx | 3 +- .../color_stops/color_stops.test.tsx | 306 +++++++++++++++++- .../color_picker/color_stops/color_stops.tsx | 2 + 4 files changed, 340 insertions(+), 3 deletions(-) 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 3400fa11833..e01bd844ecf 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 @@ -3,6 +3,7 @@ exports[`renders EuiColorStops 1`] = `

= ({ const title = buttonTitle as string; return ( = ({ }} }> -
+

({ // @ts-ignore @@ -18,6 +23,10 @@ const colorStopsArray = [ { stop: 35, color: '#0000FF' }, ]; +// Note: A couple tests that would be nice, but can't be accomplished at the moment: +// - Tab to bypass thumbs (tabindex="-1" not respected) +// - Drag to reposition thumb (we can't get real page position info) + test('renders EuiColorStops', () => { const colorStops = render( { ); expect(colorStops).toMatchSnapshot(); }); + +test('popover color selector is shown when the thumb is clicked', () => { + const colorStops = mount( + + ); + + findTestSubject(colorStops, 'euiColorStopThumb') + .first() + .simulate('click'); + const colorSelector = findTestSubject(colorStops, 'euiColorStopPopover'); + expect(colorSelector.length).toBe(1); +}); + +test('stop input updates stops', () => { + const colorStops = mount( + + ); + + findTestSubject(colorStops, 'euiColorStopThumb') + .first() + .simulate('click'); + const event = { target: { value: '10' } }; + const inputs = colorStops.find('input[type="number"]'); + expect(inputs.length).toBe(1); + inputs.simulate('change', event); + expect(onChange).toBeCalled(); + expect(onChange).toBeCalledWith( + [ + { color: '#FF0000', stop: 10 }, + { color: '#00FF00', stop: 25 }, + { color: '#0000FF', stop: 35 }, + ], + false + ); +}); + +test('stop input updates stops with error prevention (reset to bounds)', () => { + const colorStops = mount( + + ); + + findTestSubject(colorStops, 'euiColorStopThumb') + .first() + .simulate('click'); + const event = { target: { value: '1000' } }; + const inputs = colorStops.find('input[type="number"]'); + inputs.simulate('change', event); + expect(onChange).toBeCalled(); + expect(onChange).toBeCalledWith( + [ + { color: '#FF0000', stop: 100 }, + { color: '#00FF00', stop: 25 }, + { color: '#0000FF', stop: 35 }, + ], + false + ); +}); + +test('hex input updates stops', () => { + const colorStops = mount( + + ); + + findTestSubject(colorStops, 'euiColorStopThumb') + .first() + .simulate('click'); + const event = { target: { value: '#FFFFFF' } }; + const inputs = colorStops.find('input[type="text"]'); + expect(inputs.length).toBe(1); + inputs.simulate('change', event); + expect(onChange).toBeCalled(); + expect(onChange).toBeCalledWith( + [ + { color: '#FFFFFF', stop: 0 }, + { color: '#00FF00', stop: 25 }, + { color: '#0000FF', stop: 35 }, + ], + false + ); +}); + +test('hex input updates stops with error', () => { + const colorStops = mount( + + ); + + findTestSubject(colorStops, 'euiColorStopThumb') + .first() + .simulate('click'); + const event = { target: { value: '#FFFFF' } }; + const inputs = colorStops.find('input[type="text"]'); + inputs.simulate('change', event); + expect(onChange).toBeCalled(); + expect(onChange).toBeCalledWith( + [ + { color: '#FFFFF', stop: 0 }, + { color: '#00FF00', stop: 25 }, + { color: '#0000FF', stop: 35 }, + ], + true // isInvalid + ); +}); + +test('picker updates stops', () => { + const colorStops = mount( + + ); + + findTestSubject(colorStops, 'euiColorStopThumb') + .first() + .simulate('click'); + const swatches = colorStops.find('button.euiColorPicker__swatchSelect'); + expect(swatches.length).toBe(VISUALIZATION_COLORS.length); + swatches.first().simulate('click'); + expect(onChange).toBeCalled(); + expect(onChange).toBeCalledWith( + [ + { color: VISUALIZATION_COLORS[0], stop: 0 }, + { color: '#00FF00', stop: 25 }, + { color: '#0000FF', stop: 35 }, + ], + false + ); +}); + +test('thumb focus changes', () => { + const colorStops = mount( + + ); + + const wrapper = findTestSubject(colorStops, 'euiColorStops'); + const thumbs = findTestSubject(colorStops, 'euiColorStopThumb'); + wrapper.simulate('focus'); + wrapper.simulate('keydown', { + keyCode: keyCodes.DOWN, + }); + expect(thumbs.first().getDOMNode()).toEqual(document.activeElement); + thumbs.first().simulate('keydown', { + keyCode: keyCodes.DOWN, + }); + expect(thumbs.at(1).getDOMNode()).toEqual(document.activeElement); +}); + +test('thumb direction movement', () => { + const colorStops = mount( + + ); + + const wrapper = findTestSubject(colorStops, 'euiColorStops'); + const thumbs = findTestSubject(colorStops, 'euiColorStopThumb'); + wrapper.simulate('focus'); + wrapper.simulate('keydown', { + keyCode: keyCodes.DOWN, + }); + expect(thumbs.first().getDOMNode()).toEqual(document.activeElement); + thumbs.first().simulate('keydown', { + keyCode: keyCodes.RIGHT, + }); + expect(onChange).toBeCalledWith( + [ + { color: '#FF0000', stop: 1 }, + { color: '#00FF00', stop: 25 }, + { color: '#0000FF', stop: 35 }, + ], + false + ); + thumbs.first().simulate('keydown', { + keyCode: keyCodes.LEFT, + }); + expect(onChange).toBeCalledWith( + [ + { color: '#FF0000', stop: 0 }, + { color: '#00FF00', stop: 25 }, + { color: '#0000FF', stop: 35 }, + ], + false + ); +}); + +test('add new thumb via keyboard', () => { + const colorStops = mount( + + ); + + const wrapper = findTestSubject(colorStops, 'euiColorStops'); + wrapper.simulate('focus'); + wrapper.simulate('keydown', { + keyCode: keyCodes.ENTER, + }); + expect(onChange).toBeCalled(); + expect(onChange).toBeCalledWith( + [ + { color: '#FF0000', stop: 0 }, + { color: '#00FF00', stop: 25 }, + { color: '#0000FF', stop: 35 }, + { color: DEFAULT_VISUALIZATION_COLOR, stop: 45 }, + ], + false + ); +}); + +test('add new thumb via click', () => { + const colorStops = mount( + + ); + + const wrapper = findTestSubject(colorStops, 'euiColorStopsAdd'); + wrapper.simulate('click', { pageX: 45, pageY: 0 }); + expect(onChange).toBeCalled(); + // This is a very odd expecation. + // But we can't get actual page positions in this environment (no getBoundingClientRect) + // So we'll expect the _correct_ color and _incorrect_ stop value (NaN), + // with the `isInvalid` arg _correctly_ true as a result. + expect(onChange).toBeCalledWith( + [ + { color: '#FF0000', stop: 0 }, + { color: '#00FF00', stop: 25 }, + { color: '#0000FF', stop: 35 }, + { color: DEFAULT_VISUALIZATION_COLOR, stop: NaN }, + ], + true // isInvalid + ); +}); diff --git a/src/components/color_picker/color_stops/color_stops.tsx b/src/components/color_picker/color_stops/color_stops.tsx index b91ae164543..8a850beba90 100644 --- a/src/components/color_picker/color_stops/color_stops.tsx +++ b/src/components/color_picker/color_stops/color_stops.tsx @@ -282,6 +282,7 @@ export const EuiColorStops: FunctionComponent = ({ return ( = ({ compressed={compressed} />

Date: Tue, 1 Oct 2019 16:42:33 -0500 Subject: [PATCH 27/29] thumb snapshot tests --- .../color_stop_thumb.test.tsx.snap | 132 ++++++++++++++++++ .../color_stops/color_stop_thumb.test.tsx | 102 ++++++++++++++ 2 files changed, 234 insertions(+) create mode 100644 src/components/color_picker/color_stops/__snapshots__/color_stop_thumb.test.tsx.snap create mode 100644 src/components/color_picker/color_stops/color_stop_thumb.test.tsx 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 new file mode 100644 index 00000000000..0fc97c4c5d2 --- /dev/null +++ b/src/components/color_picker/color_stops/__snapshots__/color_stop_thumb.test.tsx.snap @@ -0,0 +1,132 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders EuiColorStopThumb 1`] = ` +
+
+
+
+`; + +exports[`renders disabled EuiColorStopThumb 1`] = ` +
+
+
+
+`; + +exports[`renders picker-only EuiColorStopThumb 1`] = ` +
+
+
+
+`; + +exports[`renders readOnly EuiColorStopThumb 1`] = ` +
+
+
+
+`; + +exports[`renders swatch-only EuiColorStopThumb 1`] = ` +
+
+
+
+`; diff --git a/src/components/color_picker/color_stops/color_stop_thumb.test.tsx b/src/components/color_picker/color_stops/color_stop_thumb.test.tsx new file mode 100644 index 00000000000..c91901fe892 --- /dev/null +++ b/src/components/color_picker/color_stops/color_stop_thumb.test.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { render } from 'enzyme'; + +import { EuiColorStopThumb } from './color_stop_thumb'; + +import { requiredProps } from '../../../test'; + +jest.mock('../../portal', () => ({ + // @ts-ignore + EuiPortal: ({ children }) => children, +})); + +const onChange = jest.fn(); + +// Note: Unit/interaction tests can be found in ./color_stops.test + +test('renders EuiColorStopThumb', () => { + const thumb = render( + + ); + expect(thumb).toMatchSnapshot(); +}); + +test('renders swatch-only EuiColorStopThumb', () => { + const thumb = render( + + ); + expect(thumb).toMatchSnapshot(); +}); + +test('renders picker-only EuiColorStopThumb', () => { + const thumb = render( + + ); + expect(thumb).toMatchSnapshot(); +}); + +test('renders disabled EuiColorStopThumb', () => { + const thumb = render( + + ); + expect(thumb).toMatchSnapshot(); +}); + +test('renders readOnly EuiColorStopThumb', () => { + const thumb = render( + + ); + expect(thumb).toMatchSnapshot(); +}); From 42ce4626c846a069ff4ba41bf0801ebf2873d5e8 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Tue, 1 Oct 2019 16:47:35 -0500 Subject: [PATCH 28/29] CL --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d68860295d..b46766cbf4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,11 @@ ## [`master`](https://github.com/elastic/eui/tree/master) - Update Elastic-Charts to version 13.0.0 and updated the theme object accordingly ([#2381](https://github.com/elastic/eui/pull/2381)) +- Added new `EuiColorStops` component ([#2360](https://github.com/elastic/eui/pull/2360)) **Bug fixes** -- Fix `EuiSelectable` to accept programmatic updates to its `options` prop ([#2390](https://github.com/elastic/eui/pull/2390)) +- Fix `EuiSelectable` to accept programmatic updates to its `options` prop ([#2390](https://github.com/elastic/eui/pull/2390)) ## [`14.4.0`](https://github.com/elastic/eui/tree/v14.4.0) From 6b94aa52ef4eddb61a90704b4a9d1d663043cd01 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Wed, 2 Oct 2019 10:24:22 -0500 Subject: [PATCH 29/29] use sorted array for add via keyboard location --- src/components/color_picker/color_stops/color_stops.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/color_picker/color_stops/color_stops.tsx b/src/components/color_picker/color_stops/color_stops.tsx index 8a850beba90..07b72517f5e 100644 --- a/src/components/color_picker/color_stops/color_stops.tsx +++ b/src/components/color_picker/color_stops/color_stops.tsx @@ -145,7 +145,13 @@ export const EuiColorStops: FunctionComponent = ({ }; const onAdd = () => { - const newColorStops = addStop(colorStops, addColor, max); + const stops = sortedStops.map(el => { + return { + color: el.color, + stop: el.stop, + }; + }); + const newColorStops = addStop(stops, addColor, max); setFocusStopOnUpdate(newColorStops[colorStops.length].stop); handleOnChange(newColorStops);