design-system-next 2.9.1 → 2.9.4

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.
@@ -118,7 +118,9 @@ export const useCalendar = (props: CalendarPropTypes, emit: SetupContext<Calenda
118
118
  };
119
119
 
120
120
  const handleSorting = () => {
121
- state.sort.value = state.sort.value === 'desc' ? 'asc' : 'desc';
121
+ if (state.sort.value === 'asc') state.sort.value = '';
122
+ else state.sort.value = state.sort.value === 'desc' ? 'asc' : 'desc';
123
+
122
124
  emit('update:sort', state.sort.value);
123
125
  };
124
126
 
@@ -1,164 +1,164 @@
1
- import { ref, toRefs, computed, watch } from 'vue';
2
- import { useVModel, useDebounceFn, onClickOutside } from '@vueuse/core';
3
-
4
- import type { SelectLadderizedPropTypes } from './select-ladderized';
5
-
6
- import type { MenuListType } from '@/components/list/list';
7
-
8
- export const useSelectLadderized = (
9
- props: SelectLadderizedPropTypes,
10
- emit: (event: string, ...args: unknown[]) => void,
11
- ) => {
12
- const { options, disabled } = toRefs(props);
13
-
14
- const ladderizedClasses = computed(() => ({
15
- baseClasses: 'spr-flex spr-flex-col spr-gap-size-spacing-4xs',
16
- labelClasses: 'spr-body-sm-regular spr-text-color-strong spr-block',
17
- }));
18
-
19
- // Popper Variables
20
- const ladderizedSelectPopperState = ref(false);
21
- const ladderizedSelectRef = ref(null);
22
- const isLadderizedSelectPopperDisabled = computed(() => disabled.value);
23
-
24
- // Ladderized Select Model
25
- const ladderizedSelectModel = useVModel(props, 'modelValue', emit);
26
- const ladderizedSelectOptions = computed(() => options.value);
27
-
28
- // Input Variables
29
- const inputText = ref<string>('');
30
- const isSearching = ref(false);
31
- const wasCleared = ref(false);
32
-
33
- const isLeafNode = (item: MenuListType): boolean => {
34
- return !item.sublevel || item.sublevel.length === 0;
35
- };
36
-
37
- // Helper to find the path to a selected value in the menu tree
38
- const findPathToValue = (items: MenuListType[], value: string | number, path: string[] = []): string[] | null => {
39
- for (const item of items) {
40
- const newPath = [...path, item.text];
41
-
42
- if (item.value === value) {
43
- return newPath;
44
- }
45
-
46
- if (item.sublevel) {
47
- const result = findPathToValue(item.sublevel, value, newPath);
48
-
49
- if (result) return result;
50
- }
51
- }
52
-
53
- return null;
54
- };
55
-
56
- const handleSelectedLadderizedItem = (selectedItems: string[], selectedItem?: MenuListType) => {
57
- wasCleared.value = false;
58
- ladderizedSelectModel.value = selectedItems;
59
-
60
- let itemToCheck = selectedItem;
61
-
62
- // Fallback: if selectedItem is not provided, try to find it from the value
63
- if (!itemToCheck && selectedItems.length > 0) {
64
- const findItemByValue = (items: MenuListType[], value: string | number): MenuListType | undefined => {
65
- for (const item of items) {
66
- if (item.value === value) return item;
67
-
68
- if (item.sublevel) {
69
- const found = findItemByValue(item.sublevel, value);
70
-
71
- if (found) return found;
72
- }
73
- }
74
-
75
- return undefined;
76
- };
77
-
78
- itemToCheck = findItemByValue(ladderizedSelectOptions.value, selectedItems[selectedItems.length - 1]);
79
- }
80
-
81
- if (itemToCheck) {
82
- const path = findPathToValue(ladderizedSelectOptions.value, itemToCheck.value);
83
-
84
- inputText.value = path ? path.join(' > ') : itemToCheck.text || '';
85
-
86
- if (isLeafNode(itemToCheck)) {
87
- ladderizedSelectPopperState.value = false;
88
- }
89
- } else if (selectedItems.length === 0 && !wasCleared.value) {
90
- inputText.value = '';
91
- }
92
- };
93
-
94
- const handleSearch = () => {
95
- isSearching.value = true;
96
-
97
- debouncedEmitSearch();
98
- };
99
-
100
- const debouncedEmitSearch = useDebounceFn(() => {
101
- // Optionally emit search event here if needed
102
- }, 300);
103
-
104
- const handleClear = () => {
105
- wasCleared.value = true;
106
-
107
- inputText.value = '';
108
-
109
- emit('update:modelValue', []);
110
- };
111
-
112
- const handleOptionsToggle = () => {
113
- ladderizedSelectPopperState.value = true;
114
-
115
- isSearching.value = false;
116
- };
117
-
118
- // Watch for changes in modelValue to update inputText
119
- watch(
120
- () => ladderizedSelectModel.value,
121
- (newVal) => {
122
- if (wasCleared.value) {
123
- inputText.value = '';
124
- wasCleared.value = false;
125
- return;
126
- }
127
-
128
- if (Array.isArray(newVal) && newVal.length > 0) {
129
- // Treat the array as a single path for ladderized select
130
- let currentLevel = ladderizedSelectOptions.value;
131
-
132
- const pathTexts: string[] = [];
133
-
134
- for (const value of newVal) {
135
- const found = currentLevel.find((item) => item.value === value);
136
- if (!found) break;
137
- pathTexts.push(found.text);
138
- currentLevel = found.sublevel || [];
139
- }
140
-
141
- inputText.value = pathTexts.join(' > ');
142
- }
143
- },
144
- { immediate: true },
145
- );
146
-
147
- onClickOutside(ladderizedSelectRef, () => {
148
- ladderizedSelectPopperState.value = false;
149
- });
150
-
151
- return {
152
- ladderizedClasses,
153
- ladderizedSelectPopperState,
154
- ladderizedSelectRef,
155
- ladderizedSelectOptions,
156
- isLadderizedSelectPopperDisabled,
157
- ladderizedSelectModel,
158
- inputText,
159
- handleSelectedLadderizedItem,
160
- handleSearch,
161
- handleClear,
162
- handleOptionsToggle,
163
- };
164
- };
1
+ import { ref, toRefs, computed, watch } from 'vue';
2
+ import { useVModel, useDebounceFn, onClickOutside } from '@vueuse/core';
3
+
4
+ import type { SelectLadderizedPropTypes } from './select-ladderized';
5
+
6
+ import type { MenuListType } from '@/components/list/list';
7
+
8
+ export const useSelectLadderized = (
9
+ props: SelectLadderizedPropTypes,
10
+ emit: (event: string, ...args: unknown[]) => void,
11
+ ) => {
12
+ const { options, disabled } = toRefs(props);
13
+
14
+ const ladderizedClasses = computed(() => ({
15
+ baseClasses: 'spr-flex spr-flex-col spr-gap-size-spacing-4xs',
16
+ labelClasses: 'spr-body-sm-regular spr-text-color-strong spr-block',
17
+ }));
18
+
19
+ // Popper Variables
20
+ const ladderizedSelectPopperState = ref(false);
21
+ const ladderizedSelectRef = ref(null);
22
+ const isLadderizedSelectPopperDisabled = computed(() => disabled.value);
23
+
24
+ // Ladderized Select Model
25
+ const ladderizedSelectModel = useVModel(props, 'modelValue', emit);
26
+ const ladderizedSelectOptions = computed(() => options.value);
27
+
28
+ // Input Variables
29
+ const inputText = ref<string>('');
30
+ const isSearching = ref(false);
31
+ const wasCleared = ref(false);
32
+
33
+ const isLeafNode = (item: MenuListType): boolean => {
34
+ return !item.sublevel || item.sublevel.length === 0;
35
+ };
36
+
37
+ // Helper to find the path to a selected value in the menu tree
38
+ const findPathToValue = (items: MenuListType[], value: string | number, path: string[] = []): string[] | null => {
39
+ for (const item of items) {
40
+ const newPath = [...path, item.text];
41
+
42
+ if (item.value === value) {
43
+ return newPath;
44
+ }
45
+
46
+ if (item.sublevel) {
47
+ const result = findPathToValue(item.sublevel, value, newPath);
48
+
49
+ if (result) return result;
50
+ }
51
+ }
52
+
53
+ return null;
54
+ };
55
+
56
+ const handleSelectedLadderizedItem = (selectedItems: string[], selectedItem?: MenuListType) => {
57
+ wasCleared.value = false;
58
+ ladderizedSelectModel.value = selectedItems;
59
+
60
+ let itemToCheck = selectedItem;
61
+
62
+ // Fallback: if selectedItem is not provided, try to find it from the value
63
+ if (!itemToCheck && selectedItems.length > 0) {
64
+ const findItemByValue = (items: MenuListType[], value: string | number): MenuListType | undefined => {
65
+ for (const item of items) {
66
+ if (item.value === value) return item;
67
+
68
+ if (item.sublevel) {
69
+ const found = findItemByValue(item.sublevel, value);
70
+
71
+ if (found) return found;
72
+ }
73
+ }
74
+
75
+ return undefined;
76
+ };
77
+
78
+ itemToCheck = findItemByValue(ladderizedSelectOptions.value, selectedItems[selectedItems.length - 1]);
79
+ }
80
+
81
+ if (itemToCheck) {
82
+ const path = findPathToValue(ladderizedSelectOptions.value, itemToCheck.value);
83
+
84
+ inputText.value = path ? path.join(' > ') : itemToCheck.text || '';
85
+
86
+ if (isLeafNode(itemToCheck)) {
87
+ ladderizedSelectPopperState.value = false;
88
+ }
89
+ } else if (selectedItems.length === 0 && !wasCleared.value) {
90
+ inputText.value = '';
91
+ }
92
+ };
93
+
94
+ const handleSearch = () => {
95
+ isSearching.value = true;
96
+
97
+ debouncedEmitSearch();
98
+ };
99
+
100
+ const debouncedEmitSearch = useDebounceFn(() => {
101
+ // Optionally emit search event here if needed
102
+ }, 300);
103
+
104
+ const handleClear = () => {
105
+ wasCleared.value = true;
106
+
107
+ inputText.value = '';
108
+
109
+ emit('update:modelValue', []);
110
+ };
111
+
112
+ const handleOptionsToggle = () => {
113
+ ladderizedSelectPopperState.value = true;
114
+
115
+ isSearching.value = false;
116
+ };
117
+
118
+ // Watch for changes in modelValue to update inputText
119
+ watch(
120
+ () => ladderizedSelectModel.value,
121
+ (newVal) => {
122
+ if (wasCleared.value) {
123
+ inputText.value = '';
124
+ wasCleared.value = false;
125
+ return;
126
+ }
127
+
128
+ if (Array.isArray(newVal) && newVal.length > 0) {
129
+ // Treat the array as a single path for ladderized select
130
+ let currentLevel = ladderizedSelectOptions.value;
131
+
132
+ const pathTexts: string[] = [];
133
+
134
+ for (const value of newVal) {
135
+ const found = currentLevel.find((item) => item.value === value);
136
+ if (!found) break;
137
+ pathTexts.push(found.text);
138
+ currentLevel = found.sublevel || [];
139
+ }
140
+
141
+ inputText.value = pathTexts.join(' > ');
142
+ }
143
+ },
144
+ { immediate: true },
145
+ );
146
+
147
+ onClickOutside(ladderizedSelectRef, () => {
148
+ ladderizedSelectPopperState.value = false;
149
+ });
150
+
151
+ return {
152
+ ladderizedClasses,
153
+ ladderizedSelectPopperState,
154
+ ladderizedSelectRef,
155
+ ladderizedSelectOptions,
156
+ isLadderizedSelectPopperDisabled,
157
+ ladderizedSelectModel,
158
+ inputText,
159
+ handleSelectedLadderizedItem,
160
+ handleSearch,
161
+ handleClear,
162
+ handleOptionsToggle,
163
+ };
164
+ };
@@ -31,9 +31,7 @@ export const multiSelectPropTypes = {
31
31
  required: true,
32
32
  },
