需求
为减少第三方库的非必要引用,选择使用原生封装一个popup组件。
思路
dom元素包括遮罩层和内容区两部分,使用fixed定位确定弹出位置,使用css动画对遮罩层(背景透明度)和内容区(滑动)做动画,通过插槽方式传入内容区显示内容。
效果
使用方法
import Popup from '@m/components/popup' //引入组件
import { useEffect, useState } from 'react'
import $style from './index.scss'
const Home = () => {
type Placement = 'top' | 'bottom' | 'left' | 'right' | 'center'
const [visible, setVisible] = useState(false)
const [placement, setPlacement] = useState<Placement>('top')
const open = (pos: Placement) => {
setVisible(true)
setPlacement(pos)
}
return (
<div className={$style.test_btn}>
<button onClick={() => open('top')}>顶部弹出</button>
<button onClick={() => open('bottom')}>底部弹出</button>
<button onClick={() => open('left')}>左侧弹出</button>
<button onClick={() => open('right')}>右侧弹出</button>
<button onClick={() => open('center')}>中心弹出</button>
//使用组件,placement弹出方位、visible控制打开关闭、closePopup关闭函数、popupClass自定义pupop组件样式、clickMaskClose是否点击遮罩关闭
<Popup
placement={placement}
visible={visible}
closePopup={setVisible}
popupClass={$style.popup_class}
clickMaskClose={true}
>
<div>测试popup</div>
</Popup>
</div>
)
}
export default Home
API
props
slot
代码
import { FC, useEffect, useState } from 'react'
import $style from './index.scss'
import { ISpopup } from './type'
const Popup: FC<ISpopup> = ({
children,
placement = 'top',
visible,
closePopup,
popupClass,
clickMaskClose = true
}) => {
const [isShowPop, setIsShowPop] = useState(false)
useEffect(() => {
let timer: number
if (visible) {
setIsShowPop(true)
} else {
timer = setTimeout(() => {
setIsShowPop(false)
}, 500)
}
return () => clearTimeout(timer)
}, [visible])
// 匹配打开方位 对应动画样式
const pitchPosClass = () => {
const posMap = {
top: [
$style.popup_content_top,
visible ? $style.popup_content_top_show : $style.popup_content_top_hidden
],
bottom: [
$style.popup_content_bottom,
visible ? $style.popup_content_bottom_show : $style.popup_content_bottom_hidden
],
left: [
$style.popup_content_left,
visible ? $style.popup_content_left_show : $style.popup_content_left_hidden
],
right: [
$style.popup_content_right,
visible ? $style.popup_content_right_show : $style.popup_content_right_hidden
],
center: [
$style.popup_content_center,
visible ? $style.popup_content_center_show : $style.popup_content_center_hidden
]
}
return posMap[placement]
}
const close = () => {
clickMaskClose && closePopup(false)
}
return (
isShowPop && (
<div className={$style.popup}>
<div
className={[
$style.popup_mask,
visible ? $style.popup_mask_show : $style.popup_mask_hidden
]}
onClick={close}
></div>
<div className={[popupClass, pitchPosClass()]}>{children}</div>
</div>
)
)
}
export default Popup
.popup {
// 遮罩
.popup_mask {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
}
.popup_mask_show {
background-color: rgba(0, 0, 0, 0.4);
animation: maskshow 0.5s;
}
.popup_mask_hidden {
background-color: transparent;
animation: maskhidden 0.5s;
}
// 顶部
.popup_content_top {
position: fixed !important;
background-color: #fff;
top: 0 !important;
left: 0 !important;
width: 100vw;
height: 20vh;
border-radius: 0 0 6px 6px;
}
.popup_content_top_show {
transform: none;
animation: topshow 0.5s;
}
.popup_content_top_hidden {
transform: translate(0, -100%);
animation: tophidden 0.5s;
}
// 底部
.popup_content_bottom {
position: fixed !important;
background-color: #fff;
bottom: 0 !important;
left: 0 !important;
width: 100vw;
height: 20vh;
border-radius: 6px 6px 0 0;
}
.popup_content_bottom_show {
transform: none;
animation: bottomshow 0.5s;
}
.popup_content_bottom_hidden {
transform: translate(0, 100%);
animation: bottomhidden 0.5s;
}
// 左侧
.popup_content_left {
position: fixed !important;
background-color: #fff;
top: 0 !important;
left: 0 !important;
width: 60vw;
height: 100vh;
border-radius: 0 6px 6px 0;
}
.popup_content_left_show {
transform: none;
animation: leftshow 0.5s;
}
.popup_content_left_hidden {
transform: translate(-100%, 0);
animation: lefthidden 0.5s;
}
// 右侧
.popup_content_right {
position: fixed !important;
background-color: #fff;
top: 0 !important;
right: 0 !important;
width: 60vw;
height: 100vh;
border-radius: 6px 0 0 6px;
}
.popup_content_right_show {
transform: none;
animation: rightshow 0.5s;
}
.popup_content_right_hidden {
transform: translate(100%, 0);
animation: righthidden 0.5s;
}
// 中心
.popup_content_center {
position: fixed !important;
background-color: #fff;
top: 50% !important;
left: 50% !important;
width: 30vw;
height: 20vh;
border-radius: 6px;
}
.popup_content_center_show {
transform: translate(-50%, -50%) scale(1);
animation: centershow 0.5s;
}
.popup_content_center_hidden {
transform: translate(-50%, -50%) scale(0);
animation: centerhidden 0.5s;
}
}
// 动画
// 遮罩动画
@keyframes maskshow {
0% {
opacity: 0;
background-color: transparent;
}
100% {
opacity: 1;
background-color: rgba(0, 0, 0, 0.4);
}
}
@keyframes maskhidden {
0% {
opacity: 1;
background-color: rgba(0, 0, 0, 0.4);
}
100% {
opacity: 0;
background-color: transparent;
}
}
// 顶部popup动画
@keyframes topshow {
0% {
transform: translate(0, -100%);
}
100% {
transform: none;
}
}
@keyframes tophidden {
0% {
transform: none;
}
100% {
transform: translate(0, -100%);
}
}
// 底部popup动画
@keyframes bottomshow {
0% {
transform: translate(0, 100%);
}
100% {
transform: none;
}
}
@keyframes bottomhidden {
0% {
transform: none;
}
100% {
transform: translate(0, 100%);
}
}
// 左侧popup动画
@keyframes leftshow {
0% {
transform: translate(-100%, 0);
}
100% {
transform: none;
}
}
@keyframes lefthidden {
0% {
transform: none;
}
100% {
transform: translate(-100%, 0);
}
}
// 右侧popup动画
@keyframes rightshow {
0% {
transform: translate(100%, 0);
}
100% {
transform: none;
}
}
@keyframes righthidden {
0% {
transform: none;
}
100% {
transform: translate(100%, 0);
}
}
// 中心popup动画
@keyframes centershow {
0% {
transform: translate(-50%, -50%) scale(0);
}
100% {
transform: translate(-50%, -50%) scale(1);
}
}
@keyframes centerhidden {
0% {
transform: translate(-50%, -50%) scale(1);
}
100% {
transform: translate(-50%, -50%) scale(0);
}
}