React开发组件-气泡提示,对话框与标签页

React开发组件-气泡提示,对话框与标签页

这是项目的结构:

在这里插入图片描述

dialog组件:

在这里插入图片描述

用法:

<Dialog
      title="Basic Modal"
      visible={visible}
      onOk={handleOk}
      onCancel={handleCancel}
    >
      <p>Some contents...</p>
      <p>Some contents...</p>
      <p>Some contents...</p>
      <p>Some contents...</p>
    </Dialog>

可以看到这里暴露给用户的接口有title对话框标题,visible控制对话框是否可见,onOK传入确认按钮的回调事件,onCancel传入取消按钮的回调事件,支持嵌套组件。

function Dialog(props) {
    return ReactDOM.createPortal(//dialog的重点!!一定要把它挂载在根节点的这里挂在body节点之下,防止被用户定义的容器覆盖隐藏
                    <div style={{ visibility:props.visible?'visible':'hidden'}} className="dialog" onClick={props.onCancel}>//根据props.visible决定是否展示,这里点击阴影时会关闭对话框
                        <div className="container" onClick={(e) => { e.stopPropagation() }}>//阻止冒泡触发父组件的关闭对话框事件
                            <div className="head">
                                <p id="title">{props.title}</p>
                                <span className="close">
                                    <svg onClick={props.onCancel} // 这里用svg画叉叉按钮(copy)
                                        viewBox="64 64 896 896"
                                        focusable="false"
                                        width="1em"
                                        height="1em">
                                        <path d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z">
                                        </path>
                                    </svg>
                                </span>
                            </div>
                            <hr />
                            <div style={{ marginLeft: '20px' }}>
                                {props.children} //将子组件内容读入渲染
                            </div>
                            <hr />
                            <div className="foot">
                                <button onClick={props.onCancel}>取消</button>
                                <button onClick={props.onOk}>确认</button>
                            </div>
                        </div>
                    </div>,document.body);
}

核心:

ReactDOM.createPortal(child, container)

第一个参数(child)是任何,例如一个元素,字符串或 fragment。第二个参数(container)是一个 DOM 元素。这个方法可以将我们的对话框挂载到任何我们想挂载的节点,这里挂载到body下防止覆盖和隐藏(overflow:hidden)

阻止冒泡防止内对话框内部点击时触发外部事件。

{props.children}渲染传入的子组件

样式方面:

.dialog{
    display: flex;
    position: absolute;
    width: 100%;
    height: 100%;
    justify-content: center;
    align-items: center;
    background-color: rgba(140, 140, 140, 0.85);
    z-index: 1;
    top:0px;
    left:0px;
}

主要是最外层这个容器的样式,flex做垂直和水平居中,定位为绝对定位+高度宽度100%,然后z-index提高优先级。

tabs组件:

在这里插入图片描述

用法:

<Tabs defaultActiveKey="1" tabPosition="left">
  <div tab="Tab 1" key="1">
    Content of Tab Pane 1
</div>
  <div tab="Tab 2" key="2">
    Content of Tab Pane 2
</div>
  <div tab="Tab 3" key="3">
    Content of Tab Pane 3
</div>
</Tabs>
  • defaultActiveKey:默认选择的标签key

  • tabPosition:标签的方向,实现了‘left’和‘right’

  • 嵌套子组件:tab标签值,key

function Tabs(props) {
    const [key, setKey] = useState(props.defaultActiveKey);// 设置组件的默认状态
    const buttoChang = (e) => {
        if(key!=e.target.name)
        {
            setKey(e.target.name) // 用name属性,绑定了传入得唯一key!!每次点击时更新key从而更改下方的内容
            if(props.onChange)props.onChange(e.target.name); //回调,传入当前选中key
        }
    }
    return (
        <div>
            <div style={{ display: props.tabPosition=='top'?'flex':'inline-block' ,//left时需要为弹性布局放置右部内容
            borderBottom:props.tabPosition=='top'?'solid 1px':'', //根据方向画线
            borderRight:props.tabPosition=='left'?'solid 1px':''}}>
                {
                    props.children.map(element => {
                        return <button
                            style={{display:'block'}}
                            className={key != element.key ? "myButton" : "myButton myButton-active"}
                            key={element.key} name={element.key}
                            onClick={buttoChang}>{element.props.tab}
                        </button>
                    })// map渲染按钮,文字内容为传入的tab值,根据组件状态key选择active的button
                }
            </div>
            {
                props.children.map((item) => {
                    return (
                        <div key={ item.key } style={{ position: key == item.key ? 'relative' : 'absolute', 
                        visibility: key == item.key ? 'visible' : 'hidden',
                        margin:'10px' }}>
                            {item}
                        </div>
                    )
                })
            }// 渲染三个绝对定位的div,根据选择的key让对应内容变为可见
        </div>
    )
}

