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

feat(web): add color field component #627

Merged
merged 15 commits into from
Aug 23, 2023
147 changes: 147 additions & 0 deletions web/src/beta/components/fields/ColorField/hooks.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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")} />,
};
Loading
Loading