Skip to content

Commit 9cd4877

Browse files
authored
refactor(Tooltip): 重构Tooltip (#244)
1 parent eff09a9 commit 9cd4877

File tree

6 files changed

+237
-186
lines changed

6 files changed

+237
-186
lines changed
Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
1-
import type { App } from 'vue'
2-
import Tooltip from './src/tooltip'
1+
import type { App } from 'vue';
2+
import Tooltip from './src/tooltip';
3+
export * from './src/tooltip-types';
34

4-
Tooltip.install = function (app: App) {
5-
app.component(Tooltip.name, Tooltip)
6-
}
7-
8-
export { Tooltip }
5+
export { Tooltip };
96

107
export default {
118
title: 'Tooltip提示',
129
category: '反馈',
1310
status: '70%',
1411
install(app: App): void {
15-
app.use(Tooltip as any)
16-
}
17-
}
12+
app.component(Tooltip.name, Tooltip);
13+
},
14+
};
Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,36 @@
1-
import type { ExtractPropTypes } from 'vue'
1+
import type { ComputedRef, ExtractPropTypes, PropType, Ref } from 'vue';
22

3-
export type TTooltip = 'top' | 'right' | 'bottom' | 'left';
3+
export type BasePlacement = 'top' | 'right' | 'bottom' | 'left';
44

