Skip to content

Commit

Permalink
feat(web): add color field component (#627)
Browse files Browse the repository at this point in the history
Co-authored-by: nina992 <nouralali992@gmail.com>
  • Loading branch information
nina992 and nina992 committed Aug 23, 2023
1 parent ae83bc0 commit 9e895ba
Show file tree
Hide file tree
Showing 14 changed files with 517 additions and 5 deletions.
147 changes: 147 additions & 0 deletions web/src/beta/components/fields/ColorField/hooks.ts
@@ -0,0 +1,147 @@
import { useState, useEffect, useCallback, useRef } from "react";
import tinycolor from "tinycolor2";

import { Params, RGBA } from "./types";
import { getChannelLabel, getChannelValue, getHexString } from "./utils";

export default ({ value, onChange }: Params) => {
const [colorState, setColor] = useState<string>();
const [rgba, setRgba] = useState<RGBA>(tinycolor(value).toRgb());
const [tempColor, setTempColor] = useState(colorState);
const [open, setOpen] = useState(false);
const wrapperRef = useRef<HTMLDivElement>(null);
const pickerRef = useRef<HTMLDivElement>(null);

//Actions

const handleChange = useCallback((newColor: RGBA) => {
const color = getHexString(newColor);
if (!color) return;
setTempColor(color);
setRgba(newColor);
}, []);

const handleHexInput = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
setColor(e.target.value);
setRgba(tinycolor(e.target.value ?? colorState).toRgb());
},
[colorState],
);

const handleRgbaInput = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();

handleChange({
...rgba,
[e.target.name]: e.target.value ? Number(e.target.value) : undefined,
});
},
[handleChange, rgba],
);

const handleClose = useCallback(() => {
if (value || colorState) {
setColor(value ?? colorState);
setRgba(tinycolor(value ?? colorState).toRgb());
} else {
setColor(undefined);
setRgba(tinycolor(colorState == null ? undefined : colorState).toRgb());
}
setTempColor(undefined);
setOpen(false);
}, [value, colorState]);

const handleSave = useCallback(() => {
if (!onChange) return;
if (tempColor && tempColor != value && tempColor != colorState) {
setColor(tempColor);
setRgba(tinycolor(tempColor).toRgb());
onChange(tempColor);
setTempColor(undefined);
} else if (colorState != value && colorState) {
onChange(colorState);
}
setOpen(false);
}, [colorState, onChange, tempColor, value]);

const handleHexSave = useCallback(() => {
const hexPattern = /^#?([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/;
if (colorState && hexPattern.test(colorState)) {
handleSave();
} else {
value && setColor(value);
}
}, [colorState, handleSave, value]);

//events

const handleClick = useCallback(() => setOpen(!open), [open]);

const handleKeyPress = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
handleHexSave();
}
},
[handleHexSave],
);

//UseEffects

useEffect(() => {
if (value) {
setColor(value);
setRgba(tinycolor(value).toRgb());
} else {
setColor(undefined);
}
}, [value]);

useEffect(() => {
if (!value) return;
if (rgba && tinycolor(rgba).toHex8String() !== value) {
setColor(tinycolor(rgba).toHex8String());
}
}, [rgba]); // eslint-disable-line react-hooks/exhaustive-deps

useEffect(() => {
const handleClickOutside = (e: MouseEvent | TouchEvent) => {
if (open && wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {
if (colorState != value && !open) {
handleSave();
}
handleClose();
setOpen(false);
}
};

document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("touchstart", handleClickOutside);

return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("touchstart", handleClickOutside);
};
}, [handleClose]); // eslint-disable-line react-hooks/exhaustive-deps

return {
wrapperRef,
pickerRef,
colorState,
open,
rgba,
getChannelLabel,
getChannelValue,
handleClose,
handleSave,
handleHexSave,
handleChange,
handleRgbaInput,
handleHexInput,
handleClick,
handleKeyPress,
};
};
16 changes: 16 additions & 0 deletions web/src/beta/components/fields/ColorField/index.stories.tsx
@@ -0,0 +1,16 @@
import { action } from "@storybook/addon-actions";
import { Meta, StoryObj } from "@storybook/react";

import ColorField from ".";

const meta: Meta<typeof ColorField> = {
component: ColorField,
};

export default meta;

type Story = StoryObj<typeof ColorField>;

export const ColorFieldInput: Story = {
render: () => <ColorField name="Color Field" onChange={action("onchange")} />,
};

0 comments on commit 9e895ba

Please sign in to comment.