Headless UIのListBoxをコンポーネント化

### 目的
Headkess UI のListBoxがキレイなのでコンポーネント化したかった。ちなみにサンプルのコードはだいぶ複雑なので、コンポーネント化しないと逆に使わないかも。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1259753/9ebe7326-186e-acac-4068-51ebff83c754.png" width=300>
なかなかキレイですよね。
【Headless UI】https://headlessui.com/react/listbox

#### MyListBoxというコンポーネントにして、こんな感じで使いたい！
```
...
<p className="text-sm -mb-2">Time</p>
<MyListBox regiName="time" items={["10:00", "11:00", "12:00"]} />
...
```

#### 完成形
https://github.com/yahsmassa/myNextFormTips
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1259753/1dd7b553-c740-26ae-ae51-ed11ef5c38ff.png" width=300> 
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1259753/7c02ae6f-f888-39c2-ac98-392a3c2a9215.png" width=300>

### Headless UIとは

HeadはStyleのことを言っていて、Style自体は持たず、「Active、Open、Selected」といった状態だけを内部に持っていて、それを使えば、マニアックなカスタマイズができる。サンプルのコードを読めばイメージできますが、しばらく自分も謎のままでした。使ってみて初めてわかります。

#### ちょっとやろうと思っただけなのに、使おうとすると困難の嵐 〜

- 本家サイトのサンプルが微妙に不親切？
- リストボックスやコンボボックスで選んだ値をフォームに戻すサンプル欲しかった！
- Githubにサンプルあまりなし、記事も少ない

#### 意外と難しく、くじけそうになりましたが、なんとかできました。
#### 〜 ポイント 〜

-  CreateContextでフォーム用ステートをグローバル変数化
-  コンポーネント側でフォームの変数更新
-  分割構文でフォーム用のオブジェクトを更新
-  Formタグは結局使わず（もちろん使っても良い）

あとは、実際のコードをご覧いただいて、物好きな人は参考にしてください。

```index.tsx
import React, { createContext, useState } from "react";
import MyListBox from "../components/MyListBox";
interface IForm {
  time: string;
  stylist: string;
  menu: string;
  reset: boolean;
}

const initFormData: IForm = {
  menu: "",
  stylist: "",
  time: "",
  reset: false,
};

export const myfm = createContext({});

export default function App() {
  const [fmFields, setfmFields] = useState<IForm>(initFormData);
  const value = {
    fmFields,
    setfmFields,
  };
  const reset = () => {
    setfmFields({
      ...initFormData,
      reset: true,
    });
  };
  const btnCss =
    "mt-3 ml-5 w-[100px] text-blue-500 border-2 border-blue-500 rounded-2xl hover:bg-blue-300 hover:text-white";
  return (
    <myfm.Provider value={value}>
      <div className="m-5">
        <h1 className="mb-3">Reservation Menu</h1>

        <p className="text-sm -mb-2">Time</p>
        <MyListBox regiName="time" items={["10:00", "11:00", "12:00"]} />

        <p className="text-sm mt-2 -mb-1">Stylist</p>
        <MyListBox regiName="stylist" items={["Lisa", "Bob", "Steve"]} />

        <p className="text-sm mt-2 -mb-2">Menu</p>
        <MyListBox regiName="menu" items={["cut", "perm", "color"]} />

        <button onClick={reset} className={btnCss}>
          Reset
        </button>
        <button
          onClick={() => alert(JSON.stringify(fmFields))}
          className={btnCss}
        >
          Submit
        </button>
      </div>
    </myfm.Provider>
  );
}
```

そんでもって、肝心のコンポーネントです。本家サンプルをコピペして編集した感じ。

```MyListBox.tsx
import { Fragment, useState, useEffect, useContext } from "react";
import { Listbox, Transition } from "@headlessui/react";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid";
import { myfm } from "../pages/index";
import cn from "clsx";

interface Props {
  items: any[];
  className?: string;
  regiName: string;
}

export default function MyListBox({ items, className, regiName }: Props) {
  const [selected, setSelected] = useState("");
  const { fmFields, setfmFields }: any = useContext(myfm);

  useEffect(() => {
    setfmFields({
      ...fmFields,
      [regiName]: selected,
      reset: false,
    });
  }, [selected]);

  useEffect(() => {
    if (fmFields.reset) setSelected("");
  }, [fmFields]);

  return (
    <div className={cn("w-72", className)}>
      <Listbox value={selected} onChange={setSelected}>
        <div className="relative mt-1">
          <Listbox.Button
            className="relative h-9 w-full cursor-default rounded-lg bg-white py-2 pl-3
           pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500
           focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75
            focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm"
          >
            <span className="block truncate">{selected}</span>
            <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
              <ChevronUpDownIcon
                className="h-5 w-5 text-gray-400"
                aria-hidden="true"
              />
            </span>
          </Listbox.Button>
          <Transition
            as={Fragment}
            leave="transition ease-in duration-100"
            leaveFrom="opacity-100"
            leaveTo="opacity-0"
          >
            <Listbox.Options
              className="pl-0 z-50 absolute mt-1 max-h-60 w-full overflow-auto
             rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5
              focus:outline-none sm:text-sm"
            >
              {items.map((item, idx) => (
                <Listbox.Option
                  key={idx}
                  className={({ active }) =>
                    cn(
                      "relative cursor-default select-none py-2 pl-10 pr-4",
                      active ? "bg-amber-100 text-amber-900" : "text-gray-900"
                    )
                  }
                  value={item}
                >
                  {({ selected }) => (
                    <>
                      <span
                        className={cn(
                          "block truncate",
                          selected ? "font-medium" : "font-normal"
                        )}
                      >
                        {item}
                      </span>
                      {selected ? (
                        <span className="absolute inset-y-0 left-0 flex items-center pl-3 text-amber-600">
                          <CheckIcon className="h-5 w-5" aria-hidden="true" />
                        </span>
                      ) : null}
                    </>
                  )}
                </Listbox.Option>
              ))}
            </Listbox.Options>
          </Transition>
        </div>
      </Listbox>
    </div>
  );
}

```

#### 〜 失敗したこと 〜

React-hook-formで実装したら、Headless UIの大きな特徴である、Selectedが効かなくなってしまった。https://gist.github.com/jasonabullard/dfa2ec92bff5ff0a498f649c3c4cb432 を真似すればわかります。

#### 問題点 ####
親コンポーネントと、子コンポーネントの両方で状態変数を持ってるので、どうしても制御が複雑になります。Headless UIは、ConboboxやListBox以外のものを使うのが良さそうです。