55
export const tooltipProps = {
6-
position: {
6+
content: {
77
type: String,
8-
default: 'top'
8+
default: '',
9+
},
10+
position: {
11+
type: [String, Array] as PropType<BasePlacement | Array<BasePlacement>>,
12+
default: 'top',
913
},
1014
showAnimation: {
1115
type: Boolean,
12-
default: true
16+
default: true,
1317
},
14-
content: {
15-
type: String
18+
mouseEnterDelay: {
19+
type: Number,
20+
default: 150,
1621
},
1722
mouseLeaveDelay: {
18-
type: String,
19-
default: '150'
23+
type: Number,
24+
default: 100,
2025
},
21-
mouseEnterDelay: {
22-
type: String,
23-
default: '100'
24-
}
25-
} as const
26+
};
27+
28+
export type TooltipProps = ExtractPropTypes<typeof tooltipProps>;
2629

27-
export type TooltipProps = ExtractPropTypes<typeof tooltipProps>
30+
export type UseTooltipFn = {
31+
visible: Ref<boolean>;
32+
placement: Ref<BasePlacement>;
33+
positionArr: ComputedRef<BasePlacement[]>;
34+
overlayStyles: ComputedRef<Record<string, string>>;
35+
onPositionChange: (pos: BasePlacement) => void;
36+
};
Lines changed: 75 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,80 @@
1-
@import '../../style/theme/color';
1+
@import '../../styles-var/devui-var.scss';
2+
3+
.devui-tooltip-reference {
4+
display: inline-block;
5+
}
26

37
.devui-tooltip {
4-
box-sizing: border-box;
5-
6-
.tooltip {
7-
box-sizing: border-box;
8-
position: absolute;
9-
width: fit-content;
10-
transition: all 0.5s;
11-
12-
.arrow {
13-
width: 0;
14-
height: 0;
15-
position: absolute;
16-
}
17-
18-
.tooltipcontent {
19-
box-sizing: border-box;
20-
padding: 10px;
21-
margin-left: 10px;
22-
border-radius: 4px;
23-
width: fit-content;
24-
background-color: $devui-feedback-overlay-bg;
25-
color: $devui-feedback-overlay-text;
8+
max-width: 200px;
9+
min-height: 26px;
10+
padding: 4px 16px;
11+
font-size: $devui-font-size;
12+
color: $devui-feedback-overlay-text;
13+
letter-spacing: 0;
14+
line-height: 1.5;
15+
background: $devui-feedback-overlay-bg;
16+
box-shadow: none;
17+
overflow-wrap: break-word;
18+
word-break: break-word;
19+
word-wrap: break-word;
20+
text-align: start;
21+
border-radius: $devui-border-radius-feedback;
22+
font-style: normal;
23+
font-weight: normal;
24+
line-break: auto;
25+
text-decoration: none;
26+
text-shadow: none;
27+
text-transform: none;
28+
word-spacing: normal;
29+
white-space: normal;
30+
opacity: 1;
31+
z-index: $devui-z-index-pop-up;
32+
}
33+
34+
.devui-tooltip-fade {
35+
&-bottom,
36+
&-top {
37+
&-enter-from,
38+
&-leave-to {
39+
opacity: 0.8;
40+
transform: scaleY(0.8);
41+
}
42+
43+
&-enter-to,
44+
&-leave-from {
45+
opacity: 1;
46+
transform: scaleY(1);
47+
}
48+
49+
&-enter-active {
50+
transition: transform 0.1s cubic-bezier(0.16, 0.75, 0.5, 1), opacity 0.1s cubic-bezier(0.16, 0.75, 0.5, 1);
51+
}
52+
53+
&-leave-active {
54+
transition: transform 0.1s cubic-bezier(0.5, 0, 0.84, 0.25), opacity 0.1s cubic-bezier(0.5, 0, 0.84, 0.25);
55+
}
56+
}
57+
58+
&-left,
59+
&-right {
60+
&-enter-from,
61+
&-leave-to {
62+
opacity: 0.8;
63+
transform: scaleX(0.8);
64+
}
65+
66+
&-enter-to,
67+
&-leave-from {
68+
opacity: 1;
69+
transform: scaleX(1);
70+
}
71+
72+
&-enter-active {
73+
transition: transform 0.1s cubic-bezier(0.16, 0.75, 0.5, 1), opacity 0.1s cubic-bezier(0.16, 0.75, 0.5, 1);
74+
}
75+
76+
&-leave-active {
77+
transition: transform 0.1s cubic-bezier(0.5, 0, 0.84, 0.25), opacity 0.1s cubic-bezier(0.5, 0, 0.84, 0.25);
2678
}
2779
}
2880
}
Lines changed: 34 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,40 @@
1-
import { defineComponent, reactive, ref, onMounted, onBeforeUnmount, renderSlot, useSlots} from 'vue'
2-
import { tooltipProps } from './tooltip-types'
3-
import './tooltip.scss'
1+
import { defineComponent, ref, Teleport, toRefs, Transition } from 'vue';
2+
import { FlexibleOverlay } from '../../overlay';
3+
import { TooltipProps, tooltipProps } from './tooltip-types';
4+
import { useTooltip } from './use-tooltip';
5+
import './tooltip.scss';
46

57
export default defineComponent({
68
name: 'DTooltip',
79
props: tooltipProps,
8-
setup(props){
9-
const position = reactive({
10-
left: 0,
11-
top: 0
12-
})
13-
// 设置tooltip箭头的样式
14-
const arrowStyle = (attr, value)=>{
15-
document.getElementById('devui-arrow').style[attr] = value
16-
}
10+
setup(props: TooltipProps, { slots }) {
11+
const { showAnimation, content } = toRefs(props);
12+
const origin = ref<HTMLElement>();
13+
const tooltipRef = ref<HTMLElement>();
14+
const { visible, placement, positionArr, overlayStyles, onPositionChange } = useTooltip(origin, props);
1715

18-
const slotElement = ref(null)
19-
onMounted(()=>{
20-
slotElement.value.children[0].onmouseenter = function (){
21-
// 创建tooltip元素(外层容器、箭头、内容)
22-
const tooltip = document.createElement('div')
23-
const arrow = document.createElement('div')
24-
const tooltipcontent = document.createElement('div')
25-
// 设置tooltip的样式
26-
tooltip.classList.add('tooltip')
27-
arrow.classList.add('arrow')
28-
tooltipcontent.classList.add('tooltipcontent')
29-
// 设置tooltip的id
30-
arrow.id = 'devui-arrow'
31-
tooltip.id = 'devui-tooltip1'
32-
33-
setTimeout(() => {
34-
document.getElementById('devui-tooltip').appendChild(tooltip)
35-
tooltip.appendChild(arrow)
36-
tooltip.appendChild(tooltipcontent)
37-
tooltipcontent.innerHTML = props.content
38-
39-
tooltip.style.opacity = '1'
40-
tooltip.style.zIndex = '999'
41-
arrow.style.border = '5px solid transparent'
42-
switch(props.position){
43-
case 'top':
44-
position.left = (slotElement.value.children[0].offsetLeft - tooltip.offsetWidth / 2 + slotElement.value.children[0].offsetWidth / 2) - 5;
45-
position.top = slotElement.value.children[0].offsetTop - 10 - tooltipcontent.offsetHeight
46-
// 设置箭头的样式
47-
// arrowStyle('borderTop', '5px solid rgb(70, 77, 110)')
48-
arrow.style.top = `${tooltipcontent.offsetHeight}px`
49-
arrow.style.left = `${tooltipcontent.offsetWidth/2 + 5}px`
50-
arrow.style.borderTop = '5px solid rgb(70, 77, 110)'
51-
break;
52-
case 'bottom':
53-
position.top = slotElement.value.children[0].offsetHeight + slotElement.value.children[0].offsetTop + 10
54-
position.left = (slotElement.value.children[0].offsetLeft + slotElement.value.children[0].offsetWidth/2 - tooltipcontent.offsetWidth/2) - 5;
55-
// 设置arrow.value的样式
56-
arrowStyle('borderBottom', '5px solid rgb(70, 77, 110)')
57-
arrow.style.top = '-10px'
58-
arrow.style.left = `${tooltipcontent.offsetWidth/2 + 5}px`
59-
arrow.style.borderBottom = '5px solid rgb(70, 77, 110)'
60-
break;
61-
case 'left':
62-
position.top = slotElement.value.children[0].offsetTop + slotElement.value.children[0].offsetHeight/2 - tooltipcontent.offsetHeight/2
63-
position.left = slotElement.value.children[0].offsetLeft - 20 - tooltipcontent.offsetWidth
64-
// 设置arrow.value的样式
65-
arrowStyle('borderLeft', '5px solid rgb(70, 77, 110)')
66-
arrow.style.left = `${tooltipcontent.offsetWidth + 10}px`
67-
arrow.style.top = `${tooltipcontent.offsetHeight/2 - 5}px`
68-
arrow.style.borderLeft = '5px solid rgb(70, 77, 110)'
69-
break;
70-
case 'right':
71-
// 设置tooltip 内容的样式
72-
position.left = slotElement.value.children[0].offsetLeft + slotElement.value.children[0].offsetWidth
73-
position.top = slotElement.value.children[0].offsetTop + slotElement.value.children[0].offsetHeight/2 - tooltipcontent.offsetHeight/2
74-
// 设置箭头的样式
75-
arrowStyle('borderRight', '5px solid rgb(70, 77, 110)')
76-
arrow.style.top = `${tooltipcontent.offsetHeight/2 - 5}px`
77-
arrow.style.left = '-0px'
78-
arrow.style.borderRight = '5px solid rgb(70, 77, 110)'
79-
break;
80-
}
81-
tooltip.style.top = position.top + 5 + 'px'
82-
tooltip.style.left = position.left + 'px'
83-
}, props.mouseEnterDelay)
84-
}
85-
slotElement.value.children[0].onmouseleave = function (){
86-
setTimeout(() => {
87-
document.getElementById('devui-tooltip1').removeChild(document.getElementById('devui-arrow'))
88-
document.getElementById('devui-tooltip').removeChild(document.getElementById('devui-tooltip1'))
89-
}, props.mouseLeaveDelay)
90-
}
91-
})
92-
93-
onBeforeUnmount (()=>{
94-
slotElement.value.children[0].onmouseenter = null
95-
slotElement.value.children[0].onmouseleave = null
96-
})
97-
98-
return ()=> {
99-
const defaultSlot = renderSlot(useSlots(), 'default')
100-
return (
101-
<div class="devui-tooltip" id='devui-tooltip'>
102-
<div class='slotElement' ref={slotElement}>
103-
{defaultSlot}
104-
</div>
16+
return () => (
17+
<>
18+
<div ref={origin} class='devui-tooltip-reference'>
19+
{slots.default?.()}
10520
</div>
106-
)
107-
}
108-
}
109-
});
21+
<Teleport to='body'>
22+
<Transition name={showAnimation.value ? `devui-tooltip-fade-${placement.value}` : ''}>
23+
<FlexibleOverlay
24+
v-model={visible.value}
25+
ref={tooltipRef}
26+
class='devui-tooltip'
27+
origin={origin.value}
28+
position={positionArr.value}
29+
offset={6}
30+
show-arrow
31+
style={overlayStyles.value}
32+
onPositionChange={onPositionChange}>
33+
<span innerHTML={content.value}></span>
34+
</FlexibleOverlay>
35+
</Transition>
36+
</Teleport>
37+
</>
38+
);
39+
},
40+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { onMounted, ref, toRefs, computed } from 'vue';
2+
import type { Ref } from 'vue';
3+
import { debounce } from 'lodash';
4+
import { TooltipProps, BasePlacement, UseTooltipFn } from './tooltip-types';
5+
6+
const TransformOriginMap: Record<string, string> = {
7+
top: '50% calc(100% + 8px)',
8+
bottom: '50% -8px',
9+
left: 'calc(100% + 8px)',
10+
right: '-8px 50%',
11+
};
12+
13+
export function useTooltip(origin: Ref, props: TooltipProps): UseTooltipFn {
14+
const { position, mouseEnterDelay, mouseLeaveDelay } = toRefs(props);
15+
const visible = ref<boolean>(false);
16+
const isEnter = ref<boolean>(false);
17+
const positionArr = computed(() => (typeof position.value === 'string' ? [position.value] : position.value));
18+
const placement = ref<BasePlacement>(positionArr.value[0]);
19+
const overlayStyles = computed(() => ({
20+
transformOrigin: TransformOriginMap[placement.value],
21+
}));
22+
const enter = debounce(() => {
23+
isEnter.value && (visible.value = true);
24+
}, mouseEnterDelay.value);
25+
const leave = debounce(() => {
26+
!isEnter.value && (visible.value = false);
27+
}, mouseLeaveDelay.value);
28+
29+
const onMouseenter = () => {
30+
isEnter.value = true;
31+
enter();
32+
};
33+
const onMouseleave = () => {
34+
isEnter.value = false;
35+
leave();
36+
};
37+
const onPositionChange = (pos: BasePlacement) => {
38+
placement.value = pos;
39+
};
40+
41+
onMounted(() => {
42+
origin.value.addEventListener('mouseenter', onMouseenter);
43+
origin.value.addEventListener('mouseleave', onMouseleave);
44+
});
45+
46+
return { visible, placement, positionArr, overlayStyles, onPositionChange };
47+
}

0 commit comments

Comments
 (0)