From 9edceaec15775c4e090ad03b2b4a40d4714d13f7 Mon Sep 17 00:00:00 2001 From: Vlad Shilov Date: Thu, 20 Aug 2020 17:36:17 +0300 Subject: [PATCH 01/11] `HexInput` component --- .eslintrc | 1 + demo/src/index.html | 2 +- demo/src/index.js | 8 ++++++- demo/src/styles.css | 47 +++++++++++++++++++++++++++++++++++++- src/components/HexInput.js | 45 ++++++++++++++++++++++++++++++++++++ src/packages/HexInput.js | 2 ++ src/utils/validHex.js | 6 +++++ tests/utils.test.js | 18 +++++++++++++++ 8 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 src/components/HexInput.js create mode 100644 src/packages/HexInput.js create mode 100644 src/utils/validHex.js diff --git a/.eslintrc b/.eslintrc index 7dc4f550..342f0ebf 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,6 +4,7 @@ "es6": true }, "parserOptions": { + "ecmaVersion": 2018, "sourceType": "module" }, "plugins": ["prettier", "react"], diff --git a/demo/src/index.html b/demo/src/index.html index 42cd980c..cef995bb 100644 --- a/demo/src/index.html +++ b/demo/src/index.html @@ -9,7 +9,7 @@ name="viewport" /> diff --git a/demo/src/index.js b/demo/src/index.js index 651bf728..ab8bdbe1 100644 --- a/demo/src/index.js +++ b/demo/src/index.js @@ -1,6 +1,7 @@ import React, { useState, useEffect, useCallback } from "react"; import ReactDOM from "react-dom"; import ColorPicker from "../../src"; +import HexInput from "../../src/components/HexInput"; import hexToRgb from "../../src/utils/hexToRgb"; import styles from "./styles.css"; import useFaviconColor from "./hooks/useFaviconColor"; @@ -29,7 +30,12 @@ const Demo = () => { return (
- +
+ +
+ +
+

React Colorful 🎨

diff --git a/demo/src/styles.css b/demo/src/styles.css index b9acbab8..45e12a04 100644 --- a/demo/src/styles.css +++ b/demo/src/styles.css @@ -17,12 +17,53 @@ body { transition: color 0.15s; } -.colorPicker { +.demo { + position: relative; + width: 200px; flex-shrink: 0; +} + +.colorPicker { + width: 100%; border-radius: 9px; box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2); } +.field { + position: absolute; + top: calc(100% + 16px); + left: calc(50% - 45px); + width: 90px; + font-family: "PT Mono", monospace; +} + +.field:before { + content: "#"; + position: absolute; + top: 0; + left: 0; + pointer-events: none; + opacity: 0.33; +} + +.hexInput { + display: block; + color: inherit; + width: 100%; + padding-left: 0.7em; + background: none; + outline: none; + font-variant-numeric: tabular-nums; + text-transform: uppercase; + opacity: 0.5; + transition: opacity 0.2s; +} + +.hexInput:focus, +.hexInput:hover { + opacity: 1; +} + .header { display: flex; align-items: center; @@ -85,4 +126,8 @@ body { margin-left: auto; margin-right: auto; } + + .field { + display: none; + } } diff --git a/src/components/HexInput.js b/src/components/HexInput.js new file mode 100644 index 00000000..2f360660 --- /dev/null +++ b/src/components/HexInput.js @@ -0,0 +1,45 @@ +import React, { useState, useEffect, useCallback } from "react"; +import validHex from "../utils/validHex"; + +const escape = (hex) => hex.replace(/([^0-9A-F]+)/gi, ""); + +const HexInput = ({ color, onChange, ...rest }) => { + const [value, setValue] = useState(escape(color)); + + const handleChange = useCallback( + (e) => { + const inputValue = escape(e.target.value); + setValue(inputValue); + if (onChange && validHex(inputValue)) onChange("#" + inputValue); + }, + [onChange] + ); + + const handleBlur = useCallback( + (e) => { + if (!validHex(e.target.value)) setValue(escape(color)); + }, + [color] + ); + + useEffect(() => { + setValue(escape(color)); + }, [color]); + + return ( + + ); +}; + +HexInput.defaultProps = { + color: "", +}; + +export default React.memo(HexInput); diff --git a/src/packages/HexInput.js b/src/packages/HexInput.js new file mode 100644 index 00000000..033a43e4 --- /dev/null +++ b/src/packages/HexInput.js @@ -0,0 +1,2 @@ +import HexInput from "../components/HexInput"; +export default HexInput; diff --git a/src/utils/validHex.js b/src/utils/validHex.js new file mode 100644 index 00000000..f85632e6 --- /dev/null +++ b/src/utils/validHex.js @@ -0,0 +1,6 @@ +const hex3 = /^#?[0-9A-F]{3}$/i; +const hex6 = /^#?[0-9A-F]{6}$/i; + +const validHex = (color) => hex6.test(color) || hex3.test(color); + +export default validHex; diff --git a/tests/utils.test.js b/tests/utils.test.js index c72e1236..ce052709 100644 --- a/tests/utils.test.js +++ b/tests/utils.test.js @@ -2,6 +2,7 @@ import hexToHsv from "../src/utils/hexToHsv"; import hsvToHex from "../src/utils/hsvToHex"; import equalHex from "../src/utils/equalHex"; +import validHex from "../src/utils/validHex"; // HSL import hsvToHsl from "../src/utils/hsvToHsl"; import hslToHsv from "../src/utils/hslToHsv"; @@ -110,6 +111,23 @@ it("Compares two HSV colors", () => { expect(equalColorObjects({ h: 1, s: 2, v: 3 }, { h: 4, s: 5, v: 6 })).toBe(false); }); +it("Validates HEX colors", () => { + // valid strings + expect(validHex("#8c0dba")).toBe(true); + expect(validHex("aabbcc")).toBe(true); + expect(validHex("#ABC")).toBe(true); + expect(validHex("123")).toBe(true); + // out of [0-F] range + expect(validHex("#eeffhh")).toBe(false); + // wrong length + expect(validHex("#12")).toBe(false); + expect(validHex("#12345")).toBe(false); + // empty + expect(validHex("")).toBe(false); + expect(validHex(null)).toBe(false); + expect(validHex()).toBe(false); +}); + it("Formats a class name", () => { expect(formatClassName(["one"])).toBe("one"); expect(formatClassName(["one", "two", "three"])).toBe("one two three"); From 7d4d3a693013aa9f9d9991eb9777d0e0f0f70f50 Mon Sep 17 00:00:00 2001 From: Vlad Shilov Date: Thu, 20 Aug 2020 17:43:19 +0300 Subject: [PATCH 02/11] Publish `HexInput` package to NPM --- .gitignore | 1 + package.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9437d4a0..8a1fa104 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ dist/ /hsv /rgb /rgbString +/HexInput # OSX .DS_Store diff --git a/package.json b/package.json index e5ffcc9b..a4487c3f 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,8 @@ "hslString", "hsv", "rgb", - "rgbString" + "rgbString", + "HexInput" ], "repository": "omgovich/react-colorful", "keywords": [ From 99614513ab17cf2c22f4edafaccb23f7b766a5e2 Mon Sep 17 00:00:00 2001 From: Vlad Shilov Date: Thu, 20 Aug 2020 21:17:52 +0300 Subject: [PATCH 03/11] Cover `HexInput` with tests --- tests/__snapshots__/components.test.js.snap | 8 ++++++++ tests/components.test.js | 17 +++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/tests/__snapshots__/components.test.js.snap b/tests/__snapshots__/components.test.js.snap index 1209b583..320c3476 100644 --- a/tests/__snapshots__/components.test.js.snap +++ b/tests/__snapshots__/components.test.js.snap @@ -1,5 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Renders \`HexInput\` component 1`] = ` + +`; + exports[`Renders proper HTML 1`] = `
{ expect(handleChange).toHaveReturned(); }); + +it("Renders `HexInput` component", () => { + const result = render(); + + expect(result.container.firstChild).toMatchSnapshot(); +}); + +it("Fires `onChange` when user changes `HexInput` value", () => { + const handleChange = jest.fn((hex) => hex); + const result = render(); + const input = result.container.firstChild; + + fireEvent.change(input, { target: { value: "112233" } }); + + expect(handleChange).toHaveReturnedWith("#112233"); +}); From bb833afbd586858129918658dbbf83472e342a81 Mon Sep 17 00:00:00 2001 From: Vlad Shilov Date: Thu, 20 Aug 2020 21:22:16 +0300 Subject: [PATCH 04/11] Improve `HexInput` snapshot test --- tests/__snapshots__/components.test.js.snap | 4 +++- tests/components.test.js | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/__snapshots__/components.test.js.snap b/tests/__snapshots__/components.test.js.snap index 320c3476..8b7d6015 100644 --- a/tests/__snapshots__/components.test.js.snap +++ b/tests/__snapshots__/components.test.js.snap @@ -1,8 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Renders \`HexInput\` component 1`] = ` +exports[`Renders \`HexInput\` component properly 1`] = ` diff --git a/tests/components.test.js b/tests/components.test.js index 0c43a2e5..84e723b9 100644 --- a/tests/components.test.js +++ b/tests/components.test.js @@ -80,8 +80,8 @@ it("Triggers `onChange` after a touch interaction", async () => { expect(handleChange).toHaveReturned(); }); -it("Renders `HexInput` component", () => { - const result = render(); +it("Renders `HexInput` component properly", () => { + const result = render(); expect(result.container.firstChild).toMatchSnapshot(); }); From 59bb45a2aadb3f95674afaceea5fa06faa2a2311 Mon Sep 17 00:00:00 2001 From: Vlad Shilov Date: Thu, 20 Aug 2020 21:41:03 +0300 Subject: [PATCH 05/11] Add comment to `HexInput` coomponent code --- src/components/HexInput.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/HexInput.js b/src/components/HexInput.js index 2f360660..f2fcc49b 100644 --- a/src/components/HexInput.js +++ b/src/components/HexInput.js @@ -1,11 +1,13 @@ import React, { useState, useEffect, useCallback } from "react"; import validHex from "../utils/validHex"; +// Escapes all non-hexadecimal characters including "#" const escape = (hex) => hex.replace(/([^0-9A-F]+)/gi, ""); const HexInput = ({ color, onChange, ...rest }) => { const [value, setValue] = useState(escape(color)); + // Trigger `onChange` handler only if the input value is a valid HEX-color const handleChange = useCallback( (e) => { const inputValue = escape(e.target.value); @@ -15,6 +17,7 @@ const HexInput = ({ color, onChange, ...rest }) => { [onChange] ); + // Take the color from props if the last typed color (in local state) is not valid const handleBlur = useCallback( (e) => { if (!validHex(e.target.value)) setValue(escape(color)); @@ -22,6 +25,7 @@ const HexInput = ({ color, onChange, ...rest }) => { [color] ); + // Update the local state when `color` property value is changed useEffect(() => { setValue(escape(color)); }, [color]); From 13be0954afaaddf5896a69ea7119e89412f617d9 Mon Sep 17 00:00:00 2001 From: Vlad Shilov Date: Thu, 20 Aug 2020 22:42:57 +0300 Subject: [PATCH 06/11] Add `HexInput` to README --- README.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7551680a..0d6162aa 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,35 @@ The easiest way to tweak react-colorful is to create another stylesheet to overr [See examples →](https://codesandbox.io/s/react-colorful-customization-demo-mq85z?file=/src/styles.css) +## How to paste or type a color? + +**react-colorful**'s color picker itself doesn't include any input field, but don't worry if you need one. Since `v2.1` we provide a separate component that works perfectly in pair with our color picker. + +
+ How to use `HexInput` + +```diff +import ColorPicker from "react-colorful"; ++import HexInput from "react-colorful/HexInput"; +import "react-colorful/dist/index.css"; + +const YourComponent = () => { + const [color, setColor] = useState("#aabbcc"); + return ( +
+ ++ +
+ ); +}; +``` + +`HexInput` doesn't have any default styles, but accepts all properties that a regular `input` tag does (such as `className`, `placeholder` and `autoFocus`). That means you can place and modify this component as you like. Also, that allows you to combine the color picker and input in different ways. + +By the way, `HexInput` is also minimalist-friendly — only 500 bytes gzipped. + +
+ ## Why react-colorful? Today each dependency drags more dependencies and increases your project’s bundle size uncontrollably. But size is very important for everything that intends to work in a browser. @@ -149,5 +178,5 @@ To show you the problem that **react-colorful** is trying to solve, we have perf ## Roadmap - [x] Additional modules to support different color models (like HSL and RGB) -- [ ] HEX input component +- [x] HEX input component - [ ] Preact support From 540c39b4ea1e15a724b1a1e2b8aad132c7007b54 Mon Sep 17 00:00:00 2001 From: Vlad Shilov Date: Thu, 20 Aug 2020 22:52:03 +0300 Subject: [PATCH 07/11] Update `HexInput` summary --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0d6162aa..19fad17f 100644 --- a/README.md +++ b/README.md @@ -119,10 +119,10 @@ The easiest way to tweak react-colorful is to create another stylesheet to overr ## How to paste or type a color? -**react-colorful**'s color picker itself doesn't include any input field, but don't worry if you need one. Since `v2.1` we provide a separate component that works perfectly in pair with our color picker. +As you probably noticed the color picker itself doesn't include an input field, but don't worry if you need one. **react-colorful** is a modular library that allows you to build any picker you need. Since `v2.1` we provide an additional component that works perfectly in pair with our color picker.
- How to use `HexInput` + How to use HexInput ```diff import ColorPicker from "react-colorful"; From a4ea719eceb5a352066da063b8aa8929d8b1e328 Mon Sep 17 00:00:00 2001 From: Vlad Shilov Date: Thu, 20 Aug 2020 23:09:34 +0300 Subject: [PATCH 08/11] Get rid of the spread operator polyfill --- .eslintrc | 1 - demo/src/styles.css | 1 - src/components/HexInput.js | 23 ++++++++++++----------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/.eslintrc b/.eslintrc index 342f0ebf..7dc4f550 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,7 +4,6 @@ "es6": true }, "parserOptions": { - "ecmaVersion": 2018, "sourceType": "module" }, "plugins": ["prettier", "react"], diff --git a/demo/src/styles.css b/demo/src/styles.css index 45e12a04..ebb10b46 100644 --- a/demo/src/styles.css +++ b/demo/src/styles.css @@ -53,7 +53,6 @@ body { padding-left: 0.7em; background: none; outline: none; - font-variant-numeric: tabular-nums; text-transform: uppercase; opacity: 0.5; transition: opacity 0.2s; diff --git a/src/components/HexInput.js b/src/components/HexInput.js index f2fcc49b..cd49739e 100644 --- a/src/components/HexInput.js +++ b/src/components/HexInput.js @@ -4,7 +4,8 @@ import validHex from "../utils/validHex"; // Escapes all non-hexadecimal characters including "#" const escape = (hex) => hex.replace(/([^0-9A-F]+)/gi, ""); -const HexInput = ({ color, onChange, ...rest }) => { +const HexInput = (props) => { + const { color, onChange } = props; const [value, setValue] = useState(escape(color)); // Trigger `onChange` handler only if the input value is a valid HEX-color @@ -30,16 +31,16 @@ const HexInput = ({ color, onChange, ...rest }) => { setValue(escape(color)); }, [color]); - return ( - - ); + // Spread operator replacement to get rid of the polyfill (saves 150 bytes gzipped) + const inputProps = Object.assign({}, props, { + value, + maxLength: 6, + spellCheck: "false", // the element should not be checked for spelling errors + onChange: handleChange, + onBlur: handleBlur, + }); + + return React.createElement("input", inputProps); }; HexInput.defaultProps = { From 227b920af8d050b0104f5faf3a7b990b37134f37 Mon Sep 17 00:00:00 2001 From: Vlad Shilov Date: Thu, 20 Aug 2020 23:14:34 +0300 Subject: [PATCH 09/11] Update size-limit settings --- README.md | 2 +- package.json | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 19fad17f..70a6d8e1 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ const YourComponent = () => { `HexInput` doesn't have any default styles, but accepts all properties that a regular `input` tag does (such as `className`, `placeholder` and `autoFocus`). That means you can place and modify this component as you like. Also, that allows you to combine the color picker and input in different ways. -By the way, `HexInput` is also minimalist-friendly — only 500 bytes gzipped. +By the way, `HexInput` is also minimalist-friendly — only 400 bytes gzipped.
diff --git a/package.json b/package.json index a4487c3f..e8d0b19e 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,10 @@ { "path": "hsv/index.js", "limit": "2 KB" + }, + { + "path": "HexInput/index.module.js", + "limit": "1 KB" } ], "jest": { From d59c6e6e4f6cbd8718a6923933e9ae43ef13c967 Mon Sep 17 00:00:00 2001 From: Vlad Shilov Date: Thu, 20 Aug 2020 23:17:12 +0300 Subject: [PATCH 10/11] Do not add `color` attr to `input`-tag --- src/components/HexInput.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/HexInput.js b/src/components/HexInput.js index cd49739e..2cc82832 100644 --- a/src/components/HexInput.js +++ b/src/components/HexInput.js @@ -33,6 +33,7 @@ const HexInput = (props) => { // Spread operator replacement to get rid of the polyfill (saves 150 bytes gzipped) const inputProps = Object.assign({}, props, { + color: null, // do not add `color` attr to `input`-tag value, maxLength: 6, spellCheck: "false", // the element should not be checked for spelling errors From 17a026a0c377a0fac3ebba5f9a32b0efbfffb718 Mon Sep 17 00:00:00 2001 From: Vlad Shilov Date: Thu, 20 Aug 2020 23:40:43 +0300 Subject: [PATCH 11/11] Update `Why react-colorful?` section --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 70a6d8e1..ae18e8cf 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ Today each dependency drags more dependencies and increases your project’s bun - has no dependencies (no risks in terms of vulnerabilities, no unexpected bundle size changes); - built with hooks and functional components only (no classes and polyfills for them); -- a lot of things that you probably don't need (like 8-digit HEX colors support) were stripped out. +- ships only a minimal amount of manually optimized color conversion algorithms (while most of the popular pickers import entire color manipulation libraries that increase the bundle size by more than 10 KB and make your app slower). To show you the problem that **react-colorful** is trying to solve, we have performed a simple benchmark (using [size-limit](https://github.com/ai/size-limit)) against popular React color picker libraries: