@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?.({ 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;
@@ -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 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
+ >
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.28.3",
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",