Skip to content

Commit 694f928

Browse files
committed
feat: slider tooltip with spring snap, tilt animation, and drag fixes
1 parent 95ace78 commit 694f928

5 files changed

Lines changed: 134 additions & 95 deletions

File tree

src/components/atoms/tooltip/Tooltip.vue

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<div
33
ref="target"
4-
class="relative flex w-fit flex-col items-center text-typography-default"
4+
class="flex w-fit flex-col items-center text-typography-default"
55
@mouseleave="mouseleave"
66
@mouseenter="mouseenter"
77
>
@@ -32,7 +32,7 @@
3232

3333
<script lang="ts" setup>
3434
import type { TooltipProps } from "./types";
35-
import { ref, watch, nextTick, type ComponentPublicInstance } from "vue";
35+
import { ref, watch, nextTick, onUnmounted, type ComponentPublicInstance } from "vue";
3636
import { motion, AnimatePresence } from "motion-v";
3737
3838
const props = withDefaults(defineProps<TooltipProps>(), {
@@ -43,17 +43,56 @@ const tooltip = ref(null as ComponentPublicInstance | null);
4343
const target = ref(null as HTMLDivElement | null);
4444
const tooltipStyle = ref<Record<string, string>>({ position: "fixed", top: "-9999px", left: "-9999px" });
4545
const arrowStyle = ref<Record<string, string>>({});
46+
47+
let unsubscribeRotation: (() => void) | null = null;
48+
watch(() => props.rotateValue, (mv) => {
49+
unsubscribeRotation?.();
50+
unsubscribeRotation = null;
51+
if (!mv) return;
52+
unsubscribeRotation = mv.on("change", (v) => {
53+
const el = tooltip.value?.$el as HTMLElement | undefined;
54+
if (el) {
55+
el.style.transformOrigin = "50% calc(100% + 10px)";
56+
el.style.rotate = `${v}deg`;
57+
}
58+
});
59+
}, { immediate: true });
4660
const isVisible = ref(false);
61+
const isHovered = ref(false);
62+
63+
const MIN_VISIBLE_MS = 1000;
64+
65+
let hideTimeout: ReturnType<typeof setTimeout> | null = null;
4766
4867
const mouseenter = () => {
68+
isHovered.value = true;
69+
if (hideTimeout) { clearTimeout(hideTimeout); hideTimeout = null; }
4970
isVisible.value = true;
5071
nextTick(() => requestAnimationFrame(calculateTooltipPosition));
5172
};
5273
5374
const mouseleave = () => {
54-
isVisible.value = false;
75+
isHovered.value = false;
76+
if (!props.forceVisible && !hideTimeout) {
77+
isVisible.value = false;
78+
}
5579
};
5680
81+
watch(() => props.forceVisible, (val) => {
82+
if (val) {
83+
if (hideTimeout) { clearTimeout(hideTimeout); hideTimeout = null; }
84+
isVisible.value = true;
85+
nextTick(() => requestAnimationFrame(calculateTooltipPosition));
86+
} else {
87+
if (hideTimeout) { clearTimeout(hideTimeout); hideTimeout = null; }
88+
hideTimeout = setTimeout(() => { isVisible.value = false; hideTimeout = null; }, MIN_VISIBLE_MS);
89+
}
90+
}, { flush: 'sync' });
91+
onUnmounted(() => {
92+
unsubscribeRotation?.();
93+
if (hideTimeout) clearTimeout(hideTimeout);
94+
});
95+
5796
5897
const calculateTooltipPosition = () => {
5998
const offset = 10; // Offset between the tooltip and the target element
@@ -98,9 +137,18 @@ const calculateTooltipPosition = () => {
98137
left: `${left!}px`,
99138
position: "fixed"
100139
};
140+
141+
// Also set directly on the DOM element to avoid Vue's render-cycle lag
142+
// (important when called from rAF loops during CSS transitions)
143+
if (tooltipEl) {
144+
tooltipEl.style.top = `${top!}px`;
145+
tooltipEl.style.left = `${left!}px`;
146+
}
101147
};
102148
103149
watch(() => [target.value, props.position], calculateTooltipPosition);
150+
151+
defineExpose({ recalculate: calculateTooltipPosition });
104152
</script>
105153

106154
<style lang="scss" scoped>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
import type { MotionValue } from "motion-v";
2+
13
export interface TooltipProps {
24
content: string;
35
position?: "top" | "bottom" | "left" | "right";
6+
rotateValue?: MotionValue<number>;
7+
forceVisible?: boolean;
48
}

src/components/molecules/slider/Slider.stories.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const Primary: Story = {
3939
labelLeft: "Min",
4040
labelRight: "Max",
4141
labelMid: "",
42-
positions: 5,
42+
positions: 2,
4343
minPosition: 25,
4444
midPosition: undefined,
4545
maxPosition: 150,

0 commit comments

Comments
 (0)