diff --git a/package-lock.json b/package-lock.json index 1a9ec66e4..eb8c0215a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -130,9 +130,9 @@ "integrity": "sha512-sLqWxCzC5/QHLhziXSCAksBxHfOnQlhPRVgPK0egEw+ktWvG75T2k+aYWVjVh9+WKeT3tlG3ZNbZQvZLmfuOIw==" }, "@stencil/core": { - "version": "1.12.6", - "resolved": "https://registry.npmjs.org/@stencil/core/-/core-1.12.6.tgz", - "integrity": "sha512-fOHG9x92wxlQOlp9YRouLlejFShtfvZEx4LFjNi7bqTX/nHTetdv3Ir+J+SanebjBuJgKiAmLIwXviQ0Geq23w==", + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-1.12.7.tgz", + "integrity": "sha512-Ob+xncjuHebjSHqOGh9acXx0Jw2rSTzGP1XiYNKYQnoFtcvKpGqLhr2ibWqS1Z40aEwgI/OvYYldfWsp/C+0mw==", "requires": { "typescript": "3.8.3" } diff --git a/package.json b/package.json index f2f8fdcc8..3781c3f26 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "license": "MIT", "dependencies": { "@popperjs/core": "^2.1.1", - "@stencil/core": "^1.12.6", + "@stencil/core": "^1.12.7", "@stencil/sass": "^1.1.1", "color-convert": "^2.0.1", "feather-icons": "^4.28.0", diff --git a/src/components.d.ts b/src/components.d.ts index ab3fe1ca2..6d33a2a56 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -97,6 +97,10 @@ export namespace Components { "value": string; } interface SlColorPicker { + /** + * The format to use for the generated color `value`. If opacity is enabled, these will translate to HEXA, RGBA, and HSLA respectively. Note that browser support for HEXA doesn't include pre-Chromium Edge, so it's usually safer to use RGBA or HSLA when using opacity. + */ + "format": "hex" | "rgb" | "hsl"; /** * Whether to show the opacity slider. */ @@ -836,6 +840,10 @@ declare namespace LocalJSX { "value"?: string; } interface SlColorPicker { + /** + * The format to use for the generated color `value`. If opacity is enabled, these will translate to HEXA, RGBA, and HSLA respectively. Note that browser support for HEXA doesn't include pre-Chromium Edge, so it's usually safer to use RGBA or HSLA when using opacity. + */ + "format"?: "hex" | "rgb" | "hsl"; /** * Whether to show the opacity slider. */ diff --git a/src/components/color-picker/color-picker.scss b/src/components/color-picker/color-picker.scss index b10dd47ec..4133d9038 100644 --- a/src/components/color-picker/color-picker.scss +++ b/src/components/color-picker/color-picker.scss @@ -59,7 +59,8 @@ border: solid 2px white; margin-top: calc(var(--grid-handle-size) / -2); margin-left: calc(var(--grid-handle-size) / -2); - transition: var(--sl-transition-fast) box-shadow; + transition: var(--sl-transition-fast) box-shadow, var(--sl-transition-x-fast) top ease, + var(--sl-transition-x-fast) left ease; &:focus { outline: none; @@ -99,7 +100,8 @@ border-radius: 50%; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25); margin-left: calc(var(--slider-handle-size) / -2); - transition: var(--sl-transition-fast) box-shadow; + transition: var(--sl-transition-fast) box-shadow, var(--sl-transition-x-fast) top ease, + var(--sl-transition-x-fast) left ease; cursor: pointer; &:focus { @@ -123,11 +125,6 @@ } .sl-color-picker__alpha { - background-image: linear-gradient(45deg, #eee 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #eee 75%), - linear-gradient(45deg, transparent 75%, #eee 75%), linear-gradient(45deg, #eee 25%, transparent 25%); - background-size: 10px 10px; - background-position: 0 0, 0 0, -5px -5px, 5px 5px; - .sl-color-picker__alpha-gradient { position: absolute; top: 0; @@ -143,10 +140,6 @@ position: relative; width: 2rem; height: 2rem; - background-image: linear-gradient(45deg, #eee 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #eee 75%), - linear-gradient(45deg, transparent 75%, #eee 75%), linear-gradient(45deg, #eee 25%, transparent 25%); - background-size: 10px 10px; - background-position: 0 0, 0 0, -5px -5px, 5px 5px; border-radius: 50%; margin-left: var(--sl-spacing-medium); @@ -188,14 +181,31 @@ .sl-color-picker__swatch { flex: 0 0 auto; + position: relative; width: 20px; height: 20px; border-radius: 2px; - border: solid 1px rgba(0, 0, 0, 0.125); - cursor: pointer; + + .sl-color-picker__swatch-color { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: solid 1px rgba(0, 0, 0, 0.125); + border-radius: inherit; + cursor: pointer; + } &:focus { outline: none; box-shadow: var(--sl-focus-ring-box-shadow); } } + +.sl-color-picker__transparent-bg { + background-image: linear-gradient(45deg, #eee 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #eee 75%), + linear-gradient(45deg, transparent 75%, #eee 75%), linear-gradient(45deg, #eee 25%, transparent 25%); + background-size: 10px 10px; + background-position: 0 0, 0 0, -5px -5px, 5px 5px; +} diff --git a/src/components/color-picker/color-picker.tsx b/src/components/color-picker/color-picker.tsx index 680793a76..69eda1b63 100644 --- a/src/components/color-picker/color-picker.tsx +++ b/src/components/color-picker/color-picker.tsx @@ -29,16 +29,24 @@ export class ColorPicker { this.handleGridKeyDown = this.handleGridKeyDown.bind(this); this.handleHueDrag = this.handleHueDrag.bind(this); this.handleHueKeyDown = this.handleHueKeyDown.bind(this); + this.handleUserInput = this.handleUserInput.bind(this); } @State() hue = 0; @State() saturation = 100; - @State() lightness = 50; + @State() lightness = 100; @State() alpha = 100; /** The current color. */ @Prop({ mutable: true, reflect: true }) value = ''; + /** + * The format to use for the generated color `value`. If opacity is enabled, these will translate to HEXA, RGBA, and + * HSLA respectively. Note that browser support for HEXA doesn't include pre-Chromium Edge, so it's usually safer to + * use RGBA or HSLA when using opacity. + */ + @Prop() format: 'hex' | 'rgb' | 'hsl' = 'hex'; + /** Whether to show the opacity slider. */ @Prop() opacity = false; @@ -62,6 +70,46 @@ export class ColorPicker { '#fff' ]; + componentDidLoad() { + this.syncValue(); + } + + getHex() { + const hsl = [this.hue, this.saturation, this.lightness]; + const hex = convert.hsl.hex(hsl); + const alpha = Math.ceil((this.alpha * 255) / 100 + 0x10000) + .toString(16) + .substr(-2) + .toUpperCase(); + + if (this.opacity) { + return `#${hex}${alpha}`; + } else { + return `#${hex}`; + } + } + + getRGB() { + const hsl = [this.hue, this.saturation, this.lightness]; + const rgb = convert.hsl.rgb(hsl); + + if (this.opacity) { + return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${this.alpha}%)`; + } else { + return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`; + } + } + + getHSL() { + const hsl = [this.hue, this.saturation, this.lightness]; + + if (this.opacity) { + return `hsla(${hsl[0]}, ${hsl[1]}%, ${hsl[2]}%, ${this.alpha}%)`; + } else { + return `hsl(${hsl[0]}, ${hsl[1]}%, ${hsl[2]}%)`; + } + } + handleHueInput(event: Event) { const target = event.target as HTMLInputElement; this.hue = clamp(Number(target.value), 0, 360); @@ -91,6 +139,7 @@ export class ColorPicker { this.handleDrag(event, container, x => { this.alpha = clamp(Math.round((x / width) * 100), 0, 100); + this.syncValue(); }); } @@ -103,6 +152,7 @@ export class ColorPicker { this.handleDrag(event, container, x => { this.hue = clamp(Math.round((x / width) * 360), 0, 360); + this.syncValue(); }); } @@ -116,6 +166,7 @@ export class ColorPicker { this.handleDrag(event, container, (x, y) => { this.saturation = clamp(Math.round((x / width) * 100), 0, 100); this.lightness = clamp(Math.round(100 - (y / height) * 100), 0, 100); + this.syncValue(); }); } @@ -215,26 +266,92 @@ export class ColorPicker { } } + handleUserInput(event: KeyboardEvent) { + const target = event.target as HTMLInputElement; + // this.setColor(target.value); + } + + parseColor(color: string) { + const hexPattern = /#?([a-f0-9]{1,2})([a-f0-9]{1,2})([a-f0-9]{1,2})([a-f0-9]{1,2})?/i; + let hue = 0; + let saturation = 0; + let lightness = 0; + let alpha = 100; + + color = color.trim().toLowerCase(); + + // Parse RGB + if (/^rgba?/i.test(color)) { + const rgb = color.replace(/[^\d,.%]/g, '').split(','); + [hue, saturation, lightness] = convert.rgb.hsl(rgb); + + if (rgb[3] && rgb[3].indexOf('%') > -1) { + alpha = Number(rgb[3].replace('%', '')); + } else if (rgb[3]) { + alpha = Number(rgb[3]) * 100; + } + } + + // Parse HSL + if (/^hsla?/i.test(color)) { + const hsl = color.replace(/[^\d,.%]/g, '').split(','); + hue = Number(hsl[0]); + saturation = Number(hsl[0]); + lightness = Number(hsl[0]); + + if (hsl[3] && hsl[3].indexOf('%') > -1) { + alpha = Number(hsl[3].replace('%', '')); + } else if (hsl[3]) { + alpha = Number(hsl[3]) * 100; + } + } + + // Parse hex + if (hexPattern.test(color)) { + const hex = color.match(hexPattern).slice(1, 5); + [hue, saturation, lightness] = convert.hex.hsl(hex.join('')); + + if (hex[3]) { + alpha = Math.round((parseInt(hex[3], 16) / 255) * 100); + console.log(hex[3], alpha); + + // const alpha = (Math.round((this.alpha * 255) / 100) + 0x10000).toString(16).substr(-2).toUpperCase(); + } + } + + return { + hue, + saturation, + lightness, + alpha + }; + } + setColor(color: string) { - // - // TODO: - // - // - detect what format the color is in - // - parse it - // - convert to HSL and update HSLA - // + const hsla = this.parseColor(color); + this.hue = hsla.hue; + this.saturation = hsla.saturation; + this.lightness = hsla.lightness; + this.alpha = this.opacity ? hsla.alpha : 100; + this.syncValue(); + } + + syncValue() { + if (this.format === 'hsl') { + this.value = this.getHSL(); + } else if (this.format === 'rgb') { + this.value = this.getRGB(); + } else { + this.value = this.getHex(); + } } render() { - const hsl = [this.hue, this.saturation, this.lightness]; - // const rgb = convert.hsl.rgb(hsl); - const hex = convert.hsl.hex(hsl); const x = this.saturation; const y = 100 - this.lightness; return (