核心:

对于外部传入的key的绑定与更改,这里在渲染时都给组件绑定了key值,更改组件状态的key值则是用按钮触发事件获得的e.target.name去设置的。

tips组件:

在这里插入图片描述C:\Users\heavyyang\AppData\Roaming\Typora\typora-user-images\image-20200720163239836.png
用法:

<Tip title="我是气泡,快乐的气泡" placement="top"><div>这里是上气泡提示</div></Tip>

title设置气泡的文字内容,placement则是设置气泡的位置,有top,right,bottom,left四个方向

class Tip extends React.Component {
    constructor() {
        super();
        this.state = {
            flag: false,
            arrowTop: '0px',
            arrowLeft: '0px',
            arrow: '',
            sendTop: '0px',
            sendLeft: '0px'
        }
    }
    componentDidMount() {
        const setSend = () => {
            // 计算气泡框的位置,和小箭头的位置的函数,这块虽然有效果但整体代码不是很好,以后看看怎么优化,下面会写下思路
            mywidth = this.send.clientWidth;
            myheight = this.send.clientHeight;
            const { left, top, right, bottom } = this.tip.getBoundingClientRect();//获取元素绝对位置
            const { clientWidth, clientHeight } = document.documentElement;
            switch (this.props.placement) {
                case 'top':
                    this.setState({
                        arrowTop: '40px',//padding+字体算出来的,适应性有待验证
                        arrowLeft: '20px',
                        sendTop: '-50px',
                        arrow: '#F8C301 rgba(255,255,255,0) rgba(255,255,255,0) rgba(255,255,255,0)'
                    })
                    break;
                case 'right':
                    this.setState({
                        arrowTop: '5px',
                        arrowLeft: '-16px',
                        sendLeft: this.tip.clientWidth + 8 + 'px',
                        sendTop:myheight>this.tip.clientHeight?'0px':(this.tip.clientHeight-myheight)/2+'px',
                        arrow: 'rgba(255,255,255,0) #F8C301 rgba(255,255,255,0) rgba(255,255,255,0)'
                    })
                    break;
                case 'bottom':
                    this.setState({
                        arrowTop: '-16px',
                        arrowLeft: '20px',
                        sendTop: 8 + this.tip.clientHeight + 'px',
                        arrow: 'rgba(255,255,255,0) rgba(255,255,255,0) #F8C301 rgba(255,255,255,0)'
                    })
                    break;
                case 'left':
                    this.setState({
                        arrowTop: '5px',
                        arrow: 'rgba(255,255,255,0) rgba(255,255,255,0) rgba(255,255,255,0) #F8C301',
                        sendLeft: -this.send.clientWidth - 8 + 'px',
                        sendTop:myheight>this.tip.clientHeight?'0px':(this.tip.clientHeight-myheight)/2+'px',
                        arrowLeft: this.send.clientWidth - 1 + 'px'
                    })
                    break;
                default:
                    // 不指定时自动计算
                    
                    //this.send.parentElement.style.position='absolute';
                    
                    // 计算四个方向的百分比
                    // 由于气泡是单行文本,上下方向的百分比占比会小很多,基本上气泡就只会是上下
                    // console.log(clientWidth + ' ' + right)
                    x1 = (mywidth + 8) / left;
                    y1 = (myheight + 8) / top;
                    x2 = clientWidth - right>0?(mywidth + 8) / (clientWidth - right):Infinity;
                    y2 = clientHeight - bottom>0?(myheight + 8) / (clientHeight - bottom):Infinity;
                    const array = [x1, y1, x2, y2]
                    console.log(array);
                    let m = 0, min = x1;
                    array.forEach((item, index) => {
                        if (item < min) {
                            min = item;
                            m = index;
                        }
                    })
                    switch (m) {
                        case 0: //左
                            this.setState({
                                arrowTop: '5px',
                                arrow: 'rgba(255,255,255,0) rgba(255,255,255,0) rgba(255,255,255,0) #F8C301',
                                sendTop:myheight>this.tip.clientHeight?'0px':(this.tip.clientHeight-myheight)/2+'px',
                                sendLeft: -mywidth - 8 + 'px',
                                arrowLeft: mywidth + 'px'
                            })
                            break;
                        case 1: //上
                            this.setState({
                                arrowTop: '40px',//padding+字体算出来的,适应性有待验证
                                arrowLeft: '20px',
                                sendTop: '-50px',
                                sendLeft:'0px',
                                arrow: '#F8C301 rgba(255,255,255,0) rgba(255,255,255,0) rgba(255,255,255,0)'
                            })
                            break;
                        case 2: //右
                            this.setState({
                                arrowTop: '5px',
                                arrowLeft: '-16px',
                                sendLeft: this.tip.clientWidth + 8 + 'px',
                                sendTop:myheight>this.tip.clientHeight?'0px':(this.tip.clientHeight-myheight)/2+'px',
                                arrow: 'rgba(255,255,255,0) #F8C301 rgba(255,255,255,0) rgba(255,255,255,0)'
                            })
                            break;
                        case 3: //下
                            this.setState({
                                arrowTop: '-16px',
                                arrowLeft: '20px',
                                sendTop: 8 + this.tip.clientHeight + 'px',
                                sendLeft:'0px',
                                arrow: 'rgba(255,255,255,0) rgba(255,255,255,0) #F8C301 rgba(255,255,255,0)'
                            })
                            break;
                        default:
                            break;
                    }
                    break;
            }
        }
        setSend();
        window.addEventListener('resize', debounce(setSend));
    }
    render() {
        return (
            <div
                ref={(e) => { this.tip = e }}
                onMouseOver={() => { this.setState({ flag: true }) }} //气泡隐藏
                onMouseLeave={() => { this.setState({ flag: false }) }}//触发显示气泡
                style={{ display: 'inline-block' }}  //inline-block父组件宽高度与子组件相同,方便我获取子组件的大小
            >
                <div style={{ position: 'relative', visibility: this.state.flag ? 'visible' : 'hidden' }}>
                    <div ref={(e) => { this.send = e }}
                        className='send'
                        style={{
                            top: this.state.sendTop,
                            left: this.state.sendLeft,
                            filter: this.props.placement == 'left' ?
                                'drop-shadow(-10px 0px 10px  rgba(238, 125, 55,0.5))'
                                : 'drop-shadow(10px 0px 10px  rgba(238, 125, 55,0.5))'
                        }}>// 这里气泡向左需要更改下阴影方向
                        {this.props.title}
                        <div className='arrow'
                            style={{ top: this.state.arrowTop, left: this.state.arrowLeft, borderColor: this.state.arrow }} />//小箭头
                    </div>
                </div>
                {this.props.children}
            </div>)
    }
};

