diff --git a/src/Playground.vue b/src/Playground.vue index d50946fd8e8899a48feb5643b74d90eabd010bf3..b5f1a34830077e0824c33cd4d7893cd279379d98 100644 --- a/src/Playground.vue +++ b/src/Playground.vue @@ -22,6 +22,7 @@ import Rating from '@components/Rating/Rating.vue'; import HomeIcon from '@icons/Mono/HomeIcon.vue'; import ProgressBar from '@components/ProgressBar/ProgressBar.vue'; import Carousel from '@components/Carousel/Carousel.vue'; +import Toast from '@components/Toast/Toast.vue'; const visibleDrawer = ref(false); const sliderOptions: ISliderOptions[] = [ @@ -174,11 +175,14 @@ const selectOptions = [ ]; const knob = ref(0); const pbValue = ref(0); +const toast = 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" /> <Rating theme="red"> <template #offIcon> <CrossIcon color="red" /> diff --git a/src/common/helpers/common.ts b/src/common/helpers/common.ts index e7743e9cc2996154622e019f1860ded39733a92a..75df67ab604fa138e8c3092f104acf7ff2e676cd 100644 --- a/src/common/helpers/common.ts +++ b/src/common/helpers/common.ts @@ -59,7 +59,7 @@ export const convertThemeToSecondaryColor = (theme: TThemeColor, darkness: TDark : convertThemeToColor(theme, String(100 + ((+darkness + 600) % 900))); }; -export const getValueFromSize = (size: TSize, options: string[] | number[]) => { +export const getValueFromSize = <T>(size: TSize, options: T[]): T => { if (size === 'normal') return options[1]; if (size === 'large') return options[2]; if (size === 'huge') return options[3]; diff --git a/src/common/interfaces/componentsProps.ts b/src/common/interfaces/componentsProps.ts index d36204b12127c94466d2c4db99666dbd66042a8b..05ef5a26e593555f247dca09bf7a976a6807320f 100644 --- a/src/common/interfaces/componentsProps.ts +++ b/src/common/interfaces/componentsProps.ts @@ -290,14 +290,16 @@ export interface ITSProps { } export interface IToastProps { + active?: boolean; + duration?: number | false; type?: TToastType; theme?: TThemeColor; size?: TSize; text?: string; header?: string; icon?: TIcon; - width?: string; position?: TExpandedPosition; + width?: string; static?: boolean; } diff --git a/src/components/Toast/Toast.stories.ts b/src/components/Toast/Toast.stories.ts index df07b91b6e67ced01547017b6c81b81e0219a5cc..7ec13915f8f3ab4d2c59d749f6a16089c40af88d 100644 --- a/src/components/Toast/Toast.stories.ts +++ b/src/components/Toast/Toast.stories.ts @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from '@storybook/vue3'; import Toast from './Toast.vue'; +import Button from '@components/Button/Button.vue'; import { iconsSet } from '@/common/constants/icons'; const meta: Meta = { @@ -17,6 +18,12 @@ const meta: Meta = { argTypes: { type: { control: 'select', options: ['success', 'info', 'warn', 'error'] }, size: { control: 'select', options: ['small', 'normal', 'large', 'huge'] }, + position: { + control: 'select', + options: ['topRight', 'bottomRight', 'bottomLeft', 'topLeft', 'top', 'right', 'bottom', 'left'], + }, + active: { control: 'boolean' }, + static: { control: 'boolean' }, width: { control: 'text' }, header: { control: 'text' }, text: { control: 'text' }, @@ -53,12 +60,25 @@ export const Simple: Story = { args: { active: true, }, + render: (args) => ({ + components: { Toast, Button }, + data() { + return { + active: false, + }; + }, + setup() { + return { args }; + }, + template: '<Toast v-model="active" v-bind="args" /><Button @click="active = true" label="Open toast"/>', + }), }; export const Info: Story = { args: { active: true, type: 'info', + static: true, }, }; @@ -66,6 +86,7 @@ export const Warn: Story = { args: { active: true, type: 'warn', + static: true, }, }; @@ -73,6 +94,7 @@ export const Error: Story = { args: { active: true, type: 'error', + static: true, }, }; @@ -80,6 +102,7 @@ export const Small: Story = { args: { active: true, size: 'small', + static: true, }, }; @@ -91,6 +114,7 @@ export const Large: Story = { text: 'This is a text of large toast!', icon: 'Award', theme: 'sky', + static: true, }, }; @@ -103,5 +127,6 @@ export const Huge: Story = { icon: 'Badge', theme: 'purple', header: 'Custom header', + static: true, }, }; diff --git a/src/components/Toast/Toast.vue b/src/components/Toast/Toast.vue index 389cd82071ee86b305070eba3032c291aee26c22..1d8a8661f5862bced1d6c15f14f1fa072d1a55b4 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, ref } from 'vue'; +import { computed, watch } from 'vue'; import { convertThemeToColor, getValueFromSize } from '@helpers/common'; import type { TToastType } from '@interfaces/componentsProp'; import { iconsSet } from '@/common/constants/icons'; @@ -9,13 +9,13 @@ import type { TThemeColor } from '@interfaces/common'; const props = withDefaults(defineProps<IToastProps>(), { type: 'success', - text: 'This is a toast about success.', size: 'normal', width: '300px', position: 'topRight', + duration: 5, }); -const typeToTheme: Record<TToastType, string> = { +const typeToTheme: Record<TToastType, TThemeColor> = { success: 'green', info: 'blue', warn: 'yellow', @@ -36,7 +36,7 @@ const typeToIcon: Record<TToastType, string> = { error: 'CrossRound', }; -const active = ref<boolean>(false); +const active = defineModel<boolean>(); const themeColor = computed<TThemeColor>(() => props.theme ?? typeToTheme[props.type]); const header = computed<string>(() => props.header ?? typeToHeader[props.type]); @@ -52,7 +52,9 @@ const backgroundColor = computed(() => ); const borderColor = computed(() => convertThemeToColor(themeColor.value, '500')); const fontSize = computed(() => getValueFromSize(props.size, ['12px', '16px', '20px', '24px'])); -const padding = computed(() => getValueFromSize(props.size, ['7px 10px', '10px 15px', '14px 20px', '20px 30px'])); +const padding = computed( + () => getValueFromSize(props.size, ['7px 10px', '10px 15px', '14px 20px', '20px 30px']) as string, +); const gap = computed(() => props.size === 'normal' ? '10px' : props.size === 'large' || props.size === 'huge' ? '15px' : '5px', ); @@ -62,38 +64,58 @@ const textMargin = computed(() => { }); const positionParts = computed(() => { const result = []; + if (props.position.length < 7) return [props.position]; + const position = props.position.toLowerCase(); if (position.includes('top')) result.push('top'); if (position.includes('bottom')) result.push('bottom'); if (position.includes('left')) result.push('left'); if (position.includes('right')) result.push('right'); + return result; }); +const styles = computed(() => { + if (props.static) return ''; + if (positionParts.value.length === 1) { + const position = positionParts.value[0]; + if (position === 'left' || position === 'right') + return `${position}: -100%; top: 50%; transform: translateY(-50%);`; + return `${position}: -100%; left: 50%; transform: translateX(-50%);`; + } + return `${positionParts.value[0]}: -100%; ${positionParts.value[1]}: 20px`; +}); const activeStyles = computed(() => { - let result = ''; - if (positionParts.value[0] === 'top') result += 'top: 0;'; - if (positionParts.value[0] === 'bottom') result += 'bottom: 0;'; - if (positionParts.value[1] === 'left') result += 'left: 0;'; - if (positionParts.value[1] === 'right') result += 'right: 0;'; - return result; + if (positionParts.value.length === 1) return `${positionParts.value[0]}: 20px`; + return `${positionParts.value[0]}: 20px; ${positionParts.value[1]}: 20px`; }); -const closeToast = () => {}; +const closeToast = () => (active.value = false); + +let timeout: number; + +if (props.duration) { + watch(active, () => { + if (active.value) { + timeout = setTimeout(() => (active.value = false), (props.duration as number) * 1000); + } else { + clearTimeout(timeout); + } + }); +} </script> <template> <section class="toast-container" - :style="`position: ${static ? 'relative' : 'absolute'}; - ${positionParts[0]}: ${positionParts[0] === 'top' ? '-100%' : '100%'}; - ${positionParts[1]}: ${positionParts[1] === 'left' ? '-100%' : '100%'}; + :style="`position: ${static ? 'relative' : 'fixed'}; + ${styles}; ${active ? activeStyles : null}`" > <h3 class="toast-header" :style="`font-size: calc(${fontSize} + 4px)`"> <component :is="iconsSet[icon]" :color="color" :size="iconSize" /> <span class="toast-header-text">{{ header }}</span> </h3> - <p class="toast-text">{{ text }}</p> + <p class="toast-text">{{ text ?? `This is a toast about ${type}` }}</p> <CrossIcon @click="closeToast" class="toast-close_button" @@ -106,10 +128,12 @@ const closeToast = () => {}; <style scoped> .toast-container { + z-index: 9999; padding: v-bind(padding); border: 1px solid v-bind(borderColor); border-radius: 5px; width: v-bind(width); + transition: all 0.4s ease-in-out; ::before { content: ''; position: absolute;