33
33
  modelValue: {
34
- type: [String, Number, Object, Array] as PropType<
35
- string | number | Record<string, unknown> | (string | number | Record<string, unknown>)[]
36
- >,
34
+ type: Array as PropType<(string | number | Record<string, unknown>)[]>,
37
35
  default: () => [],
38
36
  },
39
37
  options: {
@@ -102,10 +100,18 @@ export const multiSelectPropTypes = {
102
100
  type: String,
103
101
  default: '',
104
102
  },
103
+ active: {
104
+ type: Boolean,
105
+ default: false,
106
+ },
105
107
  disabled: {
106
108
  type: Boolean,
107
109
  default: false,
108
110
  },
111
+ error: {
112
+ type: Boolean,
113
+ default: false,
114
+ },
109
115
  clearable: {
110
116
  type: Boolean,
111
117
  default: false,
@@ -23,46 +23,59 @@
23
23
  width: props.width,
24
24
  }"
25
25
  >
26
- <div @click="handleOptionsToggle">
27
- <spr-input
28
- v-model="inputText"
29
- :class="{
30
- 'spr-cursor-pointer': true,
26
+ <div ref="multiSelectRef">
27
+ <div @click="handleOptionsToggle">
28
+ <spr-input
29
+ v-model="inputText"
30
+ :class="{
31
+ 'spr-cursor-pointer': true,
32
+ }"
33
+ :placeholder="props.placeholder"
34
+ autocomplete="off"
35
+ :helper-text="props.helperText"
36
+ :helper-icon="props.helperIcon"
37
+ :display-helper="props.displayHelper"
38
+ :active="props.active"
39
+ :readonly="true"
40
+ :disabled="props.disabled"
41
+ :error="props.error"
42
+ >
43
+ <template #icon>
44
+ <div class="spr-flex spr-items-center spr-gap-1">
45
+ <Icon
46
+ v-if="props.clearable && inputText"
47
+ class="spr-cursor-pointer"
48
+ icon="ph:x"
49
+ @click.stop="handleClear"
50
+ />
51
+ <Icon icon="ph:caret-down" />
52
+ </div>
53
+ </template>
54
+
55
+ <template #helperMessage>
56
+ <slot name="helperMessage" />
57
+ </template>
58
+ </spr-input>
59
+
60
+ <!-- Hidden Select for QA automation -->
61
+ <select v-if="multiSelectOptions && multiSelectOptions.length" v-model="multiSelectModel" multiple hidden>
62
+ <option v-for="option in multiSelectOptions" :key="option.value" :value="option.value">
63
+ {{ option.text }}
64
+ </option>
65
+ </select>
66
+ </div>
67
+
68
+ <!-- This div used to poppulate popper menu -->
69
+ <div
70
+ :id="props.id"
71
+ :style="{
72
+ width: props.popperWidth,
31
73
  }"
