design-system-next 2.23.1 → 2.26.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.
Files changed (35) hide show
  1. package/dist/design-system-next.es.d.ts +170 -110
  2. package/dist/design-system-next.es.js +10268 -9387
  3. package/dist/design-system-next.es.js.gz +0 -0
  4. package/dist/design-system-next.umd.js +12 -12
  5. package/dist/design-system-next.umd.js.gz +0 -0
  6. package/dist/main.css +1 -1
  7. package/dist/main.css.gz +0 -0
  8. package/package.json +3 -2
  9. package/src/components/date-picker/date-picker.ts +9 -12
  10. package/src/components/date-picker/date-picker.vue +1 -1
  11. package/src/components/date-picker/date-range-picker/date-range-picker.ts +9 -12
  12. package/src/components/date-picker/date-range-picker/use-date-range-picker.ts +64 -49
  13. package/src/components/date-picker/month-year-picker/month-year-picker.ts +134 -0
  14. package/src/components/date-picker/month-year-picker/month-year-picker.vue +233 -0
  15. package/src/components/date-picker/month-year-picker/use-month-year-picker.ts +603 -0
  16. package/src/components/date-picker/use-date-picker.ts +88 -147
  17. package/src/components/dropdown/dropdown.ts +6 -2
  18. package/src/components/dropdown/dropdown.vue +8 -1
  19. package/src/components/dropdown/use-dropdown.ts +13 -0
  20. package/src/components/list/list.ts +5 -0
  21. package/src/components/list/list.vue +2 -0
  22. package/src/components/list/use-list.ts +36 -3
  23. package/src/components/select/select-multiple/use-select-multiple.ts +7 -3
  24. package/src/components/select/use-select.ts +9 -5
  25. package/src/components/sidepanel/sidepanel.ts +11 -0
  26. package/src/components/sidepanel/sidepanel.vue +26 -4
  27. package/src/components/sidepanel/use-sidepanel.ts +7 -2
  28. package/src/components/table/table-header-dropdown/table-header-dropdown.ts +48 -0
  29. package/src/components/table/table-header-dropdown/table-header-dropdown.vue +88 -0
  30. package/src/components/table/table-pagination/table-pagination.ts +4 -0
  31. package/src/components/table/table-pagination/table-pagination.vue +9 -1
  32. package/src/components/table/table-pagination/use-table-pagination.ts +4 -3
  33. package/src/components/table/table.ts +9 -1
  34. package/src/components/table/table.vue +31 -2
  35. package/src/components/table/use-table.ts +6 -2
@@ -1,5 +1,5 @@
1
1
  import { ref, toRefs, computed, ComputedRef, SetupContext, onMounted, watch, nextTick } from 'vue';
2
- import { useVModel, onClickOutside, useDebounceFn } from '@vueuse/core';
2
+ import { useVModel, onClickOutside } from '@vueuse/core';
3
3
 
4
4
  import dayjs from 'dayjs';
5
5
  import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
