前言
在移动端我们经常会用到一些反馈类型的组件,比如Toast
,ActionSheet
,Dialog
, Modal
等组件。这些组件基本都是基于 Popup
组件实现的。
目前开源的React
的移动端组件库除了ant-mobile
其他的比较少,不像 Vue
的Vant
,Vux
,cube-ui
,mand-mobile
等。所以移动端基于React
技术栈的很多东西都需要自己去实现,接下来我们就自己去实现一个简单的Popup
组件。
效果
Portal
Portal
可以将组件渲染到组件树之外的地方,就像乾坤大挪移一样可以移花接木、移穴换位,一般是直接将组件渲染到 document.body
下面
// Portal.js
// eslint-disable-next-line no-unused-vars
import React, { memo } from 'react';
import { createPortal } from 'react-dom';
/**
* Portal
*/
const Portal = memo(props => {
const { children, getContainer = () => document.body } = props;
const container = typeof getContainer === 'function' ? getContainer() : getContainer;
if (container) {
return createPortal(children, container);
}
return children;
});
export default Portal;
Popup
我们先分析下 Popup
需要哪些东西。从展现形式上看就两个,一个遮罩,一个容器。所以 html
结构比较简单
<div className="popup">
<div className="popup__mask"></div>
<div className="popup__content">{children}</div>
</div>
- 遮罩主要就是淡入淡出的效果,点击遮罩隐藏容器
- 然后容器的位置主要有
5
种,top
,left
,right
,bottom
,center
,并有不同的动画效果,动画效果我们可以用react-transition-group
这个库来实现,和Vue
的内置动画api
类似 - 可以先把每个位置的样式写出来,动画后面在写,动画主要有四步
进入前
:xxx-enter
进入后
:xxx-enter-active
出去前
:xxx-exit
出去后
:xxx-exit-active
// Popup.js
import React, { memo, useEffect, useState } from 'react';
import { CSSTransition } from 'react-transition-group';
import clsx from 'classnames';
import Portal from './Portal';
import './style.less';
/**
* Popup
*/
const Popup = memo(props => {
const {
visible,
children,
position = 'bottom',
maskClassName,
contentClassName,
getContainer,
onMaskClick,
onEnter,
onExited,
} = props;
const defaultPosition = position || 'bottom';
const maskClass = clsx('popup__mask', maskClassName);
const contentClass = clsx(`popup__content popup__content--${defaultPosition}`, contentClassName);
const [curVisible, setCurVisible] = useState(visible);
useEffect(() => {
setCurVisible(visible);
}, [visible]);
return (
<Portal getContainer={getContainer}>
<CSSTransition
in={curVisible}
timeout={300}
classNames={defaultPosition}
unmountOnExit
onEnter={onEnter}
onExited={onExited}
>
<div className="popup">
<div className={maskClass} onClick={onMaskClick}></div>
<div className={contentClass}>{children}</div>
</div>
</CSSTransition>
</Portal>
);
});
export default Popup;
样式
- 遮罩样式比较简单,
rgba
实现半透明,动画主要是opcity
在0 - 1
之间过渡 - 内容样式先分别实现对应的位置,再分别实现对应的出入动画
// style.less
:global {
.popup {
&__mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
}
&__content {
background: #fff;
position: fixed;
&--top {
top: 0;
left: 0;
right: 0;
}
&--bottom {
bottom: 0;
left: 0;
right: 0;
}
&--left {
left: 0;
top: 0;
bottom: 0;
}
&--right {
right: 0;
top: 0;
bottom: 0;
}
&--center {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
}
.top,
.bottom,
.center,
.left,
.right {
&-enter {
.popup__mask {
opacity: 0;
}
&-active {
.popup__mask {
opacity: 1;
transition: 0.3s all ease;
}
.popup__content {
transition: 0.3s all ease;
}
}
}
&-exit {
.popup__mask {
opacity: 1;
}
&-active {
.popup__mask {
opacity: 0;
transition: 0.3s all ease;
}
.popup__content {
transition: 0.3s all ease;
}
}
}
}
.bottom {
&-enter {
.popup__content {
transform: translate(0, 100%);
}
&-active {
.popup__content {
transform: translate(0, 0);
}
}
}
&-exit {
.popup__content {
transform: translate(0, 0);
}
&-active {
.popup__content {
transform: translate(0, 100%);
}
}
}
}
.top {
&-enter {
.popup__content {
transform: translate(0, -100%);
}
&-active {
.popup__content {
transform: translate(0, 0);
}
}
}
&-exit {
.popup__content {
transform: translate(0, 0);
}
&-active {
.popup__content {
transform: translate(0, -100%);
}
}
}
}
.left {
&-enter {
.popup__content {
transform: translate(-100%, 0);
}
&-active {
.popup__content {
transform: translate(0, 0);
}
}
}
&-exit {
.popup__content {
transform: translate(0, 0);
}
&-active {
.popup__content {
transform: translate(-100%, 0);
}
}
}
}
.right {
&-enter {
.popup__content {
transform: translate(100%, 0);
}
&-active {
.popup__content {
transform: translate(0, 0);
}
}
}
&-exit {
.popup__content {
transform: translate(0, 0);
}
&-active {
.popup__content {
transform: translate(100%, 0);
}
}
}
}
.center {
&-enter {
.popup__content {
opacity: 0;
transform: translate(-50%, -50%) scale(0.7);
}
&-active {
.popup__content {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
}
&-exit {
.popup__content {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
&-active {
.popup__content {
opacity: 0;
transform: translate(-50%, -50%) scale(0.7);
}
}
}
}
}
测试
// test.js
import React, { memo, useState } from 'react';
import { Button, Popup } from 'components';
const style = {
color: '#fff',
height: '100vh',
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'center',
};
const centerStyle = {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '2em',
};
/**
* Test
*/
const Test = memo(props => {
const [bottomVisible, setBottomVisble] = useState(false);
const [topVisible, setTopVisible] = useState(false);
const [leftVisible, setLeftVisible] = useState(false);
const [rightVisible, setRightVisible] = useState(false);
const [centerVisible, setCenterVisible] = useState(false);
return (
<div style={style}>
<Button onClick={() => setBottomVisble(true)}>bottom</Button>
<Button onClick={() => setTopVisible(true)}>top</Button>
<Button onClick={() => setLeftVisible(true)}>left</Button>
<Button onClick={() => setRightVisible(true)}>right</Button>
<Button onClick={() => setCenterVisible(true)}>center</Button>
<Popup visible={bottomVisible} position="bottom" onMaskClick={() => setBottomVisble(false)}>
<div style={{ height: '20vh', ...centerStyle }}>popup-bottom</div>
</Popup>
<Popup visible={topVisible} position="top" onMaskClick={() => setTopVisible(false)}>
<div style={{ height: '20vh', ...centerStyle }}>popup-top</div>
</Popup>
<Popup visible={leftVisible} position="left" onMaskClick={() => setLeftVisible(false)}>
<div style={{ width: '50vw', height: '100%', ...centerStyle }}>popup-left</div>
</Popup>
<Popup visible={rightVisible} position="right" onMaskClick={() => setRightVisible(false)}>
<div style={{ width: '50vw', height: '100%', ...centerStyle }}>popup-right</div>
</Popup>
<Popup visible={centerVisible} position="center" onMaskClick={() => setCenterVisible(false)}>
<div style={{ width: '60vw', height: '60vw', ...centerStyle }}> popup-center</div>
</Popup>
</div>
);
});
export default Test;
结语
🎉 🎉 大功告成 🎉 🎉,这样初版就完成了。当然还有很多细节的功能没有实现,这里只是提供一种思路,可以根据后续的反馈组件的功能去逐步的打磨完善Popup
组件。