@eturnity/eturnity_reusable_components 7.45.1-EPDM-12459.1 → 7.45.2-EPDM-10609.0

Sign up to get free protection for your applications and to get access to all the features.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eturnity/eturnity_reusable_components",
3
- "version": "7.45.1-EPDM-12459.1",
3
+ "version": "7.45.2-EPDM-10609.0",
4
4
  "files": [
5
5
  "dist",
6
6
  "src"
@@ -11,6 +11,7 @@ const theme = {
11
11
  lightGray: '#f2f2f2',
12
12
  white: '#ffffff',
13
13
  blue: '#48a2d0',
14
+ blue2: '#6CD4D4',
14
15
  red: '#ff5656',
15
16
  blue1: '#e4efff',
16
17
  brightBlue: '#0068DE',
@@ -0,0 +1,79 @@
1
+ import Card from './index.vue'
2
+
3
+ export default {
4
+ title: 'Card',
5
+ component: Card,
6
+ tags: ['autodocs'],
7
+ }
8
+
9
+ // To use:
10
+ // <RCCard
11
+ // :isLoading="true"
12
+ // minWidth="150px"
13
+ // popoverText="Sample popover text"
14
+ // :showPopover="true"
15
+ // title="Sample title"
16
+ // titleClass="sampleTitleClass"
17
+ // viewCardClass="sampleViewCardClass"
18
+ // width="200px"
19
+ // />
20
+
21
+ export const Default = {
22
+ args: {
23
+ title: 'Sample card title',
24
+ },
25
+ render: (args) => ({
26
+ components: { Card },
27
+ setup() {
28
+ return { args }
29
+ },
30
+ template: `
31
+ <Card v-bind="args">
32
+ Sample card content
33
+ </ActionBanner>
34
+ `,
35
+ }),
36
+ }
37
+
38
+ export const LoadingCard = {
39
+ args: {
40
+ isLoading: true,
41
+ },
42
+ }
43
+
44
+ export const CustomWidth = {
45
+ args: {
46
+ title: 'Sample card title',
47
+ width: '600px',
48
+ },
49
+ render: (args) => ({
50
+ components: { Card },
51
+ setup() {
52
+ return { args }
53
+ },
54
+ template: `
55
+ <Card v-bind="args">
56
+ Sample card content
57
+ </ActionBanner>
58
+ `,
59
+ }),
60
+ }
61
+
62
+ export const PopoverShown = {
63
+ args: {
64
+ title: 'Sample card title',
65
+ showPopover: true,
66
+ popoverText: 'Sample popover text',
67
+ },
68
+ render: (args) => ({
69
+ components: { Card },
70
+ setup() {
71
+ return { args }
72
+ },
73
+ template: `
74
+ <Card v-bind="args">
75
+ Sample card content
76
+ </ActionBanner>
77
+ `,
78
+ }),
79
+ }
@@ -0,0 +1,135 @@
1
+ /* eslint-disable */
2
+ import { mount, config } from '@vue/test-utils'
3
+ import Card from '@/components/card'
4
+ import theme from '@/assets/theme'
5
+
6
+ config.global.components = {
7
+ EtPopover: {
8
+ template: '<div>{{ text }}</div>',
9
+ props: ['text'],
10
+ },
11
+ }
12
+
13
+ config.global.mocks = {
14
+ $gettext: (msg) => msg,
15
+ }
16
+
17
+ describe('Card Component', () => {
18
+ it('card title is shown when a title props is passed', () => {
19
+ const title = 'Test Card Title'
20
+ const wrapper = mount(Card, {
21
+ props: {
22
+ title,
23
+ },
24
+ global: {
25
+ provide: {
26
+ theme,
27
+ },
28
+ },
29
+ })
30
+
31
+ expect(wrapper.text()).toContain(title)
32
+ })
33
+
34
+ it('card spinner is shown when user set isLoading props to true', async () => {
35
+ // Start with isLoading is false
36
+ const wrapper = mount(Card, {
37
+ props: {
38
+ title: 'Test Card Title',
39
+ isLoading: false,
40
+ },
41
+ global: {
42
+ provide: {
43
+ theme,
44
+ },
45
+ },
46
+ })
47
+
48
+ let cardSpinner = wrapper.find('[data-test-id="card_spinner"]')
49
+ expect(cardSpinner.exists()).toBe(false)
50
+ await wrapper.setProps({ isLoading: true })
51
+ await wrapper.vm.$nextTick()
52
+ cardSpinner = wrapper.find('[data-test-id="card_spinner"]')
53
+ expect(cardSpinner.exists()).toBe(true)
54
+ })
55
+
56
+ it('card wrapper has custom class when viewCardClass props is passed', async () => {
57
+ const viewCardClass = 'sampleClass'
58
+ const wrapper = mount(Card, {
59
+ props: {
60
+ title: 'Test Card Title',
61
+ viewCardClass,
62
+ },
63
+ global: {
64
+ provide: {
65
+ theme,
66
+ },
67
+ },
68
+ })
69
+
70
+ const cardMainWrapper = wrapper.find('[data-test-id="card_main_wrapper"]')
71
+ expect(cardMainWrapper.classes()).toContain(viewCardClass)
72
+ })
73
+
74
+ it('card title has custom class when titleClass props is passed', async () => {
75
+ const titleClass = 'sampleClass'
76
+ const wrapper = mount(Card, {
77
+ props: {
78
+ title: 'Test Card Title',
79
+ titleClass,
80
+ },
81
+ global: {
82
+ provide: {
83
+ theme,
84
+ },
85
+ },
86
+ })
87
+
88
+ const cardTitle = wrapper.find('[data-test-id="card_title"]')
89
+ expect(cardTitle.classes()).toContain(titleClass)
90
+ })
91
+
92
+ it('card popover is shown when user set showPopover props to true', async () => {
93
+ // Start with showPopover is false
94
+ const wrapper = mount(Card, {
95
+ props: {
96
+ title: 'Test Card Title',
97
+ showPopover: false,
98
+ popoverText: 'Sample popover text',
99
+ },
100
+ global: {
101
+ provide: {
102
+ theme,
103
+ },
104
+ },
105
+ })
106
+
107
+ let cardPopover = wrapper.find('[data-test-id="card_popover"]')
108
+ expect(cardPopover.exists()).toBe(false)
109
+ await wrapper.setProps({ showPopover: true })
110
+ await wrapper.vm.$nextTick()
111
+ cardPopover = wrapper.find('[data-test-id="card_popover"]')
112
+ expect(cardPopover.exists()).toBe(true)
113
+ expect(cardPopover.text()).toContain('Sample popover text')
114
+ })
115
+
116
+ it('card popover text is set when user passed a popoverText props value', async () => {
117
+ const popoverText = 'Sample popover text'
118
+ const wrapper = mount(Card, {
119
+ props: {
120
+ title: 'Test Card Title',
121
+ showPopover: true,
122
+ popoverText,
123
+ },
124
+ global: {
125
+ provide: {
126
+ theme,
127
+ },
128
+ },
129
+ })
130
+
131
+ let cardPopover = wrapper.find('[data-test-id="card_popover"]')
132
+ expect(cardPopover.exists()).toBe(true)
133
+ expect(cardPopover.text()).toContain('Sample popover text')
134
+ })
135
+ })
@@ -2,15 +2,22 @@
2
2
  <Wrapper
3
3
  v-show="!isLoading"
4
4
  :class="viewCardClass"
5
+ data-test-id="card_main_wrapper"
5
6
  :min-width="minWidth"
6
7
  :width="width"
7
8
  >
8
- <Spinner v-if="isLoading" :limited-to-modal="true" size="50px" />
9
+ <Spinner
10
+ v-if="isLoading"
11
+ data-test-id="card_spinner"
12
+ :limited-to-modal="true"
13
+ size="50px"
14
+ />
9
15
  <CardWrapper v-else>
10
- <CardTitle :class="titleClass">
16
+ <CardTitle :class="titleClass" data-test-id="card_title">
11
17
  {{ $gettext(title) }}
12
18
  <EtPopover
13
19
  v-if="showPopover && popoverText !== ''"
20
+ data-test-id="card_popover"
14
21
  :text="popoverText"
15
22
  />
16
23
  </CardTitle>
@@ -20,6 +27,19 @@
20
27
  </template>
21
28
 
22
29
  <script>
30
+ // To use:
31
+ // import RCCard from "@eturnity/eturnity_reusable_components/src/components/card"
32
+ // <RCCard
33
+ // :isLoading="true"
34
+ // minWidth="150px"
35
+ // popoverText="Sample popover text"
36
+ // :showPopover="true"
37
+ // title="Sample title"
38
+ // titleClass="sampleTitleClass"
39
+ // viewCardClass="sampleViewCardClass"
40
+ // width="200px"
41
+ // />
42
+
23
43
  import styled from 'vue3-styled-components'
24
44
  import Spinner from '../spinner'
25
45
 
@@ -0,0 +1,4 @@
1
+ export const TEXT_OVERLAY_TOP_OFFSET = 45
2
+ export const TEXT_OVERLAY_LEFT_OFFSET = 3
3
+ export const TEXT_OVERLAY_ARROW_LEFT_OFFSET = 78
4
+ export const TEXT_OVERLAY_ARROW_RIGHT_NEGATIVE_OFFSET = 86
@@ -1,29 +1,48 @@
1
1
  <template>
2
2
  <ComponentWrapper>
3
- <IconWrapper :size="size">
3
+ <IconWrapper :size="type === 'dot' ? 'unset' : size">
4
4
  <IconImg
5
+ ref="iconImg"
5
6
  data-test-id="infoText_trigger"
6
7
  @click.prevent="toggleShowInfo()"
7
8
  @mouseenter="openTrigger == 'onHover' ? toggleShowInfo() : ''"
8
9
  @mouseleave="openTrigger == 'onHover' ? toggleShowInfo() : ''"
9
10
  >
11
+ <Dot v-if="type === 'dot'" :color="dotColor" />
10
12
  <IconComponent
13
+ v-else
11
14
  :color="iconColor"
12
15
  cursor="pointer"
13
16
  name="info"
14
17
  :size="size"
15
18
  />
16
19
  </IconImg>
17
- <TextOverlay
18
- v-if="showInfo"
19
- :align-arrow="alignArrow"
20
- :half-computed-text-info-width="halfComputedTextInfoWidth"
21
- :icon-size="size"
22
- :max-width="maxWidth"
23
- :width="width"
24
- ><slot></slot>
25
- <span v-html="text"></span>
26
- </TextOverlay>
20
+ <template v-if="!shouldUseTeleport">
21
+ <TextOverlay
22
+ v-if="showInfo"
23
+ :align-arrow="alignArrow"
24
+ :half-computed-text-info-width="halfComputedTextInfoWidth"
25
+ :icon-size="size"
26
+ :max-width="maxWidth"
27
+ :width="width"
28
+ ><slot></slot>
29
+ <span v-html="text"></span>
30
+ </TextOverlay>
31
+ </template>
32
+ <Teleport v-else to="#portal-target">
33
+ <TextOverlay
34
+ v-if="showInfo"
35
+ :align-arrow="alignArrow"
36
+ :half-computed-text-info-width="halfComputedTextInfoWidth"
37
+ :icon-size="size"
38
+ :max-width="maxWidth"
39
+ :width="width"
40
+ :top="position.top"
41
+ :left="position.left"
42
+ ><slot></slot>
43
+ <span v-html="text"></span>
44
+ </TextOverlay>
45
+ </Teleport>
27
46
  </IconWrapper>
28
47
  </ComponentWrapper>
29
48
  </template>
@@ -36,25 +55,37 @@
36
55
  // size="20px"
37
56
  // alignArrow="right" // which side the arrow should be on
38
57
  // />
58
+ import { Teleport } from 'vue'
39
59
  import theme from '../../assets/theme.js'
40
60
  import styled from 'vue3-styled-components'
41
61
  import IconComponent from '../icon'
62
+ import {
63
+ TEXT_OVERLAY_ARROW_LEFT_OFFSET,
64
+ TEXT_OVERLAY_ARROW_RIGHT_NEGATIVE_OFFSET,
65
+ TEXT_OVERLAY_TOP_OFFSET,
66
+ } from './constants.js'
42
67
 
43
68
  const textAttrs = {
44
69
  iconSize: String,
45
70
  alignArrow: String,
46
71
  width: String,
47
72
  halfComputedTextInfoWidth: Number,
73
+ top: String,
74
+ left: String,
48
75
  }
49
76
  const TextOverlay = styled('div', textAttrs)`
50
77
  position: absolute;
51
- top: ${(props) => 'calc(' + props.iconSize + ' + 15px)'};
78
+ top: ${(props) =>
79
+ props.top ? props.top : 'calc(' + props.iconSize + ' + 15px)'};
52
80
  ${(props) =>
53
- props.alignArrow === 'left'
81
+ props.left
82
+ ? 'left: ' + props.left
83
+ : props.alignArrow === 'left'
54
84
  ? 'left: calc(' + props.iconSize + ' /2 - 18px)'
55
85
  : props.alignArrow === 'center'
56
86
  ? 'left: calc((-' + props.width + ' + ' + props.iconSize + ') /2 + 2px)'
57
87
  : 'right: calc(' + props.iconSize + ' /2 - 17px)'};
88
+
58
89
  text-align: left;
59
90
  background: ${(props) => props.theme.colors.black};
60
91
  padding: 10px;
@@ -95,6 +126,14 @@
95
126
  height: ${(props) => props.size};
96
127
  `
97
128
 
129
+ const Dot = styled('div')`
130
+ width: 5px;
131
+ height: 5px;
132
+ background-color: ${(props) => props.color};
133
+ border-radius: 50%;
134
+ display: inline-block;
135
+ `
136
+
98
137
  const IconImg = styled.div`
99
138
  line-height: 0;
100
139
  `
@@ -106,29 +145,37 @@
106
145
  export default {
107
146
  name: 'InfoText',
108
147
  components: {
148
+ Dot,
109
149
  IconWrapper,
110
150
  TextOverlay,
111
151
  ComponentWrapper,
112
152
  IconImg,
113
153
  IconComponent,
154
+ Teleport,
114
155
  },
115
156
  props: {
116
157
  text: {
158
+ type: String,
117
159
  required: false,
160
+ default: '',
118
161
  },
119
162
  size: {
163
+ type: String,
120
164
  required: false,
121
165
  default: '14px',
122
166
  },
123
167
  alignArrow: {
168
+ type: String,
124
169
  required: false,
125
170
  default: 'center',
126
171
  },
127
172
  openTrigger: {
173
+ type: String,
128
174
  required: false,
129
175
  default: 'onHover', // onHover, onClick
130
176
  },
131
177
  width: {
178
+ type: String,
132
179
  required: false,
133
180
  default: '200px',
134
181
  },
@@ -136,10 +183,28 @@
136
183
  type: String,
137
184
  default: '400px',
138
185
  },
186
+ shouldUseTeleport: {
187
+ type: Boolean,
188
+ default: false,
189
+ },
190
+ dotColor: {
191
+ type: String,
192
+ required: false,
193
+ default: theme.colors.blue2,
194
+ },
195
+ type: {
196
+ type: String,
197
+ required: false,
198
+ default: 'info', // info, dot
199
+ },
139
200
  },
140
201
  data() {
141
202
  return {
142
203
  showInfo: false,
204
+ position: {
205
+ top: null,
206
+ left: null,
207
+ },
143
208
  }
144
209
  },
145
210
  computed: {
@@ -159,6 +224,10 @@
159
224
  } else {
160
225
  document.removeEventListener('click', this.clickOutside)
161
226
  }
227
+
228
+ if (this.shouldUseTeleport && this.showInfo) {
229
+ this.getTeleportPosition()
230
+ }
162
231
  },
163
232
  clickOutside(event) {
164
233
  if (this.$el.contains(event.target)) {
@@ -166,6 +235,35 @@
166
235
  }
167
236
  this.toggleShowInfo()
168
237
  },
238
+ getTextOverlayTop() {
239
+ const iconImg = this.$refs.iconImg.$el
240
+ const iconImgTop = iconImg.getBoundingClientRect().top
241
+ const iconImgHeight = iconImg.clientHeight
242
+ const textOverlayTop =
243
+ iconImgTop - iconImgHeight / 2 + TEXT_OVERLAY_TOP_OFFSET
244
+
245
+ console.log(textOverlayTop)
246
+
247
+ return `${textOverlayTop}px`
248
+ },
249
+ getTextOverlayLeft() {
250
+ const iconImg = this.$refs.iconImg.$el
251
+ const iconImgLeft = iconImg.getBoundingClientRect().left
252
+ const iconImgWidth = iconImg.clientWidth
253
+ const textOverlayLeft = iconImgLeft + iconImgWidth / 2 - 100 + 3
254
+
255
+ if (this.alignArrow === 'center') {
256
+ return `${textOverlayLeft}px`
257
+ } else if (this.alignArrow === 'left') {
258
+ return `${textOverlayLeft + TEXT_OVERLAY_ARROW_LEFT_OFFSET}px`
259
+ }
260
+
261
+ return `${textOverlayLeft - TEXT_OVERLAY_ARROW_RIGHT_NEGATIVE_OFFSET}px`
262
+ },
263
+ getTeleportPosition() {
264
+ this.position.top = this.getTextOverlayTop()
265
+ this.position.left = this.getTextOverlayLeft()
266
+ },
169
267
  },
