# 模态框中的选择器

> 把 React 颜色选择器放进对话框：为什么内联面板更稳妥，以及弹出层通过 portal 移出模态框之后会发生什么。

Source: https://chroma-panel.jscrate.dev/zh/react/recipes/in-a-modal
Last updated: 2026-09-21

在模态框、底部面板或抽屉里使用 React 颜色选择器时，请用 `ChromaPanel`。内联面板始终留在对话框内部，焦点、Escape 和外部点击都由同一个组件负责。

```tsx
"use client";

import { ChromaPanel } from "chroma-panel";
import "chroma-panel/style.css";
import type { CSSProperties } from "react";
import { useRef, useState } from "react";

export function ColorDialog() {
  const dialogRef = useRef<HTMLDialogElement>(null);
  const [color, setColor] = useState("#3366cc");

  return (
    <>
      <button type="button" onClick={() => dialogRef.current?.showModal()}>
        Edit color
      </button>

      <dialog ref={dialogRef} aria-label="Edit color">
        <form method="dialog">
          <ChromaPanel
            defaultValue={color}
            showTitleBar={false}
            onChangeComplete={(next) => setColor(next.hex)}
            style={{ "--cp-width": "100%" } as CSSProperties}
          />
          <button type="submit">Done</button>
        </form>
      </dialog>
    </>
  );
}
```

## 为什么内联面板适合模态框

`ChromaPanel` 没有触发器，也没有弹出层。它像其他普通区块一样留在对话框的布局中，遮罩和焦点陷阱仍由你的对话框控制。

如果对话框已经有了自己的标题栏和内边距，下面两个 prop 能让面板放得更妥帖。`showTitleBar={false}` 会去掉面板自带的标题行和窗口按钮。把 `--cp-width` 设为 `100%`，面板就会撑满对话框，而不是保持默认的 320px。这两项以及面板其他控制尺寸的变量，都在[主题](https://chroma-panel.jscrate.dev/zh/react/handbook/theming)页面中。

> **模态对话框位于顶层**
>
> 用 `showModal()` 打开的对话框绘制在浏览器的顶层（top layer）中。通过 portal
> 渲染到 `document.body` 的内容则不在顶层，所以无论 `z-index`
> 设得多高，弹出层都可能被挡在对话框后面。在这类对话框里，请使用内联面板。

## 弹出层在对话框中的表现

如果你确实想在非模态对话框中使用 `ColorInput`，下面是它会带来的行为。

弹出层通过 `createPortal` 渲染到 `document.body` 中，所以它是对话框的兄弟节点，而不是后代节点。正因为如此，带 `overflow: hidden` 或 `overflow: auto` 的对话框不会把面板裁切掉，打开选择器时，较矮的对话框也不会出现滚动条。弹出层自身的 `z-index` 设得很高，普通的层叠上下文不会让它落到对话框后面。

也正是因为 portal，有两种行为需要你结合自己的对话框测试一下：

- **外部点击。** 如果对话框在指针按在其元素之外时关闭，它可能会把在选择器内的点击也当成外部点击，因为选择器并不在对话框里面。打开选择器，点一下色轮：如果对话框在你眼前关掉了，就是这个原因。
- **焦点。** 如果对话框通过检查 DOM 包含关系来锁定焦点，那么弹出层刚把焦点移进来，对话框就可能把它拉回去，原因相同。

这些行为取决于对话框自身的规则。如果其中任何一项出了问题，就配置对话框，让它把选择器弹出层当作自己内容的一部分。

## Escape 和焦点的具体细节

了解选择器弹出层自己会做什么，有助于你判断到底是谁的处理函数被触发了。

Escape 在 `document` 上的捕获阶段处理。弹出层会阻止事件继续传播，然后关闭自己，并把焦点还给触发器。因此，在传播路径中更靠后监听 Escape 的对话框收不到第一次按键，第二次按键才会正常传到它那里。如果对话框也在 `document` 的捕获阶段监听，就需要专门测试这种情况。

弹出层打开时，焦点会移到其中第一个可聚焦的控件上，Tab 只在弹出层内循环。关闭时如果焦点还在弹出层内，焦点会回到打开之前拥有焦点的元素。

这些都不是对话框特有的，弹出层一直都是这样工作的，完整说明见 [Popover 参考文档](https://chroma-panel.jscrate.dev/zh/react/components/popover)。上面示例中的内联面板完全没有这些行为，这正是用它的意义所在。它的 prop 见 [ChromaPanel 参考文档](https://chroma-panel.jscrate.dev/zh/react/components/chroma-panel)。
