Feat tooltip wrapper (#3732)

* feat: TooltipWrapper 的component组件完善

* feat: TooltipWrapper 文字提示容器 render组件 & 文档完善

* feat: TooltipWrapper 文字提示容器render组件兼容tooltip属性

* fix: 修复 tooltip组件 PR中相关问题

* fix: TooltipWrapper 属性children调整 & 标题内容支持DOM解析

Co-authored-by: ”jiatianqi“ <”jiatianqi@baidu.com“>
This commit is contained in:
Ma ke 2022-03-16 15:01:33 +08:00 committed by GitHub
parent 9a19956b0f
commit 7f8a93716f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 325 additions and 113 deletions

View File

@ -26,14 +26,14 @@ order: 59
"items": [
{
"type": "tooltip-wrapper",
"tooltip": "提示文字",
"content": "提示文字",
"body": "hover 激活文字提示",
"className": "mb-1"
},
{
"type": "tooltip-wrapper",
"title": "标题",
"tooltip": "提示文字",
"content": "提示文字",
"trigger": "click",
"body": "click 激活文字提示",
"className": "mb-1"
@ -49,7 +49,7 @@ order: 59
[
{
"type": "tooltip-wrapper",
"tooltip": "删除提示",
"content": "删除提示",
"inline": true,
"body": [
{
@ -89,7 +89,7 @@ order: 59
"items": [
{
"type": "tooltip-wrapper",
"tooltip": "提示文字",
"content": "提示文字",
"body": [
{
"type": "icon",
@ -116,7 +116,7 @@ order: 59
"items": [
{
"type": "tooltip-wrapper",
"tooltip": "提示文字",
"content": "提示文字",
"placement": "left",
"body": [
{
@ -132,7 +132,7 @@ order: 59
},
{
"type": "tooltip-wrapper",
"tooltip": "提示文字",
"content": "提示文字",
"placement": "right",
"body": [
{
@ -160,7 +160,7 @@ order: 59
"items": [
{
"type": "tooltip-wrapper",
"tooltip": "提示文字",
"content": "提示文字",
"placement": "bottom",
"body": [
{
@ -180,6 +180,54 @@ order: 59
}
```
## 位置偏移
组件提供了关于相对提示位置的垂直、水平位置上的偏移,默认[0, 0]。
```schema: scope="body"
[
{
"type": "tooltip-wrapper",
"title": "标题",
"content": "文案提示位置偏移 [10, -20]",
"offset": [10, -20],
"inline": true,
"className": "mr-2",
"body": [
{
"type": "tpl",
"tpl": "向右偏移10px向上偏移20px"
}
]
}
]
```
## 展示箭头
`showArrow``false` 不展示箭头。
```schema: scope="body"
[
{
"type": "tooltip-wrapper",
"title": "标题",
"content": "提示内容",
"showArrow": false,
"inline": true,
"className": "mr-2",
"body": [
{
"type": "tpl",
"tpl": "没有箭头"
}
]
}
]
```
## 主题色
组件提供了两个不同的主题:`dark` 和 `light`,默认使用`light`。
@ -189,7 +237,7 @@ order: 59
{
"type": "tooltip-wrapper",
"title": "标题",
"tooltip": "文案提示",
"content": "文案提示",
"inline": true,
"className": "mr-2",
"body": [
@ -202,7 +250,7 @@ order: 59
{
"type": "tooltip-wrapper",
"title": "标题",
"tooltip": "文案提示",
"content": "文案提示",
"inline": true,
"tooltipTheme": "dark",
"body": [
@ -216,9 +264,34 @@ order: 59
```
## 延迟打开&关闭
`mouseEnterDelay` 为延迟展示, `mouseLeaveDelay` 为延迟隐藏,
```schema: scope="body"
[
{
"type": "tooltip-wrapper",
"title": "标题",
"content": "提示内容",
"mouseEnterDelay": 1000,
"mouseLeaveDelay": 2000,
"inline": true,
"className": "mr-2",
"body": [
{
"type": "tpl",
"tpl": "延迟1s展示延迟2s隐藏"
}
]
}
]
```
## 动态文案
`tooltip``title` 支持变量映射,可以从上下文中动态获取提示文案。
`content` 和 `title` 支持变量映射,可以从上下文中动态获取提示文案。
```schema
{
@ -228,7 +301,7 @@ order: 59
},
body: {
"type": "tooltip-wrapper",
"tooltip": "${text}",
"content": "${text}",
"body": {
"type": "html",
"style": {
@ -252,21 +325,21 @@ order: 59
[
{
"type": "tooltip-wrapper",
"tooltip": "文字提示",
"content": "文字提示",
"inline": true,
"className": "p-1 mr-3 border-2 border-solid border-indigo-400",
"body": "内联容器1"
},
{
"type": "tooltip-wrapper",
"tooltip": "文字提示",
"content": "文字提示",
"inline": true,
"className": "p-1 mr-3 border-2 border-solid border-indigo-400",
"body": "内联容器2"
},
{
"type": "tooltip-wrapper",
"tooltip": "文字提示",
"content": "文字提示",
"className": "p-1 mt-3 border-2 border-solid border-green-400",
"body": "非内联容器"
}
@ -281,7 +354,7 @@ order: 59
```schema: scope="body"
{
"type": "tooltip-wrapper",
"tooltip": "文字提示(加粗)",
"content": "文字提示(加粗)",
"inline": true,
"style": {
fontStyle: "italic"
@ -305,7 +378,7 @@ order: 59
```schema: scope="body"
{
"type": "tooltip-wrapper",
"tooltip": "文字提示",
"content": "文字提示",
"wrapperComponent": "pre",
"body": "function HelloWorld() {\n console.log('Hello World');\n}"
}
@ -317,10 +390,15 @@ order: 59
| ---------------- | ----------------------------------------------------------------------- | ------------------- | ---------------------------------------------- |
| type | `string` | `"tooltip-wrapper"` | 指定为文字提示容器组件 |
| title | `string` | `""` | 文字提示标题 |
| tooltip | `string` | `""` | 文字提示 |
| content | `string` | `""` | 文字提示内容, 兼容之前的 tooltip 属性 |
| placement | `"top" \| "left" \| "right" \| "bottom" ` | `"top"` | 文字提示浮层出现位置 |
| tooltipTheme | `"light" \| "dark"` | `"light"` | 主题样式, 默认为 light |
| offset | `[number, number]` | `[0, 0]` | 文字提示浮层位置相对偏移量,单位 px |
| showArrow | `boolean` | `true` | 是否展示浮层指向箭头 |
| disabled | `boolean` | `false` | 是否禁用浮层提示 |
| trigger | `"hover" \| "click" \| "focus" \| Array<"hover" \| "click" \| "focus">` | `"hover"` | 浮层触发方式,支持数组写法`["hover", "click"]` |
| delay | `number` | `0` | 浮层隐藏延迟时间,单位 ms |
| mouseEnterDelay | `number` | `0` | 浮层延迟展示时间,单位 ms |
| mouseLeaveDelay | `number` | `300` | 浮层延迟隐藏时间,单位 ms |
| rootClose | `boolean` | `true` | 是否点击非内容区域关闭提示 |
| inline | `boolean` | `false` | 内容区是否内联显示 |
| wrapperComponent | `string` | `"div" \| "span"` | 容器标签名 |

View File

@ -59,6 +59,8 @@
--boxShadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--boxShadowSm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--boxTooltipShadow: 0 4px 6px 1px rgb(8 14 26 / 6%),
0 1px 10px 0 rgb(8 14 26 / 5%), 0 2px 4px -1px rgb(8 14 26 / 4%);
--lineHeightBase: 1.5;
@ -1453,14 +1455,15 @@
--Tooltip-bg--dark: rgba(7, 12, 20, 0.85);
--Tooltip-body-color: var(--text-color);
--Tooltip-body-color--dark: var(--white);
--Tooltip-body-paddingX: var(--gap-sm);
--Tooltip-body-paddingX: var(--gap-base);
--Tooltip-body-paddingY: var(--gap-sm);
--Tooltip-borderColor: var(--borderColor);
--Tooltip-borderRadius: var(--borderRadiusLg);
--Tooltip-borderWidth: var(--borderWidth);
--Tooltip-boxShadow: var(--boxShadow);
--Tooltip-boxShadow: var(--boxTooltipShadow);
--Tooltip-boxShadow--dark: 0 2px 8px 0 rgba(7, 12, 20, 0.12);
--Tooltip-fontSize: var(--fontSizeSm);
--Tooltip-fontWeight: var(--fontWeightMd);
--Tooltip-maxWidth: #{px2rem(240px)};
--Tooltip-minWidth: auto;
--Tooltip-title-fontWeight: bold;
@ -1468,8 +1471,8 @@
--Tooltip-title-borderBottom-color: #{darken(darken($white, 3%), 5%)};
--Tooltip-title-color: var(--text--loud-color);
--Tooltip-title-color--dark: var(--white);
--Tooltip-title-paddingX: var(--gap-sm);
--Tooltip-title-paddingY: var(--gap-xs);
--Tooltip-title-paddingX: var(--gap-base);
--Tooltip-title-paddingY: var(--gap-sm);
--Transfer-title-bg: #f6f8f8;

View File

@ -11,7 +11,6 @@
z-index: $zindex-tooltip;
word-wrap: break-word;
background: var(--Tooltip-bg);
border: var(--Tooltip-borderWidth) solid var(--Tooltip-borderColor);
box-shadow: var(--Tooltip-boxShadow);
&-arrow {
@ -49,7 +48,6 @@
.#{$ns}Tooltip-arrow::before {
bottom: 0;
border-top-color: var(--Tooltip-arrow-outerColor);
}
.#{$ns}Tooltip-arrow::after {
@ -77,7 +75,6 @@
.#{$ns}Tooltip-arrow::before {
left: 0;
border-right-color: var(--Tooltip-arrow-outerColor);
}
.#{$ns}Tooltip-arrow::after {
@ -103,7 +100,6 @@
.#{$ns}Tooltip-arrow::before {
top: 0;
border-bottom-color: var(--Tooltip-arrow-outerColor);
}
.#{$ns}Tooltip-arrow::after {
@ -146,7 +142,6 @@
.#{$ns}Tooltip-arrow::before {
right: 0;
border-left-color: var(--Tooltip-arrow-outerColor);
}
.#{$ns}Tooltip-arrow::after {
@ -156,13 +151,11 @@
}
&-title {
padding: var(--Tooltip-title-paddingY) var(--Tooltip-title-paddingX);
padding: var(--Tooltip-title-paddingY) var(--Tooltip-title-paddingX) 0;
margin-bottom: 0; // Reset the default from Reboot
font-size: var(--fontSizeBase);
color: var(--Tooltip-title-color);
background: var(--Tooltip-title-bg);
border-bottom: var(--Tooltip-borderWidth) solid
var(--Tooltip-title-borderBottom-color);
font-weight: var(--Tooltip-fontWeight);
border-top-left-radius: calc(
var(--Tooltip-borderRadius) - var(--Tooltip-borderWidth)
);
@ -179,6 +172,8 @@
&-body {
color: var(--Tooltip-body-color);
padding: var(--Tooltip-body-paddingY) var(--Tooltip-body-paddingX);
word-break: break-all;
text-align: left;
}
&--dark {

View File

@ -97,7 +97,8 @@ class Position extends React.Component<any, any> {
overlay,
target,
container,
this.props.containerPadding
this.props.containerPadding,
this.props.offset
)
);
}
@ -173,7 +174,7 @@ interface OverlayProps {
container?: React.ReactNode | Function;
target?: React.ReactNode | Function;
watchTargetSizeChange?: boolean;
offset?: [number, number];
onEnter?(node: HTMLElement): any;
onEntering?(node: HTMLElement): any;
onEntered?(node: HTMLElement): any;
@ -238,6 +239,7 @@ export default class Overlay extends React.Component<
children,
watchTargetSizeChange,
transition: Transition,
offset,
...props
} = this.props;
@ -259,7 +261,8 @@ export default class Overlay extends React.Component<
containerPadding,
target,
placement,
shouldUpdatePosition
shouldUpdatePosition,
offset
}}
ref={this.positionRef}
>

View File

@ -18,18 +18,23 @@ interface TooltipProps extends React.HTMLProps<HTMLDivElement> {
style?: any;
arrowProps?: any;
placement?: string;
showArrow?: boolean;
tooltipTheme?: string;
[propName: string]: any;
}
export class Tooltip extends React.Component<TooltipProps> {
static defaultProps = {
className: ''
className: '',
tooltipTheme: 'light',
showArrow: true
};
render() {
const {
classPrefix: ns,
className,
tooltipTheme,
title,
children,
arrowProps,
@ -41,6 +46,7 @@ export class Tooltip extends React.Component<TooltipProps> {
positionTop,
classnames: cx,
activePlacement,
showArrow,
onMouseEnter,
onMouseLeave,
...rest
@ -52,14 +58,17 @@ export class Tooltip extends React.Component<TooltipProps> {
className={cx(
`Tooltip`,
activePlacement ? `Tooltip--${activePlacement}` : '',
className
className,
`Tooltip--${tooltipTheme === 'dark' ? 'dark' : 'light'}`
)}
style={style}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
role="tooltip"
>
<div className={cx(`Tooltip-arrow`)} {...arrowProps} />
{showArrow ? (
<div className={cx(`Tooltip-arrow`)} {...arrowProps} />
) : null}
{title ? <div className={cx('Tooltip-title')}>{title}</div> : null}
<div className={cx('Tooltip-body')}>{children}</div>
</div>

View File

@ -11,21 +11,78 @@ import {findDOMNode} from 'react-dom';
import Tooltip from './Tooltip';
import {ClassNamesFn, themeable} from '../theme';
import Overlay from './Overlay';
export interface TooltipObject {
title?: string;
content?: string;
render?: () => JSX.Element;
dom?: JSX.Element;
}
import {isObject} from '../utils/helper';
export type Trigger = 'hover' | 'click' | 'focus';
export interface TooltipObject {
/**
*
*/
title?: string;
/**
*
*/
content?: string;
/**
*
*/
placement?: 'top' | 'right' | 'bottom' | 'left';
/**
*
*/
tooltipTheme?: 'light' | 'dark';
/**
*
*/
offset?: [number, number];
/**
*
*/
style?: React.CSSProperties;
/**
*
*/
showArrow?: boolean;
/**
*
*/
disabled?: boolean;
/**
* , ms
*/
mouseEnterDelay?: number;
/**
* , ms
*/
mouseLeaveDelay?: number;
/**
* JSX渲染
*/
children?: () => JSX.Element | JSX.Element;
/**
*
*/
container?: React.ReactNode;
/**
*
*/
trigger?: Trigger | Array<Trigger>;
/**
* true
*/
rootClose?: boolean;
/**
* CSS类名
*/
tooltipClassName?: string;
}
export interface TooltipWrapperProps {
tooltip?: string | TooltipObject;
classPrefix: string;
classnames: ClassNamesFn;
placement: 'top' | 'right' | 'bottom' | 'left';
tooltip?: string | TooltipObject;
container?: React.ReactNode;
trigger: Trigger | Array<Trigger>;
rootClose: boolean;
@ -109,15 +166,26 @@ export class TooltipWrapper extends React.Component<
handleShow() {
this.timer && clearTimeout(this.timer);
waitToHide && waitToHide();
this.show();
const tooltip = this.props.tooltip;
if (isObject(tooltip)) {
const {mouseEnterDelay = 0} = tooltip as TooltipObject;
this.timer = setTimeout(this.show, mouseEnterDelay);
} else {
this.timer = setTimeout(this.show, 0);
}
}
handleHide() {
clearTimeout(this.timer);
const {delay} = this.props;
const {delay, tooltip} = this.props;
waitToHide = this.hide.bind(this);
this.timer = setTimeout(this.hide, delay);
if (isObject(tooltip)) {
const {mouseLeaveDelay = 300} = tooltip as TooltipObject;
this.timer = setTimeout(this.hide, mouseLeaveDelay);
} else {
this.timer = setTimeout(this.hide, delay);
}
}
handleFocus(e: any) {
@ -161,22 +229,42 @@ export class TooltipWrapper extends React.Component<
}
render() {
const props = this.props;
const child = React.Children.only(props.children);
if (!props.tooltip) {
return child;
}
// tooltip 对象内属性优先级更高
const tooltipObj: TooltipObject = {
placement: props.placement,
container: props.container,
trigger: props.trigger,
rootClose: props.rootClose,
tooltipClassName: props.tooltipClassName,
style: props.style,
mouseLeaveDelay: props.delay,
...(typeof props.tooltip === 'string'
? {content: props.tooltip}
: props.tooltip)
};
const {
tooltip,
children,
title,
content,
placement,
container,
trigger,
rootClose,
tooltipClassName,
style
} = this.props;
const child = React.Children.only(children);
if (!tooltip) {
return child;
}
style,
disabled = false,
offset,
tooltipTheme = 'light',
showArrow = true,
children
} = tooltipObj;
const childProps: any = {
key: 'target'
@ -204,31 +292,30 @@ export class TooltipWrapper extends React.Component<
<Overlay
key="overlay"
target={this.getTarget}
show={this.state.show}
show={this.state.show && !disabled}
onHide={this.handleHide}
rootClose={rootClose}
placement={placement}
container={container}
offset={Array.isArray(offset) ? offset : [0, 0]}
>
<Tooltip
title={typeof tooltip !== 'string' ? tooltip.title : undefined}
title={typeof title === 'string' ? title : undefined}
style={style}
className={tooltipClassName}
onMouseEnter={~triggers.indexOf('hover') && this.handleMouseOver}
onMouseLeave={~triggers.indexOf('hover') && this.handleMouseOut}
tooltipTheme={tooltipTheme}
showArrow={showArrow}
onMouseEnter={
~triggers.indexOf('hover') ? this.handleMouseOver : () => {}
}
onMouseLeave={
~triggers.indexOf('hover') ? this.handleMouseOut : () => {}
}
>
{tooltip && (tooltip as TooltipObject).render ? (
this.state.show ? (
(tooltip as TooltipObject).render!()
) : null
) : tooltip && (tooltip as TooltipObject).dom ? (
(tooltip as TooltipObject).dom!
{children ? (
<>{typeof children === 'function' ? children() : children}</>
) : (
<Html
html={
typeof tooltip === 'string' ? tooltip : tooltip.content || ''
}
/>
<Html html={typeof content === 'string' ? content : ''} />
)}
</Tooltip>
</Overlay>

View File

@ -6,7 +6,7 @@ import {escapeHtml} from '../utils/tpl-builtin';
import {buildStyle} from '../utils/style';
import {TooltipWrapper as TooltipWrapperComp} from '../components';
import type {Trigger} from '../components/TooltipWrapper';
import type {Trigger, TooltipObject} from '../components/TooltipWrapper';
export interface TooltipWrapperSchema extends BaseSchema {
/**
@ -20,7 +20,12 @@ export interface TooltipWrapperSchema extends BaseSchema {
title?: string;
/**
*
* tooltip content
*/
content?: string;
/**
* @deprecated
*/
tooltip?: string;
@ -29,15 +34,35 @@ export interface TooltipWrapperSchema extends BaseSchema {
*/
placement?: 'top' | 'right' | 'bottom' | 'left';
/**
*
*/
offset?: [number, number];
/**
*
*/
showArrow?: boolean;
/**
*
*/
disabled?: boolean;
/**
* hover
*/
trigger?: Trigger | Array<Trigger>;
/**
* ms0
* , ms
*/
delay?: number;
mouseEnterDelay?: number;
/**
* , ms
*/
mouseLeaveDelay?: number;
/**
* true
@ -60,7 +85,7 @@ export interface TooltipWrapperSchema extends BaseSchema {
inline?: boolean;
/**
* light
* light
*/
tooltipTheme?: 'light' | 'dark';
@ -97,6 +122,7 @@ export interface TooltipWrapperProps extends RendererProps {
/**
*
*/
content?: string;
tooltip?: string;
/**
*
@ -105,7 +131,11 @@ export interface TooltipWrapperProps extends RendererProps {
inline?: boolean;
trigger: Trigger | Array<Trigger>;
rootClose?: boolean;
delay?: number;
showArrow?: boolean;
offset?: [number, number];
disabled?: boolean;
mouseEnterDelay?: number;
mouseLeaveDelay?: number;
container?: React.ReactNode;
style?: React.CSSProperties;
tooltipStyle?: React.CSSProperties;
@ -124,7 +154,8 @@ export default class TooltipWrapper extends React.Component<
| 'placement'
| 'trigger'
| 'rootClose'
| 'delay'
| 'mouseEnterDelay'
| 'mouseLeaveDelay'
| 'inline'
| 'wrap'
| 'tooltipTheme'
@ -132,7 +163,8 @@ export default class TooltipWrapper extends React.Component<
placement: 'top',
trigger: 'hover',
rootClose: true,
delay: 0,
mouseEnterDelay: 0,
mouseLeaveDelay: 200,
inline: false,
wrap: false,
tooltipTheme: 'light'
@ -172,41 +204,45 @@ export default class TooltipWrapper extends React.Component<
render() {
const {
tooltipClassName,
classPrefix: ns,
classnames: cx,
tooltipClassName,
tooltipTheme,
container,
placement,
rootClose,
tooltipStyle,
title,
content,
tooltip,
delay,
mouseEnterDelay,
mouseLeaveDelay,
trigger,
tooltipTheme,
offset,
showArrow,
disabled,
data
} = this.props;
const tooltipObj = {
title: escapeHtml(filter(title, data)),
content: escapeHtml(filter(tooltip, data))
const tooltipObj: TooltipObject = {
title: filter(title, data),
content: filter(content || tooltip, data),
style: buildStyle(tooltipStyle, data),
placement,
trigger,
rootClose,
container,
tooltipTheme,
tooltipClassName,
mouseEnterDelay,
mouseLeaveDelay,
offset,
showArrow,
disabled
};
return (
<TooltipWrapperComp
classPrefix={ns}
classnames={cx}
style={buildStyle(tooltipStyle, data)}
placement={placement}
tooltip={tooltipObj}
trigger={trigger}
rootClose={rootClose}
delay={delay}
container={container}
tooltipClassName={cx(tooltipClassName, {
'Tooltip--dark': tooltipTheme === 'dark'
})}
>
<TooltipWrapperComp classPrefix={ns} classnames={cx} tooltip={tooltipObj}>
{this.renderBody()}
</TooltipWrapperComp>
);

View File

@ -82,7 +82,8 @@ export function calculatePosition(
overlayNode: any,
target: HTMLElement,
container: any,
padding: any = 0
padding: any = 0,
customOffset: [number, number] = [0, 0]
) {
const childOffset: any =
container.tagName === 'BODY'
@ -126,14 +127,14 @@ export function calculatePosition(
atX === 'left'
? childOffset.left
: atX === 'right'
? childOffset.left + childOffset.width
: childOffset.left + childOffset.width / 2;
? childOffset.left + childOffset.width
: childOffset.left + childOffset.width / 2;
positionTop =
atY === 'top'
? childOffset.top
: atY === 'bottom'
? childOffset.top + childOffset.height
: childOffset.top + childOffset.height / 2;
? childOffset.top + childOffset.height
: childOffset.top + childOffset.height / 2;
positionLeft -=
myX === 'left' ? 0 : myX === 'right' ? overlayWidth : overlayWidth / 2;
@ -141,8 +142,8 @@ export function calculatePosition(
myY === 'top'
? 0
: myY === 'bottom'
? overlayHeight
: overlayHeight / 2;
? overlayHeight
: overlayHeight / 2;
// 如果还有其他可选项,则做位置判断,是否在可视区域,不完全在则继续看其他定位情况。
if (tests.length) {
@ -215,12 +216,12 @@ export function calculatePosition(
`calcOverlayPosition(): No such placement of "${placement}" found.`
);
}
const [offSetX = 0, offSetY = 0] = customOffset;
return {
positionLeft: positionLeft / scaleX,
positionTop: positionTop / scaleY,
arrowOffsetLeft: arrowOffsetLeft / scaleX,
arrowOffsetTop: arrowOffsetTop / scaleY,
positionLeft: (positionLeft + offSetX) / scaleX,
positionTop: (positionTop + offSetY) / scaleY,
arrowOffsetLeft: (arrowOffsetLeft + offSetX) / scaleX,
arrowOffsetTop: (arrowOffsetTop + offSetY) / scaleY,
activePlacement
};
}