diff --git a/src/Playground.vue b/src/Playground.vue index b5f1a34830077e0824c33cd4d7893cd279379d98..1135975bcdd5d258a6fcb9bdc213036be5f94a40 100644 --- a/src/Playground.vue +++ b/src/Playground.vue @@ -176,13 +176,31 @@ const selectOptions = [ const knob = ref(0); const pbValue = ref(0); const toast = ref(false); +const toast2 = ref(false); +const toast3 = ref(false); +const toast4 = ref(false); const openDrawer = () => (visibleDrawer.value = true); </script> <template> <h2 class="title gradient-text">Playground</h2> - <Button label="Open toast" @click="toast = true" /> - <Toast v-model="toast" type="info" position="bottomRight" width="500px" /> + <Button + theme="black" + label="Open all toasts" + @click=" + () => { + toast = true; + toast2 = true; + toast3 = true; + toast4 = true; + } + " + /> + <Button label="Open toast" @click="toast2 = true" /> + <Toast v-model="toast" type="success" position="top" width="500px" /> + <Toast v-model="toast4" type="info" position="top" width="500px" /> + <Toast v-model="toast3" type="warn" position="top" width="500px" /> + <Toast v-model="toast2" type="error" position="top" width="500px" /> <Rating theme="red"> <template #offIcon> <CrossIcon color="red" /> diff --git a/src/common/interfaces/componentsProps.ts b/src/common/interfaces/componentsProps.ts index 05ef5a26e593555f247dca09bf7a976a6807320f..4911b32f07621935b34899a1dbe33411c1854e64 100644 --- a/src/common/interfaces/componentsProps.ts +++ b/src/common/interfaces/componentsProps.ts @@ -298,7 +298,7 @@ export interface IToastProps { text?: string; header?: string; icon?: TIcon; - position?: TExpandedPosition; + position?: Exclude<TExpandedPosition, 'left' | 'right'>; width?: string; static?: boolean; } diff --git a/src/components/Toast/Toast.stories.ts b/src/components/Toast/Toast.stories.ts index 7ec13915f8f3ab4d2c59d749f6a16089c40af88d..b660b72147e99be5189fc7f063269c9f1d45414d 100644 --- a/src/components/Toast/Toast.stories.ts +++ b/src/components/Toast/Toast.stories.ts @@ -20,7 +20,7 @@ const meta: Meta = { size: { control: 'select', options: ['small', 'normal', 'large', 'huge'] }, position: { control: 'select', - options: ['topRight', 'bottomRight', 'bottomLeft', 'topLeft', 'top', 'right', 'bottom', 'left'], + options: ['topRight', 'bottomRight', 'bottomLeft', 'topLeft', 'top', 'bottom'], }, active: { control: 'boolean' }, static: { control: 'boolean' }, @@ -103,6 +103,8 @@ export const Small: Story = { active: true, size: 'small', static: true, + theme: 'black', + width: '400px', }, }; diff --git a/src/components/Toast/Toast.vue b/src/components/Toast/Toast.vue index 1d8a8661f5862bced1d6c15f14f1fa072d1a55b4..35440a45d71a2740d4efb511c0de1094a7db5654 100644 --- a/src/components/Toast/Toast.vue +++ b/src/components/Toast/Toast.vue @@ -1,6 +1,6 @@ <script setup lang="ts"> import type { IToastProps } from '@interfaces/componentsProps'; -import { computed, watch } from 'vue'; +import { computed, onBeforeUnmount, onMounted, ref, type Ref, watch } from 'vue'; import { convertThemeToColor, getValueFromSize } from '@helpers/common'; import type { TToastType } from '@interfaces/componentsProp'; import { iconsSet } from '@/common/constants/icons'; @@ -36,20 +36,61 @@ const typeToIcon: Record<TToastType, string> = { error: 'CrossRound', }; -const active = defineModel<boolean>(); +const active = defineModel() as Ref<boolean>; + +let toastsContainer = document.querySelector(`.toasts-container.${props.position}`); +if (!toastsContainer) { + toastsContainer = document.createElement('div'); + toastsContainer.classList.add('toasts-container'); + toastsContainer.classList.add(`${props.position}`); + document.body.appendChild(toastsContainer); +} + +const toast = ref(); +watch(toast, () => { + if (toast.value) { + toastsContainer?.appendChild(toast.value); + } +}); +const activeToastsCount = ref(0); +let observer = null; + +const updateCount = () => { + activeToastsCount.value = document.querySelectorAll(`.toast-container.${props.position}.active`).length; +}; +const initObserver = () => { + const config = { attributeOldValue: true, attributes: true, characterData: true, childList: true, subtree: true }; + observer = new MutationObserver(() => { + console.log('update type: ', props.type); + updateCount(); + }); + observer.observe(toastsContainer, config); +}; + +onMounted(() => { + updateCount(); + initObserver(); +}); +onBeforeUnmount(() => { + if (observer) { + observer.disconnect(); + } +}); const themeColor = computed<TThemeColor>(() => props.theme ?? typeToTheme[props.type]); const header = computed<string>(() => props.header ?? typeToHeader[props.type]); const icon = computed(() => props.icon ?? typeToIcon[props.type]); -const color = computed(() => convertThemeToColor(themeColor.value, '400')); -const whiteOrBlack = computed(() => (themeColor.value === 'white' ? 'black' : 'white')); -const backgroundColor = computed(() => +const color = computed(() => convertThemeToColor( themeColor.value === 'white' ? 'black' : themeColor.value === 'black' ? 'white' : themeColor.value, - '800', + '400', ), ); +const whiteOrBlack = computed(() => (themeColor.value === 'white' ? 'black' : 'white')); +const backgroundColor = computed(() => + convertThemeToColor(themeColor.value, themeColor.value === 'white' || themeColor.value === 'black' ? '500' : '800'), +); const borderColor = computed(() => convertThemeToColor(themeColor.value, '500')); const fontSize = computed(() => getValueFromSize(props.size, ['12px', '16px', '20px', '24px'])); const padding = computed( @@ -84,32 +125,54 @@ const styles = computed(() => { } return `${positionParts.value[0]}: -100%; ${positionParts.value[1]}: 20px`; }); -const activeStyles = computed(() => { - if (positionParts.value.length === 1) return `${positionParts.value[0]}: 20px`; - return `${positionParts.value[0]}: 20px; ${positionParts.value[1]}: 20px`; -}); +const calcActiveStyles = () => { + const activeToasts = document.querySelectorAll(`.toast-container.${props.position}.active`); + let activeToastsHeight = 0; + for (const toast of activeToasts) { + activeToastsHeight += toast.offsetHeight; + } + + const offset = activeToastsHeight + 20 * activeToasts.length + 20 + 'px'; + console.log('activeToasts: ', `${positionParts.value[0]}: ${offset}`); + + if (positionParts.value.length === 1) return `${positionParts.value[0]}: ${offset}`; + return `${positionParts.value[0]}: ${offset}; ${positionParts.value[1]}: 20px`; +}; const closeToast = () => (active.value = false); let timeout: number; +const key = Math.random(); if (props.duration) { watch(active, () => { if (active.value) { timeout = setTimeout(() => (active.value = false), (props.duration as number) * 1000); + toastsContainer?.setAttribute('key', String(key)); } else { clearTimeout(timeout); + toastsContainer?.removeAttribute('key'); } }); } +watch(activeToastsCount, () => { + console.log(activeToastsCount.value); + calcActiveStyles(); +}); </script> <template> <section - class="toast-container" + ref="toast" + :class="[ + `toast-container ${position}`, + { + active, + }, + ]" :style="`position: ${static ? 'relative' : 'fixed'}; ${styles}; - ${active ? activeStyles : null}`" + ${active ? calcActiveStyles() : null}`" > <h3 class="toast-header" :style="`font-size: calc(${fontSize} + 4px)`"> <component :is="iconsSet[icon]" :color="color" :size="iconSize" /> @@ -131,7 +194,7 @@ if (props.duration) { z-index: 9999; padding: v-bind(padding); border: 1px solid v-bind(borderColor); - border-radius: 5px; + border-radius: 7px; width: v-bind(width); transition: all 0.4s ease-in-out; ::before { @@ -144,7 +207,7 @@ if (props.duration) { width: 100%; height: 100%; opacity: 0.55; - border-radius: 4px; + border-radius: 6px; } } .toast-header {