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

Moved the validation logic of the color picker out to a color util

This makes the color picker more inline with GitLab UI inputs whereby
we pass in the invalid feedback message and current state of the input
parent 322336c3
......@@ -23,3 +23,23 @@ export const textColorForBackground = (backgroundColor) => {
}
return '#FFFFFF';
};
/**
* Check whether a color matches the expected hex format
*
* This matches any hex (0-9 and A-F) value which is either 3 or 6 characters in length
*
* An empty string will return `null` which means that this is neither valid nor invalid.
* This is useful for forms resetting the validation state
*
* @param color string = ''
*
* @returns {null|boolean}
*/
export const validateHexColor = (color = '') => {
if (!color) {
return null;
}
return /^#([0-9A-F]{3}){1,2}$/i.test(color);
};
......@@ -3,12 +3,16 @@
* Renders a color picker input with preset colors to choose from
*
* @example
* <color-picker :label="__('Background color')" set-color="#FF0000" />
* <color-picker
:invalid-feedback="__('Please enter a valid hex (#RRGGBB or #RGB) color value')"
:label="__('Background color')"
set-color="#FF0000"
state="isValidColor"
/>
*/
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';
......@@ -24,6 +28,11 @@ export default {
GlTooltip: GlTooltipDirective,
},
props: {
invalidFeedback: {
type: String,
required: false,
default: __('Please enter a valid hex (#RRGGBB or #RGB) color value'),
},
label: {
type: String,
required: false,
......@@ -34,6 +43,11 @@ export default {
required: false,
default: '',
},
state: {
type: Boolean,
required: false,
default: null,
},
},
data() {
return {
......@@ -50,46 +64,32 @@ export default {
return gon.suggested_label_colors;
},
previewColor() {
if (this.isValidColor) {
if (this.state) {
return { backgroundColor: this.selectedColor };
}
return {};
},
previewColorClasses() {
const borderStyle = this.isInvalidColor
? 'gl-inset-border-1-red-500'
: 'gl-inset-border-1-gray-400';
const borderStyle =
this.state === false ? '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>
......@@ -100,17 +100,17 @@ export default {
:label="label"
label-for="color-picker"
:description="description"
:invalid-feedback="this.$options.i18n.invalid"
:state="isValidColor"
:invalid-feedback="invalidFeedback"
:state="state"
: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"
:state="state"
@input="handleColorChange"
>
<template #prepend>
......
import { textColorForBackground, hexToRgb } from '~/lib/utils/color_utils';
import { textColorForBackground, hexToRgb, validateHexColor } from '~/lib/utils/color_utils';
describe('Color utils', () => {
describe('Converting hex code to rgb', () => {
......@@ -32,4 +32,19 @@ describe('Color utils', () => {
expect(textColorForBackground('#000')).toEqual('#FFFFFF');
});
});
describe('Validate hex color', () => {
it.each`
color | output
${undefined} | ${null}
${null} | ${null}
${''} | ${null}
${'ABC123'} | ${false}
${'#ZZZ'} | ${false}
${'#FF0'} | ${true}
${'#FF0000'} | ${true}
`('returns $output when $color is given', ({ color, output }) => {
expect(validateHexColor(color)).toEqual(output);
});
});
});
......@@ -13,6 +13,7 @@ describe('ColorPicker', () => {
};
const setColor = '#000000';
const invalidText = 'Please enter a valid hex (#RRGGBB or #RGB) color value';
const label = () => wrapper.find(GlFormGroup).attributes('label');
const colorPreview = () => wrapper.find('[data-testid="color-preview"]');
const colorPicker = () => wrapper.find(GlFormInput);
......@@ -55,6 +56,7 @@ describe('ColorPicker', () => {
expect(colorPreview().attributes('style')).toBe(undefined);
expect(colorPicker().attributes('value')).toBe(undefined);
expect(colorInput().props('value')).toBe('');
expect(colorPreview().attributes('class')).toContain('gl-inset-border-1-gray-400');
});
it('has a color set on initialization', () => {
......@@ -67,7 +69,7 @@ describe('ColorPicker', () => {
createComponent();
await colorInput().setValue(setColor);
expect(wrapper.emitted().input[0]).toEqual([setColor]);
expect(wrapper.emitted().input[0]).toStrictEqual([setColor]);
});
it('trims spaces from submitted colors', async () => {
......@@ -75,23 +77,16 @@ describe('ColorPicker', () => {
await colorInput().setValue(` ${setColor} `);
expect(wrapper.vm.$data.selectedColor).toBe(setColor);
expect(colorPreview().attributes('class')).toContain('gl-inset-border-1-gray-400');
expect(colorInput().attributes('class')).not.toContain('is-invalid');
});
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');
it('shows invalid feedback when the state is marked as invalid', async () => {
createComponent(mount, { invalidFeedback: invalidText, state: false });
expect(invalidFeedback().text()).toBe(invalidText);
expect(colorPreview().attributes('class')).toContain('gl-inset-border-1-red-500');
expect(colorInput().attributes('class')).toContain('is-invalid');
});
});
......
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