Skip to content
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

HexInput component #18

Merged
merged 11 commits into from
Aug 21, 2020
Merged
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ dist/
/hsv
/rgb
/rgbString
/HexInput

# OSX
.DS_Store
Expand Down
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?

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.

<details>
<summary>How to use <code>HexInput</code></summary>

```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 (
<div>
<ColorPicker color={color} onChange={setColor} />
+ <HexInput color={color} onChange={setColor} />
</div>
);
};
```

`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 400 bytes gzipped.

</details>

## 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.
Expand All @@ -125,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:

Expand All @@ -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
2 changes: 1 addition & 1 deletion demo/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
name="viewport"
/>
<link
href="https://fonts.googleapis.com/css2?family=Recursive&family=Source+Sans+Pro&display=swap"
href="https://fonts.googleapis.com/css2?family=Recursive&family=PT+Mono&display=swap"
rel="stylesheet"
/>
</head>
Expand Down
8 changes: 7 additions & 1 deletion demo/src/index.js
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -29,7 +30,12 @@ const Demo = () => {
return (
<div className={styles.wrapper} style={{ color: textColor }}>
<header className={styles.header}>
<ColorPicker className={styles.colorPicker} color={color} onChange={handleChange} />
<div className={styles.demo}>
<ColorPicker className={styles.colorPicker} color={color} onChange={handleChange} />
<div className={styles.field}>
<HexInput className={styles.hexInput} color={color} onChange={handleChange} />
</div>
</div>
<div className={styles.headerContent}>
<h1 className={styles.headerTitle}>React Colorful 🎨</h1>
<h2 className={styles.headerDescription}>
Expand Down
46 changes: 45 additions & 1 deletion demo/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,52 @@ 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;
text-transform: uppercase;
opacity: 0.5;
transition: opacity 0.2s;
}

.hexInput:focus,
.hexInput:hover {
opacity: 1;
}

.header {
display: flex;
align-items: center;
Expand Down Expand Up @@ -85,4 +125,8 @@ body {
margin-left: auto;
margin-right: auto;
}

.field {
display: none;
}
}
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
{
"path": "hsv/index.js",
"limit": "2 KB"
},
{
"path": "HexInput/index.module.js",
"limit": "1 KB"
}
],
"jest": {
Expand All @@ -61,7 +65,8 @@
"hslString",
"hsv",
"rgb",
"rgbString"
"rgbString",
"HexInput"
],
"repository": "omgovich/react-colorful",
"keywords": [
Expand Down
51 changes: 51 additions & 0 deletions src/components/HexInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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 = (props) => {
const { color, onChange } = props;
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);
setValue(inputValue);
if (onChange && validHex(inputValue)) onChange("#" + inputValue);
},
[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));
},
[color]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, that's smart!

);

// Update the local state when `color` property value is changed
useEffect(() => {
setValue(escape(color));
}, [color]);

// 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
onChange: handleChange,
onBlur: handleBlur,
});

return React.createElement("input", inputProps);
};

HexInput.defaultProps = {
color: "",
};

export default React.memo(HexInput);
2 changes: 2 additions & 0 deletions src/packages/HexInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import HexInput from "../components/HexInput";
export default HexInput;
6 changes: 6 additions & 0 deletions src/utils/validHex.js
Original file line number Diff line number Diff line change
@@ -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;
10 changes: 10 additions & 0 deletions tests/__snapshots__/components.test.js.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Renders \`HexInput\` component properly 1`] = `
<input
class="custom-input"
maxlength="6"
placeholder="AABBCC"
spellcheck="false"
value="F00"
/>
`;

exports[`Renders proper HTML 1`] = `
<div
class="react-colorful container"
Expand Down
17 changes: 17 additions & 0 deletions tests/components.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import { render, cleanup, fireEvent, waitFor } from "@testing-library/react";
import ColorPicker from "../src/";
import HexInput from "../src/packages/HexInput";

afterEach(cleanup);

Expand Down Expand Up @@ -78,3 +79,19 @@ it("Triggers `onChange` after a touch interaction", async () => {

expect(handleChange).toHaveReturned();
});

it("Renders `HexInput` component properly", () => {
const result = render(<HexInput className="custom-input" color="#F00" placeholder="AABBCC" />);

expect(result.container.firstChild).toMatchSnapshot();
});

it("Fires `onChange` when user changes `HexInput` value", () => {
const handleChange = jest.fn((hex) => hex);
const result = render(<HexInput onChange={handleChange} />);
const input = result.container.firstChild;

fireEvent.change(input, { target: { value: "112233" } });

expect(handleChange).toHaveReturnedWith("#112233");
});
18 changes: 18 additions & 0 deletions tests/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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");
Expand Down