design-system-next 2.9.6 → 2.9.7

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.
@@ -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
+ };
@@ -117,6 +117,10 @@ export const multiSelectPropTypes = {
117
117
  type: Boolean,
118
118
  default: false,
119
119
  },
120
+ chipped: {
121
+ type: Boolean,
122
+ default: false,
123
+ },
120
124
  };
121
125
 
122
126
  export const multiSelectEmitTypes = {
@@ -25,38 +25,81 @@
25
25
  >
26
26
  <div ref="multiSelectRef">
27
27
  <div @click="handleOptionsToggle">
28
- <spr-input
29
- :id="`input-${props.id}`"
30
- v-model="inputText"
31
- :class="{
32
- 'spr-cursor-pointer': true,
33
- }"
34
- :placeholder="props.placeholder"
35
- autocomplete="off"
36
- :helper-text="props.helperText"
37
- :helper-icon="props.helperIcon"
38
- :display-helper="props.displayHelper"
39
- :active="props.active"
40
- :readonly="true"
41
- :disabled="props.disabled"
42
- :error="props.error"
43
- >
44
- <template #icon>
45
- <div class="spr-flex spr-items-center spr-gap-1">
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" />
28
+ <template v-if="props.chipped">
29
+ <div :class="multiSelectClasses.chippedInputTextBaseClasses">
30
+ <div ref="chippedInputTextRef" :class="multiSelectClasses.chippedInputTextClasses">
31
+ <div class="spr-h-auto spr-w-full">
32
+ <template v-if="multiSelectedListItems.length > 0">
33
+ <template v-for="item in multiSelectedListItems" :key="item.value">
34
+ <spr-chips
35
+ class="spr-m-1 spr-inline-block"
36
+ :label="String(item.text)"
37
+ closable
38
+ visible
39
+ @close="handleChippedRemoveItem(String(item.value))"
40
+ />
41
+ </template>
42
+ </template>
43
+ <template v-else>
44
+ <span class="spr-placeholder spr-px-3 spr-text-gray-400">{{ props.placeholder }}</span>
45
+ </template>
46
+ </div>
47
+ <div :class="multiSelectClasses.chippedIconClasses">
48
+ <div class="spr-flex spr-items-center spr-gap-1">
49
+ <Icon
50
+ v-if="props.clearable && inputText"
51
+ class="spr-cursor-pointer"
52
+ icon="ph:x"
53
+ @click.stop="handleClear"
54
+ />
55
+ <Icon icon="ph:caret-down" />
56
+ </div>
57
+ </div>
53
58
  </div>
54
- </template>
59
+ </div>
60
+ <div v-if="props.displayHelper" :class="multiSelectClasses.chippedHelperContainerClasses">
61
+ <div v-if="props.displayHelper" :class="multiSelectClasses.chippedHelperClasses">
62
+ <slot name="helperMessage">
63
+ <Icon v-if="props.helperIcon" :icon="props.helperIcon" width="20px" height="20px" />
64
+ <span>{{ props.helperText }}</span>
65
+ </slot>
66
+ </div>
67
+ </div>
68
+ </template>
69
+ <template v-else>
70
+ <spr-input
71
+ :id="`input-${props.id}`"
72
+ v-model="inputText"
73
+ :class="{
74
+ 'spr-cursor-pointer': true,
75
+ }"
76
+ :placeholder="props.placeholder"
77
+ autocomplete="off"
78
+ :helper-text="props.helperText"
79
+ :helper-icon="props.helperIcon"
80
+ :display-helper="props.displayHelper"
81
+ :active="props.active"
82
+ :readonly="true"
83
+ :disabled="props.disabled"
84
+ :error="props.error"
85
+ >
86
+ <template #icon>
87
+ <div class="spr-flex spr-items-center spr-gap-1">
88
+ <Icon
89
+ v-if="props.clearable && inputText"
90
+ class="spr-cursor-pointer"
91
+ icon="ph:x"
92
+ @click.stop="handleClear"
93
+ />
94
+ <Icon icon="ph:caret-down" />
95
+ </div>
96
+ </template>
55
97
 
