diff --git a/docs/components/color-picker.md b/docs/components/color-picker.md
index 92c6b2b5e..1fd0487a7 100644
--- a/docs/components/color-picker.md
+++ b/docs/components/color-picker.md
@@ -32,7 +32,7 @@ const App = () => ;
### 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
@@ -46,7 +46,7 @@ const App = () => ;
### 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`
+
```
```jsx react
@@ -64,6 +65,7 @@ const App = () => (
+
>
);
```
diff --git a/package-lock.json b/package-lock.json
index 37f58d4f5..a7f389a0a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 2309e0302..33b23bd8e 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/components/color-picker/color-picker.ts b/src/components/color-picker/color-picker.ts
index 0a163a775..bcee6845d 100644
--- a/src/components/color-picker/color-picker.ts
+++ b/src/components/color-picker/color-picker.ts
@@ -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;