@eturnity/eturnity_reusable_components 7.45.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 +1 -1
- package/src/assets/theme.js +1 -0
- package/src/components/card/Card.stories.js +79 -0
- package/src/components/card/card.spec.js +135 -0
- package/src/components/card/index.vue +22 -2
- package/src/components/infoText/constants.js +4 -0
- package/src/components/infoText/index.vue +111 -13
package/package.json
CHANGED
package/src/assets/theme.js
CHANGED
@@ -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
|
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
|
|
@@ -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
|
-
<
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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) =>
|
78
|
+
top: ${(props) =>
|
79
|
+
props.top ? props.top : 'calc(' + props.iconSize + ' + 15px)'};
|
52
80
|
${(props) =>
|
53
|
-
props.
|
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>
|