design-system-next 2.7.44 → 2.8.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 (28) hide show
  1. package/dist/design-system-next.js +6416 -5577
  2. package/dist/design-system-next.js.gz +0 -0
  3. package/dist/main.css +1 -1
  4. package/dist/main.css.gz +0 -0
  5. package/dist/package.json.d.ts +1 -1
  6. package/package.json +1 -1
  7. package/src/assets/styles/tailwind.css +3 -1
  8. package/src/components/date-picker/use-date-picker.ts +3 -10
  9. package/src/components/dropdown/__tests__/dropdown-fixes.spec.ts +106 -0
  10. package/src/components/dropdown/__tests__/dropdown-value-types.spec.ts +213 -0
  11. package/src/components/list/ladderized-list/ladderized-list.ts +12 -0
  12. package/src/components/list/ladderized-list/ladderized-list.vue +6 -0
  13. package/src/components/list/list.ts +15 -3
  14. package/src/components/list/list.vue +37 -9
  15. package/src/components/list/use-list.ts +116 -47
  16. package/src/components/select/select-ladderized/select-ladderized.ts +108 -0
  17. package/src/components/select/select-ladderized/select-ladderized.vue +111 -0
  18. package/src/components/select/select-ladderized/use-select-ladderized.ts +165 -0
  19. package/src/components/select/select-multiple/select-multiple.ts +128 -115
  20. package/src/components/select/select-multiple/select-multiple.vue +149 -88
  21. package/src/components/select/select-multiple/use-select-multiple.ts +367 -499
  22. package/src/components/select/select.ts +34 -4
  23. package/src/components/select/select.vue +137 -59
  24. package/src/components/select/use-select.ts +166 -249
  25. package/src/components/select/select-laderrized/select-laderrized.ts +0 -122
  26. package/src/components/select/select-laderrized/select-laderrized.vue +0 -110
  27. package/src/components/select/select-laderrized/use-select-laderrized.ts +0 -499
  28. package/src/examples/select-number-multi-select.vue +0 -71
