Floating UI 使用经验分享 - Dialog

上文：Floating UI 使用经验分享 - Popover

在本文中，我将分享如何使用 Floating UI 来创建另一种常见的浮动 UI 组件 ——Dialog（对话框）。Dialog 是一个浮动元素，显示需要立即关注的信息，他会出现在页面内容上并阻止与页面的交互，直到它被关闭。

它与弹出框有类似的交互，但有两个主要区别：

它是模态的，并在对话框后面呈现一个背景，使后面的内容变暗，使页面的其余部分无法访问。

它在视口中居中，不锚定到任何特定的参考元素。

一个可访问的对话框组件具有以下要点：

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 库的多个 Hook：

useFloating：用于管理对话框的打开和关闭状态。

useClick、useRole 和 useDismiss：用于处理对话框的交互，如点击和角色管理。

useInteractions：用于获取和设置交互属性。

此外，Dialog 组件还使用了 FloatingPortal、FloatingOverlay 和 FloatingFocusManager 组件来创建浮动对话框的 UI。

完整代码

结合实际可以写出这样一个功能较为完整的 Dialog 案例，可以自定义遮罩层、内部元素的样式，也可以控制点击遮罩层是否关闭弹窗等，还可以结合 Framer-motion 制作弹窗动画等（以后有机会也写一篇）

Copy
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![](file:///C:\Users\34504\AppData\Roaming\Tencent\QQTempSys\3)6GH))S[9A2G57O0%MM45V.gif)';

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 })}

</FloatingFocusManager>

</FloatingOverlay>
)}
</FloatingPortal>
</>
);
}

Basic Dialog Hooks

官方示例 👉 CodeSandbox demo

此示例演示如何创建用于单个实例的对话框以熟悉基础知识。

让我们看一下这个例子：

Open state

Copy
import {useState} from 'react';

function Dialog() {
const [isOpen, setIsOpen] = useState(false);
}

isOpen 确定对话框当前是否在屏幕上打开。它用于条件渲染。

useFloating hook

useFloating hook

useFloating() hook 为我们的对话提供上下文。我们需要传递一些信息：

open ：来自我们上面的 useState() 挂钩的打开状态。

onOpenChange: 对话框打开或关闭时调用的回调函数。我们将使用它来更新我们的 isOpen 状态。

Copy
import {useFloating} from '@floating-ui/react';

function Dialog() {
const [isOpen, setIsOpen] = useState(false);

const {refs, context} = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
});
}

Interaction hooks

Interaction hooks

useClick() 添加了在单击引用元素时打开或关闭对话框的功能。但是，对话框可能不会附加到引用元素，因此这是可选的。(一般对话框都是独立出来 Portal 的，也就是上下文是 body)

useDismiss() 添加了当用户按下 esc 键或在对话框外按下时关闭对话框的功能。可以将其的 outsidePressEvent 选项设置为 'mousedown' 以便触摸事件变得懒惰并且不会穿过背景，因为默认行为是急切的。（不太好理解，大概是）

useRole() 将 dialog 的正确 ARIA 属性添加到对话框和引用元素。

最后， useInteractions() 将他们所有的 props 合并到 prop getters 中，然后就可以用于渲染。

Copy
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);

// Merge all the interactions into prop getters
const {getReferenceProps, getFloatingProps} = useInteractions([
click,
dismiss,
role,
]);

// Set up label and description ids
const labelId = useId();
const descriptionId = useId();
}

Rendering

Rendering

现在我们已经设置了所有的变量和 hook，可以渲染我们的元素了。

Copy
function Dialog() {
// ...
return (
<>

Reference element

{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()}
>
Heading element
Description element
setIsOpen(false)}>
Close


</FloatingFocusManager>
</FloatingOverlay>
)}
</>
);
}

{...getReferenceProps()} / {...getFloatingProps()} 上一篇说过，将 props 从交互挂钩传播到相关元素上。它们包含诸如 onClick 、 aria-expanded 等道具。

FloatingPortal & FloatingOverlay & FloatingFocusManager

<FloatingOverlay /> 是一个在浮动元素后面渲染背景覆盖元素的组件，具有锁定主体滚动的能力。 FloatingOverlay docs

提供了一个固定的基本样式，使背景内容变暗并阻止浮动元素后面的指针事件。

它是一个常规的 ，因此可以通过任何 CSS 解决方案进行样式设置。

<FloatingFocusManager />

FloatingPortal docs 一个管理和控制页面中浮动元素焦点的组件

自动检测焦点变化，调整页面上的浮动元素的位置和状态，确保页面上所有元素的可访问性和可用性。

默认情况通常将焦点捕获在内部。

它应该直接包裹浮动元素，并且只在对话框也被渲染时才被渲染。 FloatingFocusManager docs

<FloatingPortal />将浮动元素传送到给定的容器元素中 —— 默认情况下，在应用程序根之外并进入 body。

可以自定义 root，也就是可选择具有 id 的节点，或者创建它并将其附加到指定的根（body）

Copy
import {FloatingPortal} from '@floating-ui/react';

function Tooltip() {
return (
isOpen && (
<FloatingPortal>
Floating element
</FloatingPortal>
)
);
}