Skip to content
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
127 changes: 95 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
- 🎨 Fully customizable styling
- 🔄 Controlled component
- 🚀 TypeScript support
- 🎨 Custom icons support
- 📝 Label support
- 🔤 Multiple input types

## 📦 Installation

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

## 🔧 Props

| Prop | Type | Default | Description |
| -------------------- | ----------------------- | -------------- | ------------------------------------- |
| value | string | "" | Text to display and edit |
| isEditing | boolean | false | Initial editing state |
| inputType | string | "text" | Type of input field |
| label | string | "" | Label for the input |
| className | string | "" | Container class name |
| inputClassName | string | "" | Input field class name |
| editButtonClassName | string | "" | Edit button class name |
| saveButtonClassName | string | "" | Save button class name |
| editWrapperClassName | string | "" | Edit mode wrapper class name |
| saveButtonLabel | React.ReactNode | "Save" | Custom save button label |
| editButtonLabel | React.ReactNode | "Edit" | Custom edit button label |
| showIcons | boolean | false | Show icons in buttons |
| editIcon | React.ReactNode | `<LuPencil />` | Custom edit icon |
| saveIcon | React.ReactNode | `<LuCheck />` | Custom save icon |
| iconPosition | "left" \| "right" | "left" | Position of icons in buttons |
| onEditButtonClick | () => void | () => {} | Callback when edit button is clicked |
| onInputChange | (value: string) => void | () => {} | Callback when input value changes |
| onSaveButtonClick | () => void | () => {} | Callback when save button is clicked |
| iconsOnly | boolean | false | Show only icons without button labels |
| Prop | Type | Default | Description |
| -------------------- | ----------------------- | -------- | ------------------------------------------- |
| value | string | "" | Text to display and edit |
| isEditing | boolean | false | Initial editing state |
| inputType | string | "text" | HTML input type (text, number, email, etc.) |
| label | string | "" | Label for the input field |
| className | string | "" | Container class name |
| inputClassName | string | "" | Input field class name |
| editButtonClassName | string | "" | Edit button class name |
| saveButtonClassName | string | "" | Save button class name |
| editWrapperClassName | string | "" | Edit mode wrapper class name |
| saveButtonLabel | React.ReactNode | "Save" | Custom save button label |
| editButtonLabel | React.ReactNode | "Edit" | Custom edit button label |
| showIcons | boolean | false | Toggle button icons visibility |
| iconsOnly | boolean | false | Show only icons without text labels |
| editIcon | React.ElementType | LuPencil | Custom edit icon component |
| saveIcon | React.ElementType | LuCheck | Custom save icon component |
| iconPosition | "left" \| "right" | "left" | Position of icons in buttons |
| onEditButtonClick | () => void | () => {} | Callback when edit button is clicked |
| onInputChange | (value: string) => void | () => {} | Callback when input value changes |
| onSaveButtonClick | () => void | () => {} | Callback when save button is clicked |

## 💡 Examples

### Basic Usage

```tsx
<InputClickEdit value={name} onInputChange={setName} />
function BasicExample() {
const [name, setName] = useState("John Doe");
return <InputClickEdit value={name} onInputChange={setName} />;
}
```

### With Icons
### With Label and Number Input

```tsx
<InputClickEdit
label="Age"
inputType="number"
value="25"
onInputChange={(value) => console.log(value)}
/>
```

### With Icons and Custom Styling

```tsx
<InputClickEdit
value="Click me to edit"
showIcons
iconPosition="right"
className="container"
inputClassName="custom-input"
saveButtonClassName="save-btn"
editButtonClassName="edit-btn"
editWrapperClassName="edit-wrapper"
/>
```

### Custom Icons
### Custom Icons and Labels

```tsx
import { FiEdit } from "react-icons/fi";
import { FiSave } from "react-icons/fi";

<InputClickEdit
value="Custom icons"
value="Custom everything"
showIcons
editIcon={<span>✍️</span>}
saveIcon={<span>👍</span>}
/>
editIcon={FiEdit}
saveIcon={FiSave}
editButtonLabel="Modify"
saveButtonLabel="Update"
/>;
```

### Icons Only (No Text Labels)
### Controlled Editing State

```tsx
function ControlledExample() {
const [isEditing, setIsEditing] = useState<boolean>(false);
const [value, setValue] = useState<string>("Control me");

return (
<InputClickEdit
value={value}
isEditing={isEditing}
onEditButtonClick={() => setIsEditing(true)}
onSaveButtonClick={() => setIsEditing(false)}
onInputChange={setValue}
/>
);
}
```

### With Icons Only

```tsx
<InputClickEdit
value="Icons only buttons"
value="Icons only"
showIcons
iconsOnly
editIcon={<span>✎</span>}
saveIcon={<span>✓</span>}
editIcon={FiEdit}
saveIcon={FiSave}
/>
```

## 🎨 Styling

The component comes with minimal default styling and can be fully customized using CSS classes. All main elements accept custom class names through props.

Example with CSS modules:

```tsx
import styles from "./styles.module.css";

<InputClickEdit
className={styles.wrapper}
inputClassName={styles.input}
editButtonClassName={styles.editButton}
saveButtonClassName={styles.saveButton}
editWrapperClassName={styles.editingWrapper}
/>;
```

## 📄 License

