From 8ba1b8808de8a857859a92b0e89a6128756fe147 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Mon, 16 Sep 2019 10:28:43 -0500 Subject: [PATCH] `EuiColorPicker` `inline` and TS conversion (#2340) * inline version * convert euicolorpicker to ts * inline docs * clean up * add inline snapshot test * forwardRef generics * CL * use display enum instead of inline boolean --- CHANGELOG.md | 2 + .../color_picker/color_picker_example.js | 33 +++ src-docs/src/views/color_picker/inline.js | 30 ++ ...est.js.snap => color_picker.test.tsx.snap} | 183 +++++++++++++ ...r_picker.test.js => color_picker.test.tsx} | 19 +- .../{color_picker.js => color_picker.tsx} | 257 ++++++++++-------- .../color_picker/color_picker_swatch.tsx | 36 +-- src/components/color_picker/index.d.ts | 48 ---- .../color_picker/{index.js => index.ts} | 0 src/components/color_picker/saturation.tsx | 8 +- src/components/index.d.ts | 1 - 11 files changed, 428 insertions(+), 189 deletions(-) create mode 100644 src-docs/src/views/color_picker/inline.js rename src/components/color_picker/__snapshots__/{color_picker.test.js.snap => color_picker.test.tsx.snap} (77%) rename src/components/color_picker/{color_picker.test.js => color_picker.test.tsx} (96%) rename src/components/color_picker/{color_picker.js => color_picker.tsx} (63%) delete mode 100644 src/components/color_picker/index.d.ts rename src/components/color_picker/{index.js => index.ts} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 341d7ae0cbe..8c8ea41f742 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ - Created `EuiSuggest` component ([#2270](https://github.com/elastic/eui/pull/2270)) - Added missing `compressed` styling to `EuiSwitch` ([#2327](https://github.com/elastic/eui/pull/2327)) - Migrate `EuiBottomBar`, `EuiHealth` and `EuiImage` to TS ([#2328](https://github.com/elastic/eui/pull/2328)) +- Converted `EuiColorPicker` to TypeScript ([#2340](https://github.com/elastic/eui/pull/2340)) +- Added inline rendering option to `EuiColorPicker` ([#2340](https://github.com/elastic/eui/pull/2340)) ## [`14.0.0`](https://github.com/elastic/eui/tree/v14.0.0) 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 2a7ca1e90a3..25006528e42 100644 --- a/src-docs/src/views/color_picker/color_picker_example.js +++ b/src-docs/src/views/color_picker/color_picker_example.js @@ -84,6 +84,17 @@ const modesPickerSnippet = `// Gradient map only /> `; +import { Inline } from './inline'; +const inlineSource = require('!!raw-loader!./inline'); +const inlineHtml = renderToHtml(Inline); +const inlineSnippet = ` +`; + import Containers from './containers'; const containersSource = require('!!raw-loader!./containers'); const containersHtml = renderToHtml(Containers); @@ -215,6 +226,28 @@ export const ColorPickerExample = { snippet: [modesSwatchSnippet, modesPickerSnippet], demo: , }, + { + title: 'Inline', + source: [ + { + type: GuideSectionTypes.JS, + code: inlineSource, + }, + { + type: GuideSectionTypes.HTML, + code: inlineHtml, + }, + ], + text: ( +

+ 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. +

+ ), + snippet: inlineSnippet, + demo: , + }, { title: 'Containers', source: [ diff --git a/src-docs/src/views/color_picker/inline.js b/src-docs/src/views/color_picker/inline.js new file mode 100644 index 00000000000..3c7f16b1706 --- /dev/null +++ b/src-docs/src/views/color_picker/inline.js @@ -0,0 +1,30 @@ +import React, { Component } from 'react'; + +import { EuiColorPicker } from '../../../../src/components'; +import { isValidHex } from '../../../../src/services'; + +export class Inline extends Component { + constructor(props) { + super(props); + this.state = { + color: '', + }; + } + + handleChange = value => { + this.setState({ color: value }); + }; + + render() { + const hasErrors = !isValidHex(this.state.color) && this.state.color !== ''; + + return ( + + ); + } +} diff --git a/src/components/color_picker/__snapshots__/color_picker.test.js.snap b/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap similarity index 77% rename from src/components/color_picker/__snapshots__/color_picker.test.js.snap rename to src/components/color_picker/__snapshots__/color_picker.test.tsx.snap index 2c1ee677c44..06a2d8a4c2b 100644 --- a/src/components/color_picker/__snapshots__/color_picker.test.js.snap +++ b/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap @@ -517,6 +517,189 @@ exports[`renders fullWidth EuiColorPicker 1`] = ` `; +exports[`renders inline EuiColorPicker 1`] = ` +
+
+
+

+ Use the arrow keys to navigate the square color gradient. The coordinates resulting from each key press will be used to calculate HSV color mode 'saturation' and 'value' numbers, in the range of 0 to 1. Left and right decrease and increase (respectively) the 'saturation' value. Up and down decrease and increase (respectively) the 'value' value. +

+

+ #ffeedd +

+
+
+
+
+
+ +

+ #ffeedd +

+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + exports[`renders readOnly EuiColorPicker 1`] = `
({ + // @ts-ignore EuiPortal: ({ children }) => children, })); -let onChange; - -beforeEach(() => { - onChange = jest.fn(); -}); +const onChange = jest.fn(); test('renders EuiColorPicker', () => { const colorPicker = render( @@ -70,6 +67,18 @@ test('renders disabled EuiColorPicker', () => { expect(colorPicker).toMatchSnapshot(); }); +test('renders inline EuiColorPicker', () => { + const colorPicker = render( + + ); + expect(colorPicker).toMatchSnapshot(); +}); + test('renders EuiColorPicker with an empty swatch when color is null', () => { const colorPicker = render( diff --git a/src/components/color_picker/color_picker.js b/src/components/color_picker/color_picker.tsx similarity index 63% rename from src/components/color_picker/color_picker.js rename to src/components/color_picker/color_picker.tsx index a0f1d3d1d2e..d60163f471e 100644 --- a/src/components/color_picker/color_picker.js +++ b/src/components/color_picker/color_picker.tsx @@ -1,15 +1,28 @@ -import React, { cloneElement, useEffect, useRef, useState } from 'react'; -import PropTypes from 'prop-types'; +import React, { + FunctionComponent, + HTMLAttributes, + ReactChild, + ReactElement, + cloneElement, + useEffect, + useRef, + useState, +} from 'react'; import classNames from 'classnames'; +import { CommonProps, Omit } from '../common'; + import { EuiScreenReaderOnly } from '../accessibility'; import { EuiColorPickerSwatch } from './color_picker_swatch'; import { EuiFocusTrap } from '../focus_trap'; import { EuiFlexGroup, EuiFlexItem } from '../flex'; -import { EuiFieldText, EuiFormControlLayout } from '../form'; +// @ts-ignore +import { EuiFieldText } from '../form/field_text'; +import { EuiFormControlLayout } from '../form/form_control_layout'; import { EuiI18n } from '../i18n'; import { EuiPopover } from '../popover'; import { + HSV, VISUALIZATION_COLORS, keyCodes, hexToHsv, @@ -20,12 +33,70 @@ import { import { EuiHue } from './hue'; import { EuiSaturation } from './saturation'; -export const EuiColorPicker = ({ +type EuiColorPickerDisplay = 'default' | 'inline'; +type EuiColorPickerMode = 'default' | 'swatch' | 'picker'; + +interface HTMLDivElementOverrides { + /** + * Hex string (3 or 6 character). Empty string will register as 'transparent' + */ + color?: string | null; + onBlur?: () => void; + onChange: (hex: string) => void; + onFocus?: () => void; +} +export interface EuiColorPickerProps + extends CommonProps, + Omit, keyof HTMLDivElementOverrides>, + HTMLDivElementOverrides { + /** + * Custom element to use instead of text input + */ + button?: ReactElement; + /** + * Use the compressed style for EuiFieldText + */ + compressed?: boolean; + display?: EuiColorPickerDisplay; + disabled?: boolean; + fullWidth?: boolean; + id?: string; + /** + * 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. + */ + mode?: EuiColorPickerMode; + /** + * Custom z-index for the popover + */ + popoverZIndex?: number; + readOnly?: boolean; + /** + * Array of hex strings (3 or 6 character) to use as swatch options. Defaults to EUI visualization colors + */ + swatches?: string[]; +} + +function isKeyboardEvent( + event: React.MouseEvent | React.KeyboardEvent +): event is React.KeyboardEvent { + return typeof event === 'object' && 'keyCode' in event; +} + +export const EuiColorPicker: FunctionComponent = ({ button, className, color, compressed = false, disabled, + display = 'default', fullWidth = false, id, isInvalid, @@ -42,11 +113,11 @@ export const EuiColorPicker = ({ color ? hexToHsv(color) : hexToHsv('') ); const [lastHex, setLastHex] = useState(color); - const [inputRef, setInputRef] = useState(null); // Ideally this is uses `useRef`, but `EuiFieldText` isn't ready for that + const [inputRef, setInputRef] = useState(null); // Ideally this is uses `useRef`, but `EuiFieldText` isn't ready for that const [popoverShouldOwnFocus, setPopoverShouldOwnFocus] = useState(false); - const satruationRef = useRef(null); - const swatchRef = useRef(null); + const satruationRef = useRef(null); + const swatchRef = useRef(null); useEffect(() => { // Mimics `componentDidMount` and `componentDidUpdate` @@ -66,7 +137,7 @@ export const EuiColorPicker = ({ const testSubjAnchor = 'colorPickerAnchor'; const testSubjPopover = 'colorPickerPopover'; - const handleOnChange = hex => { + const handleOnChange = (hex: string) => { setLastHex(hex); onChange(hex); }; @@ -110,7 +181,7 @@ export const EuiColorPicker = ({ closeColorSelector(true); }; - const handleOnKeyDown = e => { + const handleOnKeyDown = (e: React.KeyboardEvent) => { if (e.keyCode === keyCodes.ENTER) { if (isColorSelectorShown) { handleFinalSelection(); @@ -120,16 +191,22 @@ export const EuiColorPicker = ({ } }; - const handleInputActivity = e => { - if (e.keyCode === keyCodes.ENTER) { - e.preventDefault(); - handleToggle(); - } else if (!e.keyCode) { + const handleInputActivity = ( + e: + | React.KeyboardEvent + | React.MouseEvent + ) => { + if (isKeyboardEvent(e)) { + if (e.keyCode === keyCodes.ENTER) { + e.preventDefault(); + handleToggle(); + } + } else { showColorSelector(); } }; - const handleToggleOnKeyDown = e => { + const handleToggleOnKeyDown = (e: React.KeyboardEvent) => { if (e.keyCode === keyCodes.DOWN) { e.preventDefault(); if (isColorSelectorShown) { @@ -143,14 +220,14 @@ export const EuiColorPicker = ({ } }; - const handleColorInput = e => { + const handleColorInput = (e: React.ChangeEvent) => { handleOnChange(e.target.value); if (isValidHex(e.target.value)) { setColorAsHsv(hexToHsv(e.target.value)); } }; - const handleColorSelection = color => { + const handleColorSelection = (color: HSV) => { const { h } = colorAsHsv; const hue = h ? h : 1; const newHsv = { ...color, h: hue }; @@ -158,7 +235,7 @@ export const EuiColorPicker = ({ setColorAsHsv(newHsv); }; - const handleHueSelection = hue => { + const handleHueSelection = (hue: number) => { const { s, v } = colorAsHsv; const satVal = s && v ? { s, v } : { s: 1, v: 1 }; const newHsv = { ...satVal, h: hue }; @@ -166,13 +243,58 @@ export const EuiColorPicker = ({ setColorAsHsv(newHsv); }; - const handleSwatchSelection = color => { + const handleSwatchSelection = (color: string) => { handleOnChange(color); setColorAsHsv(hexToHsv(color)); handleFinalSelection(); }; + const composite = ( + + {mode !== 'swatch' && ( +
+ + +
+ )} + {mode !== 'picker' && ( + + {swatches.map((swatch, index) => ( + + + {(swatchAriaLabel: string) => ( + handleSwatchSelection(swatch)} + aria-label={swatchAriaLabel} + role="option" + ref={index === 0 ? swatchRef : undefined} + /> + )} + + + ))} + + )} +
+ ); + let buttonOrInput; if (button) { buttonOrInput = cloneElement(button, { @@ -191,7 +313,7 @@ export const EuiColorPicker = ({ type: 'arrowDown', side: 'right', } - : null + : undefined } readOnly={readOnly} fullWidth={fullWidth} @@ -199,14 +321,14 @@ export const EuiColorPicker = ({ onKeyDown={handleToggleOnKeyDown}>
+ style={{ color: showColor && color ? color : undefined }}> - {([openLabel, closeLabel]) => ( + {([openLabel, closeLabel]: ReactChild[]) => ( {composite}
+ ) : (

- {mode !== 'swatch' && ( -
- - -
- )} - {mode !== 'picker' && ( - - {swatches.map((swatch, index) => ( - - - {swatchAriaLabel => ( - handleSwatchSelection(swatch)} - aria-label={swatchAriaLabel} - role="option" - ref={index === 0 ? swatchRef : undefined} - /> - )} - - - ))} - - )} + {composite}
); }; - -EuiColorPicker.propTypes = { - /** - * Custom element to use instead of text input - */ - button: PropTypes.node, - className: PropTypes.string, - /** - * Hex string (3 or 6 character). Empty string will register as 'transparent' - */ - color: PropTypes.string, - /** - * Use the compressed style for EuiFieldText - */ - compressed: PropTypes.bool, - disabled: PropTypes.bool, - id: PropTypes.string, - /** - * Custom validation flag - */ - isInvalid: PropTypes.bool, - /** - * Choose between swatches with gradient picker (default), swatches only, or gradient picker only. - */ - mode: PropTypes.oneOf(['default', 'swatch', 'picker']), - /** - * Function called when the popover closes - */ - onBlur: PropTypes.func, - /** - * (hex: string) => void - */ - onChange: PropTypes.func.isRequired, - /** - * Function called when the popover opens - */ - onFocus: PropTypes.func, - /** - * Array of hex strings (3 or 6 character) to use as swatch options. Defaults to EUI visualization colors - */ - swatches: PropTypes.arrayOf(PropTypes.string), - /** - * Custom z-index for the popover - */ - popoverZIndex: PropTypes.number, -}; diff --git a/src/components/color_picker/color_picker_swatch.tsx b/src/components/color_picker/color_picker_swatch.tsx index f67fb53ef84..9b53b8a3254 100644 --- a/src/components/color_picker/color_picker_swatch.tsx +++ b/src/components/color_picker/color_picker_swatch.tsx @@ -1,9 +1,4 @@ -import React, { - ButtonHTMLAttributes, - FunctionComponent, - Ref, - forwardRef, -} from 'react'; +import React, { ButtonHTMLAttributes, forwardRef } from 'react'; import classNames from 'classnames'; import { CommonProps, Omit } from '../common'; @@ -13,20 +8,19 @@ export type EuiColorPickerSwatchProps = CommonProps & color?: string; }; -export const EuiColorPickerSwatch: FunctionComponent< +export const EuiColorPickerSwatch = forwardRef< + HTMLButtonElement, EuiColorPickerSwatchProps -> = forwardRef( - ({ className, color, style, ...rest }, ref: Ref) => { - const classes = classNames('euiColorPickerSwatch', className); +>(({ className, color, style, ...rest }, ref) => { + const classes = classNames('euiColorPickerSwatch', className); - return ( -