56
- <template #helperMessage>
57
- <slot name="helperMessage" />
58
- </template>
59
- </spr-input>
98
+ <template #helperMessage>
99
+ <slot name="helperMessage" />
100
+ </template>
101
+ </spr-input>
102
+ </template>
60
103
 
61
104
  <!-- Hidden Select for QA automation -->
62
105
  <select v-if="multiSelectOptions && multiSelectOptions.length" v-model="multiSelectModel" multiple hidden>
@@ -106,6 +149,7 @@ import 'floating-vue/dist/style.css';
106
149
 
107
150
  import SprInput from '../../input/input.vue';
108
151
  import SprList from '../../list/list.vue';
152
+ import SprChips from '../../chips/chips.vue';
109
153
 
110
154
  import { multiSelectPropTypes, multiSelectEmitTypes } from './select-multiple';
111
155
 
@@ -124,6 +168,7 @@ const {
124
168
  inputText,
125
169
  isMultiSelectPopperDisabled,
126
170
  handleMultiSelectedItem,
171
+ handleChippedRemoveItem,
127
172
  handleClear,
128
173
  handleOptionsToggle,
129
174
  } = useMultiSelect(props, emit);
@@ -1,5 +1,5 @@
1
1
  import { ref, toRefs, computed, ComputedRef, onMounted, watch } from 'vue';
2
- import { onClickOutside, useVModel } from '@vueuse/core';
2
+ import { onClickOutside, useVModel, useFocus } from '@vueuse/core';
3
3
 
4
4
  import classNames from 'classnames';
5
5
 
@@ -10,10 +10,15 @@ import type { MenuListType } from '../../list/list';
10
10
  interface MultiSelectClasses {
11
11
  baseClasses: string;
12
12
  labelClasses: string;
13
+ chippedInputTextBaseClasses: string;
14
+ chippedInputTextClasses: string;
15
+ chippedIconClasses: string;
16
+ chippedHelperContainerClasses: string;
17
+ chippedHelperClasses: string;
13
18
  }
14
19
 
15
20
  export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<MultiSelectEmitTypes>['emit']) => {
16
- const { displayText, options, disabled, textField, valueField } = toRefs(props);
21
+ const { displayText, options, textField, valueField, active, disabled, error } = toRefs(props);
17
22
 
18
23
  const multiSelectClasses: ComputedRef<MultiSelectClasses> = computed(() => {
19
24
  const baseClasses = classNames('spr-flex spr-flex-col spr-gap-size-spacing-4xs');
@@ -22,9 +27,66 @@ export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<M
22
27
  'spr-text-color-on-fill-disabled': disabled.value,
23
28
  });
24
29
 
30
+ const chippedInputTextBaseClasses = classNames(
31
+ 'spr-relative spr-flex spr-items-center spr-min-h-9 spr-rounded-border-radius-md spr-border-[1.5px] spr-border-solid',
32
+ {
33
+ 'spr-cursor-pointer': !disabled.value,
34
+
35
+ // Border State
36
+ 'spr-border-color-weak': !focused.value && !error.value && !disabled.value && !active.value,
37
+ 'spr-border-color-brand-base': !focused.value && active.value,
38
+ 'spr-border-color-danger-base': !focused.value && error.value,
39
+
40
+ // Border State Focused
41
+ 'focus: spr-border-kangkong-700': focused.value && !error.value && !disabled.value && !active.value,
42
+ 'focus: spr-border-tomato-600': focused.value && error.value,
43
+ 'focus: spr-border-white-100': focused.value && disabled.value,
44
+
45
+ // Disabled State
46
+ 'spr-background-color-disabled spr-cursor-not-allowed spr-border-mushroom-100': disabled.value,
47
+ },
48
+ );
49
+
50
+ const chippedInputTextClasses = classNames(
51
+ 'spr-flex spr-gap-1 spr-justify-between spr-w-full spr-outline-none spr-ring-0 spr-border-none spr-rounded-border-radius-md spr-font-size-200',
52
+ 'spr-font-size-200 [font-weight:inherit]',
53
+ 'placeholder:spr-text-mushroom-300',
54
+ {
55
+ 'spr-text-color-strong': !disabled.value,
56
+
57
+ // Disabled State
58
+ 'spr-text-color-on-fill-disabled !spr-cursor-not-allowed': disabled.value,
59
+ },
60
+ );
61
+
62
+ const chippedIconClasses = classNames(
63
+ 'spr-flex spr-items-center spr-justify-center spr-h-inherit spr-px-2 [&>svg]:spr-min-h-4 [&>svg]:spr-min-w-4',
64
+ {
65
+ 'spr-text-mushroom-300': !error.value,
66
+ 'spr-text-tomato-600': error.value,
67
+ },
68
+ );
69
+
70
+ const chippedHelperContainerClasses = classNames(
71
+ 'spr-flex spr-flex-row spr-items-start spr-justify-between spr-w-full',
72
+ );
73
+
74
+ const chippedHelperClasses = classNames(
75
+ 'spr-body-sm-regular spr-flex spr-items-center spr-gap-size-spacing-5xs spr-flex-1',
76
+ {
77
+ 'spr-text-color-danger-base': error.value,
78
+ 'spr-text-color-supporting': !error.value,
79
+ },
80
+ );
81
+
25
82
  return {
26
83
  baseClasses,
27
84
  labelClasses,
85
+ chippedInputTextBaseClasses,
86
+ chippedInputTextClasses,
87
+ chippedIconClasses,
88
+ chippedHelperContainerClasses,
89
+ chippedHelperClasses,
28
90
  };
29
91
  });
