-);
-
-ColorPalette.propTypes = {
- colors: PropTypes.array.isRequired,
- onChange: PropTypes.func.isRequired,
- value: PropTypes.string,
- colorsPerRow: PropTypes.number,
-};
diff --git a/x-pack/plugins/canvas/public/components/color_palette/color_palette.tsx b/x-pack/plugins/canvas/public/components/color_palette/color_palette.tsx
new file mode 100644
index 0000000000000..d4a2f01a58201
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/color_palette/color_palette.tsx
@@ -0,0 +1,80 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EuiIcon, EuiLink } from '@elastic/eui';
+import PropTypes from 'prop-types';
+import React, { SFC } from 'react';
+import tinycolor from 'tinycolor2';
+import { readableColor } from '../../lib/readable_color';
+import { ColorDot } from '../color_dot';
+import { ItemGrid } from '../item_grid';
+
+export interface Props {
+ /**
+ * An array of hexadecimal color values. Non-hex will be ignored.
+ * @default []
+ */
+ colors?: string[];
+ /**
+ * The number of colors to display before wrapping to a new row.
+ * @default 6
+ */
+ colorsPerRow?: number;
+ /** The function to call when the color is changed. */
+ onChange: (value: string) => void;
+ /**
+ * The value of the color in the selector. Should be hexadecimal. If it is not in the colors array, it will be ignored.
+ * @default ''
+ */
+ value?: string;
+}
+
+export const ColorPalette: SFC = ({
+ colors = [],
+ colorsPerRow = 6,
+ onChange,
+ value = '',
+}) => {
+ if (colors.length === 0) {
+ return null;
+ }
+
+ colors = colors.filter(color => {
+ const providedColor = tinycolor(color);
+ return providedColor.isValid() && providedColor.getFormat() === 'hex';
+ });
+
+ return (
+
+`;
+
+exports[`Storyshots components/ColorPicker six colors 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Storyshots components/ColorPicker six colors, value missing 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Storyshots components/ColorPicker three colors 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/x-pack/plugins/canvas/public/components/color_picker/__examples__/color_picker.examples.tsx b/x-pack/plugins/canvas/public/components/color_picker/__examples__/color_picker.examples.tsx
new file mode 100644
index 0000000000000..97209500f8ee4
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/color_picker/__examples__/color_picker.examples.tsx
@@ -0,0 +1,93 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { action } from '@storybook/addon-actions';
+import { storiesOf } from '@storybook/react';
+import React from 'react';
+import { ColorPicker } from '../color_picker';
+
+const THREE_COLORS = ['#fff', '#666', '#000'];
+const SIX_COLORS = ['#fff', '#666', '#000', '#abc', '#def', '#abcdef'];
+
+class Interactive extends React.Component<{}, { value: string; colors: string[] }> {
+ public state = {
+ value: '',
+ colors: SIX_COLORS,
+ };
+
+ public render() {
+ return (
+ this.setState({ colors: this.state.colors.concat(value) })}
+ onRemoveColor={value =>
+ this.setState({ colors: this.state.colors.filter(color => color !== value) })
+ }
+ onChange={value => this.setState({ value })}
+ value={this.state.value}
+ />
+ );
+ }
+}
+
+storiesOf('components/ColorPicker', module)
+ .addParameters({
+ info: {
+ inline: true,
+ styles: {
+ infoBody: {
+ margin: 20,
+ },
+ infoStory: {
+ margin: '40px 60px',
+ width: '320px',
+ },
+ },
+ },
+ })
+ .add('three colors', () => (
+
+ ))
+ .add('six colors', () => (
+
+ ))
+ .add('six colors, value missing', () => (
+
+ ))
+ .add('interactive', () => , {
+ info: {
+ inline: true,
+ source: false,
+ propTablesExclude: [Interactive],
+ styles: {
+ infoBody: {
+ margin: 20,
+ },
+ infoStory: {
+ margin: '40px 60px',
+ width: '320px',
+ },
+ },
+ },
+ });
diff --git a/x-pack/plugins/canvas/public/components/color_picker/color_picker.js b/x-pack/plugins/canvas/public/components/color_picker/color_picker.js
deleted file mode 100644
index 9415eb400abd8..0000000000000
--- a/x-pack/plugins/canvas/public/components/color_picker/color_picker.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React from 'react';
-import PropTypes from 'prop-types';
-import { ColorPalette } from '../color_palette';
-import { ColorManager } from '../color_manager';
-
-export const ColorPicker = ({ onChange, value, colors, addColor, removeColor }) => {
- return (
-
-
-
-
- );
-};
-
-ColorPicker.propTypes = {
- value: PropTypes.string,
- onChange: PropTypes.func,
- colors: PropTypes.array,
- addColor: PropTypes.func,
- removeColor: PropTypes.func,
-};
diff --git a/x-pack/plugins/canvas/public/components/color_picker/color_picker.tsx b/x-pack/plugins/canvas/public/components/color_picker/color_picker.tsx
new file mode 100644
index 0000000000000..111b858ece8a0
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/color_picker/color_picker.tsx
@@ -0,0 +1,75 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import PropTypes from 'prop-types';
+import React, { SFC } from 'react';
+import tinycolor from 'tinycolor2';
+import { ColorManager } from '../color_manager';
+import { ColorPalette } from '../color_palette';
+
+export interface Props {
+ /**
+ * An array of hexadecimal color values. Non-hex will be ignored.
+ * @default []
+ */
+ colors?: string[];
+ /** The function to call when the Add Color button is clicked. The button will not appear if there is no handler. */
+ onAddColor?: (value: string) => void;
+ /** The function to call when the color is changed. */
+ onChange: (value: string) => void;
+ /** The function to call when the Remove Color button is clicked. The button will not appear if there is no handler. */
+ onRemoveColor?: (value: string) => void;
+ /**
+ * The value of the color in the selector. Should be hexadecimal. If it is not in the colors array, it will be ignored.
+ * @default ''
+ */
+ value?: string;
+}
+
+export const ColorPicker: SFC = ({
+ colors = [],
+ value = '',
+ onAddColor,
+ onChange,
+ onRemoveColor,
+}) => {
+ const tc = tinycolor(value);
+ const isValidColor = tc.isValid() && tc.getFormat() === 'hex';
+
+ colors = colors.filter(color => {
+ const providedColor = tinycolor(color);
+ return providedColor.isValid() && providedColor.getFormat() === 'hex';
+ });
+
+ let canRemove = false;
+ let canAdd = false;
+
+ if (isValidColor) {
+ const match = colors.filter(color => tinycolor.equals(value, color));
+ canRemove = match.length > 0;
+ canAdd = match.length === 0;
+ }
+
+ return (
+
+
+
+
+ );
+};
+
+ColorPicker.propTypes = {
+ colors: PropTypes.array,
+ onAddColor: PropTypes.func,
+ onChange: PropTypes.func.isRequired,
+ onRemoveColor: PropTypes.func,
+ value: PropTypes.string,
+};
diff --git a/x-pack/plugins/canvas/public/components/color_picker/index.js b/x-pack/plugins/canvas/public/components/color_picker/index.ts
similarity index 100%
rename from x-pack/plugins/canvas/public/components/color_picker/index.js
rename to x-pack/plugins/canvas/public/components/color_picker/index.ts
diff --git a/x-pack/plugins/canvas/public/components/item_grid/__examples__/__snapshots__/item_grid.examples.storyshot b/x-pack/plugins/canvas/public/components/item_grid/__examples__/__snapshots__/item_grid.examples.storyshot
new file mode 100644
index 0000000000000..f2205d74033ab
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/item_grid/__examples__/__snapshots__/item_grid.examples.storyshot
@@ -0,0 +1,227 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Storyshots components/ItemGrid color dot grid 1`] = `
+
+`;
diff --git a/x-pack/plugins/canvas/public/components/item_grid/__examples__/item_grid.examples.tsx b/x-pack/plugins/canvas/public/components/item_grid/__examples__/item_grid.examples.tsx
new file mode 100644
index 0000000000000..632e238d1868b
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/item_grid/__examples__/item_grid.examples.tsx
@@ -0,0 +1,44 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { EuiIcon, IconType } from '@elastic/eui';
+import { storiesOf } from '@storybook/react';
+import React from 'react';
+import { readableColor } from '../../../lib/readable_color';
+import { ColorDot } from '../../color_dot';
+import { ItemGrid } from '../item_grid';
+
+storiesOf('components/ItemGrid', module)
+ .add('simple grid', () => (
+
{item}
} />
+ ))
+ .add('icon grid', () => (
+ }
+ />
+ ))
+ .add('color dot grid', () => (
+
+ {item => }
+
+ ))
+ .add('complex grid', () => (
+
+ }
+ >
+ {item => (
+
+
+
+ )}
+
+ ));
diff --git a/x-pack/plugins/canvas/public/components/color_manager/index.js b/x-pack/plugins/canvas/public/components/item_grid/index.ts
similarity index 55%
rename from x-pack/plugins/canvas/public/components/color_manager/index.js
rename to x-pack/plugins/canvas/public/components/item_grid/index.ts
index 119859ad0cddd..9a5ef08b8171e 100644
--- a/x-pack/plugins/canvas/public/components/color_manager/index.js
+++ b/x-pack/plugins/canvas/public/components/item_grid/index.ts
@@ -4,8 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { compose, withState } from 'recompose';
+import { pure } from 'recompose';
+import { ItemGrid as Component, Props as ComponentProps } from './item_grid';
-import { ColorManager as Component } from './color_manager';
-
-export const ColorManager = compose(withState('adding', 'setAdding', false))(Component);
+export const ItemGrid = pure>(Component);
diff --git a/x-pack/plugins/canvas/public/components/item_grid/item_grid.js b/x-pack/plugins/canvas/public/components/item_grid/item_grid.js
deleted file mode 100644
index 156d348f38dcd..0000000000000
--- a/x-pack/plugins/canvas/public/components/item_grid/item_grid.js
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React from 'react';
-import PropTypes from 'prop-types';
-import { last } from 'lodash';
-
-const defaultPerRow = 6;
-
-export const ItemGrid = ({ items, itemsPerRow, children }) => {
- if (!items) {
- return null;
- }
-
- const rows = items.reduce(
- (rows, item) => {
- if (last(rows).length >= (itemsPerRow || defaultPerRow)) {
- rows.push([]);
- }
-
- last(rows).push(children({ item }));
-
- return rows;
- },
- [[]]
- );
-
- return rows.map((row, i) => (
-
- {row}
-
- ));
-};
-
-ItemGrid.propTypes = {
- items: PropTypes.array.isRequired,
- itemsPerRow: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
- children: PropTypes.func.isRequired,
-};
diff --git a/x-pack/plugins/canvas/public/components/item_grid/item_grid.tsx b/x-pack/plugins/canvas/public/components/item_grid/item_grid.tsx
new file mode 100644
index 0000000000000..234f505071669
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/item_grid/item_grid.tsx
@@ -0,0 +1,65 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { last } from 'lodash';
+import PropTypes from 'prop-types';
+import React, { Fragment, ReactElement, ValidationMap } from 'react';
+
+const PER_ROW_DEFAULT = 6;
+
+export interface Props {
+ /** A collection of 'things' to be iterated upon by the children prop function. */
+ items: T[];
+ /**
+ * The number of items per row.
+ * @default 6
+ */
+ itemsPerRow?: number;
+ /** A function with which to iterate upon the items collection, producing nodes. */
+ children: (item: T) => ReactElement;
+}
+
+// We need this type in order to define propTypes on the object. It's a bit redundant,
+// but TS needs to know that ItemGrid can have propTypes defined on it.
+interface ItemGridType {
+ (props: Props): ReactElement;
+ propTypes?: ValidationMap>;
+}
+
+export const ItemGrid: ItemGridType = function ItemGridFunc({
+ items = [],
+ itemsPerRow = PER_ROW_DEFAULT,
+ children,
+}: Props) {
+ const reducedRows = items.reduce(
+ (rows: Array>>, item: any) => {
+ if (last(rows).length >= itemsPerRow) {
+ rows.push([]);
+ }
+
+ last(rows).push(children(item));
+
+ return rows;
+ },
+ [[]] as Array>>
+ );
+
+ return (
+
+ {reducedRows.map((row, i) => (
+
+ {row}
+
+ ))}
+
+ );
+};
+
+ItemGrid.propTypes = {
+ items: PropTypes.array,
+ itemsPerRow: PropTypes.number,
+ children: PropTypes.func.isRequired,
+};
diff --git a/x-pack/plugins/canvas/public/lib/__tests__/readable_color.test.ts b/x-pack/plugins/canvas/public/lib/__tests__/readable_color.test.ts
new file mode 100644
index 0000000000000..bd79655ca727b
--- /dev/null
+++ b/x-pack/plugins/canvas/public/lib/__tests__/readable_color.test.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { readableColor } from '../readable_color';
+
+describe('readableColor', () => {
+ test('light', () => {
+ expect(readableColor('#000')).toEqual('#FFF');
+ expect(readableColor('#000', '#EEE', '#111')).toEqual('#EEE');
+ expect(readableColor('#111')).toEqual('#FFF');
+ expect(readableColor('#111', '#EEE', '#111')).toEqual('#EEE');
+ });
+ test('dark', () => {
+ expect(readableColor('#FFF')).toEqual('#333');
+ expect(readableColor('#FFF', '#EEE', '#111')).toEqual('#111');
+ expect(readableColor('#EEE')).toEqual('#333');
+ expect(readableColor('#EEE', '#EEE', '#111')).toEqual('#111');
+ });
+});
diff --git a/x-pack/plugins/canvas/public/lib/readable_color.js b/x-pack/plugins/canvas/public/lib/readable_color.ts
similarity index 78%
rename from x-pack/plugins/canvas/public/lib/readable_color.js
rename to x-pack/plugins/canvas/public/lib/readable_color.ts
index 0dd7e2dec1c55..bad68e1b1a0bf 100644
--- a/x-pack/plugins/canvas/public/lib/readable_color.js
+++ b/x-pack/plugins/canvas/public/lib/readable_color.ts
@@ -6,9 +6,7 @@
import chroma from 'chroma-js';
-export function readableColor(background, light, dark) {
- light = light || '#FFF';
- dark = dark || '#333';
+export function readableColor(background: string, light: string = '#FFF', dark: string = '#333') {
try {
return chroma.contrast(background, '#000') < 7 ? light : dark;
} catch (e) {
diff --git a/yarn.lock b/yarn.lock
index 9ffdc68f25389..bd044040d7db9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1877,6 +1877,11 @@
resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.10.tgz#780d552467824be4a241b29510a7873a7432c4a6"
integrity sha512-fOM/Jhv51iyugY7KOBZz2ThfT1gwvsGCfWxpLpZDgkGjpEO4Le9cld07OdskikLjDUQJ43dzDaVRSFwQlpdqVg==
+"@types/chroma-js@^1.4.1":
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-1.4.1.tgz#7c52d461173d569ba1f27e0c2dd26ee76691ec82"
+ integrity sha512-i9hUiO3bwgmzZUDwBuR65WqsBQ/nwN+H2fKX0bykXCdd8cFQEuIj8vI7FXjyb2f5z5h+pv76I/uakikKSgaqTA==
+
"@types/chromedriver@^2.38.0":
version "2.38.0"
resolved "https://registry.yarnpkg.com/@types/chromedriver/-/chromedriver-2.38.0.tgz#971032b73eb7f44036f4f5bed59a7fd5b468014f"
@@ -2480,6 +2485,13 @@
dependencies:
"@types/react" "*"
+"@types/recompose@^0.30.5":
+ version "0.30.5"
+ resolved "https://registry.yarnpkg.com/@types/recompose/-/recompose-0.30.5.tgz#09890e3c504546b38193479e610e427ac0888393"
+ integrity sha512-PEQvFmudB9n0+ZvD8l7lh0olGAWmVAuVwCM4eotzWouH8/Kcr8/EcZyLhYILqoTlqzi6ey/3kbKQzJ/h3KkyXw==
+ dependencies:
+ "@types/react" "*"
+
"@types/reduce-reducers@^0.1.3":
version "0.1.3"
resolved "https://registry.yarnpkg.com/@types/reduce-reducers/-/reduce-reducers-0.1.3.tgz#69f252207622ced7e063c7526ad46ec60b69f0c0"
@@ -2587,6 +2599,11 @@
resolved "https://registry.yarnpkg.com/@types/tempy/-/tempy-0.1.0.tgz#8ba0339dcd5abb554f301683dc3396d153ec5bfd"
integrity sha512-2qeSxI2bMucW58Jsj8jrBXZxobtcKkvO44AvJzKGaD8+m/3KRuBqeKitJ5U6sqy3a9tFsqhzsxMkqR4Wcl6AmQ==
+"@types/tinycolor2@^1.4.1":
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.1.tgz#2f5670c9d1d6e558897a810ed284b44918fc1253"
+ integrity sha512-25L/RL5tqZkquKXVHM1fM2bd23qjfbcPpAZ2N/H05Y45g3UEi+Hw8CbDV28shKY8gH1SHiLpZSxPI1lacqdpGg==
+
"@types/type-detect@^4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@types/type-detect/-/type-detect-4.0.1.tgz#3b0f5ac82ea630090cbf57c57a1bf5a63a29b9b6"