MIT
11 changes: 10 additions & 1 deletion playground/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"react-icons": "^5.4.0"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
Expand Down
11 changes: 10 additions & 1 deletion playground/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";
import { LuAArrowDown, LuArrowUp } from "react-icons/lu";
import { InputClickEdit } from "@nobrainers/react-click-edit";
import "./App.css";
import { useState } from "react";
Expand All @@ -23,7 +24,15 @@ function App() {
<div className="card">
<InputClickEdit onInputChange={handleChange} value={value} showIcons />
<br />
<InputClickEdit onInputChange={handleChange} value={value} justIcons />
<InputClickEdit onInputChange={handleChange} value={value} iconsOnly />
<br />
<InputClickEdit
onInputChange={handleChange}
value={value}
showIcons
saveIcon={LuAArrowDown}
editIcon={LuArrowUp}
/>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
Expand Down
147 changes: 145 additions & 2 deletions src/InputClickEdit/InputClickEdit.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,148 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { vi } from "vitest";
import { InputClickEdit } from "./InputClickEdit";

describe("InputClickEdit", () => {
it("assert true", () => {
expect(true).toBeTruthy();
describe("Rendering", () => {
it("should render with default props", () => {
render(<InputClickEdit />);
expect(screen.getByText("Edit")).toBeInTheDocument();
});

it("should render with initial value", () => {
render(<InputClickEdit value="Test Value" />);
expect(screen.getByText("Test Value")).toBeInTheDocument();
});

it("should render with label when provided", () => {
render(<InputClickEdit label="Name" isEditing />);
const label = screen.getByText("Name");
expect(label).toBeInTheDocument();
expect(label.closest("label")).toContainElement(
screen.getByRole("textbox")
);
});
});

describe("Editing Mode", () => {
it("should enter edit mode when edit button is clicked", () => {
render(<InputClickEdit value="Initial" />);
fireEvent.click(screen.getByText("Edit"));
expect(screen.getByRole("textbox")).toBeInTheDocument();
expect(screen.getByText("Save")).toBeInTheDocument();
});

it("should start in edit mode when isEditing is true", () => {
render(<InputClickEdit isEditing value="Test" />);
expect(screen.getByRole("textbox")).toBeInTheDocument();
});

it("should exit edit mode when save button is clicked", () => {
render(<InputClickEdit value="Test" />);
fireEvent.click(screen.getByText("Edit"));
fireEvent.click(screen.getByText("Save"));
expect(screen.queryByRole("textbox")).not.toBeInTheDocument();
});
});

describe("Callbacks", () => {
it("should call onEditButtonClick when edit button is clicked", () => {
const onEditButtonClick = vi.fn();
render(<InputClickEdit onEditButtonClick={onEditButtonClick} />);
fireEvent.click(screen.getByText("Edit"));
expect(onEditButtonClick).toHaveBeenCalled();
});

it("should call onInputChange when input value changes", () => {
const onInputChange = vi.fn();
render(<InputClickEdit isEditing onInputChange={onInputChange} />);
fireEvent.change(screen.getByRole("textbox"), {
target: { value: "New Value" },
});
expect(onInputChange).toHaveBeenCalledWith("New Value");
});

it("should call onSaveButtonClick when save button is clicked", () => {
const onSaveButtonClick = vi.fn();
render(
<InputClickEdit isEditing onSaveButtonClick={onSaveButtonClick} />
);
fireEvent.click(screen.getByText("Save"));
expect(onSaveButtonClick).toHaveBeenCalled();
});
});

describe("Icons", () => {
it("should show icons when showIcons is true", () => {
render(<InputClickEdit showIcons />);
expect(screen.getByTestId("edit-icon")).toBeInTheDocument();
});

it("should position icons correctly based on iconPosition", () => {
const { getByTestId, rerender } = render(
<InputClickEdit showIcons iconPosition="right" />
);

const buttonWrapper = getByTestId("action-button");
expect(buttonWrapper.className).toContain("buttonReverse");

rerender(<InputClickEdit showIcons iconPosition="left" />);
expect(buttonWrapper.className).not.toContain("buttonReverse");
});

it("should render custom icons when provided", () => {
const CustomEditIcon = () => <span data-testid="custom-edit">✎</span>;
const CustomSaveIcon = () => <span data-testid="custom-save">✎</span>;

const { rerender } = render(
<InputClickEdit showIcons editIcon={CustomEditIcon} />
);
expect(screen.getByTestId("custom-edit")).toBeInTheDocument();
expect(screen.queryByTestId("custom-save")).not.toBeInTheDocument();

rerender(
<InputClickEdit
showIcons
isEditing={true}
editIcon={CustomEditIcon}
saveIcon={CustomSaveIcon}
/>
);

expect(screen.queryByTestId("custom-edit")).not.toBeInTheDocument();
expect(screen.getByTestId("custom-save")).toBeInTheDocument();
});
});

describe("Styling", () => {
it("should apply custom class names", () => {
const { container } = render(
<InputClickEdit
className="custom-wrapper"
inputClassName="custom-input"
editButtonClassName="custom-edit-btn"
saveButtonClassName="custom-save-btn"
editWrapperClassName="custom-edit-wrapper"
/>
);
expect(container.querySelector(".custom-wrapper")).toBeInTheDocument();
expect(container.querySelector(".custom-edit-btn")).toBeInTheDocument();
});
});

describe("Input Types", () => {
it("should render different input types", () => {
render(<InputClickEdit isEditing inputType="number" />);
expect(screen.getByRole("spinbutton")).toBeInTheDocument();
});
});

describe("Custom Labels", () => {
it("should render custom button labels", () => {
render(
<InputClickEdit editButtonLabel="Modify" saveButtonLabel="Update" />
);
expect(screen.getByText("Modify")).toBeInTheDocument();
});
});
});
Loading
Loading