30
92
 
@@ -39,7 +101,9 @@ export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<M
39
101
 
40
102
  const inputText = ref<string | number>('');
41
103
  const inputTextBackup = ref<string | number>('');
104
+ const chippedInputTextRef = ref(null);
42
105
 
106
+ const { focused } = useFocus(chippedInputTextRef);
43
107
  /**
44
108
  * Returns the normalized value of the model as an array for internal use.
45
109
  */
@@ -148,6 +212,42 @@ export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<M
148
212
  updateMultiSelectedItemsFromValue();
149
213
  };
150
214
 
215
+ /**
216
+ * Removes an item from the multi-select model by its value.
217
+ * Handles stringified objects and emits the updated model value.
218
+ */
219
+ const handleChippedRemoveItem = (itemValue: string) => {
220
+ let currentValues = Array.isArray(multiSelectModel.value) ? [...multiSelectModel.value] : [multiSelectModel.value];
221
+
222
+ currentValues = currentValues.filter((val) => {
223
+ let valToCompare = val;
224
+
225
+ if (typeof valToCompare === 'string' && valToCompare.startsWith('{') && valToCompare.endsWith('}')) {
226
+ try {
227
+ valToCompare = JSON.parse(valToCompare);
228
+ } catch {
229
+ // ignore
230
+ }
231
+ }
232
+
233
+ if (typeof itemValue === 'string' && itemValue.startsWith('{') && itemValue.endsWith('}')) {
234
+ try {
235
+ itemValue = JSON.parse(itemValue);
236
+ } catch {
237
+ // ignore
238
+ }
239
+ }
240
+
241
+ if (typeof valToCompare === 'object' && typeof itemValue === 'object') {
242
+ return JSON.stringify(valToCompare) !== JSON.stringify(itemValue);
243
+ }
244
+
245
+ return valToCompare != itemValue;
246
+ });
247
+
248
+ emit('update:modelValue', currentValues);
249
+ };
250
+
151
251
  /**
152
252
  * Updates the selected items in the multi-select based on the current model value.
153
253
  * Handles stringified objects and updates the input text accordingly.
@@ -271,6 +371,7 @@ export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<M
271
371
  inputText,
272
372
  isMultiSelectPopperDisabled,
273
373
  handleMultiSelectedItem,
374
+ handleChippedRemoveItem,
274
375
  handleClear,
275
376
  handleOptionsToggle,
276
377
  };