# 自定义触发器

> 搭配使用 Popover 和 ChromaPanel，从自己的按钮打开 React 颜色选择器，并补上 ColorInput 原本会替你设置的 ARIA 属性。

Source: https://chroma-panel.jscrate.dev/zh/react/recipes/custom-trigger
Last updated: 2026-09-21

`ColorInput` 自带一个方形的色块按钮。如果你需要的是工具栏图标、设置项或文字按钮，就把 `Popover` 和 `ChromaPanel` 组合起来使用。这样可以做出自定义的 React 颜色选择器弹出层，而不必嵌套两层浮层。

```tsx
"use client";

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

export function BrandColorButton() {
  const [anchor, setAnchor] = useState<HTMLButtonElement | null>(null);
  const [open, setOpen] = useState(false);
  const [color, setColor] = useState("#3366cc");

  return (
    <>
      <button
        ref={setAnchor}
        type="button"
        aria-haspopup="dialog"
        aria-expanded={open}
        onClick={() => setOpen((current) => !current)}
      >
        <span
          aria-hidden="true"
          style={{
            display: "inline-block",
            width: 14,
            height: 14,
            borderRadius: 4,
            background: color,
          }}
        />
        Brand color
      </button>

      <Popover anchor={anchor} open={open} onClose={() => setOpen(false)}>
        <div role="dialog" aria-label="Brand color" aria-modal="false">
          <ChromaPanel
            value={color}
            onChange={(next) => setColor(next.hex)}
            title="Brand color"
            onClose={() => {
              setOpen(false);
              anchor?.focus();
            }}
          />
        </div>
      </Popover>
    </>
  );
}
```

## 用 state 而不是 ref 保存锚点

`anchor` 是弹出层定位时参照的元素。按钮渲染出来之前，它的值是 `null`。把它存在 state 里，元素可用时 `Popover` 才会重新渲染。

## 需要补上的属性

设置 `aria-haspopup="dialog"`，并让 `aria-expanded` 与 `open` 保持同步。给触发器一个可访问名称。弹出层本身需要 `role="dialog"` 和自己的标签，写法见上面的示例。

给面板传入 `onClose` 后，点击它的红色窗口按钮就会关闭弹出层，而不是只变暗留在那里，示例中的处理函数做的就是这件事。关闭后把焦点还给触发器，则需要你自己处理。面板本身负责哪些事情，请参阅[无障碍功能](https://chroma-panel.jscrate.dev/zh/react/overview/accessibility)。

## Popover 已经做好的事

你不需要自己处理点击外部或按 Escape 关闭，弹出层自带这些行为。

- 它通过 portal 渲染到 `document.body` 中，所以任何带 `overflow: hidden` 的祖先元素都裁切不到它。
- 下方空间足够时，它显示在锚点下方，否则显示在上方；同时与视口边缘保持 8px 的距离。与锚点的间距由 `offset` 控制，默认 8px。
- 页面滚动或窗口大小变化时，它会重新定位，并把可用空间告诉面板，面板据此限制自己的高度。
- 按 Escape 会关闭它，并把焦点移回锚点。
- 在弹出层和锚点之外按下指针，也会关闭它。
- 打开时，焦点移到其中第一个可聚焦的控件上。Tab 和 Shift+Tab 只在弹出层内循环，不会跑到后面的页面上去。

## 宽度小于 640px 时

宽度小于 640px 时，弹出层默认变成底部面板（bottom sheet），同时加上遮罩并锁定页面滚动。设置 `sheetOnMobile={false}`，就能在小屏幕上继续使用锚定在按钮旁的颜色选择器下拉层。

```tsx
<Popover
  anchor={anchor}
  open={open}
  onClose={() => setOpen(false)}
  sheetOnMobile={false}
>
  <div role="dialog" aria-label="Brand color">
    <ChromaPanel value={color} onChange={(next) => setColor(next.hex)} />
  </div>
</Popover>
```

> **sheetOnMobile 是 Popover 的 prop**
>
> 只有 `Popover` 有这个 prop。`ColorInput` 既不接收它，也不会传递它，所以
> `ColorInput` 在宽度小于 640px
> 时总是变成底部面板。想避开这一行为，就得自己实现触发器。完整的 prop 列表见
> [Popover 参考文档](https://chroma-panel.jscrate.dev/zh/react/components/popover)，被你替换掉的那个触发器见
> [ColorInput 参考文档](https://chroma-panel.jscrate.dev/zh/react/components/color-input)。