170
268
  }
171
269
  </script>
@@ -120,11 +120,6 @@
120
120
  <SelectDropdown
121
121
  v-show="isSelectDropdownShown"
122
122
  ref="dropdown"
123
- :style="{
124
- transform: `translate(${dropdownPosition?.left}px, ${
125
- noRelative ? 'auto' : `${dropdownPosition?.top}px`
126
- })`,
127
- }"
128
123
  :bg-color="
129
124
  dropdownBgColor || colorMode == 'dark' ? 'black' : 'white'
130
125
  "
@@ -349,8 +344,9 @@
349
344
  box-sizing: border-box;
350
345
  z-index: ${(props) => (props.isActive ? '2' : '99999')};
351
346
  position: absolute;
352
- top: 0px;
353
- left: 0px;
347
+ top: ${(props) =>
348
+ props.noRelative ? 'auto' : props.dropdownPosition?.top + 'px'};
349
+ left: ${(props) => props.dropdownPosition?.left}px;
354
350
  border: ${BORDER_WIDTH} solid ${(props) => props.theme.colors.grey4};
355
351
  border-radius: 4px;
356
352
  display: flex;
@@ -610,10 +606,6 @@
610
606
  },
611
607
  dropdownWidth: null,
612
608
  hoveredValue: null,
