Loading src/Playground.vue +20 −2 Original line number Diff line number Diff line Loading @@ -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" /> Loading src/common/interfaces/componentsProps.ts +1 −1 Original line number Diff line number Diff line Loading @@ -298,7 +298,7 @@ export interface IToastProps { text?: string; header?: string; icon?: TIcon; position?: TExpandedPosition; position?: Exclude<TExpandedPosition, 'left' | 'right'>; width?: string; static?: boolean; } Loading src/components/Toast/Toast.stories.ts +3 −1 Original line number Diff line number Diff line Loading @@ -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' }, Loading Loading @@ -103,6 +103,8 @@ export const Small: Story = { active: true, size: 'small', static: true, theme: 'black', width: '400px', }, }; Loading src/components/Toast/Toast.vue +77 −14 Original line number Diff line number Diff line <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'; Loading Loading @@ -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( Loading Loading @@ -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" /> Loading @@ -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 { Loading @@ -144,7 +207,7 @@ if (props.duration) { width: 100%; height: 100%; opacity: 0.55; border-radius: 4px; border-radius: 6px; } } .toast-header { Loading Loading
src/Playground.vue +20 −2 Original line number Diff line number Diff line Loading @@ -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" /> Loading
src/common/interfaces/componentsProps.ts +1 −1 Original line number Diff line number Diff line Loading @@ -298,7 +298,7 @@ export interface IToastProps { text?: string; header?: string; icon?: TIcon; position?: TExpandedPosition; position?: Exclude<TExpandedPosition, 'left' | 'right'>; width?: string; static?: boolean; } Loading
src/components/Toast/Toast.stories.ts +3 −1 Original line number Diff line number Diff line Loading @@ -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' }, Loading Loading @@ -103,6 +103,8 @@ export const Small: Story = { active: true, size: 'small', static: true, theme: 'black', width: '400px', }, }; Loading
src/components/Toast/Toast.vue +77 −14 Original line number Diff line number Diff line <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'; Loading Loading @@ -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( Loading Loading @@ -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" /> Loading @@ -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 { Loading @@ -144,7 +207,7 @@ if (props.duration) { width: 100%; height: 100%; opacity: 0.55; border-radius: 4px; border-radius: 6px; } } .toast-header { Loading