@juspay/svelte-ui-components 2.111.0 → 2.111.2

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.
Files changed (83) hide show
  1. package/dist/Accordion/Accordion.svelte +1 -0
  2. package/dist/Animations/ModalAnimation.svelte +4 -0
  3. package/dist/Animations/OverlayAnimation.svelte +5 -1
  4. package/dist/AreaChart/AreaChart.svelte +1 -0
  5. package/dist/Avatar/Avatar.svelte +8 -1
  6. package/dist/Badge/Badge.svelte +6 -1
  7. package/dist/Banner/Banner.svelte +1 -0
  8. package/dist/BarChart/BarChart.svelte +4 -0
  9. package/dist/Book/Book.svelte +1 -0
  10. package/dist/BrandLoader/BrandLoader.svelte +5 -1
  11. package/dist/Breadcrumb/Breadcrumb.svelte +1 -0
  12. package/dist/Browser/Browser.svelte +7 -1
  13. package/dist/Button/Button.svelte +1 -0
  14. package/dist/Calendar/Calendar.svelte +7 -1
  15. package/dist/Card/Card.svelte +1 -0
  16. package/dist/Carousel/Carousel.svelte +3 -0
  17. package/dist/CheckListItem/CheckListItem.svelte +1 -1
  18. package/dist/Checkbox/Checkbox.svelte +3 -0
  19. package/dist/Choicebox/Choicebox.svelte +1 -0
  20. package/dist/ColorPicker/ColorPicker.svelte +1 -0
  21. package/dist/Combobox/Combobox.svelte +10 -1
  22. package/dist/CommandMenu/CommandMenu.svelte +3 -0
  23. package/dist/ContextMenu/ContextMenu.svelte +2 -0
  24. package/dist/DateRangePicker/DateRangePicker.svelte +299 -17
  25. package/dist/DateRangePicker/properties.d.ts +7 -2
  26. package/dist/DateRangePicker/timeUtils.d.ts +6 -0
  27. package/dist/DateRangePicker/timeUtils.js +71 -0
  28. package/dist/DeltaIndicator/DeltaIndicator.svelte +1 -0
  29. package/dist/DualAxisBarChart/DualAxisBarChart.svelte +2 -0
  30. package/dist/EmptyState/EmptyState.svelte +5 -1
  31. package/dist/FileInput/FileInput.svelte +2 -0
  32. package/dist/FunnelChart/FunnelChart.svelte +2 -0
  33. package/dist/Gauge/Gauge.svelte +1 -0
  34. package/dist/GridItem/GridItem.svelte +1 -0
  35. package/dist/Icon/Icon.svelte +1 -0
  36. package/dist/IconStack/IconStack.svelte +5 -1
  37. package/dist/IframeViewer/IframeViewer.svelte +1 -1
  38. package/dist/Img/Img.svelte +9 -1
  39. package/dist/Input/Input.svelte +3 -0
  40. package/dist/InputButton/InputButton.svelte +5 -1
  41. package/dist/KeyValue/KeyValue.svelte +2 -1
  42. package/dist/KeyboardInput/KeyboardInput.svelte +1 -0
  43. package/dist/LineChart/LineChart.svelte +3 -0
  44. package/dist/ListItem/ListItem.svelte +5 -0
  45. package/dist/Loader/Loader.svelte +5 -1
  46. package/dist/LoadingDots/LoadingDots.svelte +2 -0
  47. package/dist/LottiePlayer/LottiePlayer.svelte +1 -0
  48. package/dist/Menu/Menu.svelte +2 -0
  49. package/dist/Modal/Modal.svelte +4 -1
  50. package/dist/Pagination/Pagination.svelte +4 -0
  51. package/dist/Phone/Phone.svelte +5 -1
  52. package/dist/PieChart/PieChart.svelte +1 -0
  53. package/dist/Pill/Pill.svelte +1 -0
  54. package/dist/Progress/Progress.svelte +5 -1
  55. package/dist/ProportionBar/ProportionBar.svelte +4 -0
  56. package/dist/Radio/Radio.svelte +1 -0
  57. package/dist/RelativeTime/RelativeTime.svelte +2 -0
  58. package/dist/SankeyChart/SankeyChart.svelte +1 -0
  59. package/dist/Scroller/Scroller.svelte +1 -0
  60. package/dist/Select/Select.svelte +18 -5
  61. package/dist/Sheet/Sheet.svelte +2 -0
  62. package/dist/Shimmer/Shimmer.svelte +5 -1
  63. package/dist/Slider/Slider.svelte +1 -0
  64. package/dist/Snippet/Snippet.svelte +5 -1
  65. package/dist/SplitButton/SplitButton.svelte +1 -0
  66. package/dist/SplitInput/SplitInput.svelte +1 -1
  67. package/dist/StatCard/StatCard.svelte +31 -4
  68. package/dist/Status/Status.svelte +5 -1
  69. package/dist/Stepper/Stepper.svelte +6 -1
  70. package/dist/Table/BuiltinCell.svelte +13 -2
  71. package/dist/Table/Table.svelte +24 -1
  72. package/dist/Tabs/Tabs.svelte +2 -1
  73. package/dist/ThemeSwitcher/ThemeSwitcher.svelte +6 -1
  74. package/dist/Toast/Toast.svelte +4 -2
  75. package/dist/Toggle/Toggle.svelte +7 -1
  76. package/dist/Toolbar/Toolbar.svelte +7 -3
  77. package/dist/Tooltip/Tooltip.svelte +5 -4
  78. package/dist/_chart/Axis.svelte +8 -1
  79. package/dist/_chart/ChartContainer.svelte +1 -0
  80. package/dist/_chart/ChartTooltip.svelte +3 -1
  81. package/dist/_chart/Legend.svelte +1 -0
  82. package/dist/testing-attributes.d.ts +36 -0
  83. package/package.json +1 -1
@@ -1,5 +1,9 @@
1
1
  <script lang="ts">
2
- import type { DateRangePickerProperties, DateRangePreset } from './properties';
2
+ import type {
3
+ DateRangePickerProperties,
4
+ DateRangePreset,
5
+ TimeDisplayBoundary
6
+ } from './properties';
3
7
  import { tick, untrack } from 'svelte';
4
8
  import { SvelteDate } from 'svelte/reactivity';
5
9
  import Calendar from '../Calendar/Calendar.svelte';
@@ -11,6 +15,7 @@
11
15
  TIME_DISPLAY_PATTERN,
12
16
  applyTimeDisplay,
13
17
  formatTimeDisplay,
18
+ parseDateDisplay,
14
19
  toMinutesOfDay
15
20
  } from './timeUtils';
16
21
 
@@ -80,6 +85,23 @@
80
85
  let endTimeDisplay: string = $state('11:59 PM');
81
86
  let showTimeRow: boolean = $state(false);
82
87
 
88
+ // Typed text for the built-in date inputs (showDateInputs, range mode). The displayed
89
+ // text is *derived* from draftStart/draftEnd — the single source of truth — so it can
90
+ // never go stale relative to a draft change made elsewhere (preset pick, calendar
91
+ // click, the initialPresetLabel seed effect, ...). While the user is actively editing
92
+ // a field, its edit buffer holds the in-progress text instead (a half-typed string
93
+ // must never reach the calendar grid or Apply's canApply check); the buffer resets to
94
+ // null — falling back to the canonical formatted date — on every successful or
95
+ // rejected commit, see commitTypedDate.
96
+ let startDateEditBuffer: string | null = $state(null);
97
+ let endDateEditBuffer: string | null = $state(null);
98
+ const startDateInputText: string = $derived(
99
+ startDateEditBuffer ?? (draftStart !== null ? formatDate(draftStart) : '')
100
+ );
101
+ const endDateInputText: string = $derived(
102
+ endDateEditBuffer ?? (draftEnd !== null ? formatDate(draftEnd) : '')
103
+ );
104
+
83
105
  // Inline time layout (timeSelectionLayout="inline"): the start/end time inputs
84
106
  // render beside their date inputs on the same row, always visible — no clock
85
107
  // toggle and no collapsible row. All seeding/validation/fold logic is shared
@@ -99,11 +121,14 @@
99
121
 
100
122
  // When maxRangeDays is set, disable any date whose span from the in-progress
101
123
  // start would exceed the limit (inclusive of both ends), until the range is
