Replaces the Custom Color Picker on TSVB with the EuiColorPicker (#68888)

* Replace the Custom Color Picker on TSVB with the EuiColorPicker

* Remove the custom picker sass

* Remove private modules of eui and the custom color swatches

* Clear the color

* changes in test implementation

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Stratoula Kalafateli 2020-06-17 14:27:05 +03:00 committed by GitHub
parent 8a6aab33bd
commit ab1270e566
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 158 additions and 527 deletions

View file

@ -8,39 +8,6 @@
position: relative;
}
.tvbColorPicker__swatch-empty,
.tvbColorPicker__swatch {
// SASSTODO: Replace with EUI component
// sass-lint:disable-block placeholder-in-extend
@extend .euiColorPickerSwatch;
}
.tvbColorPicker__swatch-empty {
background-color: transparent;
background-size: 22px 22px;
background-image: repeating-linear-gradient(
-45deg,
$euiColorDanger,
$euiColorDanger 2px,
transparent 2px,
transparent $euiSize
);
}
.tvbColorPicker__clear {
margin-left: $euiSizeXS;
}
.tvbColorPicker__popover {
position: absolute;
top: $euiSizeL;
z-index: 2;
}
.tvbColorPicker__cover {
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0;
}

View file

@ -1,97 +0,0 @@
// EUITODO: Convert to EuiColorPicker
// with additional support for alpha, saturation, swatches
// SASSTODO: This custom picker moved all styles from react-color inline styles
// to SASS, but it should be in EUI.
// Also, some pixel values were kept as is to match inline styles from react-color
.tvbColorPickerPopUp {
@include euiBottomShadowMedium;
background-color: $euiColorEmptyShade;
border-radius: $euiBorderRadius;
box-sizing: initial;
width: 275px;
font-family: 'Menlo';
}
.tvbColorPickerPopUp__saturation {
width: 100%;
padding-bottom: 55%;
position: relative;
border-radius: $euiBorderRadius $euiBorderRadius 0 0;
overflow: hidden;
}
.tvbColorPickerPopUp__body {
padding: $euiSize;
}
.tvbColorPickerPopUp__controls {
display: flex;
}
.tvbColorPickerPopUp__color {
width: $euiSizeXL;
// The color indicator doesn't work, hiding it until it does
display: none;
}
.tvbColorPickerPopUp__color-disableAlpha {
width: $euiSizeL;
}
.tvbColorPickerPopUp__swatch {
margin-top: 6px;
width: $euiSize;
height: $euiSize;
border-radius: $euiSizeS;
position: relative;
overflow: hidden;
}
.tvbColorPickerPopUp__swatch-disableAlpha {
width: 10px;
height: 10px;
margin: 0;
}
.tvbColorPickerPopUp__active {
@include euiBottomShadowMedium;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: $euiSizeS;
z-index: 2;
}
.tvbColorPickerPopUp__toggles {
flex: 1;
}
.tvbColorPickerPopUp__hue {
height: 10px;
position: relative;
margin-bottom: $euiSizeS;
}
.tvbColorPickerPopUp__hue-disableAlpha {
margin-bottom: 0;
}
.tvbColorPickerPopUp__alpha {
height: 10px;
position: relative;
}
.tvbColorPickerPopUp__alpha-disableAlpha {
display: none;
}
.tvbColorPickerPopUp__swatches {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-top: $euiSize;
}

View file

@ -1,7 +1,6 @@
@import './annotations_editor';
@import './color_rules';
@import './color_picker';
@import './custom_color_picker';
@import './error';
@import './no_data';
@import './markdown_editor';

View file