气泡思路:

在这里插入图片描述

两个框使用绝对定位的框,如图:

arrow的处理:

position:absolute;
width:0;
height:0;
font-size:0;
border:solid 8px;
borderColor:'#F8C301 rgba(255,255,255,0) rgba(255,255,255,0) rgba(255,255,255,0)'

设置一个无内容的div,然后设置边框8px,可以想象,此时是一个16px的黑色小正方形,按照你想要设置的气泡方向去设置颜色,比如#F8C301 rgba(255,255,255,0) rgba(255,255,255,0) rgba(255,255,255,0)这样就是上部边框为黄色,其余全部为透明,三角形就出来了。

接着就是arrow位置的移动,这里除了左气泡需要获取sendwidth去移动之外,其余参数基本是常数(单行气泡的情况下)

然后是send的移动,由于使用了相对定位:

  • 上:向上移动50px,(单行情况下),padding20+字体22+箭头8;不用左右
  • 下:向下移动50px,(单行情况下),padding20+字体22+箭头8;不用左右
  • 左:向左移动(send.width+8箭头),如果文本内容的高度高于send时坐下手动居中(myheight>this.tip.clientHeight ? ‘0px’ : (this.tip.clientHeight-myheight)/2+‘px’)
  • 右:向右移动(send.width+8箭头),如果文本内容的高度高于send时坐下手动居中(myheight>this.tip.clientHeight ? ‘0px’ : (this.tip.clientHeight-myheight)/2+‘px’)

当用户不设置方向时自动计算其位置:

this.tip.getBoundingClientRect()这个函数可以获得元素所在相对浏览器的绝对位置

然后我们就可以计算上下左右的(send(高度|宽度)/所提示文本相对浏览器所剩上下左右长度),其中最小的值就是我们要设置的方向(有待商榷,暂时想到这个)

项目地址:https://github.com/yangHeavy/reactComponent

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值