32
- :placeholder="props.placeholder"
33
- :readonly="true"
34
- :disabled="props.disabled"
35
- autocomplete="off"
36
- :helper-text="props.helperText"
37
- :helper-icon="props.helperIcon"
38
- :display-helper="props.displayHelper"
39
- >
40
- <template #icon>
41
- <div class="spr-flex spr-items-center spr-gap-1">
42
- <Icon
43
- v-if="props.clearable && inputText"
44
- class="spr-cursor-pointer"
45
- icon="ph:x"
46
- @click.stop="handleClear"
47
- />
48
- <Icon icon="ph:caret-down" />
49
- </div>
50
- </template>
51
- </spr-input>
74
+ ></div>
52
75
  </div>
53
76
 
54
- <div
55
- :id="props.id"
56
- :style="{
57
- width: props.popperWidth,
58
- }"
59
- ></div>
60
-
61
77
  <template #popper>
62
- <div
63
- ref="multiSelectRef"
64
- class="spr-grid spr-max-h-[300px] spr-gap-0.5 spr-overflow-y-auto spr-overflow-x-hidden spr-p-2"
65
- >
78
+ <div class="spr-grid spr-max-h-[300px] spr-gap-0.5 spr-overflow-y-auto spr-overflow-x-hidden spr-p-2">
66
79
  <template v-if="multiSelectOptions.length > 0">
