From cdfce862dcee843ae8b07531f1bfe271c5107dbb Mon Sep 17 00:00:00 2001 From: kleinfreund Date: Thu, 16 Dec 2021 18:08:50 +0100 Subject: [PATCH] feat: add prop for hiding alpha channel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new optional `alphaChannel` prop which can be used to hide any controls related to a color’s alpha channel from the color picker. The default behavior is to show all alpha channel controls. --- README.md | 17 +- src/ColorPicker.test.js | 864 +++++++++++++--------- src/ColorPicker.vue | 170 +++-- src/constants.js | 2 + src/utilities/format-as-css-color.js | 37 +- src/utilities/format-as-css-color.test.js | 63 +- types/index.d.ts | 2 + 7 files changed, 707 insertions(+), 448 deletions(-) diff --git a/README.md b/README.md index 3c5f328..1a1772b 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Links: - [`color`](#color) - [`visibleFormats`](#visibleformats) - [`defaultFormat`](#defaultformat) + - [`alphaChannel`](#alphachannel) - [`id`](#id) - [Events](#events) - [`color-change`](#color-change) @@ -160,6 +161,18 @@ app.component('ColorPicker', ColorPicker) ``` +#### `alphaChannel` + +- **Description**: Whether to show input controls for a color’s alpha channel. If set to `'hide'`, the alpha range input and the alpha channel input are hidden, the “Copy color” button will copy a CSS color value without alpha channel, and the object emitted in a `color-change` event will have a `cssColor` property value without alpha channel. +- **Type**: `'show'` or `'hide'` +- **Required**: `false` +- **Default**: `'show'` +- **Usage**: + + ```html + + ``` + #### `id` - **Description**: The ID value will be used to prefix all `input` elements’ `id` and `label` elements’ `for` attribute values. Set this prop if you use multiple instances of the `color-picker` component on one page. @@ -177,7 +190,7 @@ app.component('ColorPicker', ColorPicker) #### `color-change` - **Description**: An `input` event is emitted each time the internal colors object is updated. -- **Data**: The event emits an object containing both the internal colors object and a CSS color value as a string based on the currently active format. +- **Data**: The event emits an object containing both the internal colors object and a CSS color value as a string based on the currently active format. The `cssColor` property will respect `alphaChannel`. ```ts { @@ -249,7 +262,7 @@ app.component('ColorPicker', ColorPicker) |:---------------:|:-------:|:------:|:------:| | 79 | 63 | 73 | 12.2 | -The browser support is derived from the use of [Object.fromEntries()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries), [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/--*), and [spread syntax in object literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax). +The browser support is derived from the use of [Object.fromEntries()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries) and [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/--*). ## Versioning diff --git a/src/ColorPicker.test.js b/src/ColorPicker.test.js index c02a5e3..9a65224 100644 --- a/src/ColorPicker.test.js +++ b/src/ColorPicker.test.js @@ -37,73 +37,6 @@ describe('ColorPicker', () => { expect(wrapper.html()).toBeTruthy() }) - test.each([ - ['hex', '#f00'], - ['rgb', { r: 1, g: 0.5, b: 0, a: 0.5 }], - ['hsl', { h: 0, s: 1, l: 0.5, a: 1 }], - ['hwb', { h: 0.5, w: 0.33, b: 0.5, a: 1 }], - ['hsv', { h: 0.5, s: 0.33, v: 0.5, a: 1 }], - ])('mounts correctly with a valid color prop', (format, colorProp) => { - const wrapper = shallowMount(ColorPicker, { - props: { - color: colorProp, - }, - }) - - expect(wrapper.vm.colors[format]).toEqual(colorProp) - }) - - test('mounts correctly with an invalid color prop', () => { - const wrapper = shallowMount(ColorPicker, { - props: { - color: '#ff', - }, - }) - - expect(wrapper.vm.colors.hex).toBe('#ffffffff') - }) - - test.each([ - [undefined, 'hsl'], - ['hex', 'hex'], - ['hsl', 'hsl'], - ['hwb', 'hwb'], - ['rgb', 'rgb'], - ])('sets active color format to “%s” when providing default format prop', (defaultFormat, expectedActiveFormat) => { - const wrapper = shallowMount(ColorPicker, { - propsData: { - defaultFormat, - }, - }) - - expect(wrapper.vm.activeFormat).toBe(expectedActiveFormat) - }) - - test.each([ - [ - '#f80c', - { r: 1, g: 0.5333333333333333, b: 0, a: 0.8 }, - ], - [ - { h: 0.5, s: 0.33, v: 0.5, a: 1 }, - { r: 0.33499999999999996, g: 0.5, b: 0.5, a: 1 }, - ], - ])('recomputes colors when color prop changes', async (colorProp, expectedColorChangePayload) => { - const wrapper = shallowMount(ColorPicker) - - await wrapper.setProps({ color: colorProp }) - let emittedColorChangeEvents = wrapper.emitted()['color-change'] - // @ts-ignore because `unknown` is clearly not a correct type for emitted records. - let emittedRgbColor = emittedColorChangeEvents[emittedColorChangeEvents.length - 1][0].colors.rgb - expect(emittedRgbColor).toEqual(expectedColorChangePayload) - - await wrapper.setProps({ color: '#fffc' }) - emittedColorChangeEvents = wrapper.emitted()['color-change'] - // @ts-ignore because `unknown` is clearly not a correct type for emitted records. - emittedRgbColor = emittedColorChangeEvents[emittedColorChangeEvents.length - 1][0].colors.rgb - expect(emittedRgbColor).toEqual({ r: 1, g: 1, b: 1, a: 0.8 }) - }) - test('removes event listeners on unmount', async () => { const wrapper = shallowMount(ColorPicker) @@ -127,368 +60,599 @@ describe('ColorPicker', () => { expect(emittedColorChangeEvents.length).toBe(2) }) - test('id attributes are set correctly', async () => { - const id = 'test-color-picker' - const wrapper = shallowMount(ColorPicker, { - props: { - id, - }, + describe('props & attributes', () => { + test.each([ + ['hex', '#f00'], + ['rgb', { r: 1, g: 0.5, b: 0, a: 0.5 }], + ['hsl', { h: 0, s: 1, l: 0.5, a: 1 }], + ['hwb', { h: 0.5, w: 0.33, b: 0.5, a: 1 }], + ['hsv', { h: 0.5, s: 0.33, v: 0.5, a: 1 }], + ])('mounts correctly with a valid color prop', (format, colorProp) => { + const wrapper = shallowMount(ColorPicker, { + props: { + color: colorProp, + }, + }) + + expect(wrapper.vm.colors[format]).toEqual(colorProp) }) - const hueInput = wrapper.find(`#${id}-hue-slider`) - expect(hueInput.exists()).toBe(true) - const alphaInput = wrapper.find(`#${id}-alpha-slider`) - expect(alphaInput.exists()).toBe(true) - - const colorHslHueInput = wrapper.find(`#${id}-color-hsl-h`) - expect(colorHslHueInput.exists()).toBe(true) - const colorHslSaturationInput = wrapper.find(`#${id}-color-hsl-s`) - expect(colorHslSaturationInput.exists()).toBe(true) - const colorHslLightnessInput = wrapper.find(`#${id}-color-hsl-l`) - expect(colorHslLightnessInput.exists()).toBe(true) - const colorHslAlphaInput = wrapper.find(`#${id}-color-hsl-a`) - expect(colorHslAlphaInput.exists()).toBe(true) - - wrapper.vm.activeFormat = 'rgb' - await flushPromises() - - const colorRgbRedInput = wrapper.find(`#${id}-color-rgb-r`) - expect(colorRgbRedInput.exists()).toBe(true) - const colorRgbGreenInput = wrapper.find(`#${id}-color-rgb-g`) - expect(colorRgbGreenInput.exists()).toBe(true) - const colorRgbBlueInput = wrapper.find(`#${id}-color-rgb-b`) - expect(colorRgbBlueInput.exists()).toBe(true) - const colorRgbAlphaInput = wrapper.find(`#${id}-color-rgb-a`) - expect(colorRgbAlphaInput.exists()).toBe(true) - }) + test('mounts correctly with an invalid color prop', () => { + const wrapper = shallowMount(ColorPicker, { + props: { + color: '#ff', + }, + }) - test('sets pointer origin when interacting with the color space element', async () => { - const wrapper = shallowMount(ColorPicker) - - expect(wrapper.vm.pointerOriginatedInColorSpace).toBe(false) + expect(wrapper.vm.colors.hex).toBe('#ffffffff') + }) - const colorSpace = wrapper.find('.vacp-color-space') - await colorSpace.trigger('mousedown') - expect(wrapper.vm.pointerOriginatedInColorSpace).toBe(true) + test.each([ + [undefined, 'hsl'], + ['hex', 'hex'], + ['hsl', 'hsl'], + ['hwb', 'hwb'], + ['rgb', 'rgb'], + ])('sets active color format to “%s” when providing default format prop', (defaultFormat, expectedActiveFormat) => { + const wrapper = shallowMount(ColorPicker, { + propsData: { + defaultFormat, + }, + }) + + expect(wrapper.vm.activeFormat).toBe(expectedActiveFormat) + }) - document.dispatchEvent(new Event('mouseup')) - expect(wrapper.vm.pointerOriginatedInColorSpace).toBe(false) + test.each([ + [ + '#f80c', + { r: 1, g: 0.5333333333333333, b: 0, a: 0.8 }, + ], + [ + { h: 0.5, s: 0.33, v: 0.5, a: 1 }, + { r: 0.33499999999999996, g: 0.5, b: 0.5, a: 1 }, + ], + ])('recomputes colors when color prop changes', async (colorProp, expectedColorChangePayload) => { + const wrapper = shallowMount(ColorPicker) + + await wrapper.setProps({ color: colorProp }) + let emittedColorChangeEvents = wrapper.emitted()['color-change'] + // @ts-ignore because `unknown` is clearly not a correct type for emitted records. + let emittedRgbColor = emittedColorChangeEvents[emittedColorChangeEvents.length - 1][0].colors.rgb + expect(emittedRgbColor).toEqual(expectedColorChangePayload) + + await wrapper.setProps({ color: '#fffc' }) + emittedColorChangeEvents = wrapper.emitted()['color-change'] + // @ts-ignore because `unknown` is clearly not a correct type for emitted records. + emittedRgbColor = emittedColorChangeEvents[emittedColorChangeEvents.length - 1][0].colors.rgb + expect(emittedRgbColor).toEqual({ r: 1, g: 1, b: 1, a: 0.8 }) + }) - await colorSpace.trigger('touchstart', { - preventDefault: jest.fn(), - touches: [{ clientX: 0, clientY: 0 }], + test('id attributes are set correctly', async () => { + const id = 'test-color-picker' + const wrapper = shallowMount(ColorPicker, { + props: { + id, + }, + }) + + const hueInput = wrapper.find(`#${id}-hue-slider`) + expect(hueInput.exists()).toBe(true) + const alphaInput = wrapper.find(`#${id}-alpha-slider`) + expect(alphaInput.exists()).toBe(true) + + const colorHslHueInput = wrapper.find(`#${id}-color-hsl-h`) + expect(colorHslHueInput.exists()).toBe(true) + const colorHslSaturationInput = wrapper.find(`#${id}-color-hsl-s`) + expect(colorHslSaturationInput.exists()).toBe(true) + const colorHslLightnessInput = wrapper.find(`#${id}-color-hsl-l`) + expect(colorHslLightnessInput.exists()).toBe(true) + const colorHslAlphaInput = wrapper.find(`#${id}-color-hsl-a`) + expect(colorHslAlphaInput.exists()).toBe(true) + + wrapper.vm.activeFormat = 'rgb' + await flushPromises() + + const colorRgbRedInput = wrapper.find(`#${id}-color-rgb-r`) + expect(colorRgbRedInput.exists()).toBe(true) + const colorRgbGreenInput = wrapper.find(`#${id}-color-rgb-g`) + expect(colorRgbGreenInput.exists()).toBe(true) + const colorRgbBlueInput = wrapper.find(`#${id}-color-rgb-b`) + expect(colorRgbBlueInput.exists()).toBe(true) + const colorRgbAlphaInput = wrapper.find(`#${id}-color-rgb-a`) + expect(colorRgbAlphaInput.exists()).toBe(true) }) - expect(wrapper.vm.pointerOriginatedInColorSpace).toBe(true) - document.dispatchEvent(new Event('touchend')) - expect(wrapper.vm.pointerOriginatedInColorSpace).toBe(false) + test.each([ + ['show', true, 'hsl(180 0% 100% / 1)'], + ['hide', false, 'hsl(180 0% 100%)'], + ])('shows/hides correct elements when setting alphaChannel', (alphaChannel, isElementVisible, expectedCssColor) => { + const id = 'test-color-picker' + const wrapper = shallowMount(ColorPicker, { + attachTo: injectTestDiv(), + props: { + id, + alphaChannel, + }, + }) + + const alphaInput = wrapper.find(`#${id}-alpha-slider`) + expect(alphaInput.exists()).toBe(isElementVisible) + + const colorHslAlphaInput = wrapper.find(`#${id}-color-hsl-a`) + expect(colorHslAlphaInput.exists()).toBe(isElementVisible) + + const format = 'hsl' + const channel = 'h' + const inputSelector = `#${wrapper.vm.id}-color-${format}-${channel}` + const inputElement = /** @type {HTMLInputElement} */ (wrapper.find(inputSelector).element) + inputElement.value = '180' + const inputEvent = { target: inputElement } + + wrapper.vm.updateColorValue(inputEvent, format, channel) + + const emittedColorChangeEvents = wrapper.emitted()['color-change'] + // @ts-ignore because `unknown` is clearly not a correct type for emitted records. + const emittedCssColor = emittedColorChangeEvents[emittedColorChangeEvents.length - 1][0].cssColor + expect(emittedCssColor).toEqual(expectedCssColor) + }) }) - test('can initiate moving the color space thumb with a mouse', async () => { - const clientX = 0 - const clientY = 0 - const mouseMoveEvent = { - buttons: 1, - preventDefault: jest.fn(), - clientX, - clientY, - } - - const wrapper = shallowMount(ColorPicker, { attachTo: injectTestDiv(), props: { color: '#f80c' } }) + describe('color space thumb interactions', () => { + test('sets pointer origin when interacting with the color space element', async () => { + const wrapper = shallowMount(ColorPicker) - let emittedColorChangeEvents = wrapper.emitted()['color-change'] - expect(emittedColorChangeEvents.length).toBe(1) + expect(wrapper.vm.pointerOriginatedInColorSpace).toBe(false) - const colorSpace = wrapper.find('.vacp-color-space') - await colorSpace.trigger('mousedown') - await colorSpace.trigger('mousemove', mouseMoveEvent) + const colorSpace = wrapper.find('.vacp-color-space') + await colorSpace.trigger('mousedown') + expect(wrapper.vm.pointerOriginatedInColorSpace).toBe(true) - emittedColorChangeEvents = wrapper.emitted()['color-change'] - expect(emittedColorChangeEvents.length).toBe(2) - - // Remove test HTML injected via the `attachTo` option during mount. - wrapper.unmount() - }) - - test('can initiate moving the color space thumb with a touch-based device', async () => { - const wrapper = shallowMount(ColorPicker, { attachTo: injectTestDiv(), props: { color: '#f80c' } }) - - let emittedColorChangeEvents = wrapper.emitted()['color-change'] - expect(emittedColorChangeEvents.length).toBe(1) - - const colorSpace = wrapper.find('.vacp-color-space') - await colorSpace.trigger('touchstart', { - preventDefault: jest.fn(), - touches: [{ clientX: 0, clientY: 0 }], - }) - await colorSpace.trigger('touchmove', { - preventDefault: jest.fn(), - touches: [{ clientX: 0, clientY: 0 }], - }) + document.dispatchEvent(new Event('mouseup')) + expect(wrapper.vm.pointerOriginatedInColorSpace).toBe(false) - emittedColorChangeEvents = wrapper.emitted()['color-change'] - expect(emittedColorChangeEvents.length).toBe(3) + await colorSpace.trigger('touchstart', { + preventDefault: jest.fn(), + touches: [{ clientX: 0, clientY: 0 }], + }) + expect(wrapper.vm.pointerOriginatedInColorSpace).toBe(true) - await colorSpace.trigger('touchstart', { - preventDefault: jest.fn(), - touches: [{ clientX: 0, clientY: 0 }], - }) - await colorSpace.trigger('touchmove', { - preventDefault: jest.fn(), - touches: [{ clientX: 0, clientY: 0 }], + document.dispatchEvent(new Event('touchend')) + expect(wrapper.vm.pointerOriginatedInColorSpace).toBe(false) }) - emittedColorChangeEvents = wrapper.emitted()['color-change'] - expect(emittedColorChangeEvents.length).toBe(5) - - // Remove test HTML injected via the `attachTo` option during mount. - wrapper.unmount() - }) + test('can initiate moving the color space thumb with a mouse', async () => { + const clientX = 0 + const clientY = 0 + const mouseMoveEvent = { + buttons: 1, + preventDefault: jest.fn(), + clientX, + clientY, + } - test('can not move the color space thumb with the wrong key', () => { - const keydownEvent = { - key: 'a', - preventDefault: jest.fn(), - } + const wrapper = shallowMount(ColorPicker, { attachTo: injectTestDiv(), props: { color: '#f80c' } }) - const wrapper = shallowMount(ColorPicker) + let emittedColorChangeEvents = wrapper.emitted()['color-change'] + expect(emittedColorChangeEvents.length).toBe(1) - wrapper.vm.moveThumbWithArrows(keydownEvent) + const colorSpace = wrapper.find('.vacp-color-space') + await colorSpace.trigger('mousedown') + await colorSpace.trigger('mousemove', mouseMoveEvent) - expect(keydownEvent.preventDefault).not.toHaveBeenCalled() - }) + emittedColorChangeEvents = wrapper.emitted()['color-change'] + expect(emittedColorChangeEvents.length).toBe(2) - test.each([ - ['ArrowDown', false, 'v', 0.99], - ['ArrowDown', true, 'v', 0.9], - ['ArrowUp', false, 'v', 1], - ['ArrowUp', true, 'v', 1], - ['ArrowRight', false, 's', 1], - ['ArrowRight', true, 's', 1], - ['ArrowLeft', false, 's', 0.99], - ['ArrowLeft', true, 's', 0.9], - ])('can move the color space thumb with the %s key (holding shift: %s)', (key, shiftKey, channel, expectedColorValue) => { - const keydownEvent = { - key, - shiftKey, - preventDefault: jest.fn(), - } - - const wrapper = shallowMount(ColorPicker, { - props: { - color: 'rgb(128, 0, 255)', - }, + // Remove test HTML injected via the `attachTo` option during mount. + wrapper.unmount() }) - expect(keydownEvent.preventDefault).not.toHaveBeenCalled() - wrapper.vm.moveThumbWithArrows(keydownEvent) + test('can initiate moving the color space thumb with a touch-based device', async () => { + const wrapper = shallowMount(ColorPicker, { attachTo: injectTestDiv(), props: { color: '#f80c' } }) + + let emittedColorChangeEvents = wrapper.emitted()['color-change'] + expect(emittedColorChangeEvents.length).toBe(1) + + const colorSpace = wrapper.find('.vacp-color-space') + await colorSpace.trigger('touchstart', { + preventDefault: jest.fn(), + touches: [{ clientX: 0, clientY: 0 }], + }) + await colorSpace.trigger('touchmove', { + preventDefault: jest.fn(), + touches: [{ clientX: 0, clientY: 0 }], + }) + + emittedColorChangeEvents = wrapper.emitted()['color-change'] + expect(emittedColorChangeEvents.length).toBe(3) + + await colorSpace.trigger('touchstart', { + preventDefault: jest.fn(), + touches: [{ clientX: 0, clientY: 0 }], + }) + await colorSpace.trigger('touchmove', { + preventDefault: jest.fn(), + touches: [{ clientX: 0, clientY: 0 }], + }) + + emittedColorChangeEvents = wrapper.emitted()['color-change'] + expect(emittedColorChangeEvents.length).toBe(5) + + // Remove test HTML injected via the `attachTo` option during mount. + wrapper.unmount() + }) - expect(keydownEvent.preventDefault).toHaveBeenCalled() - const emittedColorChangeEvents = wrapper.emitted()['color-change'] - // @ts-ignore because `unknown` is clearly not a correct type for emitted records. - const emittedHsvColor = emittedColorChangeEvents[emittedColorChangeEvents.length - 1][0].colors.hsv - expect(emittedHsvColor[channel]).toEqual(expectedColorValue) - }) + test('can not move the color space thumb with the wrong key', () => { + const keydownEvent = { + key: 'a', + preventDefault: jest.fn(), + } - test('can not increment/decrement in big steps without holding down shift', () => { - const keydownEvent = { - key: 'ArrowRight', - shiftKey: false, - } + const wrapper = shallowMount(ColorPicker) - const wrapper = shallowMount(ColorPicker) - const hueRangeInput = wrapper.find(`#${wrapper.vm.id}-hue-slider`) - const hueRangeInputElement = /** @type {HTMLInputElement} */ (hueRangeInput.element) - const originalInputValue = hueRangeInputElement.value + wrapper.vm.moveThumbWithArrows(keydownEvent) - wrapper.vm.changeInputValue(keydownEvent) + expect(keydownEvent.preventDefault).not.toHaveBeenCalled() + }) - expect(hueRangeInputElement.value).toBe(originalInputValue) + test.each([ + ['ArrowDown', false, 'v', 0.99], + ['ArrowDown', true, 'v', 0.9], + ['ArrowUp', false, 'v', 1], + ['ArrowUp', true, 'v', 1], + ['ArrowRight', false, 's', 1], + ['ArrowRight', true, 's', 1], + ['ArrowLeft', false, 's', 0.99], + ['ArrowLeft', true, 's', 0.9], + ])('can move the color space thumb with the %s key (holding shift: %s)', (key, shiftKey, channel, expectedColorValue) => { + const keydownEvent = { + key, + shiftKey, + preventDefault: jest.fn(), + } + + const wrapper = shallowMount(ColorPicker, { + props: { + color: 'rgb(128, 0, 255)', + }, + }) + expect(keydownEvent.preventDefault).not.toHaveBeenCalled() + + wrapper.vm.moveThumbWithArrows(keydownEvent) + + expect(keydownEvent.preventDefault).toHaveBeenCalled() + const emittedColorChangeEvents = wrapper.emitted()['color-change'] + // @ts-ignore because `unknown` is clearly not a correct type for emitted records. + const emittedHsvColor = emittedColorChangeEvents[emittedColorChangeEvents.length - 1][0].colors.hsv + expect(emittedHsvColor[channel]).toEqual(expectedColorValue) + }) }) - test.each([ - ['decrement', 1, 'ArrowDown', '1'], - ['decrement', 3, 'ArrowDown', '1'], - ['decrement', 1, 'ArrowLeft', '1'], - ['increment', 1, 'ArrowUp', '9'], - ['increment', 1, 'ArrowRight', '9'], - ['increment', 3, 'ArrowRight', '27'], - ])('can %s range inputs %dx in big steps with %s', (_, numberOfPresses, key, expectedValue) => { - const wrapper = shallowMount(ColorPicker) - const hueRangeInput = wrapper.find(`#${wrapper.vm.id}-hue-slider`) - const hueRangeInputElement = /** @type {HTMLInputElement} */ (hueRangeInput.element) - const keydownEvent = { - key, - shiftKey: true, - currentTarget: hueRangeInputElement, - } + describe('hue & alpha range inputs', () => { + test('can not increment/decrement in big steps without holding down shift', () => { + const keydownEvent = { + key: 'ArrowRight', + shiftKey: false, + } - expect(hueRangeInput.exists()).toBe(true) + const wrapper = shallowMount(ColorPicker) + const hueRangeInput = wrapper.find(`#${wrapper.vm.id}-hue-slider`) + const hueRangeInputElement = /** @type {HTMLInputElement} */ (hueRangeInput.element) + const originalInputValue = hueRangeInputElement.value - while (numberOfPresses--) { wrapper.vm.changeInputValue(keydownEvent) - } - expect(hueRangeInputElement.value).toBe(expectedValue) - }) + expect(hueRangeInputElement.value).toBe(originalInputValue) + }) - test.each([ - ['rgb', 'rgb(255 255 255 / 1)'], - ['hsl', 'hsl(0 0% 100% / 1)'], - ['hwb', 'hwb(0 100% 0% / 1)'], - ['hex', '#ffffffff'], - ])('copy button copies %s format as %s', (format, cssColor) => { - jest.spyOn(copyToClipboardModule, 'copyToClipboard').mockImplementation(jest.fn()) + test.each([ + ['decrement', 1, 'ArrowDown', '1'], + ['decrement', 3, 'ArrowDown', '1'], + ['decrement', 1, 'ArrowLeft', '1'], + ['increment', 1, 'ArrowUp', '9'], + ['increment', 1, 'ArrowRight', '9'], + ['increment', 3, 'ArrowRight', '27'], + ])('can %s range inputs %dx in big steps with %s', (_, numberOfPresses, key, expectedValue) => { + const wrapper = shallowMount(ColorPicker) + const hueRangeInput = wrapper.find(`#${wrapper.vm.id}-hue-slider`) + const hueRangeInputElement = /** @type {HTMLInputElement} */ (hueRangeInput.element) + const keydownEvent = { + key, + shiftKey: true, + currentTarget: hueRangeInputElement, + } + + expect(hueRangeInput.exists()).toBe(true) + + while (numberOfPresses--) { + wrapper.vm.changeInputValue(keydownEvent) + } + + expect(hueRangeInputElement.value).toBe(expectedValue) + }) - const wrapper = shallowMount(ColorPicker) + test('hue slider updates internal colors', async () => { + const hueAngle = 30 + const expectedHueValue = hueAngle / 360 - wrapper.vm.activeFormat = format - wrapper.vm.copyColor() - expect(copyToClipboardModule.copyToClipboard).toHaveBeenCalledWith(cssColor) - }) + const wrapper = shallowMount(ColorPicker) + const hueRangeInput = wrapper.find(`#${wrapper.vm.id}-hue-slider`) + const hueRangeInputElement = /** @type {HTMLInputElement} */ (hueRangeInput.element) + hueRangeInputElement.value = String(hueAngle) + const hueInputEvent = { currentTarget: hueRangeInputElement } - test('hue slider updates internal colors', async () => { - const hueAngle = 30 - const expectedHueValue = hueAngle / 360 + await hueRangeInput.trigger('input', hueInputEvent) - const wrapper = shallowMount(ColorPicker) - const hueRangeInput = wrapper.find(`#${wrapper.vm.id}-hue-slider`) - const hueRangeInputElement = /** @type {HTMLInputElement} */ (hueRangeInput.element) - hueRangeInputElement.value = String(hueAngle) - const hueInputEvent = { currentTarget: hueRangeInputElement } + let emittedColorChangeEvents = wrapper.emitted()['color-change'] + expect(emittedColorChangeEvents.length).toBe(1) - await hueRangeInput.trigger('input', hueInputEvent) + // @ts-ignore because `unknown` is clearly not a correct type for emitted records. + let emittedHsvColor = emittedColorChangeEvents[emittedColorChangeEvents.length - 1][0].colors.hsv + expect(emittedHsvColor.h).toEqual(expectedHueValue) - let emittedColorChangeEvents = wrapper.emitted()['color-change'] - expect(emittedColorChangeEvents.length).toBe(1) + const alpha = 90 + const expectedAlphaValue = alpha / 100 + + const alphaRangeInput = wrapper.find(`#${wrapper.vm.id}-alpha-slider`) + const alphaRangeInputElement = /** @type {HTMLInputElement} */ (alphaRangeInput.element) + alphaRangeInputElement.value = String(alpha) + const alphaInputEvent = { currentTarget: alphaRangeInputElement } - // @ts-ignore because `unknown` is clearly not a correct type for emitted records. - let emittedHsvColor = emittedColorChangeEvents[emittedColorChangeEvents.length - 1][0].colors.hsv - expect(emittedHsvColor.h).toEqual(expectedHueValue) + await alphaRangeInput.trigger('input', alphaInputEvent) - const alpha = 90 - const expectedAlphaValue = alpha / 100 + emittedColorChangeEvents = wrapper.emitted()['color-change'] + expect(emittedColorChangeEvents.length).toBe(2) - const alphaRangeInput = wrapper.find(`#${wrapper.vm.id}-alpha-slider`) - const alphaRangeInputElement = /** @type {HTMLInputElement} */ (alphaRangeInput.element) - alphaRangeInputElement.value = String(alpha) - const alphaInputEvent = { currentTarget: alphaRangeInputElement } + // @ts-ignore because `unknown` is clearly not a correct type for emitted records. + emittedHsvColor = emittedColorChangeEvents[emittedColorChangeEvents.length - 1][0].colors.hsv + expect(emittedHsvColor.a).toEqual(expectedAlphaValue) + }) + }) - await alphaRangeInput.trigger('input', alphaInputEvent) + describe('copy button', () => { + test.each([ + ['rgb', 'rgb(255 255 255 / 1)'], + ['hsl', 'hsl(0 0% 100% / 1)'], + ['hwb', 'hwb(0 100% 0% / 1)'], + ['hex', '#ffffffff'], + ])('copy button copies %s format as %s', (format, cssColor) => { + jest.spyOn(copyToClipboardModule, 'copyToClipboard').mockImplementation(jest.fn()) - emittedColorChangeEvents = wrapper.emitted()['color-change'] - expect(emittedColorChangeEvents.length).toBe(2) + const wrapper = shallowMount(ColorPicker) - // @ts-ignore because `unknown` is clearly not a correct type for emitted records. - emittedHsvColor = emittedColorChangeEvents[emittedColorChangeEvents.length - 1][0].colors.hsv - expect(emittedHsvColor.a).toEqual(expectedAlphaValue) + wrapper.vm.activeFormat = format + wrapper.vm.copyColor() + expect(copyToClipboardModule.copyToClipboard).toHaveBeenCalledWith(cssColor) + }) }) - test.each([ - ['rgb', 'r', '127.'], - ['hsl', 's', 'a'], - ['hwb', 'b', '25.%'], - ])('updating a %s color input with an invalid value does not update the internal color data', async (format, channel, channelValue) => { - const wrapper = shallowMount(ColorPicker) + describe('switch format button', () => { + test('clicking switch format button cycles through active formats correctly', async () => { + const wrapper = shallowMount(ColorPicker) + const formatSwitchButton = wrapper.find('.vacp-format-switch-button') - jest.resetAllMocks() + expect(wrapper.find('#color-picker-color-hsl-l').exists()).toBe(true) - wrapper.vm.activeFormat = format - await flushPromises() + await formatSwitchButton.trigger('click') + expect(wrapper.find('#color-picker-color-hwb-w').exists()).toBe(true) - const inputSelector = `#${wrapper.vm.id}-color-${format}-${channel}` - const inputElement = /** @type {HTMLInputElement} */ (wrapper.find(inputSelector).element) - inputElement.value = channelValue - const inputEvent = { target: inputElement } + await formatSwitchButton.trigger('click') + expect(wrapper.find('#color-picker-color-rgb-r').exists()).toBe(true) - wrapper.vm.updateColorValue(inputEvent, format, channel) + await formatSwitchButton.trigger('click') + expect(wrapper.find('#color-picker-color-hex').exists()).toBe(true) - const emittedColorChangeEvents = wrapper.emitted()['color-change'] - expect(emittedColorChangeEvents).toBe(undefined) + await formatSwitchButton.trigger('click') + expect(wrapper.find('#color-picker-color-hsl-l').exists()).toBe(true) + }) }) - test.each([ - ['abc'], - ['25%'], - ])('updating a hex color input with an invalid value does not update the internal color data', async (invalidHexColorString) => { - const wrapper = shallowMount(ColorPicker) + describe('color value inputs', () => { + test.each([ + ['rgb', 'r', '127.'], + ['hsl', 's', 'a'], + ['hwb', 'b', '25.%'], + ])('updating a %s color input with an invalid value does not update the internal color data', async (format, channel, channelValue) => { + const wrapper = shallowMount(ColorPicker) - jest.resetAllMocks() + jest.resetAllMocks() - wrapper.vm.activeFormat = 'hex' - await flushPromises() + wrapper.vm.activeFormat = format + await flushPromises() - const inputSelector = `#${wrapper.vm.id}-color-hex` - const inputElement = /** @type {HTMLInputElement} */ (wrapper.find(inputSelector).element) - inputElement.value = invalidHexColorString - const inputEvent = { target: inputElement } + const inputSelector = `#${wrapper.vm.id}-color-${format}-${channel}` + const inputElement = /** @type {HTMLInputElement} */ (wrapper.find(inputSelector).element) + inputElement.value = channelValue + const inputEvent = { target: inputElement } - wrapper.vm.updateHexColorValue(inputEvent) + wrapper.vm.updateColorValue(inputEvent, format, channel) - const emittedColorChangeEvents = wrapper.emitted()['color-change'] - expect(emittedColorChangeEvents).toBe(undefined) - }) + const emittedColorChangeEvents = wrapper.emitted()['color-change'] + expect(emittedColorChangeEvents).toBe(undefined) + }) - test.each([ - ['rgb', 'r', '127.5'], - ['hsl', 's', '75%'], - ['hwb', 'b', '25.5%'], - ])('updating a %s color input with a valid value updates the internal color data', async (format, channel, channelValue) => { - const wrapper = shallowMount(ColorPicker) + test.each([ + ['abc'], + ['25%'], + ])('updating a hex color input with an invalid value does not update the internal color data', async (invalidHexColorString) => { + const wrapper = shallowMount(ColorPicker) - jest.resetAllMocks() + jest.resetAllMocks() - wrapper.vm.activeFormat = format - await flushPromises() + wrapper.vm.activeFormat = 'hex' + await flushPromises() - const inputSelector = `#${wrapper.vm.id}-color-${format}-${channel}` - const inputElement = /** @type {HTMLInputElement} */ (wrapper.find(inputSelector).element) - inputElement.value = channelValue - const inputEvent = { target: inputElement } + const inputSelector = `#${wrapper.vm.id}-color-hex` + const inputElement = /** @type {HTMLInputElement} */ (wrapper.find(inputSelector).element) + inputElement.value = invalidHexColorString + const inputEvent = { target: inputElement } - wrapper.vm.updateColorValue(inputEvent, format, channel) + wrapper.vm.updateHexColorValue(inputEvent) - const emittedColorChangeEvents = wrapper.emitted()['color-change'] - expect(emittedColorChangeEvents.length).toBe(1) - }) + const emittedColorChangeEvents = wrapper.emitted()['color-change'] + expect(emittedColorChangeEvents).toBe(undefined) + }) - test.each([ - ['#ff8800cc'], - ])('updating a %s color input with a valid value updates the internal color data', async (channelValue) => { - const wrapper = shallowMount(ColorPicker) + test.each([ + ['rgb', 'r', '127.5'], + ['hsl', 's', '75%'], + ['hwb', 'b', '25.5%'], + ])('updating a %s color input with a valid value updates the internal color data', async (format, channel, channelValue) => { + const wrapper = shallowMount(ColorPicker) - jest.resetAllMocks() + jest.resetAllMocks() - wrapper.vm.activeFormat = 'hex' - await flushPromises() + wrapper.vm.activeFormat = format + await flushPromises() - const inputSelector = `#${wrapper.vm.id}-color-hex` - const inputElement = /** @type {HTMLInputElement} */ (wrapper.find(inputSelector).element) - inputElement.value = channelValue - const inputEvent = { target: inputElement } + const inputSelector = `#${wrapper.vm.id}-color-${format}-${channel}` + const inputElement = /** @type {HTMLInputElement} */ (wrapper.find(inputSelector).element) + inputElement.value = channelValue + const inputEvent = { target: inputElement } - wrapper.vm.updateHexColorValue(inputEvent) + wrapper.vm.updateColorValue(inputEvent, format, channel) - const emittedColorChangeEvents = wrapper.emitted()['color-change'] - expect(emittedColorChangeEvents.length).toBe(1) - }) + const emittedColorChangeEvents = wrapper.emitted()['color-change'] + expect(emittedColorChangeEvents.length).toBe(1) + }) - test('clicking switch format button cycles through active formats correctly', async () => { - const wrapper = shallowMount(ColorPicker) - const formatSwitchButton = wrapper.find('.vacp-format-switch-button') + test.each([ + ['#ff8800cc'], + ])('updating a %s color input with a valid value updates the internal color data', async (channelValue) => { + const wrapper = shallowMount(ColorPicker) - expect(wrapper.find('#color-picker-color-hsl-l').exists()).toBe(true) + jest.resetAllMocks() - await formatSwitchButton.trigger('click') - expect(wrapper.find('#color-picker-color-hwb-w').exists()).toBe(true) + wrapper.vm.activeFormat = 'hex' + await flushPromises() - await formatSwitchButton.trigger('click') - expect(wrapper.find('#color-picker-color-rgb-r').exists()).toBe(true) + const inputSelector = `#${wrapper.vm.id}-color-hex` + const inputElement = /** @type {HTMLInputElement} */ (wrapper.find(inputSelector).element) + inputElement.value = channelValue + const inputEvent = { target: inputElement } - await formatSwitchButton.trigger('click') - expect(wrapper.find('#color-picker-color-hex').exists()).toBe(true) + wrapper.vm.updateHexColorValue(inputEvent) - await formatSwitchButton.trigger('click') - expect(wrapper.find('#color-picker-color-hsl-l').exists()).toBe(true) + const emittedColorChangeEvents = wrapper.emitted()['color-change'] + expect(emittedColorChangeEvents.length).toBe(1) + }) + }) + + describe('color-change event', () => { + test.each([ + [ + { color: '#ff99aacc', defaultFormat: 'hsl', alphaChannel: 'show' }, + { + cssColor: 'hsl(350 100% 80% / 0.8)', + colors: { + hex: '#ff99aacc', + hsl: { h: 0.9722222222222222, s: 1, l: 0.8, a: 0.8 }, + hsv: { h: 0.9722222222222222, s: 0.4, v: 1, a: 0.8 }, + hwb: { h: 0.9722222222222222, w: 0.6, b: 0, a: 0.8 }, + rgb: { r: 1, g: 0.6, b: 0.6666666666666666, a: 0.8 }, + }, + }, + ], + [ + { color: '#f9ac', defaultFormat: 'hsl', alphaChannel: 'show' }, + { + cssColor: 'hsl(350 100% 80% / 0.8)', + colors: { + hex: '#f9ac', + hsl: { h: 0.9722222222222222, s: 1, l: 0.8, a: 0.8 }, + hsv: { h: 0.9722222222222222, s: 0.4, v: 1, a: 0.8 }, + hwb: { h: 0.9722222222222222, w: 0.6, b: 0, a: 0.8 }, + rgb: { r: 1, g: 0.6, b: 0.6666666666666666, a: 0.8 }, + }, + }, + ], + [ + { color: '#ff99aacc', defaultFormat: 'hex', alphaChannel: 'show' }, + { + cssColor: '#ff99aacc', + colors: { + hex: '#ff99aacc', + hsl: { h: 0.9722222222222222, s: 1, l: 0.8, a: 0.8 }, + hsv: { h: 0.9722222222222222, s: 0.4, v: 1, a: 0.8 }, + hwb: { h: 0.9722222222222222, w: 0.6, b: 0, a: 0.8 }, + rgb: { r: 1, g: 0.6, b: 0.6666666666666666, a: 0.8 }, + }, + }, + ], + [ + { color: '#f9ac', defaultFormat: 'hex', alphaChannel: 'show' }, + { + cssColor: '#f9ac', + colors: { + hex: '#f9ac', + hsl: { h: 0.9722222222222222, s: 1, l: 0.8, a: 0.8 }, + hsv: { h: 0.9722222222222222, s: 0.4, v: 1, a: 0.8 }, + hwb: { h: 0.9722222222222222, w: 0.6, b: 0, a: 0.8 }, + rgb: { r: 1, g: 0.6, b: 0.6666666666666666, a: 0.8 }, + }, + }, + ], + [ + { color: '#ff99aacc', defaultFormat: 'hsl', alphaChannel: 'hide' }, + { + cssColor: 'hsl(350 100% 80%)', + colors: { + hex: '#ff99aaff', + hsl: { h: 0.9722222222222222, s: 1, l: 0.8, a: 1 }, + hsv: { h: 0.9722222222222222, s: 0.4, v: 1, a: 1 }, + hwb: { h: 0.9722222222222222, w: 0.6, b: 0, a: 1 }, + rgb: { r: 1, g: 0.6, b: 0.6666666666666666, a: 1 }, + }, + }, + ], + [ + { color: '#f9ac', defaultFormat: 'hsl', alphaChannel: 'hide' }, + { + cssColor: 'hsl(350 100% 80%)', + colors: { + hex: '#f9af', + hsl: { h: 0.9722222222222222, s: 1, l: 0.8, a: 1 }, + hsv: { h: 0.9722222222222222, s: 0.4, v: 1, a: 1 }, + hwb: { h: 0.9722222222222222, w: 0.6, b: 0, a: 1 }, + rgb: { r: 1, g: 0.6, b: 0.6666666666666666, a: 1 }, + }, + }, + ], + [ + { color: '#ff99aacc', defaultFormat: 'hex', alphaChannel: 'hide' }, + { + cssColor: '#ff99aa', + colors: { + hex: '#ff99aaff', + hsl: { h: 0.9722222222222222, s: 1, l: 0.8, a: 1 }, + hsv: { h: 0.9722222222222222, s: 0.4, v: 1, a: 1 }, + hwb: { h: 0.9722222222222222, w: 0.6, b: 0, a: 1 }, + rgb: { r: 1, g: 0.6, b: 0.6666666666666666, a: 1 }, + }, + }, + ], + [ + { color: '#f9ac', defaultFormat: 'hex', alphaChannel: 'hide' }, + { + cssColor: '#f9a', + colors: { + hex: '#f9af', + hsl: { h: 0.9722222222222222, s: 1, l: 0.8, a: 1 }, + hsv: { h: 0.9722222222222222, s: 0.4, v: 1, a: 1 }, + hwb: { h: 0.9722222222222222, w: 0.6, b: 0, a: 1 }, + rgb: { r: 1, g: 0.6, b: 0.6666666666666666, a: 1 }, + }, + }, + ], + ])('emits correct data', async (props, expectedData) => { + const wrapper = shallowMount(ColorPicker, { props }) + + await wrapper.setProps({ color: props.color }) + + const emittedColorChangeEvents = wrapper.emitted()['color-change'] + // @ts-ignore because `unknown` is clearly not a correct type for emitted records. + const colorChangeData = emittedColorChangeEvents[emittedColorChangeEvents.length - 1][0] + expect(colorChangeData).toEqual(expectedData) + }) }) }) diff --git a/src/ColorPicker.vue b/src/ColorPicker.vue index 3a7a99f..b774d31 100644 --- a/src/ColorPicker.vue +++ b/src/ColorPicker.vue @@ -19,47 +19,50 @@ /> - - + + Alpha + + + + +