613
- isDisplayedAtBottom: true,
614
- selectTopPosition: 0,
615
- selectAndDropdownDistance: 0,
616
- animationFrameId: null,
617
609
  }
618
610
  },
619
611
  computed: {
@@ -684,13 +676,8 @@
684
676
  }, 10)
685
677
  await this.$nextTick()
686
678
  this.handleSetDropdownOffet()
687
- this.calculateSelectTopPosition()
688
679
  } else {
689
680
  this.dropdownPosition.left = null
690
- if (this.animationFrameId) {
691
- cancelAnimationFrame(this.animationFrameId)
692
- this.animationFrameId = null
693
- }
694
681
  setTimeout(() => {
695
682
  this.isClickOutsideActive = false
696
683
  }, 10)
@@ -703,27 +690,11 @@
703
690
  })
704
691
  }
705
692
  },
706
- isSelectDropdownShown(isShown) {
707
- if (!isShown) return
708
-
709
- // Need to wait for 1ms to make sure the dropdown menu is shown in the DOM
710
- // before getting the distance between the select and the dropdown menu
711
- setTimeout(() => {
712
- this.getDistanceBetweenSelectAndDropdownMenu()
713
- }, 100)
714
- },
715
- selectTopPosition() {
716
- this.dropdownPosition.top =
717
- this.selectTopPosition +
718
- this.$refs.select.$el.clientHeight +
719
- this.selectAndDropdownDistance
720
- },
721
693
  },