67
80
  <spr-list
68
81
  v-model="multiSelectedListItems"
@@ -118,7 +118,7 @@ export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<M
118
118
  * Opens the multi-select options.
119
119
  */
120
120
  const handleOptionsToggle = () => {
121
- multiSelectPopperState.value = true;
121
+ multiSelectPopperState.value = !multiSelectPopperState.value;
122
122
  };
123
123
 
124
124
  /**
@@ -140,7 +140,6 @@ export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<M
140
140
 
141
141
  hasUserSelected.value = true;
142
142
  multiSelectModel.value = selectedValues;
143
- multiSelectPopperState.value = true;
144
143
  inputTextBackup.value =
145
144
  multiSelectedItems.length > 3
146
145
  ? `${multiSelectedItems.length} items selected`
@@ -224,9 +223,23 @@ export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<M
224
223
  updateMultiSelectedItemsFromValue();
225
224
  });
226
225
 
227
- watch(multiSelectOptions, () => {
228
- updateMultiSelectedItemsFromValue();
229
- });
226
+ watch(
227
+ multiSelectOptions,
228
+ () => {
229
+ updateMultiSelectedItemsFromValue();
230
+ },
231
+ { deep: true },
232
+ );
233
+
234
+ // Add watcher for options prop to re-process options when changed
235
+ watch(
236
+ options,
237
+ () => {
238
+ processOptions();
239
+ updateMultiSelectedItemsFromValue();
240
+ },
241
+ { deep: true },
242
+ );
230
243
 
231
244
  /**
232
245
  * Handles closing the multi-select when clicking outside.
@@ -102,10 +102,18 @@ export const selectPropTypes = {
102
102
  type: String,
103
103
  default: '',
104
104
  },
105
+ active: {
106
+ type: Boolean,
107
+ default: false,
108
+ },
105
109
  disabled: {
106
110
  type: Boolean,
107
111
  default: false,
108
112
  },
113
+ error: {
114
+ type: Boolean,
115
+ default: false,
116
+ },
109
117
  clearable: {
110
118
  type: Boolean,
111
119
  default: false,
@@ -23,58 +23,68 @@
23
23
  width: props.width,
24
24
  }"
25
25
  >
26
- <div @click="handleOptionsToggle">
27
- <spr-input
28
- v-model="inputText"
29
- :class="{
30
- 'spr-cursor-pointer': !props.searchable,
31
- }"
32
- :placeholder="props.placeholder"
33
- :readonly="!props.searchable"
34
- :disabled="props.disabled"
35
- autocomplete="off"
36
- :helper-text="props.helperText"
37
- :helper-icon="props.helperIcon"
38
- :display-helper="props.displayHelper"
39
- @keyup="handleSearch"
40
- >
41
- <template #icon>
42
- <div class="spr-flex spr-items-center spr-gap-1">
43
- <Icon
44
- v-if="props.clearable && inputText"
45
- class="spr-cursor-pointer"
46
- icon="ph:x"
47
- @click.stop="handleClear"
48
- />
49
- <Icon icon="ph:caret-down" />
50
- </div>
51
- </template>
52
- </spr-input>
26
+ <div ref="selectRef">
27
+ <div @click="handleOptionsToggle">
28
+ <spr-input
29
+ v-model="inputText"
30
+ :class="{
31
+ 'spr-cursor-pointer': !props.searchable,
32
+ }"
33
+ :placeholder="props.placeholder"
34
+ autocomplete="off"
35
+ :helper-text="props.helperText"
36
+ :helper-icon="props.helperIcon"
37
+ :display-helper="props.displayHelper"
38
+ :active="props.active"
39
+ :readonly="!props.searchable"
40
+ :disabled="props.disabled"
41
+ :error="props.error"
42
+ @keyup="handleSearch"
43
+ >
44
+ <template #icon>
45
+ <div class="spr-flex spr-cursor-pointer spr-items-center">
46
+ <Icon
47
+ v-if="props.clearable && inputText"
48
+ class="spr-cursor-pointer"
49
+ icon="ph:x"
50
+ @click.stop="handleClear"
51
+ />
52
+ <Icon icon="ph:caret-down" />
53
+ </div>
54
+ </template>
53
55
 
54
- <select
55
- v-if="selectOptions && selectOptions.length"
56
- :value="Array.isArray(selectModel) ? selectModel[0] : selectModel"
57
- data-testid="qa-hidden-select"
58
- tabindex="-1"
59
- aria-hidden="true"
60
- hidden
61
- >
62
- <option v-for="item in selectOptions" :key="item.value" :value="item.value">
63
- {{ item.text }}
64
- </option>
65
- </select>
66
- </div>
56
+ <template #helperMessage>
57
+ <slot name="helperMessage" />
58
+ </template>
59
+ </spr-input>
60
+
61
+ <!-- Hidden Select for QA automation -->
62
+ <select
63
+ v-if="selectOptions && selectOptions.length"
64
+ :value="Array.isArray(selectModel) ? selectModel[0] : selectModel"
65
+ data-testid="qa-hidden-select"
66
+ tabindex="-1"
67
+ aria-hidden="true"
68
+ hidden
69
+ >
70
+ <option v-for="item in selectOptions" :key="item.value" :value="item.value">
71
+ {{ item.text }}
72
+ </option>
73
+ </select>
74
+ </div>
67
75
 
68
- <div
69
- :id="props.id"
70
- :style="{
71
- width: props.popperWidth,
72
- }"
73
- ></div>
76
+ <!-- This div used to poppulate popper menu -->
77
+ <div
78
+ :id="props.id"
79
+ :style="{
80
+ width: props.popperWidth,
81
+ }"
82
+ ></div>
83
+ </div>
74
84
 
75
85
  <template #popper>
76
86
  <div
77
- ref="selectRef"
87
+ ref="selectPopperRef"
78
88
  class="spr-grid spr-max-h-[300px] spr-gap-0.5 spr-overflow-y-auto spr-overflow-x-hidden spr-p-2"
79
89
  >
80
90
  <template v-if="isSearching">
@@ -153,6 +163,7 @@ const {
153
163
  selectClasses,
154
164
  selectPopperState,
155
165
  selectRef,
166
+ selectPopperRef,
156
167
  selectModel,
157
168
  selectOptions,
158
169
  filteredSelectOptions,