Radix UI の Dialog コンポーネントTips
RadixUIはアクセシビリティに焦点を当てた低レベルのUIコンポーネントライブラリで、非常に柔軟性が高く、スタイリングが制限されていない点が特徴です。ダイアログのようなUIはアクセシビリティ等をしっかり実装しようとすると考慮する点が意外に多く、実装コストがかかるのでRadixを活用していきたいところです。
この記事では、RadixのDialogコンポーネントを使用するにあたってのTipsを共有していきます。またshadcn/uiに対しても、中身はRadixが使用されているため基本的に応用が可能です。
Dialogコンポーネントの基本的な使い方はドキュメントを参照してください。
イベント時の自動制御
Dialogコンポーネントには以下のようにイベント時の自動処理が発生します。
- オープン、クローズ時に自動的にフォーカスが発生する
- Escキー押下時、ダイアログ外への操作時にダイアログがクローズする
これらを無効・制御するため、Content
コンポーネントにイベントハンドラのPropsが用意されています。
オープン、クローズ時のフォーカス制御
デフォルトではオープン時には自動でダイアログコンテンツ内の要素にフォーカスし、クローズ時は開く前の要素にフォーカスが自動で戻ります。これを制御するにはonOpenAutoFocus
、onCloseAutoFocus
を利用します。
まず、無効にしたい場合は一般的なイベント同様にpreventDefault()
が利用できます。
<Dialog.Content
onOpenAutoFocus={(e) => {
e.preventDefault();
}}>
...
</Dialog.Content>
フォーカスする要素を制御したい場合は、対象の要素のfocus()
メソッドをコールバック関数内で呼び出す形で実現します。ここでは、useRef
を用いて対象の要素への参照がある例を記載します。
<Dialog.Content
onOpenAutoFocus={(e) => {
ref.current.focus();
}}>
...
</Dialog.Content>
Escキー押下時、ダイアログ外への操作時のクローズ制御
デフォルトではEscキーを押下したり、ダイアログのオーバーレイ部分をクリックしたりするとダイアログがクローズします。
これらを制御するために、先ほど同様にEscキーの制御であればonEscapeKeyDown
、ダイアログ外操作に対する制御であればonPointerDownOutside
とonInteractOutside
が用意されています。
例えば、Escキーを押下した場合はダイアログを閉じるのではなく、別のUIを動作させたい場合などに利用できます。
<Dialog.Content
onEscapeKeyDown={(e) => {
e.preventDefault()
// 別のUIを表示させる任意の処理
}}>
...
</Dialog.Content>
`onPointerDownOutside`と`onInteractOutside`の違い
どちらもダイアログ外の操作に対して(オーバーレイ部分をクリックした場合など)イベントが発火しますが、onPointerDownOutside
はポインタイベントが発生した場合に限定されます。onInteractOutside
はその他のインタラクションも含むので基本的にはonInteractOutside
を利用するのが良いかと思います。
オープン、クローズ時の処理を一括制御
上記で触れたように、Dialogコンポーネントがクローズする契機は複数あります。(Escキー、オーバーレイ、クローズボタン押下など)。また、オープンの契機も複数作り込むことが可能です。
それぞれイベントハンドラを設定することも可能ですが、共通で行いたい処理を設定するにはRoot
コンポーネントのonOpenChange
が利用できます。onOpenChange
は開閉時にboolean
でステートが渡されるのでこちらで制御します。
<Dialog.Content
onOpenChange={(open) => {
// クローズの処理
if (!open.valueOf()) {
// 任意の処理
}
}}
>
...
</Dialog.Content>
クローズのアニメーション
クローズのアニメーションにはdata-state
属性を利用します。Overlay
やContent
コンポーネントはクローズ時にこの属性がdata-state=false
となるので、これをCSSセレクタに利用しアニメーションを設定します。
<Dialog.Root>
<Dialog.Portal>
<Dialog.Overlay className="DialogOverlay" />
<Dialog.Content className="DialogContent">
...
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
.DialogOverlay[data-state='closed'] {
animation: overlayClose 1000ms cubic-bezier(0.16, 1, 0.3, 1);
}
.DialogContent[data-state='closed'] {
animation: contentClose 1000ms cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes overlayClose {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes contentClose {
from {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
to {
opacity: 0;
transform: translate(-50%, -48%) scale(0.96);
}
}
Discussion