近期模仿Ant Design,实现了一个气泡确认框
先来看效果图
-
想要这样使用组件
<PopConfirm
title="操作1"
description="描述1"
onConfirm={handleConfirm}
onCancel={handleCancel}
>
<Button style={{ position: "fixed", top: 0, left: 0 }} type="primary">
左上角按钮
</Button>
</PopConfirm>
-
首先,我定义了一个变量
visible
,来动态的控制popConfirm
的显示与隐藏。并接受一个children
,来放置原有的按钮组件
interface PopConfirmProps {
children: ReactElement;
}
function PopConfirm({ children }: PopConfirmProps){
const [visible, setVisible] = useState<boolean>(false);
return (
<div>
<Layout show={visible}>popConfirm</Layout>
{children}
</div>
)
}
export default PopConfirm;
-
如何才能在点击按钮时,先弹出
popConfirm
呢? -
这里我使用了
cloneElement
,来为传入的children
注入onClick属性
。然后将Layout
设为固定定位fixed
,这样popConfirm
就不会影响页面的布局了。
interface PopConfirmProps {
children: ReactElement;
}
function PopConfirm({ children }: PopConfirmProps){
const [visible, setVisible] = useState<boolean>(false);
return (
<div>
<Layout show={visible}>popConfirm</Layout>
{cloneElement(children, {
onClick() {
setVisible((visible) => !visible);
}
})}
</div>
)
}
export default PopConfirm;
-
这样就可以控制
popConfirm
是否显示了。 -
现在只需要将
popConfirm
放在所点击按钮的旁边就可以了。 -
获取按钮元素的位置信息,我使用了
getBoundingClientRect
这个方法。 -
但如若要获取按钮的位置,需要先获取到按钮元素,可以使用
cloneElement
的第二个参数,为按钮元素指定ref
属性
interface PopConfirmProps {
children: ReactElement;
}
function PopConfirm({ children }: PopConfirmProps){
const [visible, setVisible] = useState<boolean>(false);
const triggerRef = useRef<HTMLElement>(null);
const popRef = useRef<HTMLDivElement>(null);
return (
<div>
<Layout show={visible} ref={popRef}>popConfirm</Layout>
{cloneElement(children, {
onClick() {
setVisible((visible) => !visible);
},
ref: triggerRef,
})}
</div>
)
}
export default PopConfirm;
-
之后就是计算位置,然后动态地设置
popConfirm
的位置 -
定义了一个变量
position
来存储top
和left
值
const [visible, setVisible] = useState<boolean>(false);
const [position, setPosition] = useState({
top: 0,
left: 0,
});
const triggerRef = useRef<HTMLElement>(null);
const popRef = useRef<HTMLDivElement>(null);
/* 计算气泡确认框的位置 */
useEffect(() => {
if (triggerRef.current && popRef.current) {
const triggerRect = triggerRef.current.getBoundingClientRect(); // 触发点击事件元素的位置信息
const popRect = popRef.current.getBoundingClientRect(); // 气泡确认框的位置信息
// 根据所点击元素的位置,决定其放置的位置(上面放不下,则放在下面,下面放不下,则放在上面......)
const newPosition = {
top:
triggerRect.top < popRect.height
? triggerRect.bottom // 放在上面
: triggerRect.top - popRect.height, // 放在下面
left:
triggerRect.left < popRect.width / 2
? triggerRect.left // 偏右
: triggerRect.left - popRect.width / 2, // 偏左
};
setPosition(newPosition);
}
}, [visible]);
-
为
Layout
组件动态的设置top
和left
值即可
<Layout show={visible} positon={position} ref={popRef}></Layout>
-
这里使用到了
Styled-components
来编写样式组件,只展示了一些关键点
export interface Position {
top: number;
left: number;
}
interface Props {
show: boolean; // 是否显示气泡确认框
positon: Position; // 位置信息
}
const excludeProps = ["positon", "show"];
const Layout = styled.div.withConfig({
shouldForwardProp: (prop) => !excludeProps.includes(prop),
})<Props>`
${({ positon: { left, top }, show }) => css`
position: fixed;
top: ${top}px;
left: ${left}px;
${!show &&
css`
display: none;
`}
`}
`;
export default Layout;
-
当点击页面中,除了
popConfirm
组件之外的地方时,需要隐藏popConfirm
。 -
我的做法是:当打开
popConfirm
时,为页面添加一个点击事件,隐藏时移除点击事件。
/* 为页面绑定点击事件,当点击气泡确认框之外的地方时,关闭气泡确认框
为什么要排除触发点击事件的元素?若不排除,当用户点击指定元素之后,此次点击会立即被刚刚所添加的全局click事件捕获
*/
useEffect(() => {
function closePopConfirm(e: MouseEvent) {
if (
triggerRef.current?.contains(e.target as Node) ||
popRef.current?.contains(e.target as Node)
) {
return;
}
setVisible(false);
}
if (visible) {
document.addEventListener("click", closePopConfirm);
}
return () => {
document.removeEventListener("click", closePopConfirm);
};
}, [visible]);
-
基本功能实现后,就可以添加个性化需求了,完整代码如下
import { cloneElement, ReactElement, useEffect, useRef, useState } from "react";
import Button from "../Button/Button";
import BtnLayout from "./components/BtnLayout";
import Description from "./components/Description";
import Layout from "./components/Layout";
import Title from "./components/Title";
interface PopConfirmProps {
children: ReactElement;
title: string; // 标题
description?: string; // 描述
okText?: string; // 确定按钮的文本
cancelText?: string; // 取消按钮的文本
onConfirm?: () => unknown; // 点击确认按钮后触发的回调
onCancel?: () => unknown; // 点击取消按钮后触发的回调
showCancel?: boolean; // 是否显示取消按钮
}
function PopConfirm({
children,
title,
description,
showCancel = true,
okText = "确认",
cancelText = "取消",
onConfirm,
onCancel,
}: PopConfirmProps) {
const [visible, setVisible] = useState<boolean>(false);
const [position, setPosition] = useState({
top: 0,
left: 0,
});
const triggerRef = useRef<HTMLElement>(null);
const popRef = useRef<HTMLDivElement>(null);
/* 为页面绑定点击事件,当点击气泡确认框之外的地方时,关闭气泡确认框
为什么要排除触发点击事件的元素?若不排除,当用户点击指定元素之后,此次点击会立即被刚刚所添加的全局click事件捕获
*/
useEffect(() => {
function closePopConfirm(e: MouseEvent) {
if (
triggerRef.current?.contains(e.target as Node) ||
popRef.current?.contains(e.target as Node)
) {
return;
}
setVisible(false);
}
if (visible) {
document.addEventListener("click", closePopConfirm);
}
return () => {
document.removeEventListener("click", closePopConfirm);
};
}, [visible]);
/* 计算气泡确认框的位置 */
useEffect(() => {
if (triggerRef.current && popRef.current) {
const triggerRect = triggerRef.current.getBoundingClientRect(); // 触发点击事件元素的位置信息
const popRect = popRef.current.getBoundingClientRect(); // 气泡确认框的位置信息
// 根据所点击元素的位置,决定其放置的位置(上面放不下,则放在下面,下面放不下,则放在上面......)
const newPosition = {
top:
triggerRect.top < popRect.height
? triggerRect.bottom // 放在上面
: triggerRect.top - popRect.height, // 放在下面
left:
triggerRect.left < popRect.width / 2
? triggerRect.left // 偏右
: triggerRect.left - popRect.width / 2, // 偏左
};
setPosition(newPosition);
}
}, [visible]);
function handleCancel() {
onCancel?.();
setVisible(false);
}
function handleConfirm() {
onConfirm?.();
setVisible(false);
}
return (
<div>
<Layout show={visible} positon={position} ref={popRef}>
<Title>{title}</Title>
{description && <Description>{description}</Description>}
<BtnLayout showCancel={showCancel}>
{showCancel && (
<Button size="small" onClick={handleCancel}>
{cancelText}
</Button>
)}
<Button size="small" type="primary" onClick={handleConfirm}>
{okText}
</Button>
</BtnLayout>
</Layout>
{cloneElement(children, {
onClick() {
setVisible((visible) => !visible);
},
ref: triggerRef,
})}
</div>
);
}
export default PopConfirm;
-
使用示例,效果图在前面
import Button from "@/ui/Button/Button";
import PopConfirm from "@/ui/PopConfirm/PopConfirm";
function Test() {
function handleCancel() {
console.log("点击了取消");
}
function handleConfirm() {
console.log("点击了确认");
}
return (
<div>
<PopConfirm
title="操作1"
description="描述1"
onConfirm={handleConfirm}
onCancel={handleCancel}
>
<Button style={{ position: "fixed", top: 0, left: 0 }} type="primary">
左上角按钮
</Button>
</PopConfirm>
<PopConfirm
title="操作2"
description="描述2"
onConfirm={handleConfirm}
onCancel={handleCancel}
>
<Button
style={{ position: "fixed", bottom: 0, left: 0 }}
type="primary"
>
左下角按钮
</Button>
</PopConfirm>
</div>
);
}
export default Test;
标签:PopConfirm,const,top,React,visible,点击,气泡,children,left
From: https://blog.csdn.net/qq_64157464/article/details/142349919