color-picker library change and hsv format support (#1072)

* sl-color-picker use lib '@ctrl/tinycolor' instead of 'color'

typescript, esm and smaller

* parseColor adjustments

removed normalizeColorString and other tweaks

* added hsv format

* fixed const
This commit is contained in:
Alan Chambers
2022-12-13 16:01:07 +00:00
committed by GitHub
parent 752f5cff55
commit 9178be576b
4 changed files with 73 additions and 212 deletions

View File

@@ -32,7 +32,7 @@ const App = () => <SlColorPicker value="#4a90e2" label="Select a color" />;
### Opacity
Use the `opacity` attribute to enable the opacity slider. When this is enabled, the value will be displayed as HEXA, RGBA, or HSLA based on `format`.
Use the `opacity` attribute to enable the opacity slider. When this is enabled, the value will be displayed as HEXA, RGBA, HSLA or HSVA based on `format`.
```html preview
<sl-color-picker value="#f5a623ff" opacity label="Select a color"></sl-color-picker>
@@ -46,7 +46,7 @@ const App = () => <SlColorPicker opacity label="Select a color" />;
### Formats
Set the color picker's format with the `format` attribute. Valid options include `hex`, `rgb`, and `hsl`. Note that the color picker's input will accept any parsable format (including CSS color names) regardless of this option.
Set the color picker's format with the `format` attribute. Valid options include `hex`, `rgb`, `hsl` and `hsv`. Note that the color picker's input will accept any parsable format (including CSS color names) regardless of this option.
To prevent users from toggling the format themselves, add the `no-format-toggle` attribute.
@@ -54,6 +54,7 @@ To prevent users from toggling the format themselves, add the `no-format-toggle`
<sl-color-picker format="hex" value="#4a90e2" label="Select a color"></sl-color-picker>
<sl-color-picker format="rgb" value="rgb(80, 227, 194)" label="Select a color"></sl-color-picker>
<sl-color-picker format="hsl" value="hsl(290, 87%, 47%)" label="Select a color"></sl-color-picker>
<sl-color-picker format="hsv" value="hsv(55, 89%, 97%)" label="Select a color"></sl-color-picker>
```
```jsx react
@@ -64,6 +65,7 @@ const App = () => (
<SlColorPicker format="hex" value="#4a90e2" />
<SlColorPicker format="rgb" value="rgb(80, 227, 194)" />
<SlColorPicker format="hsl" value="hsl(290, 87%, 47%)" />
<SlColorPicker format="hsv" value="hsv(55, 89%, 97%)" />
</>
);
```

139
package-lock.json generated
View File

@@ -9,18 +9,17 @@
"version": "2.0.0-beta.86",
"license": "MIT",
"dependencies": {
"@ctrl/tinycolor": "^3.5.0",
"@floating-ui/dom": "^1.0.7",
"@lit-labs/react": "^1.1.0",
"@shoelace-style/animations": "^1.1.0",
"@shoelace-style/localize": "^3.0.3",
"color": "4.2",
"lit": "^2.4.1",
"qr-creator": "^1.0.0"
},
"devDependencies": {
"@custom-elements-manifest/analyzer": "^0.6.6",
"@open-wc/testing": "^3.1.7",
"@types/color": "^3.0.3",
"@types/mocha": "^10.0.0",
"@types/react": "^18.0.25",
"@typescript-eslint/eslint-plugin": "^5.43.0",
@@ -547,6 +546,14 @@
"node": ">=14.6"
}
},
"node_modules/@ctrl/tinycolor": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.5.0.tgz",
"integrity": "sha512-tlJpwF40DEQcfR/QF+wNMVyGMaO9FQp6Z1Wahj4Gk3CJQYHwA2xVG7iKDFdW6zuxZY9XWOpGcfNCTsX4McOsOg==",
"engines": {
"node": ">=10"
}
},
"node_modules/@custom-elements-manifest/analyzer": {
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/@custom-elements-manifest/analyzer/-/analyzer-0.6.6.tgz",
@@ -1291,30 +1298,6 @@
"@types/qs": "*"
}
},
"node_modules/@types/color": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.3.tgz",
"integrity": "sha512-X//qzJ3d3Zj82J9sC/C18ZY5f43utPbAJ6PhYt/M7uG6etcF6MRpKdN880KBy43B0BMzSfeT96MzrsNjFI3GbA==",
"dev": true,
"dependencies": {
"@types/color-convert": "*"
}
},
"node_modules/@types/color-convert": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.0.tgz",
"integrity": "sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ==",
"dev": true,
"dependencies": {
"@types/color-name": "*"
}
},
"node_modules/@types/color-name": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
"dev": true
},
"node_modules/@types/command-line-args": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.0.tgz",
@@ -3982,22 +3965,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/color": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.1.tgz",
"integrity": "sha512-MFJr0uY4RvTQUKvPq7dh9grVOTYSFeXja2mBXioCGjnjJoXrAp9jJ1NQTDR73c9nwBSAQiNKloKl5zq9WB9UPw==",
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -4008,16 +3980,8 @@
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/color-string": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.0.tgz",
"integrity": "sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ==",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/color-support": {
"version": "1.1.3",
@@ -13336,19 +13300,6 @@
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"dev": true
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/simple-swizzle/node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
},
"node_modules/sinon": {
"version": "14.0.2",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.2.tgz",
@@ -15606,6 +15557,11 @@
"integrity": "sha512-OS/t4e5vfUyAiOcyuI1I9d4/EWCx7pA3L8uHNOQQHgjVP41tffMaKTirqRiNhkruIhmxa5Tk5fbQLRMEFapalg==",
"dev": true
},
"@ctrl/tinycolor": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.5.0.tgz",
"integrity": "sha512-tlJpwF40DEQcfR/QF+wNMVyGMaO9FQp6Z1Wahj4Gk3CJQYHwA2xVG7iKDFdW6zuxZY9XWOpGcfNCTsX4McOsOg=="
},
"@custom-elements-manifest/analyzer": {
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/@custom-elements-manifest/analyzer/-/analyzer-0.6.6.tgz",
@@ -16210,30 +16166,6 @@
"@types/qs": "*"
}
},
"@types/color": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.3.tgz",
"integrity": "sha512-X//qzJ3d3Zj82J9sC/C18ZY5f43utPbAJ6PhYt/M7uG6etcF6MRpKdN880KBy43B0BMzSfeT96MzrsNjFI3GbA==",
"dev": true,
"requires": {
"@types/color-convert": "*"
}
},
"@types/color-convert": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.0.tgz",
"integrity": "sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ==",
"dev": true,
"requires": {
"@types/color-name": "*"
}
},
"@types/color-name": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
"dev": true
},
"@types/command-line-args": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.0.tgz",
@@ -18232,19 +18164,11 @@
}
}
},
"color": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.1.tgz",
"integrity": "sha512-MFJr0uY4RvTQUKvPq7dh9grVOTYSFeXja2mBXioCGjnjJoXrAp9jJ1NQTDR73c9nwBSAQiNKloKl5zq9WB9UPw==",
"requires": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
@@ -18252,16 +18176,8 @@
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"color-string": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.0.tgz",
"integrity": "sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ==",
"requires": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"color-support": {
"version": "1.1.3",
@@ -25255,21 +25171,6 @@
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"dev": true
},
"simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
"requires": {
"is-arrayish": "^0.3.1"
},
"dependencies": {
"is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
}
}
},
"sinon": {
"version": "14.0.2",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.2.tgz",

View File

@@ -62,18 +62,17 @@
"node": ">=14.17.0"
},
"dependencies": {
"@ctrl/tinycolor": "^3.5.0",
"@floating-ui/dom": "^1.0.7",
"@lit-labs/react": "^1.1.0",
"@shoelace-style/animations": "^1.1.0",
"@shoelace-style/localize": "^3.0.3",
"color": "4.2",
"lit": "^2.4.1",
"qr-creator": "^1.0.0"
},
"devDependencies": {
"@custom-elements-manifest/analyzer": "^0.6.6",
"@open-wc/testing": "^3.1.7",
"@types/color": "^3.0.3",
"@types/mocha": "^10.0.0",
"@types/react": "^18.0.25",
"@typescript-eslint/eslint-plugin": "^5.43.0",

View File

@@ -1,4 +1,4 @@
import Color from 'color';
import { TinyColor } from '@ctrl/tinycolor';
import { html } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
@@ -125,10 +125,10 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
@property() label = '';
/**
* The format to use. If opacity is enabled, these will translate to HEXA, RGBA, and HSLA respectively. The color
* The format to use. If opacity is enabled, these will translate to HEXA, RGBA, HSLA and HSVA respectively. The color
* picker will accept user input in any format (including CSS color names) and convert it to the desired format.
*/
@property() format: 'hex' | 'rgb' | 'hsl' = 'hex';
@property() format: 'hex' | 'rgb' | 'hsl' | 'hsv' = 'hex';
/** Renders the color picker inline rather than in a dropdown. */
@property({ type: Boolean, reflect: true }) inline = false;
@@ -159,7 +159,7 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
/**
* An array of predefined color swatches to display. Can include any format the color picker can parse, including
* HEX(A), RGB(A), HSL(A), and CSS color names.
* HEX(A), RGB(A), HSL(A), HSV(A) and CSS color names.
*/
@property({ attribute: false }) swatches: string[] = [
'#d0021b',
@@ -196,7 +196,7 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
}
/** Returns the current value as a string in the specified format. */
getFormattedValue(format: 'hex' | 'hexa' | 'rgb' | 'rgba' | 'hsl' | 'hsla' = 'hex') {
getFormattedValue(format: 'hex' | 'hexa' | 'rgb' | 'rgba' | 'hsl' | 'hsla' | 'hsv' | 'hsva' = 'hex') {
const currentColor = this.parseColor(
`hsla(${this.hue}, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
);
@@ -218,6 +218,10 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
return currentColor.hsl.string;
case 'hsla':
return currentColor.hsla.string;
case 'hsv':
return currentColor.hsv.string;
case 'hsva':
return currentColor.hsva.string;
default:
return '';
}
@@ -267,9 +271,9 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
}
handleFormatToggle() {
const formats = ['hex', 'rgb', 'hsl'];
const formats = ['hex', 'rgb', 'hsl', 'hsv'];
const nextIndex = (formats.indexOf(this.format) + 1) % formats.length;
this.format = formats[nextIndex] as 'hex' | 'rgb' | 'hsl';
this.format = formats[nextIndex] as 'hex' | 'rgb' | 'hsl' | 'hsv';
this.setColor(this.value);
this.emit('sl-change');
this.emit('sl-input');
@@ -507,90 +511,33 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
event.preventDefault();
}
normalizeColorString(colorString: string) {
//
// The color module we're using doesn't parse % values for the alpha channel in RGBA and HSLA. It also doesn't parse
// hex colors when the # is missing. This pre-parser tries to normalize these edge cases to provide a better
// experience for users who type in color values.
//
if (/rgba?/i.test(colorString)) {
const rgba = colorString
.replace(/[^\d.%]/g, ' ')
.split(' ')
.map(val => val.trim())
.filter(val => val.length);
if (rgba.length < 4) {
rgba[3] = '1';
}
if (rgba[3].indexOf('%') > -1) {
rgba[3] = (parseFloat(rgba[3].replace(/%/g, '')) / 100).toString();
}
return `rgba(${rgba[0]}, ${rgba[1]}, ${rgba[2]}, ${rgba[3]})`;
}
if (/hsla?/i.test(colorString)) {
const hsla = colorString
.replace(/[^\d.%]/g, ' ')
.split(' ')
.map(val => val.trim())
.filter(val => val.length);
if (hsla.length < 4) {
hsla[3] = '1';
}
if (hsla[3].indexOf('%') > -1) {
hsla[3] = (parseFloat(hsla[3].replace(/%/g, '')) / 100).toString();
}
return `hsla(${hsla[0]}, ${hsla[1]}, ${hsla[2]}, ${hsla[3]})`;
}
if (/^[0-9a-f]+$/i.test(colorString)) {
return `#${colorString}`;
}
return colorString;
}
parseColor(colorString: string) {
let parsed: Color;
// The color module has a weak parser, so we normalize certain things to make the user experience better
colorString = this.normalizeColorString(colorString);
try {
parsed = Color(colorString);
} catch {
const color = new TinyColor(colorString);
if (!color.isValid) {
return null;
}
const hslColor = parsed.hsl();
const hslColor = color.toHsl();
// adjust saturation and lightness from 0-1 to 0-100
const hsl = {
h: hslColor.hue(),
s: hslColor.saturationl(),
l: hslColor.lightness(),
a: hslColor.alpha()
h: hslColor.h,
s: hslColor.s * 100,
l: hslColor.l * 100,
a: hslColor.a
};
const rgbColor = parsed.rgb();
const rgb = color.toRgb();
const rgb = {
r: rgbColor.red(),
g: rgbColor.green(),
b: rgbColor.blue(),
a: rgbColor.alpha()
};
const hex = color.toHexString();
const hexa = color.toHex8String();
const hex = {
r: toHex(rgb.r),
g: toHex(rgb.g),
b: toHex(rgb.b),
a: toHex(rgb.a * 255)
const hsvColor = color.toHsv();
// adjust saturation and value from 0-1 to 0-100
const hsv = {
h: hsvColor.h,
s: hsvColor.s * 100,
v: hsvColor.v * 100,
a: hsvColor.a
};
return {
@@ -609,6 +556,21 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
`hsla(${Math.round(hsl.h)}, ${Math.round(hsl.s)}%, ${Math.round(hsl.l)}%, ${hsl.a.toFixed(2).toString()})`
)
},
hsv: {
h: hsv.h,
s: hsv.s,
v: hsv.v,
string: this.setLetterCase(`hsv(${Math.round(hsv.h)}, ${Math.round(hsv.s)}%, ${Math.round(hsv.v)}%)`)
},
hsva: {
h: hsv.h,
s: hsv.s,
v: hsv.v,
a: hsv.a,
string: this.setLetterCase(
`hsva(${Math.round(hsv.h)}, ${Math.round(hsv.s)}%, ${Math.round(hsv.v)}%, ${hsv.a.toFixed(2).toString()})`
)
},
rgb: {
r: rgb.r,
g: rgb.g,
@@ -624,8 +586,8 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
`rgba(${Math.round(rgb.r)}, ${Math.round(rgb.g)}, ${Math.round(rgb.b)}, ${rgb.a.toFixed(2).toString()})`
)
},
hex: this.setLetterCase(`#${hex.r}${hex.g}${hex.b}`),
hexa: this.setLetterCase(`#${hex.r}${hex.g}${hex.b}${hex.a}`)
hex: this.setLetterCase(hex),
hexa: this.setLetterCase(hexa)
};
}
@@ -668,6 +630,8 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
this.inputValue = this.opacity ? currentColor.hsla.string : currentColor.hsl.string;
} else if (this.format === 'rgb') {
this.inputValue = this.opacity ? currentColor.rgba.string : currentColor.rgb.string;
} else if (this.format === 'hsv') {
this.inputValue = this.opacity ? currentColor.hsva.string : currentColor.hsv.string;
} else {
this.inputValue = this.opacity ? currentColor.hexa : currentColor.hex;
}
@@ -1005,11 +969,6 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
}
}
function toHex(value: number) {
const hex = Math.round(value).toString(16);
return hex.length === 1 ? `0${hex}` : hex;
}
declare global {
interface HTMLElementTagNameMap {
'sl-color-picker': SlColorPicker;