@@ -0,0 +1,213 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ref } from 'vue';
3
+ import { useDropdown } from '../use-dropdown';
4
+
5
+ describe('Dropdown Value Types', () => {
6
+ describe('Single primitive values', () => {
7
+ it('should handle string values', () => {
8
+ const modelValue = 'apple';
9
+ const menuList = ['apple', 'banana', 'cherry'];
10
+
11
+ const { dropdownMenuList, normalizedValue } = useDropdown({
12
+ id: 'test',
13
+ modelValue,
14
+ menuList
15
+ });
16
+
17
+ expect(normalizedValue.value).toEqual(['apple']);
18
+ expect(dropdownMenuList.value).toEqual([
19
+ { text: 'apple', value: 'apple' },
20
+ { text: 'banana', value: 'banana' },
21
+ { text: 'cherry', value: 'cherry' }
22
+ ]);
23
+ });
24
+
25
+ it('should handle number values', () => {
26
+ const modelValue = 42;
27
+ const menuList = [42, 55, 67];
28
+
29
+ const { dropdownMenuList, normalizedValue } = useDropdown({
30
+ id: 'test',
31
+ modelValue,
32
+ menuList
33
+ });
34
+
35
+ expect(normalizedValue.value).toEqual([42]);
36
+ expect(dropdownMenuList.value).toEqual([
37
+ { text: '42', value: '42' },
38
+ { text: '55', value: '55' },
39
+ { text: '67', value: '67' }
40
+ ]);
41
+ });
42
+ });
43
+
44
+ describe('Single object values', () => {
45
+ it('should handle object values', () => {
46
+ const employee = { systemId: 1, firstName: 'John', lastName: 'Doe' };
47
+ const modelValue = employee;
48
+ const menuList = [
49
+ { systemId: 1, firstName: 'John', lastName: 'Doe' },
50
+ { systemId: 2, firstName: 'Jane', lastName: 'Smith' }
51
+ ];
52
+
53
+ const { dropdownMenuList, normalizedValue } = useDropdown({
54
+ id: 'test',
55
+ modelValue,
56
+ menuList,
57
+ textField: 'firstName',
58
+ valueField: 'systemId'
59
+ });
60
+
61
+ expect(normalizedValue.value).toEqual([employee]);
62
+ expect(dropdownMenuList.value[0].text).toBe('John');
63
+ expect(dropdownMenuList.value[1].text).toBe('Jane');
64
+ expect(dropdownMenuList.value[0].value).toBe('1');
65
+ expect(dropdownMenuList.value[0]._originalObject).toEqual(menuList[0]);
66
+ });
67
+ });
68
+
69
+ describe('Multiple primitive values', () => {
70
+ it('should handle string arrays', () => {
71
+ const modelValue = ['apple', 'banana'];
72
+ const menuList = ['apple', 'banana', 'cherry'];
73
+
74
+ const { dropdownMenuList, normalizedValue } = useDropdown({
75
+ id: 'test',
76
+ modelValue,
77
+ menuList,
78
+ multiSelect: true
79
+ });
80
+
81
+ expect(normalizedValue.value).toEqual(['apple', 'banana']);
82
+ expect(dropdownMenuList.value).toEqual([
83
+ { text: 'apple', value: 'apple' },
84
+ { text: 'banana', value: 'banana' },
85
+ { text: 'cherry', value: 'cherry' }
86
+ ]);
87
+ });
88
+
89
+ it('should handle number arrays', () => {
90
+ const modelValue = [42, 55];
91
+ const menuList = [42, 55, 67];
92
+
93
+ const { dropdownMenuList, normalizedValue } = useDropdown({
94
+ id: 'test',
95
+ modelValue,
96
+ menuList,
97
+ multiSelect: true
98
+ });
99
+
100
+ expect(normalizedValue.value).toEqual([42, 55]);
101
+ expect(dropdownMenuList.value).toEqual([
102
+ { text: '42', value: '42' },
103
+ { text: '55', value: '55' },
104
+ { text: '67', value: '67' }
105
+ ]);
106
+ });
107
+ });
108
+
109
+ describe('Multiple object values', () => {
110
+ it('should handle object arrays', () => {
111
+ const employees = [
112
+ { systemId: 1, firstName: 'John', lastName: 'Doe' },
113
+ { systemId: 2, firstName: 'Jane', lastName: 'Smith' }
114
+ ];
115
+ const modelValue = employees;
116
+ const menuList = [
117
+ { systemId: 1, firstName: 'John', lastName: 'Doe' },
118
+ { systemId: 2, firstName: 'Jane', lastName: 'Smith' },
119
+ { systemId: 3, firstName: 'Mike', lastName: 'Johnson' }
120
+ ];
121
+
122
+ const { dropdownMenuList, normalizedValue } = useDropdown({
123
+ id: 'test',
124
+ modelValue,
125
+ menuList,
126
+ textField: 'firstName',
127
+ valueField: 'systemId',
128
+ multiSelect: true
129
+ });
130
+
131
+ expect(normalizedValue.value).toEqual(employees);
132
+ expect(dropdownMenuList.value[0].text).toBe('John');
133
+ expect(dropdownMenuList.value[1].text).toBe('Jane');
134
+ expect(dropdownMenuList.value[0].value).toBe('1');
135
+ expect(dropdownMenuList.value[0]._originalObject).toEqual(menuList[0]);
136
+ });
137
+ });
138
+
139
+ describe('Handle selected items', () => {
140
+ it('should output string for string selection', () => {
141
+ const modelValue = ref(undefined);
142
+ const menuList = ['apple', 'banana', 'cherry'];
143
+
144
+ const { handleSelectedItem } = useDropdown({
145
+ id: 'test',
146
+ modelValue,
147
+ menuList,
148
+ });
149
+
150
+ handleSelectedItem([{ text: 'apple', value: 'apple' }]);
151
+ expect(modelValue.value).toBe('apple');
152
+ });
153
+
154
+ it('should output number for number selection', () => {
155
+ const modelValue = ref(undefined);
156
+ const menuList = [42, 55, 67];
157
+
158
+ const { handleSelectedItem } = useDropdown({
159
+ id: 'test',
160
+ modelValue,
161
+ menuList,
162
+ });
163
+
164
+ handleSelectedItem([{ text: '42', value: '42' }]);
165
+ expect(modelValue.value).toBe(42);
166
+ });
167
+
168
+ it('should output original object for object selection', () => {
169
+ const modelValue = ref(undefined);
170
+ const employeeObj = { systemId: 1, firstName: 'John', lastName: 'Doe' };
171
+ const menuList = [
172
+ employeeObj,
173
+ { systemId: 2, firstName: 'Jane', lastName: 'Smith' }
174
+ ];
175
+
176
+ const { handleSelectedItem, dropdownMenuList } = useDropdown({
177
+ id: 'test',
178
+ modelValue,
179
+ menuList,
180
+ textField: 'firstName',
181
+ valueField: 'systemId'
182
+ });
183
+
184
+ // Process menu list first to get _originalObject
185
+ dropdownMenuList.value.forEach(item => {
186
+ if (item.value === '1') {
187
+ handleSelectedItem([item]);
188
+ }
189
+ });
190
+
191
+ expect(modelValue.value).toEqual(employeeObj);
192
+ });
193
+
194
+ it('should output array for multi-select', () => {
195
+ const modelValue = ref([]);
196
+ const menuList = ['apple', 'banana', 'cherry'];
197
+
198
+ const { handleSelectedItem } = useDropdown({
199
+ id: 'test',
200
+ modelValue,
201
+ menuList,
202
+ multiSelect: true
203
+ });
204
+
205
+ handleSelectedItem([
206
+ { text: 'apple', value: 'apple' },
207
+ { text: 'banana', value: 'banana' }
208
+ ]);
209
+
210
+ expect(modelValue.value).toEqual(['apple', 'banana']);
211
+ });
212
+ });
213
+ });
@@ -13,6 +13,18 @@ export const ladderizedListPropTypes = {
13
13
  required: true,
14
14
  default: [],
15
15
  },
16
+ menuLevel: {
17
+ type: Number,
18
+ default: 0,
19
+ },
20
+ searchableMenu: {
21
+ type: Boolean,
22
+ default: false,
23
+ },
24
+ searchableMenuPlaceholder: {
25
+ type: String,
26
+ default: 'Search...',
27
+ },
16
28
  removeCurrentLevelInBackLabel: {
17
29
  type: Boolean,
18
30
  default: false,
@@ -7,8 +7,11 @@
7
7
  v-model="selectedListItem"
8
8
  class="spr-p-size-spacing-3xs"
9
9
  :menu-list="activeList"
10
+ :menu-level="props.menuLevel"
10
11
  :multi-select="false"
11
12
  :ladderized="true"
13
+ :searchable-menu="props.searchableMenu"
14
+ :searchable-menu-placeholder="props.searchableMenuPlaceholder"
12
15
  @update:model-value="(value) => handleSelectedListItem(value[0])"
13
16
  />
14
17
  </div>
@@ -19,8 +22,11 @@
19
22
  v-model="selectedListItem"
20
23
  class="spr-p-size-spacing-3xs"
21
24
  :menu-list="activeList"
25
+ :menu-level="props.menuLevel"
22
26
  :multi-select="false"
23
27
  :ladderized="true"
28
+ :searchable-menu="props.searchableMenu"
29
+ :searchable-menu-placeholder="props.searchableMenuPlaceholder"
24
30
  @update:model-value="(value) => handleSelectedListItem(value[0])"
25
31
  />
26
32
  </div>
@@ -7,18 +7,18 @@ 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 | number; // Allow both string and number values
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
+ _originalObject?: Record<string, unknown>; // Store original object reference when mapping complex objects
16
16
  };
17
17
 
18
18
  export type GroupedMenuListType = {
19
19
  groupLabel: string;
20
20
  items: MenuListType[];
21
- }
21
+ };
22
22
 
