上文:Floating UI 使用経験共有 - Popover
この記事では、Floating UI を使用して別の一般的な浮動 UI コンポーネントである Dialog(ダイアログ) を作成する方法を共有します。Dialog は、即座に注意を引く必要がある情報を表示する浮動要素であり、ページのコンテンツの上に表示され、ページとのインタラクションを防ぎます。それが閉じられるまで。
ポップアップと似たインタラクションがありますが、主に 2 つの違いがあります:
- モーダルであり、ダイアログの背後に背景を表示し、背後のコンテンツを暗くし、ページの残りの部分にアクセスできなくします。
- ビューポートの中央に配置され、特定の参照要素に固定されません。
アクセシブルなダイアログコンポーネントには、以下のポイントがあります:
Dismissal
:ユーザーがesc
キーを押すか、開いているダイアログの外をクリックすると、閉じます。Role
:要素に関連する役割と ARIA 属性が付与され、スクリーンリーダーがアクセスできるようにします。Focus management
: フォーカスはダイアログ内に完全に閉じ込められ、ユーザーによって解除される必要があります。
目標コンポーネント#
目標:このような Dialog Demo 👇
次に、Dialog
という名前の React コンポーネントを作成する必要があります。これは、@floating-ui/react
ライブラリを使用して、インタラクティブな浮動ダイアログを作成します。以下は、このコンポーネントの構想です:
コンポーネントパラメータ#
Dialog
コンポーネントは、以下のパラメータを受け取る必要があります:
rootId
:浮動要素のルート要素、オプション。open
:ダイアログが開いているかどうかを制御するブール値。initialOpen
:ダイアログが初めて開いているかどうかのブール値、デフォルトはfalse
。onOpenChange
:ダイアログの開閉状態が変わったときのコールバック関数、ブール値の引数を受け取ります。render
:オブジェクトパラメータを受け取る関数で、そのオブジェクトにはダイアログを閉じるためのclose
メソッドが含まれます。この関数は、ダイアログ内にレンダリングする React ノードを返します。className
:ダイアログに適用する CSS クラス名。overlayClass
:浮動オーバーレイに適用する CSS クラス名。containerClass
:ダイアログコンテナに適用する CSS クラス名。isDismiss
:外部領域をクリックしてダイアログを閉じる機能を有効にするかどうかを決定するブール値、デフォルトはtrue
。children
:React 子要素で、クリックするとポップアップが開くボタンなど。showCloseButton
:閉じるボタンを表示するかどうかを決定するブール値、デフォルトはtrue
。
コンポーネント機能#
Dialog
コンポーネントの主な機能は、インタラクティブな浮動ダイアログを作成することです。これは、閉じるボタンをクリックするか、ダイアログの外部領域をクリックすることで閉じることができます。ダイアログの開閉状態は、open
と onOpenChange
パラメータを通じて制御(制御された)されるか、内部状態によって自動的に管理(非制御)されます。
Dialog
コンポーネントは、@floating-ui/react
ライブラリの複数のフックを使用しています:
useFloating
:ダイアログの開閉状態を管理します。useClick
、useRole
、およびuseDismiss
:ダイアログのインタラクションを処理します(クリックや役割管理など)。useInteractions
:インタラクション属性を取得して設定します。
さらに、Dialog
コンポーネントは、浮動ダイアログの UI を作成するために FloatingPortal
、FloatingOverlay
、および FloatingFocusManager
コンポーネントを使用しています。
完全なコード#
実際に機能的な Dialog の例を書くことができ、オーバーレイ、内部要素のスタイルをカスタマイズしたり、オーバーレイをクリックしてポップアップを閉じるかどうかを制御したり、Framer-motion を組み合わせてポップアップアニメーションを作成することもできます(機会があれば、また書きます)。
import {
FloatingFocusManager,
FloatingOverlay,
FloatingPortal,
useClick,
useDismiss,
useFloating,
useInteractions,
useRole,
} from '@floating-ui/react';
import clsx from 'clsx';
import React, { cloneElement, useState } from 'react';
import { CgClose } from 'react-icons/cg';
type DialogProps = {
rootId?: string;
open?: boolean;
initialOpen?: boolean;
onOpenChange?: (open: boolean) => void;
children?: JSX.Element;
render: (props: { close: () => void }) => React.ReactNode;
className?: string;
overlayClass?: string;
containerClass?: string;
isDismiss?: boolean;
showCloseButton?: boolean;
};
export default function Dialog({
initialOpen = false,
open: controlledOpen,
onOpenChange: setControlledOpen,
children,
className,
render,
rootId: customRootId,
overlayClass,
containerClass,
showCloseButton = true,
isDismiss = true,
}: DialogProps) {
const [uncontrolledOpen, setUncontrolledOpen] = useState(initialOpen);
const open = controlledOpen ?? uncontrolledOpen;
const setOpen = setControlledOpen ?? setUncontrolledOpen;
const { reference, floating, context } = useFloating({
open,
onOpenChange: setOpen,
});
const click = useClick(context);
const role = useRole(context);
const dismiss = useDismiss(context, { enabled: isDismiss, outsidePressEvent: 'mousedown' });
const { getReferenceProps, getFloatingProps } = useInteractions([click, role, dismiss]);
const onClose = () => setOpen(false);
return (
<>
{children && cloneElement(children, getReferenceProps({ ref: reference, ...children.props }))}
<FloatingPortal id={customRootId}>
{open && (
<FloatingOverlay
className={clsx('absolute inset-0 z-10 flex h-full w-full items-center', overlayClass ?? 'bg-black/60')}
lockScroll
>
<div className={clsx('m-auto grid place-items-center', containerClass)}>
<FloatingFocusManager context={context}>
<div
className={clsx('relative overflow-hidden rounded-md bg-white', className ?? 'mx-24')}
ref={floating}
{...getFloatingProps()}
>
{showCloseButton && <CgClose className="absolute right-2 top-2 h-6 w-6 cursor-pointer" onClick={onClose} />}
{render({ close: onClose })}
</div>
</FloatingFocusManager>
</div>
</FloatingOverlay>
)}
</FloatingPortal>
</>
);
}
基本ダイアログフック#
公式の例 👉 CodeSandbox デモ
この例では、単一インスタンス用のダイアログを作成して基本を理解する方法を示しています。
この例を見てみましょう:
開いている状態#
import {useState} from 'react';
function Dialog() {
const [isOpen, setIsOpen] = useState(false);
}
isOpen
は、ダイアログが現在画面に開いているかどうかを決定します。これは条件付きレンダリングに使用されます。
useFloating フック#
useFloating()
フックは、ダイアログにコンテキストを提供します。いくつかの情報を渡す必要があります:
open
:上記のuseState()
フックからの開いている状態。onOpenChange
: ダイアログが開くまたは閉じるときに呼び出されるコールバック関数。これを使用してisOpen
状態を更新します。
import {useFloating} from '@floating-ui/react';
function Dialog() {
const [isOpen, setIsOpen] = useState(false);
const {refs, context} = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
});
}
インタラクションフック#
useClick()
は、参照要素をクリックしたときにダイアログを開くまたは閉じる機能を追加します。ただし、ダイアログは参照要素に付加されない可能性があるため、これはオプションです。(一般的にダイアログはポータルとして独立しているため、コンテキストは body になります)useDismiss()
は、ユーザーがesc
キーを押すか、ダイアログの外をクリックしたときにダイアログを閉じる機能を追加します。outsidePressEvent
オプションを'mousedown'
に設定すると、タッチイベントが遅延し、背景を通過しないようになります。デフォルトの動作は急速です。(あまり理解しにくいですが、おそらく)useRole()
は、ダイアログと参照要素に正しい ARIA 属性を追加します。
最後に、useInteractions()
は、すべてのプロップをプロップゲッターに統合し、レンダリングに使用できます。
import {
// ...
useClick,
useDismiss,
useRole,
useInteractions,
useId,
} from '@floating-ui/react';
function Dialog() {
const [isOpen, setIsOpen] = useState(false);
const {refs, context} = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
});
const click = useClick(context);
const dismiss = useDismiss(context, {
outsidePressEvent: 'mousedown',
});
const role = useRole(context);
// すべてのインタラクションをプロップゲッターに統合
const {getReferenceProps, getFloatingProps} = useInteractions([
click,
dismiss,
role,
]);
// ラベルと説明の ID を設定
const labelId = useId();
const descriptionId = useId();
}
レンダリング#
すべての変数とフックを設定したので、要素をレンダリングできます。
function Dialog() {
// ...
return (
<>
<button ref={refs.setReference} {...getReferenceProps()}>
参照要素
</button>
{isOpen && (
<FloatingOverlay
lockScroll
style={{background: 'rgba(0, 0, 0, 0.8)'}}
>
<FloatingFocusManager context={context}>
<div
ref={refs.setFloating}
aria-labelledby={labelId}
aria-describedby={descriptionId}
{...getFloatingProps()}
>
<h2 id={labelId}>見出し要素</h2>
<p id={descriptionId}>説明要素</p>
<button onClick={() => setIsOpen(false)}>
閉じる
</button>
</div>
</FloatingFocusManager>
</FloatingOverlay>
)}
</>
);
}
{...getReferenceProps()}
/{...getFloatingProps()}
前回の説明で、インタラクションフックからプロップを関連要素に伝播させます。これらには、onClick
、aria-expanded
などのプロップが含まれます。
FloatingPortal & FloatingOverlay & FloatingFocusManager#
<FloatingOverlay />
は、浮動要素の背後に背景オーバーレイ要素をレンダリングするコンポーネントで、ボディのスクロールをロックする能力を持っています。 FloatingOverlay ドキュメント- 背景コンテンツを暗くし、浮動要素の背後のポインターイベントを防ぐ固定の基本スタイルを提供します。
- これは通常の
<div/>
であるため、任意の CSS ソリューションでスタイル設定できます。
<FloatingFocusManager />
- FloatingPortal ドキュメント ページ内の浮動要素のフォーカスを管理および制御するコンポーネント
- フォーカスの変化を自動的に検出し、ページ上の浮動要素の位置と状態を調整し、ページ上のすべての要素のアクセシビリティと可用性を確保します。
- デフォルトでは、通常内部にフォーカスをキャッチします。
- 浮動要素を直接包むべきであり、ダイアログがレンダリングされるときのみレンダリングされるべきです。 FloatingFocusManager ドキュメント
<FloatingPortal />
は、浮動要素を指定されたコンテナ要素に転送します。デフォルトでは、アプリケーションのルートの外にあり、body に入ります。- ルートをカスタマイズでき、ID を持つノードを選択するか、それを作成して指定されたルート(body)に追加できます。
import {FloatingPortal} from '@floating-ui/react';
function Tooltip() {
return (
isOpen && (
<FloatingPortal>
<div>浮動要素</div>
</FloatingPortal>
)
);
}