🐈‍⬛

shadcn/uiで検索ボックスを作る

2024/02/18に公開

はじめに

shadcn/uiを使って次のような検索ボックスを作ったので、メモとして残します

使用技術

前提とする使用技術は以下の通り

shadcn/uiのcomponentとしてはCommandを使用します
https://ui.shadcn.com/docs/components/command

上記のCommandのexampleは、基本的に候補となるデータがクライアント側に全て揃っている前提なので、入力文字列に応じてサーバーから候補データを取得するとなると、少し工夫が必要になります

実装

全体像は以下のようになります

"use client"
import { Command, CommandEmpty, CommandInput, CommandItem, CommandList } from "@/components/ui/command"
import { FC, useCallback, useEffect, useRef, useState } from "react"

...

const Page: FC = () => {

  const inputRef = useRef<HTMLInputElement>(null); //focus周りの挙動を制御するために使用
  const [open, setOpen] = useState(false);
  const [selected, setSelected] = useState<string>()
  const [searchResults, setSearchResults] = useState<string[]>([])
  const [inputText, setInputText] = useState('')
  const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
    const input = inputRef.current
    if (input) {
      if (e.key === "Escape") {
        input.blur();
      }
    }
  }, [])
  useEffect(() => {
    // APIから検索結果を取得
    setSearchResults(mockSearch(inputText))
  }, [inputText])
  return (
    <div className='flex items-center size-full justify-center p-10'>
      <Command shouldFilter={false} onKeyDown={handleKeyDown} value={selected} className='overflow-visible w-[512px]'>
        <CommandInput value={inputText} ref={inputRef} placeholder='猫の種類を検索' onValueChange={(text) => {
          setInputText(text)
          // 再編集時には選択済み項目をクリア
          if (selected) {
            setSelected(undefined)
          }
        }}
          onBlur={() => setOpen(false)}
          onFocus={() => {
            setOpen(true)
            if (selected) {
              inputRef.current?.select() // フォーカス時に選択済み項目がある場合、全選択する
            }
          }}
        />
        <div className="relative mt-2">
          {!selected && open && (
            <CommandList className="absolute left-0 top-0 w-full rounded bg-background shadow-md">
              <CommandEmpty className="text-muted-foreground px-4 py-2">ヒットなし</CommandEmpty>
              {searchResults?.map(v => (
                <CommandItem
                  className="flex items-center gap-2"
                  onSelect={() => {
                    setSelected(v)
                    setInputText(v)
                  }}
                  value={v} key={v}>
                  {v}
                </CommandItem>
              ))}
            </CommandList>
          )}
        </div>
      </Command>
    </div>
  )
}

export default Page

ポイント

  • CommandList(選択候補)は検索ボックスにフォーカスが当たったタイミングで表示させたいので、表示・非表示の状態をこちらで制御できるようにしています(open, setOpen)。この制御がないと、inputからフォーカスが外れた後も選択候補が表示され続けてしまいます
  • リストから候補が選択された(もしくはESCキーが押下された)場合、CommandListを閉じるようにしています
  • 入力文字列に対する一致処理はAPI側で実施する予定なので、Command側では不要なフィルタリングは行わないようにshouldFilter={false}を指定しています

その他

API側で検索を行うという意図においては、次のIssueも参考になるかもしれません
https://github.com/shadcn-ui/ui/issues/868

Discussion