前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >实现 antd 的 Popover 组件,可以很简单

实现 antd 的 Popover 组件,可以很简单

作者头像
神说要有光zxg
发布2024-04-10 19:11:17
1350
发布2024-04-10 19:11:17
举报

组件库一般都有 Popover 和 Tooltip 这两个组件,它们非常相似。

不过应用场景是有区别的:

Tooltip(文字提示) 是用来代替 title 的,做一个文案解释。

而 Popover(气泡卡片)可以放更多的内容,可以交互:

所以说,这俩虽然长得差不多,但确实要分为两个组件来写。

这个组件看起来比较简单,但实现起来很麻烦。

你可能会说,不就是写好样式,然后绝对定位到元素上面么?

不只是这样。

首先,placement 参数可以指定 12 个方向,top、topleft、topright、bottom 等:

这些不同方向的位置计算都要实现。

而且,就算你指定了 left,当左边空间不够的时候,也得做下处理,展示在右边:

而且当方向不同时,箭头的显示位置也不一样:

所以要实现这样一个 Popover 组件,光计算浮层的显示位置就是不小的工作量。

不过好在这种场景有专门的库做了封装,完全没必要自己写。

它就是 floating-ui。

看介绍就可以知道,它是专门用来创建 tooltip、popover、dropdown 这类浮动的元素的。

它的 logo 也很形象:

那它怎么用呢?

我们新建个项目试试看:

代码语言:javascript
复制
npx create-vite

用 create-vite 创建个 react 项目。

进入项目,安装依赖,然后把服务跑起来:

代码语言:javascript
复制
npm install
npm run dev

没啥问题。

改下 main.tsx,去掉 index.css,并且把 StrictMode 去掉,它会导致重复渲染:

然后安装下 floating-ui 的包:

代码语言:javascript
复制
npm install --save @floating-ui/react

改下 App.tsx

代码语言:javascript
复制
import {
  useInteractions,
  useFloating,
  useHover,
} from '@floating-ui/react';
import { useState } from 'react';
 
export default function App() {
  const [isOpen, setIsOpen] = useState(false);
 
  const {refs, floatingStyles, context} = useFloating({
    open: isOpen,
    onOpenChange: setIsOpen
  });
 
  const hover = useHover(context);

  const { getReferenceProps, getFloatingProps } = useInteractions([
    hover
  ]);

  return (
    <>
      <button ref={refs.setReference} {...getReferenceProps()}>
        hello
      </button>
      {
        isOpen && <div
            ref={refs.setFloating}
            style={floatingStyles}
            {...getFloatingProps()}
          >
            光光光光光
          </div>
      }
    </>
  );
}

先看看效果:

可以看到,hover 的时候浮层会在下面出现。

看下代码:

首先,用到了 useFloating 这个 hook,它的作用就是计算浮层位置的。

给它相对的元素、浮层元素的 ref,它就会计算出浮层的 style 来。

它可以指定浮层出现的方向:

比如当 placement 指定为 right 时,效果就是这样的:

再就是 useInteractions 这个 hook:

你可以传入 click、hover 等交互方式,然后把它返回的 props 设置到元素上,就可以绑定对应的交互事件。

比如把交互事件换成 click:

现在就是点击的时候浮层出现和消失了:

不过现在有个问题:

只有点击按钮,浮层才会消失,点击其他位置不会。

这时候可以加上 dismiss 的处理:

现在点击其它位置,浮层就会消失,并且按 ESC 键也会消失:

也就是说 useFloating 是用来给浮层确定位置的,useInteractions 是用来绑定交互事件的

有的同学会说,这也不好看啊。

我们加一下样式就好了:

加上 className,然后在 App.css 里写下样式:

代码语言:javascript
复制
.popover-floating {
  padding: 4px 8px;
  border: 1px solid #000;
  border-radius: 4px;
}

引入看下:

但是现在的定位有点问题,离着太近了,能不能修改下定位呢?

可以。

加一个 offset 的 middleware 就好了:

它的效果就是修改两者距离的:

箭头也不用自己写,有对应的中间件:

代码语言:javascript
复制
import {
  useInteractions,
  useFloating,
  useHover,
  useClick,
  useDismiss,
  offset,
  arrow,
  FloatingArrow,
} from '@floating-ui/react';
import { useRef, useState } from 'react';

import './App.css';
 
export default function App() {
  const arrowRef = useRef(null);

  const [isOpen, setIsOpen] = useState(false);
 
  const {refs, floatingStyles, context} = useFloating({
    open: isOpen,
    onOpenChange: setIsOpen,
    placement: 'right',
    middleware: [
      offset(10),
      arrow({
        element: arrowRef,
      }),
    ]
  });
 
  const click = useClick(context);
  const dismiss = useDismiss(context);

  const { getReferenceProps, getFloatingProps } = useInteractions([
    click,
    dismiss
  ]);

  return (
    <>
      <button ref={refs.setReference} {...getReferenceProps()}>
        hello
      </button>
      {
        isOpen && <div
            className='popover-floating'
            ref={refs.setFloating}
            style={floatingStyles}
            {...getFloatingProps()}
          >
            光光光
            <FloatingArrow ref={arrowRef} context={context}/>
          </div>
      }
    </>
  );
}

这样箭头就有了。

只不过样式不大对,我们修改下:

代码语言:javascript
复制
<FloatingArrow ref={arrowRef} context={context} fill="#fff" stroke="#000" strokeWidth={1}/>

这样,箭头位置就有了。

给 button 加一些 margin,我们试试其它位置的 popover 对不对:

