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 */