@ -1,125 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
// The color picker is not yet accessible.
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { EuiIconTip } from '@elastic/eui';
import { CustomColorPicker } from './custom_color_picker';
import { i18n } from '@kbn/i18n';
export class ColorPicker extends Component {
constructor(props) {
super(props);
this.state = {
displayPicker: false,
color: {},
};
}
handleChange = (color) => {
const { rgb } = color;
const part = {};
part[this.props.name] = `rgba(${rgb.r},${rgb.g},${rgb.b},${rgb.a})`;
if (this.props.onChange) this.props.onChange(part);
};
handleClick = () => {
this.setState({ displayPicker: !this.state.displayColorPicker });
};
handleClose = () => {
this.setState({ displayPicker: false });
};
handleClear = () => {
const part = {};
part[this.props.name] = null;
this.props.onChange(part);
};
renderSwatch() {
if (!this.props.value) {
return (
<button
aria-label={i18n.translate('visTypeTimeseries.colorPicker.notAccessibleAriaLabel', {
defaultMessage: 'Color picker, not accessible',
})}
className="tvbColorPicker__swatch-empty"
onClick={this.handleClick}
/>
);
}
return (
<button
aria-label={i18n.translate(
'visTypeTimeseries.colorPicker.notAccessibleWithValueAriaLabel',
{
defaultMessage: 'Color picker ({value}), not accessible',
values: { value: this.props.value },
}
)}
style={{ backgroundColor: this.props.value }}
className="tvbColorPicker__swatch"
onClick={this.handleClick}
/>
);
}
render() {
const swatch = this.renderSwatch();
const value = this.props.value || undefined;
let clear;
if (!this.props.disableTrash) {
clear = (
<div className="tvbColorPicker__clear" onClick={this.handleClear}>
<EuiIconTip
size="s"
type="cross"
color="danger"
content={i18n.translate('visTypeTimeseries.colorPicker.clearIconLabel', {
defaultMessage: 'Clear',
})}
/>
</div>
);
}
return (
<div className="tvbColorPicker" data-test-subj="tvbColorPicker">
{swatch}
{clear}
{this.state.displayPicker ? (
<div className="tvbColorPicker__popover">
<div className="tvbColorPicker__cover" onClick={this.handleClose} />
<CustomColorPicker color={value} onChangeComplete={this.handleChange} />
</div>
) : null}
</div>
);
}
}
ColorPicker.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
disableTrash: PropTypes.bool,
onChange: PropTypes.func,
};

View file

