199 lines
4.5 KiB
Vue
199 lines
4.5 KiB
Vue
<template>
|
|
<div
|
|
class="circle-button-wrap"
|
|
@mouseenter="onShow"
|
|
@mouseleave="onHide"
|
|
@focusin="onShow"
|
|
@focusout="onHide"
|
|
>
|
|
<button
|
|
ref="triggerRef"
|
|
:type="type"
|
|
class="circle-button"
|
|
:style="buttonStyle"
|
|
:disabled="disabled || loading"
|
|
@click="handleClick"
|
|
>
|
|
<slot name="icon" />
|
|
</button>
|
|
|
|
<Teleport to="body">
|
|
<Transition name="tooltip-fade">
|
|
<div
|
|
v-if="showTooltip && tooltip"
|
|
ref="tooltipRef"
|
|
class="circle-button__tooltip"
|
|
:class="`circle-button__tooltip--${actualPlacement}`"
|
|
:style="tooltipStyle"
|
|
role="tooltip"
|
|
>
|
|
{{ tooltip }}
|
|
<span class="circle-button__tooltip-arrow" :style="arrowStyle" />
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
|
import { arrow, autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom'
|
|
|
|
const props = defineProps({
|
|
type: { type: String, default: 'button' },
|
|
tooltip: { type: String, default: '' },
|
|
tooltipPosition: {
|
|
type: String,
|
|
default: 'top',
|
|
validator: (v) => ['top', 'bottom'].includes(v),
|
|
},
|
|
bgColor: { type: String, default: '#fff' },
|
|
size: { type: String, default: '2.75rem' },
|
|
disabled: { type: Boolean, default: false },
|
|
loading: { type: Boolean, default: false },
|
|
})
|
|
|
|
const emit = defineEmits(['click'])
|
|
|
|
const showTooltip = ref(false)
|
|
const triggerRef = ref(null)
|
|
const tooltipRef = ref(null)
|
|
const arrowEl = ref(null)
|
|
const tooltipStyle = ref({ position: 'fixed', top: '0', left: '0' })
|
|
const arrowStyle = ref({})
|
|
const actualPlacement = ref(props.tooltipPosition)
|
|
|
|
const buttonStyle = computed(() => ({
|
|
width: props.size,
|
|
height: props.size,
|
|
backgroundColor: props.bgColor,
|
|
}))
|
|
|
|
let cleanupAutoUpdate = null
|
|
|
|
const updatePosition = async () => {
|
|
if (!triggerRef.value || !tooltipRef.value) return
|
|
arrowEl.value = tooltipRef.value.querySelector('.circle-button__tooltip-arrow')
|
|
const middleware = [offset(6), flip(), shift({ padding: 8 })]
|
|
if (arrowEl.value) middleware.push(arrow({ element: arrowEl.value }))
|
|
|
|
const result = await computePosition(triggerRef.value, tooltipRef.value, {
|
|
placement: props.tooltipPosition,
|
|
strategy: 'fixed',
|
|
middleware,
|
|
})
|
|
|
|
tooltipStyle.value = {
|
|
position: 'fixed',
|
|
top: `${result.y}px`,
|
|
left: `${result.x}px`,
|
|
}
|
|
actualPlacement.value = result.placement
|
|
|
|
const arrowData = result.middlewareData.arrow
|
|
if (arrowEl.value && arrowData) {
|
|
const side = result.placement.split('-')[0]
|
|
const oppositeSide = { top: 'bottom', bottom: 'top', left: 'right', right: 'left' }[side]
|
|
arrowStyle.value = {
|
|
left: arrowData.x == null ? '' : `${arrowData.x}px`,
|
|
top: arrowData.y == null ? '' : `${arrowData.y}px`,
|
|
[oppositeSide]: '-0.25rem',
|
|
}
|
|
}
|
|
}
|
|
|
|
const startAutoUpdate = () => {
|
|
if (!triggerRef.value || !tooltipRef.value) return
|
|
cleanupAutoUpdate?.()
|
|
cleanupAutoUpdate = autoUpdate(triggerRef.value, tooltipRef.value, updatePosition)
|
|
}
|
|
|
|
const stopAutoUpdate = () => {
|
|
cleanupAutoUpdate?.()
|
|
cleanupAutoUpdate = null
|
|
}
|
|
|
|
watch(showTooltip, async (open) => {
|
|
if (open) {
|
|
await nextTick()
|
|
startAutoUpdate()
|
|
} else {
|
|
stopAutoUpdate()
|
|
}
|
|
})
|
|
|
|
onBeforeUnmount(stopAutoUpdate)
|
|
|
|
const onShow = () => {
|
|
if (!props.tooltip) return
|
|
showTooltip.value = true
|
|
}
|
|
const onHide = () => {
|
|
showTooltip.value = false
|
|
}
|
|
|
|
const handleClick = (event) => {
|
|
if (props.disabled || props.loading) {
|
|
event.preventDefault()
|
|
return
|
|
}
|
|
emit('click', event)
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.circle-button-wrap {
|
|
position: relative;
|
|
display: inline-block;
|
|
}
|
|
|
|
.circle-button {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: none;
|
|
border-radius: 9999px;
|
|
cursor: pointer;
|
|
padding: 0;
|
|
z-index: 20;
|
|
|
|
&:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<style lang="scss">
|
|
.circle-button__tooltip {
|
|
z-index: 1000;
|
|
white-space: nowrap;
|
|
padding: 0.25rem 0.625rem;
|
|
font-size: 0.75rem;
|
|
font-weight: 500;
|
|
color: #fff;
|
|
background-color: #000;
|
|
border-radius: 0.375rem;
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 5%);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.circle-button__tooltip-arrow {
|
|
position: absolute;
|
|
width: 0.5rem;
|
|
height: 0.5rem;
|
|
background-color: #000;
|
|
transform: rotate(45deg);
|
|
}
|
|
|
|
.tooltip-fade-enter-active,
|
|
.tooltip-fade-leave-active {
|
|
transition: opacity 0.15s ease;
|
|
}
|
|
|
|
.tooltip-fade-enter-from,
|
|
.tooltip-fade-leave-to {
|
|
opacity: 0;
|
|
}
|
|
</style>
|