Commit 80b60f5b authored by Simon Knox's avatar Simon Knox

Merge branch '287826-create-a-shared-color-picker-component' into 'master'

Create a shared color picker component

See merge request gitlab-org/gitlab!48606
parents 8cc028ad f3e4652c
<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