722
694
  mounted() {
723
695
  this.observeDropdownHeight()
724
696
  this.observeSelectWidth()
725
697
  window.addEventListener('resize', this.handleSetDropdownOffet)
726
- document.body.addEventListener('scroll', this.calculateSelectTopPosition)
727
698
  },
728
699
  beforeMount() {
729
700
  this.selectedValue = this.value
@@ -732,10 +703,6 @@
732
703
  window.removeEventListener('resize', this.handleSetDropdownOffet)
733
704
  if (this.dropdownResizeObserver) this.dropdownResizeObserver.disconnect()
734
705
  if (this.selectResizeObserver) this.selectResizeObserver.disconnect()
735
- document.body.removeEventListener(
736
- 'scroll',
737
- this.calculateSelectTopPosition
738
- )
739
706
  },
740
707
  unmounted() {
741
708
  document.removeEventListener('click', this.clickOutside)
@@ -841,11 +808,11 @@
841
808
  return
842
809
  }
843
810
  await this.$nextTick()
844
- this.isDisplayedAtBottom = await this.generateDropdownPosition()
811
+ const isDisplayedAtBottom = await this.generateDropdownPosition()
845
812
  // If the dropdown menu is going to be displayed at the bottom,
846
813
  // we need reverify its position after a dom update (nextTick)
847
814
  await this.$nextTick()
848
- if (this.isDisplayedAtBottom) this.generateDropdownPosition()
815
+ if (isDisplayedAtBottom) this.generateDropdownPosition()
849
816
  },
850
817
  async generateDropdownPosition() {
851
818
  const isDropdownNotCompletelyVisible =
@@ -938,25 +905,6 @@
938
905
  }
939
906
  }
940
907
  },
941
- getDistanceBetweenSelectAndDropdownMenu() {
942
- const wholeSelectTopPosition =
943
- this.selectTopPosition + this.$refs.select.$el.clientHeight
944
- this.selectAndDropdownDistance =
945
- this.dropdownPosition.top - wholeSelectTopPosition
946
- },
947
- calculateSelectTopPosition() {
948
- const selectRef = this.$refs.select
949
- if (selectRef) {
950
- const currentTopPosition =
951
- selectRef.$el.getBoundingClientRect().top + window.scrollY
952
- if (this.selectTopPosition !== currentTopPosition) {
953
- this.selectTopPosition = currentTopPosition
954
- }
955
- }
956
- this.animationFrameId = requestAnimationFrame(
957
- this.calculateSelectTopPosition
958
- )
959
- },
960
908
  },
961
909
  }
962
910
  </script>