Skip to content

Commit

Permalink
[XY axis] Integrates legend color picker with the eui palette (#90589)
Browse files Browse the repository at this point in the history
* XY Axis, integrate legend color picker with the eui palette

* Fix functional test to work with the eui palette

* Order eui colors by group

* Add unit test for use color picker

* Add useMemo to getColorPicker

* Remove the grey background from the first focused circle

* Fix bug caused by comparing lowercase with uppercase characters

* Fix bug on complimentary palette

* Fix CI

* fix linter

* Use uppercase for hex color

* Use eui variable instead

* Changes on charts.json

* Make the color picker accessible

* Fix ci and tests

* Allow keyboard navigation

* Close the popover on mouse click event

* Fix ci

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
stratoula and kibanamachine authored Mar 16, 2021
1 parent 0e1a2cd commit a1f15fb
Show file tree
Hide file tree
Showing 15 changed files with 326 additions and 166 deletions.
8 changes: 4 additions & 4 deletions api_docs/charts.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,26 @@
"children": [
{
"type": "Object",
"label": "{ onChange, color: selectedColor, id, label }",
"label": "{\n onChange,\n color: selectedColor,\n label,\n useLegacyColors = true,\n colorIsOverwritten = true,\n}",
"isRequired": true,
"signature": [
"ColorPickerProps"
],
"description": [],
"source": {
"path": "src/plugins/charts/public/static/components/color_picker.tsx",
"lineNumber": 83
"lineNumber": 108
}
}
],
"signature": [
"({ onChange, color: selectedColor, id, label }: ColorPickerProps) => JSX.Element"
"({ onChange, color: selectedColor, label, useLegacyColors, colorIsOverwritten, }: ColorPickerProps) => JSX.Element"
],
"description": [],
"label": "ColorPicker",
"source": {
"path": "src/plugins/charts/public/static/components/color_picker.tsx",
"lineNumber": 83
"lineNumber": 108
},
"tags": [],
"returnComment": [],
Expand Down
12 changes: 12 additions & 0 deletions src/plugins/charts/public/static/components/color_picker.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ $visColorPickerWidth: $euiSizeL * 8; // 8 columns
width: $visColorPickerWidth;
}

.visColorPicker__colorBtn {
position: relative;

input[type='radio'] {
position: absolute;
top: 50%;
left: 50%;
opacity: 0;
transform: translate(-50%, -50%);
}
}

