Skip to content

Commit 58155af

Browse files
Merge pull request #4 from matheusrocha89/custom-icons
Custom icons
2 parents 59814f6 + 58aa2af commit 58155af

File tree

6 files changed

+278
-45
lines changed

6 files changed

+278
-45
lines changed

README.md

Lines changed: 95 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
- 🎨 Fully customizable styling
99
- 🔄 Controlled component
1010
- 🚀 TypeScript support
11+
- 🎨 Custom icons support
12+
- 📝 Label support
13+
- 🔤 Multiple input types
1114

1215
## 📦 Installation
1316

@@ -29,70 +32,130 @@ function App() {
2932

3033
## 🔧 Props
3134

32-
| Prop | Type | Default | Description |
33-
| -------------------- | ----------------------- | -------------- | ------------------------------------- |
34-
| value | string | "" | Text to display and edit |
35-
| isEditing | boolean | false | Initial editing state |
36-
| inputType | string | "text" | Type of input field |
37-
| label | string | "" | Label for the input |
38-
| className | string | "" | Container class name |
39-
| inputClassName | string | "" | Input field class name |
40-
| editButtonClassName | string | "" | Edit button class name |
41-
| saveButtonClassName | string | "" | Save button class name |
42-
| editWrapperClassName | string | "" | Edit mode wrapper class name |
43-
| saveButtonLabel | React.ReactNode | "Save" | Custom save button label |
44-
| editButtonLabel | React.ReactNode | "Edit" | Custom edit button label |
45-
| showIcons | boolean | false | Show icons in buttons |
46-
| editIcon | React.ReactNode | `<LuPencil />` | Custom edit icon |
47-
| saveIcon | React.ReactNode | `<LuCheck />` | Custom save icon |
48-
| iconPosition | "left" \| "right" | "left" | Position of icons in buttons |
49-
| onEditButtonClick | () => void | () => {} | Callback when edit button is clicked |
50-
| onInputChange | (value: string) => void | () => {} | Callback when input value changes |
51-
| onSaveButtonClick | () => void | () => {} | Callback when save button is clicked |
52-
| iconsOnly | boolean | false | Show only icons without button labels |
35+
| Prop | Type | Default | Description |
36+
| -------------------- | ----------------------- | -------- | ------------------------------------------- |
37+
| value | string | "" | Text to display and edit |
38+
| isEditing | boolean | false | Initial editing state |
39+
| inputType | string | "text" | HTML input type (text, number, email, etc.) |
40+
| label | string | "" | Label for the input field |
41+
| className | string | "" | Container class name |
42+
| inputClassName | string | "" | Input field class name |
43+
| editButtonClassName | string | "" | Edit button class name |
44+
| saveButtonClassName | string | "" | Save button class name |
45+
| editWrapperClassName | string | "" | Edit mode wrapper class name |
46+
| saveButtonLabel | React.ReactNode | "Save" | Custom save button label |
47+
| editButtonLabel | React.ReactNode | "Edit" | Custom edit button label |
48+
| showIcons | boolean | false | Toggle button icons visibility |
49+
| iconsOnly | boolean | false | Show only icons without text labels |
50+
| editIcon | React.ElementType | LuPencil | Custom edit icon component |
51+
| saveIcon | React.ElementType | LuCheck | Custom save icon component |
52+
| iconPosition | "left" \| "right" | "left" | Position of icons in buttons |
53+
| onEditButtonClick | () => void | () => {} | Callback when edit button is clicked |
54+
| onInputChange | (value: string) => void | () => {} | Callback when input value changes |
55+
| onSaveButtonClick | () => void | () => {} | Callback when save button is clicked |
5356

5457
## 💡 Examples
5558

5659
### Basic Usage
5760

5861
```tsx
59-
<InputClickEdit value={name} onInputChange={setName} />
62+
function BasicExample() {
63+
const [name, setName] = useState("John Doe");
64+
return <InputClickEdit value={name} onInputChange={setName} />;
65+
}
6066
```
6167

62-
### With Icons
68+
### With Label and Number Input
69+
70+
```tsx
71+
<InputClickEdit
72+
label="Age"
73+
inputType="number"
74+
value="25"
75+
onInputChange={(value) => console.log(value)}
76+
/>
77+
```
78+
79+
### With Icons and Custom Styling
6380

6481
```tsx
6582
<InputClickEdit
6683
value="Click me to edit"
6784
showIcons
6885
iconPosition="right"
86+
className="container"
87+
inputClassName="custom-input"
6988
saveButtonClassName="save-btn"
7089
editButtonClassName="edit-btn"
90+
editWrapperClassName="edit-wrapper"
7191
/>
7292
```
7393

74-
### Custom Icons
94+
### Custom Icons and Labels
7595

7696
```tsx
97+
import { FiEdit } from "react-icons/fi";
98+
import { FiSave } from "react-icons/fi";
99+
77100
<InputClickEdit
78-
value="Custom icons"
101+
value="Custom everything"
79102
showIcons
80-
editIcon={<span>✍️</span>}
81-
saveIcon={<span>👍</span>}
82-
/>
103+
editIcon={FiEdit}
104+
saveIcon={FiSave}
105+
editButtonLabel="Modify"
106+
saveButtonLabel="Update"
107+
/>;
83108
```
84109

85-
### Icons Only (No Text Labels)
110+
### Controlled Editing State
111+
112+
```tsx
113+
function ControlledExample() {
114+
const [isEditing, setIsEditing] = useState<boolean>(false);
115+
const [value, setValue] = useState<string>("Control me");
116+
117+
return (
118+
<InputClickEdit
119+
value={value}
120+
isEditing={isEditing}
121+
onEditButtonClick={() => setIsEditing(true)}
122+
onSaveButtonClick={() => setIsEditing(false)}
123+
onInputChange={setValue}
124+
/>
125+
);
126+
}
127+
```
128+
129+
### With Icons Only
86130

87131
```tsx
88132
<InputClickEdit
89-
value="Icons only buttons"
133+
value="Icons only"
134+
showIcons
90135
iconsOnly
91-
editIcon={<span>✎</span>}
92-
saveIcon={<span>✓</span>}
136+
editIcon={FiEdit}
137+
saveIcon={FiSave}
93138
/>
94139
```
95140

141+
## 🎨 Styling
142+
143+
The component comes with minimal default styling and can be fully customized using CSS classes. All main elements accept custom class names through props.
144+
145+
Example with CSS modules:
146+
147+
```tsx
148+
import styles from "./styles.module.css";
149+
150+
<InputClickEdit
151+
className={styles.wrapper}
152+
inputClassName={styles.input}
153+
editButtonClassName={styles.editButton}
154+
saveButtonClassName={styles.saveButton}
155+
editWrapperClassName={styles.editingWrapper}
156+
/>;
157+
```
158+
96159
## 📄 License
97160

98161
MIT

playground/package-lock.json

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

playground/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
},
1212
"dependencies": {
1313
"react": "^18.3.1",
14-
"react-dom": "^18.3.1"
14+
"react-dom": "^18.3.1",
15+
"react-icons": "^5.4.0"
1516
},
1617
"devDependencies": {
1718
"@eslint/js": "^9.17.0",

playground/src/App.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import reactLogo from "./assets/react.svg";
22
import viteLogo from "/vite.svg";
3+
import { LuAArrowDown, LuArrowUp } from "react-icons/lu";
34
import { InputClickEdit } from "@nobrainers/react-click-edit";
45
import "./App.css";
56
import { useState } from "react";
@@ -23,7 +24,15 @@ function App() {
2324
<div className="card">
2425
<InputClickEdit onInputChange={handleChange} value={value} showIcons />
2526
<br />
26-
<InputClickEdit onInputChange={handleChange} value={value} justIcons />
27+
<InputClickEdit onInputChange={handleChange} value={value} iconsOnly />
28+
<br />
29+
<InputClickEdit
30+
onInputChange={handleChange}
31+
value={value}
32+
showIcons
33+
saveIcon={LuAArrowDown}
34+
editIcon={LuArrowUp}
35+
/>
2736
<p>
2837
Edit <code>src/App.tsx</code> and save to test HMR
2938
</p>
Lines changed: 145 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,148 @@
1+
import { render, screen, fireEvent } from "@testing-library/react";
2+
import { vi } from "vitest";
3+
import { InputClickEdit } from "./InputClickEdit";
4+
15
describe("InputClickEdit", () => {
2-
it("assert true", () => {
3-
expect(true).toBeTruthy();
6+
describe("Rendering", () => {
7+
it("should render with default props", () => {
8+
render(<InputClickEdit />);
9+
expect(screen.getByText("Edit")).toBeInTheDocument();
10+
});
11+
12+
it("should render with initial value", () => {
13+
render(<InputClickEdit value="Test Value" />);
14+
expect(screen.getByText("Test Value")).toBeInTheDocument();
15+
});
16+
17+
it("should render with label when provided", () => {
18+
render(<InputClickEdit label="Name" isEditing />);
19+
const label = screen.getByText("Name");
20+
expect(label).toBeInTheDocument();
21+
expect(label.closest("label")).toContainElement(
22+
screen.getByRole("textbox")
23+
);
24+
});
25+
});
26+
27+
describe("Editing Mode", () => {
28+
it("should enter edit mode when edit button is clicked", () => {
29+
render(<InputClickEdit value="Initial" />);
30+
fireEvent.click(screen.getByText("Edit"));
31+
expect(screen.getByRole("textbox")).toBeInTheDocument();
32+
expect(screen.getByText("Save")).toBeInTheDocument();
33+
});
34+
35+
it("should start in edit mode when isEditing is true", () => {
36+
render(<InputClickEdit isEditing value="Test" />);
37+
expect(screen.getByRole("textbox")).toBeInTheDocument();
38+
});
39+
40+
it("should exit edit mode when save button is clicked", () => {
41+
render(<InputClickEdit value="Test" />);
42+
fireEvent.click(screen.getByText("Edit"));
43+
fireEvent.click(screen.getByText("Save"));
44+
expect(screen.queryByRole("textbox")).not.toBeInTheDocument();
45+
});
46+
});
47+
48+
describe("Callbacks", () => {
49+
it("should call onEditButtonClick when edit button is clicked", () => {
50+
const onEditButtonClick = vi.fn();
51+
render(<InputClickEdit onEditButtonClick={onEditButtonClick} />);
52+
fireEvent.click(screen.getByText("Edit"));
53+
expect(onEditButtonClick).toHaveBeenCalled();
54+
});
55+
56+
it("should call onInputChange when input value changes", () => {
57+
const onInputChange = vi.fn();
58+
render(<InputClickEdit isEditing onInputChange={onInputChange} />);
59+
fireEvent.change(screen.getByRole("textbox"), {
60+
target: { value: "New Value" },
61+
});
62+
expect(onInputChange).toHaveBeenCalledWith("New Value");
63+
});
64+
65+
it("should call onSaveButtonClick when save button is clicked", () => {
66+
const onSaveButtonClick = vi.fn();
67+
render(
68+
<InputClickEdit isEditing onSaveButtonClick={onSaveButtonClick} />
69+
);
70+
fireEvent.click(screen.getByText("Save"));
71+
expect(onSaveButtonClick).toHaveBeenCalled();
72+
});
73+
});
74+
75+
describe("Icons", () => {
76+
it("should show icons when showIcons is true", () => {
77+
render(<InputClickEdit showIcons />);
78+
expect(screen.getByTestId("edit-icon")).toBeInTheDocument();
79+
});
80+
81+
it("should position icons correctly based on iconPosition", () => {
82+
const { getByTestId, rerender } = render(
83+
<InputClickEdit showIcons iconPosition="right" />
84+
);
85+
86+
const buttonWrapper = getByTestId("action-button");
87+
expect(buttonWrapper.className).toContain("buttonReverse");
88+
89+
rerender(<InputClickEdit showIcons iconPosition="left" />);
90+
expect(buttonWrapper.className).not.toContain("buttonReverse");
91+
});
92+
93+
it("should render custom icons when provided", () => {
94+
const CustomEditIcon = () => <span data-testid="custom-edit"></span>;
95+
const CustomSaveIcon = () => <span data-testid="custom-save"></span>;
96+
97+
const { rerender } = render(
98+
<InputClickEdit showIcons editIcon={CustomEditIcon} />
99+
);
100+
expect(screen.getByTestId("custom-edit")).toBeInTheDocument();
101+
expect(screen.queryByTestId("custom-save")).not.toBeInTheDocument();
102+
103+
rerender(
104+
<InputClickEdit
105+
showIcons
106+
isEditing={true}
107+
editIcon={CustomEditIcon}
108+
saveIcon={CustomSaveIcon}
109+
/>
110+
);
111+
112+
expect(screen.queryByTestId("custom-edit")).not.toBeInTheDocument();
113+
expect(screen.getByTestId("custom-save")).toBeInTheDocument();
114+
});
115+
});
116+
117+
describe("Styling", () => {
118+
it("should apply custom class names", () => {
119+
const { container } = render(
120+
<InputClickEdit
121+
className="custom-wrapper"
122+
inputClassName="custom-input"
123+
editButtonClassName="custom-edit-btn"
124+
saveButtonClassName="custom-save-btn"
125+
editWrapperClassName="custom-edit-wrapper"
126+
/>
127+
);
128+
expect(container.querySelector(".custom-wrapper")).toBeInTheDocument();
129+
expect(container.querySelector(".custom-edit-btn")).toBeInTheDocument();
130+
});
131+
});
132+
133+
describe("Input Types", () => {
134+
it("should render different input types", () => {
135+
render(<InputClickEdit isEditing inputType="number" />);
136+
expect(screen.getByRole("spinbutton")).toBeInTheDocument();
137+
});
138+
});
139+
140+
describe("Custom Labels", () => {
141+
it("should render custom button labels", () => {
142+
render(
143+
<InputClickEdit editButtonLabel="Modify" saveButtonLabel="Update" />
144+
);
145+
expect(screen.getByText("Modify")).toBeInTheDocument();
146+
});
4147
});
5148
});

0 commit comments

Comments
 (0)