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";

Loading...