@ -1,69 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { ColorPicker } from './color_picker';
import { mount } from 'enzyme';
import { CustomColorPicker } from './custom_color_picker';
const defaultProps = {
name: 'color',
value: null,
onChange: jest.fn(),
disableTrash: true,
};
describe('ColorPicker', () => {
it('should change state after click', () => {
const wrapper = mount(<ColorPicker {...defaultProps} />);
const stateBefore = wrapper.state();
wrapper.find('button').at(0).simulate('click');
const stateAfter = wrapper.state();
expect(stateBefore.displayPicker).toBe(!stateAfter.displayPicker);
});
it('should close popup after click', () => {
const wrapper = mount(<ColorPicker {...defaultProps} />);
wrapper.setState({ displayPicker: true });
wrapper.find('.tvbColorPicker__cover').simulate('click');
expect(wrapper.state().displayPicker).toBeFalsy();
});
it('should exist CustomColorPickerUI component', () => {
const wrapper = mount(<ColorPicker {...defaultProps} />);
wrapper.setState({ displayPicker: true });
const w2 = wrapper.find(CustomColorPicker).exists();
expect(w2).toBeTruthy();
});
it('should call clear function', () => {
const props = { ...defaultProps, disableTrash: false, value: 'rgba(85,66,177,1)' };
const wrapper = mount(<ColorPicker {...props} />);
wrapper.find('.tvbColorPicker__clear').simulate('click');
expect(defaultProps.onChange).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,61 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { ColorPicker, ColorPickerProps } from './color_picker';
import { mount } from 'enzyme';
import { ReactWrapper } from 'enzyme';
import { EuiColorPicker, EuiIconTip } from '@elastic/eui';
describe('ColorPicker', () => {
const defaultProps: ColorPickerProps = {
name: 'color',
value: null,
onChange: jest.fn(),
disableTrash: true,
};
let component: ReactWrapper<ColorPickerProps>;
it('should render the EuiColorPicker', () => {
component = mount(<ColorPicker {...defaultProps} />);
expect(component.find(EuiColorPicker).length).toBe(1);
});
it('should not render the clear button', () => {
component = mount(<ColorPicker {...defaultProps} />);
expect(component.find('.tvbColorPicker__clear').length).toBe(0);
});
it('should render the correct aria label to the color swatch button', () => {
const props = { ...defaultProps, value: 'rgba(85,66,177,0.59)' };
component = mount(<ColorPicker {...props} />);
const button = component.find('.tvbColorPicker button');
expect(button.prop('aria-label')).toBe('Color picker (rgba(85,66,177,0.59)), not accessible');
});
it('should call clear function if the disableTrash prop is false', () => {
const props = { ...defaultProps, disableTrash: false, value: 'rgba(85,66,177,1)' };
component = mount(<ColorPicker {...props} />);
component.find('.tvbColorPicker__clear').simulate('click');
expect(component.find(EuiIconTip).length).toBe(1);
expect(defaultProps.onChange).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,95 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
// The color picker is not yet accessible.
import React, { useState } from 'react';
import {
EuiIconTip,
EuiColorPicker,
EuiColorPickerProps,
EuiColorPickerSwatch,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
const COMMAS_NUMS_ONLY_RE = /[^0-9,]/g;
interface ColorProps {
[key: string]: string | null;
}
export interface ColorPickerProps {
name: string;
value: string | null;
disableTrash?: boolean;
onChange: (props: ColorProps) => void;
}
export function ColorPicker({ name, value, disableTrash = false, onChange }: ColorPickerProps) {
const initialColorValue = value ? value.replace(COMMAS_NUMS_ONLY_RE, '') : '';
const [color, setColor] = useState(initialColorValue);
const handleColorChange: EuiColorPickerProps['onChange'] = (text: string, { rgba, hex }) => {
setColor(text);
const part: ColorProps = {};
part[name] = hex ? `rgba(${rgba.join(',')})` : '';
onChange(part);
};
const handleClear = () => {
setColor('');
const part: ColorProps = {};
part[name] = null;
onChange(part);
};
const label = value
? i18n.translate('visTypeTimeseries.colorPicker.notAccessibleWithValueAriaLabel', {
defaultMessage: 'Color picker ({value}), not accessible',
values: { value },
})
: i18n.translate('visTypeTimeseries.colorPicker.notAccessibleAriaLabel', {
defaultMessage: 'Color picker, not accessible',
});
return (
<div className="tvbColorPicker" data-test-subj="tvbColorPicker">
<EuiColorPicker
onChange={handleColorChange}
color={color}
secondaryInputDisplay="top"
showAlpha
button={<EuiColorPickerSwatch color={color} aria-label={label} />}
/>
{!disableTrash && (
<div className="tvbColorPicker__clear" onClick={handleClear}>
<EuiIconTip
size="s"
type="cross"
color="danger"
content={i18n.translate('visTypeTimeseries.colorPicker.clearIconLabel', {
defaultMessage: 'Clear',
})}
/>
</div>
)}
</div>
);
}

View file

@ -1,200 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import PropTypes from 'prop-types';
import React, { PureComponent } from 'react';
import {
ColorWrap as colorWrap,
Saturation,
Hue,
Alpha,
Checkboard,
} from 'react-color/lib/components/common';
import ChromeFields from 'react-color/lib/components/chrome/ChromeFields';
import ChromePointer from 'react-color/lib/components/chrome/ChromePointer';
import ChromePointerCircle from 'react-color/lib/components/chrome/ChromePointerCircle';
import CompactColor from 'react-color/lib/components/compact/CompactColor';
import color from 'react-color/lib/helpers/color';
class CustomColorPickerUI extends PureComponent {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(data) {
this.props.onChange(data);
}
render() {
const rgb = this.props.rgb;
const styles = {
active: {
background: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`,
},
Saturation: {
radius: '2px 2px 0 0 ',
},
Hue: {
radius: '2px',
},
Alpha: {
radius: '2px',
},
};
const handleSwatchChange = (data) => {
if (data.hex) {
color.isValidHex(data.hex) &&
this.props.onChange({
hex: data.hex,
source: 'hex',
});
} else {
this.props.onChange(data);
}
};
const swatches = this.props.colors.map((c) => {
return <CompactColor key={c} color={c} onClick={handleSwatchChange} />;
});
return (
<div className="tvbColorPickerPopUp" data-test-subj="tvbColorPickerPopUp">
<div className="tvbColorPickerPopUp__saturation">
<Saturation
style={styles.Saturation}
{...this.props}
pointer={ChromePointerCircle}
onChange={this.handleChange}
/>
</div>
<div className="tvbColorPickerPopUp__body">
<div className="tvbColorPickerPopUp__controls">
<div
className={
this.props.disableAlpha
? 'tvbColorPickerPopUp__color-disableAlpha'
: 'tvbColorPickerPopUp__color'
}
>
<div
className={
this.props.disableAlpha
? 'tvbColorPickerPopUp__swatch-disableAlpha'
: 'tvbColorPickerPopUp__swatch'
}
>
<div className="tvbColorPickerPopUp__active" />
<Checkboard />
</div>
</div>
<div className="tvbColorPickerPopUp__toggles">
<div
className={
this.props.disableAlpha
? 'tvbColorPickerPopUp__hue-disableAlpha'
: 'tvbColorPickerPopUp__hue'
}
>
<Hue
style={styles.Hue}
{...this.props}
pointer={ChromePointer}
onChange={this.handleChange}
/>
</div>
<div
className={
this.props.disableAlpha
? 'tvbColorPickerPopUp__alpha-disableAlpha'
: 'tvbColorPickerPopUp__alpha'
}
>
<Alpha
style={styles.Alpha}
{...this.props}
pointer={ChromePointer}
onChange={this.handleChange}
/>
</div>
</div>
</div>
<ChromeFields
{...this.props}
onChange={this.handleChange}
disableAlpha={this.props.disableAlpha}
/>
<div className="tvbColorPickerPopUp__swatches">{swatches}</div>
</div>
</div>
);
}
}
CustomColorPickerUI.defaultProps = {
colors: [
'#4D4D4D',
'#999999',
'#FFFFFF',
'#F44E3B',
'#FE9200',
'#FCDC00',
'#DBDF00',
'#A4DD00',
'#68CCCA',
'#73D8FF',
'#AEA1FF',
'#FDA1FF',
'#333333',
'#808080',
'#cccccc',
'#D33115',
'#E27300',
'#FCC400',
'#B0BC00',
'#68BC00',
'#16A5A5',
'#009CE0',
'#7B64FF',
'#FA28FF',
'#0F1419',
'#666666',
'#B3B3B3',
'#9F0500',
'#C45100',
'#FB9E00',
'#808900',
'#194D33',
'#0C797D',
'#0062B1',
'#653294',
'#AB149E',
],
};
CustomColorPickerUI.propTypes = {
color: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
onChangeComplete: PropTypes.func,
onChange: PropTypes.func,
};
export const CustomColorPicker = colorWrap(CustomColorPickerUI);

View file

@ -502,14 +502,14 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro
public async setBackgroundColor(colorHex: string): Promise<void> {
await this.clickColorPicker();
await this.checkColorPickerPopUpIsPresent();
await find.setValue('.tvbColorPickerPopUp input', colorHex);
await find.setValue('.euiColorPicker input', colorHex);
await this.clickColorPicker();
await PageObjects.visChart.waitForVisualizationRenderingStabilized();
}
public async checkColorPickerPopUpIsPresent(): Promise<void> {
log.debug(`Check color picker popup is present`);
await testSubjects.existOrFail('tvbColorPickerPopUp', { timeout: 5000 });
await testSubjects.existOrFail('colorPickerPopover', { timeout: 5000 });
}
public async changePanelPreview(nth: number = 0): Promise<void> {