- {errorMessage ? (
+ {errorMessage && isInvalid && !isValid ? (
<>{errorMessage}>
@@ -65,32 +87,40 @@ const Input = forwardRef<"input", InputProps>((props, ref) => {
getDescriptionProps,
]);
- const innerWrapper = useMemo(() => {
- if (startContent || end) {
- return (
-
- {startContent}
-
- {end}
-
- );
- }
+ const startWrapper = useMemo(() => {
+ return startContent &&
{startContent}
;
+ }, [startContent, getStartContentWrapperProps]);
+
+ const endWrapper = useMemo(() => {
+ return endContent &&
{endContent}
;
+ }, [endContent, getEndContentWrapperProps]);
+ const innerWrapper = useMemo(() => {
return (
+ {startWrapper}
+ {/*{isInvalid ? invalid : isValid && valid}*/}
+ {isInvalid && !isValid ? invalid : valid}
+ {clearable}
+ {endWrapper}
);
- }, [startContent, end, getInputProps, getInnerWrapperProps]);
+ }, [
+ startContent,
+ endContent,
+ getInputProps,
+ getInnerWrapperProps,
+ getStartContentWrapperProps,
+ getEndContentWrapperProps,
+ ]);
const mainWrapper = useMemo(() => {
if (shouldLabelBeOutside) {
return (
-
- {!isOutsideLeft ? labelContent : null}
- {innerWrapper}
-
+ {!isOutsideLeft ? labelContent : null}
+
{innerWrapper}
{helperWrapper}
);
@@ -117,6 +147,8 @@ const Input = forwardRef<"input", InputProps>((props, ref) => {
getInputWrapperProps,
getErrorMessageProps,
getDescriptionProps,
+ getEndContentWrapperProps,
+ getStartContentWrapperProps,
]);
return (
diff --git a/packages/components/input/src/textarea.tsx b/packages/components/input/src/textarea.tsx
index 6f8a691f..fddeb1de 100644
--- a/packages/components/input/src/textarea.tsx
+++ b/packages/components/input/src/textarea.tsx
@@ -1,8 +1,9 @@
import {dataAttr} from "@jala-banyu/shared-utils";
import {forwardRef} from "@jala-banyu/system";
import {mergeProps} from "@react-aria/utils";
-import {useMemo, useState} from "react";
+import React, {useMemo, useState} from "react";
import TextareaAutosize from "react-textarea-autosize";
+import {CheckIcon, ExclamationIcon} from "@jala-banyu/shared-icons";
import {UseInputProps, useInput} from "./use-input";
@@ -40,6 +41,11 @@ export interface TextAreaProps extends Omit
,
* @default 8
*/
maxRows?: number;
+ /**
+ * Maximum number of rows up to which the textarea can grow
+ * @default 8
+ */
+ maxLength?: number | undefined;
/**
* Reuse previously computed measurements when computing height of textarea.
* @default false
@@ -65,6 +71,7 @@ const Textarea = forwardRef<"textarea", TextAreaProps>(
cacheMeasurements = false,
disableAutosize = false,
onHeightChange,
+ maxLength,
...otherProps
},
ref,
@@ -75,24 +82,28 @@ const Textarea = forwardRef<"textarea", TextAreaProps>(
description,
startContent,
endContent,
+ isInvalid,
+ isValid,
hasHelper,
shouldLabelBeOutside,
- shouldLabelBeInside,
+ maxLengthContent,
errorMessage,
getBaseProps,
getLabelProps,
- getInputProps,
getInnerWrapperProps,
getInputWrapperProps,
getHelperWrapperProps,
getDescriptionProps,
getErrorMessageProps,
+ getTextareaProps,
+ getInvalidIconProps,
+ getValidIconProps,
} = useInput({...otherProps, ref, isMultiline: true});
const [hasMultipleRows, setIsHasMultipleRows] = useState(minRows > 1);
const [isLimitReached, setIsLimitReached] = useState(false);
const labelContent = label ? : null;
- const inputProps = getInputProps();
+ const textareaProps = getTextareaProps();
const handleHeightChange = (height: number, meta: TextareaHeightChangeMeta) => {
if (minRows === 1) {
@@ -107,41 +118,59 @@ const Textarea = forwardRef<"textarea", TextAreaProps>(
onHeightChange?.(height, meta);
};
+ const invalid = useMemo(() => {
+ return (
+
+
+
+ );
+ }, [isInvalid, getInvalidIconProps]);
+
+ const valid = useMemo(() => {
+ return (
+
+
+
+ );
+ }, [isValid, getValidIconProps]);
+
const content = disableAutosize ? (
-
+
) : (
);
const innerWrapper = useMemo(() => {
- if (startContent || endContent) {
- return (
-
- {startContent}
- {content}
- {endContent}
-
- );
- }
-
- return {content}
;
- }, [startContent, inputProps, endContent, getInnerWrapperProps]);
+ return (
+
+ {startContent}
+ {content}
+ {isInvalid && !isValid ? invalid : valid}
+ {endContent}
+
+ );
+ }, [startContent, textareaProps, endContent, getInnerWrapperProps]);
return (
{shouldLabelBeOutside ? labelContent : null}
- {shouldLabelBeInside ? labelContent : null}
{innerWrapper}
+ {maxLengthContent}
{hasHelper ? (
{errorMessage ? (
diff --git a/packages/components/input/src/use-input.ts b/packages/components/input/src/use-input.ts
index 56c5b979..a606b954 100644
--- a/packages/components/input/src/use-input.ts
+++ b/packages/components/input/src/use-input.ts
@@ -33,10 +33,18 @@ export interface Props
;
+
+ startContentWrapper?: Ref;
+
+ endContentWrapper?: Ref;
+
+ maxLengthWrapper?: Ref;
/**
* Element to be rendered in the left side of the input.
*/
startContent?: React.ReactNode;
+
+ maxLengthContent?: React.ReactNode;
/**
* Element to be rendered in the right side of the input.
* if you pass this prop and the `onClear` prop, the passed element
@@ -91,6 +99,7 @@ export function useInput(() => {
- if ((!originalProps.labelPlacement || originalProps.labelPlacement === "inside") && !label) {
+ if (!originalProps.labelPlacement && !label) {
return "outside";
}
- return originalProps.labelPlacement ?? "inside";
+ return originalProps.labelPlacement ?? "outside";
}, [originalProps.labelPlacement, label]);
const isClearable = !!onClear || originalProps.isClearable;
@@ -190,7 +200,7 @@ export function useInput {
+ return {
+ ref: domRef,
+ "data-slot": "textarea",
+ "data-filled": dataAttr(isFilled),
+ "data-filled-within": dataAttr(isFilledWithin),
+ "data-has-start-content": dataAttr(hasStartContent),
+ "data-has-end-content": dataAttr(!!endContent),
+ className: slots.textarea({
+ class: clsx(classNames?.textarea, !!inputValue ? "is-filled" : ""),
+ }),
+ ...mergeProps(
+ focusProps,
+ inputProps,
+ filterDOMProps(otherProps, {
+ enabled: true,
+ labelable: true,
+ omitEventNames: new Set(Object.keys(inputProps)),
+ }),
+ props,
+ ),
+ required: originalProps.isRequired,
+ "aria-readonly": dataAttr(originalProps.isReadOnly),
+ "aria-required": dataAttr(originalProps.isRequired),
+ onChange: chain(inputProps.onChange, onChange),
+ };
+ },
+ [
+ slots,
+ inputValue,
+ focusProps,
+ inputProps,
+ otherProps,
+ isFilled,
+ isFilledWithin,
+ hasStartContent,
+ endContent,
+ classNames?.input,
+ originalProps.isReadOnly,
+ originalProps.isRequired,
+ onChange,
+ ],
+ );
const getInputWrapperProps: PropGetter = useCallback(
(props = {}) => {
@@ -433,6 +487,65 @@ export function useInput {
+ return {
+ ...props,
+ className: slots.inValidIcon({class: clsx(classNames?.inValidIcon, props?.className)}),
+ };
+ },
+ [slots, classNames?.inValidIcon],
+ );
+
+ const getValidIconProps: PropGetter = useCallback(
+ (props = {}) => {
+ return {
+ ...props,
+ className: slots.validIcon({class: clsx(classNames?.validIcon, props?.className)}),
+ };
+ },
+ [slots, classNames?.validIcon],
+ );
+
+ const getStartContentWrapperProps: PropGetter = useCallback(
+ (props = {}) => {
+ return {
+ ...props,
+ "data-slot": "start-content-wrapper",
+ className: slots.startContentWrapper({
+ class: clsx(classNames?.startContentWrapper, props?.className),
+ }),
+ };
+ },
+ [slots, classNames?.startContentWrapper],
+ );
+
+ const getEndContentWrapperProps: PropGetter = useCallback(
+ (props = {}) => {
+ return {
+ ...props,
+ "data-slot": "end-content-wrapper",
+ className: slots.endContentWrapper({
+ class: clsx(classNames?.endContentWrapper, props?.className),
+ }),
+ };
+ },
+ [slots, classNames?.endContentWrapper],
+ );
+
+ const getMaxLengthWrapperProps: PropGetter = useCallback(
+ (props = {}) => {
+ return {
+ ...props,
+ "data-slot": "max-length-wrapper",
+ className: slots.maxLengthWrapper({
+ class: clsx(classNames?.maxLengthWrapper, props?.className),
+ }),
+ };
+ },
+ [slots, classNames?.maxLengthWrapper],
+ );
+
return {
Component,
classNames,
@@ -444,13 +557,14 @@ export function useInput (
+
+
+
+ ),
+ ],
+} as Meta;
+
+const defaultProps = {
+ ...input.defaultVariants,
+};
+
+const Template = (args) => (
+
+
+
+);
+
+const RegexValidationTemplate = (args) => {
+ const [value, setValue] = React.useState("");
+
+ const validateEmail = (value: string) => value.match(/^[A-Z0-9._%+-]+@[A-Z0-9.-]+.[A-Z]{2,4}$/i);
+
+ const validationState = React.useMemo(() => {
+ if (value === "") return false;
+
+ return !!validateEmail(value);
+ }, [value]);
+
+ return (
+
+
+
+ );
+};
+
+const StartContentTemplate = (args) => (
+
+
+
+
+
+ }
+ type="number"
+ />
+
+);
+
+const EndContentTemplate = (args) => (
+
+
+ .tech
+
+ }
+ label="Website"
+ placeholder="Domain name"
+ type="url"
+ />
+
+);
+
+const StartAndEndContentTemplate = (args) => (
+