Park UIでSlot Recipeを使用しているコンポーネントについて調べる

Park UIのSlot Recipeを使用しているコンポーネント、主にFieldコンポーネントを使う際に混乱した部分を整理する。

Park UIのFieldコンポーネントはField.Root
の中にField.Label
、Field.Input
などを記述することでラベルと入力欄を紐づけることができる。また、requiredやinvalidなどのプロパティをFiled.Root
のpropsに指定することで、Field.Root
の子要素にその状態を伝播することができる。
なお、この挙動自体はPark UIではなく、大元のArk UIの方で実装されている。

まず、Fieldコンポーネントに自前で用意したスタイルをあてこむ際に若干混乱した。park-ui/cliを用いて追加したFieldコンポーネントを見てみる。

src/components/ui/field.tsx
はrc/components/ui//styledfield.tsx
からexportされたコンポーネント群をas Field
として再度exportしている。

src/components/ui/styled/field.tsx
ではcreateStyleContext関数からwithProvider関数とwithContext関数を返し、それらを使用してArk UIのコンポーネントにpandaのrecipeに定義したスタイルを適用している。

createStyleContext関数は引数としてpandaのrecipeを受け取り、withRootProvider、withProvider、withContextという3つの関数を返す。
また、createStyleContext関数はジェネリック型としてRを宣言し、recipeの型をRとして扱う。recipeおよび型RはcreateStyleContext関数内で定義される各関数で使用される。

createStyleContext関数ではreactのContextが作成され、withProvider関数が返すコンポーネントをProviderでラップしている。
これによりwithContext関数が返すコンポーネントが、StyleContextの値にアクセスすることができるようになる。StyleContextの中に入る具体的な値はpandaのrecipe関数の返り値である。

withRootProvider関数はFieldコンポーネントで使用されている箇所が見当たらないので省略。

withProvider関数は引数としてComponent、slot、optionsを受け取る。Componentはpandaのrecipeを適用する対象となるコンポーネント、slotはrecipeのslotsに定義された任意のslotを受け取る。optionsはforwardPropsというフィールドを持ったオブジェクトで、これによりスタイリング対象となる要素ではなく、その子要素にpropsを伝播させることができる。
返り値の型はForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>
となっており、forwardRef関数でラップされたrefを受け取るコンポーネントであることを示す。
型引数のTはHTMLDivElementなどDOMノードの型を指定でき、forwardRef関数の型引数として使用される。型引数のPはwithProvider関数が返すコンポーネントが受け取るpropsの型を示す。

StyledComponentはpanda codegen
実行時に生成されるstyled-system
フォルダ(デフォルトの名前)からexportされたstyled関数を用いてComponentをラップしている。おそらくpandaのpresetやconfigに定義した型を参照できるようにするために、withProvider関数の引数として渡されたComponentをstyled関数でラップしているのかもしれない。

StyledSlotProviderはStyledComponentをfowardRef関数でラップし、refを受け取るコンポーネントとして定義している。

variantProps、otherPropsはrecipeのsplitVariantPropsの返り値として定義される。splitVariantPropsにコンポーネントのpropsを渡すことで、recipeで定義したvariant用propsと、それ以外のpropsとで区別している。
slotStylesはvariantPropsを引数としたrecipeを関数として実行して返り値を受け取る。slotStylesにrecipeのslotsで定義したslot名を指定してアクセスすることで、recipeで定義したslot名に対応するクラスを参照することができる。

createStyleContext関数の最初の方で作成したStyleContextを用いてStyledComponentをラップしたコンポーネントを返している。StyleContext.ProviderのvalueにはslotStylesを代入し、Context内のコンポーネントが共通のrecipeを参照できるようにしている。
StyledComponentにはrecipeのsplitVariantProps関数を実行して取り出したotherPropsと、ref、classNameをpropsとして渡している。classNameはpandaのcx関数を使用してslotStylesの特定のslot名に該当するクラスと、props.classNameとして渡されたクラスをマージした値を渡している。

次はwithContext関数。インターフェースはwithProvider関数とほぼ同じで、違いは引数optionsがないぐらい。

withContext関数の中身もwithProvider関数のそれと近い。Componentをstyled関数でラップしてStyledComponentとして定義している。

StyledComponentをforwardRef関数でラップしている。また、slotStylesをuseContextを通して取得し、StyledComponentのclassNameにslotStylesから取り出したクラスを適用している。

ここまで読んでみて、createStyleContext関数はSlot Recipeで定義したスタイルを適用した状態のコンポーネントを返すために実装された関数であることが分かる。refやotherPropsをStyledComponentにpropsとして渡しているのはArk UIの挙動を実現するためで、Park UIが独自で挙動を追加したいからというわけではなさそう。