design-system-next 2.9.6 → 2.9.9

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.
@@ -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
  };
@@ -5,6 +5,7 @@ type QuickAction = {
5
5
  items: QuickActionItem[];
6
6
  };
7
7
 
8
+
8
9
  type QuickActionItem = {
9
10
  title: string;
10
11
  description: string;
@@ -14,23 +15,29 @@ type QuickActionItem = {
14
15
  hidden: boolean;
15
16
  };
16
17
 
17
- type NavLinks = {
18
+ export type NavLinks = {
18
19
  top: { parentLinks: ParentLinkItem[] }[];
19
20
  bottom: { parentLinks: ParentLinkItem[] }[];
20
21
  };
21
22
 
22
- type ParentLinkItem = {
23
+ export type NavAPILinks = {
24
+ top: { parentLinks: NavItem[] }[];
25
+ bottom: { parentLinks: NavItem[] }[];
26
+ };
27
+
28
+ export type ParentLinkItem = {
23
29
  title: string;
24
30
  icon: string;
25
- link: string;
26
- redirect: Redirect;
31
+ link?: string;
32
+ redirect?: Redirect;
27
33
  menuLinks: MenuLink[];
28
- hidden: boolean;
34
+ submenuLinks?: SubmenuLink[];
35
+ hidden?: boolean;
29
36
  };
30
37
 
31
38
  type MenuLink = {
32
39
  menuHeading: string;
33
- items: MenuLinkItem[];
40
+ items: MenuLinkItem[] | ParentLinkItem[];
34
41
  };
35
42
 
36
43
  type MenuLinkItem = {
@@ -42,7 +49,7 @@ type MenuLinkItem = {
42
49
 
43
50
  type SubmenuLink = {
44
51
  subMenuHeading: string;
45
- items: SubmenuLinkItem[];
52
+ items: SubmenuLinkItem[] | ParentLinkItem[];
46
53
  };
47
54
 
48
55
  type SubmenuLinkItem = {
@@ -70,6 +77,34 @@ type UserMenuItem = {
70
77
  hidden: boolean;
71
78
  redirect: Redirect;
72
79
  };
80
+ export interface NavItem {
81
+ groupId: string;
82
+ label: string;
83
+ icon?: string | null;
84
+ url?: string | null;
85
+ isNewTab?: boolean;
86
+ children?: NavItem[] | null;
87
+ groupName?: string | null;
88
+ hidden?: boolean;
89
+ }
90
+
91
+ export interface MappedNavItem {
92
+ title: string;
93
+ icon?: string;
94
+ redirect?: {
95
+ openInNewTab: boolean;
96
+ isAbsoluteURL: boolean;
97
+ link: string;
98
+ };
99
+ menuLinks?: {
100
+ menuHeading: string;
101
+ items: MappedNavItem[];
102
+ }[];
103
+ submenuLinks?: {
104
+ subMenuHeading: string;
105
+ items: MappedNavItem[];
106
+ }[];
107
+ }
73
108
 
74
109
  export const sidenavPropTypes = {
75
110
  quickActions: {
@@ -120,6 +155,11 @@ export const sidenavPropTypes = {
120
155
  validator: (value: unknown) => typeof value === 'boolean',
121
156
  default: false,
122
157
  },
158
+ isNavApi: {
159
+ type: Boolean,
160
+ validator: (value: unknown) => typeof value === 'boolean',
161
+ default: false,
162
+ }
123
163
  };
124
164
 
125
165
  export const sidenavEmitTypes = {
@@ -139,7 +139,7 @@
139
139
  </div>
140
140
 
141
141
  <!-- Grouped Nav Links -->
142
- <template v-for="(navLink, navLinkIndex) in props.navLinks.top" :key="navLinkIndex">
142
+ <template v-for="(navLink, navLinkIndex) in navLinks.top" :key="navLinkIndex">
143
143
  <template v-for="(parentLink, parentLinkIndex) in navLink.parentLinks" :key="parentLinkIndex">
144
144
  <!-- Parent link with menu links -->
145
145
  <template v-if="parentLink.menuLinks && parentLink.menuLinks.length > 0">
@@ -352,7 +352,7 @@
352
352
  </template>
353
353
  </template>
354
354
  <div
355
- v-if="props.navLinks.top.length > 0 && navLinkIndex < props.navLinks.top.length - 1"
355
+ v-if="navLinks.top.length > 0 && navLinkIndex < navLinks.top.length - 1"
356
356
  class="spr-background-color-hover spr-h-[2px] spr-w-full"
357
357
  ></div>
358
358
  </template>
@@ -360,11 +360,11 @@
360
360
 
361
361
  <!-- Bottom Section -->
362
362
  <div
363
- v-if="props.navLinks.bottom && props.navLinks.bottom.length > 0"
363
+ v-if="navLinks.bottom && navLinks.bottom.length > 0"
364
364
  class="spr-grid spr-justify-center spr-gap-2 spr-px-3 spr-pb-4 spr-pt-0"
365
365
  >
366
366
  <!-- Grouped Nav Links -->
367
- <template v-for="(navLink, navLinkIndex) in props.navLinks.bottom" :key="navLinkIndex">
367
+ <template v-for="(navLink, navLinkIndex) in navLinks.bottom" :key="navLinkIndex">
368
368
  <template v-for="(parentLink, parentLinkIndex) in navLink.parentLinks" :key="parentLinkIndex">
369
369
  <!-- Parent link with menu links -->
370
370
  <template v-if="parentLink.menuLinks && parentLink.menuLinks.length > 0">
@@ -573,7 +573,7 @@
573
573
  </template>
574
574
  </template>
575
575
  <div
576
- v-if="props.navLinks.bottom.length > 0 && navLinkIndex < props.navLinks.bottom.length - 1"
576
+ v-if="navLinks.bottom.length > 0 && navLinkIndex < navLinks.bottom.length - 1"
577
577
  class="spr-background-color-hover spr-h-[2px] spr-w-full"
578
578
  ></div>
579
579
  </template>
@@ -767,7 +767,7 @@ import SprBadge from '../badge/badge.vue';
767
767
  const props = defineProps(sidenavPropTypes);
768
768
  const emit = defineEmits(sidenavEmitTypes);
769
769
 
770
- const { isQuckActionMenuVisible, isUserMenuVisible, userProfileError, getUserInitials, handleRedirect, generateId } = useSidenav(
770
+ const { navLinks, isQuckActionMenuVisible, isUserMenuVisible, userProfileError, getUserInitials, handleRedirect, generateId } = useSidenav(
771
771
  props,
772
772
  emit,
773
773
  );
@@ -1,7 +1,7 @@
1
- import { ref } from 'vue';
1
+ import { ref, onMounted } from 'vue';
2
2
  import type { SetupContext } from 'vue';
3
3
 
4
- import type { SidenavPropTypes, SidenavEmitTypes } from './sidenav';
4
+ import type { SidenavPropTypes, SidenavEmitTypes, ParentLinkItem, NavLinks, NavItem } from './sidenav';
5
5
 
6
6
  interface ObjectItem {
7
7
  redirect: {
@@ -17,6 +17,7 @@ interface ObjectItem {
17
17
  }
18
18
 
19
19
  export const useSidenav = (props: SidenavPropTypes, emit: SetupContext<SidenavEmitTypes>['emit']) => {
20
+ const navLinks = ref<NavLinks>(props.navLinks);
20
21
  const isQuckActionMenuVisible = ref(false);
21
22
 
22
23
  const isUserMenuVisible = ref(false);
@@ -67,13 +68,106 @@ export const useSidenav = (props: SidenavPropTypes, emit: SetupContext<SidenavEm
67
68
  const generateId = (...titles: string[]): string => {
68
69
  return titles.map(transformToCamelCaseId).join('_');
69
70
  }
71
+ const confirmIfOwnDomain = (url: string) => {
72
+ const domain = window.location.href;
73
+ const urlHostname = new URL(url).hostname;
74
+ const isOwnDomain = domain === urlHostname || window.location.hostname === 'localhost'
75
+ return isOwnDomain;
76
+ }
77
+
78
+ const getPathFromUrl = (url: string): string => {
79
+ const parsedUrl = new URL(url);
80
+ return parsedUrl ? parsedUrl.pathname : '';
81
+
82
+ }
83
+
84
+ const navLinkCondition = (link: NavItem) => {
85
+ if (confirmIfOwnDomain(link.url as string)) {
86
+ return getPathFromUrl(link.url as string);
87
+ } else {
88
+ return link.url;
89
+ }
90
+ }
91
+
92
+ const groupByGroupId = (items: NavItem[]) => {
93
+ const groups: Record<string, NavItem[]> = {};
94
+ items.forEach(item => {
95
+ if (!groups[item.groupId]) {
96
+ groups[item.groupId] = [];
97
+ }
98
+ groups[item.groupId].push(item);
99
+ });
100
+ return Object.values(groups).map(group => ({ parentLinks: group.map(mapItemToNav) }));
101
+ }
102
+
103
+ const mapItemToNav = (item: NavItem): ParentLinkItem => {
104
+ return {
105
+ title: item.label,
106
+ icon: item.icon || "",
107
+ redirect: item.url
108
+ ? {
109
+ openInNewTab: item.isNewTab || false,
110
+ isAbsoluteURL: !confirmIfOwnDomain(item.url),
111
+ link: navLinkCondition(item) || "",
112
+ }
113
+ : undefined,
114
+ menuLinks: item.children && item.children.length > 0
115
+ ? [{
116
+ menuHeading: '',
117
+ items: item.children.map(child => mapItemToNav(child))
118
+ }]
119
+ : [],
120
+ submenuLinks: item.children && item.children.length > 0 && !item.children.some(c => c.children)
121
+ ? [{
122
+ subMenuHeading: '',
123
+ items: item.children.map(child => mapItemToNav(child)),
124
+ }]
125
+ : [],
126
+ }
127
+ }
128
+
129
+ // Helper function to extract valid NavItems from an array of objects
130
+ const extractValidNavItems = <T extends Record<string, unknown>>(items: T[]): NavItem[] => {
131
+ return items.filter((item): item is NavItem & T => (
132
+ item !== null &&
133
+ 'groupId' in item &&
134
+ 'label' in item &&
135
+ typeof item.groupId === 'string' &&
136
+ typeof item.label === 'string'
137
+ )) as NavItem[];
138
+ };
139
+
140
+ const transformedNavItems = async (apiData: NavLinks) => {
141
+ // Output type matches navLinks ref type
142
+ const transformedData: NavLinks = { top: [], bottom: [] };
143
+
144
+ if (apiData.top && Array.isArray(apiData.top)) {
145
+ const validTopItems = extractValidNavItems(apiData.top);
146
+ transformedData.top = groupByGroupId(validTopItems);
147
+ }
148
+
149
+ if (apiData.bottom && Array.isArray(apiData.bottom)) {
150
+ const validBottomItems = extractValidNavItems(apiData.bottom);
151
+ transformedData.bottom = groupByGroupId(validBottomItems);
152
+ }
153
+
154
+ return transformedData;
155
+ }
156
+
157
+ onMounted(async () => {
158
+ if (props.isNavApi) {
159
+ navLinks.value = await transformedNavItems(props.navLinks);
160
+ }
161
+ })
70
162
 
71
163
  return {
164
+ navLinks,
72
165
  isQuckActionMenuVisible,
73
166
  isUserMenuVisible,
74
167
  userProfileError,
75
168
  getUserInitials,
76
169
  handleRedirect,
77
- generateId
170
+ generateId,
171
+ transformedNavItems,
78
172
  };
79
173
  };
@@ -0,0 +1,6 @@
1
+ interface ImportMeta { readonly glob: ImportMetaGlob }
2
+
3
+ interface ImportMetaGlob {
4
+ (pattern: string, options?: { eager?: boolean }): Record<string, () => Promise<unknown>>
5
+ (pattern: string, options?: { eager: true }): Record<string, unknown>
6
+ }