@@ -143,8 +143,6 @@ export const useDatePicker = (props: DatePickerPropTypes, emit: SetupContext<Dat
143
143
  const dateInput = ref<string>('');
144
144
  const yearInput = ref<string>('');
145
145
 
146
- const datePickerErrors = ref<{ title: string; message: string }[]>([]);
147
-
148
146
  // #region - Calendar Tab
149
147
  const calendarTabPageData = ref({
150
148
  selectedMonth: dayjs().month(),
@@ -295,12 +293,18 @@ export const useDatePicker = (props: DatePickerPropTypes, emit: SetupContext<Dat
295
293
  }
296
294
 
297
295
  // Check if date and month are selected, but not year
298
- if (dateInput.value && monthInput && !yearInput.value && !calendarTabIsDateIsDisabled(day)) {
296
+ if (dateInput.value && monthInput.value && !yearInput.value && !calendarTabIsDateIsDisabled(day)) {
299
297
  return day.date.getDate() === Number(dateInput.value) && day.date.getMonth() === monthValue && !day.inactive;
300
298
  }
301
299
 
302
300
  // Check if date, month, and year are selected
303
- if (dateInput.value && monthInput.value && yearInput.value && !calendarTabIsDateIsDisabled(day)) {
301
+ if (
302
+ dateInput.value &&
303
+ monthInput.value &&
304
+ yearInput.value &&
305
+ monthValue !== undefined &&
306
+ !calendarTabIsDateIsDisabled(day)
307
+ ) {
304
308
  return (
305
309
  day.date.getDate() === Number(dateInput.value) &&
306
310
  day.date.getMonth() === monthValue &&
@@ -471,10 +475,6 @@ export const useDatePicker = (props: DatePickerPropTypes, emit: SetupContext<Dat
471
475
  emitDateFormats();
472
476
  emitInputValue();
473
477
 
474
- datePickerErrors.value = [];
475
-
476
- emit('getDateErrors', datePickerErrors.value);
477
-
478
478
  setTimeout(() => {
479
479
  datePopperState.value = false;
480
480
  }, 100);
@@ -494,11 +494,7 @@ export const useDatePicker = (props: DatePickerPropTypes, emit: SetupContext<Dat
494
494
  handleConvertMonthIfValid();
495
495
  calendarTabUpdateCalendar();
496
496
  emitDateFormats();
497
- emitPartialInputValue();
498
-
499
- datePickerErrors.value = [];
500
-
501
- emit('getDateErrors', datePickerErrors.value);
497
+ emitInputValue();
502
498
  };
503
499
  // #endregion - Month Tab
504
500
 
@@ -564,11 +560,7 @@ export const useDatePicker = (props: DatePickerPropTypes, emit: SetupContext<Dat
564
560
  handleConvertMonthIfValid();
565
561
  calendarTabUpdateCalendar();
566
562
  emitDateFormats();
567
- emitPartialInputValue();
568
-
569
- datePickerErrors.value = [];
570
-
571
- emit('getDateErrors', datePickerErrors.value);
563
+ emitInputValue();
572
564
  };
573
565
  // #endregion - Year Tab
574
566
 
@@ -640,8 +632,6 @@ export const useDatePicker = (props: DatePickerPropTypes, emit: SetupContext<Dat
640
632
  const date = formattedDate.format('DD');
641
633
  const year = formattedDate.format('YYYY');
642
634
 
643
- handleValidateDate();
644
-
645
635
  monthInput.value = month;
646
636
  dateInput.value = date;
647
637
  yearInput.value = year;
@@ -655,13 +645,7 @@ export const useDatePicker = (props: DatePickerPropTypes, emit: SetupContext<Dat
655
645
  handleConvertMonthIfValid();
656
646
  calendarTabUpdateCalendar();
657
647
  emitDateFormats();
658
-
659
- // Use the specified format for the input value
660
- if (!monthInput.value && !dateInput.value && !yearInput.value) {
661
- emit('getInputValue', null);
662
- } else {
663
- emit('getInputValue', formattedDate.format(format.value));
664
- }
648
+ emitInputValue();
665
649
  } else {
666
650
  console.error(`Error: Could not parse date "${modelValue.value}" with format "${format.value}"`);
667
651
  }
@@ -686,48 +670,71 @@ export const useDatePicker = (props: DatePickerPropTypes, emit: SetupContext<Dat
686
670
  };
687
671
 
688
672
  const handleMonthInput = () => {
689
- datePopperState.value = false;
690
-
691
673
  monthInput.value = monthInput.value.replace(/[^A-Za-z0-9\s]/g, '').toLocaleUpperCase();
692
674
 
693
- datePickerErrors.value = [];
694
-
695
- emit('getDateErrors', datePickerErrors.value);
696
-
697
675
  handleConvertMonthIfValid();
698
676
 
699
- handleValidateDate();
677
+ // Update calendar if month, date, and year are all provided
678
+ if (monthInput.value && dateInput.value && yearInput.value) {
679
+ const monthValue = getMonthObject('text', monthInput.value)?.monthValue;
680
+ const yearNumber = Number(yearInput.value);
681
+
682
+ if (monthValue !== undefined && yearNumber >= minMaxYear.value.min && yearNumber <= minMaxYear.value.max) {
683
+ calendarTabPageData.value.selectedMonth = monthValue;
684
+ calendarTabPageData.value.selectedYear = yearNumber;
685
+ calendarTabUpdateCalendar();
686
+ }
687
+ } else if (!monthInput.value && !dateInput.value && !yearInput.value) {
688
+ // Clear modelValue when all inputs are empty
689
+ modelValue.value = '';
690
+ }
700
691
 
701
692
  // Emit the partial date value as user types
702
- emitPartialInputValue();
693
+ emitInputValue();
703
694
  };
704
695
 
705
696
  const handleDateInput = () => {
706
- datePopperState.value = false;
707
-
708
697
  dateInput.value = dateInput.value.replace(/[^0-9]/g, '');
709
698
 
710
- datePickerErrors.value = [];
711
-
712
- emit('getDateErrors', datePickerErrors.value);
699
+ // Update calendar if month, date, and year are all provided
700
+ if (monthInput.value && dateInput.value && yearInput.value) {
701
+ const monthValue = getMonthObject('text', monthInput.value)?.monthValue;
702
+ const yearNumber = Number(yearInput.value);
713
703
 
714
- handleValidateDate();
704
+ if (monthValue !== undefined && yearNumber >= minMaxYear.value.min && yearNumber <= minMaxYear.value.max) {
705
+ calendarTabPageData.value.selectedMonth = monthValue;
706
+ calendarTabPageData.value.selectedYear = yearNumber;
707
+ calendarTabUpdateCalendar();
708
+ }
709
+ } else if (!monthInput.value && !dateInput.value && !yearInput.value) {
710
+ // Clear modelValue when all inputs are empty
711
+ modelValue.value = '';
712
+ }
715
713
 
716
714
  // Emit the partial date value as user types
717
- emitPartialInputValue();
715
+ emitInputValue();
718
716
  };
719
717
 
720
718
  const handleYearInput = () => {
721
- datePopperState.value = false;
722
-
723
719
  yearInput.value = yearInput.value.replace(/[^0-9]/g, '');
724
720
 
725
- datePickerErrors.value = [];
721
+ // Update calendar if month, date, and year are all provided
722
+ if (monthInput.value && dateInput.value && yearInput.value) {
723
+ const monthValue = getMonthObject('text', monthInput.value)?.monthValue;
724
+ const yearNumber = Number(yearInput.value);
726
725
 
727
- emit('getDateErrors', datePickerErrors.value);
726
+ if (monthValue !== undefined && yearNumber >= minMaxYear.value.min && yearNumber <= minMaxYear.value.max) {
727
+ calendarTabPageData.value.selectedMonth = monthValue;
728
+ calendarTabPageData.value.selectedYear = yearNumber;
729
+ calendarTabUpdateCalendar();
730
+ }
731
+ } else if (!monthInput.value && !dateInput.value && !yearInput.value) {
732
+ // Clear modelValue when all inputs are empty
733
+ modelValue.value = '';
734
+ }
728
735
 
729
736
  // Emit the partial date value as user types
730
- emitPartialInputValue();
737
+ emitInputValue();
731
738
  };
732
739
 
733
740
  const handleConvertMonthIfValid = () => {
@@ -755,51 +762,6 @@ export const useDatePicker = (props: DatePickerPropTypes, emit: SetupContext<Dat
755
762
  }
756
763
  };
757
764
 
758
- const handleValidateDate = useDebounceFn(() => {
759
- if (monthInput.value && dateInput.value && yearInput.value) {
760
- const selectedDate = `${monthInput.value}-${dateInput.value}-${yearInput.value}`;
761
-
762
- const isDateValid = dayjs(selectedDate, 'MM-DD-YYYY').isValid();
763
- const isYearValid =
764
- Number(yearInput.value) >= minMaxYear.value.min && Number(yearInput.value) <= minMaxYear.value.max;
765
-
766
- if (isDateValid && isYearValid) {
767
- datePickerErrors.value = datePickerErrors.value.filter((error) => error.title !== 'Invalid Date');
768
-
769
- const monthValue = getMonthObject('text', monthInput.value)?.monthValue;
770
-
771
- calendarTabPageData.value.selectedMonth = Number(monthValue);
772
- calendarTabPageData.value.selectedYear = Number(yearInput.value);
773
-
774
- calendarTabUpdateCalendar();
775
- emitDateFormats();
776
- } else {
777
- const errorExists = datePickerErrors.value.some((error) => error.title === 'Invalid Date');
778
-
779
- if (!errorExists) {
780
- let errorMessage;
781
-
782
- if (!isYearValid) {
783
- errorMessage = `Year must be between ${minMaxYear.value.min} and ${minMaxYear.value.max}.`;
784
- } else {
785
- errorMessage = `Invalid Date Format. Please use ${format.value}`;
786
- }
787
-
788
- datePickerErrors.value.push({
789
- title: 'Invalid Date',
790
- message: errorMessage,
791
- });
792
-
793
- datePopperState.value = false;
794
-
795
- emit('getDateErrors', datePickerErrors.value);
796
-
797
- console.error(`Invalid Date: "${selectedDate}". ${errorMessage}`);
798
- }
799
- }
800
- }
801
- }, 500);
802
-
803
765
  const handleTabClick = (tab: string) => {
804
766
  if (currentTab.value === tab) {
805
767
  currentTab.value = 'tab-calendar';
@@ -824,34 +786,28 @@ export const useDatePicker = (props: DatePickerPropTypes, emit: SetupContext<Dat
824
786
  }
825
787
  };
826
788
 
827
- const emitPartialInputValue = () => {
828
- // Convert month to numeric format if it's text
829
- let emittedMonth = monthInput.value;
830
-
831
- if (monthInput.value) {
832
- const isNumeric = !isNaN(Number(monthInput.value)) && !isNaN(parseFloat(monthInput.value));
833
-
834
- if (!isNumeric) {
835
- const monthIsValid = monthsList.value.find(
836
- (_month: MonthsList) => _month.text.toLowerCase() === monthInput.value.toLowerCase(),
837
- );
789
+ const emitInputValue = () => {
790
+ if (monthInput.value || dateInput.value || yearInput.value) {
791
+ emit('getInputValue', `${monthInput.value}-${dateInput.value}-${yearInput.value}`);
792
+ } else {
793
+ emit('getInputValue', null);
838
794
 
839
- if (monthIsValid) {
840
- emittedMonth =
841
- monthIsValid.monthValue < 10 ? `0${monthIsValid.monthValue + 1}` : `${monthIsValid.monthValue + 1}`;
842
- }
843
- }
795
+ modelValue.value = '';
844
796
  }
845
797
 
846
- // Build the partial date string with zeros for empty fields
847
- const partialMonth = emittedMonth || '0';
848
- const partialDate = dateInput.value || '0';
849
- const partialYear = yearInput.value || '0';
798
+ if (monthInput.value && dateInput.value && yearInput.value) {
799
+ const monthIsValid = monthsList.value.find(
800
+ (_month: MonthsList) => _month.text.toLowerCase() === monthInput.value.toLowerCase(),
801
+ );
850
802
 
851
- const partialDateString = `${partialMonth}-${partialDate}-${partialYear}`;
803
+ const yearIsValid = yearTabPageData.value.yearsArray.find((_year) => _year === Number(yearInput.value));
852
804
 
853
- // Emit the partial date string
854
- emit('getInputValue', partialDateString);
805
+ if (monthIsValid && yearIsValid) {
806
+ const _date = dayjs(`${monthInput.value}-${dateInput.value}-${yearInput.value}`, 'MM-DD-YYYY');
807
+
808
+ modelValue.value = _date.format(format.value);
809
+ }
810
+ }
855
811
  };
856
812
 
857
813
  const emitDateFormats = () => {
@@ -904,28 +860,6 @@ export const useDatePicker = (props: DatePickerPropTypes, emit: SetupContext<Dat
904
860
  }
905
861
  };
906
862
 
907
- const emitInputValue = () => {
908
- let emittedMonth = monthInput.value;
909
-
910
- const isNumeric = !isNaN(Number(monthInput.value)) && !isNaN(parseFloat(monthInput.value));
911
-
912
- if (!isNumeric) {
913
- const monthIsValid = monthsList.value.find(
914
- (_month: MonthsList) => _month.text.toLowerCase() === monthInput.value.toLowerCase(),
915
- );
916
-
917
- if (monthIsValid) {
918
- emittedMonth =
919
- monthIsValid.monthValue < 10 ? `0${monthIsValid.monthValue + 1}` : `${monthIsValid.monthValue + 1}`;
920
- }
921
- }
922
- // Format the date according to the format prop
923
- const dateObj = dayjs(`${emittedMonth}-${dateInput.value}-${yearInput.value}`, 'MM-DD-YYYY');
924
-
925
- // Use the specified format for the input value
926
- emit('getInputValue', (modelValue.value = dateObj.format(format.value)));
927
- };
928
-
929
863
  const emitMonthList = () => {
930
864
  emit('getMonthList', monthsList.value);
931
865
  };
@@ -939,6 +873,9 @@ export const useDatePicker = (props: DatePickerPropTypes, emit: SetupContext<Dat
939
873
  monthInput.value = '';
940
874
  dateInput.value = '';
941
875
  yearInput.value = '';
876
+ modelValue.value = '';
877
+
878
+ emitInputValue();
942
879
  };
943
880
 
944
881
  const handleSlotClick = () => {
@@ -972,14 +909,19 @@ export const useDatePicker = (props: DatePickerPropTypes, emit: SetupContext<Dat
972
909
  }
973
910
  });
974
911
 
975
- watch(minMaxYear, () => {
976
- yearTabPageData.value.yearsArray = Array.from(
977
- { length: minMaxYear.value.max - minMaxYear.value.min + 1 },
978
- (_, index) => minMaxYear.value.min + index,
979
- ).filter((year) => year <= minMaxYear.value.max && year >= minMaxYear.value.min);
980
-
981
- yearTabPageData.value.currentPage = 0;
982
- });
912
+ watch(
913
+ minMaxYear,
914
+ () => {
915
+ yearTabPageData.value.yearsArray = Array.from(
916
+ { length: minMaxYear.value.max - minMaxYear.value.min + 1 },
917
+ (_, index) => minMaxYear.value.min + index,
918
+ ).filter((year) => year <= minMaxYear.value.max && year >= minMaxYear.value.min);
919
+
920
+ yearTabPageData.value.currentPage = 0;
921
+ emitYearList();
922
+ },
923
+ { deep: true },
924
+ );
983
925
 
984
926
  onClickOutside(datePickerRef, () => {
985
927
  datePopperState.value = false;
@@ -1014,7 +956,6 @@ export const useDatePicker = (props: DatePickerPropTypes, emit: SetupContext<Dat
1014
956
  dateInput,
1015
957
  monthInput,
1016
958
  yearInput,
1017
- datePickerErrors,
1018
959
  calendarTabPageData,
1019
960
  calendarTabIsMinMonth,
1020
961
  calendarTabIsMaxMonth,
@@ -141,13 +141,17 @@ export const dropdownPropTypes = {
141
141
  type: Boolean,
142
142
  default: false,
143
143
  },
144
+ noPadding: {
145
+ type: Boolean,
146
+ default: false,
147
+ }
144
148
  };
145
149
 
146
150
  export const dropdownEmitTypes = {
147
- 'infinite-scroll-trigger': Boolean,
151
+ 'infinite-scroll-trigger': (value: boolean) => typeof value === 'boolean',
148
152
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
149
153
  'update:modelValue': (_value: unknown) => true, // Accept any type of value
150
- 'popper-state': Boolean,
154
+ 'popper-state': (state: boolean) => typeof state === 'boolean',
151
155
  };
152
156
 
153
157
  export type DropdownPropTypes = ExtractPropTypes<typeof dropdownPropTypes>;
@@ -32,7 +32,7 @@
32
32
  <template #popper>
33
33
  <template v-if="$slots.popper">
34
34
  <div
35
- class="spr-overflow-y-auto spr-overflow-x-hidden spr-p-4"
35
+ :class="['spr-overflow-y-auto spr-overflow-x-hidden', !props.noPadding && 'spr-p-4']"
36
36
  :style="{
37
37
  width: props.popperInnerWidth,
38
38
  }"
@@ -111,5 +111,12 @@ const {
111
111
  dropdownValue,
112
112
  removeCurrentLevelInBackLabel,
113
113
  isLadderizedSearch,
114
+ showDropdown,
115
+ hideDropdown,
114
116
  } = useDropdown(props, emit);
117
+
118
+ defineExpose({
119
+ showDropdown,
120
+ hideDropdown,
121
+ });
115
122
  </script>
@@ -82,6 +82,17 @@ export const useDropdown = (props: DropdownPropTypes, emit: SetupContext<Dropdow
82
82
  const dropdownPopperState = ref<boolean>(false);
83
83
  const isDropdownPopperDisabled = computed(() => disabled.value);
84
84
 
85
+ // Exposed methods to show/hide dropdown. This is for custom trigger handling for custom dropdown.
86
+ // To use these methods, set :triggers="[]" on the SprDropdown component to disable default triggers. (reference: https://floating-vue.starpad.dev/api/#shown)
87
+ /* #region - Exposed Methods */
88
+ const showDropdown = () => {
89
+ dropdownPopperState.value = true;
90
+ };
91
+ const hideDropdown = () => {
92
+ dropdownPopperState.value = false;
93
+ };
94
+ /* #endregion - Exposed Methods */
95
+
85
96
  const isLadderizedSearch = computed(
86
97
  () => ladderized.value && searchString.value !== '' && normalizedValue.value.length === 0,
87
98
  );
@@ -503,5 +514,7 @@ export const useDropdown = (props: DropdownPropTypes, emit: SetupContext<Dropdow
503
514
  dropdownValue: compatPreSelectedItems, // Use compatible format for lists
504
515
  removeCurrentLevelInBackLabel,
505
516
  isLadderizedSearch,
517
+ showDropdown,
518
+ hideDropdown,
506
519
  };
507
520
  };
@@ -123,12 +123,17 @@ export const listPropTypes = {
123
123
  type: Boolean,
124
124
  default: false,
125
125
  },
126
+ allowDeselect: {
127
+ type: Boolean,
128
+ default: false
129
+ }
126
130
  };
127
131
 
128
132
  export const listEmitTypes = {
129
133
  'update:modelValue': (value: MenuListType[]) => value,
130
134
  'update:searchValue': (value: string) => typeof value === 'string',
131
135
  'get-single-selected-item': (item: MenuListType) => item,
136
+ 'get-single-deselected-item': (item: MenuListType) => item,
132
137
  };
133
138
 
134
139
  export type ListPropTypes = ExtractPropTypes<typeof listPropTypes>;
@@ -8,6 +8,7 @@
8
8
  v-model="searchText"
9
9
  :placeholder="props.searchableMenuPlaceholder"
10
10
  autocomplete="off"
11
+ @keyup="handleSearchKeyup"
11
12
  />
12
13
  <span
13
14
  v-if="props.supportingDisplayText || props.displayListItemSelected"
@@ -121,5 +122,6 @@ const {
121
122
  isItemSelected,
122
123
  getListItemClasses,
123
124
  handleSelectedItem,
125
+ handleSearchKeyup,
124
126
  } = useList(props, emit);
125
127
  </script>
@@ -22,6 +22,7 @@ export const useList = (props: ListPropTypes, emit: SetupContext<ListEmitTypes>[
22
22
  noCheck,
23
23
  disabledUnselectedItems,
24
24
  stickySearchOffset,
25
+ allowDeselect,
25
26
  } = toRefs(props);
26
27
 
27
28
  const listClasses: ComputedRef<ListClasses> = computed(() => {
@@ -518,14 +519,31 @@ export const useList = (props: ListPropTypes, emit: SetupContext<ListEmitTypes>[
518
519
  // Track the deselection but DON'T add items back to selectedItems when deselecting
519
520
  trackNewlySelectedItems(item, true);
520
521
  }
522
+ emit('get-single-selected-item', item);
521
523
  } else {
522
524
  // For single-select, simply replace the selection
523
- selectedItems.value = [item];
525
+ if (allowDeselect.value) {
526
+ handleDeselect(item);
527
+ } else {
528
+ handleSingleSelect(item);
529
+ }
530
+ }
531
+ };
524
532
 
525
- if (item.onClickFn) item.onClickFn();
533
+ const handleDeselect = (item: MenuListType) => {
534
+ if (selectedItems.value.length === 0 || !isItemSelected(item)) {
535
+ selectedItems.value = [item];
536
+ emit('get-single-selected-item', item);
537
+ } else {
538
+ selectedItems.value = [];
539
+ emit('get-single-deselected-item', item);
526
540
  }
541
+ };
527
542
 
528
- emit('get-single-selected-item', item);
543
+ const handleSingleSelect = (item: MenuListType) => {
544
+ selectedItems.value = [item];
545
+ if (item.onClickFn) item.onClickFn();
546
+ emit('get-single-selected-item', item);
529
547
  };
530
548
  // #endregion - Helper Methods
531
549
 
@@ -608,6 +626,20 @@ export const useList = (props: ListPropTypes, emit: SetupContext<ListEmitTypes>[
608
626
  setPreSelectedItems();
609
627
  });
610
628
 
629
+ // Handle search keyup to ignore modifier-only keys
630
+ const handleSearchKeyup = (event: KeyboardEvent) => {
631
+ // Ignore pure modifier keys: Shift, Control, Alt, Meta (Command/Windows), CapsLock
632
+ const modifierOnlyKeys = ['Shift', 'Control', 'Alt', 'Meta', 'CapsLock'];
633
+
634
+ if (!modifierOnlyKeys.includes(event.key)) {
635
+ // Allow the search to proceed - v-model will handle the actual update
636
+ return;
637
+ }
638
+
639
+ // For modifier-only keys, prevent default behavior if needed
640
+ event.preventDefault();
641
+ };
642
+
611
643
  return {
612
644
  listClasses,
613
645
  stickyOffsetStyle,
@@ -623,5 +655,6 @@ export const useList = (props: ListPropTypes, emit: SetupContext<ListEmitTypes>[
623
655
  handleSearch,
624
656
  handleSelectedItem,
625
657
  trackNewlySelectedItems,
658
+ handleSearchKeyup,
626
659
  };
627
660
  };
@@ -376,10 +376,14 @@ export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<M
376
376
  { deep: true },
377
377
  );
378
378
 
379
- watch(searchInput, () => {
380
- search.value = searchInput.value;
379
+ watch(searchInput, (newVal, oldVal) => {
380
+ search.value = newVal;
381
381
 
382
- emit('search-string', searchInput.value);
382
+ // Only emit search-string if value actually changed (not just modifier keys)
383
+ // Modifier key presses alone won't change the input value
384
+ if (newVal !== oldVal) {
385
+ emit('search-string', newVal);
386
+ }
383
387
  });
384
388
 
385
389
  watch(multiSelectPopperState, (newState) => {
@@ -148,11 +148,15 @@ export const useSelect = (props: SelectPropTypes, emit: SetupContext<SelectEmitT
148
148
  return selectOptions.value.filter((item) => item.text?.toString().toLowerCase().includes(query));
149
149
  });
150
150
 
151
- // Search handler: always emit search-string, but only filter locally if local search is enabled
152
- const handleSearch = () => {
153
- isSearching.value = true;
154
-
155
- debouncedEmitSearch();
151
+ // Search handler: only emit search-string on regular keys and ENTER, ignore modifier-only keys
152
+ const handleSearch = (event: KeyboardEvent) => {
153
+ // Ignore pure modifier keys: Shift, Control, Alt, Meta (Command/Windows), CapsLock
154
+ const modifierOnlyKeys = ['Shift', 'Control', 'Alt', 'Meta', 'CapsLock'];
155
+
156
+ if (!modifierOnlyKeys.includes(event.key)) {
157
+ isSearching.value = true;
158
+ debouncedEmitSearch();
159
+ }
156
160
  };
157
161
 
158
162
  const debouncedEmitSearch = useDebounceFn(() => {
@@ -20,6 +20,9 @@ export const sidepanelPropTypes = {
20
20
  type: String,
21
21
  default: 'Sidepanel Header',
22
22
  },
23
+ headerSubtitle: {
24
+ type: String
25
+ },
23
26
  /**
24
27
  * @description Specifies the size of the side panel.
25
28
  * Acceptable values are: `'sm'`, `'md'`, `'lg'`, `'xl'`.
@@ -104,6 +107,14 @@ export const sidepanelPropTypes = {
104
107
  isActivePanel: {
105
108
  type: Boolean,
106
109
  default: false
110
+ },
111
+ footerNoTopBorder: {
112
+ type: Boolean,
113
+ default: false,
114
+ },
115
+ isLoading: {
116
+ type: Boolean,
117
+ default: false,
107
118
  }
108
119
  };
109
120
 
@@ -17,21 +17,43 @@
17
17
  aria-labelledby="sidepanel-title"
18
18
  aria-describedby="sidepanel-content"
19
19
  :class="sidepanelClasses.sidepanelBaseClasses"
20
+ data-testid="sidepanel-dialog"
20
21
  :style="{ height: typeof height === 'number' ? `${height}px` : height }"
21
22
  >
22
23
  <template v-if="!props.hideHeader">
23
24
  <div v-if="!$slots.header" :class="sidepanelClasses.sidepanelHeaderClasses">
24
- <div id="sidepanel-title" :class="sidepanelClasses.sidepanelHeaderTitleClasses">
25
- {{ headerTitle }}
25
+ <div v-if="!isLoading" id="headers">
26
+ <div id="sidepanel-title" :class="sidepanelClasses.sidepanelHeaderTitleClasses">
27
+ {{ headerTitle }}
28
+ </div>
29
+ <div id="sidepanel-subtitle" :class="sidepanelClasses.sidepanelHeaderSubtitleClasses">
30
+ <slot name="subtitle">
31
+ <span class="spr-text-color-base">{{ headerSubtitle }}</span>
32
+ </slot>
33
+ </div>
26
34
  </div>
27
- <div class="spr-flex spr-items-center spr-gap-size-spacing-3xs">
35
+ <div v-else id="header-loaders" class="spr-w-full spr-flex spr-flex-col spr-gap-size-spacing-4xs">
36
+ <div class="spr-skeletal-loader spr-h-4 spr-w-[90%] spr-rounded-md"></div>
37
+ <div
38
+ v-if="headerSubtitle || $slots.subtitle"
39
+ class="spr-skeletal-loader spr-h-8 spr-w-[95%] spr-rounded-md"
40
+ ></div>
41
+ </div>
42
+
43
+ <div class="spr-flex spr-items-center spr-gap-size-spacing-3xs">
28
44
  <Icon
29
45
  v-if="props.isExpandable"
30
46
  :class="sidepanelClasses.sidepanelHeaderIconClasses"
31
47
  :icon="isExpanded ? 'ph:arrows-in-simple' : 'ph:arrows-out-simple'"
48
+ data-testid="expand-icon"
32
49
  @click="handlePanelExpansion"
33
50
  />
34
- <Icon :class="sidepanelClasses.sidepanelHeaderIconClasses" icon="ph:x" @click="handleClose" />
51
+ <Icon
52
+ :class="sidepanelClasses.sidepanelHeaderIconClasses"
53
+ icon="ph:x"
54
+ data-testid="x-icon"
55
+ @click="handleClose"
56
+ />
35
57
  </div>
36
58
  </div>
37
59
  <div v-else>