@finsweet/webflow-apps-utils 1.0.14 → 1.0.15

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.
@@ -122,7 +122,7 @@
122
122
  let hasRightIcon = $derived(!!rightIcon);
123
123
  let hasText = $derived(!!text);
124
124
  let currentText = $derived(loading ? loadingText : text);
125
- let shouldShowTooltip = $derived(!!tooltip?.message || !!tooltip?.tooltipContent);
125
+ let shouldShowTooltip = $derived(!!tooltip?.message || !!tooltip?.tooltip);
126
126
 
127
127
  // Default styling (no size variations)
128
128
  let computedPadding = $derived(padding || '4px 8px');
@@ -15,7 +15,7 @@ export interface ButtonProps {
15
15
  class?: string;
16
16
  loadingText?: string;
17
17
  fullWidth?: boolean;
18
- tooltipContent?: string;
18
+ tooltip?: string;
19
19
  invalid?: boolean;
20
20
  style?: string;
21
21
  ariaLabel?: string;
@@ -171,7 +171,7 @@
171
171
  <div class="color-swatch" style="background-color: {color || '#000000'}"></div>
172
172
  </div>
173
173
  {/snippet}
174
- {#snippet tooltipContent()}
174
+ {#snippet tooltip()}
175
175
  <ColorSelect {color} oncolorchange={handleFullColorChange} />
176
176
  {/snippet}
177
177
  </Tooltip>
@@ -34,7 +34,7 @@
34
34
  class="popup-button-chevron"
35
35
  />
36
36
  {/snippet}
37
- {#snippet tooltipContent()}
37
+ {#snippet tooltip()}
38
38
  <div role="button" tabindex="0" class="popup-content">
39
39
  <div class="popup-content-list">
40
40
  {#each button.popupButtons || [] as popupButton, popupIndex (popupIndex)}
@@ -94,7 +94,7 @@
94
94
  onclick={button.onClick || (() => {})}
95
95
  />
96
96
  {/snippet}
97
- {#snippet tooltipContent()}
97
+ {#snippet tooltip()}
98
98
  <div
99
99
  class="tooltip-content {button.tooltip?.className || ''}"
100
100
  style="display: {button.tooltip?.icon ? 'grid' : 'inline-block'}"
@@ -10,6 +10,7 @@
10
10
  import { Section } from '../../section';
11
11
  import { Switch } from '../../switch';
12
12
  import Text from '../../text/Text.svelte';
13
+ import { Tooltip } from '../../tooltip';
13
14
  import Layout from '../Layout.svelte';
14
15
  import type { LayoutNotification, LayoutTab } from '../types';
15
16
 
@@ -309,6 +310,40 @@
309
310
  }}
310
311
  width="130px"
311
312
  />
313
+
314
+ <Tooltip
315
+ onshow={(value) => console.log('Tooltip shown:', value)}
316
+ onclose={(value) => console.log('Tooltip closed:', value)}
317
+ >
318
+ {#snippet target()}
319
+ <button>Hover me</button>
320
+ {/snippet}
321
+
322
+ {#snippet tooltip()}
323
+ <div>Custom tooltip content here!</div>
324
+ {/snippet}
325
+ </Tooltip>
326
+
327
+ <Tooltip
328
+ onshow={(value) => console.log('Tooltip shown:', value)}
329
+ onclose={(value) => console.log('Tooltip closed:', value)}
330
+ listener="click"
331
+ listenerout="click"
332
+ stopPropagation={false}
333
+ >
334
+ {#snippet target()}
335
+ <button>Click me</button>
336
+ {/snippet}
337
+
338
+ {#snippet tooltip()}
339
+ <div class="click-tests">
340
+ <Text link onclick={() => console.log('Tooltip clicked')}>Click me</Text>
341
+ <Text link onclick={() => console.log('Tooltip another click')}
342
+ >Another click me</Text
343
+ >
344
+ </div>
345
+ {/snippet}
346
+ </Tooltip>
312
347
  </div>
313
348
  <Section clickable disabled>
314
349
  <Text fontSize="md" fontWeight="normal">Clickable disabled</Text>
@@ -343,6 +378,12 @@
343
378
  </div>
344
379
 
345
380
  <style>
381
+ .click-tests {
382
+ display: flex;
383
+ flex-direction: column;
384
+ gap: 8px;
385
+ }
386
+
346
387
  /* Main container layout */
347
388
  .example-container {
348
389
  display: flex;
@@ -121,7 +121,7 @@
121
121
  let ariaDisabled = $derived(clickable ? disabled : undefined);
122
122
 
123
123
  // Determine tooltip configuration
124
- let shouldShowTooltip = $derived(!!tooltip?.message || !!tooltip?.tooltipContent);
124
+ let shouldShowTooltip = $derived(!!tooltip?.message || !!tooltip?.tooltip);
125
125
  let shouldShowDisabledTooltip = $derived(!!disabledMessage);
126
126
  let hasAnyTooltip = $derived(shouldShowTooltip || shouldShowDisabledTooltip);
127
127
 
@@ -282,7 +282,7 @@
282
282
  let shouldShowTooltip = $derived(
283
283
  !!tooltip &&
284
284
  Object.keys(tooltip).length > 0 &&
285
- (tooltip.message || tooltip.tooltipContent) &&
285
+ (tooltip.message || tooltip.tooltip) &&
286
286
  !disabled &&
287
287
  (isPopupHidden || !hasPopup)
288
288
  );
@@ -784,7 +784,7 @@
784
784
  {/if}
785
785
  </span>
786
786
  {/snippet}
787
- {#snippet tooltipContent()}
787
+ {#snippet tooltip()}
788
788
  <div
789
789
  class="popup-wrapper"
790
790
  style={!popupConfig.active ? 'display:none' : ''}
@@ -0,0 +1,48 @@
1
+ <script lang="ts">
2
+ import type { Placement } from '@floating-ui/dom';
3
+
4
+ import Tooltip from './Tooltip.svelte';
5
+
6
+ interface Props {
7
+ message?: string;
8
+ hidden?: boolean;
9
+ disabled?: boolean;
10
+ listener?: 'click' | 'hover';
11
+ listenerout?: 'click' | 'hover';
12
+ placement?: Placement;
13
+ bgColor?: string;
14
+ class?: string;
15
+ onshow?: (event: boolean) => void;
16
+ onclose?: (event: boolean) => void;
17
+ }
18
+
19
+ let {
20
+ message = '',
21
+ hidden = false,
22
+ disabled = false,
23
+ listener = 'hover',
24
+ listenerout = 'hover',
25
+ placement = 'right',
26
+ bgColor,
27
+ class: className,
28
+ onshow,
29
+ onclose
30
+ }: Props = $props();
31
+ </script>
32
+
33
+ <Tooltip
34
+ {message}
35
+ {hidden}
36
+ {disabled}
37
+ {listener}
38
+ {listenerout}
39
+ {placement}
40
+ {bgColor}
41
+ class={className}
42
+ {onshow}
43
+ {onclose}
44
+ >
45
+ {#snippet target()}
46
+ <button>Hover me</button>
47
+ {/snippet}
48
+ </Tooltip>
@@ -0,0 +1,16 @@
1
+ import type { Placement } from '@floating-ui/dom';
2
+ interface Props {
3
+ message?: string;
4
+ hidden?: boolean;
5
+ disabled?: boolean;
6
+ listener?: 'click' | 'hover';
7
+ listenerout?: 'click' | 'hover';
8
+ placement?: Placement;
9
+ bgColor?: string;
10
+ class?: string;
11
+ onshow?: (event: boolean) => void;
12
+ onclose?: (event: boolean) => void;
13
+ }
14
+ declare const TestTooltipWrapper: import("svelte").Component<Props, {}, "">;
15
+ type TestTooltipWrapper = ReturnType<typeof TestTooltipWrapper>;
16
+ export default TestTooltipWrapper;
@@ -8,23 +8,51 @@
8
8
  type Placement,
9
9
  shift
10
10
  } from '@floating-ui/dom';
11
- import { tick } from 'svelte';
11
+ import { type Component, onDestroy, onMount, type Snippet, tick } from 'svelte';
12
+ import { writable } from 'svelte/store';
12
13
  import { v4 as uuidv4 } from 'uuid';
13
14
 
14
- import { cleanupTooltipMessage } from '../../../utils/index.js';
15
-
16
- import type { TooltipEvents, TooltipInstance, TooltipProps } from './types.js';
17
-
18
- type Props = TooltipProps;
15
+ import { cleanupTooltipMessage } from '../../../utils';
16
+
17
+ import Text from '../text/Text.svelte';
18
+
19
+ const activeTooltips = writable<string[]>([]);
20
+
21
+ interface Props {
22
+ message?: string;
23
+ listener?: 'click' | 'hover';
24
+ listenerout?: 'click' | 'hover';
25
+ placement?: Placement;
26
+ position?: string;
27
+ showArrow?: boolean;
28
+ offsetVal?: number;
29
+ hidden?: boolean;
30
+ disabled?: boolean;
31
+ tooltipIcon?: Component | null;
32
+ tooltipIconColor?: string;
33
+ width?: string;
34
+ padding?: string;
35
+ raw?: boolean;
36
+ isActive?: boolean;
37
+ fallbackPlacements?: Placement[];
38
+ stopPropagation?: boolean;
39
+ fontColor?: string;
40
+ bgColor?: string;
41
+ class?: string;
42
+ /** Target element snippet */
43
+ target?: Snippet;
44
+ /** Tooltip content snippet */
45
+ tooltip?: Snippet;
46
+ onshow?: (event: boolean) => void;
47
+ onclose?: (event: boolean) => void;
48
+ }
19
49
 
20
50
  let {
21
- bgColor = 'var(--background3)',
22
51
  message = '',
23
52
  listener = 'hover',
24
53
  listenerout = 'hover',
25
54
  placement = 'right',
26
55
  position = 'absolute',
27
- class: classNames = '',
28
56
  showArrow = true,
29
57
  offsetVal = 10,
30
58
  hidden = $bindable(false),
@@ -38,51 +66,30 @@
38
66
  fallbackPlacements = [],
39
67
  stopPropagation = true,
40
68
  fontColor = 'var(--text2)',
69
+ bgColor = '',
70
+ class: className = '',
41
71
  target,
42
- tooltipContent,
43
- targetText
72
+ tooltip,
73
+ onshow,
74
+ onclose
44
75
  }: Props = $props();
45
76
 
46
- // Generate appropriate target text based on trigger if not provided
47
- let defaultTargetText = $derived(
48
- targetText ||
49
- (listener === 'click' && listenerout === 'click'
50
- ? 'Click me!'
51
- : listener === 'click' && listenerout === 'hover'
52
- ? 'Click to show'
53
- : listener === 'hover' && listenerout === 'click'
54
- ? 'Hover to show'
55
- : 'Hover me!')
56
- );
57
-
58
- // State management with runes
59
- let tooltipInstance: TooltipInstance | null = $state(null);
60
- let targetElement: HTMLDivElement = $state()!;
61
- let tooltipElement: HTMLDivElement = $state()!;
62
- let arrowElement: HTMLDivElement = $state()!;
63
- let observer: MutationObserver | null = $state(null);
64
-
65
- // Generate unique ID for this tooltip instance
66
- const tooltipId = $derived(`tooltip-${uuidv4()}`);
67
-
68
- // Format message with cleanup utility
69
- let formattedMessage = $derived(cleanupTooltipMessage(message));
77
+ type TooltipInstance = {
78
+ toggle: HTMLElement;
79
+ tooltip: HTMLElement;
80
+ arrowElement?: HTMLElement;
81
+ cleanup: () => void;
82
+ showTooltip: () => void;
83
+ hideTooltip: () => void;
84
+ };
70
85
 
71
- // Tooltip visibility state
72
- let isTooltipVisible = $derived(tooltipElement ? tooltipElement.style.display === 'flex' : false);
73
-
74
- // Global store for tracking active tooltips
75
- let activeTooltips: string[] = $state([]);
76
-
77
- // Event dispatcher
78
- function dispatchEvent(eventType: keyof TooltipEvents, detail: boolean) {
79
- const event = new CustomEvent(eventType, {
80
- detail,
81
- bubbles: true,
82
- cancelable: true
83
- });
84
- targetElement?.dispatchEvent(event);
85
- }
86
+ let tooltipInstance: TooltipInstance | null = $state(null);
87
+ let targetElement: HTMLDivElement | undefined = $state();
88
+ let tooltipElement: HTMLDivElement | undefined = $state();
89
+ let arrowElement: HTMLDivElement | undefined = $state();
90
+ let observer: MutationObserver | null = null;
91
+ let documentClickListener: ((event: MouseEvent) => void) | null = null;
92
+ const tooltipId = `tooltip-${uuidv4()}`;
86
93
 
87
94
  /**
88
95
  * Dismisses other tooltips based on trigger compatibility.
@@ -92,217 +99,197 @@
92
99
  * - hover tooltips should NOT dismiss click tooltips (they're "sticky")
93
100
  * - click tooltips CAN dismiss hover tooltips (higher priority)
94
101
  */
95
- function dismissOtherTooltips() {
102
+ const dismissOtherTooltips = () => {
96
103
  const currentTriggerType = listener === 'click' && listenerout === 'click' ? 'click' : 'hover';
97
104
 
98
- document.querySelectorAll<HTMLDivElement>('.tooltip[role="tooltip"]').forEach((item) => {
99
- if (item.id !== tooltipId) {
100
- const existingTriggerType = item.getAttribute('data-trigger-type');
105
+ activeTooltips.update(() => {
106
+ document.querySelectorAll<HTMLDivElement>('.tooltip[role="tooltip"]').forEach((item) => {
107
+ if (item.id !== tooltipId) {
108
+ const existingTriggerType = item.getAttribute('data-trigger-type');
101
109
 
102
- const shouldDismiss =
103
- currentTriggerType === 'click' ||
104
- (currentTriggerType === 'hover' && existingTriggerType === 'hover');
110
+ const shouldDismiss =
111
+ currentTriggerType === 'click' ||
112
+ (currentTriggerType === 'hover' && existingTriggerType === 'hover'); // Hover only dismisses other hover
105
113
 
106
- if (shouldDismiss) {
107
- item.style.display = 'none';
108
- item.setAttribute('aria-hidden', 'true');
114
+ if (shouldDismiss) {
115
+ item.style.display = 'none';
116
+ item.setAttribute('aria-hidden', 'true');
117
+ }
109
118
  }
110
- }
119
+ });
120
+
121
+ return [tooltipId];
111
122
  });
123
+ };
112
124
 
113
- activeTooltips = [tooltipId];
114
- }
125
+ /**
126
+ * Inits the tooltip.
127
+ */
128
+ const initTooltip = async (): Promise<TooltipInstance | null> => {
129
+ if (!tooltipElement || (showArrow && !arrowElement) || !targetElement) return null;
130
+ return setupTooltip(targetElement, tooltipElement, arrowElement);
131
+ };
115
132
 
116
133
  /**
117
- * Sets up the tooltip with floating-ui positioning
134
+ * Sets up the tooltip.
135
+ * @param toggle
136
+ * @param tooltip
137
+ * @param arrowElement
118
138
  */
119
- function setupTooltip(
139
+ const setupTooltip = (
120
140
  toggle: HTMLElement,
121
- tooltipEl: HTMLElement,
122
- arrowEl?: HTMLElement
123
- ): TooltipInstance {
141
+ tooltip: HTMLElement,
142
+ arrowElement?: HTMLElement
143
+ ): TooltipInstance => {
124
144
  /**
125
- * Updates the tooltip position using floating-ui
145
+ * Updates the tooltip position.
126
146
  */
127
147
  const update = () => {
128
- computePosition(toggle, tooltipEl, {
148
+ computePosition(toggle, tooltip, {
129
149
  placement: placement,
130
150
  middleware: [
131
151
  offset(offsetVal),
132
152
  flip(
133
153
  fallbackPlacements?.length > 0
134
- ? { fallbackPlacements }
154
+ ? {
155
+ fallbackPlacements
156
+ }
135
157
  : {
136
158
  fallbackAxisSideDirection: 'start',
137
159
  fallbackStrategy: 'bestFit'
138
160
  }
139
161
  ),
140
162
  shift({ padding: 5 }),
141
- showArrow && arrowEl ? arrow({ element: arrowEl }) : undefined
163
+ showArrow && arrowElement ? arrow({ element: arrowElement }) : undefined
142
164
  ].filter(Boolean)
143
- }).then(({ x, y, placement: finalPlacement, middlewareData }) => {
144
- // Position tooltip
145
- Object.assign(tooltipEl.style, {
165
+ }).then(({ x, y, placement, middlewareData }) => {
166
+ Object.assign(tooltip.style, {
146
167
  left: `${x}px`,
147
168
  top: `${y}px`
148
169
  });
149
170
 
150
- // Position arrow using floating-ui's default behavior
151
- if (showArrow && arrowEl && middlewareData?.arrow) {
152
- const { x: arrowX, y: arrowY } = middlewareData.arrow;
153
- const side = finalPlacement.split('-')[0];
171
+ // Accessing the data
172
+ const staticSide = {
173
+ top: 'bottom',
174
+ right: 'left',
175
+ bottom: 'top',
176
+ left: 'right'
177
+ }[placement.split('-')[0]];
154
178
 
155
- const staticSide = {
156
- top: 'bottom',
157
- right: 'left',
158
- bottom: 'top',
159
- left: 'right'
160
- }[side];
179
+ if (showArrow && arrowElement) {
180
+ if (!middlewareData?.arrow) return;
181
+ const { x: arrowX, y: arrowY } = middlewareData.arrow;
161
182
 
162
- Object.assign(arrowEl.style, {
183
+ Object.assign(arrowElement.style, {
163
184
  left: arrowX != null ? `${arrowX}px` : '',
164
185
  top: arrowY != null ? `${arrowY}px` : '',
165
186
  right: '',
166
187
  bottom: '',
167
- [staticSide as string]: '-4px'
188
+ [`${staticSide}`]: '-4px'
168
189
  });
169
190
  }
170
191
  });
171
192
  };
172
193
 
173
194
  /**
174
- * Shows the tooltip
195
+ * Shows the tooltip.
175
196
  */
176
197
  const showTooltip = () => {
177
- if (disabled) return;
198
+ if (disabled || !tooltipElement) return;
178
199
 
179
200
  dismissOtherTooltips();
180
201
 
181
- tooltipEl.style.display = 'flex';
182
- tooltipEl.setAttribute('aria-hidden', 'false');
202
+ tooltipElement.style.display = 'flex';
203
+ tooltipElement.setAttribute('aria-hidden', 'false');
183
204
 
184
205
  // Set trigger type for dismissal logic
185
206
  const triggerType = listener === 'click' && listenerout === 'click' ? 'click' : 'hover';
186
- tooltipEl.setAttribute('data-trigger-type', triggerType);
207
+ tooltipElement.setAttribute('data-trigger-type', triggerType);
187
208
 
188
209
  isActive = true;
189
- hidden = false;
190
210
 
191
- if (!activeTooltips.includes(tooltipId)) {
192
- activeTooltips = [...activeTooltips.filter((id) => id !== tooltipId), tooltipId];
193
- }
211
+ activeTooltips.update((ids) => [...ids.filter((id) => id !== tooltipId), tooltipId]);
194
212
 
195
213
  update();
196
- dispatchEvent('show', true);
214
+ onshow?.(true);
197
215
  };
198
216
 
199
217
  /**
200
- * Hides the tooltip
218
+ * Hides the tooltip.
201
219
  */
202
220
  const hideTooltip = () => {
203
- // Hide immediately in test environment
204
- const isTestEnv =
205
- (typeof process !== 'undefined' && process.env?.NODE_ENV === 'test') ||
206
- (typeof globalThis !== 'undefined' && '__vitest__' in globalThis);
207
- const delay = isTestEnv ? 0 : 100;
208
-
209
221
  setTimeout(() => {
210
- tooltipEl.style.display = 'none';
211
- tooltipEl.setAttribute('aria-hidden', 'true');
212
- tooltipEl.removeAttribute('data-trigger-type');
222
+ if (!tooltipElement) return;
223
+
224
+ tooltipElement.style.display = 'none';
225
+ tooltipElement.setAttribute('aria-hidden', 'true');
226
+ tooltipElement.removeAttribute('data-trigger-type');
213
227
  isActive = false;
214
- hidden = true;
215
- activeTooltips = activeTooltips.filter((id) => id !== tooltipId);
216
228
 
217
- dispatchEvent('close', true);
218
- }, delay);
229
+ activeTooltips.update((ids) => ids.filter((id) => id !== tooltipId));
230
+
231
+ onclose?.(true);
232
+ }, 50);
219
233
  };
220
234
 
221
- // Set up event listeners based on trigger configuration
222
- let eventOptions: Array<[string, () => void]> = [];
235
+ let opts;
223
236
 
224
237
  if (listener === 'click' && listenerout === 'click') {
225
- eventOptions = [['click', showTooltip]];
226
-
227
- // Global click handler for dismissal
228
- const handleGlobalClick = (event: MouseEvent) => {
229
- if (!tooltipEl.contains(event.target as Node) && !toggle.contains(event.target as Node)) {
238
+ opts = [['click', showTooltip]];
239
+
240
+ // Store reference to the click handler for cleanup
241
+ documentClickListener = (event: MouseEvent) => {
242
+ if (
243
+ tooltipElement &&
244
+ toggle &&
245
+ !tooltipElement.contains(event.target as Node) &&
246
+ !toggle.contains(event.target as Node)
247
+ ) {
230
248
  hideTooltip();
231
249
  }
232
250
  };
233
251
 
234
- document.addEventListener('click', handleGlobalClick);
235
-
236
- // Return cleanup function that removes global listener
237
- const originalCleanup = autoUpdate(toggle, tooltipEl, update);
238
- const cleanup = () => {
239
- originalCleanup();
240
- document.removeEventListener('click', handleGlobalClick);
241
- };
242
-
243
- // Add event listeners to toggle element
244
- eventOptions.forEach(([event, handler]) => {
245
- toggle.addEventListener(event, (evt) => {
246
- if (stopPropagation) {
247
- evt.stopPropagation();
248
- evt.preventDefault();
249
- }
250
- handler();
251
- });
252
- });
253
-
254
- return {
255
- toggle,
256
- tooltip: tooltipEl,
257
- arrowElement: arrowEl,
258
- cleanup,
259
- showTooltip,
260
- hideTooltip
261
- };
252
+ document.addEventListener('click', documentClickListener);
262
253
  } else {
263
- eventOptions = [
254
+ opts = [
264
255
  listener === 'click' ? ['click', showTooltip] : undefined,
265
256
  listener === 'hover' ? ['mouseenter', showTooltip] : undefined,
266
257
  listener === 'hover' ? ['mouseleave', hideTooltip] : undefined,
267
258
  listener === 'hover' ? ['focus', showTooltip] : undefined,
268
259
  ['blur', hideTooltip]
269
- ].filter(Boolean) as Array<[string, () => void]>;
260
+ ];
270
261
  }
271
262
 
272
- // Add event listeners
273
- eventOptions.forEach(([event, handler]) => {
263
+ type EventOption = [string, () => void];
264
+ const options: EventOption[] = opts.filter(Boolean) as EventOption[];
265
+
266
+ options.forEach(([event, listener]) => {
274
267
  toggle.addEventListener(event, (evt) => {
275
268
  if (stopPropagation) {
276
269
  evt.stopPropagation();
277
270
  evt.preventDefault();
278
271
  }
279
- handler();
272
+ listener();
280
273
  });
281
274
  });
282
275
 
283
- const cleanup = autoUpdate(toggle, tooltipEl, update);
284
-
285
- return { toggle, tooltip: tooltipEl, arrowElement: arrowEl, cleanup, showTooltip, hideTooltip };
286
- }
287
-
288
- /**
289
- * Initializes the tooltip instance
290
- */
291
- async function initTooltip(): Promise<TooltipInstance | null> {
292
- if (!tooltipElement || (showArrow && !arrowElement) || !targetElement) return null;
293
- return setupTooltip(targetElement, tooltipElement, arrowElement);
294
- }
276
+ const cleanup = () => {
277
+ if (tooltipElement) {
278
+ autoUpdate(toggle, tooltipElement, update)();
279
+ }
280
+ if (documentClickListener) {
281
+ document.removeEventListener('click', documentClickListener);
282
+ documentClickListener = null;
283
+ }
284
+ };
295
285
 
296
- // Public API methods
297
- export function show() {
298
- tooltipInstance?.showTooltip();
299
- }
286
+ return { toggle, tooltip: tooltipElement!, arrowElement, cleanup, showTooltip, hideTooltip };
287
+ };
300
288
 
301
- export function hide() {
302
- tooltipInstance?.hideTooltip();
303
- }
289
+ export const show = () => tooltipInstance?.showTooltip();
290
+ export const hide = () => tooltipInstance?.hideTooltip();
304
291
 
305
- // Effect to handle hidden prop changes
292
+ // Svelte 5 effect for hidden prop
306
293
  $effect(() => {
307
294
  if (hidden) {
308
295
  setTimeout(() => {
@@ -316,129 +303,102 @@
316
303
  }
317
304
  });
318
305
 
319
- $effect(() => {
320
- if (tooltipElement) {
321
- const handleTooltipInteraction = (evt: Event) => {
322
- evt.stopPropagation();
323
- };
324
-
325
- tooltipElement.addEventListener('click', handleTooltipInteraction, true);
326
- tooltipElement.addEventListener('mousedown', handleTooltipInteraction, true);
327
- tooltipElement.addEventListener('mouseup', handleTooltipInteraction, true);
306
+ const formattedMessage = $derived(cleanupTooltipMessage(message));
328
307
 
329
- return () => {
330
- tooltipElement.removeEventListener('click', handleTooltipInteraction, true);
331
- tooltipElement.removeEventListener('mousedown', handleTooltipInteraction, true);
332
- tooltipElement.removeEventListener('mouseup', handleTooltipInteraction, true);
333
- };
334
- }
308
+ onMount(async () => {
309
+ await tick();
310
+ tooltipInstance = await initTooltip();
335
311
  });
336
312
 
337
- // Initialize tooltip when component mounts
338
- $effect(() => {
339
- if (targetElement && tooltipElement) {
340
- tick().then(async () => {
341
- tooltipInstance = await initTooltip();
342
-
343
- if (tooltipElement) {
344
- observer = new MutationObserver((mutations) => {
345
- mutations.forEach((mutation) => {
346
- if (mutation.type === 'attributes' && tooltipElement?.style) {
347
- const isVisible = tooltipElement.style.display !== 'none';
348
- dispatchEvent(isVisible ? 'show' : 'close', true);
349
- }
350
- });
351
- });
313
+ onDestroy(() => {
314
+ // Disconnect observer first to prevent any new callbacks
315
+ if (observer) {
316
+ observer.disconnect();
317
+ observer = null;
318
+ }
352
319
 
353
- observer.observe(tooltipElement, { attributes: true });
354
- }
355
- });
320
+ // Clean up document click listener
321
+ if (documentClickListener) {
322
+ document.removeEventListener('click', documentClickListener);
323
+ documentClickListener = null;
356
324
  }
357
325
 
358
- // Cleanup effect
359
- return () => {
360
- if (tooltipInstance) {
361
- tooltipInstance.cleanup();
362
- activeTooltips = activeTooltips.filter((id) => id !== tooltipId);
363
- }
364
- if (observer) {
365
- observer.disconnect();
366
- observer = null;
367
- }
368
- };
326
+ if (tooltipInstance) {
327
+ tooltipInstance.cleanup();
328
+ activeTooltips.update((ids) => ids.filter((id) => id !== tooltipId));
329
+ tooltipInstance = null;
330
+ }
369
331
  });
370
332
  </script>
371
333
 
372
- <div class="target {classNames}" bind:this={targetElement} aria-describedby={tooltipId}>
334
+ <div class="target" bind:this={targetElement} aria-describedby={tooltipId}>
373
335
  {#if target}
374
336
  {@render target()}
375
- {:else}
376
- <button
377
- style="
378
- padding: 4px 8px;
379
- background: #007bff;
380
- color: white;
381
- border: none;
382
- border-radius: 4px;
383
- cursor: pointer;
384
- "
385
- >
386
- {defaultTargetText}
387
- </button>
388
337
  {/if}
389
338
 
390
339
  <div
391
- class="tooltip"
340
+ class="tooltip {className}"
392
341
  bind:this={tooltipElement}
393
342
  role="tooltip"
394
343
  id={tooltipId}
395
- aria-hidden={isTooltipVisible ? 'false' : 'true'}
396
- style="position:{position}; width:{width}; padding:{padding}; background:{bgColor};"
344
+ style="position:{position}; width:{width}; padding:{padding}; {bgColor
345
+ ? `background-color: ${bgColor};`
346
+ : ''}"
397
347
  >
398
348
  {#if tooltipIcon}
399
- {@const IconComponent = tooltipIcon}
349
+ {@const Icon = tooltipIcon}
400
350
  <div class="icon" style="color:{tooltipIconColor};">
401
- <IconComponent />
351
+ <Icon />
402
352
  </div>
403
353
  {/if}
404
354
 
405
- <div class="content-wrapper">
406
- {#if tooltipContent}
407
- {@render tooltipContent()}
408
- {:else if message && raw}
409
- <div class="message" style="color:{fontColor};">
410
- {@html message}
411
- </div>
412
- {:else if message}
413
- <div class="message">
414
- <span>{formattedMessage}</span>
415
- </div>
416
- {/if}
417
- </div>
355
+ {#if tooltip}
356
+ {@render tooltip()}
357
+ {:else if message && raw}
358
+ <div class="message" style="color:{fontColor};">
359
+ {@html message}
360
+ </div>
361
+ {:else if message}
362
+ <div class="message">
363
+ <Text label={formattedMessage} fontSize="11px" fontWeight="500" {fontColor} />
364
+ </div>
365
+ {/if}
418
366
 
419
367
  {#if showArrow}
420
- <div class="arrow" bind:this={arrowElement} style="background: {bgColor};"></div>
368
+ <div
369
+ class="arrow"
370
+ id="arrow_{tooltipId}"
371
+ bind:this={arrowElement}
372
+ style={bgColor ? `background-color: ${bgColor};` : ''}
373
+ ></div>
421
374
  {/if}
422
375
  </div>
423
376
  </div>
424
377
 
425
378
  <style>
379
+ .message {
380
+ background: var(--background3, #383838);
381
+ text-wrap: normal;
382
+ white-space: normal;
383
+ color: var(--text2, #b9b9b9);
384
+ font-weight: 500;
385
+ font-size: 11px;
386
+ }
426
387
  .tooltip {
427
388
  display: none;
428
389
  top: 0;
429
390
  left: 0;
430
- border-radius: 4px;
431
391
  z-index: 99999999999;
432
- border-radius: var(--border-radius, 4px);
433
- background: var(--background3);
392
+ border-radius: 4px;
393
+ background: var(--background3, #383838);
434
394
  box-shadow:
435
- 0 12px 24px 8px #0000004d,
436
- 0 8px 16px 4px #0000004d,
437
- 0 4px 8px 2px #0000,
438
- 0 2px 6px #00000014,
439
- 0 -0.5px 0.5px #0000 inset,
440
- 0 0.5px 0.5px #ffffff57 inset;
441
- color: var(--text2);
395
+ 0px 12px 24px 8px rgb(0 0 0 / 30%),
396
+ 0px 8px 16px 4px rgb(0 0 0 / 30%),
397
+ 0px 4px 8px 2px rgb(0 0 0 / 0%),
398
+ 0px 2px 6px 0px rgba(0, 0, 0, 0.08),
399
+ 0px -0.5px 0.5px 0px rgb(0 0 0 / 0%) inset,
400
+ 0px 0.5px 0.5px 0px rgb(255 255 255 / 34%) inset;
401
+ color: var(--text2, #b9b9b9);
442
402
  font-family: Inter;
443
403
  line-height: 120%;
444
404
  width: 150px;
@@ -448,38 +408,20 @@
448
408
  align-items: start;
449
409
  justify-content: start;
450
410
  }
451
-
452
- .content-wrapper {
453
- flex: 1;
454
- min-width: 0;
455
- border-radius: var(--border-radius, 4px);
456
- z-index: 3;
457
- background: inherit;
458
- }
459
-
460
- .message {
461
- text-wrap: normal;
462
- white-space: normal;
463
- color: var(--text2);
464
- font-weight: 500;
465
- font-size: 11px;
466
- }
467
-
468
411
  .arrow {
412
+ z-index: 1;
413
+ background: var(--background3, #383838);
469
414
  position: absolute;
470
- width: 10px;
471
- height: 10px;
415
+ width: 8px;
416
+ height: 8px;
472
417
  transform: rotate(45deg);
473
418
  }
474
-
475
- .target {
419
+ .target :global(div[slot='tooltip']) {
420
+ width: 100%;
421
+ }
422
+ .target :global(div[slot='target']) {
476
423
  display: flex;
477
424
  align-items: center;
478
425
  justify-content: center;
479
426
  }
480
-
481
- .icon {
482
- position: relative;
483
- z-index: 1;
484
- }
485
427
  </style>
@@ -1,7 +1,36 @@
1
- import type { TooltipProps } from './types.js';
2
- declare const Tooltip: import("svelte").Component<TooltipProps, {
3
- show: () => void;
4
- hide: () => void;
1
+ import { type Placement } from '@floating-ui/dom';
2
+ import { type Component, type Snippet } from 'svelte';
3
+ interface Props {
4
+ message?: string;
5
+ listener?: 'click' | 'hover';
6
+ listenerout?: 'click' | 'hover';
7
+ placement?: Placement;
8
+ position?: string;
9
+ showArrow?: boolean;
10
+ offsetVal?: number;
11
+ hidden?: boolean;
12
+ disabled?: boolean;
13
+ tooltipIcon?: Component | null;
14
+ tooltipIconColor?: string;
15
+ width?: string;
16
+ padding?: string;
17
+ raw?: boolean;
18
+ isActive?: boolean;
19
+ fallbackPlacements?: Placement[];
20
+ stopPropagation?: boolean;
21
+ fontColor?: string;
22
+ bgColor?: string;
23
+ class?: string;
24
+ /** Target element snippet */
25
+ target?: Snippet;
26
+ /** Tooltip content snippet */
27
+ tooltip?: Snippet;
28
+ onshow?: (event: boolean) => void;
29
+ onclose?: (event: boolean) => void;
30
+ }
31
+ declare const Tooltip: Component<Props, {
32
+ show: () => void | undefined;
33
+ hide: () => void | undefined;
5
34
  }, "hidden" | "isActive">;
6
35
  type Tooltip = ReturnType<typeof Tooltip>;
7
36
  export default Tooltip;
@@ -81,7 +81,7 @@ export interface TooltipProps {
81
81
  /**
82
82
  * Tooltip content snippet (custom tooltip content)
83
83
  */
84
- tooltipContent?: Snippet;
84
+ tooltip?: Snippet;
85
85
  /**
86
86
  * Default target text when no target snippet is provided (for Storybook compatibility)
87
87
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finsweet/webflow-apps-utils",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "description": "Shared utilities for Webflow apps",
5
5
  "homepage": "https://github.com/finsweet/webflow-apps-utils",
6
6
  "repository": {