Skip to content

Advanced color slider story #1076

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions packages/uui-color-slider/lib/uui-color-slider.element.ts
Original file line number Diff line number Diff line change
@@ -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<string, any>) {
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);
}
}
}
180 changes: 179 additions & 1 deletion packages/uui-color-slider/lib/uui-color-slider.story.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
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',
component: 'uui-color-slider',
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` <div style="display: flex; gap: 20px;">
<div style="display: flex; flex-direction: column; gap: 10px;">
${repeat(sliders, (slider: any) => {
return html`<div style="display: flex; gap: 10px 20px;">
<label>${slider.label}</label>
<uui-color-slider
.type=${slider.type}
.value=${slider.value}
.min=${slider.min}
.max=${slider.max}
.color=${slider.type !== 'hue'
? getHexString(value.h, value.s, value.l)
: undefined}
?precision=${ifDefined(slider.precision)}
@change=${(e: Event) => 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',
})}>
</uui-color-slider>
<uui-input
type="number"
.min=${slider.min}
.max=${slider.max}
.step=${slider.precision > 1 ? slider.max / 10 : 1}
.value=${slider.value}
@change=${(e: Event) => handleInputChange(e, slider)}
style="width: 60px;">
</uui-input>
</div>`;
})}
</div>
<div
style="width: 100px; height: 100px;
border: 1px solid var(--uui-color-border-standalone);
background-image: linear-gradient(45deg, var(--uui-palette-grey) 25%, transparent 25%), linear-gradient(45deg, transparent 75%, var(--uui-palette-grey) 75%), linear-gradient(45deg, transparent 75%, var(--uui-palette-grey) 75%), linear-gradient(45deg, var(--uui-palette-grey) 25%, transparent 25%);
background-size: 10px 10px;
background-position: 0 0, 0 0, -5px -5px, 5px 5px;">
<div
style="width: 100%; height: 100%; background-color: hsla(${value.h}, ${value.s}%, ${value.l}%, ${value.a});"></div>
</div>
</div>`;
},
};

Unchanged files with check annotations Beta

type Constructor<T = {}> = new (...args: any[]) => T;
export declare class SelectableMixinInterface extends LitElement {

Check warning on line 8 in packages/uui-base/lib/mixins/SelectableMixin.ts

GitHub Actions / test

Class declaration should be prefixed with "UUI"
/**
* Enable the ability to select this element.
* @attr
* @fires {UUISelectableEvent} selected - fires when the media card is selected
* @fires {UUISelectableEvent} deselected - fires when the media card is deselected
*/
class SelectableMixinClass extends superClass {

Check warning on line 42 in packages/uui-base/lib/mixins/SelectableMixin.ts

GitHub Actions / test

Class declaration should be prefixed with "UUI"
private _selectable = false;
/**
* Enable the ability to select this element.
type Constructor<T = {}> = new (...args: any[]) => T;
export declare class SelectOnlyMixinInterface extends SelectableMixinInterface {

Check warning on line 6 in packages/uui-base/lib/mixins/SelectOnlyMixin.ts

GitHub Actions / test

Class declaration should be prefixed with "UUI"
selectOnly: boolean;
}
>(
superClass: T,
) => {
class SelectOnlyMixinClass extends superClass {

Check warning on line 22 in packages/uui-base/lib/mixins/SelectOnlyMixin.ts

GitHub Actions / test

Class declaration should be prefixed with "UUI"
private _selectOnly = false;
/**
type Constructor<T = {}> = new (...args: any[]) => T;
export declare class PopoverTargetMixinInterface {

Check warning on line 7 in packages/uui-base/lib/mixins/PopoverTargetMixin.ts

GitHub Actions / test

Class declaration should be prefixed with "UUI"
/**
* Set a popovertarget.
* @type {string}
/**
* Popover target mixin class containing the popover target functionality.
*/
class PopoverTargetMixinClass extends superClass {

Check warning on line 34 in packages/uui-base/lib/mixins/PopoverTargetMixin.ts

GitHub Actions / test

Class declaration should be prefixed with "UUI"
/**
* Set a popovertarget.
* @type {string}
type Constructor<T = {}> = new (...args: any[]) => T;
export declare class LabelMixinInterface {

Check warning on line 6 in packages/uui-base/lib/mixins/LabelMixin.ts

GitHub Actions / test

Class declaration should be prefixed with "UUI"
/**
* Label to be used for aria-label and potentially as visual label for some components
* @type {string}
/**
* Label mixin class containing the label functionality.
*/
class LabelMixinClass extends superClass {

Check warning on line 31 in packages/uui-base/lib/mixins/LabelMixin.ts

GitHub Actions / test

Class declaration should be prefixed with "UUI"
/**
* Label to be used for aria-label and potentially as visual label for some components
* @type {string}
type Constructor<T = {}> = new (...args: any[]) => T;
export declare class ActiveMixinInterface {

Check warning on line 6 in packages/uui-base/lib/mixins/ActiveMixin.ts

GitHub Actions / test

Class declaration should be prefixed with "UUI"
/**
* Set this boolean to true for then the related composition is sorted.
* @type {boolean}
export const ActiveMixin = <T extends Constructor<LitElement>>(
superClass: T,
) => {
class ActiveMixinClass extends superClass {

Check warning on line 25 in packages/uui-base/lib/mixins/ActiveMixin.ts

GitHub Actions / test

Class declaration should be prefixed with "UUI"
/**
* Set this boolean to true for then the related composition is sorted.
* @type {boolean}