@juspay/svelte-ui-components 2.28.3 → 2.30.0
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.
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
minDate = null,
|
|
14
14
|
maxDate = null,
|
|
15
15
|
disabledDates = [],
|
|
16
|
+
maxRangeDays = null,
|
|
16
17
|
presets = null,
|
|
17
18
|
placeholder = 'Select date',
|
|
18
19
|
dualMonth,
|
|
@@ -44,6 +45,41 @@
|
|
|
44
45
|
let draftCompareStart: Date | null = $state(null);
|
|
45
46
|
let draftCompareEnd: Date | null = $state(null);
|
|
46
47
|
|
|
48
|
+
// Tracks the label of the currently active preset, or null when the user
|
|
49
|
+
// made a custom calendar selection (or before any selection is made).
|
|
50
|
+
let selectedPresetLabel: string | null = $state(null);
|
|
51
|
+
|
|
52
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
53
|
+
|
|
54
|
+
function isBaseDisabledDate(date: Date): boolean {
|
|
55
|
+
if (typeof disabledDates === 'function') {
|
|
56
|
+
return disabledDates(date);
|
|
57
|
+
}
|
|
58
|
+
return disabledDates.some((disabled) => isSameDay(disabled, date));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// When maxRangeDays is set, disable any date whose span from the in-progress
|
|
62
|
+
// start would exceed the limit (inclusive of both ends), until the range is
|
|
63
|
+
// completed. Falls back to the raw disabledDates otherwise.
|
|
64
|
+
const rangeConstrainedDisabledDates = $derived.by(() => {
|
|
65
|
+
const limit = maxRangeDays;
|
|
66
|
+
if (limit === null) {
|
|
67
|
+
return disabledDates;
|
|
68
|
+
}
|
|
69
|
+
const anchor = draftStart;
|
|
70
|
+
const selecting = anchor !== null && draftEnd === null;
|
|
71
|
+
return (date: Date): boolean => {
|
|
72
|
+
if (isBaseDisabledDate(date)) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
if (!selecting || anchor === null) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
const diffDays = Math.abs(Math.round((date.getTime() - anchor.getTime()) / MS_PER_DAY));
|
|
79
|
+
return diffDays > limit - 1;
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
|
|
47
83
|
let isOpen: boolean = $state(false);
|
|
48
84
|
let panelRef: HTMLDivElement | null = $state(null);
|
|
49
85
|
let triggerRef: HTMLDivElement | null = $state(null);
|
|
@@ -93,6 +129,9 @@
|
|
|
93
129
|
draftCompareStart = compareStart !== null ? compareStart : null;
|
|
94
130
|
draftCompareEnd = compareEnd !== null ? compareEnd : null;
|
|
95
131
|
|
|
132
|
+
// Reset preset tracking so each picker session starts clean.
|
|
133
|
+
selectedPresetLabel = null;
|
|
134
|
+
|
|
96
135
|
// Navigate left calendar so it shows the committed start month (or today)
|
|
97
136
|
const anchor = rangeStart !== null ? rangeStart : value !== null ? value : now;
|
|
98
137
|
leftYear = anchor.getFullYear();
|
|
@@ -120,7 +159,7 @@
|
|
|
120
159
|
if (draftStart !== null && draftEnd !== null) {
|
|
121
160
|
rangeStart = draftStart;
|
|
122
161
|
rangeEnd = draftEnd;
|
|
123
|
-
onapply?.({ rangeStart: draftStart, rangeEnd: draftEnd });
|
|
162
|
+
onapply?.({ rangeStart: draftStart, rangeEnd: draftEnd, presetLabel: selectedPresetLabel });
|
|
124
163
|
}
|
|
125
164
|
if (
|
|
126
165
|
typeof compareCalendar === 'function' &&
|
|
@@ -129,12 +168,16 @@
|
|
|
129
168
|
) {
|
|
130
169
|
compareStart = draftCompareStart;
|
|
131
170
|
compareEnd = draftCompareEnd;
|
|
132
|
-
onapplycompare?.({
|
|
171
|
+
onapplycompare?.({
|
|
172
|
+
compareStart: draftCompareStart,
|
|
173
|
+
compareEnd: draftCompareEnd,
|
|
174
|
+
presetLabel: selectedPresetLabel
|
|
175
|
+
});
|
|
133
176
|
}
|
|
134
177
|
} else {
|
|
135
178
|
if (draftValue !== null) {
|
|
136
179
|
value = draftValue;
|
|
137
|
-
onapplysingle?.({ date: draftValue });
|
|
180
|
+
onapplysingle?.({ date: draftValue, presetLabel: selectedPresetLabel });
|
|
138
181
|
}
|
|
139
182
|
}
|
|
140
183
|
closePicker();
|
|
@@ -147,6 +190,7 @@
|
|
|
147
190
|
|
|
148
191
|
function handlePreset(preset: DateRangePreset): void {
|
|
149
192
|
const { start, end } = preset.getValue();
|
|
193
|
+
selectedPresetLabel = preset.label;
|
|
150
194
|
if (mode === 'range') {
|
|
151
195
|
draftStart = start;
|
|
152
196
|
draftEnd = end;
|
|
@@ -162,10 +206,14 @@
|
|
|
162
206
|
function handleRangeSelect(event: { rangeStart: Date; rangeEnd: Date }): void {
|
|
163
207
|
draftStart = event.rangeStart;
|
|
164
208
|
draftEnd = event.rangeEnd;
|
|
209
|
+
// A direct calendar click is not a preset selection.
|
|
210
|
+
selectedPresetLabel = null;
|
|
165
211
|
}
|
|
166
212
|
|
|
167
213
|
function handleSingleSelect(event: { date: Date }): void {
|
|
168
214
|
draftValue = event.date;
|
|
215
|
+
// A direct calendar click is not a preset selection.
|
|
216
|
+
selectedPresetLabel = null;
|
|
169
217
|
}
|
|
170
218
|
|
|
171
219
|
// Close on outside-click
|
|
@@ -313,7 +361,7 @@
|
|
|
313
361
|
{locale}
|
|
314
362
|
{minDate}
|
|
315
363
|
{maxDate}
|
|
316
|
-
{
|
|
364
|
+
disabledDates={rangeConstrainedDisabledDates}
|
|
317
365
|
initialMonth={leftInitialMonth}
|
|
318
366
|
onrangeselect={handleRangeSelect}
|
|
319
367
|
classes="drp-calendar-embedded"
|
|
@@ -327,7 +375,7 @@
|
|
|
327
375
|
{locale}
|
|
328
376
|
{minDate}
|
|
329
377
|
{maxDate}
|
|
330
|
-
{
|
|
378
|
+
disabledDates={rangeConstrainedDisabledDates}
|
|
331
379
|
initialMonth={rightInitialMonth}
|
|
332
380
|
onrangeselect={handleRangeSelect}
|
|
333
381
|
classes="drp-calendar-embedded"
|
|
@@ -22,6 +22,8 @@ export type OptionalDateRangePickerProperties = {
|
|
|
22
22
|
maxDate?: Date | null;
|
|
23
23
|
/** Specific dates to disable, or a predicate. */
|
|
24
24
|
disabledDates?: Date[] | ((date: Date) => boolean);
|
|
25
|
+
/** Maximum number of days a selected range may span, inclusive of both endpoints (range mode only). Once a start date is picked, dates that would exceed this span are disabled until the range is completed. Omit for no limit. */
|
|
26
|
+
maxRangeDays?: number | null;
|
|
25
27
|
/** Preset options shown in the sidebar. Omit to hide the sidebar. */
|
|
26
28
|
presets?: DateRangePreset[] | null;
|
|
27
29
|
/** Placeholder shown when no range is selected. */
|
|
@@ -50,19 +52,22 @@ export type OptionalDateRangePickerProperties = {
|
|
|
50
52
|
triggerIcon?: Snippet;
|
|
51
53
|
};
|
|
52
54
|
export type DateRangePickerEventProperties = {
|
|
53
|
-
/** Fired when the user clicks Apply in range mode. */
|
|
55
|
+
/** Fired when the user clicks Apply in range mode. presetLabel is the sidebar preset name if one was active, or null for a custom calendar selection. */
|
|
54
56
|
onapply?: (event: {
|
|
55
57
|
rangeStart: Date;
|
|
56
58
|
rangeEnd: Date;
|
|
59
|
+
presetLabel: string | null;
|
|
57
60
|
}) => void;
|
|
58
|
-
/** Fired when the user clicks Apply in single mode. */
|
|
61
|
+
/** Fired when the user clicks Apply in single mode. presetLabel is the sidebar preset name if one was active, or null for a custom calendar selection. */
|
|
59
62
|
onapplysingle?: (event: {
|
|
60
63
|
date: Date;
|
|
64
|
+
presetLabel: string | null;
|
|
61
65
|
}) => void;
|
|
62
|
-
/** Fired when the compare range is applied. */
|
|
66
|
+
/** Fired when the compare range is applied. presetLabel is the sidebar preset name if one was active, or null for a custom calendar selection. */
|
|
63
67
|
onapplycompare?: (event: {
|
|
64
68
|
compareStart: Date;
|
|
65
69
|
compareEnd: Date;
|
|
70
|
+
presetLabel: string | null;
|
|
66
71
|
}) => void;
|
|
67
72
|
/** Fired when the user dismisses without applying. */
|
|
68
73
|
oncancel?: () => void;
|
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
bottomContent,
|
|
15
15
|
optionIndicator,
|
|
16
16
|
testId,
|
|
17
|
+
itemTestId,
|
|
17
18
|
onchange,
|
|
18
19
|
classes,
|
|
19
|
-
open = $bindable(false)
|
|
20
|
+
open = $bindable(false),
|
|
21
|
+
dropdownAlign = 'left'
|
|
20
22
|
}: SelectProperties = $props();
|
|
21
23
|
|
|
22
24
|
function normalizeItems(source: SelectItem[] | string[]): SelectItem[] {
|
|
@@ -302,12 +304,17 @@
|
|
|
302
304
|
</div>
|
|
303
305
|
|
|
304
306
|
{#if open && !disabled}
|
|
305
|
-
<div
|
|
307
|
+
<div
|
|
308
|
+
class="select-dropdown"
|
|
309
|
+
class:select-dropdown-right={dropdownAlign === 'right'}
|
|
310
|
+
role="listbox"
|
|
311
|
+
id={listboxId}
|
|
312
|
+
aria-multiselectable={multiple}
|
|
313
|
+
>
|
|
306
314
|
{#if filteredItems.length === 0}
|
|
307
315
|
<div class="select-empty">No results</div>
|
|
308
316
|
{:else}
|
|
309
317
|
{#each filteredItems as item, index (item.id)}
|
|
310
|
-
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
311
318
|
<div
|
|
312
319
|
class="select-option"
|
|
313
320
|
class:multi={multiple}
|
|
@@ -317,6 +324,13 @@
|
|
|
317
324
|
id={`${listboxId}-option-${index}`}
|
|
318
325
|
aria-selected={value.includes(item.id)}
|
|
319
326
|
tabindex="-1"
|
|
327
|
+
{...item.testId
|
|
328
|
+
? { 'data-pw': item.testId }
|
|
329
|
+
: typeof itemTestId === 'string'
|
|
330
|
+
? { 'data-pw': `${itemTestId}-${item.id}` }
|
|
331
|
+
: typeof testId === 'string'
|
|
332
|
+
? { 'data-pw': `${testId}-${item.id}` }
|
|
333
|
+
: {}}
|
|
320
334
|
onclick={() => selectItem(item.id)}
|
|
321
335
|
onmouseenter={() => (highlightedIndex = index)}
|
|
322
336
|
>
|
|
@@ -450,6 +464,13 @@
|
|
|
450
464
|
z-index: var(--select-dropdown-z-index, 10);
|
|
451
465
|
}
|
|
452
466
|
|
|
467
|
+
.select-dropdown-right {
|
|
468
|
+
left: auto;
|
|
469
|
+
right: 0;
|
|
470
|
+
min-width: 100%;
|
|
471
|
+
width: max-content;
|
|
472
|
+
}
|
|
473
|
+
|
|
453
474
|
.select-option {
|
|
454
475
|
padding: var(--select-option-padding, 8px 12px);
|
|
455
476
|
color: var(--select-option-color, #333333);
|
|
@@ -3,6 +3,8 @@ export type SelectProperties = MandatorySelectProperties & OptionalSelectPropert
|
|
|
3
3
|
export type SelectItem = {
|
|
4
4
|
id: string;
|
|
5
5
|
label: string;
|
|
6
|
+
/** Optional per-option test id, emitted as `data-pw` on the option element. */
|
|
7
|
+
testId?: string;
|
|
6
8
|
};
|
|
7
9
|
export type MandatorySelectProperties = {
|
|
8
10
|
items: SelectItem[] | string[];
|
|
@@ -18,9 +20,13 @@ export type OptionalSelectProperties = {
|
|
|
18
20
|
checked: boolean;
|
|
19
21
|
}]>;
|
|
20
22
|
testId?: string;
|
|
23
|
+
/** Fallback per-option test id prefix. Each option emits `data-pw="{itemTestId}-{id}"` when its own `item.testId` is not set. */
|
|
24
|
+
itemTestId?: string;
|
|
21
25
|
classes?: string;
|
|
22
26
|
/** Bindable. Controls whether the dropdown is open; the component writes back on open/close so parents can `bind:open` to observe or drive it. Unbound, the component manages its own state. */
|
|
23
27
|
open?: boolean;
|
|
28
|
+
/** Horizontal anchor of the dropdown panel. `'left'` (default) anchors to the trigger's left edge; `'right'` anchors to the right edge so a content-wider panel hangs leftward instead of overflowing. */
|
|
29
|
+
dropdownAlign?: 'left' | 'right';
|
|
24
30
|
};
|
|
25
31
|
export type SelectEventProperties = {
|
|
26
32
|
onchange?: (value: string[]) => void;
|