bee-tooltip
Version:
bee-tooltip ui component for react
299 lines (279 loc) • 9.41 kB
JavaScript
import classnames from 'classnames';
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import OverlayTrigger from 'bee-overlay/build/OverlayTrigger';
const propTypes = {
/**
* @required
*/
id: PropTypes.oneOfType([
PropTypes.string, PropTypes.number,
]),
inverse: PropTypes.bool,
visible: PropTypes.bool,
onVisibleChange: PropTypes.func,
/**
* 相对目标元素显示上下左右的位置
*/
placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left', 'topLeft', 'topRight', 'bottomLeft', 'bottomRight', 'leftTop', 'leftBottom', 'rightTop', 'rightBottom']),
/**
* 绝对定位上边距.
*/
positionTop: PropTypes.oneOfType([
PropTypes.number, PropTypes.string,
]),
/**
* 绝对定位左边距
*/
positionLeft: PropTypes.oneOfType([
PropTypes.number, PropTypes.string,
]),
/**
* 与目标Top的距离
*/
arrowOffsetTop: PropTypes.oneOfType([
PropTypes.number, PropTypes.string,
]),
/**
* 与目标Left的距离
*/
arrowOffsetLeft: PropTypes.oneOfType([
PropTypes.number, PropTypes.string,
]),
/**
* 是否开启内容区域按可视区最大可用高度设置
*/
overlayMaxHeight: PropTypes.bool
};
const defaultProps = {
placement: 'right',
clsPrefix: 'u-tooltip',
overlayMaxHeight: false
};
function OverlayNode(props){
let { id, className, classNames, style, overlay, overlayStyle, otherProps } = props;
// style 包含用户传入的自定义样式,以及 bee-overlay 计算返回的样式。
// overlayStyle 是用户传入的自定义样式。
if(overlayStyle && overlayStyle.width) {
style.width = overlayStyle.width;
} else {
delete style.width;
}
let {top, left, width, height, maxWidth, maxHeight} = style
let reg = /\d+/g
// top、left 设置最小值0
// 如果是负数被校正为0,则相应调整宽高,避免弹层遮挡触发元素
if(top !== undefined && top < 0) {
reg.test(height) && (style.height = Math.max(height + top, 0))
reg.test(maxHeight) && (style.maxHeight = Math.max(maxHeight + top, 0))
style.top = 0
}
if(left !== undefined && left < 0) {
reg.test(width) && (style.width = Math.max(width + left, 0))
reg.test(maxWidth) && (style.maxWidth = Math.max(maxWidth + left, 0))
style.left = 0
}
return (
<div
id={id}
role="tooltip"
className={classnames(className, classNames)}
onMouseEnter={props.onMouseEnter}
onMouseLeave={props.onMouseLeave}
style={style}
{...otherProps}
>
{
overlay?(
<div className='tooltip-arrow'/>
):''
}
{
overlay?(
<div className='tooltip-inner'>
{overlay}
</div>
):''
}
</div>
)
}
function cloneElement(element, props) {
return React.cloneElement(
element,
typeof props === 'function' ? props(element.props || {}) : props
)
}
class Tooltip extends React.Component {
constructor(props){
super(props);
let initState = {
isHoverShow: false,
autoHeightStyle: {}
}
if ('visible' in props) {
Object.assign(initState, {
visible: props.visible
})
}
this.state = initState;
this.childRef = React.createRef(null)
}
componentDidMount(){
let { visible, overlayMaxHeight } = this.props;
if((visible || this.state.isHoverShow) && overlayMaxHeight){
this.autoAdjustPlacement()
}
}
componentDidUpdate(prevProps) {
let { visible, overlayMaxHeight } = this.props;
if ('visible' in this.props && prevProps.visible !== visible) {
if(visible && overlayMaxHeight){
this.autoAdjustPlacement()
}
this.setState({
visible: visible
})
}
}
handleVisibleChange = (visible) => {
const { onVisibleChange, overlayMaxHeight } = this.props
onVisibleChange && onVisibleChange(visible)
if (visible && overlayMaxHeight) {
this.autoAdjustPlacement()
}
}
/**
* 自动校正max-height和placement
*
*/
autoAdjustPlacement = () => {
const {findDOMNode} = ReactDOM
const autoHeight = {}
const placement = this.props.placement || defaultProps.placement
const ARROW_SPACE = 10, EXTRA_SPACE = 15
const bodyStyle = document.body.getBoundingClientRect()
const childStyle = findDOMNode(this.childRef.current) && findDOMNode(this.childRef.current).getBoundingClientRect()
if(childStyle) {
const availableSpace = {
top: childStyle.top - ARROW_SPACE,
bottom: bodyStyle.height - childStyle.bottom - ARROW_SPACE
}
if (placement.indexOf('top') >= 0 || placement.indexOf('bottom') >= 0) {
Object.assign(autoHeight, {
maxHeight: Math.max(availableSpace.top, availableSpace.bottom) - EXTRA_SPACE + 'px',
overflowY: 'scroll'
})
} else if (placement === 'left' || placement === 'right') {
Object.assign(autoHeight, {
maxHeight: bodyStyle.height - EXTRA_SPACE + 'px',
overflowY: 'scroll'
})
} else if (placement.indexOf('Top') >= 0) {
Object.assign(autoHeight, {
maxHeight: bodyStyle.height - childStyle.top - EXTRA_SPACE + 'px',
overflowY: 'scroll'
})
} else if (placement.indexOf('Bottom') >= 0) {
Object.assign(autoHeight, {
maxHeight: childStyle.bottom - EXTRA_SPACE + 'px',
overflowY: 'scroll'
})
}
this.setState({autoHeightStyle : autoHeight});
}
// console.log(placement, bodyStyle, autoHeight);
}
/**
* @desc 鼠标划入时候的事件
*/
onMouseEnter = () => {
let {trigger} = this.props;
if(trigger === 'click') return;
this.setState({
isHoverShow: true
})
}
/**
* @desc 鼠标划出时候的事件
*/
onMouseLeave = () => {
let {trigger} = this.props;
if(trigger === 'click') return;
this.setState({
isHoverShow: false
})
}
handleOnHide = () => {
let { onHide } = this.props;
onHide && onHide(false)
}
render() {
const {
placement,
id,
arrowOffsetTop,
arrowOffsetLeft,
className,
style,
children,
clsPrefix,
overlay,
inverse,
trigger,
onVisibleChange,
onHide,
rootClose,
visible,
defaultOverlayShown,
positionTop,
positionLeft,
overlayMaxHeight,
...others
} = this.props;
const { autoHeightStyle, isHoverShow } = this.state;
let overlayStyle = Object.assign({},autoHeightStyle,style);
let classes = {
[placement]: true,
'inverse': inverse
};
let classNames = classnames(clsPrefix, classes);
let overlayNode = <OverlayNode
id={id}
className={className}
classNames={classNames}
overlay={overlay}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
style={overlayStyle}
overlayStyle={overlayStyle} // 用户自定义样式
arrowOffsetTop={arrowOffsetTop}
arrowOffsetLeft={arrowOffsetLeft}
otherProps={others}
/>
const visibleProp = 'visible' in this.props ? { visible: this.state.visible } : { isHoverShow: isHoverShow }
return (
<OverlayTrigger
{...others}
{...visibleProp}
onVisibleChange={this.handleVisibleChange}
ref={ref => this.trigger = ref}
shouldUpdatePosition
placement={placement}
overlay={overlayNode}
onHide={this.handleOnHide}
rootClose={rootClose}
defaultOverlayShown={defaultOverlayShown}
positionTop={positionTop}
positionLeft={positionLeft}
trigger={trigger}
>
{ cloneElement(children, {ref: this.childRef}) }
</OverlayTrigger>
)
}
}
Tooltip.propTypes = propTypes;
Tooltip.defaultProps = defaultProps;
export default Tooltip;