分别设置不同 placement:

top-end

left-start

left

都没问题。

不过现在并没有做边界的处理:

设置 top 的时候,浮层超出可视区域,这时候应该显示在下面。

加上 flip 中间件就好了:

这样,popover 的功能就完成了。

我们封装下 Popover 组件。

新建 Popover/index.tsx

代码语言:javascript
复制
import { CSSProperties, PropsWithChildren, ReactNode } from "react";
import {
    useInteractions,
    useFloating,
    useClick,
    useDismiss,
    offset,
    arrow,
    FloatingArrow,
    flip,
    useHover,
} from '@floating-ui/react';
import { useRef, useState } from 'react';
import './index.css';
  
type Alignment = 'start' | 'end';
type Side = 'top' | 'right' | 'bottom' | 'left';
type AlignedPlacement = `${Side}-${Alignment}`;

interface PopoverProps extends PropsWithChildren {
    content: ReactNode,
    trigger?: 'hover' | 'click'
    placement?: Side | AlignedPlacement,
    open?: boolean,
    onOpenChange?: (open: boolean) => void,
    className?: string;
    style?: CSSProperties
}

export default function Popover(props: PopoverProps) {

    const {
        open,
        onOpenChange,
        content,
        children,
        trigger = 'hover',
        placement = 'bottom',
        className,
        style
    } = props;

    const arrowRef = useRef(null);

    const [isOpen, setIsOpen] = useState(open);
     
    const {refs, floatingStyles, context} = useFloating({
      open: isOpen,
      onOpenChange: (open) => {
        setIsOpen(open);
        onOpenChange?.(open);
      },
      placement,
      middleware: [
        offset(10),
        arrow({
          element: arrowRef,
        }),
        flip()
      ]
    });
   
    const interaction = trigger === 'hover' ? useHover(context) : useClick(context);

    const dismiss = useDismiss(context);
  
    const { getReferenceProps, getFloatingProps } = useInteractions([
        interaction,
        dismiss
    ]);
  
    return (
      <>
        <span ref={refs.setReference} {...getReferenceProps()} className={className} style={style}>
          {children}
        </span>
        {
          isOpen && <div
              className='popover-floating'
              ref={refs.setFloating}
              style={floatingStyles}
              {...getFloatingProps()}
            >
              {content}
              <FloatingArrow ref={arrowRef} context={context} fill="#fff" stroke="#000" strokeWidth={1}/>
            </div>
        }
      </>
    );
}

Popover/index.css

代码语言:javascript
复制
.popover-floating {
    padding: 4px 8px;
    border: 1px solid #000;
    border-radius: 4px;
}

整体代码和之前差不多,有几处不同:

参数继承 PropsWithChildren,可以传入 children 参数。

可以传入 content,也就是浮层的内容。

trigger 参数是触发浮层的方式,可以是 click 或者 hover。

placement 就是 12 个方向。

而 open、onOpenChange 则是可以在组件外控制 popover 的显示隐藏。

className 和 style 设置到内层的 span 元素上:

在 App.tsx 里引入下:

代码语言:javascript
复制
import Popover from './Popover';

export default function App() {

  const popoverContent = <div>
    光光光
    <button onClick={() => {alert(1)}}>111</button>
  </div>;

  return <Popover
    content={popoverContent}
    placement='bottom'
    trigger='click'
    style={{margin: '200px'}}
  >
    <button>点我点我</button>
  </Popover>
}

这样,Popover 组件的基本功能就完成了。

但现在 Popover 组件还有个问题:

浮层使用 position:absolute 定位的,应该是相对于 body 定位,但如果中间有个元素也设置了 position: relative 或者 absolute,那样定位就是相对于那个元素了。

所以,要把浮层用 createPortal 渲染到 body 之下。

代码语言:javascript
复制
const el = useMemo(() => {
    const el = document.createElement('div');
    el.className = `wrapper`;

    document.body.appendChild(el);
    return el;
}, []);

const floating = isOpen && <div
    className='popover-floating'
    ref={refs.setFloating}
    style={floatingStyles}
    {...getFloatingProps()}
>
    {content}
    <FloatingArrow ref={arrowRef} context={context} fill="#fff" stroke="#000" strokeWidth={1}/>
</div>

return (
  <>
    <span ref={refs.setReference} {...getReferenceProps()} className={className} style={style}>
      {children}
    </span>
    {
      createPortal(floating, el)
    }
  </>
);

这样,Popover 浮层就渲染到了 body 下:

至此,Popover 组件就封装完了。

案例代码上传了 react 小册仓库:https://github.com/QuarkGluonPlasma/react-course-code/tree/main/popover-component

总结

今天我们封装了 Popover 组件。

如果完全自己实现,计算位置还是挺麻烦的,有 top、right、left 等不同位置,而且到达边界的时候也要做特殊处理。

所以我们直接基于 floating-ui 来做,它是专门用于 tooltip、popover、dropdown 等浮动组件的。

用 useFloating 的 hook 来计算位置,用 useIntersections 的 hook 来处理交互。

它支持很多中间件,比如 offset 来设置偏移、arrow 来处理箭头位置,可以完成各种复杂的定位功能。

我们封装了一层,加了一些参数,然后把浮层用 createPortal 渲染到了 body 下。

这样就是一个功能完整的 Popover 组件了。

如果完全自己实现 Popover 组件,还是挺麻烦的,但是基于 floating-ui 封装,就很简单。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-04-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 神光的编程秘籍 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 总结
相关产品与服务
消息队列 TDMQ
消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档