first commit
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user