@livestorm/ui-vue 1.1.0 → 1.1.2
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/.env +1 -0
- package/.eslintignore +4 -0
- package/.eslintrc.js +47 -0
- package/.pkg +11 -0
- package/babel.config.js +5 -0
- package/build/Dockerfile +31 -0
- package/ci/ci.yml +54 -0
- package/components-vite.js +29 -0
- package/components-webpack.js +29 -0
- package/components.d.ts +4 -0
- package/index.d.ts +3 -0
- package/index.js +24 -0
- package/package.json +12 -16
- package/plugins.js +17 -0
- package/postcss.config.js +1 -0
- package/src/components/BaseAutocomplete/BaseAutocomplete.spec.js +506 -0
- package/src/components/BaseAutocomplete/BaseAutocomplete.stories.js +184 -0
- package/src/components/BaseAutocomplete/BaseAutocomplete.vue +431 -0
- package/src/components/BaseBanner/BaseBanner.spec.js +32 -0
- package/src/components/BaseBanner/BaseBanner.stories.js +133 -0
- package/src/components/BaseBanner/BaseBanner.vue +229 -0
- package/src/components/BaseButton/BaseButton.spec.js +98 -0
- package/src/components/BaseButton/BaseButton.stories.js +291 -0
- package/src/components/BaseButton/BaseButton.vue +193 -0
- package/src/components/BaseButton/__snapshots__/BaseButton.spec.js.snap +37 -0
- package/src/components/BaseButton/__snapshots__/BaseButton.spec.js.snap.old +31 -0
- package/src/components/BaseCheckbox/BaseCheckbox.stories.js +82 -0
- package/src/components/BaseCheckbox/BaseCheckbox.vue +161 -0
- package/src/components/BaseDropdownMenu/BaseDropdownMenu.stories.js +84 -0
- package/src/components/BaseDropdownMenu/BaseDropdownMenu.vue +251 -0
- package/src/components/BaseDropdownMenu/BaseDropdownMenuButton.vue +26 -0
- package/src/components/BaseDropdownMenu/BaseDropdownMenuDivider.vue +10 -0
- package/src/components/BaseDropdownMenu/BaseDropdownMenuItem.vue +206 -0
- package/src/components/BaseDropdownMenu/BaseDropdownMenuSearch.vue +49 -0
- package/src/components/BaseDropdownMenu/BaseDropdownMenuTitle.vue +26 -0
- package/src/components/BaseModal/BaseModal.stories.js +143 -0
- package/src/components/BaseModal/BaseModal.vue +187 -0
- package/src/components/BaseModal/BaseModalContent.vue +164 -0
- package/src/components/BaseModal/BaseModalContentOld.vue +242 -0
- package/src/components/BaseRating/BaseRating.spec.js +46 -0
- package/src/components/BaseRating/BaseRating.stories.js +46 -0
- package/src/components/BaseRating/BaseRating.vue +119 -0
- package/src/components/BaseSelect/BaseSelect.stories.js +177 -0
- package/src/components/BaseSelect/BaseSelect.vue +443 -0
- package/src/components/BaseSelect/BaseSelectGroup.vue +34 -0
- package/src/components/BaseSelect/BaseSelectOption.vue +219 -0
- package/src/components/BaseSwitch/BaseSwitch.stories.js +44 -0
- package/src/components/BaseSwitch/BaseSwitch.vue +114 -0
- package/src/components/BaseTag/BaseTag.stories.js +123 -0
- package/src/components/BaseTag/BaseTag.vue +244 -0
- package/src/css/body.postcss +29 -0
- package/src/css/button.postcss +354 -0
- package/src/css/checkbox-radio.postcss +152 -0
- package/src/css/icon.postcss +9 -0
- package/src/css/loader.postcss +120 -0
- package/src/css/popper.postcss +143 -0
- package/src/css/tailwind-utilities.postcss +1 -0
- package/src/css/tailwind.postcss +2 -0
- package/src/css/transitions.postcss +66 -0
- package/src/mixins/DynamicAction.js +59 -0
- package/src/utils/breakpoints.js +8 -0
- package/src/utils/bus.js +3 -0
- package/src/utils/clipboard.js +38 -0
- package/src/utils/devices.js +134 -0
- package/src/utils/elements.js +25 -0
- package/src/utils/index.js +8 -0
- package/src/utils/responsive.js +61 -0
- package/src/utils/router.js +94 -0
- package/src/utils/slot.js +27 -0
- package/src/utils/stack-view.js +116 -0
- package/src/utils/uid.js +5 -0
- package/style-postcss.d.ts +3 -0
- package/style-postcss.js +2 -0
- package/style.js +12 -0
- package/tailwind.config.base.cjs +88 -0
- package/tailwind.config.js +9 -0
- package/tsconfig.check.json +13 -0
- package/tsconfig.json +66 -0
- package/vite.config.js +43 -0
- package/vitest.setup.js +26 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { InjectionKey } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface PropsFromContent {
|
|
5
|
+
locked?: boolean
|
|
6
|
+
fullScreen?: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const InjectPropsFromContent = Symbol('BaseModal.propsFromContent') as InjectionKey<PropsFromContent>
|
|
10
|
+
|
|
11
|
+
export const InjectClose = Symbol('BaseModal.close') as InjectionKey<() => void>
|
|
12
|
+
|
|
13
|
+
type OnCloseHandler = () => unknown
|
|
14
|
+
|
|
15
|
+
export const InjectOnClose = Symbol('BaseModal.onClose') as InjectionKey<(handler: OnCloseHandler) => void>
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<script setup lang="ts">
|
|
19
|
+
import { computed, onMounted, onUnmounted, provide, reactive, ref, toRefs } from 'vue'
|
|
20
|
+
// @ts-ignore
|
|
21
|
+
import { useStackedComponent, useStackAutofocus } from '../../utils'
|
|
22
|
+
|
|
23
|
+
const props = withDefaults(defineProps<{
|
|
24
|
+
shown?: boolean
|
|
25
|
+
}>(), { shown: true })
|
|
26
|
+
|
|
27
|
+
const emit = defineEmits([
|
|
28
|
+
'close',
|
|
29
|
+
'update:shown',
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
const propsFromContent = reactive<PropsFromContent>({
|
|
33
|
+
locked: false,
|
|
34
|
+
fullScreen: false,
|
|
35
|
+
})
|
|
36
|
+
provide(InjectPropsFromContent, propsFromContent)
|
|
37
|
+
|
|
38
|
+
const lockedFinal = computed(() => propsFromContent.locked)
|
|
39
|
+
const fullScreenFinal = computed(() => propsFromContent.fullScreen)
|
|
40
|
+
|
|
41
|
+
function close () {
|
|
42
|
+
if (!lockedFinal.value) {
|
|
43
|
+
emit('update:shown', false)
|
|
44
|
+
emit('close')
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
provide(InjectClose, close)
|
|
49
|
+
|
|
50
|
+
const onCloseHandlers: OnCloseHandler[] = []
|
|
51
|
+
|
|
52
|
+
provide(InjectOnClose, (handler) => {
|
|
53
|
+
onCloseHandlers.push(handler)
|
|
54
|
+
|
|
55
|
+
// Child context
|
|
56
|
+
// On child unmount
|
|
57
|
+
onUnmounted(() => {
|
|
58
|
+
const index = onCloseHandlers.indexOf(handler)
|
|
59
|
+
onCloseHandlers.splice(index, 1)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
function closeHandler () {
|
|
64
|
+
if (!lockedFinal.value) {
|
|
65
|
+
close()
|
|
66
|
+
onCloseHandlers.forEach(handler => handler())
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { shown } = toRefs(props)
|
|
71
|
+
const {
|
|
72
|
+
isActiveStacked,
|
|
73
|
+
stackIndex,
|
|
74
|
+
onClose,
|
|
75
|
+
} = useStackedComponent(shown, 'modal')
|
|
76
|
+
onClose(closeHandler)
|
|
77
|
+
|
|
78
|
+
// Allow the opening animation in case `shown` is initially true
|
|
79
|
+
|
|
80
|
+
const firstFrame = ref(false)
|
|
81
|
+
|
|
82
|
+
onMounted(() => {
|
|
83
|
+
requestAnimationFrame(() => {
|
|
84
|
+
firstFrame.value = true
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const finalShown = computed(() => shown.value && firstFrame.value)
|
|
89
|
+
|
|
90
|
+
// Autofocus
|
|
91
|
+
useStackAutofocus()
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<template>
|
|
95
|
+
<SafeTeleport
|
|
96
|
+
to="#teleport-end"
|
|
97
|
+
>
|
|
98
|
+
<transition
|
|
99
|
+
:duration="200"
|
|
100
|
+
name="modal"
|
|
101
|
+
appear
|
|
102
|
+
>
|
|
103
|
+
<!-- Suspense will wait for async child components before rendering (so the transition will wait for them to be loaded) -->
|
|
104
|
+
<Suspense
|
|
105
|
+
v-if="finalShown"
|
|
106
|
+
>
|
|
107
|
+
<div
|
|
108
|
+
class="base-modal-wrapper fixed inset-0 overflow-y-auto"
|
|
109
|
+
:class="[
|
|
110
|
+
isActiveStacked ? 'stack-active' : 'grayscale stack-inactive',
|
|
111
|
+
{
|
|
112
|
+
'full-screen': fullScreenFinal,
|
|
113
|
+
},
|
|
114
|
+
]"
|
|
115
|
+
:style="{
|
|
116
|
+
'z-index': stackIndex + 8000,
|
|
117
|
+
}"
|
|
118
|
+
data-testid="modal-wrapper"
|
|
119
|
+
>
|
|
120
|
+
<!-- Close overlay -->
|
|
121
|
+
<div
|
|
122
|
+
class="absolute inset-0"
|
|
123
|
+
@click="closeHandler()"
|
|
124
|
+
/>
|
|
125
|
+
|
|
126
|
+
<div class="backdrop bg-alpha-black-300 fixed inset-0 -z-10" />
|
|
127
|
+
|
|
128
|
+
<slot />
|
|
129
|
+
</div>
|
|
130
|
+
</Suspense>
|
|
131
|
+
</transition>
|
|
132
|
+
</SafeTeleport>
|
|
133
|
+
</template>
|
|
134
|
+
|
|
135
|
+
<style lang="postcss" scoped>
|
|
136
|
+
.base-modal-wrapper {
|
|
137
|
+
&.full-screen {
|
|
138
|
+
overflow-y: initial;
|
|
139
|
+
|
|
140
|
+
:deep(.close-button) {
|
|
141
|
+
height: 100%;
|
|
142
|
+
display: flex;
|
|
143
|
+
flex-direction: column;
|
|
144
|
+
justify-content: center;
|
|
145
|
+
margin: 0;
|
|
146
|
+
.icon {
|
|
147
|
+
margin: 0;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
:deep(.base-modal-layout) {
|
|
152
|
+
height: 100%;
|
|
153
|
+
padding: 0;
|
|
154
|
+
|
|
155
|
+
.base-modal-header {
|
|
156
|
+
background-color: #fff;
|
|
157
|
+
margin: 0 !important;
|
|
158
|
+
z-index: 50;
|
|
159
|
+
position: sticky;
|
|
160
|
+
top: 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
& > section {
|
|
164
|
+
margin: 0;
|
|
165
|
+
padding: 0;
|
|
166
|
+
width: 100%;
|
|
167
|
+
height: 100%;
|
|
168
|
+
max-width: unset;
|
|
169
|
+
border-radius: 0px;
|
|
170
|
+
display: flex;
|
|
171
|
+
flex-direction: column;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.base-modal-content {
|
|
175
|
+
height: 100%;
|
|
176
|
+
overflow-y: auto;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
&.stack-inactive {
|
|
182
|
+
:deep(.base-modal-layout > section) {
|
|
183
|
+
@apply scale-95;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
</style>
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, defineEmits, defineProps, inject, watch } from 'vue'
|
|
3
|
+
import BaseButton from '../BaseButton/BaseButton.vue'
|
|
4
|
+
import { useSlotsAsComponents } from '../../utils/slot'
|
|
5
|
+
import { InjectClose, InjectOnClose, InjectPropsFromContent } from './BaseModal.vue'
|
|
6
|
+
|
|
7
|
+
interface ModalProps {
|
|
8
|
+
title: string
|
|
9
|
+
locked?: boolean
|
|
10
|
+
width?: number
|
|
11
|
+
confirmLabel?: string
|
|
12
|
+
cancelLabel?: string
|
|
13
|
+
confirmLoading?: boolean
|
|
14
|
+
confirmColor?: string
|
|
15
|
+
confirmHref?: string
|
|
16
|
+
type?: 'alert'|'warning'|'info'|'success'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const props = defineProps<ModalProps>()
|
|
20
|
+
|
|
21
|
+
// Send back to BaseModal the props used in BaseModal
|
|
22
|
+
|
|
23
|
+
const propsToParent = inject(InjectPropsFromContent)
|
|
24
|
+
|
|
25
|
+
if (!propsToParent) {
|
|
26
|
+
throw new Error('BaseModalContent must be used inside a BaseModal')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
watch(() => props.locked, value => {
|
|
30
|
+
propsToParent.locked = value
|
|
31
|
+
}, { immediate: true })
|
|
32
|
+
|
|
33
|
+
// Close
|
|
34
|
+
|
|
35
|
+
const closeParent = inject(InjectClose)!
|
|
36
|
+
|
|
37
|
+
function close (alsoCloseParent = true) {
|
|
38
|
+
if (!props.locked) {
|
|
39
|
+
emit('close')
|
|
40
|
+
if (alsoCloseParent) {
|
|
41
|
+
closeParent()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const onParentClose = inject(InjectOnClose)!
|
|
47
|
+
|
|
48
|
+
onParentClose(() => {
|
|
49
|
+
close(false)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// Slots
|
|
53
|
+
const slots = useSlotsAsComponents()
|
|
54
|
+
const contentSlot = computed(() => slots.content ? slots.content() : [])
|
|
55
|
+
const buttonsSlot = computed(() => slots['additional-buttons']!())
|
|
56
|
+
|
|
57
|
+
const emit = defineEmits([
|
|
58
|
+
'confirm',
|
|
59
|
+
'cancel',
|
|
60
|
+
'close',
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
// Types
|
|
64
|
+
|
|
65
|
+
const typeIcon = computed(() => {
|
|
66
|
+
switch (props.type) {
|
|
67
|
+
case 'alert':
|
|
68
|
+
return 'i-ls-alert-circle text-red-600'
|
|
69
|
+
case 'warning':
|
|
70
|
+
return 'i-ls-alert-triangle text-orange-600'
|
|
71
|
+
case 'info':
|
|
72
|
+
return 'i-ls-info text-livestorm-600'
|
|
73
|
+
case 'success':
|
|
74
|
+
return 'i-ls-check-circle text-green-600'
|
|
75
|
+
default:
|
|
76
|
+
return 'i-ls-alert-circle text-livestorm-600'
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<template>
|
|
82
|
+
<div class="flex min-h-full items-end justify-center p-2 text-center sm:items-center">
|
|
83
|
+
<div
|
|
84
|
+
class="relative bg-white w-full min-w-[320px] max-w-[650px] rounded-8 base-modal-box"
|
|
85
|
+
:style="{ 'max-width': `${width}px` }"
|
|
86
|
+
@click.stop
|
|
87
|
+
>
|
|
88
|
+
<div class="flex justify-between">
|
|
89
|
+
<div
|
|
90
|
+
v-if="props.title"
|
|
91
|
+
class="flex items-center px-6 pt-6"
|
|
92
|
+
>
|
|
93
|
+
<div
|
|
94
|
+
v-if="props.type"
|
|
95
|
+
class="w-6 h-6 mr-2 sm:mr-3 shrink-0"
|
|
96
|
+
:class="typeIcon"
|
|
97
|
+
/>
|
|
98
|
+
<span
|
|
99
|
+
data-testid="modal-title"
|
|
100
|
+
class="block text-16-semibold text-grey-900 text-left"
|
|
101
|
+
:class="{ 'pb-6': !contentSlot }"
|
|
102
|
+
>
|
|
103
|
+
{{ props.title }}
|
|
104
|
+
</span>
|
|
105
|
+
</div>
|
|
106
|
+
<span v-else />
|
|
107
|
+
<div
|
|
108
|
+
v-if="!locked"
|
|
109
|
+
class="i-ls-x w-6 h-6 text-grey-700 hover:text-grey-900 cursor-pointer shrink-0 mt-6 mr-6"
|
|
110
|
+
data-testid="modal-close-icon"
|
|
111
|
+
tabindex="0"
|
|
112
|
+
role="button"
|
|
113
|
+
aria-label="close"
|
|
114
|
+
@click="close()"
|
|
115
|
+
@keydown.enter="close()"
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
<div
|
|
119
|
+
v-if="contentSlot"
|
|
120
|
+
class="p-6 text-left text-grey-700"
|
|
121
|
+
data-testid="modal-content"
|
|
122
|
+
>
|
|
123
|
+
<slot name="content" />
|
|
124
|
+
</div>
|
|
125
|
+
<div
|
|
126
|
+
v-if="confirmLabel || cancelLabel || buttonsSlot"
|
|
127
|
+
class="px-6 pb-6 flex flex-row-reverse w-full sm:w-auto"
|
|
128
|
+
:class="{ 'pt-6': !props.title && !contentSlot }"
|
|
129
|
+
>
|
|
130
|
+
<div
|
|
131
|
+
class="flex flex-col-reverse sm:flex-row gap-2.5 w-full justify-end"
|
|
132
|
+
>
|
|
133
|
+
<slot name="additional-buttons" />
|
|
134
|
+
|
|
135
|
+
<BaseButton
|
|
136
|
+
v-if="cancelLabel"
|
|
137
|
+
class="base-button-anti-primary w-full sm:w-auto"
|
|
138
|
+
@click="$emit('cancel')"
|
|
139
|
+
>
|
|
140
|
+
{{ cancelLabel }}
|
|
141
|
+
</BaseButton>
|
|
142
|
+
|
|
143
|
+
<BaseButton
|
|
144
|
+
v-if="confirmLabel"
|
|
145
|
+
:loading="confirmLoading"
|
|
146
|
+
:href="confirmHref"
|
|
147
|
+
:target="confirmHref ? '_blank' : null"
|
|
148
|
+
class="w-full sm:w-auto"
|
|
149
|
+
:class="confirmColor ? `base-button-${confirmColor}` : 'base-button-primary'"
|
|
150
|
+
@click="$emit('confirm', $event)"
|
|
151
|
+
>
|
|
152
|
+
{{ confirmLabel }}
|
|
153
|
+
</BaseButton>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</template>
|
|
159
|
+
|
|
160
|
+
<style scoped lang="postcss">
|
|
161
|
+
.base-modal-box {
|
|
162
|
+
box-shadow: 0px 2px 4px 0px #0D16261A;
|
|
163
|
+
}
|
|
164
|
+
</style>
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* @TODO Remove it once the fullscreen mode is supported
|
|
4
|
+
* in BaseModal or another dedicated component (to define)
|
|
5
|
+
*/
|
|
6
|
+
import { computed, inject, watch, toRefs } from 'vue'
|
|
7
|
+
import { useSlotsAsComponents } from '../../utils/slot'
|
|
8
|
+
import { InjectPropsFromContent, InjectClose, InjectOnClose } from './BaseModal.vue'
|
|
9
|
+
|
|
10
|
+
const props = defineProps({
|
|
11
|
+
locked: {
|
|
12
|
+
type: Boolean,
|
|
13
|
+
default: false,
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
width: {
|
|
17
|
+
type: Number,
|
|
18
|
+
default: null,
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
// @deprecated
|
|
22
|
+
fullScreen: {
|
|
23
|
+
type: Boolean,
|
|
24
|
+
default: false,
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
// @deprecated
|
|
28
|
+
noPadding: {
|
|
29
|
+
type: Boolean,
|
|
30
|
+
default: false,
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
dataTestid: {
|
|
34
|
+
type: String,
|
|
35
|
+
default: null,
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const {
|
|
40
|
+
locked,
|
|
41
|
+
fullScreen,
|
|
42
|
+
width,
|
|
43
|
+
noPadding,
|
|
44
|
+
} = toRefs(props)
|
|
45
|
+
|
|
46
|
+
const emit = defineEmits([
|
|
47
|
+
'close',
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
// Send back to BaseModal the props used in BaseModal
|
|
51
|
+
|
|
52
|
+
const propsToParent = inject(InjectPropsFromContent)
|
|
53
|
+
|
|
54
|
+
if (!propsToParent) {
|
|
55
|
+
throw new Error('BaseModalContentOld must be used inside BaseModal')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
watch(() => props.locked, value => {
|
|
59
|
+
propsToParent.locked = value
|
|
60
|
+
}, { immediate: true })
|
|
61
|
+
watch(() => props.fullScreen, value => {
|
|
62
|
+
propsToParent.fullScreen = value
|
|
63
|
+
}, { immediate: true })
|
|
64
|
+
|
|
65
|
+
// Close
|
|
66
|
+
|
|
67
|
+
const closeParent = inject(InjectClose)!
|
|
68
|
+
|
|
69
|
+
function close (alsoCloseParent = true) {
|
|
70
|
+
if (!locked.value) {
|
|
71
|
+
emit('close')
|
|
72
|
+
if (alsoCloseParent) {
|
|
73
|
+
closeParent()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const onParentClose = inject(InjectOnClose)!
|
|
79
|
+
|
|
80
|
+
onParentClose(() => {
|
|
81
|
+
close(false)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// Slots
|
|
85
|
+
|
|
86
|
+
const slots = useSlotsAsComponents()
|
|
87
|
+
const titleSlot = computed(() => slots.title!())
|
|
88
|
+
const subtitleSlot = computed(() => slots.subtitle!())
|
|
89
|
+
const contentSlot = computed(() => slots.content!())
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<template>
|
|
93
|
+
<div
|
|
94
|
+
:class="{
|
|
95
|
+
'has-header': titleSlot || subtitleSlot,
|
|
96
|
+
'has-title': titleSlot,
|
|
97
|
+
'has-subtitle': subtitleSlot,
|
|
98
|
+
'has-content': contentSlot,
|
|
99
|
+
}"
|
|
100
|
+
class="base-modal-layout"
|
|
101
|
+
@click.stop
|
|
102
|
+
>
|
|
103
|
+
<section
|
|
104
|
+
:style="{ 'max-width': `${width}px` }"
|
|
105
|
+
:class="[noPadding ? 'p-0': 'p-5']"
|
|
106
|
+
class="transition-transform"
|
|
107
|
+
:data-testid="dataTestid"
|
|
108
|
+
>
|
|
109
|
+
<slot name="outside-header" />
|
|
110
|
+
|
|
111
|
+
<div class="base-modal-header">
|
|
112
|
+
<div
|
|
113
|
+
v-if="!locked && !fullScreen"
|
|
114
|
+
class="close-button"
|
|
115
|
+
data-testid="modal-close-icon"
|
|
116
|
+
@click="close()"
|
|
117
|
+
>
|
|
118
|
+
<div
|
|
119
|
+
class="i-ls-x icon"
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div
|
|
124
|
+
v-if="titleSlot"
|
|
125
|
+
class="title text-grey-900"
|
|
126
|
+
:class="{
|
|
127
|
+
'flex flex-row items-center border-b h-16 border-grey-200 px-6 py-4': fullScreen,
|
|
128
|
+
}"
|
|
129
|
+
>
|
|
130
|
+
<div
|
|
131
|
+
v-if="fullScreen"
|
|
132
|
+
class="i-ls-chevron-left arrow-back cursor-pointer w-6 h-6 mr-2 text-grey-600 hover:text-grey-600"
|
|
133
|
+
@click="close()"
|
|
134
|
+
/>
|
|
135
|
+
<RenderSlot :vnodes="titleSlot" />
|
|
136
|
+
</div>
|
|
137
|
+
<div
|
|
138
|
+
v-if="subtitleSlot"
|
|
139
|
+
class="subtitle"
|
|
140
|
+
>
|
|
141
|
+
<RenderSlot :vnodes="subtitleSlot" />
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div
|
|
146
|
+
v-if="contentSlot"
|
|
147
|
+
class="base-modal-content"
|
|
148
|
+
>
|
|
149
|
+
<RenderSlot :vnodes="contentSlot" />
|
|
150
|
+
</div>
|
|
151
|
+
</section>
|
|
152
|
+
</div>
|
|
153
|
+
</template>
|
|
154
|
+
|
|
155
|
+
<style lang="postcss" scoped>
|
|
156
|
+
.close-button {
|
|
157
|
+
position: absolute;
|
|
158
|
+
top: 0;
|
|
159
|
+
right: 0;
|
|
160
|
+
cursor: pointer;
|
|
161
|
+
opacity: 0.5;
|
|
162
|
+
transition: opacity 0.2s ease;
|
|
163
|
+
margin: 2px;
|
|
164
|
+
padding: 16px;
|
|
165
|
+
.icon {
|
|
166
|
+
width: 24px;
|
|
167
|
+
height: 24px;
|
|
168
|
+
}
|
|
169
|
+
&:hover {
|
|
170
|
+
opacity: 1;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.base-modal-layout {
|
|
175
|
+
position: absolute;
|
|
176
|
+
top: 0;
|
|
177
|
+
left: 0;
|
|
178
|
+
width: 100%;
|
|
179
|
+
height: 0;
|
|
180
|
+
z-index: 8001;
|
|
181
|
+
padding: 0 10px;
|
|
182
|
+
@apply text-grey-900;
|
|
183
|
+
& > section {
|
|
184
|
+
position: relative;
|
|
185
|
+
background: #fff;
|
|
186
|
+
margin: 30px auto;
|
|
187
|
+
max-width: 650px;
|
|
188
|
+
@apply rounded-8;
|
|
189
|
+
}
|
|
190
|
+
&.has-content {
|
|
191
|
+
&.has-header {
|
|
192
|
+
.base-modal-header {
|
|
193
|
+
margin-bottom: 16px;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
&.has-title {
|
|
198
|
+
&.has-subtitle {
|
|
199
|
+
.base-modal-header {
|
|
200
|
+
.subtitle {
|
|
201
|
+
margin-top: 6px;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
&:not(.has-header) {
|
|
207
|
+
.close-button {
|
|
208
|
+
padding: 6px;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.base-modal-header {
|
|
214
|
+
@apply text-grey-700;
|
|
215
|
+
text-align: center;
|
|
216
|
+
& > span {
|
|
217
|
+
display: block;
|
|
218
|
+
}
|
|
219
|
+
.title {
|
|
220
|
+
font-size: 1.1em;
|
|
221
|
+
font-weight: 600;
|
|
222
|
+
}
|
|
223
|
+
.subtitle {
|
|
224
|
+
font-weight: 300;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.img-ico {
|
|
229
|
+
display: block;
|
|
230
|
+
margin: 0 auto 16px auto;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.cover-ico {
|
|
234
|
+
width: 36px;
|
|
235
|
+
height: 36px;
|
|
236
|
+
margin: 0 auto 16px auto;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
:deep(.warning-title) {
|
|
240
|
+
@apply font-semibold text-grey-900
|
|
241
|
+
}
|
|
242
|
+
</style>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils'
|
|
2
|
+
import BaseRating from './BaseRating.vue'
|
|
3
|
+
|
|
4
|
+
const Selectors = {
|
|
5
|
+
Item: '[data-testid="base-rating-item"]',
|
|
6
|
+
ItemLabel: 'label',
|
|
7
|
+
ItemActive: '.active',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('BaseRating', () => {
|
|
11
|
+
test('it should interact with the user', async () => {
|
|
12
|
+
const wrapper = mount(BaseRating)
|
|
13
|
+
await wrapper.findAll(Selectors.Item)[2].find(Selectors.ItemLabel).trigger('click')
|
|
14
|
+
expect(wrapper.emitted()['update:modelValue'][0][0]).toBe(3)
|
|
15
|
+
|
|
16
|
+
await wrapper.findAll(Selectors.Item)[4].find(Selectors.ItemLabel).trigger('click')
|
|
17
|
+
expect(wrapper.emitted()['update:modelValue'][1][0]).toBe(5)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('it should active items on hover', async () => {
|
|
21
|
+
const wrapper = mount(BaseRating, {
|
|
22
|
+
props: {
|
|
23
|
+
hover: true,
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
expect(wrapper.findAll(`${Selectors.Item}${Selectors.ItemActive}`).length).toBe(0)
|
|
27
|
+
|
|
28
|
+
await wrapper.findAll(Selectors.Item)[2].trigger('mouseenter')
|
|
29
|
+
expect(wrapper.findAll(Selectors.Item)[0].classes()).toContain(Selectors.ItemActive.slice(1))
|
|
30
|
+
expect(wrapper.findAll(Selectors.Item)[1].classes()).toContain(Selectors.ItemActive.slice(1))
|
|
31
|
+
expect(wrapper.findAll(Selectors.Item)[2].classes()).toContain(Selectors.ItemActive.slice(1))
|
|
32
|
+
expect(wrapper.findAll(Selectors.Item)[3].classes()).not.toContain(Selectors.ItemActive.slice(1))
|
|
33
|
+
expect(wrapper.findAll(Selectors.Item)[4].classes()).not.toContain(Selectors.ItemActive.slice(1))
|
|
34
|
+
expect(wrapper.emitted()['update:modelValue']).toBeUndefined()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('it should not react to events when readonly', async () => {
|
|
38
|
+
const wrapper = mount(BaseRating, {
|
|
39
|
+
props: {
|
|
40
|
+
readonly: true,
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
await wrapper.findAll(Selectors.Item)[2].find(Selectors.ItemLabel).trigger('click')
|
|
44
|
+
expect(wrapper.emitted()['update:modelValue']).toBeUndefined()
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import BaseRating from './BaseRating.vue'
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
title: 'common/vue/components/BaseRating',
|
|
5
|
+
component: BaseRating,
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const defaultTemplate = (args) => ({
|
|
9
|
+
setup: () => ({ args }),
|
|
10
|
+
template: `
|
|
11
|
+
<BaseRating v-bind="args" />
|
|
12
|
+
`,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export const Simple = defaultTemplate.bind({})
|
|
16
|
+
|
|
17
|
+
export const Labels = defaultTemplate.bind({})
|
|
18
|
+
Labels.args = {
|
|
19
|
+
itemLabels: ['Very poor', 'Poor', 'Average', 'Good', 'Great'],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const StyledLabels = defaultTemplate.bind({})
|
|
23
|
+
StyledLabels.args = {
|
|
24
|
+
itemLabels: ['Very poor', 'Poor', 'Not so bad', 'Good', 'Very good'],
|
|
25
|
+
itemLabelClass: 'w-[50px] border border-grey-200 rounded p-1',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const TenItems = defaultTemplate.bind({})
|
|
29
|
+
TenItems.args = {
|
|
30
|
+
length: 10,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const Rated = defaultTemplate.bind({})
|
|
34
|
+
Rated.args = {
|
|
35
|
+
modelValue: 3,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const Hover = defaultTemplate.bind({})
|
|
39
|
+
Hover.args = {
|
|
40
|
+
hover: true,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const Name = defaultTemplate.bind({})
|
|
44
|
+
Name.args = {
|
|
45
|
+
name: 'session-quality',
|
|
46
|
+
}
|