@eturnity/eturnity_reusable_components 9.28.3-ZOO-3.0 → 9.28.3-panel-placement.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eturnity/eturnity_reusable_components",
3
- "version": "9.28.3-ZOO-3.0",
3
+ "version": "9.28.3-panel-placement.0",
4
4
  "files": [
5
5
  "dist",
6
6
  "src"
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Uploaded to svgrepo.com" width="800px" height="800px" viewBox="0 0 32 32" xml:space="preserve">
2
+ <style type="text/css">
3
+ .linesandangles_een{fill:#000000;}
4
+ </style>
5
+ <path class="linesandangles_een" d="M26.281,30l-2-8H25v-5h-8V2h-2v15H7v5h0.719l-2,8H26.281z M23.719,28H21v-4h-2v4h-2v-4h-2v4h-2 v-4h-2v4H8.281l1.5-6h12.438L23.719,28z M9,19h14v1H9V19z"/>
6
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="10" height="14" viewBox="0 0 10 14" fill="none">
2
+ <g transform="translate(10,0) rotate(90)">
3
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M0.199219 9.85718V0.142822H1.75351L1.75352 9.85718H0.199219ZM5.38021 8.20572L2.14209 4.99999L5.38021 1.79425V4.19855L8.61833 4.19855V1.79425L11.8564 4.99999L8.61832 8.20572L8.61833 5.80142L5.38021 5.80142V8.20572ZM12.2449 0.142822V9.85718H13.7992V0.142822H12.2449Z" fill="white"/>
4
+ </g>
5
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M2 4H12V8L2 10V4Z" fill="white" />
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M3 10L6 4H13L10 10H3Z" fill="white" />
3
+ </svg>
@@ -1,11 +1,11 @@
1
1
  <svg width="15" height="16" viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <rect x="2.80078" y="4.06415" width="8" height="1.6" transform="rotate(90 2.80078 4.06415)" fill="white"/>
3
- <rect x="7.00195" y="3.8642" width="4" height="1.6" transform="rotate(-180 7.00195 3.8642)" fill="white"/>
4
- <path d="M8.5 3.07846L6.00098 0.935599L6.00098 5.22131L8.5 3.07846Z" fill="white"/>
5
- <rect x="11.002" y="13.8643" width="8" height="1.6" transform="rotate(-180 11.002 13.8643)" fill="white"/>
6
- <path fill-rule="evenodd" clip-rule="evenodd" d="M2 4.06409C2.55228 4.06409 3 3.61637 3 3.06409C3 2.5118 2.55228 2.06409 2 2.06409C1.44772 2.06409 1 2.5118 1 3.06409C1 3.61637 1.44772 4.06409 2 4.06409ZM2 5.06409C3.10457 5.06409 4 4.16866 4 3.06409C4 1.95952 3.10457 1.06409 2 1.06409C0.895431 1.06409 0 1.95952 0 3.06409C0 4.16866 0.895431 5.06409 2 5.06409Z" fill="white"/>
7
- <rect x="12.7988" y="4.06433" width="8" height="1.6" transform="rotate(90 12.7988 4.06433)" fill="white"/>
8
- <path fill-rule="evenodd" clip-rule="evenodd" d="M12.002 4.06421C12.5542 4.06421 13.002 3.61649 13.002 3.06421C13.002 2.51192 12.5542 2.06421 12.002 2.06421C11.4497 2.06421 11.002 2.51192 11.002 3.06421C11.002 3.61649 11.4497 4.06421 12.002 4.06421ZM12.002 5.06421C13.1065 5.06421 14.002 4.16878 14.002 3.06421C14.002 1.95964 13.1065 1.06421 12.002 1.06421C10.8974 1.06421 10.002 1.95964 10.002 3.06421C10.002 4.16878 10.8974 5.06421 12.002 5.06421Z" fill="white"/>
9
- <path fill-rule="evenodd" clip-rule="evenodd" d="M12.002 14.0643C12.5542 14.0643 13.002 13.6166 13.002 13.0643C13.002 12.512 12.5542 12.0643 12.002 12.0643C11.4497 12.0643 11.002 12.512 11.002 13.0643C11.002 13.6166 11.4497 14.0643 12.002 14.0643ZM12.002 15.0643C13.1065 15.0643 14.002 14.1688 14.002 13.0643C14.002 11.9597 13.1065 11.0643 12.002 11.0643C10.8974 11.0643 10.002 11.9597 10.002 13.0643C10.002 14.1688 10.8974 15.0643 12.002 15.0643Z" fill="white"/>
10
- <path fill-rule="evenodd" clip-rule="evenodd" d="M2 14.0644C2.55228 14.0644 3 13.6167 3 13.0644C3 12.5121 2.55228 12.0644 2 12.0644C1.44772 12.0644 1 12.5121 1 13.0644C1 13.6167 1.44772 14.0644 2 14.0644ZM2 15.0644C3.10457 15.0644 4 14.169 4 13.0644C4 11.9598 3.10457 11.0644 2 11.0644C0.895431 11.0644 0 11.9598 0 13.0644C0 14.169 0.895431 15.0644 2 15.0644Z" fill="white"/>
11
- </svg>
2
+ <path transform="rotate(90 2.80078 4.06415)" fill="white" d="M2.8008 4.0642h8v1.6H2.8008Z" />
3
+ <path transform="rotate(-180 7.00195 3.8642)" fill="white" d="M7.002 3.8642h4v1.6H7.002Z" />
4
+ <path d="M8.5 3.07846L6.00098 0.935599L6.00098 5.22131L8.5 3.07846Z" fill="white" />
5
+ <path transform="rotate(-180 11.002 13.8643)" fill="white" d="M11.002 13.8643h8v1.6H11.002Z" />
6
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M2 4.06409C2.55228 4.06409 3 3.61637 3 3.06409C3 2.5118 2.55228 2.06409 2 2.06409C1.44772 2.06409 1 2.5118 1 3.06409C1 3.61637 1.44772 4.06409 2 4.06409ZM2 5.06409C3.10457 5.06409 4 4.16866 4 3.06409C4 1.95952 3.10457 1.06409 2 1.06409C0.895431 1.06409 0 1.95952 0 3.06409C0 4.16866 0.895431 5.06409 2 5.06409Z" fill="white" />
7
+ <path transform="rotate(90 12.7988 4.06433)" fill="white" d="M12.7988 4.0643h8v1.6H12.7988Z" />
8
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M12.002 4.06421C12.5542 4.06421 13.002 3.61649 13.002 3.06421C13.002 2.51192 12.5542 2.06421 12.002 2.06421C11.4497 2.06421 11.002 2.51192 11.002 3.06421C11.002 3.61649 11.4497 4.06421 12.002 4.06421ZM12.002 5.06421C13.1065 5.06421 14.002 4.16878 14.002 3.06421C14.002 1.95964 13.1065 1.06421 12.002 1.06421C10.8974 1.06421 10.002 1.95964 10.002 3.06421C10.002 4.16878 10.8974 5.06421 12.002 5.06421Z" fill="white" />
9
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M12.002 14.0643C12.5542 14.0643 13.002 13.6166 13.002 13.0643C13.002 12.512 12.5542 12.0643 12.002 12.0643C11.4497 12.0643 11.002 12.512 11.002 13.0643C11.002 13.6166 11.4497 14.0643 12.002 14.0643ZM12.002 15.0643C13.1065 15.0643 14.002 14.1688 14.002 13.0643C14.002 11.9597 13.1065 11.0643 12.002 11.0643C10.8974 11.0643 10.002 11.9597 10.002 13.0643C10.002 14.1688 10.8974 15.0643 12.002 15.0643Z" fill="white" />
10
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M2 14.0644C2.55228 14.0644 3 13.6167 3 13.0644C3 12.5121 2.55228 12.0644 2 12.0644C1.44772 12.0644 1 12.5121 1 13.0644C1 13.6167 1.44772 14.0644 2 14.0644ZM2 15.0644C3.10457 15.0644 4 14.169 4 13.0644C4 11.9598 3.10457 11.0644 2 11.0644C0.895431 11.0644 0 11.9598 0 13.0644C0 14.169 0.895431 15.0644 2 15.0644Z" fill="white" />
11
+ </svg>
@@ -0,0 +1 @@
1
+ module.exports = 'test-file-stub'
@@ -157,6 +157,46 @@ describe('ButtonIcon.vue', () => {
157
157
  }
158
158
  })
159
159
 
160
+ it('replaces the icon with a spinner while loading', async () => {
161
+ expect(wrapper.find('[data-test-id="spinner_container"]').exists()).toBe(
162
+ false
163
+ )
164
+ expect(wrapper.findComponent({ name: 'Icon' }).exists()).toBe(true)
165
+
166
+ await wrapper.setProps({ isLoading: true })
167
+ expect(wrapper.find('[data-test-id="spinner_container"]').exists()).toBe(
168
+ true
169
+ )
170
+ expect(wrapper.findComponent({ name: 'Icon' }).exists()).toBe(false)
171
+
172
+ await wrapper.setProps({ isLoading: false })
173
+ expect(wrapper.find('[data-test-id="spinner_container"]').exists()).toBe(
174
+ false
175
+ )
176
+ expect(wrapper.findComponent({ name: 'Icon' }).props('name')).toBe(
177
+ 'settings'
178
+ )
179
+ })
180
+
181
+ it('keeps rendering the text while loading', async () => {
182
+ await wrapper.setProps({ isLoading: true })
183
+ expect(wrapper.find('[data-test-id="button-icon-text"]').text()).toBe(
184
+ 'Click Me'
185
+ )
186
+ })
187
+
188
+ it('renders a white spinner on types with white button text', async () => {
189
+ await wrapper.setProps({ isLoading: true, type: 'primary' })
190
+ expect(wrapper.findComponent({ name: 'NewSpinner' }).props('isWhite')).toBe(
191
+ true
192
+ )
193
+
194
+ await wrapper.setProps({ type: 'secondary' })
195
+ expect(wrapper.findComponent({ name: 'NewSpinner' }).props('isWhite')).toBe(
196
+ false
197
+ )
198
+ })
199
+
160
200
  it('renders caret based on showCaret prop', async () => {
161
201
  await wrapper.setProps({ showCaret: true })
162
202
  let caret = wrapper.find('[data-test-id="button-icon-caret"]')
@@ -28,7 +28,9 @@
28
28
  :type="type"
29
29
  :variant="variant"
30
30
  >
31
+ <RCSpinner v-if="isLoading" :is-white="isSpinnerWhite" size="tiny" />
31
32
  <Icon
33
+ v-else
32
34
  :color="getIconColor"
33
35
  :disable-hover="disableIconHover"
34
36
  :fill-type="fillType"
@@ -83,9 +85,11 @@
83
85
  // iconName="external_icon"
84
86
  // fontColor="white"
85
87
  // fontSize="12px"
88
+ // :isLoading="false" // swaps the icon for a spinner
86
89
  // />
87
90
 
88
91
  import Icon from '../../icon'
92
+ import RCSpinner from '../../spinner'
89
93
  import styled from 'vue3-styled-components'
90
94
  import theme from '../../../assets/theme'
91
95
 
@@ -419,6 +423,7 @@
419
423
  ButtonWrapper,
420
424
  IconContainer,
421
425
  Icon,
426
+ RCSpinner,
422
427
  NumberContainer,
423
428
  NumberText,
424
429
  CountBadge,
@@ -541,11 +546,24 @@
541
546
  default: false,
542
547
  type: Boolean,
543
548
  },
549
+ isLoading: {
550
+ required: false,
551
+ default: false,
552
+ type: Boolean,
553
+ },
544
554
  },
545
555
  computed: {
546
556
  theme() {
547
557
  return theme
548
558
  },
559
+ // The spinner only offers white/purple, so pick whichever contrasts with
560
+ // the button background the same way the label already does.
561
+ isSpinnerWhite() {
562
+ return (
563
+ this.theme.mainButton[this.appTheme][this.type][this.variant].default
564
+ .textColor === this.theme.colors.white
565
+ )
566
+ },
549
567
  getIconColor() {
550
568
  if (this.iconColor) return this.iconColor
551
569
  if (this.isDisabled) {
@@ -484,6 +484,7 @@
484
484
  display: flex;
485
485
  gap: 10px;
486
486
  align-items: center;
487
+ padding: ${(props) => (props.isInteractive ? '0 4px' : '0')};
487
488
  cursor: ${(props) => (props.isInteractive ? 'ew-resize' : 'auto')};
488
489
  border: ${(props) =>
489
490
  props.alignItems == 'vertical'
@@ -729,6 +730,11 @@
729
730
  required: false,
730
731
  default: true,
731
732
  },
733
+ undefinedState: {
734
+ type: Boolean,
735
+ required: false,
736
+ default: false,
737
+ },
732
738
  allowNegative: {
733
739
  type: Boolean,
734
740
  required: false,
@@ -844,6 +850,7 @@
844
850
  },
845
851
  computed: {
846
852
  displayedPlaceholder() {
853
+ if (this.undefinedState) return '-'
847
854
  if (this.placeholder) return this.placeholder
848
855
  if (this.defaultNumber)
849
856
  return `${this.defaultNumber}${this.hasSpaceBetweenUnit ? ' ' : ''}${
@@ -1121,6 +1128,9 @@
1121
1128
  }
1122
1129
  },
1123
1130
  formatWithCurrency(value) {
1131
+ if (this.undefinedState && !this.isFocused) {
1132
+ return '-'
1133
+ }
1124
1134
  let adjustedMinValue =
1125
1135
  value || value === 0
1126
1136
  ? value
@@ -31,15 +31,16 @@
31
31
  :border-color="borderColor"
32
32
  :color-mode="colorMode"
33
33
  :data-id="item.hasOwnProperty('dataId') ? item.dataId : ''"
34
+ :disabled="!!item.disabled"
34
35
  :height="height"
35
36
  :inactive-border-color="inactiveBorderColor"
36
37
  :inactive-color="inactiveColor"
37
- :is-active="value == item.value"
38
+ :is-active="!undefinedState && value == item.value"
38
39
  :primary-color="primaryColor"
39
40
  :secondary-color="secondaryColor"
40
41
  :size="size"
41
42
  :width="item.width || 'auto'"
42
- @click="selectItem(item.value)"
43
+ @click="selectItem(item)"
43
44
  >
44
45
  <OptionIconContainer v-if="item.icon">
45
46
  <RCIcon
@@ -145,6 +146,7 @@
145
146
  height: String,
146
147
  borderColor: String,
147
148
  inactiveBorderColor: String,
149
+ disabled: Boolean,
148
150
  }
149
151
  const SwitchOption = styled('div', optionAttrs)`
150
152
  color: ${(props) =>
@@ -167,17 +169,19 @@
167
169
  props.height
168
170
  ? props.height
169
171
  : props.size === 'medium'
170
- ? '24px'
171
- : props.size === 'small'
172
- ? '16px'
173
- : '24px'};
172
+ ? '24px'
173
+ : props.size === 'small'
174
+ ? '16px'
175
+ : '24px'};
174
176
  padding: 0 10px;
175
177
  margin-right: -1px;
176
178
  transition: all 0.1s ease-in-out;
177
179
  overflow: hidden;
178
180
  gap: 5px;
181
+ opacity: ${(props) => (props.disabled ? 0.4 : 1)};
182
+ pointer-events: ${(props) => (props.disabled ? 'none' : 'auto')};
179
183
  & :hover {
180
- cursor: pointer;
184
+ cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')};
181
185
  }
182
186
  &:first-child {
183
187
  border-radius: 4px 0 0 4px;
@@ -264,6 +268,11 @@
264
268
  default: false,
265
269
  type: Boolean,
266
270
  },
271
+ undefinedState: {
272
+ required: false,
273
+ default: false,
274
+ type: Boolean,
275
+ },
267
276
  },
268
277
  data() {
269
278
  return {
@@ -282,19 +291,19 @@
282
291
  this.borderColor = this.secondaryColor
283
292
  this.inactiveBorderColor = theme.semanticColors.teal[600]
284
293
  } else {
285
- this.primaryColor = theme.semanticColors.teal[50]
286
- this.secondaryColor = theme.semanticColors.teal[800]
287
- this.inactiveColor = theme.semanticColors.teal[50]
294
+ this.primaryColor = theme.semanticColors.teal[100]
295
+ this.secondaryColor = theme.semanticColors.purple[500]
296
+ this.inactiveColor = theme.semanticColors.purple[500]
288
297
  this.borderColor = this.secondaryColor
289
- this.inactiveBorderColor = theme.semanticColors.teal[50]
298
+ this.inactiveBorderColor = theme.semanticColors.teal[200]
290
299
  }
291
300
  },
292
301
  methods: {
293
- selectItem(value) {
294
- if (this.disabled) {
302
+ selectItem(item) {
303
+ if (this.disabled || item.disabled) {
295
304
  return
296
305
  }
297
- this.$emit('on-switch-change', value)
306
+ this.$emit('on-switch-change', item.value)
298
307
  },
299
308
  },
300
309
  }
@@ -65,9 +65,9 @@ describe('SwitchField', () => {
65
65
  })
66
66
 
67
67
  it('renders the info text only when a message is provided', () => {
68
- expect(mountSwitch({ label: 'Mode' }).findComponent(InfoText).exists()).toBe(
69
- false
70
- )
68
+ expect(
69
+ mountSwitch({ label: 'Mode' }).findComponent(InfoText).exists()
70
+ ).toBe(false)
71
71
  expect(
72
72
  mountSwitch({ label: 'Mode', infoTextMessage: 'Helpful hint' })
73
73
  .findComponent(InfoText)
@@ -32,6 +32,7 @@
32
32
  role="checkbox"
33
33
  :size="size"
34
34
  tabindex="0"
35
+ :undefined-state="undefinedState"
35
36
  @keydown.space.prevent="onToggleChange"
36
37
  >
37
38
  <ToggleBackground
@@ -41,6 +42,7 @@
41
42
  data-test-id="toggle_background"
42
43
  :disabled="disabled"
43
44
  :is-checked="isChecked"
45
+ :undefined-state="undefinedState"
44
46
  />
45
47
  <ToggleDot
46
48
  data-test-id="toggle_dot"
@@ -49,6 +51,7 @@
49
51
  :size="size"
50
52
  :toggle-color="toggleColor"
51
53
  :toggle-margin="toggleMargin"
54
+ :undefined-state="undefinedState"
52
55
  />
53
56
  </ToggleWrapper>
54
57
  <LabelContainer
@@ -125,6 +128,7 @@
125
128
  disabled: Boolean,
126
129
  backgroundColor: String,
127
130
  isChecked: Boolean,
131
+ undefinedState: Boolean,
128
132
  }
129
133
  const LabelText = styled('div', toggleAttrs)`
130
134
  color: ${(props) =>
@@ -140,6 +144,8 @@
140
144
  border: ${(props) =>
141
145
  props.disabled
142
146
  ? '1px solid ' + props.theme.colors.disabled
147
+ : props.undefinedState
148
+ ? '1px solid ' + props.theme.colors.grey3
143
149
  : props.isChecked
144
150
  ? props.backgroundColor
145
151
  ? '1px solid ' + props.backgroundColor
@@ -175,6 +181,7 @@
175
181
  backgroundColor: String,
176
182
  isChecked: Boolean,
177
183
  disabled: Boolean,
184
+ undefinedState: Boolean,
178
185
  }
179
186
  const ToggleBackground = styled('span', backgroundAttrs)`
180
187
  display: block;
@@ -184,6 +191,8 @@
184
191
  background-color: ${(props) =>
185
192
  props.disabled
186
193
  ? props.theme.colors.grey5
194
+ : props.undefinedState
195
+ ? props.theme.colors.grey3
187
196
  : props.isChecked
188
197
  ? props.backgroundColor
189
198
  ? props.backgroundColor
@@ -198,6 +207,7 @@
198
207
  size: String,
199
208
  disabled: Boolean,
200
209
  toggleMargin: String,
210
+ undefinedState: Boolean,
201
211
  }
202
212
  const ToggleDot = styled('span', toggleProps)`
203
213
  position: absolute;
@@ -233,7 +243,13 @@
233
243
  border-radius: 100%;
234
244
  transition: transform 0.4s ease;
235
245
  transform: ${(props) =>
236
- props.isChecked
246
+ props.undefinedState
247
+ ? props.size === 'medium'
248
+ ? 'translateX(15px)'
249
+ : props.size === 'small'
250
+ ? 'translateX(6px)'
251
+ : 'translateX(15px)'
252
+ : props.isChecked
237
253
  ? props.size === 'medium'
238
254
  ? 'translateX(25px)'
239
255
  : props.size === 'small'
@@ -336,6 +352,11 @@
336
352
  default: null,
337
353
  required: false,
338
354
  },
355
+ undefinedState: {
356
+ type: Boolean,
357
+ default: false,
358
+ required: false,
359
+ },
339
360
  },
340
361
  methods: {
341
362
  onToggleChange() {
@@ -77,16 +77,16 @@ describe('RCToggle', () => {
77
77
  expect(wrapper.find('[data-test-id="label_left_container"]').exists()).toBe(
78
78
  false
79
79
  )
80
- expect(wrapper.find('[data-test-id="label_right_container"]').exists()).toBe(
81
- false
82
- )
80
+ expect(
81
+ wrapper.find('[data-test-id="label_right_container"]').exists()
82
+ ).toBe(false)
83
83
  })
84
84
 
85
85
  it('renders the label on the right by default', () => {
86
86
  const wrapper = mountToggle({ label: 'My Label' })
87
- expect(wrapper.find('[data-test-id="label_right_container"]').exists()).toBe(
88
- true
89
- )
87
+ expect(
88
+ wrapper.find('[data-test-id="label_right_container"]').exists()
89
+ ).toBe(true)
90
90
  expect(wrapper.find('[data-test-id="label_left_container"]').exists()).toBe(
91
91
  false
92
92
  )
@@ -98,9 +98,9 @@ describe('RCToggle', () => {
98
98
  expect(wrapper.find('[data-test-id="label_left_container"]').exists()).toBe(
99
99
  true
100
100
  )
101
- expect(wrapper.find('[data-test-id="label_right_container"]').exists()).toBe(
102
- false
103
- )
101
+ expect(
102
+ wrapper.find('[data-test-id="label_right_container"]').exists()
103
+ ).toBe(false)
104
104
  })
105
105
 
106
106
  it('renders the info text only when infoTextMessage is provided', () => {
@@ -120,31 +120,8 @@
120
120
  v-if="group.children?.length"
121
121
  :is-collapsed="isCollapsed"
122
122
  >
123
- <GroupHeader
124
- v-if="!isCollapsed"
125
- :data-id="`nav_section_${groupKey(group, groupIdx)}`"
126
- :data-qa-id="`nav_section_${groupKey(group, groupIdx)}`"
127
- @click="toggleGroup(groupKey(group, groupIdx))"
128
- >
129
- <GroupName>{{ group.groupName }}</GroupName>
130
- <GroupChevron>
131
- <IconComponent
132
- :color="theme.semanticColors.grey[700]"
133
- :hovered-color="theme.semanticColors.purple[500]"
134
- :name="
135
- isGroupCollapsed(groupKey(group, groupIdx))
136
- ? 'chevron_down'
137
- : 'chevron_up'
138
- "
139
- size="10px"
140
- />
141
- </GroupChevron>
142
- </GroupHeader>
143
- <ItemList
144
- v-if="
145
- isCollapsed || !isGroupCollapsed(groupKey(group, groupIdx))
146
- "
147
- >
123
+ <GroupName v-if="!isCollapsed">{{ group.groupName }}</GroupName>
124
+ <ItemList>
148
125
  <template
149
126
  v-for="(item, itemIdx) in group.children"
150
127
  :key="item.id ?? itemIdx"
@@ -160,7 +137,7 @@
160
137
  :is-disabled="item.isDisabled"
161
138
  @click="
162
139
  !item.isDisabled &&
163
- $emit('on-menu-item-click', { item })
140
+ $emit('on-menu-item-click', { item })
164
141
  "
165
142
  >
166
143
  <IconCell :is-collapsed="isCollapsed">
@@ -197,7 +174,7 @@
197
174
  type="tertiary"
198
175
  @click="
199
176
  !item.isDisabled &&
200
- $emit('on-menu-item-click', { item })
177
+ $emit('on-menu-item-click', { item })
201
178
  "
202
179
  />
203
180
  </template>
@@ -267,7 +244,7 @@
267
244
  :is-disabled="sub.isDisabled"
268
245
  @click="
269
246
  !sub.isDisabled &&
270
- $emit('on-menu-item-click', { item: sub })
247
+ $emit('on-menu-item-click', { item: sub })
271
248
  "
272
249
  >
273
250
  <span>{{ sub.name }}</span>
@@ -330,8 +307,8 @@
330
307
  p.isCollapsed
331
308
  ? `${NAV_SIDEBAR_COLLAPSED_OUTER_WIDTH_PX}px`
332
309
  : p.expandedWidth
333
- ? `${p.expandedWidth}px`
334
- : 'max-content'};
310
+ ? `${p.expandedWidth}px`
311
+ : 'max-content'};
335
312
  min-width: ${(p) =>
336
313
  p.isCollapsed ? `${NAV_SIDEBAR_COLLAPSED_OUTER_WIDTH_PX}px` : '0'};
337
314
  border-right: 1px solid ${(p) => p.theme.semanticColors.grey[300]};
@@ -441,18 +418,6 @@
441
418
  padding: ${(p) => (p.isCollapsed ? '10px 8px' : '10px 16px')};
442
419
  `
443
420
 
444
- const GroupHeader = styled.div`
445
- display: flex;
446
- align-items: center;
447
- justify-content: space-between;
448
- gap: 6px;
449
- cursor: pointer;
450
-
451
- &:hover > div:first-child {
452
- color: ${(p) => p.theme.semanticColors.purple[500]};
453
- }
454
- `
455
-
456
421
  const GroupName = styled.div`
457
422
  font-size: 10px;
458
423
  font-weight: 400;
@@ -461,12 +426,6 @@
461
426
  line-height: 150%;
462
427
  `
463
428
 
464
- const GroupChevron = styled.div`
465
- display: flex;
466
- align-items: center;
467
- flex-shrink: 0;
468
- `
469
-
470
429
  const ItemList = styled.div`
471
430
  display: flex;
472
431
  flex-direction: column;
@@ -799,9 +758,7 @@
799
758
  ScrollbarThumb,
800
759
  GroupsList,
801
760
  GroupSection,
802
- GroupHeader,
803
761
  GroupName,
804
- GroupChevron,
805
762
  ItemList,
806
763
  MenuItem,
807
764
  IconCell,
@@ -880,7 +837,6 @@
880
837
  'on-menu-item-click',
881
838
  'on-footer-button-click',
882
839
  'sidebar-dimensions',
883
- 'section-collapse-toggle',
884
840
  ],
885
841
  data() {
886
842
  return {
@@ -888,7 +844,6 @@
888
844
  isCollapsed: false,
889
845
  responsibleSelectSurfaceOpen: false,
890
846
  openDropdownId: null,
891
- collapsedGroups: {},
892
847
  isWrapperHovered: false,
893
848
  expandedWidth: null,
894
849
  scrollThumbStyle: {},
@@ -1165,27 +1120,6 @@
1165
1120
  this.openDropdownId = this.openDropdownId === id ? null : id
1166
1121
  }
1167
1122
  },
1168
- groupKey(group, idx) {
1169
- return group.id ?? group.groupName ?? String(idx)
1170
- },
1171
- isGroupCollapsed(key) {
1172
- return !!this.collapsedGroups[key]
1173
- },
1174
- toggleGroup(key) {
1175
- if (this.collapsedGroups[key]) {
1176
- delete this.collapsedGroups[key]
1177
- } else {
1178
- this.collapsedGroups[key] = true
1179
- }
1180
- this.$emit('section-collapse-toggle', {
1181
- key,
1182
- collapsed: this.isGroupCollapsed(key),
1183
- })
1184
- this.$nextTick(() => {
1185
- this.updateScrollThumb()
1186
- this.updateExpandedWidth()
1187
- })
1188
- },
1189
1123
  },
1190
1124
  }
1191
1125
  </script>
@@ -113,7 +113,8 @@
113
113
  size: {
114
114
  type: String,
115
115
  default: 'medium',
116
- validator: (value) => ['small', 'medium', 'large'].includes(value),
116
+ validator: (value) =>
117
+ ['tiny', 'small', 'medium', 'large'].includes(value),
117
118
  },
118
119
  isWhite: {
119
120
  required: false,
@@ -1,145 +0,0 @@
1
- import { mount } from '@vue/test-utils'
2
- import NavigationSideMenu from '@/components/navigationSideMenu'
3
-
4
- jest.mock('@/components/icon/iconCache.mjs', () => ({
5
- // need to mock this due to how jest handles import.meta
6
- fetchIcon: jest.fn(() => Promise.resolve('mocked-icon-url.svg')),
7
- }))
8
-
9
- const menuData = [
10
- {
11
- groupName: 'Project',
12
- children: [
13
- { id: 'technologies', name: 'Technologies', dataId: 'nav_technologies' },
14
- { id: 'finance', name: 'Finance', dataId: 'nav_finance' },
15
- ],
16
- },
17
- {
18
- groupName: 'Workflow',
19
- children: [
20
- {
21
- id: 'workflow_template',
22
- name: 'Workflow template',
23
- dataId: 'nav_workflow_template',
24
- },
25
- ],
26
- },
27
- ]
28
-
29
- const mountMenu = (props = {}) =>
30
- mount(NavigationSideMenu, { props: { menuData, ...props } })
31
-
32
- const item = (wrapper, dataId) => wrapper.find(`[data-id="${dataId}"]`)
33
- const section = (wrapper, key) => wrapper.find(`[data-id="nav_section_${key}"]`)
34
-
35
- describe('navigationSideMenu/index.vue', () => {
36
- describe('default state', () => {
37
- it('renders a header for each non-empty group', () => {
38
- const wrapper = mountMenu()
39
-
40
- expect(section(wrapper, 'Project').exists()).toBe(true)
41
- expect(section(wrapper, 'Workflow').exists()).toBe(true)
42
- })
43
-
44
- it('shows every group item when nothing is collapsed', () => {
45
- const wrapper = mountMenu()
46
-
47
- expect(item(wrapper, 'nav_technologies').exists()).toBe(true)
48
- expect(item(wrapper, 'nav_finance').exists()).toBe(true)
49
- expect(item(wrapper, 'nav_workflow_template').exists()).toBe(true)
50
- })
51
-
52
- it('reports no group as collapsed initially', () => {
53
- const wrapper = mountMenu()
54
-
55
- expect(wrapper.vm.isGroupCollapsed('Project')).toBe(false)
56
- expect(wrapper.vm.isGroupCollapsed('Workflow')).toBe(false)
57
- })
58
- })
59
-
60
- describe('toggling a section', () => {
61
- it("hides that section's items when its header is clicked, and shows them again on a second click", async () => {
62
- const wrapper = mountMenu()
63
-
64
- await section(wrapper, 'Project').trigger('click')
65
- expect(item(wrapper, 'nav_technologies').exists()).toBe(false)
66
- expect(item(wrapper, 'nav_finance').exists()).toBe(false)
67
-
68
- await section(wrapper, 'Project').trigger('click')
69
- expect(item(wrapper, 'nav_technologies').exists()).toBe(true)
70
- expect(item(wrapper, 'nav_finance').exists()).toBe(true)
71
- })
72
-
73
- it('collapses sections independently — collapsing one leaves the others expanded', async () => {
74
- const wrapper = mountMenu()
75
-
76
- await section(wrapper, 'Project').trigger('click')
77
-
78
- expect(item(wrapper, 'nav_technologies').exists()).toBe(false)
79
- expect(item(wrapper, 'nav_workflow_template').exists()).toBe(true)
80
- expect(wrapper.vm.isGroupCollapsed('Project')).toBe(true)
81
- expect(wrapper.vm.isGroupCollapsed('Workflow')).toBe(false)
82
- })
83
-
84
- it('emits section-collapse-toggle with the key and post-toggle collapsed state', async () => {
85
- const wrapper = mountMenu()
86
-
87
- await section(wrapper, 'Project').trigger('click')
88
- await section(wrapper, 'Project').trigger('click')
89
-
90
- const events = wrapper.emitted('section-collapse-toggle')
91
- expect(events).toHaveLength(2)
92
- expect(events[0]).toEqual([{ key: 'Project', collapsed: true }])
93
- expect(events[1]).toEqual([{ key: 'Project', collapsed: false }])
94
- })
95
- })
96
-
97
- describe('groupKey', () => {
98
- it('prefers an explicit id over the group name', () => {
99
- const wrapper = mountMenu()
100
-
101
- expect(wrapper.vm.groupKey({ id: 'a', groupName: 'B' }, 0)).toBe('a')
102
- })
103
-
104
- it('falls back to the group name when there is no id', () => {
105
- const wrapper = mountMenu()
106
-
107
- expect(wrapper.vm.groupKey({ groupName: 'B' }, 0)).toBe('B')
108
- })
109
-
110
- it('falls back to the index (as a string) when neither id nor name is present', () => {
111
- const wrapper = mountMenu()
112
-
113
- expect(wrapper.vm.groupKey({}, 3)).toBe('3')
114
- })
115
- })
116
-
117
- describe('edge cases', () => {
118
- it('mounts without error and renders no sections when menuData is empty', () => {
119
- const wrapper = mount(NavigationSideMenu, { props: { menuData: [] } })
120
-
121
- expect(wrapper.find('[data-id^="nav_section_"]').exists()).toBe(false)
122
- })
123
-
124
- it('treats groups that share a groupName as one section (name is the key)', async () => {
125
- const dupData = [
126
- {
127
- groupName: 'Other',
128
- children: [{ id: 'a', name: 'A', dataId: 'nav_a' }],
129
- },
130
- {
131
- groupName: 'Other',
132
- children: [{ id: 'b', name: 'B', dataId: 'nav_b' }],
133
- },
134
- ]
135
- const wrapper = mount(NavigationSideMenu, {
136
- props: { menuData: dupData },
137
- })
138
-
139
- await wrapper.findAll('[data-id="nav_section_Other"]')[0].trigger('click')
140
-
141
- expect(item(wrapper, 'nav_a').exists()).toBe(false)
142
- expect(item(wrapper, 'nav_b').exists()).toBe(false)
143
- })
144
- })
145
- })
@@ -1,133 +0,0 @@
1
- import { action } from '@storybook/addon-actions'
2
- import NavigationSideMenu from './index.vue'
3
- import theme from '@/assets/theme'
4
-
5
- // Grouped menu mirroring the beta settings navigation: each top-level entry is a
6
- // section ({ groupName, children }). Click a section header (e.g. "OFFER") to
7
- // collapse/expand its items — sections collapse independently.
8
- const settingsMenu = [
9
- {
10
- groupName: 'Project',
11
- children: [
12
- {
13
- id: 'technologies',
14
- name: 'Technologies',
15
- dataId: 'nav_technologies',
16
- icon: 'settings',
17
- },
18
- {
19
- id: 'finance',
20
- name: 'Finance',
21
- dataId: 'nav_finance',
22
- icon: 'documents',
23
- },
24
- ],
25
- },
26
- {
27
- groupName: 'Workflow',
28
- children: [
29
- {
30
- id: 'workflow_template',
31
- name: 'Workflow template',
32
- dataId: 'nav_workflow_template',
33
- icon: 'settings',
34
- },
35
- {
36
- id: 'activity_type',
37
- name: 'Activity type definition',
38
- dataId: 'nav_activity_type',
39
- icon: 'documents',
40
- },
41
- ],
42
- },
43
- {
44
- groupName: 'Offer',
45
- children: [
46
- {
47
- id: 'branding',
48
- name: 'Branding',
49
- dataId: 'nav_branding',
50
- icon: 'settings',
51
- },
52
- {
53
- id: 'document_templates',
54
- name: 'Document templates',
55
- dataId: 'nav_document_templates',
56
- icon: 'documents',
57
- // An item-level dropdown (its own children) — orthogonal to section collapse.
58
- children: [
59
- { id: 'quotation', name: 'Quotation', dataId: 'nav_quotation' },
60
- { id: 'invoice', name: 'Invoice', dataId: 'nav_invoice' },
61
- ],
62
- },
63
- ],
64
- },
65
- ]
66
-
67
- const projectManagerOptions = [
68
- { id: 1, full_name: 'Firstname Lastname' },
69
- { id: 2, full_name: 'Alex Meyer' },
70
- ]
71
-
72
- export default {
73
- title: 'Components/Navigation/NavigationSideMenu',
74
- component: NavigationSideMenu,
75
- tags: ['autodocs'],
76
- parameters: {
77
- layout: 'fullscreen',
78
- },
79
- }
80
-
81
- const Template = (args) => ({
82
- components: { NavigationSideMenu },
83
- setup() {
84
- const onSectionToggle = (payload) =>
85
- action('section-collapse-toggle')(payload)
86
- const onItemClick = (payload) => action('on-menu-item-click')(payload)
87
- return { args, onSectionToggle, onItemClick }
88
- },
89
- template: `
90
- <div data-test-id="navigation-side-menu-story" style="display: flex; min-height: 100vh; background: #f5f5f5;">
91
- <NavigationSideMenu
92
- v-bind="args"
93
- @section-collapse-toggle="onSectionToggle"
94
- @on-menu-item-click="onItemClick"
95
- />
96
- <main style="flex: 1; padding: 24px;">Main content area</main>
97
- </div>
98
- `,
99
- provide: {
100
- theme,
101
- },
102
- })
103
-
104
- // Default: all sections expanded. Click a section header to collapse/expand it;
105
- // multiple sections can be collapsed at once (independent, not accordion).
106
- export const CollapsibleSections = Template.bind({})
107
- CollapsibleSections.args = {
108
- menuData: settingsMenu,
109
- headerTitle: 'Settings',
110
- activeItemId: 'technologies',
111
- }
112
-
113
- // An item that has its own children ("Document templates") renders an item-level
114
- // dropdown, which behaves independently of section collapse.
115
- export const WithNestedDropdown = Template.bind({})
116
- WithNestedDropdown.args = {
117
- menuData: settingsMenu,
118
- headerTitle: 'Settings',
119
- activeItemId: 'quotation',
120
- activeParentId: 'document_templates',
121
- }
122
-
123
- // The project-view placement of the same component: project-manager select in the
124
- // header plus a footer button. Section collapse works identically here.
125
- export const ProjectPlacement = Template.bind({})
126
- ProjectPlacement.args = {
127
- menuData: settingsMenu,
128
- showProjectManagerSelect: true,
129
- projectManagerOptions,
130
- projectManagerId: 1,
131
- footerButtonText: 'Documents',
132
- activeItemId: 'finance',
133
- }