Commit f3e4652c authored by Robert Hunt's avatar Robert Hunt Committed by Simon Knox

Creating color picker component

- Implemented input type of color for preview area
- Implemented text HEX input
- Implemented validation on the HEX value
- Implemented preset color selections
- Added tests for the new component
- Added translations
- Added a changelog
parent 49da3932
<script>
/**
* Renders a color picker input with preset colors to choose from
*
* @example
* <color-picker :label="__('Background color')" set-color="#FF0000" />
*/
import { GlFormGroup, GlFormInput, GlFormInputGroup, GlLink, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
const VALID_RGB_HEX_COLOR = /^#([0-9A-F]{3}){1,2}$/i;
const PREVIEW_COLOR_DEFAULT_CLASSES =
'gl-relative gl-w-7 gl-bg-gray-10 gl-rounded-top-left-base gl-rounded-bottom-left-base';
export default {
name: 'ColorPicker',
components: {
GlFormGroup,
GlFormInput,
GlFormInputGroup,
GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
label: {
type: String,
required: false,
default: '',
},
setColor: {
type: String,
required: false,
default: '',
},
},
data() {
return {
selectedColor: this.setColor.trim() || '',
};
},
computed: {
description() {
return this.hasSuggestedColors
? this.$options.i18n.fullDescription
: this.$options.i18n.shortDescription;
},
suggestedColors() {
return gon.suggested_label_colors;
},
previewColor() {
if (this.isValidColor) {
return { backgroundColor: this.selectedColor };
}
return {};
},
previewColorClasses() {
const borderStyle = this.isInvalidColor
? 'gl-inset-border-1-red-500'
: 'gl-inset-border-1-gray-400';
return `${PREVIEW_COLOR_DEFAULT_CLASSES} ${borderStyle}`;
},
hasSuggestedColors() {
return Object.keys(this.suggestedColors).length;
},
isInvalidColor() {
return this.isValidColor === false;
},
isValidColor() {
if (this.selectedColor === '') {
return null;
}
return VALID_RGB_HEX_COLOR.test(this.selectedColor);
},
},
methods: {
handleColorChange(color) {
this.selectedColor = color.trim();
if (this.isValidColor) {
this.$emit('input', this.selectedColor);
}
},
},
i18n: {
fullDescription: __('Choose any color. Or you can choose one of the suggested colors below'),
shortDescription: __('Choose any color'),
invalid: __('Please enter a valid hex (#RRGGBB or #RGB) color value'),
},
};
</script>
<template>
<div>
<gl-form-group
:label="label"
label-for="color-picker"
:description="description"
:invalid-feedback="this.$options.i18n.invalid"
:state="isValidColor"
:class="{ 'gl-mb-3!': hasSuggestedColors }"
>
<gl-form-input-group
id="color-picker"
:state="isValidColor"
max-length="7"
type="text"
class="gl-align-center gl-rounded-0 gl-rounded-top-right-base gl-rounded-bottom-right-base"
:value="selectedColor"
@input="handleColorChange"
>
<template #prepend>
<div :class="previewColorClasses" :style="previewColor" data-testid="color-preview">
<gl-form-input
type="color"
class="gl-absolute gl-top-0 gl-left-0 gl-h-full! gl-p-0! gl-m-0! gl-cursor-pointer gl-opacity-0"
tabindex="-1"
:value="selectedColor"
@input="handleColorChange"
/>
</div>
</template>
</gl-form-input-group>
</gl-form-group>
<div v-if="hasSuggestedColors" class="gl-mb-3">
<gl-link
v-for="(name, hex) in suggestedColors"
:key="hex"
v-gl-tooltip
:title="name"
:style="{ backgroundColor: hex }"
class="gl-rounded-base gl-w-7 gl-h-7 gl-display-inline-block gl-mr-3 gl-mb-3 gl-text-decoration-none"
@click.prevent="handleColorChange(hex)"
/>
</div>
</div>
</template>
......@@ -5452,9 +5452,15 @@ msgstr ""
msgid "Choose a type..."
msgstr ""
msgid "Choose any color"
msgstr ""
msgid "Choose any color."
msgstr ""
msgid "Choose any color. Or you can choose one of the suggested colors below"
msgstr ""
msgid "Choose between %{code_open}clone%{code_close} or %{code_open}fetch%{code_close} to get the recent application code"
msgstr ""
......@@ -20590,6 +20596,9 @@ msgstr ""
msgid "Please enter a valid URL format, ex: http://www.example.com/home"
msgstr ""
msgid "Please enter a valid hex (#RRGGBB or #RGB) color value"
msgstr ""
msgid "Please enter a valid number"
msgstr ""
......
import { GlFormGroup, GlFormInput, GlFormInputGroup, GlLink } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import ColorPicker from '~/vue_shared/components/color_picker/color_picker.vue';
describe('ColorPicker', () => {
let wrapper;
const createComponent = (fn = mount, propsData = {}) => {
wrapper = fn(ColorPicker, {
propsData,
});
};
const setColor = '#000000';
const label = () => wrapper.find(GlFormGroup).attributes('label');
const colorPreview = () => wrapper.find('[data-testid="color-preview"]');
const colorPicker = () => wrapper.find(GlFormInput);
const colorInput = () => wrapper.find(GlFormInputGroup).find('input[type="text"]');
const invalidFeedback = () => wrapper.find('.invalid-feedback');
const description = () => wrapper.find(GlFormGroup).attributes('description');
const presetColors = () => wrapper.findAll(GlLink);
beforeEach(() => {
gon.suggested_label_colors = {
[setColor]: 'Black',
'#0033CC': 'UA blue',
'#428BCA': 'Moderate blue',
'#44AD8E': 'Lime green',
};
createComponent(shallowMount);
});
afterEach(() => {
wrapper.destroy();
});
describe('label', () => {
it('hides the label if the label is not passed', () => {
expect(label()).toBe('');
});
it('shows the label if the label is passed', () => {
createComponent(shallowMount, { label: 'test' });
expect(label()).toBe('test');
});
});
describe('behavior', () => {
it('by default has no values', () => {
createComponent();
expect(colorPreview().attributes('style')).toBe(undefined);
expect(colorPicker().attributes('value')).toBe(undefined);
expect(colorInput().props('value')).toBe('');
});
it('has a color set on initialization', () => {
createComponent(shallowMount, { setColor });
expect(wrapper.vm.$data.selectedColor).toBe(setColor);
});
it('emits input event from component when a color is selected', async () => {
createComponent();
await colorInput().setValue(setColor);
expect(wrapper.emitted().input[0]).toEqual([setColor]);
});
it('trims spaces from submitted colors', async () => {
createComponent();
await colorInput().setValue(` ${setColor} `);
expect(wrapper.vm.$data.selectedColor).toBe(setColor);
});
it('shows invalid feedback when an invalid color is used', async () => {
createComponent();
await colorInput().setValue('abcd');
expect(invalidFeedback().text()).toBe(
'Please enter a valid hex (#RRGGBB or #RGB) color value',
);
expect(wrapper.emitted().input).toBe(undefined);
});
it('shows an invalid feedback border on the preview when an invalid color is used', async () => {
createComponent();
await colorInput().setValue('abcd');
expect(colorPreview().attributes('class')).toContain('gl-inset-border-1-red-500');
});
});
describe('inputs', () => {
it('has color input value entered', async () => {
createComponent();
await colorInput().setValue(setColor);
expect(wrapper.vm.$data.selectedColor).toBe(setColor);
});
it('has color picker value entered', async () => {
createComponent();
await colorPicker().setValue(setColor);
expect(wrapper.vm.$data.selectedColor).toBe(setColor);
});
});
describe('preset colors', () => {
it('hides the suggested colors if they are empty', () => {
gon.suggested_label_colors = {};
createComponent(shallowMount);
expect(description()).toBe('Choose any color');
expect(presetColors().exists()).toBe(false);
});
it('shows the suggested colors', () => {
createComponent(shallowMount);
expect(description()).toBe(
'Choose any color. Or you can choose one of the suggested colors below',
);
expect(presetColors()).toHaveLength(4);
});
it('has preset color selected', async () => {
createComponent();
await presetColors()
.at(0)
.trigger('click');
expect(wrapper.vm.$data.selectedColor).toBe(setColor);
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment