diff --git a/packages/docs/page-config/ui-elements/toast/examples/Position.vue b/packages/docs/page-config/ui-elements/toast/examples/Position.vue
index 06f807d7e2..bb9112a237 100644
--- a/packages/docs/page-config/ui-elements/toast/examples/Position.vue
+++ b/packages/docs/page-config/ui-elements/toast/examples/Position.vue
@@ -5,6 +5,12 @@
>
top-right
+
+ top-center
+
bottom-right
+
+ bottom-center
+
({
`,
})
-export const PositionTopenter: StoryFn = () => ({
+export const PositionBottomLeft: StoryFn = () => ({
+ components: { VaToast: VaToast },
+
+ setup () {
+ const { notify } = useToast()
+
+ return {
+ notify,
+ }
+ },
+
+ template: '',
+})
+
+export const PositionTopLeft: StoryFn = () => ({
+ components: { VaToast: VaToast },
+
+ setup () {
+ const { notify } = useToast()
+
+ return {
+ notify,
+ }
+ },
+
+ template: '',
+})
+
+export const PositionTopCenter: StoryFn = () => ({
components: { VaToast: VaToast },
setup () {
diff --git a/packages/ui/src/components/va-toast/VaToast.vue b/packages/ui/src/components/va-toast/VaToast.vue
index 842ce57270..9ae801173b 100644
--- a/packages/ui/src/components/va-toast/VaToast.vue
+++ b/packages/ui/src/components/va-toast/VaToast.vue
@@ -1,5 +1,5 @@
-
+
-
+
@@ -78,7 +79,7 @@ const props = defineProps({
icon: { type: String, default: 'close' },
customClass: { type: String, default: '' },
duration: { type: [Number, String], default: 5000 },
- color: { type: String, default: '' },
+ color: { type: String, default: 'primary' },
closeable: { type: Boolean, default: true },
onClose: { type: Function },
onClick: { type: Function },
@@ -86,7 +87,7 @@ const props = defineProps({
position: {
type: String as PropType,
default: 'top-right',
- validator: (value: string) => ['top-right', 'top-left', 'bottom-right', 'bottom-left'].includes(value),
+ validator: (value: string) => ['top-right', 'top-center', 'top-left', 'bottom-right', 'bottom-center', 'bottom-left'].includes(value),
},
render: { type: Function },
ariaCloseLabel: useTranslationProp('$t:close'),
@@ -107,20 +108,30 @@ const durationComputed = useNumericProp('duration') as ComputedRef
const visible = ref(false)
+const {
+ yOffset,
+ updateYOffset,
+} = useToastService(props)
+
+const positionObject = computed(() => ({
+ vertical: props.position.includes('top') ? 'top' : 'bottom',
+ horizontal: props.position.includes('center') ? 'center' : props.position.includes('right') ? 'right' : 'left',
+}))
+
const getPositionStyle = () => {
- const vertical = props.position.includes('top') ? 'top' : 'bottom'
- const horizontal = props.position.includes('center') ? 'center' : props.position.includes('right') ? 'right' : 'left'
+ const vertical = positionObject.value.vertical
+ const horizontal = positionObject.value.horizontal
if (horizontal === 'center') {
return {
- [vertical]: `${offsetYComputed.value}px`,
+ [vertical]: `${offsetYComputed.value + yOffset.value}px`,
left: '50%',
- transform: 'translateX(-50%)',
+ '--va-toast-x-shift': '-50%',
}
}
return {
- [vertical]: `${offsetYComputed.value}px`,
+ [vertical]: `${offsetYComputed.value + yOffset.value}px`,
[horizontal]: `${offsetXComputed.value}px`,
}
}
@@ -129,6 +140,7 @@ const toastClasses = computed(() => [
props.customClass,
props.multiLine ? 'va-toast--multiline' : '',
props.inline ? 'va-toast--inline' : '',
+ [`va-toast--${props.position}`],
])
const toastStyles = computed(() => ({
@@ -163,14 +175,16 @@ const onToastClick = () => {
const onToastClose = () => {
visible.value = false
+ updateYOffset()
+}
- rootElement.value?.addEventListener('transitionend', destroyElement)
-
+const onHidden = () => {
if (typeof props.onClose === 'function') {
props.onClose()
} else {
emit('on-close')
}
+ destroyElement()
}
const timer = useTimer()
@@ -193,6 +207,10 @@ onMounted(() => {
@import "variables";
.va-toast {
+ --va-toast-x-shift: 0px;
+ --va-toast-animation-x-shift: 0px;
+ --va-toast-animation-y-shift: 100%;
+
position: fixed;
box-sizing: border-box;
width: var(--va-toast-width);
@@ -207,26 +225,30 @@ onMounted(() => {
overflow: hidden;
z-index: var(--va-toast-z-index);
font-family: var(--va-font-family);
+ transform: translateX(var(--va-toast-x-shift));
- &--inline {
- position: static;
+ &--top-right,
+ &--bottom-right {
+ --va-toast-animation-x-shift: 100%;
}
- &--multiline {
- min-height: 70px;
+ &--top-left,
+ &--bottom-left {
+ --va-toast-animation-x-shift: -100%;
}
- &--right {
- right: 16px;
+ &--top-left,
+ &--top-center,
+ &--top-right {
+ --va-toast-animation-y-shift: -100%;
}
- &--left {
- left: 16px;
+ &--inline {
+ position: static;
}
- &__group {
- margin-left: var(--va-toast-group-margin-left);
- margin-right: var(--va-toast-group-margin-right);
+ &--multiline {
+ min-height: 70px;
}
&__title {
@@ -269,19 +291,14 @@ onMounted(() => {
}
}
-.va-toast-fade-enter {
- &.right {
- right: 0;
- transform: translateX(100%);
+.va-toast-fade {
+ &-enter-from {
+ transform: translateX(calc(var(--va-toast-animation-x-shift) + var(--va-toast-x-shift)));
}
- &.left {
- left: 0;
- transform: translateX(-100%);
+ &-leave-to {
+ transform: translateY(var(--va-toast-animation-y-shift));
+ opacity: 0;
}
}
-
-.va-toast-fade-leave-active {
- opacity: 0;
-}
diff --git a/packages/ui/src/components/va-toast/_variables.scss b/packages/ui/src/components/va-toast/_variables.scss
index d0e62236f5..d186b9835e 100644
--- a/packages/ui/src/components/va-toast/_variables.scss
+++ b/packages/ui/src/components/va-toast/_variables.scss
@@ -2,7 +2,7 @@
:host {
--va-toast-display: flex;
--va-toast-width: 330px;
- --va-toast-padding: 14px 26px 14px 13px;
+ --va-toast-padding: 14px 1.25rem 14px 1.25rem;
--va-toast-border-radius: 8px;
--va-toast-border-color: transparent;
--va-toast-border: 1px solid var(--va-toast-border-color);
@@ -11,10 +11,6 @@
--va-toast-transition: opacity 0.3s, transform 0.3s, left 0.3s, right 0.3s, top 0.4s, bottom 0.3s;
--va-toast-z-index: calc(var(--va-z-index-teleport-overlay) + 100);
- /* Group */
- --va-toast-group-margin-left: 13px;
- --va-toast-group-margin-right: 8px;
-
/* Title */
--va-toast-title-font-weight: bold;
--va-toast-title-font-size: 1rem;
diff --git a/packages/ui/src/components/va-toast/hooks/useToastService.ts b/packages/ui/src/components/va-toast/hooks/useToastService.ts
new file mode 100644
index 0000000000..bf2f4a4da2
--- /dev/null
+++ b/packages/ui/src/components/va-toast/hooks/useToastService.ts
@@ -0,0 +1,61 @@
+import { computed, getCurrentInstance, onBeforeUnmount, onMounted, Ref, ref, VNode } from 'vue'
+import { ToastOptions } from '../types'
+
+const GAP = 5
+
+// Expect as client-side used only
+const toastInstances = ref([]) as Ref
+
+type OptionKeys = keyof ToastOptions;
+
+const getNodeProps = (vNode: VNode): Record => {
+ return (vNode.component?.props as Record) || {}
+}
+
+const getTranslateValue = (item: VNode) => {
+ if (item.el) {
+ return (item.el.offsetHeight + GAP)
+ }
+ return 0
+}
+
+export const useToastService = (props: {
+ position: NonNullable,
+}) => {
+ const currentInstance = getCurrentInstance()!
+
+ const yOffset = computed(() => {
+ const currentIndex = toastInstances.value.findIndex((instance) => instance === currentInstance.vnode)
+
+ if (currentIndex === -1) { return 0 }
+
+ return toastInstances.value.slice(currentIndex + 1).reduce((acc, instance) => {
+ const {
+ position: itemPosition,
+ } = getNodeProps(instance)
+
+ const { position } = props
+
+ if (position === itemPosition) {
+ return getTranslateValue(instance) + acc
+ }
+
+ return acc
+ }, 0)
+ })
+
+ onMounted(() => {
+ toastInstances.value.unshift(currentInstance.vnode)
+ })
+
+ onBeforeUnmount(() => {
+ toastInstances.value = toastInstances.value.filter((item) => item !== currentInstance.vnode)
+ })
+
+ return {
+ yOffset,
+ updateYOffset: () => {
+ toastInstances.value = toastInstances.value.filter((item) => item !== currentInstance.vnode)
+ },
+ }
+}
diff --git a/packages/ui/src/components/va-toast/toast.ts b/packages/ui/src/components/va-toast/toast.ts
index 6dfd320f0e..57d09d83ab 100644
--- a/packages/ui/src/components/va-toast/toast.ts
+++ b/packages/ui/src/components/va-toast/toast.ts
@@ -8,7 +8,6 @@ import { withConfigTransport } from '../../services/config-transport'
export const VaToast = withConfigTransport(_VaToast)
-const GAP = 5
let seed = 1
declare global {
@@ -23,19 +22,6 @@ type OptionKeys = keyof ToastOptions;
export type VaToastId = string
-const getTranslateValue = (item: VNode, position: string) => {
- if (item.el) {
- const direction = position.includes('bottom') ? -1 : 1
- return (item.el.offsetHeight + GAP) * direction
- }
- return 0
-}
-
-const getNewTranslateValue = (transformY: string, redundantHeight: number, position: string) => {
- const direction = position.includes('bottom') ? -1 : 1
- return parseInt(transformY, 10) - (redundantHeight + GAP) * direction
-}
-
const getNodeProps = (vNode: VNode): Record => {
return (vNode.component?.props as Record) || {}
}
@@ -52,30 +38,13 @@ const closeNotification = (targetInstance: VNode | null, destroyElementFn: () =>
if (targetInstanceIndex < 0) { return }
- const nodeProps = getNodeProps(targetInstance)
-
- const {
- offsetX: targetOffsetX,
- offsetY: targetOffsetY,
- position: targetPosition,
- } = nodeProps
- const redundantHeight: number | null = targetInstance.el?.offsetHeight
-
destroyElementFn()
getGlobal().vaToastInstances = getGlobal().vaToastInstances.reduce((acc: any[], instance, index) => {
if (instance === targetInstance) {
return acc
}
- if (instance.component) {
- const { offsetX, offsetY, position } = getNodeProps(instance)
- const isNextInstance = index > targetInstanceIndex && targetOffsetX === offsetX && targetOffsetY === offsetY && targetPosition === position
- if (isNextInstance && instance.el && redundantHeight) {
- const [_, transformY] = instance.el.style.transform.match(/[\d-]+(?=px)/g)
- const transformYNew = getNewTranslateValue(transformY, redundantHeight, position)
- instance.el.style.transform = `translate(0, ${transformYNew}px)`
- }
- }
+
return [...acc, instance]
}, [])
@@ -163,25 +132,9 @@ export const createToastInstance = (customProps: ToastOptions | string, appConte
if (el && vNode.el && nodeProps) {
document.body.appendChild(el.childNodes[0])
- const { offsetX, offsetY, position } = nodeProps
- vNode.el.style.display = 'flex'
vNode.el.id = 'notification_' + seed
- let transformY = 0
- getGlobal().vaToastInstances.filter(item => {
- const {
- offsetX: itemOffsetX,
- offsetY: itemOffsetY,
- position: itemPosition,
- } = getNodeProps(item)
-
- return itemOffsetX === offsetX && itemOffsetY === offsetY && position === itemPosition
- }).forEach((item) => {
- transformY += getTranslateValue(item, position)
- })
- vNode.el.style.transform = `translate(0, ${transformY}px)`
-
seed += 1
getGlobal().vaToastInstances.push(vNode)
diff --git a/packages/ui/src/components/va-toast/types.ts b/packages/ui/src/components/va-toast/types.ts
index d0c285db8f..d19ec61413 100644
--- a/packages/ui/src/components/va-toast/types.ts
+++ b/packages/ui/src/components/va-toast/types.ts
@@ -2,11 +2,11 @@ import { VNode } from 'vue'
export type ToastPosition =
'top-right'
+ | 'top-center'
| 'top-left'
| 'bottom-right'
- | 'bottom-left'
- | 'top-center'
| 'bottom-center'
+ | 'bottom-left'
export interface ToastOptions {
/** Title */