Skip to content

Commit 178b901

Browse files
authored
fix(tooltip): replace popover with custom (#132)
1 parent 5eda65d commit 178b901

File tree

9 files changed

+212
-17
lines changed

9 files changed

+212
-17
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@indielayer/ui": patch
3+
---
4+
5+
fix(tooltip): replace popover with custom

packages/ui/src/components/tooltip/ToggleTip.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const computedIcon = computed(() => props.icon || infoIcon)
2222
</script>
2323

2424
<template>
25-
<x-tooltip placement="auto">
25+
<x-tooltip position="right">
2626
<x-icon :icon="computedIcon" class="text-secondary-500 dark:text-secondary-300 cursor-pointer" />
2727
<template #tooltip>
2828
<div v-html="content"></div>
Lines changed: 185 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,207 @@
11
<script lang="ts">
22
const tooltipProps = {
3+
tag: {
4+
type: String,
5+
default: 'div',
6+
},
37
tooltip: String,
4-
placement: {
5-
type: String as PropType<PopoverPlacement>,
8+
disabled: Boolean,
9+
position: {
10+
type: String as PropType<TooltipPosition>,
611
default: 'bottom',
712
},
813
}
914
10-
export type PopoverPlacement = Placement
15+
export type TooltipPosition = 'top' | 'bottom' | 'left' | 'right'
1116
1217
export type TooltipProps = ExtractPublicPropTypes<typeof tooltipProps>
1318
19+
type InternalClasses = 'tooltip'
20+
export interface TooltipTheme extends ThemeComponent<TooltipProps, InternalClasses> {}
21+
1422
export default { name: 'XTooltip' }
1523
</script>
1624

1725
<script setup lang="ts">
26+
import { computed, ref, useSlots } from 'vue'
1827
import type { ExtractPublicPropTypes, PropType } from 'vue'
19-
import XPopover from '../../components/popover/Popover.vue'
20-
import XPopoverContainer from '../../components/popover/PopoverContainer.vue'
21-
import { type Placement } from 'floating-vue'
28+
import { useTheme, type ThemeComponent } from '../../composables/useTheme'
29+
30+
const props = defineProps(tooltipProps)
31+
32+
const triggerRef = ref<HTMLElement | null>(null)
33+
const tooltipRef = ref<HTMLElement | null>(null)
34+
const isVisible = ref(false)
35+
const tooltipStyle = ref<Record<string, string>>({})
36+
const actualPosition = ref<TooltipPosition>(props.position)
37+
38+
const slots = useSlots()
39+
const hasTooltip = computed(() => props.tooltip || slots.tooltip)
40+
41+
const computedDisabled = computed(() => props.disabled || !hasTooltip.value)
42+
43+
const arrowPositionClasses = computed(() => {
44+
const placements = {
45+
top: '-bottom-2.5 left-1/2 -translate-x-1/2 w-3.5',
46+
bottom: '-top-2.5 left-1/2 -translate-x-1/2 w-3.5',
47+
left: '-right-2.5 top-1/2 -translate-y-1/2 h-3.5',
48+
right: '-left-2.5 top-1/2 -translate-y-1/2 h-3.5',
49+
}
50+
51+
return placements[actualPosition.value]
52+
})
53+
54+
const arrowRotationClasses = computed(() => {
55+
const placements = {
56+
top: '-rotate-45 origin-top-left',
57+
bottom: 'rotate-45 origin-bottom-left',
58+
left: 'rotate-45 origin-top-left',
59+
right: '-rotate-45 origin-top-right',
60+
}
61+
62+
return placements[actualPosition.value]
63+
})
64+
65+
const animationOriginClasses = computed(() => {
66+
const origins = {
67+
top: 'origin-bottom',
68+
bottom: 'origin-top',
69+
left: 'origin-right',
70+
right: 'origin-left',
71+
}
72+
73+
return origins[actualPosition.value]
74+
})
75+
76+
const calculatePosition = () => {
77+
if (!triggerRef.value || !tooltipRef.value) return
2278
23-
defineProps(tooltipProps)
79+
const triggerRect = triggerRef.value.getBoundingClientRect()
80+
const tooltipRect = tooltipRef.value.getBoundingClientRect()
81+
const spacing = 8 // 0.5rem = 8px
82+
const viewportWidth = window.innerWidth
83+
const viewportHeight = window.innerHeight
84+
85+
let position = props.position
86+
let top = 0
87+
let left = 0
88+
89+
// Calculate initial position based on placement
90+
switch (position) {
91+
case 'top':
92+
top = triggerRect.top - tooltipRect.height - spacing
93+
left = triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2
94+
// Check if tooltip goes off-screen top
95+
if (top < 0) {
96+
position = 'bottom'
97+
top = triggerRect.bottom + spacing
98+
}
99+
break
100+
case 'bottom':
101+
top = triggerRect.bottom + spacing
102+
left = triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2
103+
// Check if tooltip goes off-screen bottom
104+
if (top + tooltipRect.height > viewportHeight) {
105+
position = 'top'
106+
top = triggerRect.top - tooltipRect.height - spacing
107+
}
108+
break
109+
case 'left':
110+
top = triggerRect.top + triggerRect.height / 2 - tooltipRect.height / 2
111+
left = triggerRect.left - tooltipRect.width - spacing
112+
// Check if tooltip goes off-screen left
113+
if (left < 0) {
114+
position = 'right'
115+
left = triggerRect.right + spacing
116+
}
117+
break
118+
case 'right':
119+
top = triggerRect.top + triggerRect.height / 2 - tooltipRect.height / 2
120+
left = triggerRect.right + spacing
121+
// Check if tooltip goes off-screen right
122+
if (left + tooltipRect.width > viewportWidth) {
123+
position = 'left'
124+
left = triggerRect.left - tooltipRect.width - spacing
125+
}
126+
break
127+
}
128+
129+
// Adjust horizontal position to stay within viewport
130+
if (position === 'top' || position === 'bottom') {
131+
if (left < 0) left = spacing
132+
if (left + tooltipRect.width > viewportWidth) {
133+
left = viewportWidth - tooltipRect.width - spacing
134+
}
135+
}
136+
137+
// Adjust vertical position to stay within viewport
138+
if (position === 'left' || position === 'right') {
139+
if (top < 0) top = spacing
140+
if (top + tooltipRect.height > viewportHeight) {
141+
top = viewportHeight - tooltipRect.height - spacing
142+
}
143+
}
144+
145+
actualPosition.value = position
146+
tooltipStyle.value = {
147+
top: `${top}px`,
148+
left: `${left}px`,
149+
}
150+
}
151+
152+
const showTooltip = () => {
153+
if (computedDisabled.value) return
154+
isVisible.value = true
155+
setTimeout(calculatePosition, 0)
156+
}
157+
158+
const hideTooltip = () => {
159+
if (computedDisabled.value) return
160+
isVisible.value = false
161+
}
162+
163+
const { classes, className } = useTheme('Tooltip', {}, props)
24164
</script>
25165

26166
<template>
27-
<x-popover :triggers="['hover', 'click']" class="inline-block" :hide-arrow="false" :placement="placement">
167+
<component
168+
:is="tag"
169+
ref="triggerRef"
170+
:class="[
171+
className,
172+
{
173+
'inline-block': !$attrs.class,
174+
}]"
175+
@mouseenter="showTooltip"
176+
@mouseleave="hideTooltip"
177+
>
28178
<slot></slot>
29-
<template #content>
30-
<div class="dark">
31-
<x-popover-container class="p-2 text-white text-xs w-max max-w-xs">
32-
<slot name="tooltip">{{ tooltip }}</slot>
33-
</x-popover-container>
179+
</component>
180+
181+
<teleport v-if="!computedDisabled" to="body">
182+
<transition
183+
enter-active-class="transition-opacity duration-150 ease-out"
184+
leave-active-class="transition-opacity duration-150 ease-in"
185+
enter-from-class="opacity-0"
186+
enter-to-class="opacity-100"
187+
leave-from-class="opacity-100"
188+
leave-to-class="opacity-0"
189+
>
190+
<div
191+
v-if="isVisible"
192+
ref="tooltipRef"
193+
:style="tooltipStyle"
194+
class="fixed z-[9999] pointer-events-none"
195+
:class="[
196+
classes.tooltip,
197+
animationOriginClasses
198+
]"
199+
>
200+
<slot name="tooltip">{{ tooltip }}</slot>
201+
<div :class="['absolute overflow-hidden shadow-lg z-10', arrowPositionClasses]">
202+
<div :class="['h-2.5 w-2.5 bg-secondary-700 transform border border-secondary-800', arrowRotationClasses]"></div>
203+
</div>
34204
</div>
35-
</template>
36-
</x-popover>
205+
</transition>
206+
</teleport>
37207
</template>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export { default as XTooltip } from './Tooltip.vue'
2-
export type { TooltipProps } from './Tooltip.vue'
2+
export type { TooltipProps, TooltipTheme } from './Tooltip.vue'
33