.visColorPicker__valueDot {
cursor: pointer;

Expand Down
138 changes: 91 additions & 47 deletions src/plugins/charts/public/static/components/color_picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,19 @@
import classNames from 'classnames';
import React, { BaseSyntheticEvent } from 'react';

import { EuiButtonEmpty, EuiFlexItem, EuiIcon } from '@elastic/eui';
import {
EuiButtonEmpty,
EuiFlexItem,
EuiIcon,
euiPaletteColorBlind,
EuiScreenReaderOnly,
EuiFlexGroup,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';

import './color_picker.scss';

export const legendColors: string[] = [
export const legacyColors: string[] = [
'#3F6833',
'#967302',
'#2F575E',
Expand Down Expand Up @@ -74,54 +81,91 @@ export const legendColors: string[] = [
];

interface ColorPickerProps {
id?: string;
/**
* Label that characterizes the color that is going to change
*/
label: string | number | null;
/**
* Callback on the color change
*/
onChange: (color: string | null, event: BaseSyntheticEvent) => void;
/**
* Initial color.
*/
color: string;
/**
* Defines if the compatibility (legacy) or eui palette is going to be used. Defauls to true.
*/
useLegacyColors?: boolean;
/**
* Defines if the default color is overwritten. Defaults to true.
*/
colorIsOverwritten?: boolean;
/**
* Callback for onKeyPress event
*/
onKeyDown?: (e: React.KeyboardEvent<HTMLElement>) => void;
}
const euiColors = euiPaletteColorBlind({ rotations: 4, order: 'group' });

export const ColorPicker = ({ onChange, color: selectedColor, id, label }: ColorPickerProps) => (
<div className="visColorPicker">
<span id={`${id}ColorPickerDesc`} className="euiScreenReaderOnly">
<FormattedMessage
id="charts.colorPicker.setColor.screenReaderDescription"
defaultMessage="Set color for value {legendDataLabel}"
values={{ legendDataLabel: label }}
/>
</span>
<div className="visColorPicker__value" role="listbox">
{legendColors.map((color) => (
<EuiIcon
role="option"
tabIndex={0}
type="dot"
size="l"
color={selectedColor}
key={color}
aria-label={color}
aria-describedby={`${id}ColorPickerDesc`}
aria-selected={color === selectedColor}
onClick={(e) => onChange(color, e)}
onKeyPress={(e) => onChange(color, e)}
className={classNames('visColorPicker__valueDot', {
// eslint-disable-next-line @typescript-eslint/naming-convention
'visColorPicker__valueDot-isSelected': color === selectedColor,
})}
style={{ color }}
data-test-subj={`visColorPickerColor-${color}`}
/>
))}
export const ColorPicker = ({
onChange,
color: selectedColor,
label,
useLegacyColors = true,
colorIsOverwritten = true,
onKeyDown,
}: ColorPickerProps) => {
const legendColors = useLegacyColors ? legacyColors : euiColors;

return (
<div className="visColorPicker">
<fieldset>
<EuiScreenReaderOnly>
<legend>
<FormattedMessage
id="charts.colorPicker.setColor.screenReaderDescription"
defaultMessage="Set color for value {legendDataLabel}"
values={{ legendDataLabel: label }}
/>
</legend>
</EuiScreenReaderOnly>
<EuiFlexGroup wrap={true} gutterSize="none" className="visColorPicker__value">
{legendColors.map((color) => (
<label key={color} className="visColorPicker__colorBtn">
<input
type="radio"
onChange={(e) => onChange(color, e)}
value={selectedColor}
name="visColorPicker__radio"
checked={color === selectedColor}
onKeyDown={onKeyDown}
/>
<EuiIcon
type="dot"
size="l"
color={selectedColor}
className={classNames('visColorPicker__valueDot', {
// eslint-disable-next-line @typescript-eslint/naming-convention
'visColorPicker__valueDot-isSelected': color === selectedColor,
})}
style={{ color }}
data-test-subj={`visColorPickerColor-${color}`}
/>
<EuiScreenReaderOnly>
<span>{color}</span>
</EuiScreenReaderOnly>
</label>
))}
</EuiFlexGroup>
</fieldset>
{legendColors.some((c) => c === selectedColor) && colorIsOverwritten && (
<EuiFlexItem grow={false}>
<EuiButtonEmpty size="s" onClick={(e: any) => onChange(null, e)}>
<FormattedMessage id="charts.colorPicker.clearColor" defaultMessage="Reset color" />
</EuiButtonEmpty>
</EuiFlexItem>
)}
</div>
{legendColors.some((c) => c === selectedColor) && (
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="s"
onClick={(e: any) => onChange(null, e)}
onKeyPress={(e: any) => onChange(null, e)}
>
<FormattedMessage id="charts.colorPicker.clearColor" defaultMessage="Clear color" />
</EuiButtonEmpty>
</EuiFlexItem>
)}
</div>
);
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,8 @@ describe('VisLegend Component', () => {
first.simulate('click');

const popover = wrapper.find('.visColorPicker').first();
const firstColor = popover.find('.visColorPicker__valueDot').first();
firstColor.simulate('click');
const firstColor = popover.find('.visColorPicker__colorBtn input').first();
firstColor.simulate('change');

const colors = mockState.get('vis.colors');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,6 @@ export class VisLegend extends PureComponent<VisLegendProps, VisLegendState> {
canFilter={this.state.filterableLabels.has(item.label)}
onFilter={this.filter}
onSelect={this.toggleDetails}
legendId={this.legendId}
setColor={this.setColor}
getColor={this.getColor}
onHighlight={this.highlight}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import { ColorPicker } from '../../../../../charts/public';

interface Props {
item: LegendItem;
legendId: string;
selected: boolean;
canFilter: boolean;
anchorPosition: EuiPopoverProps['anchorPosition'];
Expand All @@ -39,7 +38,6 @@ interface Props {

const VisLegendItemComponent = ({
item,
legendId,
selected,
canFilter,
anchorPosition,
Expand Down Expand Up @@ -150,7 +148,6 @@ const VisLegendItemComponent = ({
{canFilter && renderFilterBar()}

<ColorPicker
id={legendId}
label={item.label}
color={getColor(item.label)}
onChange={(c, e) => setColor(item.label, c, e)}
Expand Down
99 changes: 99 additions & 0 deletions src/plugins/vis_type_xy/public/utils/get_color_picker.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import React from 'react';
import { LegendColorPickerProps, XYChartSeriesIdentifier } from '@elastic/charts';
import { EuiPopover } from '@elastic/eui';
import { mountWithIntl } from '@kbn/test/jest';
import { ComponentType, ReactWrapper } from 'enzyme';
import { getColorPicker } from './get_color_picker';
import { ColorPicker } from '../../../charts/public';
import type { PersistedState } from '../../../visualizations/public';

jest.mock('@elastic/charts', () => {
const original = jest.requireActual('@elastic/charts');

return {
...original,
getSpecId: jest.fn(() => {}),
};
});

describe('getColorPicker', function () {
const mockState = new Map();
const uiState = ({
get: jest
.fn()
.mockImplementation((key, fallback) => (mockState.has(key) ? mockState.get(key) : fallback)),
set: jest.fn().mockImplementation((key, value) => mockState.set(key, value)),
emit: jest.fn(),
setSilent: jest.fn(),
} as unknown) as PersistedState;

let wrapperProps: LegendColorPickerProps;
const Component: ComponentType<LegendColorPickerProps> = getColorPicker(
'left',
jest.fn(),
jest.fn().mockImplementation((seriesIdentifier) => seriesIdentifier.seriesKeys[0]),
'default',
uiState
);
let wrapper: ReactWrapper<LegendColorPickerProps>;

beforeAll(() => {
wrapperProps = {
color: 'rgb(109, 204, 177)',
onClose: jest.fn(),
onChange: jest.fn(),
anchor: document.createElement('div'),
seriesIdentifiers: [
{
yAccessor: 'col-2-1',
splitAccessors: {},
seriesKeys: ['Logstash Airways', 'col-2-1'],
specId: 'histogram-col-2-1',
key:
'groupId{__pseudo_stacked_group-ValueAxis-1__}spec{histogram-col-2-1}yAccessor{col-2-1}splitAccessors{col-1-3-Logstash Airways}',
} as XYChartSeriesIdentifier,
],
};
});

it('renders the color picker', () => {
wrapper = mountWithIntl(<Component {...wrapperProps} />);
expect(wrapper.find(ColorPicker).length).toBe(1);
});

it('renders the color picker with the colorIsOverwritten prop set to false if color is not overwritten for the specific series', () => {
wrapper = mountWithIntl(<Component {...wrapperProps} />);
expect(wrapper.find(ColorPicker).prop('colorIsOverwritten')).toBe(false);
});

it('renders the color picker with the colorIsOverwritten prop set to true if color is overwritten for the specific series', () => {
uiState.set('vis.colors', { 'Logstash Airways': '#6092c0' });
wrapper = mountWithIntl(<Component {...wrapperProps} />);
expect(wrapper.find(ColorPicker).prop('colorIsOverwritten')).toBe(true);
});

it('renders the picker on the correct position', () => {
wrapper = mountWithIntl(<Component {...wrapperProps} />);
expect(wrapper.find(EuiPopover).prop('anchorPosition')).toEqual('rightCenter');
});

it('renders the picker for kibana palette with useLegacyColors set to true', () => {
const LegacyPaletteComponent: ComponentType<LegendColorPickerProps> = getColorPicker(
'left',
jest.fn(),
jest.fn(),
'kibana_palette',
uiState
);
wrapper = mountWithIntl(<LegacyPaletteComponent {...wrapperProps} />);
expect(wrapper.find(ColorPicker).prop('useLegacyColors')).toBe(true);
});
});
Loading

0 comments on commit a1f15fb

Please sign in to comment.