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(textarea): introduce isClearable #3477

Open
wants to merge 30 commits into
base: canary
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6c2d9d2
feat(components): add clear button to the textarea component
IsDyh01 Jul 15, 2024
de5b54e
docs(textarea): add test and changeset
IsDyh01 Jul 16, 2024
6f92cdb
feat(textarea): modify the changeset file
IsDyh01 Jul 21, 2024
e471830
Merge branch 'canary' into pr/3477
wingkwong Oct 5, 2024
36b4540
feat(textarea): add clear button to textarea
IsDyh01 Oct 9, 2024
bc6f329
feat(textarea): add isClearable prop to textarea
IsDyh01 Oct 9, 2024
41a29ff
docs(textarea): add documentation to textarea
IsDyh01 Oct 9, 2024
bc18964
docs(textarea): add documentation to textarea
IsDyh01 Oct 9, 2024
ac31ed0
feat(textarea): replace the textarea component clear icon and modify …
IsDyh01 Oct 10, 2024
815193a
feat(textarea): revise the clear button position
IsDyh01 Oct 11, 2024
9ff14d5
feat(textarea): revise the clear button structure
IsDyh01 Oct 12, 2024
1ce8a2b
feat(textarea): revise the styles of clear button and textarea
IsDyh01 Oct 13, 2024
ecd3fa2
feat(textarea): revise the styles of RTL case
IsDyh01 Oct 13, 2024
7e1bd65
Merge remote-tracking branch 'upstream/canary' into feat/textarea-add…
IsDyh01 Oct 13, 2024
989b309
feat(textarea): change the rtl to pe
IsDyh01 Oct 13, 2024
590587f
feat(textarea): delete the px classname
IsDyh01 Oct 13, 2024
7eabb9f
chore(changeset): update package and message
wingkwong Oct 13, 2024
65f4074
test(textarea): add test case
IsDyh01 Oct 13, 2024
073599e
Merge remote-tracking branch 'upstream/canary' into feat/textarea-add…
IsDyh01 Oct 16, 2024
ac1ff15
feat(textarea): change the clear button structure
IsDyh01 Oct 16, 2024
6295aad
feat(textarea): optimized code
IsDyh01 Oct 17, 2024
811f214
chore(textarea): update the changeset file
IsDyh01 Oct 18, 2024
b3dcdd6
Merge remote-tracking branch 'upstream/canary' into feat/textarea-add…
IsDyh01 Oct 31, 2024
a1fe23d
docs(textarea): add slots doc to textarea
IsDyh01 Oct 31, 2024
7b136d9
Merge branch 'feat/textarea-add-clearButton' of github.com:IsDyh01/ne…
IsDyh01 Oct 31, 2024
80eead7
chore(textarea): update peerDevpeerDependencies version
IsDyh01 Oct 31, 2024
d57ed70
chore(textarea): add usecallback dep
IsDyh01 Oct 31, 2024
a457246
Merge remote-tracking branch 'upstream/canary' into feat/textarea-add…
IsDyh01 Nov 4, 2024
a4c2310
Update .changeset/five-adults-protect.md
jrgarciadev Nov 4, 2024
c2b16d4
feat(textarea): modify the clear button icon
IsDyh01 Nov 5, 2024
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
7 changes: 7 additions & 0 deletions .changeset/five-adults-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@nextui-org/input": minor
"@nextui-org/shared-icons": minor
IsDyh01 marked this conversation as resolved.
Show resolved Hide resolved
"@nextui-org/theme": minor
---
jrgarciadev marked this conversation as resolved.
Show resolved Hide resolved

