Skip to content

refactor(Tooltip): 重构Tooltip #244

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 7 additions & 10 deletions packages/devui-vue/devui/tooltip/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import type { App } from 'vue'
import Tooltip from './src/tooltip'
import type { App } from 'vue';
import Tooltip from './src/tooltip';
export * from './src/tooltip-types';

Tooltip.install = function (app: App) {
app.component(Tooltip.name, Tooltip)
}

export { Tooltip }
export { Tooltip };

export default {
title: 'Tooltip提示',
category: '反馈',
status: '70%',
install(app: App): void {
app.use(Tooltip as any)
}
}
app.component(Tooltip.name, Tooltip);
},
};
39 changes: 24 additions & 15 deletions packages/devui-vue/devui/tooltip/src/tooltip-types.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,36 @@
import type { ExtractPropTypes } from 'vue'
import type { ComputedRef, ExtractPropTypes, PropType, Ref } from 'vue';

export type TTooltip = 'top' | 'right' | 'bottom' | 'left';
export type BasePlacement = 'top' | 'right' | 'bottom' | 'left';

export const tooltipProps = {
position: {
content: {
type: String,
default: 'top'
default: '',
},
position: {
type: [String, Array] as PropType<BasePlacement | Array<BasePlacement>>,
default: 'top',
},
showAnimation: {
type: Boolean,
default: true
default: true,
},
content: {
type: String
mouseEnterDelay: {
type: Number,
default: 150,
},
mouseLeaveDelay: {
type: String,
default: '150'
type: Number,
default: 100,
},
mouseEnterDelay: {
type: String,
default: '100'
}
} as const
};

export type TooltipProps = ExtractPropTypes<typeof tooltipProps>;

export type TooltipProps = ExtractPropTypes<typeof tooltipProps>
export type UseTooltipFn = {
visible: Ref<boolean>;
placement: Ref<BasePlacement>;
positionArr: ComputedRef<BasePlacement[]>;
overlayStyles: ComputedRef<Record<string, string>>;
onPositionChange: (pos: BasePlacement) => void;
};
98 changes: 75 additions & 23 deletions packages/devui-vue/devui/tooltip/src/tooltip.scss
Original file line number Diff line number Diff line change
@@ -1,28 +1,80 @@
@import '../../style/theme/color';
@import '../../styles-var/devui-var.scss';

.devui-tooltip-reference {
display: inline-block;
}

.devui-tooltip {
box-sizing: border-box;

.tooltip {
box-sizing: border-box;
position: absolute;
width: fit-content;
transition: all 0.5s;

.arrow {
width: 0;
height: 0;
position: absolute;
}

.tooltipcontent {
box-sizing: border-box;
padding: 10px;
margin-left: 10px;
border-radius: 4px;
width: fit-content;
background-color: $devui-feedback-overlay-bg;
color: $devui-feedback-overlay-text;
max-width: 200px;
min-height: 26px;
padding: 4px 16px;
font-size: $devui-font-size;
color: $devui-feedback-overlay-text;
letter-spacing: 0;
line-height: 1.5;
background: $devui-feedback-overlay-bg;
box-shadow: none;
overflow-wrap: break-word;
word-break: break-word;
word-wrap: break-word;
text-align: start;
border-radius: $devui-border-radius-feedback;
font-style: normal;
font-weight: normal;
line-break: auto;
text-decoration: none;
text-shadow: none;
text-transform: none;
word-spacing: normal;
white-space: normal;
opacity: 1;
z-index: $devui-z-index-pop-up;
}

.devui-tooltip-fade {
&-bottom,
&-top {
&-enter-from,
&-leave-to {
opacity: 0.8;
transform: scaleY(0.8);
}

&-enter-to,
&-leave-from {
opacity: 1;
transform: scaleY(1);
}

&-enter-active {
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);
}

&-leave-active {
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);
}
}

&-left,
&-right {
&-enter-from,
&-leave-to {
opacity: 0.8;
transform: scaleX(0.8);
}

&-enter-to,
&-leave-from {
opacity: 1;
transform: scaleX(1);
}

&-enter-active {
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);
}

&-leave-active {
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);
}
}
}
137 changes: 34 additions & 103 deletions packages/devui-vue/devui/tooltip/src/tooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,109 +1,40 @@
import { defineComponent, reactive, ref, onMounted, onBeforeUnmount, renderSlot, useSlots} from 'vue'
import { tooltipProps } from './tooltip-types'
import './tooltip.scss'
import { defineComponent, ref, Teleport, toRefs, Transition } from 'vue';
import { FlexibleOverlay } from '../../overlay';
import { TooltipProps, tooltipProps } from './tooltip-types';
import { useTooltip } from './use-tooltip';
import './tooltip.scss';

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