44
export { default as XToggleTip } from './ToggleTip.vue'
55
export type { ToggleTipProps } from './ToggleTip.vue'
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { TooltipTheme } from '../Tooltip.vue'
2+
3+
const theme: TooltipTheme = {
4+
classes: {
5+
tooltip: 'bg-secondary-700 shadow-lg rounded-md border border-secondary-800 p-2 text-white text-xs w-max max-w-sm',
6+
},
7+
}
8+
9+
export default theme
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { TooltipTheme } from '../Tooltip.vue'
2+
3+
import BaseTheme from './Tooltip.base.theme'
4+
5+
const theme: TooltipTheme = BaseTheme
6+
7+
export default theme

packages/ui/src/theme.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import type {
5151
TagTheme,
5252
TextareaTheme,
5353
ToggleTheme,
54+
TooltipTheme,
5455
UploadTheme,
5556
} from './components'
5657

@@ -106,6 +107,7 @@ export type ComponentThemes = {
106107
Tag: TagTheme;
107108
Textarea: TextareaTheme;
108109
Toggle: ToggleTheme;
110+
Tooltip: TooltipTheme;
109111
Upload: UploadTheme;
110112
}
111113

packages/ui/src/themes/base/components.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,5 @@ export { default as TableRow } from '../../components/table/theme/TableRow.base.
4949
export { default as Tag } from '../../components/tag/theme/Tag.base.theme'
5050
export { default as Textarea } from '../../components/textarea/theme/Textarea.base.theme'
5151
export { default as Toggle } from '../../components/toggle/theme/Toggle.base.theme'
52+
export { default as Tooltip } from '../../components/tooltip/theme/Tooltip.base.theme'
5253
export { default as Upload } from '../../components/upload/theme/Upload.base.theme'

packages/ui/src/themes/carbon/components.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,5 @@ export { default as TableRow } from '../../components/table/theme/TableRow.carbo
4949
export { default as Tag } from '../../components/tag/theme/Tag.carbon.theme'
5050
export { default as Textarea } from '../../components/textarea/theme/Textarea.carbon.theme'
5151
export { default as Toggle } from '../../components/toggle/theme/Toggle.carbon.theme'
52+
export { default as Tooltip } from '../../components/tooltip/theme/Tooltip.carbon.theme'
5253
export { default as Upload } from '../../components/upload/theme/Upload.carbon.theme'

0 commit comments

Comments
 (0)