You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何复用Material UI中的Popper触发按钮组件

如何复用Material UI中的Popper触发按钮组件

嘿,你已经把Popper的动态内容搞定了,现在想把触发按钮也做成可复用的,这样不管是筛选、搜索还是其他场景都能直接用对吧?这事儿很简单,咱们只需要把触发按钮里那些可变的部分(比如按钮里的文字、图标、样式)都改成通过props从父组件传入,同时保留原有的Popper状态逻辑就行,不用重复写那些显示隐藏的代码。

我给你改好了可复用的组件,你看看:

import * as React from "react";
import Popper from "@mui/material/Popper";

// 现在这个组件支持自定义触发按钮的所有可变内容啦
export default function ReusablePopper({
  triggerContent, // 触发按钮里的内容,比如图标+文字,随便你传什么React节点都行
  triggerProps = {}, // 按钮的自定义属性,比如额外的className、disabled状态这些,用户传的会覆盖默认值
  children, // 原来的Popper弹出内容,和之前一样用children传
}) {
  const [anchorEl, setAnchorEl] = React.useState(null);

  const handleClick = (event) => {
    setAnchorEl(anchorEl ? null : event.currentTarget);
  };

  const open = Boolean(anchorEl);
  const id = open ? "simple-popper" : undefined;

  return (
    <div>
      <button
        // 合并默认属性和用户传入的props,用户传的会覆盖默认的
        {...triggerProps}
        // 保留必要的aria属性,确保 accessibility
        aria-describedby={triggerProps["aria-describedby"] ?? id}
        // 点击事件还是用组件内部的逻辑,不用用户自己写
        onClick={handleClick}
        // 默认样式保留,同时允许用户加自定义的className
        className={`flex items-center gap-x-2 rounded-lg lg:rounded-xl border border-[#d2d1d3] lg:py-2 lg:pl-3 lg:pr-8 px-3 py-2 ${triggerProps.className || ""}`}
        // 默认type设为button,避免表单里的默认提交行为
        type={triggerProps.type ?? "button"}
      >
        {/* 这里渲染用户传进来的按钮内容,比如图标+文字 */}
        {triggerContent}
      </button>
      <Popper id={id} open={open} anchorEl={anchorEl} placement="bottom-start">
        {children}
      </Popper>
    </div>
  );
}

怎么用这个组件?举两个场景例子你就懂了

1. 搜索场景的用法

你可以给触发按钮传搜索图标+“搜索”文字,弹出内容放搜索框:

<ReusablePopper
  triggerContent={
    <>
      {/* 这里可以放你自己的搜索图标,或者用MUI的Icon组件 */}
      <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
      </svg>
      <span>搜索</span>
    </>
  }
  // 可以加自定义的className,比如给搜索按钮加个特殊的样式
  triggerProps={{ className: "bg-gray-50" }}
>
  {/* 这里是Popper弹出的搜索内容 */}
  <div className="p-3 bg-white shadow-lg rounded-lg w-72">
    <input type="text" placeholder="输入关键词搜索" className="border w-full p-2 rounded" />
    <div className="mt-2 text-sm text-gray-500">最近搜索:React, MUI Popper</div>
  </div>
</ReusablePopper>

2. 筛选场景的用法

换个图标和文字,就能直接当筛选按钮用:

<ReusablePopper
  triggerContent={
    <>
      <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
      </svg>
      <span>筛选</span>
    </>
  }
  triggerProps={{ className: "bg-blue-50 text-blue-700 border-blue-200" }}
>
  {/* 筛选的弹出内容,比如各种筛选选项 */}
  <div className="p-3 bg-white shadow-lg rounded-lg w-64">
    <div className="mb-2">
      <label className="block text-sm font-medium">任务状态</label>
      <select className="w-full border p-2 rounded mt-1">
        <option>全部</option>
        <option>已完成</option>
        <option>进行中</option>
      </select>
    </div>
    <button className="w-full bg-blue-600 text-white p-2 rounded">应用筛选</button>
  </div>
</ReusablePopper>

要是你想要更灵活的控制?

如果有时候你不想用button当触发元素(比如想用div或者MUI的IconButton),那咱们可以再改得更灵活一点,让父组件直接渲染触发元素,组件只负责Popper的状态管理:

import * as React from "react";
import Popper from "@mui/material/Popper";

export default function SuperReusablePopper({
  renderTrigger, // 父组件自己写触发元素的渲染逻辑,组件会把必要的状态传过去
  children,
}) {
  const [anchorEl, setAnchorEl] = React.useState(null);

  const handleClick = (event) => {
    setAnchorEl(anchorEl ? null : event.currentTarget);
  };

  const open = Boolean(anchorEl);
  const id = open ? "simple-popper" : undefined;

  return (
    <div>
      {/* 把点击事件、打开状态、id都传给父组件,让父组件自己控制触发元素 */}
      {renderTrigger({ handleClick, open, id })}
      <Popper id={id} open={open} anchorEl={anchorEl} placement="bottom-start">
        {children}
      </Popper>
    </div>
  );
}

用的时候就可以完全自定义触发元素了,比如:

<SuperReusablePopper
  renderTrigger={({ handleClick, open, id }) => (
    <div
      aria-describedby={id}
      onClick={handleClick}
      className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer ${open ? "bg-gray-100" : ""}`}
    >
      <span>用户中心</span>
      {/* 打开的时候图标旋转,这个逻辑也可以自己控制 */}
      <svg className={`w-4 h-4 transition-transform ${open ? "rotate-180" : ""}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
      </svg>
    </div>
  )}
>
  <div className="p-3 bg-white shadow-lg rounded-lg">
    <div className="p-2 hover:bg-gray-100 cursor-pointer">我的资料</div>
    <div className="p-2 hover:bg-gray-100 cursor-pointer">账户设置</div>
    <div className="p-2 hover:bg-gray-100 cursor-pointer">退出登录</div>
  </div>
</SuperReusablePopper>

核心思路就是把可变的部分交给父组件自定义,把不变的Popper状态逻辑留在复用组件里,这样不管什么场景,你都只需要写一次Popper的显示隐藏逻辑,触发按钮想怎么改就怎么改,完全不用动核心代码。

备注:内容来源于stack exchange,提问作者Sina Sakhaei

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.16 12:13:05