diff --git a/packages/uui-color-slider/lib/uui-color-slider.element.ts b/packages/uui-color-slider/lib/uui-color-slider.element.ts index df98d89e6..543e0c373 100644 --- a/packages/uui-color-slider/lib/uui-color-slider.element.ts +++ b/packages/uui-color-slider/lib/uui-color-slider.element.ts @@ -1,4 +1,5 @@ import { LitElement, html, css } from 'lit'; +import { Colord } from 'colord'; import { defineElement } from '@umbraco-ui/uui-base/lib/registration'; import { property } from 'lit/decorators.js'; import { styleMap } from 'lit/directives/style-map.js'; @@ -13,8 +14,7 @@ import { UUIColorSliderEvent } from './UUIColorSliderEvent'; import { LabelMixin } from '@umbraco-ui/uui-base/lib/mixins'; export type UUIColorSliderOrientation = 'horizontal' | 'vertical'; -//TODO implement saturation and lightness types for color slider -export type UUIColorSliderType = 'hue' | 'opacity'; +export type UUIColorSliderType = 'hue' | 'opacity' | 'saturation' | 'lightness'; /** * @element uui-color-slider @@ -107,11 +107,29 @@ export class UUIColorSliderElement extends LabelMixin('label', LitElement) { willUpdate(changedProperties: Map) { if (changedProperties.has('type')) { if (this.type === 'hue') { - this.max = 360; - this.precision = 1; + this.max = this.max ?? 360; + } else if (this.type === 'saturation') { + this.max = this.max ?? 100; + } else if (this.type === 'lightness') { + this.max = this.max ?? 100; } else if (this.type === 'opacity') { - this.max = 100; - this.precision = 1; + this.max = this.max ?? 100; + } + + this.precision = this.precision ?? 1; + + if (this.color) { + const colord = new Colord(this.color); + const { h, s, l } = colord.toHsl(); + + const gradient = + this.type === 'saturation' + ? `linear-gradient(to ${this.vertical ? 'top' : 'right'}, hsl(${h}, 0%, ${l}%), hsl(${h}, 100%, ${l}%))` + : this.type === 'lightness' + ? `linear-gradient(to ${this.vertical ? 'top' : 'right'}, hsl(${h}, ${s}%, 0%), hsl(${h}, ${s}%, 100%))` + : null; + + this.style.setProperty('--uui-slider-background-image', gradient); } } } diff --git a/packages/uui-color-slider/lib/uui-color-slider.story.ts b/packages/uui-color-slider/lib/uui-color-slider.story.ts index 130f3cece..754728f2c 100644 --- a/packages/uui-color-slider/lib/uui-color-slider.story.ts +++ b/packages/uui-color-slider/lib/uui-color-slider.story.ts @@ -1,8 +1,16 @@ import '.'; import readme from '../README.md?raw'; import { html } from 'lit'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { styleMap } from 'lit/directives/style-map.js'; import type { Meta, StoryObj } from '@storybook/web-components'; +import { useState } from '@storybook/preview-api'; import { spread } from '../../../storyhelpers'; +import { repeat } from 'lit/directives/repeat.js'; + +import { colord, HslaColor } from 'colord'; + +import type { UUIColorSliderElement } from '@umbraco-ui/uui-color-slider/lib'; const meta: Meta = { id: 'uui-color-slider', @@ -10,7 +18,7 @@ const meta: Meta = { title: 'Inputs/Color/Color Slider', argTypes: { type: { - options: ['hue', 'opacity'], + options: ['hue', 'opacity', 'saturation', 'lightness'], control: { type: 'select' }, }, }, @@ -53,3 +61,173 @@ export const Vertical: Story = { vertical: true, }, }; + +export const Advanced: Story = { + render: () => { + const sliders = [ + { + label: 'H', + type: 'hue', + color: '#0075ff', + value: 0, + min: 0, + max: 360, + }, + { + label: 'S', + type: 'saturation', + color: '#0075ff', + value: 100, + min: 0, + max: 100, + }, + { + label: 'L', + type: 'lightness', + color: '#0075ff', + value: 50, + min: 0, + max: 100, + }, + { + label: 'A', + type: 'opacity', + color: '#0075ff', + value: 1, + min: 0, + max: 1, + precision: 2, + }, + ]; + + const [value, setValue] = useState({ h: 0, s: 100, l: 50, a: 1 }); + + function handleSliderChange(e: Event, slider: any) { + e.stopPropagation(); + + const element = e.target as UUIColorSliderElement; + + if (isNaN(element.value)) return; + + const newColor: HslaColor = { + h: value.h, + s: value.s, + l: value.l, + a: value.a, + }; + + if (slider.type === 'hue') { + newColor.h = element.value; + } else if (slider.type === 'saturation') { + newColor.s = element.value; + } else if (slider.type === 'lightness') { + newColor.l = element.value; + } else if (slider.type === 'opacity') { + newColor.a = element.value; + } + + slider.value = element.value; + + setValue({ h: newColor.h, s: newColor.s, l: newColor.l, a: newColor.a }); + } + + function handleInputChange(e: Event, slider: any) { + e.stopPropagation(); + + const input = e.target as HTMLInputElement; + + let newValue = parseFloat(input.value); + + if (isNaN(newValue)) { + newValue = 0; + input.value = '0'; + } + + const newColor: HslaColor = { + h: value.h, + s: value.s, + l: value.l, + a: value.a, + }; + + if (slider.type === 'hue') { + newColor.h = newValue; + } else if (slider.type === 'saturation') { + newColor.s = newValue; + } else if (slider.type === 'lightness') { + newColor.l = newValue; + } else if (slider.type === 'opacity') { + newColor.a = newValue; + } + + slider.value = newValue; + + setValue({ h: newColor.h, s: newColor.s, l: newColor.l, a: newColor.a }); + } + + /** Generates a hex string from HSL values. Hue must be 0-360. All other arguments must be 0-100. */ + function getHexString( + hue: number, + saturation: number, + lightness: number, + alpha = 100, + ) { + const color = colord( + `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha / 100})`, + ); + if (!color.isValid()) { + return ''; + } + + return color.toHex(); + } + + return html`
+
+ ${repeat(sliders, (slider: any) => { + return html`
+ + handleSliderChange(e, slider)} + style=${styleMap({ + '--uui-slider-background-image': + slider.type === 'saturation' + ? `linear-gradient(to right, hsl(${value.h}, 0%, ${value.l}%), hsl(${value.h}, 100%, ${value.l}%))` + : slider.type === 'lightness' + ? `linear-gradient(to right, hsl(${value.h}, ${value.s}%, 0%), hsl(${value.h}, ${value.s}%, ${slider.value}%))` + : undefined, + width: '400px', + })}> + + 1 ? slider.max / 10 : 1} + .value=${slider.value} + @change=${(e: Event) => handleInputChange(e, slider)} + style="width: 60px;"> + +
`; + })} +
+
+
+
+
`; + }, +};