introduce `isClearable` to Textarea component (#2348, #2112)
23 changes: 23 additions & 0 deletions apps/docs/content/components/textarea/clear-button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const App = `import {Textarea} from "@nextui-org/react";

export default function App() {
return (
<Textarea
isClearable
label="Email"
variant="bordered"
placeholder="Enter your email"
defaultValue="junior@nextui.org"
onClear={() => console.log("textarea cleared")}
className="max-w-xs"
/>
);
}`;

const react = {
"/App.jsx": App,
};

export default {
...react,
};
2 changes: 2 additions & 0 deletions apps/docs/content/components/textarea/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import errorMessage from "./error-message";
import description from "./description";
import controlled from "./controlled";
import disableAutosize from "./disable-autosize";
import clearButton from "./clear-button";
IsDyh01 marked this conversation as resolved.
Show resolved Hide resolved

export const textareaContent = {
usage,
Expand All @@ -20,4 +21,5 @@ export const textareaContent = {
description,
controlled,
disableAutosize,
clearButton,
};
9 changes: 9 additions & 0 deletions apps/docs/content/docs/components/textarea.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@ You can use the `value` and `onValueChange` properties to control the input valu

<CodeDemo title="Controlled" files={textareaContent.controlled} />

### Clear Button

If you pass the `isClearable` property to the textarea, it will have a clear button at the
end of the textarea, it will be visible when the textarea has a value.

<CodeDemo title="Clear Button" files={textareaContent.clearButton} />

> **Note**: NextUI `Textarea` also supports native events like `onChange`, useful for form libraries
> such as [Formik](https://formik.org/) and [React Hook Form](https://react-hook-form.com/).

Expand All @@ -100,6 +107,7 @@ You can use the `value` and `onValueChange` properties to control the input valu
- **input**: The textarea input element.
- **description**: The description of the textarea.
- **errorMessage**: The error message of the textarea.
- **headerWrapper**: Wraps the `label` and the `clearButton`.

<Spacer y={4} />

Expand Down Expand Up @@ -163,6 +171,7 @@ You can use the `value` and `onValueChange` properties to control the input valu
| isRequired | `boolean` | Whether user input is required on the textarea before form submission. | `false` |
| isReadOnly | `boolean` | Whether the textarea can be selected but not changed by the user. | |
| isDisabled | `boolean` | Whether the textarea is disabled. | `false` |
| isClearable | `boolean` | Whether the textarea should have a clear button. | `false` |
| isInvalid | `boolean` | Whether the textarea is invalid. | `false` |
| validationState | `valid` \| `invalid` | Whether the textarea should display its "valid" or "invalid" visual styling. (**Deprecated**) use **isInvalid** instead. | - |
| disableAutosize | `boolean` | Whether the textarea auto vertically resize should be disabled. | `false` |
Expand Down
81 changes: 81 additions & 0 deletions packages/components/input/__tests__/textarea.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import * as React from "react";
import {render} from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import {Textarea} from "../src";

describe("Textarea", () => {
IsDyh01 marked this conversation as resolved.
Show resolved Hide resolved
it("should clear the value and onClear is triggered", async () => {
const onClear = jest.fn();

const ref = React.createRef<HTMLTextAreaElement>();

const {getByRole} = render(
<Textarea
ref={ref}
isClearable
defaultValue="junior@nextui.org"
label="test textarea"
onClear={onClear}
/>,
);

const clearButton = getByRole("button");

expect(clearButton).not.toBeNull();

const user = userEvent.setup();

await user.click(clearButton);

expect(ref.current?.value)?.toBe("");

expect(onClear).toHaveBeenCalledTimes(1);
});
IsDyh01 marked this conversation as resolved.
Show resolved Hide resolved

it("should disable clear button when isReadOnly is true", async () => {
const onClear = jest.fn();

const ref = React.createRef<HTMLTextAreaElement>();

const {getByRole} = render(
<Textarea
ref={ref}
isClearable
isReadOnly
defaultValue="readOnly test for clear button"
label="test textarea"
onClear={onClear}
/>,
);

const clearButton = getByRole("button")!;

expect(clearButton).not.toBeNull();

const user = userEvent.setup();

await user.click(clearButton);

expect(onClear).toHaveBeenCalledTimes(0);
});
IsDyh01 marked this conversation as resolved.
Show resolved Hide resolved

it("should appear clear button when just define onClear but not define isClearable", async () => {
const onClear = jest.fn();

const ref = React.createRef<HTMLTextAreaElement>();

const {getByRole} = render(
<Textarea
ref={ref}
defaultValue="junior@nextui.org"
label="test textarea"
onClear={onClear}
/>,
);

const clearButton = getByRole("button");

expect(clearButton).not.toBeNull();
});
IsDyh01 marked this conversation as resolved.
Show resolved Hide resolved
});
IsDyh01 marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 2 additions & 2 deletions packages/components/input/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18",
"@nextui-org/theme": ">=2.1.0",
"@nextui-org/theme": ">=2.3.0",
"@nextui-org/system": ">=2.0.0"
},
"dependencies": {
Expand All @@ -62,4 +62,4 @@
"react-hook-form": "^7.51.3"
},
"clean-package": "../../../clean-package.config.json"
}
}
24 changes: 18 additions & 6 deletions packages/components/input/src/textarea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {forwardRef} from "@nextui-org/system";
import {mergeProps} from "@react-aria/utils";
import {useMemo, useState} from "react";
import TextareaAutosize from "react-textarea-autosize";
import {TrashIcon} from "@nextui-org/shared-icons";

