feat(Toast): implement progress duration

Resolves #51
This commit is contained in:
Benjamin Canac
2024-04-11 15:40:37 +02:00
parent 7350e8e46b
commit d726e4ddac
5 changed files with 29 additions and 17 deletions

View File

@@ -2,7 +2,7 @@ export default defineAppConfig({
toaster: { toaster: {
position: 'bottom-right' as const, position: 'bottom-right' as const,
expand: true, expand: true,
duration: 60000 duration: 5000
}, },
ui: { ui: {
primary: 'sky', primary: 'sky',

View File

@@ -109,7 +109,7 @@ function removeToast () {
<template> <template>
<div class="flex flex-col items-center gap-8"> <div class="flex flex-col items-center gap-8">
<div> <div class="flex flex-col gap-2">
<URadioGroup v-model="appConfig.toaster.position" :options="positions" /> <URadioGroup v-model="appConfig.toaster.position" :options="positions" />
<UCheckbox v-model="appConfig.toaster.expand" label="Expand" class="mt-1" /> <UCheckbox v-model="appConfig.toaster.expand" label="Expand" class="mt-1" />
<UInput v-model="appConfig.toaster.duration" label="Duration" type="number" class="mt-1" /> <UInput v-model="appConfig.toaster.duration" label="Duration" type="number" class="mt-1" />

View File

@@ -5,7 +5,7 @@ import type { ToastRootProps, ToastRootEmits } from 'radix-vue'
import type { AppConfig } from '@nuxt/schema' import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config' import _appConfig from '#build/app.config'
import theme from '#build/ui/toast' import theme from '#build/ui/toast'
import type { AvatarProps, ButtonProps, IconProps } from '#ui/types' import type { AvatarProps, ButtonProps, IconProps, ToasterContext } from '#ui/types'
const appConfig = _appConfig as AppConfig & { ui: { toast: Partial<typeof theme> } } const appConfig = _appConfig as AppConfig & { ui: { toast: Partial<typeof theme> } }
@@ -19,17 +19,17 @@ export interface ToastProps extends Omit<ToastRootProps, 'asChild' | 'forceMount
icon?: IconProps['name'] icon?: IconProps['name']
avatar?: AvatarProps avatar?: AvatarProps
color?: ToastVariants['color'] color?: ToastVariants['color']
actions?: (ButtonProps & { click?: () => void })[] actions?: ButtonProps[]
close?: ButtonProps | null close?: ButtonProps | null
class?: any class?: any
ui?: Partial<typeof toast.slots> ui?: Partial<typeof toast.slots>
} }
export interface ToastEmits extends ToastRootEmits { } export interface ToastEmits extends ToastRootEmits {}
</script> </script>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, inject, onMounted } from 'vue'
import { ToastRoot, ToastTitle, ToastDescription, ToastAction, ToastClose, useForwardPropsEmits } from 'radix-vue' import { ToastRoot, ToastTitle, ToastDescription, ToastAction, ToastClose, useForwardPropsEmits } from 'radix-vue'
import { reactivePick } from '@vueuse/core' import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
@@ -38,14 +38,15 @@ import { UIcon, UAvatar } from '#components'
const props = defineProps<ToastProps>() const props = defineProps<ToastProps>()
const emits = defineEmits<ToastEmits>() const emits = defineEmits<ToastEmits>()
const toaster = inject<ToasterContext>('Toaster')
const appConfig = useAppConfig() const appConfig = useAppConfig()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'defaultOpen', 'duration', 'open', 'type'), emits) const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'defaultOpen', 'duration', 'open', 'type'), emits)
const multiline = computed(() => !!props.title && !!props.description) const multiline = computed(() => !!props.title && !!props.description)
const duration = computed(() => props.duration || toaster?.value.duration)
const ui = computed(() => tv({ extend: toast, slots: props.ui })({ const ui = computed(() => tv({ extend: toast, slots: props.ui })({ color: props.color }))
color: props.color
}))
const el = ref() const el = ref()
const height = ref(0) const height = ref(0)
@@ -56,7 +57,7 @@ onMounted(() => {
} }
setTimeout(() => { setTimeout(() => {
height.value = el.value.$el.getBoundingClientRect().height height.value = el.value.$el.getBoundingClientRect()?.height
}, 0) }, 0)
}) })
@@ -66,7 +67,13 @@ defineExpose({
</script> </script>
<template> <template>
<ToastRoot ref="el" v-bind="rootProps" :class="ui.root({ class: props.class, multiline })" :style="{ '--height': height }"> <ToastRoot
ref="el"
v-slot="{ remaining }"
v-bind="rootProps"
:class="ui.root({ class: props.class, multiline })"
:style="{ '--height': height }"
>
<UAvatar v-if="avatar" size="2xl" v-bind="avatar" :class="ui.avatar()" /> <UAvatar v-if="avatar" size="2xl" v-bind="avatar" :class="ui.avatar()" />
<UIcon v-else-if="icon" :name="icon" :class="ui.icon()" /> <UIcon v-else-if="icon" :name="icon" :class="ui.icon()" />
@@ -110,7 +117,6 @@ defineExpose({
</ToastClose> </ToastClose>
</div> </div>
<div :class="ui.progress()" /> <div v-if="remaining && duration" :class="ui.progress()" :style="{ width: `${remaining / duration * 100}%` }" />
<div :class="ui.mask()" />
</ToastRoot> </ToastRoot>
</template> </template>

View File

@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { ComputedRef } from 'vue'
import { tv, type VariantProps } from 'tailwind-variants' import { tv, type VariantProps } from 'tailwind-variants'
import type { ToastProviderProps } from 'radix-vue' import type { ToastProviderProps } from 'radix-vue'
import type { AppConfig } from '@nuxt/schema' import type { AppConfig } from '@nuxt/schema'
@@ -17,17 +18,21 @@ export interface ToasterProps extends Omit<ToastProviderProps, 'swipeDirection'>
class?: any class?: any
ui?: Partial<typeof toaster.slots> ui?: Partial<typeof toaster.slots>
} }
export type ToasterContext = ComputedRef<{
duration: number
}>
</script> </script>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed, provide } from 'vue'
import { ToastProvider, ToastViewport, useForwardProps } from 'radix-vue' import { ToastProvider, ToastViewport, useForwardProps } from 'radix-vue'
import { reactivePick } from '@vueuse/core' import { reactivePick } from '@vueuse/core'
import { useToast } from '#imports' import { useToast } from '#imports'
import { UToast } from '#components' import { UToast } from '#components'
import { omit } from '#ui/utils' import { omit } from '#ui/utils'
const props = withDefaults(defineProps<ToasterProps>(), { expand: true }) const props = withDefaults(defineProps<ToasterProps>(), { expand: true, duration: 5000 })
const providerProps = useForwardProps(reactivePick(props, 'duration', 'label', 'swipeThreshold')) const providerProps = useForwardProps(reactivePick(props, 'duration', 'label', 'swipeThreshold'))
@@ -73,6 +78,8 @@ const frontHeight = computed(() => refs.value[refs.value.length - 1]?.height ||
function getOffset (index: number) { function getOffset (index: number) {
return refs.value.slice(index + 1).reduce((acc, { height }) => acc + height + 16, 0) return refs.value.slice(index + 1).reduce((acc, { height }) => acc + height + 16, 0)
} }
provide<ToasterContext>('Toaster', providerProps)
</script> </script>
<template> <template>

View File

@@ -7,8 +7,7 @@ export default (config: { colors: string[] }) => ({
icon: 'shrink-0 size-5', icon: 'shrink-0 size-5',
avatar: 'shrink-0', avatar: 'shrink-0',
actions: 'flex gap-1.5 shrink-0', actions: 'flex gap-1.5 shrink-0',
progress: 'absolute inset-0 rounded-lg border-b-2 z-[-1]', progress: 'absolute inset-0 border-b-2 z-[-1]',
mask: 'absolute top-0 inset-0 bottom-[2px] bg-white dark:bg-gray-900 z-[-1]',
close: 'p-1' close: 'p-1'
}, },
variants: { variants: {