diff --git a/packages/docs/src/examples/v-alert/prop-duration.vue b/packages/docs/src/examples/v-alert/prop-duration.vue new file mode 100644 index 00000000000..4d7ee8f6d50 --- /dev/null +++ b/packages/docs/src/examples/v-alert/prop-duration.vue @@ -0,0 +1,25 @@ + + + diff --git a/packages/docs/src/pages/en/components/alerts.md b/packages/docs/src/pages/en/components/alerts.md index 97701b717a5..7959420d393 100644 --- a/packages/docs/src/pages/en/components/alerts.md +++ b/packages/docs/src/pages/en/components/alerts.md @@ -131,6 +131,12 @@ The **closable** prop adds a [v-icon](/components/icons) on the far right, after +#### Duration + +The **duration** prop causes the alert to automatically dismiss itself after the specified number of milliseconds. Set to `-1` (the default) to disable auto-dismiss. Combine with **closable** to give users both auto-dismiss and manual control. + + + The close icon automatically applies a default `aria-label` and is configurable by using the **close-label** prop or changing **close** value in your locale. ::: info diff --git a/packages/vuetify/src/components/VAlert/VAlert.tsx b/packages/vuetify/src/components/VAlert/VAlert.tsx index 42672ddc0e0..b30684c40d7 100644 --- a/packages/vuetify/src/components/VAlert/VAlert.tsx +++ b/packages/vuetify/src/components/VAlert/VAlert.tsx @@ -25,7 +25,7 @@ import { makeThemeProps, provideTheme } from '@/composables/theme' import { genOverlays, makeVariantProps, useVariant } from '@/composables/variant' // Utilities -import { toRef } from 'vue' +import { onMounted, onScopeDispose, toRef, watch } from 'vue' import { genericComponent, propsFactory } from '@/util' // Types @@ -57,6 +57,10 @@ export const makeVAlertProps = propsFactory({ type: String, default: '$vuetify.close', }, + duration: { + type: [Number, String], + default: -1, + }, icon: { type: [Boolean, String, Function, Object] as PropType, default: null, @@ -107,6 +111,28 @@ export const VAlert = genericComponent()({ setup (props, { emit, slots }) { const isActive = useProxiedModel(props, 'modelValue') + + let dismissTimer = -1 + function clearDismissTimer () { + if (dismissTimer !== -1) { + window.clearTimeout(dismissTimer) + dismissTimer = -1 + } + } + function scheduleDismiss () { + clearDismissTimer() + const ms = Number(props.duration) + if (ms > 0 && isActive.value) { + dismissTimer = window.setTimeout(() => { + isActive.value = false + dismissTimer = -1 + }, ms) + } + } + onMounted(scheduleDismiss) + watch(() => [props.duration, isActive.value], scheduleDismiss) + onScopeDispose(clearDismissTimer) + const icon = toRef(() => { if (props.icon === false) return undefined if (!props.type) return props.icon diff --git a/packages/vuetify/src/components/VList/VListItem.tsx b/packages/vuetify/src/components/VList/VListItem.tsx index 1c68df64dc6..bf59237c55b 100644 --- a/packages/vuetify/src/components/VList/VListItem.tsx +++ b/packages/vuetify/src/components/VList/VListItem.tsx @@ -263,6 +263,8 @@ export const VListItem = genericComponent()({ } } + const clickHandler = computed(() => (isClickable.value || attrs.onClick) ? onClick : undefined) + useRender(() => { const Tag = isLink.value ? 'a' : props.tag const hasTitle = (slots.title || props.title != null) @@ -317,7 +319,7 @@ export const VListItem = genericComponent()({ tabindex={ props.tabindex ?? (isClickable.value ? (list ? -2 : 0) : undefined) } aria-selected={ ariaSelected.value } role={ role.value } - onClick={ onClick } + onClick={ clickHandler.value } onKeydown={ isClickable.value && !isLink.value && onKeyDown } v-ripple={ isClickable.value && rippleOptions.value } >