@finsweet/webflow-apps-utils 1.0.13 → 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 : 50;
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,111 +303,102 @@
316
303
  }
317
304
  });
318
305
 
319
- // Initialize tooltip when component mounts
320
- $effect(() => {
321
- if (targetElement && tooltipElement) {
322
- tick().then(async () => {
323
- tooltipInstance = await initTooltip();
324
-
325
- if (tooltipElement) {
326
- observer = new MutationObserver((mutations) => {
327
- mutations.forEach((mutation) => {
328
- if (mutation.type === 'attributes' && tooltipElement?.style) {
329
- const isVisible = tooltipElement.style.display !== 'none';
330
- dispatchEvent(isVisible ? 'show' : 'close', true);
331
- }
332
- });
333
- });
306
+ const formattedMessage = $derived(cleanupTooltipMessage(message));
334
307
 
335
- observer.observe(tooltipElement, { attributes: true });
336
- }
337
- });
308
+ onMount(async () => {
309
+ await tick();
310
+ tooltipInstance = await initTooltip();
311
+ });
312
+
313
+ onDestroy(() => {
314
+ // Disconnect observer first to prevent any new callbacks
315
+ if (observer) {
316
+ observer.disconnect();
317
+ observer = null;
338
318
  }
339
319
 
340
- // Cleanup effect
341
- return () => {
342
- if (tooltipInstance) {
343
- tooltipInstance.cleanup();
344
- activeTooltips = activeTooltips.filter((id) => id !== tooltipId);
345
- }
346
- if (observer) {
347
- observer.disconnect();
348
- observer = null;
349
- }
350
- };
320
+ // Clean up document click listener
321
+ if (documentClickListener) {
322
+ document.removeEventListener('click', documentClickListener);
323
+ documentClickListener = null;
324
+ }
325
+
326
+ if (tooltipInstance) {
327
+ tooltipInstance.cleanup();
328
+ activeTooltips.update((ids) => ids.filter((id) => id !== tooltipId));
329
+ tooltipInstance = null;
330
+ }
351
331
  });
352
332
  </script>
353
333
 
354
- <div class="target {classNames}" bind:this={targetElement} aria-describedby={tooltipId}>
334
+ <div class="target" bind:this={targetElement} aria-describedby={tooltipId}>
355
335
  {#if target}
356
336
  {@render target()}
357
- {:else}
358
- <button
359
- style="
360
- padding: 4px 8px;
361
- background: #007bff;
362
- color: white;
363
- border: none;
364
- border-radius: 4px;
365
- cursor: pointer;
366
- "
367
- >
368
- {defaultTargetText}
369
- </button>
370
337
  {/if}
371
338
 
372
339
  <div
373
- class="tooltip"
340
+ class="tooltip {className}"
374
341
  bind:this={tooltipElement}
375
342
  role="tooltip"
376
343
  id={tooltipId}
377
- aria-hidden={isTooltipVisible ? 'false' : 'true'}
378
- style="position:{position}; width:{width}; padding:{padding}; background:{bgColor};"
344
+ style="position:{position}; width:{width}; padding:{padding}; {bgColor
345
+ ? `background-color: ${bgColor};`
346
+ : ''}"
379
347
  >
380
348
  {#if tooltipIcon}
381
- {@const IconComponent = tooltipIcon}
349
+ {@const Icon = tooltipIcon}
382
350
  <div class="icon" style="color:{tooltipIconColor};">
383
- <IconComponent />
351
+ <Icon />
384
352
  </div>
385
353
  {/if}
386
354
 
387
- <div class="content-wrapper">
388
- {#if tooltipContent}
389
- {@render tooltipContent()}
390
- {:else if message && raw}
391
- <div class="message" style="color:{fontColor};">
392
- {@html message}
393
- </div>
394
- {:else if message}
395
- <div class="message">
396
- <span>{formattedMessage}</span>
397
- </div>
398
- {/if}
399
- </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}
400
366
 
401
367
  {#if showArrow}
402
- <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>
403
374
  {/if}
404
375
  </div>
405
376
  </div>
406
377
 
407
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
+ }
408
387
  .tooltip {
409
388
  display: none;
410
389
  top: 0;
411
390
  left: 0;
412
- border-radius: 4px;
413
391
  z-index: 99999999999;
414
- border-radius: var(--border-radius, 4px);
415
- background: var(--background3);
392
+ border-radius: 4px;
393
+ background: var(--background3, #383838);
416
394
  box-shadow:
417
- 0 12px 24px 8px #0000004d,
418
- 0 8px 16px 4px #0000004d,
419
- 0 4px 8px 2px #0000,
420
- 0 2px 6px #00000014,
421
- 0 -0.5px 0.5px #0000 inset,
422
- 0 0.5px 0.5px #ffffff57 inset;
423
- 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);
424
402
  font-family: Inter;
425
403
  line-height: 120%;
426
404
  width: 150px;
@@ -430,38 +408,20 @@
430
408
  align-items: start;
431
409
  justify-content: start;
432
410
  }
433
-
434
- .content-wrapper {
435
- flex: 1;
436
- min-width: 0;
437
- border-radius: var(--border-radius, 4px);
438
- z-index: 3;
439
- background: inherit;
440
- }
441
-
442
- .message {
443
- text-wrap: normal;
444
- white-space: normal;
445
- color: var(--text2);
446
- font-weight: 500;
447
- font-size: 11px;
448
- }
449
-
450
411
  .arrow {
412
+ z-index: 1;
413
+ background: var(--background3, #383838);
451
414
  position: absolute;
452
- width: 10px;
453
- height: 10px;
415
+ width: 8px;
416
+ height: 8px;
454
417
  transform: rotate(45deg);
455
418
  }
456
-
457
- .target {
419
+ .target :global(div[slot='tooltip']) {
420
+ width: 100%;
421
+ }
422
+ .target :global(div[slot='target']) {
458
423
  display: flex;
459
424
  align-items: center;
460
425
  justify-content: center;
461
426
  }
462
-
463
- .icon {
464
- position: relative;
465
- z-index: 1;
466
- }
467
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.13",
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": {