@juspay/svelte-ui-components 2.29.0 → 2.31.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;
|
|
@@ -13,11 +13,13 @@
|
|
|
13
13
|
disabled = false,
|
|
14
14
|
bottomContent,
|
|
15
15
|
optionIndicator,
|
|
16
|
+
triggerSummary,
|
|
16
17
|
testId,
|
|
17
18
|
itemTestId,
|
|
18
19
|
onchange,
|
|
19
20
|
classes,
|
|
20
|
-
open = $bindable(false)
|
|
21
|
+
open = $bindable(false),
|
|
22
|
+
dropdownAlign = 'left'
|
|
21
23
|
}: SelectProperties = $props();
|
|
22
24
|
|
|
23
25
|
function normalizeItems(source: SelectItem[] | string[]): SelectItem[] {
|
|
@@ -255,30 +257,48 @@
|
|
|
255
257
|
tabindex={disabled ? -1 : searchable ? -1 : 0}
|
|
256
258
|
>
|
|
257
259
|
{#if multiple}
|
|
258
|
-
{#
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
260
|
+
{#if typeof triggerSummary === 'function'}
|
|
261
|
+
{@render triggerSummary({ value, items })}
|
|
262
|
+
{#if searchable}
|
|
263
|
+
<input
|
|
264
|
+
class="select-search"
|
|
265
|
+
type="text"
|
|
266
|
+
value={query}
|
|
267
|
+
oninput={handleSearchInput}
|
|
268
|
+
onfocus={handleSearchFocus}
|
|
269
|
+
bind:this={searchInputEl}
|
|
270
|
+
placeholder={value.length === 0 ? placeholder : ''}
|
|
271
|
+
{disabled}
|
|
272
|
+
autocomplete="off"
|
|
273
|
+
tabindex={disabled ? -1 : 0}
|
|
274
|
+
/>
|
|
275
|
+
{/if}
|
|
276
|
+
{:else}
|
|
277
|
+
{#each value as id (id)}
|
|
278
|
+
<Pill
|
|
279
|
+
text={getLabel(id)}
|
|
280
|
+
dismissible
|
|
281
|
+
{disabled}
|
|
282
|
+
ondismiss={() => removeItem(id)}
|
|
283
|
+
{...typeof testId === 'string' ? { testId: `${testId}-pill-${id}` } : {}}
|
|
284
|
+
/>
|
|
285
|
+
{/each}
|
|
286
|
+
{#if searchable}
|
|
287
|
+
<input
|
|
288
|
+
class="select-search"
|
|
289
|
+
type="text"
|
|
290
|
+
value={query}
|
|
291
|
+
oninput={handleSearchInput}
|
|
292
|
+
onfocus={handleSearchFocus}
|
|
293
|
+
bind:this={searchInputEl}
|
|
294
|
+
placeholder={value.length === 0 ? placeholder : ''}
|
|
295
|
+
{disabled}
|
|
296
|
+
autocomplete="off"
|
|
297
|
+
tabindex={disabled ? -1 : 0}
|
|
298
|
+
/>
|
|
299
|
+
{:else if value.length === 0}
|
|
300
|
+
<span class="select-placeholder">{placeholder}</span>
|
|
301
|
+
{/if}
|
|
282
302
|
{/if}
|
|
283
303
|
{:else if searchable}
|
|
284
304
|
<input
|
|
@@ -303,7 +323,13 @@
|
|
|
303
323
|
</div>
|
|
304
324
|
|
|
305
325
|
{#if open && !disabled}
|
|
306
|
-
<div
|
|
326
|
+
<div
|
|
327
|
+
class="select-dropdown"
|
|
328
|
+
class:select-dropdown-right={dropdownAlign === 'right'}
|
|
329
|
+
role="listbox"
|
|
330
|
+
id={listboxId}
|
|
331
|
+
aria-multiselectable={multiple}
|
|
332
|
+
>
|
|
307
333
|
{#if filteredItems.length === 0}
|
|
308
334
|
<div class="select-empty">No results</div>
|
|
309
335
|
{:else}
|
|
@@ -457,6 +483,13 @@
|
|
|
457
483
|
z-index: var(--select-dropdown-z-index, 10);
|
|
458
484
|
}
|
|
459
485
|
|
|
486
|
+
.select-dropdown-right {
|
|
487
|
+
left: auto;
|
|
488
|
+
right: 0;
|
|
489
|
+
min-width: 100%;
|
|
490
|
+
width: max-content;
|
|
491
|
+
}
|
|
492
|
+
|
|
460
493
|
.select-option {
|
|
461
494
|
padding: var(--select-option-padding, 8px 12px);
|
|
462
495
|
color: var(--select-option-color, #333333);
|
|
@@ -25,6 +25,13 @@ export type OptionalSelectProperties = {
|
|
|
25
25
|
classes?: string;
|
|
26
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. */
|
|
27
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';
|
|
30
|
+
/** Snippet for rendering a compact trigger summary in multiple mode instead of one Pill per selected value. Receives `{ value, items }` so the consumer can compute e.g. "All" / "3 selected". When not provided the default Pill-per-value behaviour is used. */
|
|
31
|
+
triggerSummary?: import('svelte').Snippet<[{
|
|
32
|
+
value: string[];
|
|
33
|
+
items: SelectItem[];
|
|
34
|
+
}]>;
|
|
28
35
|
};
|
|
29
36
|
export type SelectEventProperties = {
|
|
30
37
|
onchange?: (value: string[]) => void;
|