@finsweet/webflow-apps-utils 1.0.14 → 1.0.16

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');
@@ -148,7 +148,7 @@
148
148
  </script>
149
149
 
150
150
  {#if shouldShowTooltip}
151
- <Tooltip {...tooltip}>
151
+ <Tooltip {...tooltip} stopPropagation={false}>
152
152
  {#snippet target()}
153
153
  <button
154
154
  bind:this={buttonElement}
@@ -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>
@@ -23,6 +23,7 @@
23
23
  listener={button.popupTrigger || 'click'}
24
24
  listenerout={button.popupTrigger || 'click'}
25
25
  width="210px"
26
+ stopPropagation={false}
26
27
  >
27
28
  {#snippet target()}
28
29
  <Button
@@ -34,7 +35,7 @@
34
35
  class="popup-button-chevron"
35
36
  />
36
37
  {/snippet}
37
- {#snippet tooltipContent()}
38
+ {#snippet tooltip()}
38
39
  <div role="button" tabindex="0" class="popup-content">
39
40
  <div class="popup-content-list">
40
41
  {#each button.popupButtons || [] as popupButton, popupIndex (popupIndex)}
@@ -84,6 +85,7 @@
84
85
  placement={button.tooltip?.placement || 'top'}
85
86
  offsetVal={10}
86
87
  showArrow={button.tooltip?.showArrow !== false}
88
+ stopPropagation={false}
87
89
  >
88
90
  {#snippet target()}
89
91
  <Button
@@ -94,7 +96,7 @@
94
96
  onclick={button.onClick || (() => {})}
95
97
  />
96
98
  {/snippet}
97
- {#snippet tooltipContent()}
99
+ {#snippet tooltip()}
98
100
  <div
99
101
  class="tooltip-content {button.tooltip?.className || ''}"
100
102
  style="display: {button.tooltip?.icon ? 'grid' : 'inline-block'}"
@@ -186,6 +188,7 @@
186
188
  min-width: 150px;
187
189
  background: var(--background);
188
190
  border-radius: 4px;
191
+ width: 100%;
189
192
  }
190
193
  .popup-content-icon {
191
194
  display: flex;
@@ -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
 
@@ -187,13 +187,14 @@
187
187
  message={defaultDisabledMessage}
188
188
  width={disabledTooltipWidth}
189
189
  class="not-allowed"
190
+ stopPropagation={false}
190
191
  >
191
192
  {#snippet target()}
192
193
  {@render sectionContent()}
193
194
  {/snippet}
194
195
  </Tooltip>
195
196
  {:else if shouldShowTooltip}
196
- <Tooltip {...tooltip}>
197
+ <Tooltip {...tooltip} stopPropagation={false}>
197
198
  {#snippet target()}
198
199
  {@render sectionContent()}
199
200
  {/snippet}
@@ -227,7 +227,8 @@
227
227
  active: true,
228
228
  title: 'Remove',
229
229
  subtitle: 'Alt + click',
230
- description: 'This will remove the current selection.'
230
+ description: 'This will remove the current selection.',
231
+ onclick: () => console.log('Popup action clicked')
231
232
  }
232
233
  }}
233
234
  />
@@ -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
  );
@@ -550,6 +550,7 @@
550
550
  bind:hidden={isPopupHidden}
551
551
  listener="click"
552
552
  listenerout="click"
553
+ stopPropagation={false}
553
554
  >
554
555
  {#snippet target()}
555
556
  <span class="dropdown-label item {popupConfig.active ? 'active' : ''}">
@@ -784,7 +785,7 @@
784
785
  {/if}
785
786
  </span>
786
787
  {/snippet}
787
- {#snippet tooltipContent()}
788
+ {#snippet tooltip()}
788
789
  <div
789
790
  class="popup-wrapper"
790
791
  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,53 @@
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 { TooltipInfoCircleFilled } from '../../icons';
16
+ import ToolTipInfoCircleIcon from '../../icons/ToolTipInfoCircleIcon.svelte';
17
+ import { cleanupTooltipMessage } from '../../../utils';
18
+
19
+ import Text from '../text/Text.svelte';
20
+
21
+ const activeTooltips = writable<string[]>([]);
22
+
23
+ interface Props {
24
+ message?: string;
25
+ listener?: 'click' | 'hover';
26
+ listenerout?: 'click' | 'hover';
27
+ placement?: Placement;
28
+ position?: string;
29
+ showArrow?: boolean;
30
+ offsetVal?: number;
31
+ hidden?: boolean;
32
+ disabled?: boolean;
33
+ tooltipIcon?: Component | null;
34
+ tooltipIconColor?: string;
35
+ width?: string;
36
+ padding?: string;
37
+ raw?: boolean;
38
+ isActive?: boolean;
39
+ fallbackPlacements?: Placement[];
40
+ stopPropagation?: boolean;
41
+ fontColor?: string;
42
+ bgColor?: string;
43
+ class?: string;
44
+ /** Target element snippet */
45
+ target?: Snippet;
46
+ /** Tooltip content snippet */
47
+ tooltip?: Snippet;
48
+ onshow?: (event: boolean) => void;
49
+ onclose?: (event: boolean) => void;
50
+ }
19
51
 
20
52
  let {
21
- bgColor = 'var(--background3)',
22
53
  message = '',
23
54
  listener = 'hover',
24
55
  listenerout = 'hover',
25
56
  placement = 'right',
26
57
  position = 'absolute',
27
- class: classNames = '',
28
58
  showArrow = true,
29
59
  offsetVal = 10,
30
60
  hidden = $bindable(false),
@@ -38,51 +68,30 @@
38
68
  fallbackPlacements = [],
39
69
  stopPropagation = true,
40
70
  fontColor = 'var(--text2)',
71
+ bgColor = '',
72
+ class: className = '',
41
73
  target,
42
- tooltipContent,
43
- targetText
74
+ tooltip,
75
+ onshow,
76
+ onclose
44
77
  }: Props = $props();
45
78
 
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));
79
+ type TooltipInstance = {
80
+ toggle: HTMLElement;
81
+ tooltip: HTMLElement;
82
+ arrowElement?: HTMLElement;
83
+ cleanup: () => void;
84
+ showTooltip: () => void;
85
+ hideTooltip: () => void;
86
+ };
70
87
 
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
- }
88
+ let tooltipInstance: TooltipInstance | null = $state(null);
89
+ let targetElement: HTMLDivElement | undefined = $state();
90
+ let tooltipElement: HTMLDivElement | undefined = $state();
91
+ let arrowElement: HTMLDivElement | undefined = $state();
92
+ let observer: MutationObserver | null = null;
93
+ let documentClickListener: ((event: MouseEvent) => void) | null = null;
94
+ const tooltipId = `tooltip-${uuidv4()}`;
86
95
 
87
96
  /**
88
97
  * Dismisses other tooltips based on trigger compatibility.
@@ -92,217 +101,197 @@
92
101
  * - hover tooltips should NOT dismiss click tooltips (they're "sticky")
93
102
  * - click tooltips CAN dismiss hover tooltips (higher priority)
94
103
  */
95
- function dismissOtherTooltips() {
104
+ const dismissOtherTooltips = () => {
96
105
  const currentTriggerType = listener === 'click' && listenerout === 'click' ? 'click' : 'hover';
97
106
 
98
- document.querySelectorAll<HTMLDivElement>('.tooltip[role="tooltip"]').forEach((item) => {
99
- if (item.id !== tooltipId) {
100
- const existingTriggerType = item.getAttribute('data-trigger-type');
107
+ activeTooltips.update(() => {
108
+ document.querySelectorAll<HTMLDivElement>('.tooltip[role="tooltip"]').forEach((item) => {
109
+ if (item.id !== tooltipId) {
110
+ const existingTriggerType = item.getAttribute('data-trigger-type');
101
111
 
102
- const shouldDismiss =
103
- currentTriggerType === 'click' ||
104
- (currentTriggerType === 'hover' && existingTriggerType === 'hover');
112
+ const shouldDismiss =
113
+ currentTriggerType === 'click' ||
114
+ (currentTriggerType === 'hover' && existingTriggerType === 'hover'); // Hover only dismisses other hover
105
115
 
106
- if (shouldDismiss) {
107
- item.style.display = 'none';
108
- item.setAttribute('aria-hidden', 'true');
116
+ if (shouldDismiss) {
117
+ item.style.display = 'none';
118
+ item.setAttribute('aria-hidden', 'true');
119
+ }
109
120
  }
110
- }
121
+ });
122
+
123
+ return [tooltipId];
111
124
  });
125
+ };
112
126
 
113
- activeTooltips = [tooltipId];
114
- }
127
+ /**
128
+ * Inits the tooltip.
129
+ */
130
+ const initTooltip = async (): Promise<TooltipInstance | null> => {
131
+ if (!tooltipElement || (showArrow && !arrowElement) || !targetElement) return null;
132
+ return setupTooltip(targetElement, tooltipElement, arrowElement);
133
+ };
115
134
 
116
135
  /**
117
- * Sets up the tooltip with floating-ui positioning
136
+ * Sets up the tooltip.
137
+ * @param toggle
138
+ * @param tooltip
139
+ * @param arrowElement
118
140
  */
119
- function setupTooltip(
141
+ const setupTooltip = (
120
142
  toggle: HTMLElement,
121
- tooltipEl: HTMLElement,
122
- arrowEl?: HTMLElement
123
- ): TooltipInstance {
143
+ tooltip: HTMLElement,
144
+ arrowElement?: HTMLElement
145
+ ): TooltipInstance => {
124
146
  /**
125
- * Updates the tooltip position using floating-ui
147
+ * Updates the tooltip position.
126
148
  */
127
149
  const update = () => {
128
- computePosition(toggle, tooltipEl, {
150
+ computePosition(toggle, tooltip, {
129
151
  placement: placement,
130
152
  middleware: [
131
153
  offset(offsetVal),
132
154
  flip(
133
155
  fallbackPlacements?.length > 0
134
- ? { fallbackPlacements }
156
+ ? {
157
+ fallbackPlacements
158
+ }
135
159
  : {
136
160
  fallbackAxisSideDirection: 'start',
137
161
  fallbackStrategy: 'bestFit'
138
162
  }
139
163
  ),
140
164
  shift({ padding: 5 }),
141
- showArrow && arrowEl ? arrow({ element: arrowEl }) : undefined
165
+ showArrow && arrowElement ? arrow({ element: arrowElement }) : undefined
142
166
  ].filter(Boolean)
143
- }).then(({ x, y, placement: finalPlacement, middlewareData }) => {
144
- // Position tooltip
145
- Object.assign(tooltipEl.style, {
167
+ }).then(({ x, y, placement, middlewareData }) => {
168
+ Object.assign(tooltip.style, {
146
169
  left: `${x}px`,
147
170
  top: `${y}px`
148
171
  });
149
172
 
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];
173
+ // Accessing the data
174
+ const staticSide = {
175
+ top: 'bottom',
176
+ right: 'left',
177
+ bottom: 'top',
178
+ left: 'right'
179
+ }[placement.split('-')[0]];
154
180
 
155
- const staticSide = {
156
- top: 'bottom',
157
- right: 'left',
158
- bottom: 'top',
159
- left: 'right'
160
- }[side];
181
+ if (showArrow && arrowElement) {
182
+ if (!middlewareData?.arrow) return;
183
+ const { x: arrowX, y: arrowY } = middlewareData.arrow;
161
184
 
162
- Object.assign(arrowEl.style, {
185
+ Object.assign(arrowElement.style, {
163
186
  left: arrowX != null ? `${arrowX}px` : '',
164
187
  top: arrowY != null ? `${arrowY}px` : '',
165
188
  right: '',
166
189
  bottom: '',
167
- [staticSide as string]: '-4px'
190
+ [`${staticSide}`]: '-4px'
168
191
  });
169
192
  }
170
193
  });
171
194
  };
172
195
 
173
196
  /**
174
- * Shows the tooltip
197
+ * Shows the tooltip.
175
198
  */
176
199
  const showTooltip = () => {
177
- if (disabled) return;
200
+ if (disabled || !tooltipElement) return;
178
201
 
179
202
  dismissOtherTooltips();
180
203
 
181
- tooltipEl.style.display = 'flex';
182
- tooltipEl.setAttribute('aria-hidden', 'false');
204
+ tooltipElement.style.display = 'flex';
205
+ tooltipElement.setAttribute('aria-hidden', 'false');
183
206
 
184
207
  // Set trigger type for dismissal logic
185
208
  const triggerType = listener === 'click' && listenerout === 'click' ? 'click' : 'hover';
186
- tooltipEl.setAttribute('data-trigger-type', triggerType);
209
+ tooltipElement.setAttribute('data-trigger-type', triggerType);
187
210
 
188
211
  isActive = true;
189
- hidden = false;
190
212
 
191
- if (!activeTooltips.includes(tooltipId)) {
192
- activeTooltips = [...activeTooltips.filter((id) => id !== tooltipId), tooltipId];
193
- }
213
+ activeTooltips.update((ids) => [...ids.filter((id) => id !== tooltipId), tooltipId]);
194
214
 
195
215
  update();
196
- dispatchEvent('show', true);
216
+ onshow?.(true);
197
217
  };
198
218
 
199
219
  /**
200
- * Hides the tooltip
220
+ * Hides the tooltip.
201
221
  */
202
222
  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
223
  setTimeout(() => {
210
- tooltipEl.style.display = 'none';
211
- tooltipEl.setAttribute('aria-hidden', 'true');
212
- tooltipEl.removeAttribute('data-trigger-type');
224
+ if (!tooltipElement) return;
225
+
226
+ tooltipElement.style.display = 'none';
227
+ tooltipElement.setAttribute('aria-hidden', 'true');
228
+ tooltipElement.removeAttribute('data-trigger-type');
213
229
  isActive = false;
214
- hidden = true;
215
- activeTooltips = activeTooltips.filter((id) => id !== tooltipId);
216
230
 
217
- dispatchEvent('close', true);
218
- }, delay);
231
+ activeTooltips.update((ids) => ids.filter((id) => id !== tooltipId));
232
+
233
+ onclose?.(true);
234
+ }, 50);
219
235
  };
220
236
 
221
- // Set up event listeners based on trigger configuration
222
- let eventOptions: Array<[string, () => void]> = [];
237
+ let opts;
223
238
 
224
239
  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)) {
240
+ opts = [['click', showTooltip]];
241
+
242
+ // Store reference to the click handler for cleanup
243
+ documentClickListener = (event: MouseEvent) => {
244
+ if (
245
+ tooltipElement &&
246
+ toggle &&
247
+ !tooltipElement.contains(event.target as Node) &&
248
+ !toggle.contains(event.target as Node)
249
+ ) {
230
250
  hideTooltip();
231
251
  }
232
252
  };
233
253
 
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
- };
254
+ document.addEventListener('click', documentClickListener);
262
255
  } else {
263
- eventOptions = [
256
+ opts = [
264
257
  listener === 'click' ? ['click', showTooltip] : undefined,
265
258
  listener === 'hover' ? ['mouseenter', showTooltip] : undefined,
266
259
  listener === 'hover' ? ['mouseleave', hideTooltip] : undefined,
267
260
  listener === 'hover' ? ['focus', showTooltip] : undefined,
268
261
  ['blur', hideTooltip]
269
- ].filter(Boolean) as Array<[string, () => void]>;
262
+ ];
270
263
  }
271
264
 
272
- // Add event listeners
273
- eventOptions.forEach(([event, handler]) => {
265
+ type EventOption = [string, () => void];
266
+ const options: EventOption[] = opts.filter(Boolean) as EventOption[];
267
+
268
+ options.forEach(([event, listener]) => {
274
269
  toggle.addEventListener(event, (evt) => {
275
270
  if (stopPropagation) {
276
271
  evt.stopPropagation();
277
272
  evt.preventDefault();
278
273
  }
279
- handler();
274
+ listener();
280
275
  });
281
276
  });
282
277
 
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
- }
278
+ const cleanup = () => {
279
+ if (tooltipElement) {
280
+ autoUpdate(toggle, tooltipElement, update)();
281
+ }
282
+ if (documentClickListener) {
283
+ document.removeEventListener('click', documentClickListener);
284
+ documentClickListener = null;
285
+ }
286
+ };
295
287
 
296
- // Public API methods
297
- export function show() {
298
- tooltipInstance?.showTooltip();
299
- }
288
+ return { toggle, tooltip: tooltipElement!, arrowElement, cleanup, showTooltip, hideTooltip };
289
+ };
300
290
 
301
- export function hide() {
302
- tooltipInstance?.hideTooltip();
303
- }
291
+ export const show = () => tooltipInstance?.showTooltip();
292
+ export const hide = () => tooltipInstance?.hideTooltip();
304
293
 
305
- // Effect to handle hidden prop changes
294
+ // Svelte 5 effect for hidden prop
306
295
  $effect(() => {
307
296
  if (hidden) {
308
297
  setTimeout(() => {
@@ -316,129 +305,107 @@
316
305
  }
317
306
  });
318
307
 
319
- $effect(() => {
320
- if (tooltipElement) {
321
- const handleTooltipInteraction = (evt: Event) => {
322
- evt.stopPropagation();
323
- };
308
+ let isClickTarget = $state(listener === 'click' && listenerout === 'click');
324
309
 
325
- tooltipElement.addEventListener('click', handleTooltipInteraction, true);
326
- tooltipElement.addEventListener('mousedown', handleTooltipInteraction, true);
327
- tooltipElement.addEventListener('mouseup', handleTooltipInteraction, true);
310
+ const formattedMessage = $derived(cleanupTooltipMessage(message));
328
311
 
329
- return () => {
330
- tooltipElement.removeEventListener('click', handleTooltipInteraction, true);
331
- tooltipElement.removeEventListener('mousedown', handleTooltipInteraction, true);
332
- tooltipElement.removeEventListener('mouseup', handleTooltipInteraction, true);
333
- };
334
- }
312
+ onMount(async () => {
313
+ await tick();
314
+ tooltipInstance = await initTooltip();
335
315
  });
336
316
 
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
- });
317
+ onDestroy(() => {
318
+ // Disconnect observer first to prevent any new callbacks
319
+ if (observer) {
320
+ observer.disconnect();
321
+ observer = null;
322
+ }
352
323
 
353
- observer.observe(tooltipElement, { attributes: true });
354
- }
355
- });
324
+ // Clean up document click listener
325
+ if (documentClickListener) {
326
+ document.removeEventListener('click', documentClickListener);
327
+ documentClickListener = null;
356
328
  }
357
329
 
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
- };
330
+ if (tooltipInstance) {
331
+ tooltipInstance.cleanup();
332
+ activeTooltips.update((ids) => ids.filter((id) => id !== tooltipId));
333
+ tooltipInstance = null;
334
+ }
369
335
  });
370
336
  </script>
371
337
 
372
- <div class="target {classNames}" bind:this={targetElement} aria-describedby={tooltipId}>
338
+ <div class="target" bind:this={targetElement} aria-describedby={tooltipId}>
373
339
  {#if target}
374
340
  {@render target()}
375
341
  {: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>
342
+ <!-- Default target for Demo only -->
343
+ <Text link label={isClickTarget ? 'Click me' : 'Hover me'} />
388
344
  {/if}
389
345
 
390
346
  <div
391
- class="tooltip"
347
+ class="tooltip {className}"
392
348
  bind:this={tooltipElement}
393
349
  role="tooltip"
394
350
  id={tooltipId}
395
- aria-hidden={isTooltipVisible ? 'false' : 'true'}
396
- style="position:{position}; width:{width}; padding:{padding}; background:{bgColor};"
351
+ style="position:{position}; width:{width}; padding:{padding}; {bgColor
352
+ ? `background-color: ${bgColor};`
353
+ : ''}"
397
354
  >
398
355
  {#if tooltipIcon}
399
- {@const IconComponent = tooltipIcon}
356
+ {@const Icon = tooltipIcon}
400
357
  <div class="icon" style="color:{tooltipIconColor};">
401
- <IconComponent />
358
+ <Icon />
402
359
  </div>
403
360
  {/if}
404
361
 
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>
362
+ {#if tooltip}
363
+ {@render tooltip()}
364
+ {:else if message && raw}
365
+ <div class="message" style="color:{fontColor};">
366
+ {@html message}
367
+ </div>
368
+ {:else if message}
369
+ <div class="message">
370
+ <Text label={formattedMessage} fontSize="11px" fontWeight="500" {fontColor} />
371
+ </div>
372
+ {/if}
418
373
 
419
374
  {#if showArrow}
420
- <div class="arrow" bind:this={arrowElement} style="background: {bgColor};"></div>
375
+ <div
376
+ class="arrow"
377
+ id="arrow_{tooltipId}"
378
+ bind:this={arrowElement}
379
+ style={bgColor ? `background-color: ${bgColor};` : ''}
380
+ ></div>
421
381
  {/if}
422
382
  </div>
423
383
  </div>
424
384
 
425
385
  <style>
386
+ .message {
387
+ background: var(--background3, #383838);
388
+ text-wrap: normal;
389
+ white-space: normal;
390
+ color: var(--text2, #b9b9b9);
391
+ font-weight: 500;
392
+ font-size: 11px;
393
+ }
426
394
  .tooltip {
427
395
  display: none;
428
396
  top: 0;
429
397
  left: 0;
430
- border-radius: 4px;
431
398
  z-index: 99999999999;
432
- border-radius: var(--border-radius, 4px);
433
- background: var(--background3);
399
+ border-radius: 4px;
400
+ background: var(--background3, #383838);
434
401
  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);
402
+ 0px 12px 24px 8px rgb(0 0 0 / 30%),
403
+ 0px 8px 16px 4px rgb(0 0 0 / 30%),
404
+ 0px 4px 8px 2px rgb(0 0 0 / 0%),
405
+ 0px 2px 6px 0px rgba(0, 0, 0, 0.08),
406
+ 0px -0.5px 0.5px 0px rgb(0 0 0 / 0%) inset,
407
+ 0px 0.5px 0.5px 0px rgb(255 255 255 / 34%) inset;
408
+ color: var(--text2, #b9b9b9);
442
409
  font-family: Inter;
443
410
  line-height: 120%;
444
411
  width: 150px;
@@ -448,38 +415,20 @@
448
415
  align-items: start;
449
416
  justify-content: start;
450
417
  }
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
418
  .arrow {
419
+ z-index: 1;
420
+ background: var(--background3, #383838);
469
421
  position: absolute;
470
- width: 10px;
471
- height: 10px;
422
+ width: 8px;
423
+ height: 8px;
472
424
  transform: rotate(45deg);
473
425
  }
474
-
475
- .target {
426
+ .target :global(div[slot='tooltip']) {
427
+ width: 100%;
428
+ }
429
+ .target :global(div[slot='target']) {
476
430
  display: flex;
477
431
  align-items: center;
478
432
  justify-content: center;
479
433
  }
480
-
481
- .icon {
482
- position: relative;
483
- z-index: 1;
484
- }
485
434
  </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.16",
4
4
  "description": "Shared utilities for Webflow apps",
5
5
  "homepage": "https://github.com/finsweet/webflow-apps-utils",
6
6
  "repository": {