Files
mareike/app/Views/Components/InfoIcon.vue

203 lines
4.3 KiB
Vue

<!-- InfoIcon.vue -->
<template>
<span
class="info-icon-wrapper"
role="button"
:aria-label="ariaLabel"
tabindex="0"
@mouseenter="open"
@mouseleave="close"
@focus="open"
@blur="close"
@keydown="onKeydown"
>
<slot name="icon">
<!-- default info SVG -->
<svg class="info-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="10" fill="currentColor" opacity="0.08"></circle>
<path d="M11 17h2v-6h-2v6zm0-8h2V7h-2v2z" fill="currentColor"></path>
</svg>
</slot>
<transition name="fade-scale">
<div
v-if="visible"
class="tooltip"
:class="positionClass"
role="tooltip"
:id="tooltipId"
>
<div class="tooltip-inner" v-html="text"></div>
<!-- small arrow -->
<div class="tooltip-arrow" aria-hidden="true"></div>
</div>
</transition>
</span>
</template>
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
text: { type: String, required: true }, // Tooltiptext (HTML erlaubt)
position: { type: String, default: 'top' }, // top | right | bottom | left
delay: { type: Number, default: 80 }, // ms - delay für Öffnen/Schließen (klein)
ariaLabel: { type: String, default: 'Info' }, // aria-label für das Icon (z.B. "Mehr Informationen")
})
const visible = ref(false)
let openTimer = null
let closeTimer = null
const tooltipId = `info-icon-tooltip-${Math.round(Math.random()*1e6)}`
const positionClass = computed(() => {
const p = props.position
return `pos-${p}`
})
function open() {
clearTimeout(closeTimer)
openTimer = setTimeout(() => (visible.value = true), props.delay)
}
function close() {
clearTimeout(openTimer)
closeTimer = setTimeout(() => (visible.value = false), props.delay)
}
function onKeydown(e) {
if (e.key === 'Escape' || e.key === 'Esc') {
visible.value = false
e.stopPropagation()
} else if (e.key === 'Enter' || e.key === ' ') {
// toggle on Enter / Space
e.preventDefault()
visible.value = !visible.value
}
}
</script>
<style scoped>
.info-icon-wrapper {
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
line-height: 0;
cursor: help;
outline: none;
}
/* focus ring */
.info-icon-wrapper:focus {
box-shadow: 0 0 0 4px rgba(50,115,220,0.12);
border-radius: 6px;
}
/* SVG sizing */
.info-icon {
display: block;
width: 18px;
height: 18px;
}
/* Tooltip baseline */
/* Tooltip baseline */
.tooltip {
position: absolute;
z-index: 999;
min-width: 180px; /* optional, sorgt für nicht zu kleinen Tooltip */
font-size: 13px;
line-height: 1.3;
padding: 8px 10px;
background: #59a3da;
color: #fff;
border-radius: 6px;
box-shadow: 0 6px 20px rgba(0,0,0,0.2);
transform-origin: center;
pointer-events: none;
white-space: normal; /* erlaubt Umbruch */
word-break: break-word; /* lange Wörter umbrechen */
}
/* Arrow */
.tooltip-arrow {
position: absolute;
width: 10px;
height: 10px;
transform: rotate(45deg);
background: inherit;
box-shadow: inherit;
filter: blur(0);
}
/* Positions */
.pos-top {
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
}
.pos-top .tooltip-arrow {
bottom: -5px;
left: 50%;
transform: translateX(-50%) rotate(45deg);
}
.pos-bottom {
top: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
}
.pos-bottom .tooltip-arrow {
top: -5px;
left: 50%;
transform: translateX(-50%) rotate(45deg);
}
.pos-left {
right: calc(100% + 8px);
top: 50%;
transform: translateY(-50%);
}
.pos-left .tooltip-arrow {
right: -5px;
top: 50%;
transform: translateY(-50%) rotate(45deg);
}
.pos-right {
left: calc(100% + 8px);
top: 50%;
transform: translateY(-50%);
}
.pos-right .tooltip-arrow {
left: -5px;
top: 50%;
transform: translateY(-50%) rotate(45deg);
}
/* inner tooltip text styling */
.tooltip-inner {
white-space: normal;
word-break: break-word;
}
/* enter/leave animation */
.fade-scale-enter-active,
.fade-scale-leave-active {
transition: opacity 160ms ease, transform 160ms ease;
}
.fade-scale-enter-from,
.fade-scale-leave-to {
opacity: 0;
transform: scale(0.95);
}
.fade-scale-enter-to,
.fade-scale-leave-from {
opacity: 1;
transform: scale(1);
}
</style>