diff --git a/README.md b/README.md index a4ab6aa..84ddd1c 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,9 @@ - 🎨 Fully customizable styling - 🔄 Controlled component - 🚀 TypeScript support +- 🎨 Custom icons support +- 📝 Label support +- 🔤 Multiple input types ## 📦 Installation @@ -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 | `` | Custom edit icon | -| saveIcon | React.ReactNode | `` | 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 - +function BasicExample() { + const [name, setName] = useState("John Doe"); + return ; +} ``` -### With Icons +### With Label and Number Input + +```tsx + console.log(value)} +/> +``` + +### With Icons and Custom Styling ```tsx ``` -### Custom Icons +### Custom Icons and Labels ```tsx +import { FiEdit } from "react-icons/fi"; +import { FiSave } from "react-icons/fi"; + ✍️} - saveIcon={👍} -/> + editIcon={FiEdit} + saveIcon={FiSave} + editButtonLabel="Modify" + saveButtonLabel="Update" +/>; ``` -### Icons Only (No Text Labels) +### Controlled Editing State + +```tsx +function ControlledExample() { + const [isEditing, setIsEditing] = useState(false); + const [value, setValue] = useState("Control me"); + + return ( + setIsEditing(true)} + onSaveButtonClick={() => setIsEditing(false)} + onInputChange={setValue} + /> + ); +} +``` + +### With Icons Only ```tsx ✎} - saveIcon={} + 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"; + +; +``` + ## 📄 License MIT diff --git a/playground/package-lock.json b/playground/package-lock.json index be5e14d..c784413 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.0", "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", @@ -2620,6 +2621,14 @@ "react": "^18.3.1" } }, + "node_modules/react-icons": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz", + "integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", diff --git a/playground/package.json b/playground/package.json index fb3fc1f..8ed27f1 100644 --- a/playground/package.json +++ b/playground/package.json @@ -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", diff --git a/playground/src/App.tsx b/playground/src/App.tsx index 77f93e0..928bcee 100644 --- a/playground/src/App.tsx +++ b/playground/src/App.tsx @@ -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"; @@ -23,7 +24,15 @@ function App() {

- + +
+

Edit src/App.tsx and save to test HMR

diff --git a/src/InputClickEdit/InputClickEdit.test.tsx b/src/InputClickEdit/InputClickEdit.test.tsx index 2fed3de..f3eb906 100644 --- a/src/InputClickEdit/InputClickEdit.test.tsx +++ b/src/InputClickEdit/InputClickEdit.test.tsx @@ -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(); + expect(screen.getByText("Edit")).toBeInTheDocument(); + }); + + it("should render with initial value", () => { + render(); + expect(screen.getByText("Test Value")).toBeInTheDocument(); + }); + + it("should render with label when provided", () => { + render(); + 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(); + 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(); + expect(screen.getByRole("textbox")).toBeInTheDocument(); + }); + + it("should exit edit mode when save button is clicked", () => { + render(); + 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(); + fireEvent.click(screen.getByText("Edit")); + expect(onEditButtonClick).toHaveBeenCalled(); + }); + + it("should call onInputChange when input value changes", () => { + const onInputChange = vi.fn(); + render(); + 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( + + ); + fireEvent.click(screen.getByText("Save")); + expect(onSaveButtonClick).toHaveBeenCalled(); + }); + }); + + describe("Icons", () => { + it("should show icons when showIcons is true", () => { + render(); + expect(screen.getByTestId("edit-icon")).toBeInTheDocument(); + }); + + it("should position icons correctly based on iconPosition", () => { + const { getByTestId, rerender } = render( + + ); + + const buttonWrapper = getByTestId("action-button"); + expect(buttonWrapper.className).toContain("buttonReverse"); + + rerender(); + expect(buttonWrapper.className).not.toContain("buttonReverse"); + }); + + it("should render custom icons when provided", () => { + const CustomEditIcon = () => ; + const CustomSaveIcon = () => ; + + const { rerender } = render( + + ); + expect(screen.getByTestId("custom-edit")).toBeInTheDocument(); + expect(screen.queryByTestId("custom-save")).not.toBeInTheDocument(); + + rerender( + + ); + + expect(screen.queryByTestId("custom-edit")).not.toBeInTheDocument(); + expect(screen.getByTestId("custom-save")).toBeInTheDocument(); + }); + }); + + describe("Styling", () => { + it("should apply custom class names", () => { + const { container } = render( + + ); + expect(container.querySelector(".custom-wrapper")).toBeInTheDocument(); + expect(container.querySelector(".custom-edit-btn")).toBeInTheDocument(); + }); + }); + + describe("Input Types", () => { + it("should render different input types", () => { + render(); + expect(screen.getByRole("spinbutton")).toBeInTheDocument(); + }); + }); + + describe("Custom Labels", () => { + it("should render custom button labels", () => { + render( + + ); + expect(screen.getByText("Modify")).toBeInTheDocument(); + }); }); }); diff --git a/src/InputClickEdit/InputClickEdit.tsx b/src/InputClickEdit/InputClickEdit.tsx index b51c42e..3d592ea 100644 --- a/src/InputClickEdit/InputClickEdit.tsx +++ b/src/InputClickEdit/InputClickEdit.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { LuPencil } from "react-icons/lu"; import { LuCheck } from "react-icons/lu"; import cn from "classnames"; @@ -7,7 +7,7 @@ import styles from "./InputClickEdit.module.css"; type InputClickEditProps = { className?: string; - isEditing: boolean; + isEditing?: boolean; inputClassName?: string; editButtonClassName?: string; saveButtonClassName?: string; @@ -18,8 +18,8 @@ type InputClickEditProps = { label?: string; inputType?: string; showIcons?: boolean; - editIcon?: React.ReactNode; - saveIcon?: React.ReactNode; + editIcon?: React.ElementType; + saveIcon?: React.ElementType; iconPosition?: "left" | "right"; iconsOnly?: boolean; onEditButtonClick?: () => void; @@ -40,8 +40,8 @@ const InputClickEdit = ({ editButtonLabel = "Edit", label = "", showIcons = false, - saveIcon = , - editIcon = , + saveIcon, + editIcon, iconsOnly = false, iconPosition = "left", onEditButtonClick = () => {}, @@ -49,6 +49,9 @@ const InputClickEdit = ({ onSaveButtonClick = () => {}, }: InputClickEditProps) => { const [editing, setEditing] = useState(isEditing); + useEffect(() => { + setEditing(isEditing); + }, [isEditing]); const onEditClick = () => { setEditing(true); onEditButtonClick?.(); @@ -74,6 +77,9 @@ const InputClickEdit = ({ [styles.buttonReverse]: iconPosition === "right", }; + const EditIcon = editIcon || LuPencil; + const SaveIcon = saveIcon || LuCheck; + return (
{editing ? ( @@ -87,11 +93,12 @@ const InputClickEdit = ({ )}
@@ -99,11 +106,12 @@ const InputClickEdit = ({
{value}