@juspay/svelte-ui-components 2.80.8 → 2.80.10

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.
@@ -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) {
@@ -982,6 +982,7 @@
982
982
  flex-direction: column;
983
983
  min-width: var(--drp-panel-min-width, 320px);
984
984
  max-width: var(--drp-panel-max-width, 760px);
985
+ max-height: var(--drp-panel-max-height, calc(100dvh - 80px));
985
986
  overflow: hidden;
986
987
  }
987
988
 
@@ -998,6 +999,9 @@
998
999
  .drp-panel-inner {
999
1000
  display: flex;
1000
1001
  flex-direction: row;
1002
+ flex: 1;
1003
+ min-height: 0;
1004
+ overflow-y: auto;
1001
1005
  }
1002
1006
 
1003
1007
  /* ── Sidebar ── */
@@ -1139,7 +1143,10 @@
1139
1143
  --calendar-box-shadow: none;
1140
1144
  --calendar-background: transparent;
1141
1145
  --calendar-padding: 0;
1142
- /* Suppress Calendar's own header in dual-month context via CSS var */
1146
+ }
1147
+
1148
+ /* Suppress Calendar's own header in dual-month mode; single mode keeps it for navigation */
1149
+ :global(.drp-months-row .drp-calendar-embedded) {
1143
1150
  --calendar-header-display: none;
1144
1151
  }
1145
1152
 
@@ -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 class="tooltip-bubble {position}" role="tooltip">
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.80.8",
3
+ "version": "2.80.10",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",