102
- // completed. Falls back to the raw disabledDates otherwise.
124
+ // completed. Falls back to the raw disabledDates otherwise. Always returns a
125
+ // callable predicate (rather than sometimes the raw Date[]/predicate union) so
126
+ // it can be called directly — by both the Calendar grid and isDateSelectable's
127
+ // typed-date validation — without a union-type call-signature error.
103
128
  const rangeConstrainedDisabledDates = $derived.by(() => {
104
129
  const limit = maxRangeDays;
105
130
  if (limit === null) {
106
- return disabledDates;
131
+ return (date: Date): boolean => isBaseDisabledDate(date);
107
132
  }
108
133
  const anchor = draftStart;
109
134
  const selecting = anchor !== null && draftEnd === null;
@@ -200,10 +225,6 @@
200
225
  return d.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' });
201
226
  }
202
227
 
203
- // Read-only date-input box contents (opt-in via showDateInputs), reflecting the draft.
204
- const draftStartDateLabel: string = $derived(draftStart !== null ? formatDate(draftStart) : '');
205
- const draftEndDateLabel: string = $derived(draftEnd !== null ? formatDate(draftEnd) : '');
206
-
207
228
  const isStartTimeValid: boolean = $derived(TIME_DISPLAY_PATTERN.test(startTimeDisplay.trim()));
208
229
  const isEndTimeValid: boolean = $derived(TIME_DISPLAY_PATTERN.test(endTimeDisplay.trim()));
209
230
  // Apply is blocked while a time is malformed, or while both ends fall on the same
@@ -259,6 +280,13 @@
259
280
  // by label. A direct calendar click later clears this, reverting to date matching.
260
281
  selectedPresetLabel = committedPresetLabel;
261
282
 
283
+ // Discard any in-progress typed text from a previous session — the date-input
284
+ // display text is derived from draftStart/draftEnd above, so it already reflects
285
+ // whatever draft this open seeds (including a later re-seed from initialPresetLabel,
286
+ // which runs in an $effect.pre after this synchronous assignment).
287
+ startDateEditBuffer = null;
288
+ endDateEditBuffer = null;
289
+
262
290
  // Seed the time inputs from the committed range's time-of-day, and collapse the
263
291
  // time row so each open starts from the date view.
264
292
  if (showTimeSelection) {
@@ -407,6 +435,8 @@
407
435
  draftEnd = rangeEnd;
408
436
  draftValue = value;
409
437
  selectedPresetLabel = committedPresetLabel;
438
+ startDateEditBuffer = null;
439
+ endDateEditBuffer = null;
410
440
  const anchor = rangeStart !== null ? rangeStart : value !== null ? value : now;
411
441
  leftYear = anchor.getFullYear();
412
442
  leftMonth = anchor.getMonth();
@@ -420,6 +450,10 @@
420
450
  if (mode === 'range') {
421
451
  draftStart = start;
422
452
  draftEnd = end;
453
+ // Discard any in-progress typed text — the date-input display text is derived
454
+ // from draftStart/draftEnd, so it already reflects the preset's own start/end.
455
+ startDateEditBuffer = null;
456
+ endDateEditBuffer = null;
423
457
  // Reseed the time-of-day inputs from the preset's own start/end so a
424
458
  // time-bearing preset (e.g. "Last 30 minutes" / "Last 12 Hours") is not
425
459
  // silently overwritten by a stale display string in handleApply's
@@ -443,6 +477,9 @@
443
477
  draftEnd = event.rangeEnd;
444
478
  // A direct calendar click is not a preset selection.
445
479
  selectedPresetLabel = null;
480
+ // Discard any in-progress typed text in favor of the calendar's own selection.
481
+ startDateEditBuffer = null;
482
+ endDateEditBuffer = null;
446
483
  if (showTimeSelection) {
447
484
  startTimeDisplay = '12:00 AM';
448
485
  endTimeDisplay = isSameDay(event.rangeEnd, now) ? formatTimeDisplay(now) : '11:59 PM';
@@ -455,6 +492,177 @@
455
492
  selectedPresetLabel = null;
456
493
  }
457
494
 
495
+ // Whether a typed date passes the same minDate/maxDate/disabledDates/maxRangeDays
496
+ // constraints the calendar grid itself enforces — a typed date must never commit
497
+ // somewhere the grid would have refused to let the user click. Checks
498
+ // rangeConstrainedDisabledDates (not just the raw disabledDates) so a typed end date
499
+ // that would blow past maxRangeDays mid-selection is rejected exactly like a click on
500
+ // the same grid cell would be.
501
+ function isDateSelectable(date: Date): boolean {
502
+ if (minDate !== null) {
503
+ const normalizedMinDate = new SvelteDate(
504
+ minDate.getFullYear(),
505
+ minDate.getMonth(),
506
+ minDate.getDate()
507
+ );
508
+ if (date.getTime() < normalizedMinDate.getTime()) {
509
+ return false;
510
+ }
511
+ }
512
+ if (maxDate !== null) {
513
+ const normalizedMaxDate = new SvelteDate(
514
+ maxDate.getFullYear(),
515
+ maxDate.getMonth(),
516
+ maxDate.getDate()
517
+ );
518
+ if (date.getTime() > normalizedMaxDate.getTime()) {
519
+ return false;
520
+ }
521
+ }
522
+ return !rangeConstrainedDisabledDates(date);
523
+ }
524
+
525
+ // The single acceptance rule for a typed date, shared by the live invalid-border
526
+ // feedback and by commitTypedDate. Keeping one predicate is the point: while these
527
+ // were two separate expressions, the border could stay clean for a date that commit
528
+ // then silently rejected, leaving no cue as to why the field reverted.
529
+ //
530
+ // Three ways a parsed date is refused:
531
+ // 1. it isn't selectable at all (minDate/maxDate/disabled, and the maxRangeDays
532
+ // span while a range is mid-selection),
533
+ // 2. it crosses the opposite boundary (a start after the end, or vice versa),
534
+ // 3. it would stretch an *already-complete* range past maxRangeDays.
535
+ // Case 3 is not covered by isDateSelectable: rangeConstrainedDisabledDates only
536
+ // applies its span check while `selecting` is true (draftStart set, draftEnd still
537
+ // null). Once both boundaries exist that guard goes quiet, so retyping either one
538
+ // could otherwise commit a range longer than the calendar grid would ever allow.
539
+ function isTypedDateAcceptable(boundary: TimeDisplayBoundary, candidate: Date): boolean {
540
+ if (!isDateSelectable(candidate)) {
541
+ return false;
542
+ }
543
+ const otherBoundaryDate = boundary === 'start' ? draftEnd : draftStart;
544
+ if (otherBoundaryDate === null) {
545
+ return true;
546
+ }
547
+ const crossesOtherBoundary =
548
+ boundary === 'start'
549
+ ? candidate.getTime() > otherBoundaryDate.getTime()
550
+ : candidate.getTime() < otherBoundaryDate.getTime();
551
+ if (crossesOtherBoundary) {
552
+ return false;
553
+ }
554
+ if (maxRangeDays !== null) {
555
+ const spanDays =
556
+ Math.abs(Math.round((candidate.getTime() - otherBoundaryDate.getTime()) / MS_PER_DAY)) + 1;
557
+ if (spanDays > maxRangeDays) {
558
+ return false;
559
+ }
560
+ }
561
+ return true;
562
+ }
563
+
564
+ // Live validity feedback for the typed date-input text, mirroring isStartTimeValid /
565
+ // isEndTimeValid — the field is flagged invalid as soon as the typed text can't parse
566
+ // into a date this picker would accept, matching the time input's parse-as-you-type
567
+ // styling. Empty text is not flagged invalid; nothing has been typed yet.
568
+ const isStartDateValid: boolean = $derived.by(() => {
569
+ const trimmedStartDateText = startDateInputText.trim();
570
+ if (trimmedStartDateText === '') {
571
+ return true;
572
+ }
573
+ const parsedStartDate = parseDateDisplay(startDateInputText);
574
+ return parsedStartDate !== null && isTypedDateAcceptable('start', parsedStartDate);
575
+ });
576
+ const isEndDateValid: boolean = $derived.by(() => {
577
+ const trimmedEndDateText = endDateInputText.trim();
578
+ if (trimmedEndDateText === '') {
579
+ return true;
580
+ }
581
+ const parsedEndDate = parseDateDisplay(endDateInputText);
582
+ return parsedEndDate !== null && isTypedDateAcceptable('end', parsedEndDate);
583
+ });
584
+
585
+ // True when date's month is already one of the two visible calendar months (or the
586
+ // single visible month outside dual-month mode) — used to avoid an unnecessary
587
+ // re-navigation/remount when a typed date is already on screen.
588
+ function isMonthVisible(date: Date): boolean {
589
+ const isLeftMonthVisible = date.getFullYear() === leftYear && date.getMonth() === leftMonth;
590
+ if (!isDualMonth) {
591
+ return isLeftMonthVisible;
592
+ }
593
+ const rightMonth = new SvelteDate(leftYear, leftMonth + 1, 1);
594
+ const isRightMonthVisible =
595
+ date.getFullYear() === rightMonth.getFullYear() && date.getMonth() === rightMonth.getMonth();
596
+ return isLeftMonthVisible || isRightMonthVisible;
597
+ }
598
+
599
+ function navigateCalendarTo(date: Date): void {
600
+ if (isMonthVisible(date)) {
601
+ return;
602
+ }
603
+ leftYear = date.getFullYear();
604
+ leftMonth = date.getMonth();
605
+ calendarKey++;
606
+ }
607
+
608
+ // Discard one boundary's in-progress edit buffer, falling back to the derived display
609
+ // of its current draft value. Used both to normalize a successful commit and to revert
610
+ // a rejected/unparseable one.
611
+ function clearDateEditBuffer(boundary: TimeDisplayBoundary): void {
612
+ if (boundary === 'start') {
613
+ startDateEditBuffer = null;
614
+ } else {
615
+ endDateEditBuffer = null;
616
+ }
617
+ }
618
+
619
+ // Record the in-progress typed text for one date-input boundary as the user types.
620
+ function handleDateInputChange(event: Event, boundary: TimeDisplayBoundary): void {
621
+ if (!(event.currentTarget instanceof HTMLInputElement)) {
622
+ return;
623
+ }
624
+ if (boundary === 'start') {
625
+ startDateEditBuffer = event.currentTarget.value;
626
+ } else {
627
+ endDateEditBuffer = event.currentTarget.value;
628
+ }
629
+ }
630
+
631
+ // Parse and commit the typed text for one date-input boundary (called on blur and on
632
+ // Enter). Invalid text (unparseable, outside minDate/maxDate, disabled, exceeding
633
+ // maxRangeDays, or crossing the other boundary) is rejected — the field reverts to the
634
+ // last valid value instead of committing garbage into draftStart/draftEnd. A no-op
635
+ // when the field was never edited (buffer already null — nothing to commit).
636
+ function commitTypedDate(boundary: TimeDisplayBoundary): void {
637
+ const editBuffer = boundary === 'start' ? startDateEditBuffer : endDateEditBuffer;
638
+ if (editBuffer === null) {
639
+ return;
640
+ }
641
+ const parsedDate = parseDateDisplay(editBuffer);
642
+
643
+ if (parsedDate === null || !isTypedDateAcceptable(boundary, parsedDate)) {
644
+ clearDateEditBuffer(boundary);
645
+ return;
646
+ }
647
+
648
+ if (boundary === 'start') {
649
+ draftStart = parsedDate;
650
+ } else {
651
+ draftEnd = parsedDate;
652
+ }
653
+ // A typed date is a custom selection, same as a direct calendar click.
654
+ selectedPresetLabel = null;
655
+ navigateCalendarTo(parsedDate);
656
+ clearDateEditBuffer(boundary);
657
+ }
658
+
659
+ function handleDateInputKeyDown(event: KeyboardEvent, boundary: TimeDisplayBoundary): void {
660
+ if (event.key === 'Enter') {
661
+ event.preventDefault();
662
+ commitTypedDate(boundary);
663
+ }
664
+ }
665
+
458
666
  // Close on outside-click
459
667
  function handleDocumentClick(event: MouseEvent): void {
460
668
  if (!isOpen && !openCompare) {
@@ -549,7 +757,7 @@
549
757
 
550
758
  <svelte:document onclick={handleDocumentClick} onkeydown={handleDocumentKeyDown} />
551
759
 
552
- <div class="drp-root {classes ?? ''}" data-pw={testId}>
760
+ <div class="drp-root {classes ?? ''}" data-pw={testId} testID={testId}>
553
761
  <!-- Trigger wrapper — bind:this here so outside-click detection works -->
554
762
  <div bind:this={triggerRef} class="drp-trigger-wrapper">
555
763
  <Button
@@ -654,6 +862,7 @@
654
862
  aria-label="Date range picker"
655
863
  aria-modal="true"
656
864
  data-pw={typeof testId === 'string' ? `${testId}-panel` : null}
865
+ testID={typeof testId === 'string' ? `${testId}-panel` : null}
657
866
  >
658
867
  <div class="drp-panel-inner">
659
868
  <!-- Preset sidebar -->
@@ -679,6 +888,7 @@
679
888
  aria-selected={isPresetActive(preset)}
680
889
  onclick={() => handlePreset(preset)}
681
890
  data-pw={typeof testId === 'string' ? `${testId}-preset-${preset.label}` : null}
891
+ testID={typeof testId === 'string' ? `${testId}-preset-${preset.label}` : null}
682
892
  >
683
893
  {preset.label}
684
894
  {#if presetCheckmark && isPresetActive(preset)}
@@ -687,6 +897,7 @@
687
897
  class="drp-preset-check"
688
898
  aria-hidden="true"
689
899
  data-pw={typeof testId === 'string' ? `${testId}-preset-check` : null}
900
+ testID={typeof testId === 'string' ? `${testId}-preset-check` : null}
690
901
  >{@html checkmarkSvg}</span
691
902
  >
692
903
  {/if}
@@ -702,9 +913,21 @@
702
913
  <div class="drp-date-input-row">
703
914
  {#if isInlineTime}
704
915
  <div class="drp-date-time-group">
705
- <div class="drp-date-input" data-pw={testId ? `${testId}-start-date` : null}>
706
- <span class="drp-date-input-value">{draftStartDateLabel || 'Start date'}</span
707
- >
916
+ <div class="drp-date-input" class:drp-date-input-invalid={!isStartDateValid}>
917
+ <input
918
+ type="text"
919
+ class="drp-date-input-value"
920
+ value={startDateInputText}
921
+ oninput={(event) => handleDateInputChange(event, 'start')}
922
+ maxlength="32"
923
+ placeholder="Start date"
924
+ aria-label="Start date"
925
+ aria-invalid={!isStartDateValid}
926
+ onblur={() => commitTypedDate('start')}
927
+ onkeydown={(event) => handleDateInputKeyDown(event, 'start')}
928
+ data-pw={testId ? `${testId}-start-date` : null}
929
+ testID={testId ? `${testId}-start-date` : null}
930
+ />
708
931
  </div>
709
932
  <div
710
933
  class="drp-time-input drp-time-input-inline"
@@ -721,14 +944,28 @@
721
944
  aria-label="Start time"
722
945
  aria-invalid={!isStartTimeValid}
723
946
  data-pw={testId ? `${testId}-start-time` : null}
947
+ testID={testId ? `${testId}-start-time` : null}
724
948
  />
725
949
  </div>
726
950
  </div>
727
951
  <!-- eslint-disable-next-line svelte/no-at-html-tags -->
728
952
  <span class="drp-datetime-arrow" aria-hidden="true">{@html chevronRightSvg}</span>
729
953
  <div class="drp-date-time-group">
730
- <div class="drp-date-input" data-pw={testId ? `${testId}-end-date` : null}>
731
- <span class="drp-date-input-value">{draftEndDateLabel || 'End date'}</span>
954
+ <div class="drp-date-input" class:drp-date-input-invalid={!isEndDateValid}>
955
+ <input
956
+ type="text"
957
+ class="drp-date-input-value"
958
+ value={endDateInputText}
959
+ oninput={(event) => handleDateInputChange(event, 'end')}
960
+ maxlength="32"
961
+ placeholder="End date"
962
+ aria-label="End date"
963
+ aria-invalid={!isEndDateValid}
964
+ onblur={() => commitTypedDate('end')}
965
+ onkeydown={(event) => handleDateInputKeyDown(event, 'end')}
966
+ data-pw={testId ? `${testId}-end-date` : null}
967
+ testID={testId ? `${testId}-end-date` : null}
968
+ />
732
969
  </div>
733
970
  <div
734
971
  class="drp-time-input drp-time-input-inline"
@@ -745,17 +982,44 @@
745
982
  aria-label="End time"
746
983
  aria-invalid={!isEndTimeValid}
747
984
  data-pw={testId ? `${testId}-end-time` : null}
985
+ testID={testId ? `${testId}-end-time` : null}
748
986
  />
749
987
  </div>
750
988
  </div>
751
989
  {:else}
752
- <div class="drp-date-input" data-pw={testId ? `${testId}-start-date` : null}>
753
- <span class="drp-date-input-value">{draftStartDateLabel || 'Start date'}</span>
990
+ <div class="drp-date-input" class:drp-date-input-invalid={!isStartDateValid}>
991
+ <input
992
+ type="text"
993
+ class="drp-date-input-value"
994
+ value={startDateInputText}
995
+ oninput={(event) => handleDateInputChange(event, 'start')}
996
+ maxlength="32"
997
+ placeholder="Start date"
998
+ aria-label="Start date"
999
+ aria-invalid={!isStartDateValid}
1000
+ onblur={() => commitTypedDate('start')}
1001
+ onkeydown={(event) => handleDateInputKeyDown(event, 'start')}
1002
+ data-pw={testId ? `${testId}-start-date` : null}
1003
+ testID={testId ? `${testId}-start-date` : null}
1004
+ />
754
1005
  </div>
755
1006
  <!-- eslint-disable-next-line svelte/no-at-html-tags -->
756
1007
  <span class="drp-datetime-arrow" aria-hidden="true">{@html chevronRightSvg}</span>
757
- <div class="drp-date-input" data-pw={testId ? `${testId}-end-date` : null}>
758
- <span class="drp-date-input-value">{draftEndDateLabel || 'End date'}</span>
1008
+ <div class="drp-date-input" class:drp-date-input-invalid={!isEndDateValid}>
1009
+ <input
1010
+ type="text"
1011
+ class="drp-date-input-value"
1012
+ value={endDateInputText}
1013
+ oninput={(event) => handleDateInputChange(event, 'end')}
1014
+ maxlength="32"
1015
+ placeholder="End date"
1016
+ aria-label="End date"
1017
+ aria-invalid={!isEndDateValid}
1018
+ onblur={() => commitTypedDate('end')}
1019
+ onkeydown={(event) => handleDateInputKeyDown(event, 'end')}
1020
+ data-pw={testId ? `${testId}-end-date` : null}
1021
+ testID={testId ? `${testId}-end-date` : null}
1022
+ />
759
1023
  </div>
760
1024
  {#if showTimeSelection}
761
1025
  <button
@@ -765,6 +1029,7 @@
765
1029
  aria-label="Toggle time selection"
766
1030
  aria-pressed={showTimeRow}
767
1031
  data-pw={testId ? `${testId}-time-toggle` : null}
1032
+ testID={testId ? `${testId}-time-toggle` : null}
768
1033
  onclick={() => (showTimeRow = !showTimeRow)}
769
1034
  >
770
1035
  <!-- eslint-disable-next-line svelte/no-at-html-tags -->
@@ -787,6 +1052,7 @@
787
1052
  aria-label="Start time"
788
1053
  aria-invalid={!isStartTimeValid}
789
1054
  data-pw={testId ? `${testId}-start-time` : null}
1055
+ testID={testId ? `${testId}-start-time` : null}
790
1056
  />
791
1057
  </div>
792
1058
  <!-- eslint-disable-next-line svelte/no-at-html-tags -->
@@ -803,6 +1069,7 @@
803
1069
  aria-label="End time"
804
1070
  aria-invalid={!isEndTimeValid}
805
1071
  data-pw={testId ? `${testId}-end-time` : null}
1072
+ testID={testId ? `${testId}-end-time` : null}
806
1073
  />
807
1074
  </div>
808
1075
  </div>
@@ -1194,15 +1461,30 @@
1194
1461
  background: var(--drp-date-input-background, #ffffff);
1195
1462
  }
1196
1463
 
1464
+ .drp-date-input-invalid {
1465
+ border-color: var(--drp-date-input-invalid-border, #e5484d);
1466
+ }
1467
+
1197
1468
  .drp-date-input-value {
1198
1469
  display: block;
1470
+ width: 100%;
1471
+ min-width: 0;
1472
+ padding: 0;
1473
+ border: none;
1474
+ outline: none;
1475
+ background: transparent;
1199
1476
  color: var(--drp-date-input-color, #333333);
1200
1477
  font-size: var(--drp-date-input-font-size, 13px);
1478
+ font-family: inherit;
1201
1479
  white-space: nowrap;
1202
1480
  overflow: hidden;
1203
1481
  text-overflow: ellipsis;
1204
1482
  }
1205
1483
 
1484
+ .drp-date-input-value::placeholder {
1485
+ color: var(--drp-date-input-placeholder-color, #aaaaaa);
1486
+ }
1487
+
1206
1488
  .drp-datetime-arrow {
1207
1489
  flex-shrink: 0;
1208
1490
  display: inline-flex;
@@ -43,8 +43,13 @@ export type OptionalDateRangePickerProperties = {
43
43
  */
44
44
  presetCheckmark?: boolean;
45
45
  /**
46
- * Show read-only start/end date boxes at the top of the calendar area (range mode),
47
- * reflecting the current draft selection. Opt-in. Default: false.
46
+ * Show typeable start/end date boxes at the top of the calendar area (range mode).
47
+ * Each box seeds from the current draft selection and can be edited directly by
48
+ * typing a date; the typed text is parsed and committed on blur or Enter. Accepts
49
+ * the component's own display format ("Jul 10, 2026"), numeric "M/D/YYYY", or ISO
50
+ * "YYYY-MM-DD". Unparseable, out-of-range (minDate/maxDate), disabled, or
51
+ * boundary-crossing (start after end, or vice versa) input is rejected — the field
52
+ * reverts to its last valid value instead of committing garbage. Opt-in. Default: false.
48
53
  */
49
54
  showDateInputs?: boolean;
50
55
  /**
@@ -12,3 +12,9 @@ export declare const formatTimeDisplay: (date: Date) => string;
12
12
  export declare const applyTimeDisplay: (date: Date, display: string, boundary: TimeDisplayBoundary) => Date | null;
13
13
  /** Minutes-since-midnight for ordering comparisons, or null when the display is invalid. */
14
14
  export declare const toMinutesOfDay: (display: string) => number | null;
15
+ /**
16
+ * Parse a typed calendar-date string into a local-midnight Date, or null when the text is
17
+ * unparseable or names a calendar date that does not exist (e.g. "Feb 30, 2026"). Accepts the
18
+ * component's own display format ("Jul 10, 2026"), numeric "M/D/YYYY", and ISO "YYYY-MM-DD".
19
+ */
20
+ export declare const parseDateDisplay: (display: string) => Date | null;
@@ -45,3 +45,74 @@ export const toMinutesOfDay = (display) => {
45
45
  const parsed = parseTimeDisplay(display);
46
46
  return parsed === null ? null : parsed.hours * 60 + parsed.minutes;
47
47
  };
48
+ const SHORT_MONTH_NAMES = [
49
+ 'jan',
50
+ 'feb',
51
+ 'mar',
52
+ 'apr',
53
+ 'may',
54
+ 'jun',
55
+ 'jul',
56
+ 'aug',
57
+ 'sep',
58
+ 'oct',
59
+ 'nov',
60
+ 'dec'
61
+ ];
62
+ /** Matches the DateRangePicker's own display format, e.g. "Jul 10, 2026" or "Jul 10 2026". */
63
+ const MONTH_NAME_DATE_PATTERN = /^([A-Za-z]{3,})\.?\s+(\d{1,2}),?\s+(\d{4})$/;
64
+ /** Matches numeric "M/D/YYYY" or "M-D-YYYY" input. */
65
+ const NUMERIC_SLASH_DATE_PATTERN = /^(\d{1,2})[/-](\d{1,2})[/-](\d{4})$/;
66
+ /** Matches ISO "YYYY-MM-DD" input. */
67
+ const ISO_DATE_PATTERN = /^(\d{4})-(\d{1,2})-(\d{1,2})$/;
68
+ /**
69
+ * Build a local-midnight Date for the given calendar parts.
70
+ *
71
+ * `new Date(year, ...)` maps years 0-99 onto 1900-1999, so a literal the parser accepts
72
+ * as four digits — "0050-01-01" — would silently land in 1950. setFullYear has no such
73
+ * remapping, so the date is constructed neutrally and then stamped.
74
+ */
75
+ const localMidnightFromParts = (year, monthIndex, day) => {
76
+ const date = new Date();
77
+ date.setFullYear(year, monthIndex, day);
78
+ date.setHours(0, 0, 0, 0);
79
+ return date;
80
+ };
81
+ const daysInMonth = (year, monthIndex) =>
82
+ // Day 0 of the next month is the last day of this one.
83
+ localMidnightFromParts(year, monthIndex + 1, 0).getDate();
84
+ /** Build a local-midnight Date from year/monthIndex/day, or null when the calendar date does not exist. */
85
+ const buildDateFromParts = (year, monthIndex, day) => {
86
+ if (monthIndex < 0 || monthIndex > 11 || day < 1 || day > daysInMonth(year, monthIndex)) {
87
+ return null;
88
+ }
89
+ return localMidnightFromParts(year, monthIndex, day);
90
+ };
91
+ /**
92
+ * Parse a typed calendar-date string into a local-midnight Date, or null when the text is
93
+ * unparseable or names a calendar date that does not exist (e.g. "Feb 30, 2026"). Accepts the
94
+ * component's own display format ("Jul 10, 2026"), numeric "M/D/YYYY", and ISO "YYYY-MM-DD".
95
+ */
96
+ export const parseDateDisplay = (display) => {
97
+ const trimmedDisplay = display.trim();
98
+ if (trimmedDisplay === '') {
99
+ return null;
100
+ }
101
+ const isoMatch = ISO_DATE_PATTERN.exec(trimmedDisplay);
102
+ if (isoMatch !== null) {
103
+ return buildDateFromParts(Number(isoMatch[1]), Number(isoMatch[2]) - 1, Number(isoMatch[3]));
104
+ }
105
+ const monthNameMatch = MONTH_NAME_DATE_PATTERN.exec(trimmedDisplay);
106
+ if (monthNameMatch !== null) {
107
+ const monthIndex = SHORT_MONTH_NAMES.indexOf(monthNameMatch[1].slice(0, 3).toLowerCase());
108
+ if (monthIndex === -1) {
109
+ return null;
110
+ }
111
+ return buildDateFromParts(Number(monthNameMatch[3]), monthIndex, Number(monthNameMatch[2]));
112
+ }
113
+ const slashMatch = NUMERIC_SLASH_DATE_PATTERN.exec(trimmedDisplay);
114
+ if (slashMatch !== null) {
115
+ return buildDateFromParts(Number(slashMatch[3]), Number(slashMatch[1]) - 1, Number(slashMatch[2]));
116
+ }
117
+ return null;
118
+ };
@@ -37,6 +37,7 @@
37
37
  <span
38
38
  class="delta-indicator delta-{tone} {classes ?? ''}"
39
39
  data-pw={typeof testId === 'string' ? testId : null}
40
+ testID={typeof testId === 'string' ? testId : null}
40
41
  >
41
42
  {#if !hideArrow && direction !== 'neutral'}
42
43
  <span class="delta-indicator-arrow delta-arrow-{direction}" aria-hidden="true">
@@ -441,6 +441,7 @@
441
441
  class="dual-axis-bar-chart {classes ?? ''}"
442
442
  bind:this={containerEl}
443
443
  data-pw={typeof testId === 'string' ? testId : null}
444
+ testID={typeof testId === 'string' ? testId : null}
444
445
  >
445
446
  {#if !isEmpty}
446
447
  {#if showLegend && (chartWidth === 0 || hideLegendBelow === 0 || chartWidth >= hideLegendBelow)}
@@ -568,6 +569,7 @@
568
569
  height={dims.innerHeight}
569
570
  fill="transparent"
570
571
  data-pw={`hover-target-${hr.catIdx}`}
572
+ testID={`hover-target-${hr.catIdx}`}
571
573
  data-category-index={hr.catIdx}
572
574
  tabindex="0"
573
575
  role="button"
@@ -13,7 +13,11 @@
13
13
  }: EmptyStateProperties = $props();
14
14
  </script>
15
15
 
16
- <div class="empty-state {classes ?? ''}" data-pw={typeof testId === 'string' ? testId : null}>
16
+ <div
17
+ class="empty-state {classes ?? ''}"
18
+ data-pw={typeof testId === 'string' ? testId : null}
19
+ testID={typeof testId === 'string' ? testId : null}
20
+ >
17
21
  {#if typeof icon === 'function'}
18
22
  <div class="empty-state-icon">
19
23
  {@render icon()}
@@ -139,6 +139,7 @@
139
139
  class:file-input-dragover={dragOver}
140
140
  class:file-input-disabled={disabled}
141
141
  data-pw={testId}
142
+ testID={testId}
142
143
  role="button"
143
144
  tabindex={disabled ? -1 : 0}
144
145
  aria-disabled={disabled}
@@ -156,6 +157,7 @@
156
157
  {multiple}
157
158
  {disabled}
158
159
  data-pw={typeof testId === 'string' ? `${testId}-input` : null}
160
+ testID={typeof testId === 'string' ? `${testId}-input` : null}
159
161
  onchange={handleInputChange}
160
162
  tabindex="-1"
161
163
  aria-hidden="true"
@@ -294,6 +294,7 @@
294
294
  class="funnel-chart {classes ?? ''}"
295
295
  bind:this={containerEl}
296
296
  data-pw={typeof testId === 'string' ? testId : null}
297
+ testID={typeof testId === 'string' ? testId : null}
297
298
  >
298
299
  {#if isEmpty && typeof empty === 'function'}
299
300
  <div class="chart-empty">{@render empty()}</div>
@@ -333,6 +334,7 @@
333
334
  x={bx}
334
335
  y={by}
335
336
  data-pw={`funnel-bar-${index}`}
337
+ testID={`funnel-bar-${index}`}
336
338
  width={stageColumnWidth}
337
339
  height={bh}
338
340
  fill={color}
@@ -30,6 +30,7 @@
30
30
  <div
31
31
  class="gauge {classes ?? ''}"
32
32
  data-pw={typeof testId === 'string' ? testId : null}
33
+ testID={typeof testId === 'string' ? testId : null}
33
34
  role="progressbar"
34
35
  aria-valuenow={clamped}
35
36
  aria-valuemin={0}
@@ -25,6 +25,7 @@
25
25
  role="button"
26
26
  tabindex="0"
27
27
  data-pw={typeof testId === 'string' ? testId : null}
28
+ testID={typeof testId === 'string' ? testId : null}
28
29
  >
29
30
  <div class="grid-header">
30
31
  <img src={headerIcon} alt="" class="grid-item-header-icon" />