@wishbone-media/spark 0.40.0 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wishbone-media/spark",
3
- "version": "0.40.0",
3
+ "version": "0.41.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -22,6 +22,20 @@
22
22
  white-space: pre-line;
23
23
  }
24
24
 
25
+ /* Interactive tooltips: allow hovering/clicking inside the tooltip */
26
+ .spark-tooltip-interactive {
27
+ pointer-events: auto;
28
+ }
29
+
30
+ .spark-tooltip-interactive a {
31
+ color: #93c5fd; /* blue-300 */
32
+ text-decoration: underline;
33
+ }
34
+
35
+ .spark-tooltip-interactive a:hover {
36
+ color: #bfdbfe; /* blue-200 */
37
+ }
38
+
25
39
  .spark-tooltip-arrow {
26
40
  position: absolute;
27
41
  width: 8px;
@@ -30,21 +44,17 @@
30
44
  transform: rotate(45deg);
31
45
  }
32
46
 
33
- /* Transitions (for SparkTooltip component) */
47
+ /* Transitions (for SparkTooltip component) — opacity only to avoid
48
+ conflicting with @floating-ui's transform: translate() positioning */
34
49
  .spark-tooltip-enter-active {
35
- transition:
36
- opacity 150ms ease,
37
- transform 150ms ease;
50
+ transition: opacity 150ms ease;
38
51
  }
39
52
 
40
53
  .spark-tooltip-leave-active {
41
- transition:
42
- opacity 100ms ease,
43
- transform 100ms ease;
54
+ transition: opacity 100ms ease;
44
55
  }
45
56
 
46
57
  .spark-tooltip-enter-from,
47
58
  .spark-tooltip-leave-to {
48
59
  opacity: 0;
49
- transform: scale(0.95);
50
60
  }
@@ -3,9 +3,9 @@
3
3
  ref="referenceRef"
4
4
  class="inline-flex"
5
5
  @mouseenter="show"
6
- @mouseleave="hide"
6
+ @mouseleave="startHide"
7
7
  @focusin="show"
8
- @focusout="hide"
8
+ @focusout="startHide"
9
9
  >
10
10
  <slot />
11
11
  </div>
@@ -16,8 +16,10 @@
16
16
  v-if="isVisible"
17
17
  ref="floatingRef"
18
18
  :style="floatingStyles"
19
- class="spark-tooltip"
19
+ :class="['spark-tooltip', { 'spark-tooltip-interactive': interactive }]"
20
20
  role="tooltip"
21
+ @mouseenter="onTooltipMouseenter"
22
+ @mouseleave="onTooltipMouseleave"
21
23
  >
22
24
  <slot name="content">
23
25
  <!-- eslint-disable-next-line vue/no-v-html -- html prop is opt-in, consumer responsibility -->
@@ -36,7 +38,8 @@
36
38
  * Wraps a trigger element (default slot) and displays a floating tooltip on hover/focus.
37
39
  *
38
40
  * For simple text tooltips, prefer the v-spark-tooltip directive.
39
- * Use this component when you need slot-based HTML content or complex tooltip layouts.
41
+ * Use this component when you need slot-based HTML content, interactive tooltips
42
+ * with clickable links, or complex tooltip layouts.
40
43
  */
41
44
  import { ref, computed, useSlots, onUnmounted } from 'vue'
42
45
  import {
@@ -84,6 +87,11 @@ const props = defineProps({
84
87
  type: Boolean,
85
88
  default: true,
86
89
  },
90
+ /** Allow hovering over the tooltip content (for clickable links, buttons, etc.) */
91
+ interactive: {
92
+ type: Boolean,
93
+ default: false,
94
+ },
87
95
  })
88
96
 
89
97
  const slots = useSlots()
@@ -96,6 +104,12 @@ const isVisible = ref(false)
96
104
  let showTimeout = null
97
105
  let hideTimeout = null
98
106
 
107
+ const hideDelay = computed(() => {
108
+ if (props.interactive)
109
+ return typeof props.delay === 'number' ? 150 : Math.max(props.delay.hide ?? 0, 150)
110
+ return typeof props.delay === 'number' ? 0 : (props.delay.hide ?? 0)
111
+ })
112
+
99
113
  const middleware = computed(() => {
100
114
  const mw = [offsetMiddleware(props.offset), flip(), shift({ padding: 8 })]
101
115
  if (props.showArrow) {
@@ -139,12 +153,23 @@ function show() {
139
153
  }, delay)
140
154
  }
141
155
 
142
- function hide() {
156
+ function startHide() {
143
157
  clearTimeout(showTimeout)
144
- const delay = typeof props.delay === 'number' ? 0 : (props.delay.hide ?? 0)
145
158
  hideTimeout = setTimeout(() => {
146
159
  isVisible.value = false
147
- }, delay)
160
+ }, hideDelay.value)
161
+ }
162
+
163
+ function onTooltipMouseenter() {
164
+ if (props.interactive) {
165
+ clearTimeout(hideTimeout)
166
+ }
167
+ }
168
+
169
+ function onTooltipMouseleave() {
170
+ if (props.interactive) {
171
+ startHide()
172
+ }
148
173
  }
149
174
 
150
175
  onUnmounted(() => {
@@ -24,13 +24,16 @@ function normalizeConfig(value) {
24
24
  html: false,
25
25
  delay: { show: 200, hide: 0 },
26
26
  arrow: true,
27
+ interactive: false,
27
28
  ...value,
28
29
  }
29
30
  }
30
31
 
31
32
  function createTooltipElement(config) {
32
33
  const tooltip = document.createElement('div')
33
- tooltip.className = TOOLTIP_CLASS
34
+ tooltip.className = config.interactive
35
+ ? `${TOOLTIP_CLASS} spark-tooltip-interactive`
36
+ : TOOLTIP_CLASS
34
37
  tooltip.setAttribute('role', 'tooltip')
35
38
 
36
39
  if (config.html) {
@@ -103,6 +106,16 @@ function showTooltip(el) {
103
106
  document.body.appendChild(tooltip)
104
107
  state.tooltipEl = tooltip
105
108
 
109
+ // Interactive: keep tooltip visible while hovering over it
110
+ if (state.config.interactive) {
111
+ tooltip.addEventListener('mouseenter', () => {
112
+ clearTimeout(state.hideTimeout)
113
+ })
114
+ tooltip.addEventListener('mouseleave', () => {
115
+ hideTooltip(el)
116
+ })
117
+ }
118
+
106
119
  updatePosition(el, tooltip, state.config)
107
120
  }, delay)
108
121
  }
@@ -113,7 +126,9 @@ function hideTooltip(el) {
113
126
 
114
127
  clearTimeout(state.showTimeout)
115
128
 
116
- const delay = typeof state.config.delay === 'number' ? 0 : (state.config.delay?.hide ?? 0)
129
+ let delay = typeof state.config.delay === 'number' ? 0 : (state.config.delay?.hide ?? 0)
130
+ // Interactive tooltips need minimum delay so user can move mouse to the tooltip
131
+ if (state.config.interactive) delay = Math.max(delay, 150)
117
132
 
118
133
  state.hideTimeout = setTimeout(() => {
119
134
  if (state.tooltipEl) {