design-system-next 2.7.35 → 2.7.37

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.
@@ -6,30 +6,149 @@ import type { DropdownPropTypes, DropdownEmitTypes } from './dropdown';
6
6
  import type { MenuListType } from '../list/list';
7
7
 
8
8
  export const useDropdown = (props: DropdownPropTypes, emit: SetupContext<DropdownEmitTypes>['emit']) => {
9
- const { menuList, searchString, disabled, multiSelect, removeCurrentLevelInBackLabel, ladderized } = toRefs(props);
9
+ const { menuList, searchString, disabled, multiSelect, removeCurrentLevelInBackLabel, ladderized, textField, valueField } = toRefs(props);
10
10
 
11
11
  // Dropdown component ref variables
12
- const dropdownValue = useVModel(props, 'modelValue', emit); // v-model value of dropdown component
12
+ const dropdownValue = useVModel(props, 'modelValue', emit); // v-model value of dropdown component
13
13
  const dropdownRef = ref<HTMLDivElement | null>(null);
14
14
 
15
15
  // List component ref variables
16
16
  const selectedListItems = ref<MenuListType[]>([]); // v-model value of the list component
17
17
  const dropdownMenuList = ref<MenuListType[]>([]); // menu list for the list component
18
18
 
19
+ // Normalized value for internal use - always an array
20
+ const normalizedValue = computed(() => {
21
+ // If already an array, use it
22
+ if (Array.isArray(dropdownValue.value)) {
23
+ return dropdownValue.value;
24
+ }
25
+ // If not an array but has a value, make it a single-item array
26
+ if (dropdownValue.value !== undefined && dropdownValue.value !== null) {
27
+ return [dropdownValue.value];
28
+ }
29
+ // Default empty array
30
+ return [];
31
+ });
32
+
33
+ // Compatibility layer for pre-selected items (List component expects string[] format)
34
+ const compatPreSelectedItems = computed(() => {
35
+ // For ladderized dropdown with search, handle the special format
36
+ if (props.ladderized && Array.isArray(dropdownValue.value) && dropdownValue.value.length === 2) {
37
+ // We return only the second value from the [subvalue, value] format which is the actual selected value
38
+ return [dropdownValue.value[1]?.toString() || ''];
39
+ }
40
+
41
+ // For regular arrays (multi-select)
42
+ if (Array.isArray(dropdownValue.value)) {
43
+ return dropdownValue.value.map(item => {
44
+ if (item === undefined || item === null) return '';
45
+
46
+ // For numbers, preserve the original number value instead of converting to string
47
+ if (typeof item === 'number') return item;
48
+
49
+ // For objects, pass the original object reference
50
+ // This is key for proper multi-select of objects
51
+ if (typeof item === 'object') return item;
52
+
53
+ // For strings, pass as is
54
+ return item.toString();
55
+ });
56
+ }
57
+
58
+ // For single values (single-select)
59
+ return dropdownValue.value !== undefined && dropdownValue.value !== null
60
+ ? [typeof dropdownValue.value === 'object' ?
61
+ // Pass object reference directly instead of stringifying
62
+ dropdownValue.value :
63
+ // For numbers, preserve the original number value
64
+ typeof dropdownValue.value === 'number' ?
65
+ dropdownValue.value :
66
+ dropdownValue.value.toString()]
67
+ : [];
68
+ });
69
+
19
70
  // Popper state
20
71
  const dropdownPopperState = ref<boolean>(false);
21
72
  const isDropdownPopperDisabled = computed(() => disabled.value);
22
73
 
23
74
  const isLadderizedSearch = computed(
24
- () => ladderized.value && searchString.value !== '' && dropdownValue.value.length === 0,
75
+ () => ladderized.value && searchString.value !== '' && normalizedValue.value.length === 0,
25
76
  );
26
77
 
27
- const initializeMenuList = () => {
28
- dropdownMenuList.value = [...menuList.value];
78
+ const processMenuList = () => {
79
+ // Handle empty array or non-array values
80
+ if (!menuList.value || !Array.isArray(menuList.value) || menuList.value.length === 0) {
81
+ dropdownMenuList.value = [];
82
+ return;
83
+ }
84
+
85
+ // If ladderized is true and menu list items already conform to MenuListType, don't transform
86
+ if (ladderized.value) {
87
+ // Verify the items have the required structure for ladderized lists
88
+ const allValid = menuList.value.every(item =>
89
+ typeof item === 'object' && item !== null && 'text' in item && 'value' in item
90
+ );
91
+
92
+ if (allValid) {
93
+ dropdownMenuList.value = menuList.value as MenuListType[];
94
+ } else {
95
+ console.warn('Ladderized dropdown requires menu items in {text, value} format');
96
+ dropdownMenuList.value = [];
97
+ }
98
+ return;
99
+ }
100
+
101
+ const firstItem = menuList.value[0];
102
+
103
+ // Handle array of strings
104
+ if (typeof firstItem === 'string') {
105
+ dropdownMenuList.value = (menuList.value as string[]).map(item => ({
106
+ text: item,
107
+ value: item
108
+ }));
109
+ return;
110
+ }
111
+
112
+ // Handle array of numbers
113
+ if (typeof firstItem === 'number') {
114
+ dropdownMenuList.value = (menuList.value as number[]).map(item => ({
115
+ text: item.toString(),
116
+ value: item // Keep the value as a number instead of converting to string
117
+ }));
118
+ return;
119
+ }
120
+
121
+ // Handle array of objects with dynamic attributes
122
+ if (typeof firstItem === 'object' && firstItem !== null) {
123
+ // Check if it's already in MenuListType format
124
+ if ('text' in firstItem && 'value' in firstItem) {
125
+ dropdownMenuList.value = menuList.value as MenuListType[];
126
+ return;
127
+ }
128
+
129
+ // Transform to MenuListType format using textField and valueField
130
+ dropdownMenuList.value = (menuList.value as Record<string, unknown>[]).map(item => {
131
+ const displayText = item[textField.value] || 'Unnamed';
132
+ // Use the specified value field if available, otherwise use the entire object
133
+ const itemValue = valueField.value && item[valueField.value] !== undefined
134
+ ? item[valueField.value]
135
+ : item;
136
+
137
+ return {
138
+ text: displayText,
139
+ value: typeof itemValue === 'object' ? JSON.stringify(itemValue) : itemValue.toString(),
140
+ _originalObject: item // Store the original object for reference
141
+ };
142
+ });
143
+ return;
144
+ }
145
+
146
+ // Default fallback
147
+ dropdownMenuList.value = menuList.value as MenuListType[];
29
148
  };
30
149
 
31
150
  watch(menuList, () => {
32
- initializeMenuList();
151
+ processMenuList();
33
152
  });
34
153
 
35
154
  const handleSearch = () => {
@@ -40,8 +159,13 @@ export const useDropdown = (props: DropdownPropTypes, emit: SetupContext<Dropdow
40
159
  if (!multiSelect.value) {
41
160
  singleSelectSearch();
42
161
  } else {
43
- dropdownMenuList.value = [...menuList.value];
44
- // TODO: Handle multi-select search
162
+ // Process menu list for searching
163
+ processMenuList();
164
+
165
+ // Handle multi-select search - filter based on search string
166
+ if (searchString.value.trim() !== '') {
167
+ dropdownMenuList.value = getFilteredMenuList(dropdownMenuList.value);
168
+ }
45
169
  }
46
170
  };
47
171
 
@@ -54,19 +178,25 @@ export const useDropdown = (props: DropdownPropTypes, emit: SetupContext<Dropdow
54
178
  };
55
179
 
56
180
  const basicSearch = () => {
57
- dropdownMenuList.value = getFilteredMenuList(menuList.value);
181
+ // Process menu list first
182
+ processMenuList();
183
+
184
+ // Then filter based on search string
185
+ if (searchString.value.trim() !== '') {
186
+ dropdownMenuList.value = getFilteredMenuList(dropdownMenuList.value);
187
+ }
58
188
  };
59
189
 
60
190
  const ladderizedSearch = () => {
61
191
  //revert to initial list if search string is empty or dropdownValue is not empty
62
- if (searchString.value === '' || dropdownValue.value.length > 0) {
63
- dropdownMenuList.value = [...menuList.value];
192
+ if (searchString.value === '' || normalizedValue.value.length > 0) {
193
+ dropdownMenuList.value = [...menuList.value] as MenuListType[];
64
194
  return;
65
195
  }
66
196
 
67
- const menuListSubLevels = getAllSublevelItems(menuList.value);
197
+ const menuListSubLevels = getAllSublevelItems(menuList.value as MenuListType[]);
68
198
 
69
- const filteredMenuList = getFilteredMenuList(menuList.value);
199
+ const filteredMenuList = getFilteredMenuList(menuList.value as MenuListType[]);
70
200
  const filteredMenuListSubLevels = getFilteredMenuList(menuListSubLevels);
71
201
 
72
202
  if (filteredMenuList.length > 0) {
@@ -83,12 +213,12 @@ export const useDropdown = (props: DropdownPropTypes, emit: SetupContext<Dropdow
83
213
  // compile sublevel items from menuList to a single array
84
214
  // and add text and value of the parent item to all sublevel items as subtext and subvalue
85
215
  const getAllSublevelItems = (menuList: MenuListType[]) => {
86
- return menuList.reduce((currentValue, currentItem) => {
216
+ return menuList.reduce<MenuListType[]>((currentValue, currentItem) => {
87
217
  if (currentItem.sublevel) {
88
218
  const mappedSublevel = currentItem.sublevel.map((sublevelItem: MenuListType) => ({
89
219
  ...sublevelItem, //text and value of a sublevel item
90
220
  subtext: currentItem.text, // text of parent of a sublevel item
91
- subvalue: currentItem.value, // value of parent of a sublevel item
221
+ subvalue: typeof currentItem.value === 'string' ? currentItem.value : String(currentItem.value), // value of parent of a sublevel item as string
92
222
  }));
93
223
 
94
224
  return [...currentValue, ...mappedSublevel];
@@ -125,15 +255,80 @@ export const useDropdown = (props: DropdownPropTypes, emit: SetupContext<Dropdow
125
255
  // Handle selected item for simple list component
126
256
  const handleSelectedItem = (selectedItems: MenuListType[]) => {
127
257
  if (!props.ladderized) {
128
- dropdownValue.value = selectedItems.map((item) => item.value.toString());
129
- }
130
-
131
- if (!multiSelect.value) {
132
- if (props.ladderized && props.searchString !== '') {
258
+ // Determine the type of value to emit based on the original data type and multiSelect
259
+ if (multiSelect.value) {
260
+ // For multi-select, always return an array
261
+ const values = selectedItems.map(item => {
262
+ // If we stored the original object, use it
263
+ if ('_originalObject' in item) {
264
+ return item._originalObject;
265
+ }
266
+
267
+ // For simple types, handle value type conversion properly
268
+ const val = item.value;
269
+
270
+ // If it's already a number, keep it as a number
271
+ if (typeof val === 'number') {
272
+ return val;
273
+ }
274
+
275
+ // For strings that look like numbers, convert them
276
+ if (typeof val === 'string' && !isNaN(Number(val)) && val.trim() !== '') {
277
+ // Only convert if it looks like a proper number format
278
+ if (/^-?\d+(\.\d+)?$/.test(val)) {
279
+ return Number(val);
280
+ }
281
+ }
282
+
283
+ // Return the original value for all other cases
284
+ return val;
285
+ });
286
+
287
+ dropdownValue.value = values as (string | number | Record<string, unknown>)[];
288
+ } else {
289
+ // For single-select
290
+ if (selectedItems.length === 0) {
291
+ dropdownValue.value = props.multiSelect ? [] : '';
292
+ return;
293
+ }
294
+
295
+ const item = selectedItems[0];
296
+
297
+ // If we stored the original object, use it
298
+ if ('_originalObject' in item) {
299
+ dropdownValue.value = item._originalObject as Record<string, unknown>;
300
+ } else {
301
+ // For simple types, return the value (try to convert number strings to numbers)
302
+ const val = item.value;
303
+ if (typeof val === 'string' && !isNaN(Number(val)) && val.trim() !== '') {
304
+ dropdownValue.value = Number(val);
305
+ } else {
306
+ dropdownValue.value = val;
307
+ }
308
+ }
309
+ }
310
+ } else if (props.ladderized) {
311
+ if (props.searchString !== '') {
133
312
  // generate dropdown value if ladderized with search string
134
- dropdownValue.value = [selectedItems[0].subvalue ?? '', selectedItems[0].value];
313
+ const subvalue = selectedItems[0]?.subvalue;
314
+ const value = selectedItems[0]?.value;
315
+ if (subvalue !== undefined && value !== undefined) {
316
+ dropdownValue.value = [subvalue, value] as [string, string | number];
317
+ }
318
+ } else {
319
+ // For regular ladderized dropdown selection without search
320
+ if (selectedItems.length > 0) {
321
+ const item = selectedItems[0];
322
+ // Use the value directly for ladderized items
323
+ if (item && item.value) {
324
+ dropdownValue.value = item.value;
325
+ }
326
+ }
135
327
  }
328
+ }
136
329
 
330
+ // Always close dropdown for single selection, regardless of value type
331
+ if (!multiSelect.value) {
137
332
  setTimeout(() => {
138
333
  dropdownPopperState.value = false;
139
334
  }, 10);
@@ -142,6 +337,11 @@ export const useDropdown = (props: DropdownPropTypes, emit: SetupContext<Dropdow
142
337
 
143
338
  // Handle selected item for ladderized list component
144
339
  const handleSelectedLadderizedItem = (selectedItems: string[]) => {
340
+ // Update the model value with the selected ladderized items
341
+ if (selectedItems.length > 0) {
342
+ dropdownValue.value = selectedItems;
343
+ }
344
+
145
345
  // If item is from last sublevel, close the dropdown
146
346
  if (checkIfItemFromLastSublevel(selectedItems)) {
147
347
  dropdownPopperState.value = false;
@@ -164,8 +364,113 @@ export const useDropdown = (props: DropdownPropTypes, emit: SetupContext<Dropdow
164
364
  return true;
165
365
  };
166
366
 
367
+ // Update selected items when model value changes externally
368
+ const updateSelectedItemsFromValue = () => {
369
+ if (!dropdownMenuList.value.length) return;
370
+
371
+ const values = normalizedValue.value;
372
+ if (!values || !values.length) {
373
+ selectedListItems.value = [];
374
+ return;
375
+ }
376
+
377
+ // Store both original values and string versions for flexible matching
378
+ const valueData = values.map(val => {
379
+ if (val === undefined || val === null) return { original: '', string: '' };
380
+
381
+ // For objects, use JSON string representation
382
+ if (typeof val === 'object') {
383
+ return {
384
+ original: val,
385
+ string: JSON.stringify(val),
386
+ isObject: true,
387
+ id: 'id' in val ? val.id : undefined
388
+ };
389
+ }
390
+
391
+ // For numbers and strings, keep original and string versions
392
+ return {
393
+ original: val,
394
+ string: val.toString(),
395
+ isObject: false
396
+ };
397
+ });
398
+
399
+ // Extract just string values for comparison
400
+ const valueStrings = valueData.map(v => v.string);
401
+
402
+ if (props.ladderized) {
403
+ // Special handling for ladderized dropdowns
404
+ if (Array.isArray(dropdownValue.value) && dropdownValue.value.length === 2) {
405
+ // Handle [subvalue, value] format used in ladderized dropdowns with search
406
+ const subvalue = dropdownValue.value[0]?.toString() || '';
407
+ const value = dropdownValue.value[1]?.toString() || '';
408
+
409
+ selectedListItems.value = dropdownMenuList.value.filter(item => {
410
+ return item.value === value && (!item.subvalue || item.subvalue === subvalue);
411
+ });
412
+ } else {
413
+ // Regular ladderized dropdown value
414
+ selectedListItems.value = dropdownMenuList.value.filter(item => {
415
+ // Convert both to strings for comparison or check direct equality for numbers
416
+ if (typeof item.value === 'number') {
417
+ return valueData.some(v => v.original === item.value || v.string === String(item.value));
418
+ } else {
419
+ return valueStrings.includes(String(item.value));
420
+ }
421
+ });
422
+ }
423
+ } else {
424
+ // Regular dropdown value
425
+ selectedListItems.value = dropdownMenuList.value.filter(item => {
426
+ // Handle objects with _originalObject property
427
+ if ('_originalObject' in item && item._originalObject) {
428
+ return valueData.some(v => {
429
+ // If both are objects, compare by JSON string or by ID
430
+ if (v.isObject && typeof v.original === 'object') {
431
+ const originalObj = item._originalObject as Record<string, unknown>;
432
+
433
+ // First try direct equality comparison
434
+ if (v.original === originalObj) return true;
435
+
436
+ // Try JSON string comparison
437
+ const itemJson = JSON.stringify(originalObj);
438
+ if (v.string === itemJson) return true;
439
+
440
+ // Try ID-based comparison if both have ID fields
441
+ if (v.id !== undefined && 'id' in originalObj) {
442
+ return v.id === originalObj.id;
443
+ }
444
+ }
445
+ return false;
446
+ });
447
+ }
448
+
449
+ // Handle both numeric and string values correctly
450
+ if (typeof item.value === 'number') {
451
+ return valueData.some(v => v.original === item.value || v.string === String(item.value));
452
+ } else {
453
+ return valueStrings.includes(String(item.value));
454
+ }
455
+ });
456
+ }
457
+ };
458
+
459
+ watch(dropdownValue, () => {
460
+ updateSelectedItemsFromValue();
461
+ });
462
+
463
+ watch(dropdownMenuList, () => {
464
+ updateSelectedItemsFromValue();
465
+ });
466
+
167
467
  onMounted(() => {
168
- initializeMenuList();
468
+ processMenuList();
469
+
470
+ // Set initial selected items based on model value
471
+ if (normalizedValue.value.length > 0) {
472
+ updateSelectedItemsFromValue();
473
+ }
169
474
  });
170
475
 
171
476
  return {
@@ -176,7 +481,7 @@ export const useDropdown = (props: DropdownPropTypes, emit: SetupContext<Dropdow
176
481
  selectedListItems,
177
482
  handleSelectedItem,
178
483
  handleSelectedLadderizedItem,
179
- dropdownValue,
484
+ dropdownValue: compatPreSelectedItems, // Use compatible format for lists
180
485
  removeCurrentLevelInBackLabel,
181
486
  isLadderizedSearch,
182
487
  };
@@ -77,7 +77,7 @@
77
77
  :key="menu.field"
78
78
  v-model:shown="mappedFilterMenuList[menu.field].isFilterVisible"
79
79
  aria-id="filter-menu-wrapper"
80
- placement="right-start"
80
+ placement="right"
81
81
  :triggers="['click']"
82
82
  :auto-hide="false"
83
83
  >
@@ -118,7 +118,7 @@
118
118
 
119
119
  <div
120
120
  v-if="getSelectedFilterMenuOption.length > 0"
121
- class="spr-space-x-2 spr-space-y-2 spr-p-size-spacing-2xs"
121
+ class="spr-h-[70px] spr-space-x-2 spr-space-y-2 spr-overflow-auto spr-p-size-spacing-2xs"
122
122
  >
123
123
  <spr-chips
124
124
  v-for="(FilterMenuOption, i) in getSelectedFilterMenuOption"
@@ -133,7 +133,7 @@
133
133
  v-if="getFiltereredMenuOption.length > 0"
134
134
  :id="menu.field"
135
135
  ref="filterMenuOptionList"
136
- class="spr-max-h-[264px] spr-space-y-size-spacing-6xs spr-overflow-auto spr-p-size-spacing-2xs"
136
+ class="spr-h-[264px] spr-space-y-size-spacing-6xs spr-overflow-auto spr-p-size-spacing-2xs"
137
137
  >
138
138
  <div
139
139
  v-for="(option, key) in getFiltereredMenuOption"
@@ -144,11 +144,6 @@ export const useFilter = (props: FilterPropTypes, emit: SetupContext<FilterEmitT
144
144
  emit('infiniteScrollTrigger', true);
145
145
  };
146
146
 
147
- watch(loading, () => {
148
- if (loading.value) return;
149
- getMappedMenuOptionList();
150
- });
151
-
152
147
  watch(selectedColumn, (_value) => {
153
148
  setFilterVisible(_value);
154
149
  });
@@ -222,7 +217,9 @@ export const useFilter = (props: FilterPropTypes, emit: SetupContext<FilterEmitT
222
217
  const PopperContentClasses = classNames(
223
218
  'spr-flex spr-w-[328px] spr-flex-wrap spr-gap-size-spacing-2xs spr-p-size-spacing-xs',
224
219
  );
225
- const LoadingStateClasses = classNames('spr-p-size-spacing-sm spr-flex spr-items-center spr-justify-center');
220
+ const LoadingStateClasses = classNames(
221
+ 'spr-h-[264px] spr-p-size-spacing-sm spr-flex spr-items-center spr-justify-center',
222
+ );
226
223
  const ActionButtonClasses = classNames('spr-flex spr-justify-end spr-gap-2 spr-p-size-spacing-2xs');
227
224
  const filterListClasses = classNames(
228
225
  'spr-body-xs-regular spr-flex spr-cursor-pointer spr-justify-between spr-rounded-border-radius-md spr-p-size-spacing-4xs',
@@ -7,11 +7,12 @@ const GROUPED_ITEMS_BY_TYPES = ['A-Z', 'Z-A', 'default'] as const;
7
7
  export type MenuListType = {
8
8
  text: string;
9
9
  subtext?: string;
10
- value: string;
10
+ value: string | number; // Allow both string and number values
11
11
  subvalue?: string;
12
12
  sublevel?: MenuListType[];
13
13
  group?: string;
14
14
  disabled?: boolean;
15
+ _originalObject?: Record<string, unknown>; // Store original object reference when mapping complex objects
15
16
  };
16
17
 
17
18
  export type GroupedMenuListType = {
@@ -40,8 +41,8 @@ export const listPropTypes = {
40
41
  default: false,
41
42
  },
42
43
  preSelectedItems: {
43
- type: Array as PropType<string[]>,
44
- default: [],
44
+ type: Array as PropType<(string | number | Record<string, unknown>)[]>,
45
+ default: () => [],
45
46
  },
46
47
  ladderized: {
47
48
  type: Boolean,