diff --git a/packages/color-slider/README.md b/packages/color-slider/README.md new file mode 100644 index 00000000000..c38cd8a87d9 --- /dev/null +++ b/packages/color-slider/README.md @@ -0,0 +1,58 @@ +## Description + +An `` lets users visually change an individual channel of a color. The background of the `` is a visual representation of the range of values a user can select from. This can represent color properties such as hues, color channel values (such as RGB or CMYK levels), or opacity. + +### Usage + +[![See it on NPM!](https://img.shields.io/npm/v/@spectrum-web-components/color-slider?style=for-the-badge)](https://www.npmjs.com/package/@spectrum-web-components/color-slider) +[![How big is this package in your project?](https://img.shields.io/bundlephobia/minzip/@spectrum-web-components/color-slider?style=for-the-badge)](https://bundlephobia.com/result?p=@spectrum-web-components/color-slider) + +``` +yarn add @spectrum-web-components/color-slider +``` + +Import the side effectful registration of `` via: + +``` +import '@spectrum-web-components/color-slider/sp-color-slider.js'; +``` + +When looking to leverage the `ColorSlider` base class as a type and/or for extension purposes, do so via: + +``` +import { ColorSlider } from '@spectrum-web-components/color-slider'; +``` + +## Color Formatting + +When using the color elements, use `el.color` to access the `color` property, which should manage itself in the colour format supplied. If you supply a color in `rgb()` format, `el.color` should return the color in `rgb()` format, as well. + +The current color formats supported are as follows: + +- Hex3, Hex4, Hex6, Hex8 +- HSV, HSVA +- HSL, HSLA +- RGB, RGBA +- Strings (eg "red", "blue") +- TinyColor + +**Please note for the following formats: HSV, HSVA, HSL, HSLA** +When setting a color's lightness or value to 100%, the hue and saturation value are not preserved. This is detailed in the [TinyColor documentation](https://www.npmjs.com/package/@ctrl/tinycolor). Currently, the Spectrum Web Components has a workaround to support the preservation of the hue. + +## Default + +```html + +``` + +### Vertical + +```html + +``` + +### Disabled + +```html + +``` diff --git a/packages/color-slider/package.json b/packages/color-slider/package.json new file mode 100644 index 00000000000..348f587a0fb --- /dev/null +++ b/packages/color-slider/package.json @@ -0,0 +1,59 @@ +{ + "name": "@spectrum-web-components/color-slider", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/adobe/spectrum-web-components.git", + "directory": "packages/color-slider" + }, + "bugs": { + "url": "https://github.com/adobe/spectrum-web-components/issues" + }, + "homepage": "https://adobe.github.io/spectrum-web-components/components/color-slider", + "keywords": [ + "spectrum css", + "web components", + "lit-element", + "lit-html" + ], + "version": "0.0.1", + "description": "", + "main": "src/index.js", + "module": "src/index.js", + "type": "module", + "exports": { + "./src/": "./src/", + "./custom-elements.json": "./custom-elements.json", + "./package.json": "./package.json", + "./sp-color-slider": "./sp-color-slider.js", + "./sp-color-slider.js": "./sp-color-slider.js" + }, + "files": [ + "custom-elements.json", + "*.d.ts", + "*.js", + "*.js.map", + "/src/" + ], + "sideEffects": [ + "./sp-*.js", + "./sp-*.ts" + ], + "scripts": { + "test": "echo \"Error: run tests from mono-repo root.\" && exit 1" + }, + "author": "", + "license": "Apache-2.0", + "devDependencies": { + "@spectrum-css/colorslider": "^1.0.0-beta.4" + }, + "dependencies": { + "@ctrl/tinycolor": "^3.3.3", + "@spectrum-web-components/base": "^0.3.0", + "@spectrum-web-components/color-handle": "^0.0.1", + "@spectrum-web-components/shared": "^0.9.0", + "tslib": "^2.0.0" + } +} diff --git a/packages/color-slider/sp-color-slider.ts b/packages/color-slider/sp-color-slider.ts new file mode 100644 index 00000000000..8a4f7d02c9c --- /dev/null +++ b/packages/color-slider/sp-color-slider.ts @@ -0,0 +1,21 @@ +/* +Copyright 2020 Adobe. All rights reserved. +This file is licensed 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 REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { ColorSlider } from './src/ColorSlider.js'; + +customElements.define('sp-color-slider', ColorSlider); + +declare global { + interface HTMLElementTagNameMap { + 'sp-color-slider': ColorSlider; + } +} diff --git a/packages/color-slider/src/ColorSlider.ts b/packages/color-slider/src/ColorSlider.ts new file mode 100644 index 00000000000..655cbfae9ea --- /dev/null +++ b/packages/color-slider/src/ColorSlider.ts @@ -0,0 +1,345 @@ +/* +Copyright 2020 Adobe. All rights reserved. +This file is licensed 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 REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { + html, + CSSResultArray, + TemplateResult, + property, + query, + streamingListener, +} from '@spectrum-web-components/base'; + +import { Focusable } from '@spectrum-web-components/shared/src/focusable.js'; +import '@spectrum-web-components/color-handle/sp-color-handle.js'; +import styles from './color-slider.css.js'; +import { + ColorHandle, + ColorValue, +} from '@spectrum-web-components/color-handle/src/ColorHandle'; +import { TinyColor } from '@ctrl/tinycolor'; + +/** + * @element sp-color-slider + */ +export class ColorSlider extends Focusable { + public static get styles(): CSSResultArray { + return [styles]; + } + + @property({ type: Boolean, reflect: true }) + public disabled = false; + + @property({ type: Boolean, reflect: true }) + public focused = false; + + @query('.handle') + private handle!: ColorHandle; + + @property({ type: Boolean, reflect: true }) + public vertical = false; + + @property({ type: Number }) + public get value(): number { + return this._value; + } + + public set value(hue: number) { + const value = Math.min(360, Math.max(0, hue)); + if (value === this.value) { + return; + } + const oldValue = this.value; + const { s, v } = this._color.toHsv(); + this._color = new TinyColor({ h: value, s, v }); + this._value = value; + + if (value !== this.sliderHandlePosition) { + this.sliderHandlePosition = 100 * (value / 360); + } + + this.requestUpdate('value', oldValue); + } + + private _value = 0; + + @property({ type: Number, reflect: true }) + public sliderHandlePosition = 0; + + @property({ type: String }) + public get color(): ColorValue { + switch (this._format.format) { + case 'rgb': + return this._format.isString + ? this._color.toRgbString() + : this._color.toRgb(); + case 'prgb': + return this._format.isString + ? this._color.toPercentageRgbString() + : this._color.toPercentageRgb(); + case 'hex': + case 'hex3': + case 'hex4': + case 'hex6': + return this._format.isString + ? this._color.toHexString() + : this._color.toHex(); + case 'hex8': + return this._format.isString + ? this._color.toHex8String() + : this._color.toHex8(); + case 'name': + return this._color.toName() || this._color.toRgbString(); + case 'hsl': + return this._format.isString + ? this._color.toHslString() + : this._color.toHsl(); + case 'hsv': + return this._format.isString + ? this._color.toHsvString() + : this._color.toHsv(); + default: + return 'No color format applied.'; + } + } + + public set color(color: ColorValue) { + if (color === this.color) { + return; + } + const oldValue = this._color; + this._color = new TinyColor(color); + const format = this._color.format; + let isString = typeof color === 'string' || color instanceof String; + + if (format.startsWith('hex')) { + isString = (color as string).startsWith('#'); + } + + this._format = { + format, + isString, + }; + + if (isString && format.startsWith('hs')) { + const hueExp = /^hs[v|va|l|la]\((\d{1,3})/; + const values = hueExp.exec(color as string); + + if (values !== null) { + const [, h] = values; + this.value = Number(h); + } + } else if (!isString && format.startsWith('hs')) { + const colorInput = this._color.originalInput; + const colorValues = Object.values(colorInput); + this.value = colorValues[0]; + + // The below code line causes some tests to fail + //this.value = parseFloat((color as HSV).h.toString()); + } else { + const { h } = this._color.toHsv(); + this.value = h; + } + this._previousColor = oldValue; + this.requestUpdate('color', oldValue); + } + + private _color = new TinyColor({ h: 0, s: 1, v: 1 }); + + private _previousColor = new TinyColor({ h: 0, s: 1, v: 1 }); + + private _format: { format: string; isString: boolean } = { + format: '', + isString: false, + }; + + @property({ type: Number }) + public step = 1; + + private get altered(): number { + return this._altered; + } + + private set altered(altered: number) { + this._altered = altered; + this.step = Math.max(1, this.altered * 10); + } + + private _altered = 0; + + private altKeys = new Set(); + + @query('input') + public input!: HTMLInputElement; + + public get focusElement(): HTMLInputElement { + return this.input; + } + + private handleKeydown(event: KeyboardEvent): void { + event.preventDefault(); + const { key } = event; + if (['Shift', 'Meta', 'Control', 'Alt'].includes(key)) { + this.altKeys.add(key); + this.altered = this.altKeys.size; + } + let delta = 0; + switch (key) { + case 'ArrowUp': + delta = this.step; + break; + case 'ArrowDown': + delta = -this.step; + break; + case 'ArrowLeft': + delta = this.step * (this.isLTR ? -1 : 1); + break; + case 'ArrowRight': + delta = this.step * (this.isLTR ? 1 : -1); + break; + } + this.sliderHandlePosition = Math.min( + 100, + Math.max(0, this.sliderHandlePosition + delta) + ); + } + + private handleKeyup(event: KeyboardEvent): void { + event.preventDefault(); + const { key } = event; + if (['Shift', 'Meta', 'Control', 'Alt'].includes(key)) { + this.altKeys.delete(key); + this.altered = this.altKeys.size; + } + } + + private handleFocus(): void { + this.focused = true; + } + + private handleBlur(): void { + this.focused = false; + } + + private boundingClientRect!: DOMRect; + + private handlePointerdown(event: PointerEvent): void { + this._previousColor = this._color.clone(); + this.boundingClientRect = this.getBoundingClientRect(); + (event.target as HTMLElement).setPointerCapture(event.pointerId); + } + + private handlePointermove(event: PointerEvent): void { + this.sliderHandlePosition = this.calculateHandlePosition(event); + this.value = 360 * (this.sliderHandlePosition / 100); + + //this.color = `hsl(${this.value}, 100%, 50%)`; + this._color = new TinyColor({ h: this.value, s: '100%', l: '50%' }); + + this.dispatchEvent( + new Event('input', { + bubbles: true, + composed: true, + cancelable: true, + }) + ); + } + + private handlePointerup(event: PointerEvent): void { + // Retain focus on input element after mouse up to enable keyboard interactions + (event.target as HTMLElement).releasePointerCapture(event.pointerId); + + const applyDefault = this.dispatchEvent( + new Event('change', { + bubbles: true, + composed: true, + cancelable: true, + }) + ); + if (!applyDefault) { + this._color = this._previousColor; + } + } + + /** + * Returns the value under the cursor + * @param: PointerEvent on slider + * @return: Slider value that correlates to the position under the pointer + */ + private calculateHandlePosition(event: PointerEvent): number { + /* c8 ignore next 3 */ + if (!this.boundingClientRect) { + return this.sliderHandlePosition; + } + const rect = this.boundingClientRect; + const minOffset = this.vertical ? rect.top : rect.left; + const offset = this.vertical ? event.clientY : event.clientX; + const size = this.vertical ? rect.height : rect.width; + + const percent = Math.max(0, Math.min(1, (offset - minOffset) / size)); + const sliderHandlePosition = 100 * percent; + + return this.isLTR ? sliderHandlePosition : 100 - sliderHandlePosition; + } + + private handleGradientPointerdown(event: PointerEvent): void { + event.stopPropagation(); + event.preventDefault(); + this.handle.dispatchEvent(new PointerEvent('pointerdown', event)); + this.handlePointermove(event); + } + + protected render(): TemplateResult { + return html` + + + + `; + } +} diff --git a/packages/color-slider/src/color-slider.css b/packages/color-slider/src/color-slider.css new file mode 100644 index 00000000000..09aabd46c26 --- /dev/null +++ b/packages/color-slider/src/color-slider.css @@ -0,0 +1,28 @@ +/* +Copyright 2020 Adobe. All rights reserved. +This file is licensed 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 REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +@import './spectrum-color-slider.css'; + +:host { + --sp-color-slider-gradient-fallback: rgb(255, 0, 0) 0%, rgb(255, 255, 0) 17%, + rgb(0, 255, 0) 33%, rgb(0, 255, 255) 50%, rgb(0, 0, 255) 67%, + rgb(255, 0, 255) 83%, rgb(255, 0, 0) 100%; +} + +.gradient { + overflow: hidden; +} + +::slotted(*) { + width: 100%; + height: 100%; +} diff --git a/packages/color-slider/src/index.ts b/packages/color-slider/src/index.ts new file mode 100644 index 00000000000..9680b227328 --- /dev/null +++ b/packages/color-slider/src/index.ts @@ -0,0 +1,13 @@ +/* +Copyright 2020 Adobe. All rights reserved. +This file is licensed 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 REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +export * from './ColorSlider.js'; diff --git a/packages/color-slider/src/spectrum-color-slider.css b/packages/color-slider/src/spectrum-color-slider.css new file mode 100644 index 00000000000..bb071229d69 --- /dev/null +++ b/packages/color-slider/src/spectrum-color-slider.css @@ -0,0 +1,188 @@ +/* stylelint-disable */ /* +Copyright 2020 Adobe. All rights reserved. +This file is licensed 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 REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. + +THIS FILE IS MACHINE GENERATED. DO NOT EDIT */ +:host([focused]) .handle { + /* .spectrum-ColorSlider.is-focused .spectrum-ColorSlider-handle */ + width: calc( + var( + --spectrum-colorhandle-size, + var(--spectrum-global-dimension-size-200) + ) * 2 + ); + height: calc( + var( + --spectrum-colorhandle-size, + var(--spectrum-global-dimension-size-200) + ) * 2 + ); + margin-left: calc( + -1 * var(--spectrum-colorhandle-size, var(--spectrum-global-dimension-size-200)) + ); + margin-top: calc( + -1 * var(--spectrum-colorhandle-size, var(--spectrum-global-dimension-size-200)) + ); +} +.slider { + /* .spectrum-ColorSlider-slider */ + opacity: 0.0001; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 0; + margin: 0; + pointer-events: none; +} +:host { + /* .spectrum-ColorSlider */ + position: relative; + display: block; + width: var( + --spectrum-colorslider-default-length, + var(--spectrum-global-dimension-size-2400) + ); + height: var( + --spectrum-colorslider-height, + var(--spectrum-global-dimension-size-300) + ); + -webkit-user-select: none; + user-select: none; + cursor: default; +} +:host([focused]) { + /* .spectrum-ColorSlider.is-focused */ + z-index: 2; +} +:host([disabled]) { + /* .spectrum-ColorSlider.is-disabled */ + pointer-events: none; +} +:host([vertical]) { + /* .spectrum-ColorSlider--vertical */ + display: inline-block; + width: var( + --spectrum-colorslider-vertical-width, + var(--spectrum-global-dimension-size-300) + ); + height: var( + --spectrum-colorslider-vertical-default-length, + var(--spectrum-global-dimension-size-2400) + ); +} +:host([vertical]) .handle { + /* .spectrum-ColorSlider--vertical .spectrum-ColorSlider-handle */ + left: 50%; + top: 0; +} +.handle { + /* .spectrum-ColorSlider-handle */ + left: 0; + top: 50%; +} +.handle:after { + /* .spectrum-ColorSlider-handle:after */ + border-radius: 0; + width: var(--spectrum-global-dimension-size-300); + height: var(--spectrum-global-dimension-size-300); +} +.checkerboard { + /* .spectrum-ColorSlider-checkerboard */ + background-size: var(--spectrum-global-dimension-static-size-200, 16px) + var(--spectrum-global-dimension-static-size-200, 16px); + background-position: 0 0, + 0 var(--spectrum-global-dimension-static-size-100, 8px), + var(--spectrum-global-dimension-static-size-100, 8px) + calc(-1 * var(--spectrum-global-dimension-static-size-100, 8px)), + calc(-1 * var(--spectrum-global-dimension-static-size-100, 8px)) 0; +} +.checkerboard:before { + /* .spectrum-ColorSlider-checkerboard:before */ + content: ''; + z-index: 1; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + border-radius: var( + --spectrum-colorslider-border-radius, + var(--spectrum-alias-border-radius-regular) + ); +} +.checkerboard, +.gradient { + /* .spectrum-ColorSlider-checkerboard, + * .spectrum-ColorSlider-gradient */ + width: 100%; + height: 100%; + border-radius: var( + --spectrum-colorslider-border-radius, + var(--spectrum-alias-border-radius-regular) + ); +} +.checkerboard { + /* .spectrum-ColorSlider-checkerboard */ + background-color: var(--spectrum-global-color-static-white, #fff); + background-image: linear-gradient( + -45deg, + transparent 75.5%, + var(--spectrum-global-color-static-gray-500, #bcbcbc) 0 + ), + linear-gradient( + 45deg, + transparent 75.5%, + var(--spectrum-global-color-static-gray-500, #bcbcbc) 0 + ), + linear-gradient( + -45deg, + var(--spectrum-global-color-static-gray-500, #bcbcbc) 25.5%, + transparent 0 + ), + linear-gradient( + 45deg, + var(--spectrum-global-color-static-gray-500, #bcbcbc) 25.5%, + transparent 0 + ); +} +.checkerboard:before { + /* .spectrum-ColorSlider-checkerboard:before */ + box-shadow: inset 0 0 0 + var( + --spectrum-colorslider-border-size, + var(--spectrum-alias-border-size-thin) + ) + var(--spectrum-colorarea-border-color); +} +:host([disabled]) .checkerboard { + /* .spectrum-ColorSlider.is-disabled .spectrum-ColorSlider-checkerboard */ + background: var( + --spectrum-colorslider-fill-color-disabled, + var(--spectrum-global-color-gray-300) + ); +} +:host([disabled]) .checkerboard:before { + /* .spectrum-ColorSlider.is-disabled .spectrum-ColorSlider-checkerboard:before */ + box-shadow: 0 0 0 + var( + --spectrum-colorslider-border-size, + var(--spectrum-alias-border-size-thin) + ) + var( + --spectrum-colorslider-border-color-disabled, + var(--spectrum-global-color-gray-300) + ); +} +:host([disabled]) .gradient { + /* .spectrum-ColorSlider.is-disabled .spectrum-ColorSlider-gradient */ + display: none; +} diff --git a/packages/color-slider/src/spectrum-config.js b/packages/color-slider/src/spectrum-config.js new file mode 100644 index 00000000000..6301304ccb5 --- /dev/null +++ b/packages/color-slider/src/spectrum-config.js @@ -0,0 +1,60 @@ +/* +Copyright 2020 Adobe. All rights reserved. +This file is licensed 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 REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const config = { + spectrum: 'colorslider', + components: [ + { + name: 'color-slider', + host: { + selector: '.spectrum-ColorSlider', + }, + attributes: [ + { + name: 'focused', + type: 'boolean', + selector: '.is-focused', + }, + { + name: 'disabled', + type: 'boolean', + selector: '.is-disabled', + }, + { + name: 'vertical', + type: 'boolean', + selector: '.spectrum-ColorSlider--vertical', + }, + ], + classes: [ + { + name: 'checkerboard', + selector: '.spectrum-ColorSlider-checkerboard', + }, + { + name: 'gradient', + selector: '.spectrum-ColorSlider-gradient', + }, + { + name: 'slider', + selector: '.spectrum-ColorSlider-slider', + }, + { + name: 'handle', + selector: '.spectrum-ColorSlider-handle', + }, + ], + }, + ], +}; + +export default config; diff --git a/packages/color-slider/stories/color-slider.stories.ts b/packages/color-slider/stories/color-slider.stories.ts new file mode 100644 index 00000000000..07bb32fc060 --- /dev/null +++ b/packages/color-slider/stories/color-slider.stories.ts @@ -0,0 +1,92 @@ +/* +Copyright 2020 Adobe. All rights reserved. +This file is licensed 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 REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { html, TemplateResult } from '@spectrum-web-components/base'; + +import '../sp-color-slider.js'; + +export default { + title: 'Color/Slider', + component: 'sp-color-slider', +}; + +export const Default = (): TemplateResult => { + return html` + + `; +}; + +export const alpha = (): TemplateResult => { + return html` + + `; +}; + +export const disabled = (): TemplateResult => { + return html` + + `; +}; + +export const vertical = (): TemplateResult => { + return html` + + `; +}; + +export const canvas = (): TemplateResult => { + requestAnimationFrame(() => { + const canvas = document.querySelector( + 'canvas[slot="gradient"]' + ) as HTMLCanvasElement; + canvas.width = canvas.offsetWidth; + canvas.height = canvas.offsetHeight; + const context = canvas.getContext('2d'); + if (context) { + context.rect(0, 0, canvas.width, canvas.height); + + const gradient = context.createLinearGradient( + 0, + 0, + canvas.width, + canvas.height + ); + + gradient.addColorStop(0, 'rgb(255, 0, 0)'); + gradient.addColorStop(0.17, 'rgb(255, 255, 0)'); + gradient.addColorStop(0.33, 'rgb(0, 255, 0)'); + gradient.addColorStop(0.5, 'rgb(0, 255, 255)'); + gradient.addColorStop(0.67, 'rgb(0, 0, 255)'); + gradient.addColorStop(0.83, 'rgb(255, 0, 255)'); + gradient.addColorStop(1, 'rgb(255, 0, 0)'); + + context.fillStyle = gradient; + context.fill(); + } + }); + return html` + + + + `; +}; + +export const image = (): TemplateResult => { + return html` + + + + `; +}; diff --git a/packages/color-slider/stories/gradientimg.png b/packages/color-slider/stories/gradientimg.png new file mode 100644 index 00000000000..c7cb4d15687 Binary files /dev/null and b/packages/color-slider/stories/gradientimg.png differ diff --git a/packages/color-slider/test/benchmark/basic-test.ts b/packages/color-slider/test/benchmark/basic-test.ts new file mode 100644 index 00000000000..91d9deb2930 --- /dev/null +++ b/packages/color-slider/test/benchmark/basic-test.ts @@ -0,0 +1,19 @@ +/* +Copyright 2020 Adobe. All rights reserved. +This file is licensed 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 REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import '@spectrum-web-components/color-slider/sp-color-slider.js'; +import { html } from '@spectrum-web-components/base'; +import { measureFixtureCreation } from '../../../../test/benchmark/helpers.js'; + +measureFixtureCreation(html` + +`); diff --git a/packages/color-slider/test/color-slider.test.ts b/packages/color-slider/test/color-slider.test.ts new file mode 100644 index 00000000000..83e29697541 --- /dev/null +++ b/packages/color-slider/test/color-slider.test.ts @@ -0,0 +1,518 @@ +/* +Copyright 2020 Adobe. All rights reserved. +This file is licensed 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 REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { fixture, elementUpdated, expect, html } from '@open-wc/testing'; +import { + shiftEvent, + arrowUpEvent, + arrowDownEvent, + arrowLeftEvent, + arrowRightEvent, + arrowUpKeyupEvent, + arrowDownKeyupEvent, + arrowLeftKeyupEvent, + arrowRightKeyupEvent, + shiftKeyupEvent, +} from '../../../test/testing-helpers.js'; + +import '../sp-color-slider.js'; +import { ColorSlider } from '..'; +import { HSL, HSLA, HSV, HSVA, RGB, RGBA, TinyColor } from '@ctrl/tinycolor'; + +describe('ColorSlider', () => { + it('loads default color-slider accessibly', async () => { + const el = await fixture( + html` + + ` + ); + + await elementUpdated(el); + + await expect(el).to.be.accessible(); + }); + it('manages [focused]', async () => { + const el = await fixture( + html` + + ` + ); + + await elementUpdated(el); + + el.focusElement.dispatchEvent(new FocusEvent('focus')); + await elementUpdated(el); + + expect(el.focused); + + el.focusElement.dispatchEvent(new FocusEvent('blur')); + await elementUpdated(el); + + expect(!el.focused); + }); + it('accepts "Arrow*" keypresses', async () => { + const el = await fixture( + html` + + ` + ); + + await elementUpdated(el); + + expect(el.sliderHandlePosition).to.equal(0); + + const input = el.focusElement; + + input.dispatchEvent(arrowUpEvent); + input.dispatchEvent(arrowUpKeyupEvent); + input.dispatchEvent(arrowUpEvent); + input.dispatchEvent(arrowUpKeyupEvent); + + await elementUpdated(el); + + expect(el.sliderHandlePosition).to.equal(2); + + input.dispatchEvent(arrowRightEvent); + input.dispatchEvent(arrowRightKeyupEvent); + input.dispatchEvent(arrowRightEvent); + input.dispatchEvent(arrowRightKeyupEvent); + + await elementUpdated(el); + + expect(el.sliderHandlePosition).to.equal(4); + + input.dispatchEvent(arrowDownEvent); + input.dispatchEvent(arrowDownKeyupEvent); + input.dispatchEvent(arrowDownEvent); + input.dispatchEvent(arrowDownKeyupEvent); + + await elementUpdated(el); + + expect(el.sliderHandlePosition).to.equal(2); + + input.dispatchEvent(arrowLeftEvent); + input.dispatchEvent(arrowLeftKeyupEvent); + input.dispatchEvent(arrowLeftEvent); + input.dispatchEvent(arrowLeftKeyupEvent); + + await elementUpdated(el); + + expect(el.sliderHandlePosition).to.equal(0); + }); + it('accepts "Arrow*" keypresses in dir="rtl"', async () => { + const el = await fixture( + html` + + ` + ); + + await elementUpdated(el); + + expect(el.sliderHandlePosition).to.equal(0); + + const input = el.focusElement; + + input.dispatchEvent(arrowUpEvent); + input.dispatchEvent(arrowUpKeyupEvent); + input.dispatchEvent(arrowUpEvent); + input.dispatchEvent(arrowUpKeyupEvent); + + await elementUpdated(el); + + expect(el.sliderHandlePosition).to.equal(2); + + input.dispatchEvent(arrowRightEvent); + input.dispatchEvent(arrowRightKeyupEvent); + input.dispatchEvent(arrowRightEvent); + input.dispatchEvent(arrowRightKeyupEvent); + + await elementUpdated(el); + + expect(el.sliderHandlePosition).to.equal(0); + + input.dispatchEvent(arrowLeftEvent); + input.dispatchEvent(arrowLeftKeyupEvent); + input.dispatchEvent(arrowLeftEvent); + input.dispatchEvent(arrowLeftKeyupEvent); + + await elementUpdated(el); + + expect(el.sliderHandlePosition).to.equal(2); + + input.dispatchEvent(arrowDownEvent); + input.dispatchEvent(arrowDownKeyupEvent); + input.dispatchEvent(arrowDownEvent); + input.dispatchEvent(arrowDownKeyupEvent); + + await elementUpdated(el); + + expect(el.sliderHandlePosition).to.equal(0); + }); + it('accepts "Arrow*" keypresses with alteration', async () => { + const el = await fixture( + html` + + ` + ); + + await elementUpdated(el); + + expect(el.sliderHandlePosition).to.equal(0); + + const input = el.focusElement; + + input.dispatchEvent(shiftEvent); + input.dispatchEvent(arrowUpEvent); + input.dispatchEvent(arrowUpKeyupEvent); + input.dispatchEvent(arrowUpEvent); + input.dispatchEvent(arrowUpKeyupEvent); + + await elementUpdated(el); + + expect(el.sliderHandlePosition).to.equal(20); + + input.dispatchEvent(arrowRightEvent); + input.dispatchEvent(arrowRightKeyupEvent); + input.dispatchEvent(arrowRightEvent); + input.dispatchEvent(arrowRightKeyupEvent); + + await elementUpdated(el); + + expect(el.sliderHandlePosition).to.equal(40); + + input.dispatchEvent(arrowDownEvent); + input.dispatchEvent(arrowDownKeyupEvent); + input.dispatchEvent(arrowDownEvent); + input.dispatchEvent(arrowDownKeyupEvent); + + await elementUpdated(el); + + expect(el.sliderHandlePosition).to.equal(20); + + input.dispatchEvent(arrowLeftEvent); + input.dispatchEvent(arrowLeftKeyupEvent); + input.dispatchEvent(arrowLeftEvent); + input.dispatchEvent(arrowLeftKeyupEvent); + input.dispatchEvent(shiftKeyupEvent); + + await elementUpdated(el); + + expect(el.sliderHandlePosition).to.equal(0); + }); + it('accepts pointer events', async () => { + const el = await fixture( + html` + + ` + ); + + await elementUpdated(el); + + const { handle } = (el as unknown) as { handle: HTMLElement }; + + handle.setPointerCapture = () => { + return; + }; + handle.releasePointerCapture = () => { + return; + }; + + expect(el.sliderHandlePosition).to.equal(0); + + const root = el.shadowRoot ? el.shadowRoot : el; + const gradient = root.querySelector('.gradient') as HTMLElement; + gradient.dispatchEvent( + new PointerEvent('pointerdown', { + pointerId: 1, + clientX: 100, + clientY: 15, + bubbles: true, + composed: true, + cancelable: true, + }) + ); + + await elementUpdated(el); + + expect(el.sliderHandlePosition).to.equal(47.91666666666667); + + handle.dispatchEvent( + new PointerEvent('pointermove', { + pointerId: 1, + clientX: 110, + clientY: 15, + bubbles: true, + composed: true, + cancelable: true, + }) + ); + handle.dispatchEvent( + new PointerEvent('pointerup', { + pointerId: 1, + clientX: 110, + clientY: 15, + bubbles: true, + composed: true, + cancelable: true, + }) + ); + + await elementUpdated(el); + + expect(el.sliderHandlePosition).to.equal(53.125); + }); + it('accepts pointer events while [vertical]', async () => { + const el = await fixture( + html` + + ` + ); + + await elementUpdated(el); + + const { handle } = (el as unknown) as { handle: HTMLElement }; + + handle.setPointerCapture = () => { + return; + }; + handle.releasePointerCapture = () => { + return; + }; + + expect(el.sliderHandlePosition).to.equal(0); + + const root = el.shadowRoot ? el.shadowRoot : el; + const gradient = root.querySelector('.gradient') as HTMLElement; + gradient.dispatchEvent( + new PointerEvent('pointerdown', { + pointerId: 1, + clientX: 15, + clientY: 100, + bubbles: true, + composed: true, + cancelable: true, + }) + ); + + await elementUpdated(el); + + expect(el.sliderHandlePosition).to.equal(47.91666666666667); + + handle.dispatchEvent( + new PointerEvent('pointermove', { + pointerId: 1, + clientX: 15, + clientY: 110, + bubbles: true, + composed: true, + cancelable: true, + }) + ); + handle.dispatchEvent( + new PointerEvent('pointerup', { + pointerId: 1, + clientX: 15, + clientY: 110, + bubbles: true, + composed: true, + cancelable: true, + }) + ); + + await elementUpdated(el); + + expect(el.sliderHandlePosition).to.equal(53.125); + }); + it('accepts pointer events in dir="rtl"', async () => { + const el = await fixture( + html` + + ` + ); + document.documentElement.dir = 'rtl'; + await elementUpdated(el); + + const { handle } = (el as unknown) as { handle: HTMLElement }; + const clientWidth = document.body.offsetWidth; + + handle.setPointerCapture = () => { + return; + }; + handle.releasePointerCapture = () => { + return; + }; + + expect(el.sliderHandlePosition).to.equal(0); + + const root = el.shadowRoot ? el.shadowRoot : el; + const gradient = root.querySelector('.gradient') as HTMLElement; + gradient.dispatchEvent( + new PointerEvent('pointerdown', { + pointerId: 1, + clientX: clientWidth - 100, + clientY: 15, + bubbles: true, + composed: true, + cancelable: true, + }) + ); + + await elementUpdated(el); + + expect(el.sliderHandlePosition).to.equal(56.25); + + handle.dispatchEvent( + new PointerEvent('pointermove', { + pointerId: 1, + clientX: clientWidth - 110, + clientY: 15, + bubbles: true, + composed: true, + cancelable: true, + }) + ); + handle.dispatchEvent( + new PointerEvent('pointerup', { + pointerId: 1, + clientX: clientWidth - 110, + clientY: 15, + bubbles: true, + composed: true, + cancelable: true, + }) + ); + + await elementUpdated(el); + + expect(el.sliderHandlePosition).to.equal(61.45833333333333); + }); + const colorFormats: { + name: string; + color: + | string + | number + | TinyColor + | HSVA + | HSV + | RGB + | RGBA + | HSL + | HSLA; + }[] = [ + //rgb + { name: 'RGB String', color: 'rgb(204, 51, 204)' }, + { name: 'RGB', color: { r: 204, g: 51, b: 204, a: 1 } }, + //prgb + { name: 'PRGB String', color: 'rgb(80%, 20%, 80%)' }, + { name: 'PRGB', color: { r: '80%', g: '20%', b: '80%', a: 1 } }, + // hex + { name: 'Hex', color: 'cc33cc' }, + { name: 'Hex String', color: '#cc33cc' }, + // hex8 + { name: 'Hex8', color: 'cc33ccff' }, + { name: 'Hex8 String', color: '#cc33ccff' }, + // name + { name: 'string', color: 'red' }, + // hsl + { name: 'HSL String', color: 'hsl(300, 60%, 50%)' }, + { name: 'HSL', color: { h: 300, s: 0.6000000000000001, l: 0.5, a: 1 } }, + // hsv + { name: 'HSV String', color: 'hsv(300, 75%, 100%)' }, + { name: 'HSV', color: { h: 300, s: 0.75, v: 1, a: 1 } }, + ]; + colorFormats.map((format) => { + it(`maintains \`color\` format as ${format.name}`, async () => { + const el = await fixture( + html` + + ` + ); + + el.color = format.color; + if (format.name.startsWith('Hex')) { + expect(el.color).to.equal(format.color); + } else expect(el.color).to.deep.equal(format.color); + }); + }); + it(`maintains \`color\` format as TinyColor`, async () => { + const el = await fixture( + html` + + ` + ); + const color = new TinyColor('rgb(204, 51, 204)'); + el.color = color; + expect(color.equals(el.color)); + }); + it(`resolves Hex3 format to Hex6 format`, async () => { + const el = await fixture( + html` + + ` + ); + el.color = '0f0'; + expect(el.color).to.equal('00ff00'); + + el.color = '#1e0'; + expect(el.color).to.equal('#11ee00'); + }); + it(`resolves Hex4 format to Hex8 format`, async () => { + const el = await fixture( + html` + + ` + ); + el.color = 'f3af'; + expect(el.color).to.equal('ff33aaff'); + + el.color = '#f3af'; + expect(el.color).to.equal('#ff33aaff'); + }); + it(`maintains hue value`, async () => { + const el = await fixture( + html` + + ` + ); + el.color = 'hsl(300, 60%, 100%)'; + expect(el.value).to.equal(300); + + el.color = 'hsla(300, 60%, 100%, 1)'; + expect(el.value).to.equal(300); + + el.color = 'hsv(300, 60%, 100%)'; + expect(el.value).to.equal(300); + + el.color = 'hsva(300, 60%, 100%, 1)'; + expect(el.value).to.equal(300); + + el.color = new TinyColor({ h: 300, s: 60, v: 100 }); + expect(el.value).to.equal(300); + + el.color = new TinyColor({ h: 300, s: 60, v: 100, a: 1 }); + expect(el.value).to.equal(300); + + el.color = new TinyColor({ h: 300, s: 60, l: 100 }); + expect(el.value).to.equal(300); + + el.color = new TinyColor({ h: 300, s: 60, l: 100, a: 1 }); + expect(el.value).to.equal(300); + }); +}); diff --git a/packages/color-slider/tsconfig.json b/packages/color-slider/tsconfig.json new file mode 100644 index 00000000000..75919f9078c --- /dev/null +++ b/packages/color-slider/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "composite": true, + "rootDir": "./" + }, + "include": ["*.ts", "src/*.ts"], + "exclude": ["test/*.ts", "stories/*.ts"], + "references": [{ "path": "../base" }] +}