Skip to content

Commit

Permalink
feat(VaToast #4373): add bottom-center, top-center position options (#…
Browse files Browse the repository at this point in the history
…4377)

* feat(VaToast #4373): add bottom-center, top-center position options

* fix: correctly handle multiple toasts offset

* chore(toast): fix paddings

* chore: remove redundant

* fix(toast): improve appear and close animations

---------

Co-authored-by: Parsons <[email protected]>
Co-authored-by: Maksim Nedoshev <[email protected]>
  • Loading branch information
3 people authored Sep 23, 2024
1 parent 280534b commit 5b53670
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 90 deletions.
14 changes: 14 additions & 0 deletions packages/docs/page-config/ui-elements/toast/examples/Position.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
>
top-right
</VaButton>
<VaButton
class="mr-2 mb-2"
@click="$vaToast.init({ message: 'Top-center', position: 'top-center' })"
>
top-center
</VaButton>
<VaButton
class="mr-2 mb-2"
@click="$vaToast.init({ message: 'Top-left', position: 'top-left' })"
Expand All @@ -19,6 +25,14 @@
>
bottom-right
</VaButton>
<VaButton
class="mr-2 mb-2"
@click="
$vaToast.init({ message: 'Bottom-center', position: 'bottom-center' })
"
>
bottom-center
</VaButton>
<VaButton
class="mr-2 mb-2"
@click="$vaToast.init({ message: 'Bottom-left', position: 'bottom-left' })"
Expand Down
2 changes: 1 addition & 1 deletion packages/docs/page-config/ui-elements/toast/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export default definePageConfig({
}),
block.example("Position", {
title: "Position",
description: "Use `position` property to set the custom position of the toast. Available are `top-right`, `top-left`, `bottom-right`, `bottom-left`."
description: "Use `position` property to set the custom position of the toast. Available are `top-right`, `top-center`, `top-left`, `bottom-right`, `bottom-center`, `bottom-left`."
}),
block.example("Close", {
title: "Close",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/.stylelintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module.exports = {
'value-list-max-empty-lines': 1,
'function-calc-no-unspaced-operator': null,
'value-keyword-case': null,
'length-zero-no-unit': null,
'selector-pseudo-class-no-unknown': [
true,
{
Expand Down
30 changes: 29 additions & 1 deletion packages/ui/src/components/va-toast/VaToast.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,35 @@ export const Color: StoryFn = () => ({
`,
})

export const PositionTopenter: StoryFn = () => ({
export const PositionBottomLeft: StoryFn = () => ({
components: { VaToast: VaToast },

setup () {
const { notify } = useToast()

return {
notify,
}
},

template: '<button @click="notify({ message: \'Test\', position: \'bottom-left\' })">Show</button>',
})

export const PositionTopLeft: StoryFn = () => ({
components: { VaToast: VaToast },

setup () {
const { notify } = useToast()

return {
notify,
}
},

template: '<button @click="notify({ message: \'Test\', position: \'top-left\' })">Show</button>',
})

export const PositionTopCenter: StoryFn = () => ({
components: { VaToast: VaToast },

setup () {
Expand Down
83 changes: 50 additions & 33 deletions packages/ui/src/components/va-toast/VaToast.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<transition name="va-toast-fade">
<Transition name="va-toast-fade" @after-leave="onHidden">
<div
v-show="visible"
ref="rootElement"
Expand Down Expand Up @@ -38,7 +38,7 @@
/>
</div>
</div>
</transition>
</Transition>
</template>

<script lang="ts">
Expand All @@ -47,6 +47,7 @@ import { PropType, ref, computed, onMounted, shallowRef, defineComponent, Comput
import { useComponentPresetProp, useColors, useTimer, useTextColor, useTranslation, useTranslationProp, useNumericProp } from '../../composables'
import { ToastPosition } from './types'
import { useToastService } from './hooks/useToastService'
import { StringWithAutocomplete } from '../../utils/types/prop-type'
</script>
Expand Down Expand Up @@ -78,15 +79,15 @@ 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 },
multiLine: { type: Boolean, default: false },
position: {
type: String as PropType<ToastPosition>,
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'),
Expand All @@ -107,20 +108,30 @@ const durationComputed = useNumericProp('duration') as ComputedRef<number>
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`,
}
}
Expand All @@ -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(() => ({
Expand Down Expand Up @@ -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()
Expand All @@ -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);
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
}
</style>
6 changes: 1 addition & 5 deletions packages/ui/src/components/va-toast/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand Down
61 changes: 61 additions & 0 deletions packages/ui/src/components/va-toast/hooks/useToastService.ts
Original file line number Diff line number Diff line change
@@ -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<VNode[]>

type OptionKeys = keyof ToastOptions;

const getNodeProps = (vNode: VNode): Record<OptionKeys, any> => {
return (vNode.component?.props as Record<OptionKeys, any>) || {}
}

const getTranslateValue = (item: VNode) => {
if (item.el) {
return (item.el.offsetHeight + GAP)
}
return 0
}

export const useToastService = (props: {
position: NonNullable<ToastOptions['position']>,
}) => {
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)
},
}
}
Loading

0 comments on commit 5b53670

Please sign in to comment.