import {UseInputProps, useInput} from "./use-input";

Expand All @@ -14,11 +15,7 @@ type TextareaAutoSizeStyle = Omit<
height?: number;
};

type OmittedInputProps =
| "isClearButtonFocusVisible"
| "isLabelPlaceholder"
| "isClearable"
| "isTextarea";
type OmittedInputProps = "isClearButtonFocusVisible" | "isLabelPlaceholder" | "isTextarea";

export type TextareaHeightChangeMeta = {
rowHeight: number;
Expand Down Expand Up @@ -88,6 +85,9 @@ const Textarea = forwardRef<"textarea", TextAreaProps>(
getHelperWrapperProps,
getDescriptionProps,
getErrorMessageProps,
isClearable,
getClearButtonProps,
getHeaderWrapperProps,
} = useInput<HTMLTextAreaElement>({...otherProps, ref, isMultiline: true});

const [hasMultipleRows, setIsHasMultipleRows] = useState(minRows > 1);
Expand Down Expand Up @@ -122,6 +122,10 @@ const Textarea = forwardRef<"textarea", TextAreaProps>(
/>
);

const clearButtonContent = useMemo(() => {
return isClearable ? <button {...getClearButtonProps()}>{<TrashIcon />}</button> : null;
wingkwong marked this conversation as resolved.
Show resolved Hide resolved
IsDyh01 marked this conversation as resolved.
Show resolved Hide resolved
}, [isClearable, getClearButtonProps]);

const innerWrapper = useMemo(() => {
if (startContent || endContent) {
return (
Expand All @@ -140,7 +144,15 @@ const Textarea = forwardRef<"textarea", TextAreaProps>(
<Component {...getBaseProps()}>
{shouldLabelBeOutside ? labelContent : null}
<div {...getInputWrapperProps()} data-has-multiple-rows={dataAttr(hasMultipleRows)}>
{shouldLabelBeInside ? labelContent : null}
{isClearable ? (
<div {...getHeaderWrapperProps()}>
{shouldLabelBeInside ? labelContent : <label {...getLabelProps()}>{}</label>}
{clearButtonContent}
</div>
) : shouldLabelBeInside ? (
labelContent
) : null}
IsDyh01 marked this conversation as resolved.
Show resolved Hide resolved

{innerWrapper}
</div>
{hasHelper ? (
Expand Down
27 changes: 24 additions & 3 deletions packages/components/input/src/use-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ export function useInput<T extends HTMLInputElement | HTMLTextAreaElement = HTML
"data-has-start-content": dataAttr(hasStartContent),
"data-has-end-content": dataAttr(!!endContent),
className: slots.input({
class: clsx(classNames?.input, isFilled ? "is-filled" : ""),
class: clsx(classNames?.input, isFilled ? "is-filled" : "", isMultiline ? "pe-0" : ""),
}),
...mergeProps(
focusProps,
Expand Down Expand Up @@ -412,7 +412,11 @@ export function useInput<T extends HTMLInputElement | HTMLTextAreaElement = HTML
"data-focus-visible": dataAttr(isFocusVisible),
"data-focus": dataAttr(isFocused),
className: slots.inputWrapper({
class: clsx(classNames?.inputWrapper, isFilled ? "is-filled" : ""),
class: clsx(
classNames?.inputWrapper,
isFilled ? "is-filled" : "",
isMultiline ? "flex-col items-start gap-0" : "",
),
}),
...mergeProps(props, hoverProps),
onClick: (e) => {
Expand Down Expand Up @@ -516,13 +520,29 @@ export function useInput<T extends HTMLInputElement | HTMLTextAreaElement = HTML
"aria-label": "clear input",
"data-slot": "clear-button",
"data-focus-visible": dataAttr(isClearButtonFocusVisible),
className: slots.clearButton({class: clsx(classNames?.clearButton, props?.className)}),
className: slots.clearButton({
class: clsx(
classNames?.clearButton,
props?.className,
isMultiline ? "relative block opacity-100 p-0 -m-0 end-0" : "",
IsDyh01 marked this conversation as resolved.
Show resolved Hide resolved
),
}),
...mergeProps(clearPressProps, clearFocusProps),
};
},
[slots, isClearButtonFocusVisible, clearPressProps, clearFocusProps, classNames?.clearButton],
);

const getHeaderWrapperProps: PropGetter = useCallback(
(props = {}) => {
return {
...props,
className: slots.headerWrapper({class: clsx(classNames?.headerWrapper, props?.className)}),
};
},
[slots, classNames?.headerWrapper],
);

return {
Component,
classNames,
Expand Down Expand Up @@ -553,6 +573,7 @@ export function useInput<T extends HTMLInputElement | HTMLTextAreaElement = HTML
getDescriptionProps,
getErrorMessageProps,
getClearButtonProps,
getHeaderWrapperProps,
};
}

Expand Down
12 changes: 12 additions & 0 deletions packages/components/input/stories/textarea.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -302,3 +302,15 @@ export const IsInvalid = {
errorMessage: "Please enter a valid description",
},
};

export const Clearable = {
render: Template,

args: {
...defaultProps,
placeholder: "Enter your description",
defaultValue: "junior@nextui.org",
// eslint-disable-next-line no-console
onClear: () => console.log("textarea cleared"),
},
};
1 change: 1 addition & 0 deletions packages/core/theme/src/components/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const input = tv({
helperWrapper: "hidden group-data-[has-helper=true]:flex p-1 relative flex-col gap-1.5",
description: "text-tiny text-foreground-400",
errorMessage: "text-tiny text-danger",
headerWrapper: "flex pb-1 justify-between w-full items-center",
IsDyh01 marked this conversation as resolved.
Show resolved Hide resolved
},
variants: {
variant: {
Expand Down
2 changes: 2 additions & 0 deletions packages/utilities/shared-icons/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export * from "./info-circle";
export * from "./warning";
export * from "./danger";
export * from "./success";
export * from "./trash";

// sets
export * from "./bulk";
export * from "./bold";
Expand Down
25 changes: 25 additions & 0 deletions packages/utilities/shared-icons/src/trash.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {IconSvgProps} from "./types";

export const TrashIcon = (props: IconSvgProps) => (
<svg
fill="none"
height="16"
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M14.0071 3.8847L14.0087 3.88486C14.0648 3.8902 14.1093 3.93884 14.108 3.99818C14.0999 4.04945 14.0549 4.08666 14.0067 4.08666H14.0066H14.0064H14.0063H14.0062H14.0061H14.006H14.0059H14.0057H14.0056H14.0055H14.0054H14.0053H14.0052H14.005H14.0049H14.0048H14.0047H14.0046H14.0045H14.0044H14.0042H14.0041H14.004H14.0039H14.0038H14.0037H14.0036H14.0034H14.0033H14.0032H14.0031H14.003H14.0029H14.0028H14.0027H14.0025H14.0024H14.0023H14.0022H14.0021H14.002H14.0019H14.0018H14.0017H14.0015H14.0014H14.0013H14.0012H14.0011H14.001H14.0009H14.0008H14.0007H14.0005H14.0004H14.0003H14.0002H14.0001H14H13.9999H13.9998H13.9997H13.9996H13.9995H13.9993H13.9992H13.9991H13.999H13.9989H13.9988H13.9987H13.9986H13.9985H13.9984H13.9983H13.9982H13.9981H13.9979H13.9978H13.9977H13.9976H13.9975H13.9974H13.9973H13.9972H13.9971H13.997H13.9969H13.9968H13.9967H13.9966H13.9965H13.9964H13.9962H13.9961H13.996H13.9959H13.9958H13.9957H13.9956H13.9955H13.9954H13.9953H13.9952H13.9951H13.995H13.9949H13.9948H13.9947H13.9946H13.9945H13.9944H13.9943H13.9942H13.9941H13.9939H13.9938H13.9937H13.9936H13.9935H13.9934H13.9933H13.9932H13.9931H13.993H13.9929H13.9928H13.9927H13.9926H13.9925H13.9924H13.9923H13.9922H13.9921H13.992H13.9919H13.9918H13.9917H13.9916H13.9915H13.9914H13.9913H13.9912H13.9911H13.991H13.9909H13.9908H13.9907H13.9906H13.9905H13.9904H13.9903H13.9902H13.9901H13.99H13.9899H13.9898H13.9897H13.9896H13.9895H13.9894H13.9893H13.9892H13.9891H13.989H13.9889H13.9888H13.9887H13.9886H13.9885H13.9884H13.9883H13.9882H13.9881H13.988H13.9879H13.9878H13.9877H13.9876H13.9875H13.9874H13.9873H13.9872H13.9871H13.987H13.9869H13.9868H13.9867H13.9866H13.9865H13.9864H13.9863H13.9862H13.9861H13.986H13.9859H13.9858H13.9857H13.9856H13.9855H13.9854H13.9853H13.9852H13.9851H13.985H13.9849H13.9848H13.9847H13.9846H13.9845H13.9844H13.9843H13.9842H13.9841H13.984H13.9839H13.9838H13.9837H13.9836H13.9835H13.9834H13.9833H13.9832H13.9831H13.983H13.9829H13.9828H13.9827H13.9826H13.9825H13.9824H13.9823H13.9823H13.9822H13.9821H13.982H13.9819H13.9818H13.9817H13.9816H13.9815H13.9814H13.9813H13.9812H13.9811H13.981H13.9809H13.9808H13.9807H13.9806H13.9805H13.9804H13.9803H13.9802H13.9801H13.98H13.9799H13.9798H13.9797H13.9796H13.9795H13.9794H13.9793H13.9792H13.9791H13.979H13.9789H13.9788H13.9787H13.9786H13.9785H13.9784H13.9783H13.9782H13.9781H13.9781H13.978H13.9779H13.9778H13.9777H13.9776H13.9775H13.9774H13.9773H13.9772H13.9771H13.977H13.9769H13.9768H13.9767H13.9766H13.9765H13.9764H13.9763H13.9762H13.9761H13.976H13.9759H13.9758H13.9757H13.9756H13.9755H13.9754H13.9753H13.9752H13.9751H13.975H13.9749H13.9748H13.9747H13.9746H13.9745H13.9744H13.9743H13.9742H13.9741H13.974H13.9739H13.9738H13.9737H13.9736H13.9735H13.9734H13.9733H13.9733C10.4314 3.7333 6.88973 3.59909 3.36755 3.94858C3.36742 3.94859 3.36729 3.9486 3.36717 3.94862L2.00875 4.0818C2.00861 4.08181 2.00848 4.08182 2.00835 4.08183C1.94717 4.08747 1.90326 4.04725 1.89811 3.99449C1.8929 3.9411 1.93182 3.89052 1.99126 3.88486L1.99237 3.88475L3.35187 3.75147C3.35194 3.75146 3.35201 3.75145 3.35208 3.75145C3.96457 3.69218 4.57811 3.65256 5.19909 3.6125L5.51656 3.59202L5.56806 3.27809L5.708 2.42509C5.76399 2.08568 5.81767 1.81209 5.98475 1.60624C6.12965 1.42771 6.42122 1.23333 7.12 1.23333H8.86667C9.56355 1.23333 9.85752 1.43445 10.0046 1.61915C10.1739 1.83176 10.2261 2.10927 10.2786 2.43103L10.2785 2.43103L10.2789 2.4334L10.42 3.26694V3.61839L10.7978 3.63938C11.8728 3.6991 12.9403 3.77868 14.0071 3.8847Z"
fill="#A1A1AA"
stroke="#A1A1AA"
strokeWidth="0.8"
/>
<path
d="M12.2134 5.56667C12.3292 5.56667 12.4438 5.61405 12.5285 5.70064C12.6088 5.78957 12.6542 5.90963 12.6474 6.03693C12.6474 6.03722 12.6473 6.03752 12.6473 6.03782L12.2342 12.8669C12.1959 13.3993 12.1525 13.8463 11.9087 14.1798C11.69 14.4792 11.2352 14.7733 10.14 14.7733H5.86004C4.76563 14.7733 4.31057 14.4779 4.09155 14.1776C3.8475 13.843 3.80414 13.3957 3.76583 12.8668L3.35272 6.03052C3.35271 6.03034 3.3527 6.03016 3.35269 6.02998C3.34597 5.90978 3.39108 5.78807 3.47441 5.69749C3.54714 5.61844 3.66453 5.56667 3.7867 5.56667H12.2134ZM6.8867 12.2333H9.1067C9.60095 12.2333 10.0067 11.8276 10.0067 11.3333C10.0067 10.8391 9.60095 10.4333 9.1067 10.4333H6.8867C6.39246 10.4333 5.9867 10.8391 5.9867 11.3333C5.9867 11.8276 6.39246 12.2333 6.8867 12.2333ZM6.33337 9.56667H9.6667C10.161 9.56667 10.5667 9.16092 10.5667 8.66667C10.5667 8.17242 10.161 7.76667 9.6667 7.76667H6.33337C5.83912 7.76667 5.43337 8.17242 5.43337 8.66667C5.43337 9.16092 5.83912 9.56667 6.33337 9.56667Z"
fill="#A1A1AA"
stroke="#A1A1AA"
strokeWidth="0.8"
/>
</svg>
);