@juspay/svelte-ui-components 2.80.7 → 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/Calendar/Calendar.svelte +8 -0
- package/dist/DateRangePicker/DateRangePicker.svelte +2 -2
- package/dist/DateRangePicker/properties.d.ts +1 -0
- package/dist/DateRangePicker/timeUtils.d.ts +2 -1
- package/dist/DateRangePicker/timeUtils.js +7 -3
- 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) {
|
|
@@ -154,6 +154,12 @@
|
|
|
154
154
|
return n;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
function endOfDay(d: Date): Date {
|
|
158
|
+
const newDate = new SvelteDate(d);
|
|
159
|
+
newDate.setHours(23, 59, 59, 999);
|
|
160
|
+
return newDate;
|
|
161
|
+
}
|
|
162
|
+
|
|
157
163
|
function isSameDay(a: Date, b: Date): boolean {
|
|
158
164
|
return (
|
|
159
165
|
a.getFullYear() === b.getFullYear() &&
|
|
@@ -206,6 +212,8 @@
|
|
|
206
212
|
if (date.getTime() < normalizeDate(start).getTime()) {
|
|
207
213
|
rangeEnd = start;
|
|
208
214
|
rangeStart = date;
|
|
215
|
+
} else if (isSameDay(date, start)) {
|
|
216
|
+
rangeEnd = endOfDay(date);
|
|
209
217
|
} else {
|
|
210
218
|
rangeEnd = date;
|
|
211
219
|
}
|
|
@@ -347,10 +347,10 @@
|
|
|
347
347
|
if (draftStart !== null && draftEnd !== null) {
|
|
348
348
|
// Fold the time-of-day inputs onto the committed dates when time selection is on.
|
|
349
349
|
const appliedStart = showTimeSelection
|
|
350
|
-
? (applyTimeDisplay(draftStart, startTimeDisplay) ?? draftStart)
|
|
350
|
+
? (applyTimeDisplay(draftStart, startTimeDisplay, 'start') ?? draftStart)
|
|
351
351
|
: draftStart;
|
|
352
352
|
const appliedEnd = showTimeSelection
|
|
353
|
-
? (applyTimeDisplay(draftEnd, endTimeDisplay) ?? draftEnd)
|
|
353
|
+
? (applyTimeDisplay(draftEnd, endTimeDisplay, 'end') ?? draftEnd)
|
|
354
354
|
: draftEnd;
|
|
355
355
|
rangeStart = appliedStart;
|
|
356
356
|
rangeEnd = appliedEnd;
|
|
@@ -16,6 +16,7 @@ export type DateRangePreset = {
|
|
|
16
16
|
export type DateRangePickerMode = 'range' | 'single';
|
|
17
17
|
export type DateRangePickerAlign = 'left' | 'right';
|
|
18
18
|
export type DateRangePickerTimeLayout = 'toggle' | 'inline';
|
|
19
|
+
export type TimeDisplayBoundary = 'start' | 'end';
|
|
19
20
|
export type OptionalDateRangePickerProperties = {
|
|
20
21
|
/** Currently selected start of range (bindable). */
|
|
21
22
|
rangeStart?: Date | null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TimeDisplayBoundary } from './properties';
|
|
1
2
|
/** Matches a 12-hour clock display such as "2:30 PM" or "02:05 am". */
|
|
2
3
|
export declare const TIME_DISPLAY_PATTERN: RegExp;
|
|
3
4
|
/** Parse a 12-hour display string into 24-hour hours/minutes, or null when invalid. */
|
|
@@ -8,6 +9,6 @@ export declare const parseTimeDisplay: (display: string) => {
|
|
|
8
9
|
/** Format a Date's time-of-day as a 12-hour display string, e.g. "02:30 PM". */
|
|
9
10
|
export declare const formatTimeDisplay: (date: Date) => string;
|
|
10
11
|
/** Return a new Date with the time-of-day from a 12-hour display string applied, or null when invalid. */
|
|
11
|
-
export declare const applyTimeDisplay: (date: Date, display: string) => Date | null;
|
|
12
|
+
export declare const applyTimeDisplay: (date: Date, display: string, boundary: TimeDisplayBoundary) => Date | null;
|
|
12
13
|
/** Minutes-since-midnight for ordering comparisons, or null when the display is invalid. */
|
|
13
14
|
export declare const toMinutesOfDay: (display: string) => number | null;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// Time-of-day helpers for the DateRangePicker's built-in date + time inputs.
|
|
2
1
|
/** Matches a 12-hour clock display such as "2:30 PM" or "02:05 am". */
|
|
3
2
|
export const TIME_DISPLAY_PATTERN = /^(1[0-2]|0?[1-9]):([0-5][0-9])\s?(AM|PM)$/i;
|
|
4
3
|
const pad = (n) => n.toString().padStart(2, '0');
|
|
@@ -27,13 +26,18 @@ export const formatTimeDisplay = (date) => {
|
|
|
27
26
|
return `${pad(hours12)}:${pad(date.getMinutes())} ${meridiem}`;
|
|
28
27
|
};
|
|
29
28
|
/** Return a new Date with the time-of-day from a 12-hour display string applied, or null when invalid. */
|
|
30
|
-
export const applyTimeDisplay = (date, display) => {
|
|
29
|
+
export const applyTimeDisplay = (date, display, boundary) => {
|
|
31
30
|
const parsed = parseTimeDisplay(display);
|
|
32
31
|
if (parsed === null) {
|
|
33
32
|
return null;
|
|
34
33
|
}
|
|
35
34
|
const next = new Date(date.getTime());
|
|
36
|
-
|
|
35
|
+
if (boundary === 'end') {
|
|
36
|
+
next.setHours(parsed.hours, parsed.minutes, 59, 999);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
next.setHours(parsed.hours, parsed.minutes, 0, 0);
|
|
40
|
+
}
|
|
37
41
|
return next;
|
|
38
42
|
};
|
|
39
43
|
/** Minutes-since-midnight for ordering comparisons, or null when the display is invalid. */
|
|
@@ -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;
|