const slotElement = ref(null)
onMounted(()=>{
slotElement.value.children[0].onmouseenter = function (){
// 创建tooltip元素(外层容器、箭头、内容)
const tooltip = document.createElement('div')
const arrow = document.createElement('div')
const tooltipcontent = document.createElement('div')
// 设置tooltip的样式
tooltip.classList.add('tooltip')
arrow.classList.add('arrow')
tooltipcontent.classList.add('tooltipcontent')
// 设置tooltip的id
arrow.id = 'devui-arrow'
tooltip.id = 'devui-tooltip1'

setTimeout(() => {
document.getElementById('devui-tooltip').appendChild(tooltip)
tooltip.appendChild(arrow)
tooltip.appendChild(tooltipcontent)
tooltipcontent.innerHTML = props.content

tooltip.style.opacity = '1'
tooltip.style.zIndex = '999'
arrow.style.border = '5px solid transparent'
switch(props.position){
case 'top':
position.left = (slotElement.value.children[0].offsetLeft - tooltip.offsetWidth / 2 + slotElement.value.children[0].offsetWidth / 2) - 5;
position.top = slotElement.value.children[0].offsetTop - 10 - tooltipcontent.offsetHeight
// 设置箭头的样式
// arrowStyle('borderTop', '5px solid rgb(70, 77, 110)')
arrow.style.top = `${tooltipcontent.offsetHeight}px`
arrow.style.left = `${tooltipcontent.offsetWidth/2 + 5}px`
arrow.style.borderTop = '5px solid rgb(70, 77, 110)'
break;
case 'bottom':
position.top = slotElement.value.children[0].offsetHeight + slotElement.value.children[0].offsetTop + 10
position.left = (slotElement.value.children[0].offsetLeft + slotElement.value.children[0].offsetWidth/2 - tooltipcontent.offsetWidth/2) - 5;
// 设置arrow.value的样式
arrowStyle('borderBottom', '5px solid rgb(70, 77, 110)')
arrow.style.top = '-10px'
arrow.style.left = `${tooltipcontent.offsetWidth/2 + 5}px`
arrow.style.borderBottom = '5px solid rgb(70, 77, 110)'
break;
case 'left':
position.top = slotElement.value.children[0].offsetTop + slotElement.value.children[0].offsetHeight/2 - tooltipcontent.offsetHeight/2
position.left = slotElement.value.children[0].offsetLeft - 20 - tooltipcontent.offsetWidth
// 设置arrow.value的样式
arrowStyle('borderLeft', '5px solid rgb(70, 77, 110)')
arrow.style.left = `${tooltipcontent.offsetWidth + 10}px`
arrow.style.top = `${tooltipcontent.offsetHeight/2 - 5}px`
arrow.style.borderLeft = '5px solid rgb(70, 77, 110)'
break;
case 'right':
// 设置tooltip 内容的样式
position.left = slotElement.value.children[0].offsetLeft + slotElement.value.children[0].offsetWidth
position.top = slotElement.value.children[0].offsetTop + slotElement.value.children[0].offsetHeight/2 - tooltipcontent.offsetHeight/2
// 设置箭头的样式
arrowStyle('borderRight', '5px solid rgb(70, 77, 110)')
arrow.style.top = `${tooltipcontent.offsetHeight/2 - 5}px`
arrow.style.left = '-0px'
arrow.style.borderRight = '5px solid rgb(70, 77, 110)'
break;
}
tooltip.style.top = position.top + 5 + 'px'
tooltip.style.left = position.left + 'px'
}, props.mouseEnterDelay)
}
slotElement.value.children[0].onmouseleave = function (){
setTimeout(() => {
document.getElementById('devui-tooltip1').removeChild(document.getElementById('devui-arrow'))
document.getElementById('devui-tooltip').removeChild(document.getElementById('devui-tooltip1'))
}, props.mouseLeaveDelay)
}
})

onBeforeUnmount (()=>{
slotElement.value.children[0].onmouseenter = null
slotElement.value.children[0].onmouseleave = null
})

return ()=> {
const defaultSlot = renderSlot(useSlots(), 'default')
return (
<div class="devui-tooltip" id='devui-tooltip'>
<div class='slotElement' ref={slotElement}>
{defaultSlot}
</div>
return () => (
<>
<div ref={origin} class='devui-tooltip-reference'>
{slots.default?.()}
</div>
)
}
}
});
<Teleport to='body'>
<Transition name={showAnimation.value ? `devui-tooltip-fade-${placement.value}` : ''}>
<FlexibleOverlay
v-model={visible.value}
ref={tooltipRef}
class='devui-tooltip'
origin={origin.value}
position={positionArr.value}
offset={6}
show-arrow
style={overlayStyles.value}
onPositionChange={onPositionChange}>
<span innerHTML={content.value}></span>
</FlexibleOverlay>
</Transition>
</Teleport>
</>
);
},
});
47 changes: 47 additions & 0 deletions packages/devui-vue/devui/tooltip/src/use-tooltip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { onMounted, ref, toRefs, computed } from 'vue';
import type { Ref } from 'vue';
import { debounce } from 'lodash';
import { TooltipProps, BasePlacement, UseTooltipFn } from './tooltip-types';

const TransformOriginMap: Record<string, string> = {
top: '50% calc(100% + 8px)',
bottom: '50% -8px',
left: 'calc(100% + 8px)',
right: '-8px 50%',
};

export function useTooltip(origin: Ref, props: TooltipProps): UseTooltipFn {
const { position, mouseEnterDelay, mouseLeaveDelay } = toRefs(props);
const visible = ref<boolean>(false);
const isEnter = ref<boolean>(false);
const positionArr = computed(() => (typeof position.value === 'string' ? [position.value] : position.value));
const placement = ref<BasePlacement>(positionArr.value[0]);
const overlayStyles = computed(() => ({
transformOrigin: TransformOriginMap[placement.value],
}));
const enter = debounce(() => {
isEnter.value && (visible.value = true);
}, mouseEnterDelay.value);
const leave = debounce(() => {
!isEnter.value && (visible.value = false);
}, mouseLeaveDelay.value);

const onMouseenter = () => {
isEnter.value = true;
enter();
};
const onMouseleave = () => {
isEnter.value = false;
leave();
};
const onPositionChange = (pos: BasePlacement) => {
placement.value = pos;
};

onMounted(() => {
origin.value.addEventListener('mouseenter', onMouseenter);
origin.value.addEventListener('mouseleave', onMouseleave);
});

return { visible, placement, positionArr, overlayStyles, onPositionChange };
}
Loading