@juspay/svelte-ui-components 2.29.0 → 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?.({ compareStart: draftCompareStart, compareEnd: draftCompareEnd });
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
- {disabledDates}
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
- {disabledDates}
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;
@@ -17,7 +17,8 @@
17
17
  itemTestId,
18
18
  onchange,
19
19
  classes,
20
- open = $bindable(false)
20
+ open = $bindable(false),
21
+ dropdownAlign = 'left'
21
22
  }: SelectProperties = $props();
22
23
 
23
24
  function normalizeItems(source: SelectItem[] | string[]): SelectItem[] {
@@ -303,7 +304,13 @@
303
304
  </div>
304
305
 
305
306
  {#if open && !disabled}
306
- <div class="select-dropdown" role="listbox" id={listboxId} aria-multiselectable={multiple}>
307
+ <div
308
+ class="select-dropdown"
309
+ class:select-dropdown-right={dropdownAlign === 'right'}
310
+ role="listbox"
311
+ id={listboxId}
312
+ aria-multiselectable={multiple}
313
+ >
307
314
  {#if filteredItems.length === 0}
308
315
  <div class="select-empty">No results</div>
309
316
  {:else}
@@ -457,6 +464,13 @@
457
464
  z-index: var(--select-dropdown-z-index, 10);
458
465
  }
459
466
 
467
+ .select-dropdown-right {
468
+ left: auto;
469
+ right: 0;
470
+ min-width: 100%;
471
+ width: max-content;
472
+ }
473
+
460
474
  .select-option {
461
475
  padding: var(--select-option-padding, 8px 12px);
462
476
  color: var(--select-option-color, #333333);
@@ -25,6 +25,8 @@ 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';
28
30
  };
29
31
  export type SelectEventProperties = {
30
32
  onchange?: (value: string[]) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.29.0",
3
+ "version": "2.30.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",