23
23
  export const listPropTypes = {
24
24
  modelValue: {
@@ -40,6 +40,18 @@ export const listPropTypes = {
40
40
  type: Boolean,
41
41
  default: false,
42
42
  },
43
+ menuLevel: {
44
+ type: Number,
45
+ default: 0,
46
+ },
47
+ searchableMenu: {
48
+ type: Boolean,
49
+ default: false,
50
+ },
51
+ searchableMenuPlaceholder: {
52
+ type: String,
53
+ default: 'Search...',
54
+ },
43
55
  preSelectedItems: {
44
56
  type: Array as PropType<(string | number | Record<string, unknown>)[]>,
45
57
  default: () => [],
@@ -1,5 +1,16 @@
1
1
  <template>
2
2
  <div class="spr-font-main">
3
+ <div v-if="props.searchableMenu" class="spr-mb-3 spr-grid spr-gap-3">
4
+ <spr-input
5
+ v-model="searchText"
6
+ :placeholder="props.searchableMenuPlaceholder"
7
+ autocomplete="off"
8
+ @keyup="handleSearch"
9
+ />
10
+
11
+ <div v-if="isParentMenu" class="spr-background-color-surface spr-h-[1px]"></div>
12
+ </div>
13
+
3
14
  <template v-if="props.groupItemsBy">
4
15
  <div class="spr-grid spr-gap-2">
5
16
  <div v-for="(list, listIndex) in groupedMenuList" :key="listIndex" class="spr-grid spr-gap-0.5">
@@ -46,11 +57,21 @@
46
57
  @click="handleSelectedItem(item)"
47
58
  >
48
59
  <spr-checkbox v-if="props.multiSelect" :disabled="item.disabled" :checked="isItemSelected(item)" />
49
- <div :class="['spr-flex spr-flex-auto spr-flex-col spr-justify-start', { 'spr-text-color-disabled': item.disabled }]">
60
+ <div
61
+ :class="[
62
+ 'spr-flex spr-flex-auto spr-flex-col spr-justify-start',
63
+ { 'spr-text-color-disabled': item.disabled },
64
+ ]"
65
+ >
50
66
  <span class="spr-text-left spr-text-xs">{{ item.text }}</span>
51
- <span v-if="item.subtext" :class="['spr-body-xs-regular spr-text-color-base spr-text-left', { 'spr-text-color-disabled': item.disabled }]">{{
52
- item.subtext
53
- }}</span>
67
+ <span
68
+ v-if="item.subtext"
69
+ :class="[
70
+ 'spr-body-xs-regular spr-text-color-base spr-text-left',
71
+ { 'spr-text-color-disabled': item.disabled },
72
+ ]"
73
+ >{{ item.subtext }}</span
74
+ >
54
75
  </div>
55
76
  <Icon
56
77
  v-if="isItemSelected(item) && !props.multiSelect"
@@ -73,13 +94,20 @@ import { Icon } from '@iconify/vue';
73
94
  import { listPropTypes, listEmitTypes } from './list';
74
95
  import { useList } from './use-list';
75
96
 
76
- import SprCheckbox from '../checkbox/checkbox.vue';
97
+ import SprCheckbox from '@/components/checkbox/checkbox.vue';
98
+ import SprInput from '@/components/input/input.vue';
77
99
 
78
100
  const props = defineProps(listPropTypes);
79
101
  const emit = defineEmits(listEmitTypes);
80
102
 
81
- const { localizedMenuList, groupedMenuList, isItemSelected, getListItemClasses, handleSelectedItem } = useList(
82
- props,
83
- emit,
84
- );
103
+ const {
104
+ searchText,
105
+ localizedMenuList,
106
+ groupedMenuList,
107
+ isParentMenu,
108
+ isItemSelected,
109
+ getListItemClasses,
110
+ handleSearch,
111
+ handleSelectedItem,
112
+ } = useList(props, emit);
85
113
  </script>
@@ -13,7 +13,7 @@ interface ListClasses {
13
13
  export const useList = (props: ListPropTypes, emit: SetupContext<ListEmitTypes>['emit']) => {
14
14
  const selectedItems = useVModel(props, 'modelValue', emit);
15
15
 
16
- const { menuList, groupItemsBy, multiSelect, preSelectedItems } = toRefs(props);
16
+ const { menuList, menuLevel, groupItemsBy, multiSelect, preSelectedItems } = toRefs(props);
17
17
 
18
18
  const listClasses: ComputedRef<ListClasses> = computed(() => {
19
19
  const listItemClasses = classNames(
@@ -26,6 +26,8 @@ export const useList = (props: ListPropTypes, emit: SetupContext<ListEmitTypes>[
26
26
  return { listItemClasses };
27
27
  });
28
28
 
29
+ const searchText = ref<string>('');
30
+
29
31
  const localizedMenuList = ref<MenuListType[]>([]);
30
32
  const groupedMenuList = ref<GroupedMenuListType[]>([
31
33
  {
@@ -35,66 +37,72 @@ export const useList = (props: ListPropTypes, emit: SetupContext<ListEmitTypes>[
35
37
  ]);
36
38
 
37
39
  // #region - Helper Methods
40
+ const isParentMenu = computed(() => menuLevel.value === 0);
41
+
38
42
  const isItemSelected = (item: MenuListType) => {
39
43
  // First check standard selection via the selectedItems array
40
44
  const directSelected = selectedItems.value.some((selectedItem) => {
41
45
  // Compare both text and value properties to handle different value types
42
46
  if (selectedItem.text === item.text) return true;
43
-
47
+
44
48
  // Ensure comparison works for both string and number values
45
49
  const selectedItemValue = selectedItem.value;
46
50
  const itemValue = item.value;
47
-
51
+
48
52
  // For primitives, use string comparison to handle number-string comparison properly
49
53
  if (typeof selectedItemValue !== 'object' && typeof itemValue !== 'object') {
50
54
  return String(selectedItemValue) === String(itemValue);
51
55
  }
52
-
56
+
53
57
  // For objects, use JSON.stringify for comparison (will match for equality)
54
- if (typeof selectedItemValue === 'object' && selectedItemValue !== null &&
55
- typeof itemValue === 'object' && itemValue !== null) {
58
+ if (
59
+ typeof selectedItemValue === 'object' &&
60
+ selectedItemValue !== null &&
61
+ typeof itemValue === 'object' &&
62
+ itemValue !== null
63
+ ) {
56
64
  return JSON.stringify(selectedItemValue) === JSON.stringify(itemValue);
57
65
  }
58
-
66
+
59
67
  return false;
60
68
  });
61
-
69
+
62
70
  if (directSelected) return true;
63
-
71
+
64
72
  // Additional check for objects stored in _originalObject property
65
73
  if ('_originalObject' in item && item._originalObject && preSelectedItems.value?.length) {
66
- return preSelectedItems.value.some(preSelectedValue => {
74
+ return preSelectedItems.value.some((preSelectedValue) => {
67
75
  // Direct reference comparison (most accurate)
68
76
  if (preSelectedValue === item._originalObject) {
69
77
  return true;
70
78
  }
71
-
79
+
72
80
  // If both are objects, compare their serialized forms
73
81
  if (typeof preSelectedValue === 'object' && preSelectedValue !== null) {
74
82
  const originalObj = item._originalObject as Record<string, unknown>;
75
-
83
+
76
84
  if (typeof originalObj === 'object') {
77
85
  // First try comparing by ID for more reliable object comparison
78
86
  if ('id' in preSelectedValue && 'id' in originalObj) {
79
87
  return preSelectedValue.id === originalObj.id;
80
88
  }
81
-
89
+
82
90
  // Fallback to full object comparison
83
91
  const valString = JSON.stringify(preSelectedValue);
84
92
  const itemString = JSON.stringify(originalObj);
85
93
  return valString === itemString;
86
94
  }
87
-
95
+
88
96
  // If object has an id field, check if it matches with the item value
89
97
  if ('id' in preSelectedValue) {
90
98
  return String(item.value).includes(String(preSelectedValue.id));
91
99
  }
92
100
  }
93
-
101
+
94
102
  return false;
95
103
  });
96
104
  }
97
-
105
+
98
106
  return false;
99
107
  };
100
108
 
@@ -159,52 +167,48 @@ export const useList = (props: ListPropTypes, emit: SetupContext<ListEmitTypes>[
159
167
  // For objects, check for matching _originalObject properties
160
168
  if (typeof preSelectedItem === 'object' && preSelectedItem !== null) {
161
169
  // Try to find an item with a matching _originalObject
162
- const objectMatch = localizedMenuList.value.find(menuItem => {
170
+ const objectMatch = localizedMenuList.value.find((menuItem) => {
163
171
  if (!menuItem._originalObject) return false;
164
-
172
+
165
173
  // Compare serialized versions for deep equality
166
174
  return JSON.stringify(menuItem._originalObject) === JSON.stringify(preSelectedItem);
167
175
  });
168
-
176
+
169
177
  if (objectMatch) return objectMatch;
170
-
178
+
171
179
  // If no direct object match, try matching on ID if both have it
172
180
  if ('id' in preSelectedItem) {
173
- const idMatch = localizedMenuList.value.find(menuItem => {
181
+ const idMatch = localizedMenuList.value.find((menuItem) => {
174
182
  if (menuItem._originalObject && 'id' in menuItem._originalObject) {
175
183
  return menuItem._originalObject.id === preSelectedItem.id;
176
184
  }
177
185
  // Also check if the value field contains a stringified version that includes the id
178
186
  return String(menuItem.value).includes(String(preSelectedItem.id));
179
187
  });
180
-
188
+
181
189
  if (idMatch) return idMatch;
182
190
  }
183
191
  }
184
-
192
+
185
193
  // First try direct value comparison (for exact matches)
186
- const directMatch = localizedMenuList.value.find(
187
- (menuItem) => menuItem.value === preSelectedItem
188
- );
194
+ const directMatch = localizedMenuList.value.find((menuItem) => menuItem.value === preSelectedItem);
189
195
  if (directMatch) return directMatch;
190
-
196
+
191
197
  // Special handling for number values in the preSelectedItems array
192
198
  if (typeof preSelectedItem === 'number') {
193
199
  // Find items that match the number value either directly or as a string
194
200
  const numericMatch = localizedMenuList.value.find(
195
- (menuItem) =>
201
+ (menuItem) =>
196
202
  // Match if menuItem.value is the same number
197
203
  (typeof menuItem.value === 'number' && menuItem.value === preSelectedItem) ||
198
204
  // Match if menuItem.value is a string representation of the number
199
- (typeof menuItem.value === 'string' && menuItem.value === String(preSelectedItem))
205
+ (typeof menuItem.value === 'string' && menuItem.value === String(preSelectedItem)),
200
206
  );
201
207
  if (numericMatch) return numericMatch;
202
208
  }
203
-
209
+
204
210
  // Then try string comparison for cases where types differ (string vs number)
205
- return localizedMenuList.value.find(
206
- (menuItem) => String(menuItem.value) === String(preSelectedItem)
207
- );
211
+ return localizedMenuList.value.find((menuItem) => String(menuItem.value) === String(preSelectedItem));
208
212
  })
209
213
  .filter(Boolean) as MenuListType[];
210
214
 
@@ -229,50 +233,112 @@ export const useList = (props: ListPropTypes, emit: SetupContext<ListEmitTypes>[
229
233
  const getListItemClasses = (item: MenuListType) => ({
230
234
  [listClasses.value.listItemClasses]: !item.disabled,
231
235
  'spr-background-color-single-active': isItemSelected(item) && !item.disabled,
232
- 'hover:spr-cursor-not-allowed spr-flex spr-cursor-pointer spr-items-center spr-gap-1.5 spr-rounded-lg spr-p-2': item.disabled,
236
+ 'hover:spr-cursor-not-allowed spr-flex spr-cursor-pointer spr-items-center spr-gap-1.5 spr-rounded-lg spr-p-2':
237
+ item.disabled,
233
238
  });
234
239
 
240
+ const handleSearch = () => {
241
+ const search = searchText.value.trim().toLowerCase();
242
+
243
+ if (!search) {
244
+ setMenuList();
245
+
246
+ return;
247
+ }
248
+
249
+ // Filter items by text or subtext
250
+ const filtered = props.menuList.filter((item) => {
251
+ const textMatch = item.text.toLowerCase().includes(search);
252
+ const subtextMatch = item.subtext ? item.subtext.toLowerCase().includes(search) : false;
253
+
254
+ return textMatch || subtextMatch;
255
+ });
256
+
257
+ localizedMenuList.value = filtered;
258
+
259
+ // If grouping is enabled, regroup the filtered list
260
+ if (groupItemsBy?.value) {
261
+ groupedMenuList.value = [{ groupLabel: 'no-group', items: [] }];
262
+ if (groupItemsBy.value === 'default') {
263
+ filtered.forEach((item) => {
264
+ const groupKey = item.group || 'no-group';
265
+
266
+ if (groupedMenuList.value.some((g) => g.groupLabel === groupKey)) {
267
+ groupedMenuList.value.find((g) => g.groupLabel === groupKey)?.items.push(item);
268
+ } else {
269
+ groupedMenuList.value.push({ groupLabel: groupKey, items: [item] });
270
+ }
271
+ });
272
+ } else {
273
+ filtered
274
+ .sort((a, b) => {
275
+ if (groupItemsBy.value === 'A-Z') return a.text.localeCompare(b.text);
276
+ if (groupItemsBy.value === 'Z-A') return b.text.localeCompare(a.text);
277
+
278
+ return 0;
279
+ })
280
+ .forEach((item) => {
281
+ const firstCharacter = item.text.charAt(0);
282
+ const groupKey = /^\d/.test(firstCharacter) ? 'no-group' : firstCharacter.toUpperCase();
283
+
284
+ if (groupedMenuList.value.some((g) => g.groupLabel === groupKey)) {
285
+ groupedMenuList.value.find((g) => g.groupLabel === groupKey)?.items.push(item);
286
+ } else {
287
+ groupedMenuList.value.push({ groupLabel: groupKey, items: [item] });
288
+ }
289
+ });
290
+ }
291
+ }
292
+ };
293
+
235
294
  const handleSelectedItem = (item: MenuListType) => {
236
- if(item.disabled) return;
295
+ if (item.disabled) return;
237
296
 
238
297
  if (multiSelect.value) {
239
298
  // For multi-select, check if item is already selected
240
299
  const index = selectedItems.value.findIndex((selectedItem: MenuListType) => {
241
300
  // Compare text values first for simple match
242
301
  if (selectedItem.text === item.text) return true;
243
-
302
+
244
303
  // Compare primitive values with string conversion for compatibility
245
304
  if (typeof selectedItem.value !== 'object' && typeof item.value !== 'object') {
246
305
  return String(selectedItem.value) === String(item.value);
247
306
  }
248
-
307
+
249
308
  // For objects, compare their JSON string representations
250
- if (typeof selectedItem.value === 'object' && selectedItem.value !== null &&
251
- typeof item.value === 'object' && item.value !== null) {
309
+ if (
310
+ typeof selectedItem.value === 'object' &&
311
+ selectedItem.value !== null &&
312
+ typeof item.value === 'object' &&
313
+ item.value !== null
314
+ ) {
252
315
  return JSON.stringify(selectedItem.value) === JSON.stringify(item.value);
253
316
  }
254
-
317
+
255
318
  // Compare _originalObject if available (most reliable for complex objects)
256
- if ('_originalObject' in selectedItem && selectedItem._originalObject &&
257
- '_originalObject' in item && item._originalObject) {
258
-
319
+ if (
320
+ '_originalObject' in selectedItem &&
321
+ selectedItem._originalObject &&
322
+ '_originalObject' in item &&
323
+ item._originalObject
324
+ ) {
259
325
  // Direct reference equality check (fastest)
260
326
  if (selectedItem._originalObject === item._originalObject) {
261
327
  return true;
262
328
  }
263
-
329
+
264
330
  // ID-based comparison (reliable for objects with IDs)
265
331
  const selectedObj = selectedItem._originalObject as Record<string, unknown>;
266
332
  const itemObj = item._originalObject as Record<string, unknown>;
267
-
333
+
268
334
  if ('id' in selectedObj && 'id' in itemObj) {
269
335
  return selectedObj.id === itemObj.id;
270
336
  }
271
-
337
+
272
338
  // Full JSON comparison (most comprehensive but slower)
273
339
  return JSON.stringify(selectedItem._originalObject) === JSON.stringify(item._originalObject);
274
340
  }
275
-
341
+
276
342
  return false;
277
343
  });
278
344
 
@@ -306,11 +372,14 @@ export const useList = (props: ListPropTypes, emit: SetupContext<ListEmitTypes>[
306
372
  });
307
373
 
308
374
  return {
375
+ searchText,
309
376
  listClasses,
310
377
  localizedMenuList,
311
378
  groupedMenuList,
379
+ isParentMenu,
312
380
  isItemSelected,
313
381
  getListItemClasses,
382
+ handleSearch,
314
383
  handleSelectedItem,
315
384
  };
316
385
  };