Skip to content

Commit 52d4cc7

Browse files
committed
refactor(color inputs): improve color control and add clipboard feature
1 parent 140b49f commit 52d4cc7

20 files changed

+163
-123
lines changed

jest.setup.ts

+5
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,8 @@ global.ResizeObserver = jest.fn(() => ({
55
disconnect: jest.fn(),
66
unobserve: jest.fn(),
77
}));
8+
9+
jest.mock("@uidotdev/usehooks", () => ({
10+
__esModule: true,
11+
useCopyToClipboard: jest.fn(() => ["", jest.fn()]),
12+
}));

src/components/color-inputs/__tests__/panel.test.tsx

-19
This file was deleted.

src/components/color-inputs/background.tsx

+2-11
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,10 @@ import { useAtom } from "jotai";
44

55
import { background } from "@/store";
66

7-
import { Panel } from "./panel";
8-
import { ColorControl, PopoverColorChannels } from "./color-control";
7+
import { ColorControl } from "./color-control";
98

109
export function BackgroundInput() {
1110
const [bg, setBg] = useAtom(background);
1211

13-
return (
14-
<Panel color={bg} label="Background Color">
15-
<ColorControl
16-
popover={<PopoverColorChannels sourceColor={bg} onChange={setBg} />}
17-
sourceColor={bg}
18-
onChange={setBg}
19-
/>
20-
</Panel>
21-
);
12+
return <ColorControl label="Background Color" sourceColor={bg} onChange={setBg} />;
2213
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { describe, it } from "@jest/globals";
2+
import { fireEvent, render } from "@testing-library/react";
3+
4+
import { ButtonClipboard } from "../button-clipboard";
5+
6+
describe("Color Control / Button Picker", () => {
7+
it("Correct rendering and unmount", () => {
8+
const screen = render(<ButtonClipboard />);
9+
10+
expect(() => screen.unmount()).not.toThrow();
11+
});
12+
13+
it("Should icon check on click", () => {
14+
const screen = render(<ButtonClipboard color="#000" />);
15+
const btn = screen.getByRole("button");
16+
17+
fireEvent.click(btn);
18+
19+
const checkSvg = screen.getByLabelText("check");
20+
21+
expect(checkSvg).toBeInTheDocument();
22+
});
23+
});

src/components/color-inputs/color-control/__tests__/color-control.test.tsx

+6-11
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,21 @@ import { describe, it } from "@jest/globals";
22
import { fireEvent, render } from "@testing-library/react";
33
import { useState } from "react";
44

5-
import { ColorControl, PopoverColorChannels } from "..";
5+
import { ColorControl } from "..";
66

77
describe("Color Control", () => {
88
it("Correct rendering and unmount", () => {
9-
const screen = render(<ColorControl popover={<PopoverColorChannels />} />);
9+
const screen = render(<ColorControl label="Background" />);
1010

11+
expect(screen.getByText("Background")).toBeInTheDocument();
1112
expect(() => screen.unmount()).not.toThrow();
1213
});
1314

1415
it("Should update value on change input", () => {
1516
function Render() {
1617
const [value, setValue] = useState("#000");
1718

18-
return (
19-
<ColorControl
20-
popover={<PopoverColorChannels />}
21-
sourceColor={value}
22-
onChange={(value) => setValue(value)}
23-
/>
24-
);
19+
return <ColorControl sourceColor={value} onChange={(value) => setValue(value)} />;
2520
}
2621

2722
const screen = render(<Render />);
@@ -35,8 +30,8 @@ describe("Color Control", () => {
3530
});
3631

3732
it("Should show popover color channels", () => {
38-
const screen = render(<ColorControl popover={<PopoverColorChannels />} />);
39-
const buttonSettings = screen.getByLabelText("settings button");
33+
const screen = render(<ColorControl />);
34+
const [buttonSettings] = screen.getAllByLabelText("picker button");
4035

4136
fireEvent.click(buttonSettings);
4237

src/components/color-inputs/color-control/__tests__/popover-color-channels.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useAtom } from "jotai";
55

66
import { pickingColor } from "@/store";
77

8-
import { PopoverColorChannels } from "..";
8+
import { PopoverColorChannels } from "../popover-color-channels";
99

1010
describe("Popover Color Channels", () => {
1111
it("Correct rendering and unmount", () => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useEffect, useRef } from "react";
2+
import { useCopyToClipboard } from "@uidotdev/usehooks";
3+
import { css } from "@root/styled-system/css";
4+
5+
import { useToggle } from "@/hooks/use-toggle";
6+
import { Button } from "@/components/primitives/button";
7+
import { CheckFill, ClipboardLine } from "@/components/icons";
8+
9+
export function ButtonClipboard({ color }: { color?: string }) {
10+
const [copiedText, copyToClipboard] = useCopyToClipboard();
11+
const { isEnabled, onOpen, onClose } = useToggle();
12+
const timeoutId = useRef<number>();
13+
14+
function onClick() {
15+
if (color) copyToClipboard(color);
16+
onOpen();
17+
}
18+
19+
useEffect(() => {
20+
if (timeoutId.current) clearTimeout(timeoutId.current);
21+
22+
timeoutId.current = window.setTimeout(onClose, 1500);
23+
}, [copiedText, onClose, timeoutId]);
24+
25+
return (
26+
<Button
27+
aria-label="picker button"
28+
className={css({ border: "none", fontSize: "icon-24" })}
29+
onClick={onClick}
30+
>
31+
{isEnabled ? <CheckFill aria-label="check" /> : <ClipboardLine aria-label="clipboard" />}
32+
</Button>
33+
);
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as Popover from "@radix-ui/react-popover";
2+
import { css } from "@root/styled-system/css";
3+
4+
import { useToggle } from "@/hooks/use-toggle";
5+
import { Button } from "@/components/primitives/button";
6+
import { ColorPickerFill } from "@/components/icons";
7+
8+
import { PopoverColorChannels } from "./popover-color-channels";
9+
10+
interface ButtonPickerProps {
11+
sourceColor?: string;
12+
onChange?: (color: string) => void;
13+
}
14+
15+
export function ButtonPicker({ sourceColor, onChange }: ButtonPickerProps) {
16+
const { isEnabled: isOpen, setOpen } = useToggle();
17+
18+
return (
19+
<Popover.Root open={isOpen} onOpenChange={setOpen}>
20+
<Popover.Trigger asChild>
21+
<Button aria-label="picker button" className={css({ border: "none", fontSize: "icon-24" })}>
22+
<ColorPickerFill />
23+
</Button>
24+
</Popover.Trigger>
25+
{isOpen ? <PopoverColorChannels sourceColor={sourceColor} onChange={onChange} /> : null}
26+
</Popover.Root>
27+
);
28+
}
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import { css } from "@root/styled-system/css";
2+
import { stack } from "@root/styled-system/patterns";
23

34
export default {
45
root: css({ display: "flex", alignItems: "center", flex: 1, width: "100%", gap: "4" }),
56

67
label: css({
7-
color: "text-primary",
8-
fontWeight: "bold",
8+
color: "text-secondary",
99
display: "block",
10-
mb: "3",
10+
fontWeight: "500",
11+
mb: "2",
1112
}),
1213

13-
rightContent: css({ flex: 1 }),
14+
content: css({ flex: 1 }),
1415

1516
preview: css({
1617
w: "20",
@@ -19,5 +20,8 @@ export default {
1920
rounded: "xl",
2021
border: "1px solid",
2122
borderColor: "border-secondary",
23+
flexShrink: 0,
2224
}),
25+
26+
actions: stack({ gap: "1" }),
2327
};
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,28 @@
11
"use client";
22

3-
import type { ReactNode } from "react";
4-
5-
import * as Popover from "@radix-ui/react-popover";
6-
7-
import { useToggle } from "@/hooks/use-toggle";
8-
93
import { ColorInput } from "./color-input";
4+
import { ButtonPicker } from "./button-picker";
5+
import { ButtonClipboard } from "./button-clipboard";
6+
import classes from "./color-control.styled";
107

118
interface Iprops {
12-
popover: ReactNode;
9+
label?: string;
1310
sourceColor?: string;
1411
onChange?: (color: string) => void;
1512
}
1613

17-
export function ColorControl({ popover, sourceColor, onChange }: Iprops) {
18-
const { isEnabled: isOpen, setOpen } = useToggle();
19-
14+
export function ColorControl({ sourceColor, onChange, label }: Iprops) {
2015
return (
21-
<Popover.Root open={isOpen} onOpenChange={setOpen}>
22-
<ColorInput value={sourceColor} onChange={onChange} />
23-
{isOpen ? popover : null}
24-
</Popover.Root>
16+
<div className={classes.root}>
17+
<div className={classes.preview} style={{ backgroundColor: sourceColor }} />
18+
<div className={classes.content}>
19+
<label className={classes.label}>{label}</label>
20+
<ColorInput value={sourceColor} onChange={onChange} />
21+
</div>
22+
<div className={classes.actions}>
23+
<ButtonPicker sourceColor={sourceColor} onChange={onChange} />
24+
<ButtonClipboard color={sourceColor} />
25+
</div>
26+
</div>
2527
);
2628
}
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import { css } from "@root/styled-system/css";
22

33
export default {
4-
settings: css({
5-
color: "fg-primary",
6-
borderLeft: "1px solid",
7-
borderColor: "border-secondary",
8-
pl: "4",
9-
my: "2",
10-
display: "inline-block",
11-
cursor: "pointer",
12-
}),
4+
root: css({ display: "flex", alignItems: "center", gap: "1", flex: 1 }),
5+
6+
icon: css({ fontSize: "icon-24" }),
137

14-
icon: css({ fontSize: "icon-20" }),
8+
input: css({
9+
textStyle: "display-md",
10+
fontWeight: "500",
11+
width: "100%",
12+
"&:focus": {
13+
outlineColor: "text-primary",
14+
},
15+
}),
1516
};

src/components/color-inputs/color-control/color-input.tsx

+7-17
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@
22

33
import type { ChangeEvent } from "react";
44

5-
import * as Popover from "@radix-ui/react-popover";
6-
7-
import { ColorPickerFill } from "@/components/icons";
8-
import { Input } from "@/components/primitives/input";
9-
import { InputGroup } from "@/components/primitives/input-group";
105
import { useControllable } from "@/hooks/use-controllable";
6+
import { InputGroup } from "@/components/primitives/input-group";
7+
import { PencilFill } from "@/components/icons";
118

129
import { regexColorHexadecimal } from "./utils";
1310
import classes from "./color-input.styled";
@@ -24,7 +21,6 @@ export function ColorInput({ value: valueProp, onChange }: Iprops) {
2421

2522
function handleChange(e: ChangeEvent<HTMLInputElement>) {
2623
const nextValue = e.target.value;
27-
2824
const omitOnChange = !regexColorHexadecimal.test(nextValue);
2925

3026
setValue(nextValue, omitOnChange);
@@ -35,22 +31,16 @@ export function ColorInput({ value: valueProp, onChange }: Iprops) {
3531
}
3632

3733
return (
38-
<InputGroup
39-
endContent={
40-
<Popover.Trigger asChild>
41-
<button aria-label="settings button" className={classes.settings} type="button">
42-
<ColorPickerFill className={classes.icon} />
43-
</button>
44-
</Popover.Trigger>
45-
}
46-
>
47-
<Input
34+
<div className={classes.root}>
35+
<PencilFill className={classes.icon} />
36+
<input
37+
className={classes.input}
4838
placeholder={fallback}
4939
type="text"
5040
value={value}
5141
onBlur={handleBlur}
5242
onChange={handleChange}
5343
/>
54-
</InputGroup>
44+
</div>
5545
);
5646
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
11
export { ColorControl } from "./color-control";
2-
3-
export { PopoverColorChannels } from "./popover-color-channels";

src/components/color-inputs/foreground.tsx

+2-11
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,10 @@ import { useAtom } from "jotai";
44

55
import { foreground } from "@/store";
66

7-
import { Panel } from "./panel";
8-
import { ColorControl, PopoverColorChannels } from "./color-control";
7+
import { ColorControl } from "./color-control";
98

109
export function ForegroundInput() {
1110
const [fg, setFg] = useAtom(foreground);
1211

13-
return (
14-
<Panel color={fg} label="Text Color">
15-
<ColorControl
16-
popover={<PopoverColorChannels sourceColor={fg} onChange={setFg} />}
17-
sourceColor={fg}
18-
onChange={setFg}
19-
/>
20-
</Panel>
21-
);
12+
return <ColorControl label="Text Color" sourceColor={fg} onChange={setFg} />;
2213
}

src/components/color-inputs/panel.tsx

-21
This file was deleted.

src/components/color-inputs/suggestions-button.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ export function SuggestionsButton() {
2121
aria-label="swap button"
2222
className={css({
2323
fontSize: "icon-24",
24-
borderColor: "transparent",
2524
display: { lg: "none" },
2625
})}
2726
onClick={onOpen}

0 commit comments

Comments
 (0)