Open7

shadcn/uiでアイコン付きInputコンポーネントを作成する

takeyu1013takeyu1013

Create Next Appを実行する。

$ npx create-next-app@latest --experimental-app
✔ What is your project named? … my-app
✔ Would you like to use TypeScript with this project? … No / Yes
✔ Would you like to use ESLint with this project? … No / Yes
✔ Would you like to use Tailwind CSS with this project? … No / Yes
✔ Would you like to use `src/` directory with this project? … No / Yes
✔ What import alias would you like configured? … @/*
takeyu1013takeyu1013

Inputコンポーネントをインストールする。

npx shadcn-ui add input
takeyu1013takeyu1013

/components/ui/input.tsxを編集する。

/components/ui/input.tsx
"use client";

import { cn } from "@/lib/utils";
import { useMergedRef } from "@mantine/hooks";
import {
  ElementType,
  InputHTMLAttributes,
  forwardRef,
  useReducer,
  useRef,
} from "react";

type InputProps = InputHTMLAttributes<HTMLInputElement> & {
  icon?: ElementType;
};

const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ className, type, icon: Icon, ...props }, ref) => {
    const inputRef = useRef<HTMLInputElement>(null);
    const [isFocused, handleFocusChange] = useReducer(
      (_: boolean, isFocused: boolean) => {
        const { current } = inputRef;
        if (!current) return isFocused;
        if (isFocused === false) {
          current.blur();
        } else {
          current.focus();
        }
        return isFocused;
      },
      false
    );
    const mergedRef = useMergedRef(ref, inputRef);

    return (
      <div
        className={cn(
          "relative w-full flex items-center min-w-[10rem] focus:outline-none focus:ring-2 bg-white hover:bg-gray-50 text-gray-500 border-gray-300 focus:ring-blue-200 rounded-md border shadow-sm",
          isFocused && "ring-2",
          className
        )}
        onFocus={() => {
          handleFocusChange(true);
        }}
        onBlur={() => {
          handleFocusChange(false);
        }}
      >
        {Icon ? (
          <Icon
            className="shrink-0 h-5 w-5 text-gray-400 ml-3.5"
            aria-hidden="true"
          />
        ) : null}
        <input
          className="w-full focus:outline-none focus:ring-0 bg-transparent pl-4 pr-4 py-2 font-medium border-0 placeholder:text-gray-500"
          type={type}
          ref={mergedRef}
          {...props}
        />
      </div>
    );
  }
);
Input.displayName = "Input";

export { Input };
takeyu1013takeyu1013

以下を作成する。

/components/Search.tsx
"use client";

import { MagnifyingGlassIcon } from "@heroicons/react/24/solid";
import { Input } from "@/components/ui/input";

export const Search = () => {
  return (
    <Input type="search" icon={MagnifyingGlassIcon} placeholder="Search..." />
  );
};

ページでインポートする。

/app/page.tsx
import { Search } from "@/components/Search";

export default function Home() {
  return (
    <main className="p-2 bg-neutral-100 h-screen">
      <Search />
    </main>
  );
}

https://codesandbox.io/p/sandbox/gracious-bash-ll47h2

背景

shadcn/uiのInput要素はアイコン表示に対応していなかったので、TremorのTextInputコンポーネントを採用しようとした。
https://www.tremor.so/docs/components/text-input
しかし、TremorはInputのフォントサイズが14ptで固定されており、classNameで変更できなかった。classNameで変更できるのは、コンポーネントの一番外側のrootスタイルのみで、内側の子コンポーネントのスタイルを変更できない。そのため、上記のようなコンポーネントを作成した。