diff --git a/src/components/TextArea.tsx b/src/components/TextArea.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1a538f42132dec57d940482ee726d74fab90f75a --- /dev/null +++ b/src/components/TextArea.tsx @@ -0,0 +1,54 @@ +import { Textarea } from "@nextui-org/react"; +import { TextInputProps } from "./TextInput"; +import { baseInputClassNames } from "./utils/baseComponents"; + +interface TextAreaProps extends Omit<TextInputProps, OmittedTextInputProps> { + minRows?: number; + maxRows?: number; + disableAutosize?: boolean; +} + +type OmittedTextInputProps = "isClearable"; + +const TextArea = ({ + isRequired = false, + isDisabled = false, + isReadOnly = false, + label = "", + minRows = 3, + maxRows = 8, + maxLength, + value, + onValueChange, + isInvalid = false, + errorMessage, + disableAutosize = false, +}: TextAreaProps) => { + const hasClearButton = false; + const hasMainWrapper = false; + + return ( + <Textarea + isRequired={isRequired} + isDisabled={isDisabled} + isReadOnly={isReadOnly} + label={label} + minRows={minRows} + maxRows={maxRows} + maxLength={maxLength} + value={value} + onValueChange={onValueChange} + isInvalid={isInvalid} + errorMessage={errorMessage} + disableAutosize={disableAutosize} + labelPlacement="outside" + variant="bordered" + classNames={{ + ...baseInputClassNames(isDisabled, hasMainWrapper, hasClearButton), + input: disableAutosize && "resize-y", + }} + ></Textarea> + ); +}; + +export default TextArea; diff --git a/src/components/TextInput.tsx b/src/components/TextInput.tsx index faa164b8f7f7fd197d3236f97fe3bf13b195c26a..14cec5527f615360759a96cfad73edc138ecbe96 100644 --- a/src/components/TextInput.tsx +++ b/src/components/TextInput.tsx @@ -68,3 +68,4 @@ const TextInput = ({ }; export default TextInput; +export type { TextInputProps }; diff --git a/src/components/utils/baseComponents.tsx b/src/components/utils/baseComponents.tsx index 3551dbc17a3429a0e6e53c41af06b19e6d848348..42dda5b59020f051a811b07e55fb5c4d50a5776f 100644 --- a/src/components/utils/baseComponents.tsx +++ b/src/components/utils/baseComponents.tsx @@ -42,7 +42,9 @@ type BaseInputProps = { type baseInputClassNamesObj = { [key in InputSlots]: string }; export const baseInputClassNames = ( - isDisabled: boolean + isDisabled: boolean, + hasMainWrapper: boolean = true, // for some components (i.e., textarea) the inputwrapper is the main wrapper, + hasClearButton: boolean = true ): baseInputClassNamesObj => { // this is the border color (use text-*) const mainWrapperClass = (isDisabled: boolean) => @@ -64,9 +66,13 @@ export const baseInputClassNames = ( base: cursorPointer(isDisabled), label: "!text-foreground-body", input: "!text-foreground-body bg-default", - inputWrapper: clsx("px-2 rounded-[4px]", formFieldBg(isDisabled)), - mainWrapper: mainWrapperClass(isDisabled), - clearButton: clearButtonClass(isDisabled), + inputWrapper: clsx( + "px-2 rounded-[4px]", + formFieldBg(isDisabled), + !hasMainWrapper && mainWrapperClass(isDisabled) + ), + mainWrapper: hasMainWrapper && mainWrapperClass(isDisabled), + clearButton: hasClearButton && clearButtonClass(isDisabled), } as baseInputClassNamesObj; }; diff --git a/src/index.ts b/src/index.ts index e7ea8b04764cf2fbeaa01b43646f2ee39dd2e918..5bcf613893aa80f072c055b200db36164169839b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ export { default as ProgressBar } from "./components/ProgressBar"; export { default as Radio } from "./components/Radio"; export { default as Slider } from "./components/Slider"; export { default as Table } from "./components/Table"; +export { default as TextArea } from "./components/TextArea"; export { default as TextInput } from "./components/TextInput"; export { default as Toggle } from "./components/Toggle"; export { default as Typography } from "./components/Typography"; diff --git a/src/stories/TextArea.stories.tsx b/src/stories/TextArea.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5213320b92056406651e6ed9b7c30fbd21070be0 --- /dev/null +++ b/src/stories/TextArea.stories.tsx @@ -0,0 +1,122 @@ +import { Meta } from "@storybook/react"; +import { useMemo, useState } from "react"; +import TextArea from "src/components/TextArea.tsx"; + +const meta = { + title: "Components/Text Area", + component: TextArea, +} satisfies Meta; + +export default meta; + +export const Default = () => { + const [value, setValue] = useState(""); + return ( + <TextArea + label="Some long story goes here." + value={value} + onValueChange={setValue} + ></TextArea> + ); +}; + +export const CustomValidation = () => { + const [value, setValue] = useState(""); + + const validateOnlyCapitals = (str: string) => str.match(/^[A-Z]+/g); + + const isInvalid = useMemo(() => { + if (value === "") return false; + return !validateOnlyCapitals(value); + }, [value]); + + return ( + <TextArea + label="Only capitals" + value={value} + onValueChange={setValue} + isInvalid={isInvalid} + errorMessage={isInvalid ? "Only capitals are allowed!" : ""} + /> + ); +}; + +export const Required = () => { + const [value, setValue] = useState(""); + return ( + <TextArea + label="I require a value" + isRequired={true} + value={value} + onValueChange={setValue} + /> + ); +}; + +export const Disabled = () => { + return ( + <TextArea + label="You can't interact with this component!" + isDisabled={true} + value="*Evil laughter*" + /> + ); +}; + +export const ReadOnly = () => { + return ( + <TextArea + label="You can't change this value!" + isReadOnly={true} + value="*Neutral laughter*" + /> + ); +}; + +export const MaxLength = () => { + const [value, setValue] = useState(""); + return ( + <TextArea + label="Maximum of 100 characters" + maxLength={100} + value={value} + onValueChange={setValue} + /> + ); +}; + +export const MaxRows = () => { + const [value, setValue] = useState(""); + return ( + <TextArea + label="Maximum of 2 rows" + maxRows={2} + value={value} + onValueChange={setValue} + /> + ); +}; + +export const MinRows = () => { + const [value, setValue] = useState(""); + return ( + <TextArea + label="Minimum of 3 rows" + maxRows={3} + value={value} + onValueChange={setValue} + /> + ); +}; + +export const DisableAutoSize = () => { + const [value, setValue] = useState(""); + return ( + <TextArea + label="Autosize is disabled" + disableAutosize={true} + value={value} + onValueChange={setValue} + /> + ); +}; \ No newline at end of file