@juspay/svelte-ui-components 2.80.8 → 2.80.9
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/dist/Button/Button.svelte +6 -0
- package/dist/Tooltip/Tooltip.svelte +160 -10
- package/package.json +1 -1
|
@@ -248,6 +248,12 @@
|
|
|
248
248
|
.button-text {
|
|
249
249
|
order: var(--button-text-order, 3);
|
|
250
250
|
display: var(--button-text-display);
|
|
251
|
+
|
|
252
|
+
/* With nowrap as the default, a label longer than the button's box would
|
|
253
|
+
otherwise bleed past it — truncate with an ellipsis instead. Full labels
|
|
254
|
+
stay available via the button's accessible name / title at the call site. */
|
|
255
|
+
overflow: hidden;
|
|
256
|
+
text-overflow: ellipsis;
|
|
251
257
|
}
|
|
252
258
|
|
|
253
259
|
.button-el:hover:not(.disabled) {
|
|
@@ -18,6 +18,77 @@
|
|
|
18
18
|
let visible = $state(false);
|
|
19
19
|
let delayTimeout = $state<ReturnType<typeof setTimeout> | null>(null);
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Cross-axis shift (px) applied to the inline bubble so it stays inside the
|
|
23
|
+
* viewport: horizontal for top/bottom tooltips, vertical for left/right ones.
|
|
24
|
+
* The arrow compensates by the same amount, so it keeps pointing at the trigger.
|
|
25
|
+
*/
|
|
26
|
+
let bubbleShift = $state(0);
|
|
27
|
+
|
|
28
|
+
/** Minimum gap kept between the bubble and the viewport edge. */
|
|
29
|
+
const VIEWPORT_MARGIN = 8;
|
|
30
|
+
|
|
31
|
+
/** Opposite side per position, used to flip a bubble whose main axis overflows. */
|
|
32
|
+
const OPPOSITE_POSITION: Record<TooltipPosition, TooltipPosition> = {
|
|
33
|
+
top: 'bottom',
|
|
34
|
+
bottom: 'top',
|
|
35
|
+
left: 'right',
|
|
36
|
+
right: 'left'
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Set when the bubble's own side overflows the viewport (e.g. a `right`
|
|
41
|
+
* tooltip on a trigger near the right screen edge): the bubble renders on
|
|
42
|
+
* the opposite side instead, where cross-axis shifting alone cannot help.
|
|
43
|
+
*/
|
|
44
|
+
let flipped = $state(false);
|
|
45
|
+
|
|
46
|
+
const displayPosition = $derived(flipped ? OPPOSITE_POSITION[position] : position);
|
|
47
|
+
|
|
48
|
+
const mainAxisOverflows = (bubbleRect: DOMRect, pos: TooltipPosition): boolean => {
|
|
49
|
+
if (pos === 'right') {
|
|
50
|
+
return bubbleRect.right > window.innerWidth - VIEWPORT_MARGIN;
|
|
51
|
+
}
|
|
52
|
+
if (pos === 'left') {
|
|
53
|
+
return bubbleRect.left < VIEWPORT_MARGIN;
|
|
54
|
+
}
|
|
55
|
+
if (pos === 'top') {
|
|
56
|
+
return bubbleRect.top < VIEWPORT_MARGIN;
|
|
57
|
+
}
|
|
58
|
+
return bubbleRect.bottom > window.innerHeight - VIEWPORT_MARGIN;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Action for the inline (non-portal) bubble: on mount it measures the bubble's
|
|
63
|
+
* natural position (shift is 0 and no flip is applied at that point) and keeps
|
|
64
|
+
* it inside the viewport — flipping to the opposite side when its own side
|
|
65
|
+
* overflows, and shifting along the cross axis when the perpendicular edges
|
|
66
|
+
* clip. Flipping mirrors only the main axis, so the cross-axis measurement
|
|
67
|
+
* stays valid after a flip. On unmount both reset so the next show starts
|
|
68
|
+
* from the natural position.
|
|
69
|
+
*/
|
|
70
|
+
const clampInlineBubble = (node: HTMLElement): { destroy: () => void } => {
|
|
71
|
+
if (typeof window !== 'undefined') {
|
|
72
|
+
const bubbleRect = node.getBoundingClientRect();
|
|
73
|
+
flipped = mainAxisOverflows(bubbleRect, position);
|
|
74
|
+
if (position === 'top' || position === 'bottom') {
|
|
75
|
+
const overflowRight = bubbleRect.right - (window.innerWidth - VIEWPORT_MARGIN);
|
|
76
|
+
const overflowLeft = VIEWPORT_MARGIN - bubbleRect.left;
|
|
77
|
+
bubbleShift = overflowRight > 0 ? -overflowRight : overflowLeft > 0 ? overflowLeft : 0;
|
|
78
|
+
} else {
|
|
79
|
+
const overflowBottom = bubbleRect.bottom - (window.innerHeight - VIEWPORT_MARGIN);
|
|
80
|
+
const overflowTop = VIEWPORT_MARGIN - bubbleRect.top;
|
|
81
|
+
bubbleShift = overflowBottom > 0 ? -overflowBottom : overflowTop > 0 ? overflowTop : 0;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
destroy: () => {
|
|
86
|
+
bubbleShift = 0;
|
|
87
|
+
flipped = false;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
21
92
|
/** Reference to the trigger wrapper element, used for `getBoundingClientRect` in portal mode. */
|
|
22
93
|
let containerEl: HTMLDivElement | null = $state(null);
|
|
23
94
|
|
|
@@ -86,6 +157,76 @@
|
|
|
86
157
|
return `${base}top:50%;right:100%;transform:translateY(-50%);border-width:${arrowSize}px ${arrowSize}px ${arrowSize}px 0;border-color:${t} ${bg} ${t} ${t};`;
|
|
87
158
|
};
|
|
88
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Flips an appended portal bubble to the opposite side of the trigger when
|
|
162
|
+
* its own side overflows the viewport, repositioning the bubble and its
|
|
163
|
+
* arrow. Returns the side the bubble ends up on.
|
|
164
|
+
*/
|
|
165
|
+
const flipPortalBubbleIfNeeded = (
|
|
166
|
+
bubble: HTMLDivElement,
|
|
167
|
+
rect: DOMRect,
|
|
168
|
+
pos: TooltipPosition
|
|
169
|
+
): TooltipPosition => {
|
|
170
|
+
if (typeof window === 'undefined') {
|
|
171
|
+
return pos;
|
|
172
|
+
}
|
|
173
|
+
const bubbleRect = bubble.getBoundingClientRect();
|
|
174
|
+
if (!mainAxisOverflows(bubbleRect, pos)) {
|
|
175
|
+
return pos;
|
|
176
|
+
}
|
|
177
|
+
const flippedPos = OPPOSITE_POSITION[pos];
|
|
178
|
+
const coords = computePortalCoords(rect, flippedPos);
|
|
179
|
+
bubble.style.top = `${coords.top}px`;
|
|
180
|
+
bubble.style.left = `${coords.left}px`;
|
|
181
|
+
bubble.style.transform = coords.transform;
|
|
182
|
+
const arrowEl = bubble.firstElementChild;
|
|
183
|
+
if (arrowEl instanceof HTMLElement) {
|
|
184
|
+
arrowEl.style.cssText = computeArrowStyle(flippedPos);
|
|
185
|
+
}
|
|
186
|
+
return flippedPos;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Shifts an appended portal bubble back inside the viewport (cross-axis only)
|
|
191
|
+
* and moves its arrow the opposite way so it still points at the trigger.
|
|
192
|
+
* Margins are used so the shift composes with the centring transform.
|
|
193
|
+
*/
|
|
194
|
+
const clampPortalBubble = (bubble: HTMLDivElement, pos: TooltipPosition): void => {
|
|
195
|
+
if (typeof window === 'undefined') {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const bubbleRect = bubble.getBoundingClientRect();
|
|
199
|
+
let shiftX = 0;
|
|
200
|
+
let shiftY = 0;
|
|
201
|
+
if (pos === 'top' || pos === 'bottom') {
|
|
202
|
+
if (bubbleRect.right > window.innerWidth - VIEWPORT_MARGIN) {
|
|
203
|
+
shiftX = window.innerWidth - VIEWPORT_MARGIN - bubbleRect.right;
|
|
204
|
+
} else if (bubbleRect.left < VIEWPORT_MARGIN) {
|
|
205
|
+
shiftX = VIEWPORT_MARGIN - bubbleRect.left;
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
if (bubbleRect.bottom > window.innerHeight - VIEWPORT_MARGIN) {
|
|
209
|
+
shiftY = window.innerHeight - VIEWPORT_MARGIN - bubbleRect.bottom;
|
|
210
|
+
} else if (bubbleRect.top < VIEWPORT_MARGIN) {
|
|
211
|
+
shiftY = VIEWPORT_MARGIN - bubbleRect.top;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (shiftX === 0 && shiftY === 0) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
bubble.style.marginLeft = `${shiftX}px`;
|
|
218
|
+
bubble.style.marginTop = `${shiftY}px`;
|
|
219
|
+
const arrowEl = bubble.firstElementChild;
|
|
220
|
+
if (arrowEl instanceof HTMLElement) {
|
|
221
|
+
if (shiftX !== 0) {
|
|
222
|
+
arrowEl.style.left = `calc(50% - ${shiftX}px)`;
|
|
223
|
+
}
|
|
224
|
+
if (shiftY !== 0) {
|
|
225
|
+
arrowEl.style.top = `calc(50% - ${shiftY}px)`;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
89
230
|
const createPortalBubble = (rect: DOMRect, pos: TooltipPosition): HTMLDivElement | null => {
|
|
90
231
|
if (typeof document === 'undefined') {
|
|
91
232
|
return null;
|
|
@@ -159,6 +300,8 @@
|
|
|
159
300
|
portalBubbleEl = createPortalBubble(rect, pos);
|
|
160
301
|
if (portalBubbleEl !== null) {
|
|
161
302
|
document.body.appendChild(portalBubbleEl);
|
|
303
|
+
const effectivePos = flipPortalBubbleIfNeeded(portalBubbleEl, rect, pos);
|
|
304
|
+
clampPortalBubble(portalBubbleEl, effectivePos);
|
|
162
305
|
}
|
|
163
306
|
}
|
|
164
307
|
};
|
|
@@ -217,7 +360,12 @@
|
|
|
217
360
|
{/if}
|
|
218
361
|
{@render children()}
|
|
219
362
|
{#if visible && !usePortal}
|
|
220
|
-
<div
|
|
363
|
+
<div
|
|
364
|
+
use:clampInlineBubble
|
|
365
|
+
class="tooltip-bubble {displayPosition}"
|
|
366
|
+
role="tooltip"
|
|
367
|
+
style:--tooltip-shift="{bubbleShift}px"
|
|
368
|
+
>
|
|
221
369
|
<div class="tooltip-arrow"></div>
|
|
222
370
|
{#if typeof content === 'function'}
|
|
223
371
|
{@render content()}
|
|
@@ -266,16 +414,18 @@
|
|
|
266
414
|
border-style: solid;
|
|
267
415
|
}
|
|
268
416
|
|
|
269
|
-
/* Top position
|
|
417
|
+
/* Top position. --tooltip-shift is the viewport-edge clamp: the bubble slides
|
|
418
|
+
along its cross axis to stay on screen while the arrow compensates the other
|
|
419
|
+
way so it keeps pointing at the trigger. */
|
|
270
420
|
.top {
|
|
271
421
|
bottom: calc(100% + var(--tooltip-offset, 8px));
|
|
272
422
|
left: 50%;
|
|
273
|
-
transform: translateX(-50%);
|
|
423
|
+
transform: translateX(calc(-50% + var(--tooltip-shift, 0px)));
|
|
274
424
|
}
|
|
275
425
|
|
|
276
426
|
.top .tooltip-arrow {
|
|
277
427
|
top: 100%;
|
|
278
|
-
left: 50
|
|
428
|
+
left: calc(50% - var(--tooltip-shift, 0px));
|
|
279
429
|
transform: translateX(-50%);
|
|
280
430
|
border-width: var(--tooltip-arrow-size, 5px) var(--tooltip-arrow-size, 5px) 0
|
|
281
431
|
var(--tooltip-arrow-size, 5px);
|
|
@@ -287,12 +437,12 @@
|
|
|
287
437
|
.bottom {
|
|
288
438
|
top: calc(100% + var(--tooltip-offset, 8px));
|
|
289
439
|
left: 50%;
|
|
290
|
-
transform: translateX(-50%);
|
|
440
|
+
transform: translateX(calc(-50% + var(--tooltip-shift, 0px)));
|
|
291
441
|
}
|
|
292
442
|
|
|
293
443
|
.bottom .tooltip-arrow {
|
|
294
444
|
bottom: 100%;
|
|
295
|
-
left: 50
|
|
445
|
+
left: calc(50% - var(--tooltip-shift, 0px));
|
|
296
446
|
transform: translateX(-50%);
|
|
297
447
|
border-width: 0 var(--tooltip-arrow-size, 5px) var(--tooltip-arrow-size, 5px)
|
|
298
448
|
var(--tooltip-arrow-size, 5px);
|
|
@@ -304,12 +454,12 @@
|
|
|
304
454
|
.left {
|
|
305
455
|
right: calc(100% + var(--tooltip-offset, 8px));
|
|
306
456
|
top: 50%;
|
|
307
|
-
transform: translateY(-50%);
|
|
457
|
+
transform: translateY(calc(-50% + var(--tooltip-shift, 0px)));
|
|
308
458
|
}
|
|
309
459
|
|
|
310
460
|
.left .tooltip-arrow {
|
|
311
461
|
left: 100%;
|
|
312
|
-
top: 50
|
|
462
|
+
top: calc(50% - var(--tooltip-shift, 0px));
|
|
313
463
|
transform: translateY(-50%);
|
|
314
464
|
border-width: var(--tooltip-arrow-size, 5px) 0 var(--tooltip-arrow-size, 5px)
|
|
315
465
|
var(--tooltip-arrow-size, 5px);
|
|
@@ -321,12 +471,12 @@
|
|
|
321
471
|
.right {
|
|
322
472
|
left: calc(100% + var(--tooltip-offset, 8px));
|
|
323
473
|
top: 50%;
|
|
324
|
-
transform: translateY(-50%);
|
|
474
|
+
transform: translateY(calc(-50% + var(--tooltip-shift, 0px)));
|
|
325
475
|
}
|
|
326
476
|
|
|
327
477
|
.right .tooltip-arrow {
|
|
328
478
|
right: 100%;
|
|
329
|
-
top: 50
|
|
479
|
+
top: calc(50% - var(--tooltip-shift, 0px));
|
|
330
480
|
transform: translateY(-50%);
|
|
331
481
|
border-width: var(--tooltip-arrow-size, 5px) var(--tooltip-arrow-size, 5px)
|
|
332
482
|
var(--tooltip-arrow-size, 5px) 0;
|