Skip to content

Commit a1f15fb

Browse files
[XY axis] Integrates legend color picker with the eui palette (#90589)
* 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 <42973632+kibanamachine@users.noreply.github.com>
1 parent 0e1a2cd commit a1f15fb

File tree

15 files changed

+326
-166
lines changed

15 files changed

+326
-166
lines changed

api_docs/charts.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,26 @@
99
"children": [
1010
{
1111
"type": "Object",
12-
"label": "{ onChange, color: selectedColor, id, label }",
12+
"label": "{\n onChange,\n color: selectedColor,\n label,\n useLegacyColors = true,\n colorIsOverwritten = true,\n}",
1313
"isRequired": true,
1414
"signature": [
1515
"ColorPickerProps"
1616
],
1717
"description": [],
1818
"source": {
1919
"path": "src/plugins/charts/public/static/components/color_picker.tsx",
20-
"lineNumber": 83
20+
"lineNumber": 108
2121
}
2222
}
2323
],
2424
"signature": [
25-
"({ onChange, color: selectedColor, id, label }: ColorPickerProps) => JSX.Element"
25+
"({ onChange, color: selectedColor, label, useLegacyColors, colorIsOverwritten, }: ColorPickerProps) => JSX.Element"
2626
],
2727
"description": [],
2828
"label": "ColorPicker",
2929
"source": {
3030
"path": "src/plugins/charts/public/static/components/color_picker.tsx",
31-
"lineNumber": 83
31+
"lineNumber": 108
3232
},
3333
"tags": [],
3434
"returnComment": [],

src/plugins/charts/public/static/components/color_picker.scss

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ $visColorPickerWidth: $euiSizeL * 8; // 8 columns
44
width: $visColorPickerWidth;
55
}
66

