Input
Categoryprimitive
ArchitectureReact
Client
Component
Dependencies
Usage
import { DemoWrapper } from "@/demos/react/demoWrapper";
import { Input } from "@/components/react/primitive/input";
import { UserIcon } from "@heroicons/react/24/solid";
export const InputDemo = ({ code }: { code: string }) => {
return (
<DemoWrapper code={code}>
<Input
iconLeft={<UserIcon />}
placeholder="username"
className="max-w-[250px] m-auto"
clearable
/>
</DemoWrapper>
);
};
Install Instructions
Copy and paste the code into that file
import {
Dispatch,
ComponentProps,
RefObject,
SetStateAction,
forwardRef,
useRef,
ReactNode,
} from "react";
// Hero Icons
import { BackspaceIcon } from "@heroicons/react/24/solid";
// Sub-Components
import { Button } from "@/components/react/styled/button";
// CVA
import { cva } from "class-variance-authority";
// TW-Merge
import { twMerge } from "tailwind-merge";
const InputWrapperClassNames = cva([
"inline-flex",
"w-[100%]",
"relative",
"overflow-hidden",
"gap-2",
"data-[invalid=true]:border-red-500",
"transition-all",
"border",
"border-transparent",
"focus-within:border-primary-400",
"bg-neutral-200",
"dark:bg-neutral-800",
"text-sm",
"rounded-xl",
"[&:has(input:read-only)]:select-none",
"[&:has(input:read-only)]:text-neutral-700",
"[&:has(input:read-only)]:fill-neutral-700",
"dark:[&:has(input:read-only)]:text-neutral-300",
"dark:[&:has(input:read-only)]:fill-neutral-300",
]);
const iconClassNames = cva(
[
"overflow-hidden",
"block",
"justify-center",
"absolute",
"rounded-none",
"top-0",
"p-1",
"h-full",
"outline-none",
"opacity-75",
"[&_span_svg]:w-auto",
"[&_span_svg]:h-full",
"[&_span_svg]:fill-inherit",
],
{
variants: {
clickable: {
true: [],
false: ["pointer-events-none"],
},
},
}
);
const InputClassNames = cva(
[
"bg-transparent",
"text-inherit",
"outline-none",
"px-4",
"py-1",
"max-w-[100%]",
"grow",
"h-full",
],
{
variants: {
leftIcon: {
true: ["pl-8"],
false: [],
},
rightIcon: {
true: ["pr-8"],
false: [],
},
},
defaultVariants: {
leftIcon: false,
rightIcon: false,
},
}
);
// Utils
const clear = (
ref: RefObject<HTMLInputElement>,
onValue?: Dispatch<SetStateAction<string>>
) => {
onValue && onValue("");
if (typeof onValue === "undefined") {
(ref.current as HTMLInputElement).value = "";
}
};
export interface InputProps
extends Omit<ComponentProps<"input">, "size" | "id"> {
value?: string;
onValue?: Dispatch<SetStateAction<string>>;
iconRight?: ReactNode;
iconLeft?: ReactNode;
clearable?: boolean;
invalid?: boolean;
disabled?: boolean;
readOnly?: boolean;
pattern?: string;
id?: string;
}
export const Input = forwardRef<HTMLInputElement, InputProps>(
(
{
value,
onValue,
iconRight,
iconLeft,
clearable = false,
invalid = false,
readOnly = false,
onChange,
className = "",
...props
},
ref
) => {
const internalRef = useRef<HTMLInputElement>(null);
return (
<div
data-invalid={invalid}
data-read-only={readOnly}
className={twMerge(InputWrapperClassNames(), className)}
>
{iconLeft && (
<Button
variant="ghost"
disabled={readOnly}
square={true}
className={twMerge(iconClassNames({ clickable: false }), "left-0")}
>
{iconLeft}
</Button>
)}
<input
value={value}
onChange={(e) => {
onValue && onValue(e.target.value);
onChange && onChange(e);
}}
className={twMerge(
InputClassNames({
leftIcon: typeof iconLeft !== "undefined",
rightIcon: typeof iconRight !== "undefined" || clearable,
})
)}
ref={ref ? ref : internalRef}
readOnly={readOnly}
{...props}
/>
{clearable ? (
<Button
variant="ghost"
square={true}
onClick={clear.bind(
null,
ref
? (ref as unknown as RefObject<HTMLInputElement>)
: internalRef,
onValue?.bind(null, "")
)}
disabled={readOnly}
className={twMerge(
iconClassNames({ clickable: true }),
"hover:bg-primary-400/50 focus-visible:bg-primary-400/50 right-0"
)}
>
<BackspaceIcon className="w-auto h-full fill-inherit" />
</Button>
) : (
iconRight && (
<Button
variant="ghost"
square={true}
disabled={readOnly}
className={twMerge(
iconClassNames({ clickable: false }),
"right-0"
)}
>
{iconRight}
</Button>
)
)}
</div>
);
}
);
Input.displayName = "Input";