7+
.visColorPicker__colorBtn {
8+
position: relative;
9+
10+
input[type='radio'] {
11+
position: absolute;
12+
top: 50%;
13+
left: 50%;
14+
opacity: 0;
15+
transform: translate(-50%, -50%);
16+
}
17+
}
18+
719
.visColorPicker__valueDot {
820
cursor: pointer;
921

src/plugins/charts/public/static/components/color_picker.tsx

Lines changed: 91 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,19 @@
99
import classNames from 'classnames';
1010
import React, { BaseSyntheticEvent } from 'react';
1111

12-
import { EuiButtonEmpty, EuiFlexItem, EuiIcon } from '@elastic/eui';
12+
import {
13+
EuiButtonEmpty,
14+
EuiFlexItem,
15+
EuiIcon,
16+
euiPaletteColorBlind,
17+
EuiScreenReaderOnly,
18+
EuiFlexGroup,
19+
} from '@elastic/eui';
1320
import { FormattedMessage } from '@kbn/i18n/react';
1421

1522
import './color_picker.scss';
1623

17-
export const legendColors: string[] = [
24+
export const legacyColors: string[] = [
1825
'#3F6833',
1926
'#967302',
2027
'#2F575E',
@@ -74,54 +81,91 @@ export const legendColors: string[] = [
7481
];
7582

7683
interface ColorPickerProps {
77-
id?: string;
84+
/**
85+
* Label that characterizes the color that is going to change
86+
*/
7887
label: string | number | null;
88+
/**
89+
* Callback on the color change
90+
*/
7991
onChange: (color: string | null, event: BaseSyntheticEvent) => void;
92+
/**
93+
* Initial color.
94+
*/
8095
color: string;
96+
/**
97+
* Defines if the compatibility (legacy) or eui palette is going to be used. Defauls to true.
98+
*/
99+
useLegacyColors?: boolean;
100+
/**
101+
* Defines if the default color is overwritten. Defaults to true.
102+
*/
103+
colorIsOverwritten?: boolean;
104+
/**
105+
* Callback for onKeyPress event
106+
*/
107+
onKeyDown?: (e: React.KeyboardEvent<HTMLElement>) => void;
81108
}
109+
const euiColors = euiPaletteColorBlind({ rotations: 4, order: 'group' });
82110

83-
export const ColorPicker = ({ onChange, color: selectedColor, id, label }: ColorPickerProps) => (
84-
<div className="visColorPicker">
85-
<span id={`${id}ColorPickerDesc`} className="euiScreenReaderOnly">
86-
<FormattedMessage
87-
id="charts.colorPicker.setColor.screenReaderDescription"
88-
defaultMessage="Set color for value {legendDataLabel}"
89-
values={{ legendDataLabel: label }}
90-
/>
91-
</span>
92-
<div className="visColorPicker__value" role="listbox">
93-
{legendColors.map((color) => (
94-
<EuiIcon
95-
role="option"
96-
tabIndex={0}
97-
type="dot"
98-
size="l"
99-
color={selectedColor}
100-
key={color}
101-
aria-label={color}
102-
aria-describedby={`${id}ColorPickerDesc`}
103-
aria-selected={color === selectedColor}
104-
onClick={(e) => onChange(color, e)}
105-
onKeyPress={(e) => onChange(color, e)}
106-
className={classNames('visColorPicker__valueDot', {
107-
// eslint-disable-next-line @typescript-eslint/naming-convention
108-
'visColorPicker__valueDot-isSelected': color === selectedColor,
109-
})}
110-
style={{ color }}
111-
data-test-subj={`visColorPickerColor-${color}`}
112-
/>
113-
))}
111+
export const ColorPicker = ({
112+
onChange,
113+
color: selectedColor,
114+
label,
115+
useLegacyColors = true,
116+
colorIsOverwritten = true,
117+
onKeyDown,
118+
}: ColorPickerProps) => {
119+
const legendColors = useLegacyColors ? legacyColors : euiColors;
120+
121+
return (
122+
<div className="visColorPicker">
123+
<fieldset>
124+
<EuiScreenReaderOnly>
125+
<legend>
126+
<FormattedMessage
127+
id="charts.colorPicker.setColor.screenReaderDescription"
128+
defaultMessage="Set color for value {legendDataLabel}"
129+
values={{ legendDataLabel: label }}
130+
/>
131+
</legend>
132+
</EuiScreenReaderOnly>
133+
<EuiFlexGroup wrap={true} gutterSize="none" className="visColorPicker__value">
134+
{legendColors.map((color) => (
135+
<label key={color} className="visColorPicker__colorBtn">
136+
<input
137+
type="radio"
138+
onChange={(e) => onChange(color, e)}
139+
value={selectedColor}
140+
name="visColorPicker__radio"
141+
checked={color === selectedColor}
142+
onKeyDown={onKeyDown}
143+
/>
144+
<EuiIcon
145+
type="dot"
146+
size="l"
147+
color={selectedColor}
148+
className={classNames('visColorPicker__valueDot', {
149+
// eslint-disable-next-line @typescript-eslint/naming-convention
150+
'visColorPicker__valueDot-isSelected': color === selectedColor,
151+
})}
152+
style={{ color }}
153+
data-test-subj={`visColorPickerColor-${color}`}
154+
/>
155+
<EuiScreenReaderOnly>
156+
<span>{color}</span>
157+
</EuiScreenReaderOnly>
158+
</label>
159+
))}
160+
</EuiFlexGroup>
161+
</fieldset>
162+
{legendColors.some((c) => c === selectedColor) && colorIsOverwritten && (
163+
<EuiFlexItem grow={false}>
164+
<EuiButtonEmpty size="s" onClick={(e: any) => onChange(null, e)}>
165+
<FormattedMessage id="charts.colorPicker.clearColor" defaultMessage="Reset color" />
166+
</EuiButtonEmpty>
167+
</EuiFlexItem>
168+
)}
114169
</div>
115-
{legendColors.some((c) => c === selectedColor) && (
116-
<EuiFlexItem grow={false}>
117-
<EuiButtonEmpty
118-
size="s"
119-
onClick={(e: any) => onChange(null, e)}
120-
onKeyPress={(e: any) => onChange(null, e)}
121-
>
122-
<FormattedMessage id="charts.colorPicker.clearColor" defaultMessage="Clear color" />
123-
</EuiButtonEmpty>
124-
</EuiFlexItem>
125-
)}
126-
</div>
127-
);
170+
);
171+
};

src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,8 +246,8 @@ describe('VisLegend Component', () => {
246246
first.simulate('click');
247247

248248
const popover = wrapper.find('.visColorPicker').first();
249-
const firstColor = popover.find('.visColorPicker__valueDot').first();
250-
firstColor.simulate('click');
249+
const firstColor = popover.find('.visColorPicker__colorBtn input').first();
250+
firstColor.simulate('change');
251251

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

src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,6 @@ export class VisLegend extends PureComponent<VisLegendProps, VisLegendState> {
233233
canFilter={this.state.filterableLabels.has(item.label)}
234234
onFilter={this.filter}
235235
onSelect={this.toggleDetails}
236-
legendId={this.legendId}
237236
setColor={this.setColor}
238237
getColor={this.getColor}
239238
onHighlight={this.highlight}

src/plugins/vis_type_vislib/public/vislib/components/legend/legend_item.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import { ColorPicker } from '../../../../../charts/public';
2525

2626
interface Props {
2727
item: LegendItem;
28-
legendId: string;
2928
selected: boolean;
3029
canFilter: boolean;
3130
anchorPosition: EuiPopoverProps['anchorPosition'];
@@ -39,7 +38,6 @@ interface Props {
3938

4039
const VisLegendItemComponent = ({
4140
item,
42-
legendId,
4341
selected,
4442
canFilter,
4543
anchorPosition,
@@ -150,7 +148,6 @@ const VisLegendItemComponent = ({
150148
{canFilter && renderFilterBar()}
151149

152150
<ColorPicker
153-
id={legendId}
154151
label={item.label}
155152
color={getColor(item.label)}
156153
onChange={(c, e) => setColor(item.label, c, e)}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
import React from 'react';
10+
import { LegendColorPickerProps, XYChartSeriesIdentifier } from '@elastic/charts';
11+
import { EuiPopover } from '@elastic/eui';
12+
import { mountWithIntl } from '@kbn/test/jest';
13+
import { ComponentType, ReactWrapper } from 'enzyme';
14+
import { getColorPicker } from './get_color_picker';
15+
import { ColorPicker } from '../../../charts/public';
16+
import type { PersistedState } from '../../../visualizations/public';
17+
18+
jest.mock('@elastic/charts', () => {
19+
const original = jest.requireActual('@elastic/charts');
20+
21+
return {
22+
...original,
23+
getSpecId: jest.fn(() => {}),
24+
};
25+
});
26+
27+
describe('getColorPicker', function () {
28+
const mockState = new Map();
29+
const uiState = ({
30+
get: jest
31+
.fn()
32+
.mockImplementation((key, fallback) => (mockState.has(key) ? mockState.get(key) : fallback)),
33+
set: jest.fn().mockImplementation((key, value) => mockState.set(key, value)),
34+
emit: jest.fn(),
35+
setSilent: jest.fn(),
36+
} as unknown) as PersistedState;
37+
38+
let wrapperProps: LegendColorPickerProps;
39+
const Component: ComponentType<LegendColorPickerProps> = getColorPicker(
40+
'left',
41+
jest.fn(),
42+
jest.fn().mockImplementation((seriesIdentifier) => seriesIdentifier.seriesKeys[0]),
43+
'default',
44+
uiState
45+
);
46+
let wrapper: ReactWrapper<LegendColorPickerProps>;
47+
48+
beforeAll(() => {
49+
wrapperProps = {
50+
color: 'rgb(109, 204, 177)',
51+
onClose: jest.fn(),
52+
onChange: jest.fn(),
53+
anchor: document.createElement('div'),
54+
seriesIdentifiers: [
55+
{
56+
yAccessor: 'col-2-1',
57+
splitAccessors: {},
58+
seriesKeys: ['Logstash Airways', 'col-2-1'],
59+
specId: 'histogram-col-2-1',
60+
key:
61+
'groupId{__pseudo_stacked_group-ValueAxis-1__}spec{histogram-col-2-1}yAccessor{col-2-1}splitAccessors{col-1-3-Logstash Airways}',
62+
} as XYChartSeriesIdentifier,
63+
],
64+
};
65+
});
66+
67+
it('renders the color picker', () => {
68+
wrapper = mountWithIntl(<Component {...wrapperProps} />);
69+
expect(wrapper.find(ColorPicker).length).toBe(1);
70+
});
71+
72+
it('renders the color picker with the colorIsOverwritten prop set to false if color is not overwritten for the specific series', () => {
73+
wrapper = mountWithIntl(<Component {...wrapperProps} />);
74+
expect(wrapper.find(ColorPicker).prop('colorIsOverwritten')).toBe(false);
75+
});
76+
77+
it('renders the color picker with the colorIsOverwritten prop set to true if color is overwritten for the specific series', () => {
78+
uiState.set('vis.colors', { 'Logstash Airways': '#6092c0' });
79+
wrapper = mountWithIntl(<Component {...wrapperProps} />);
80+
expect(wrapper.find(ColorPicker).prop('colorIsOverwritten')).toBe(true);
81+
});
82+
83+
it('renders the picker on the correct position', () => {
84+
wrapper = mountWithIntl(<Component {...wrapperProps} />);
85+
expect(wrapper.find(EuiPopover).prop('anchorPosition')).toEqual('rightCenter');
86+
});
87+
88+
it('renders the picker for kibana palette with useLegacyColors set to true', () => {
89+
const LegacyPaletteComponent: ComponentType<LegendColorPickerProps> = getColorPicker(
90+
'left',
91+
jest.fn(),
92+
jest.fn(),
93+
'kibana_palette',
94+
uiState
95+
);
96+
wrapper = mountWithIntl(<LegacyPaletteComponent {...wrapperProps} />);
97+
expect(wrapper.find(ColorPicker).prop('useLegacyColors')).toBe(true);
98+
});
99+
});

0 commit comments

Comments
 (0)