-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(AlertDialog): New component (#8)
- Loading branch information
Showing
8 changed files
with
257 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
<script setup lang="ts"> | ||
import { UiAlertDialog, UiContainer } from '#components'; | ||
const wait = () => new Promise((resolve) => setTimeout(resolve, 2000)); | ||
async function onConfirm() { | ||
console.log('Starting action'); | ||
await wait(); | ||
console.log('Completed action!'); | ||
} | ||
</script> | ||
|
||
<template> | ||
<UiContainer class="py-8"> | ||
<h1 class="demo-page-title">Alert Dialog</h1> | ||
<p class="demo-page-description"> | ||
A modal dialog that interrupts the user with important content and expects a response. | ||
</p> | ||
|
||
<div class="demo-category-container mt-4 items-start"> | ||
<span class="demo-category-title">Demo</span> | ||
|
||
<UiAlertDialog | ||
variant="danger" | ||
title="Confirm delete" | ||
description="Do you really want to delete this item? This action cannot be undone" | ||
:confirm-btn="{ label: 'Confirm', action: onConfirm, variant: 'black-solid' }" | ||
:cancel-btn="{ label: 'Nevermind', variant: 'black-ghost' }" | ||
> | ||
<template #trigger> | ||
<UiButton label="Delete item" class="mt-2" /> | ||
</template> | ||
</UiAlertDialog> | ||
</div> | ||
</UiContainer> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
<script setup lang="ts"> | ||
// @ts-expect-error | ||
import appConfig from '#build/app.config'; | ||
import UiButton from '#ui/components/elements/Button.vue'; | ||
import { useUI } from '#ui/composables/useUI'; | ||
import type { AlertDialogProps, Strategy, UiOverlayEmits } from '#ui/types'; | ||
import { alertDialog } from '#ui/ui.config'; | ||
import { mergeConfig } from '#ui/utils'; | ||
import { uiToTransitionProps } from '#ui/utils/transitions'; | ||
import { usePreferredReducedMotion, useVModel } from '@vueuse/core'; | ||
import { AlertDialog } from 'radix-vue/namespaced'; | ||
import { computed, defineOptions, ref, toRef, withDefaults } from 'vue'; | ||
const config = mergeConfig<typeof alertDialog>( | ||
appConfig.ui?.alertDialog?.strategy, | ||
appConfig.ui?.alertDialog, | ||
alertDialog, | ||
); | ||
type UiConfig = Partial<typeof config> & { strategy?: Strategy }; | ||
defineOptions({ inheritAttrs: false }); | ||
const props = withDefaults(defineProps<AlertDialogProps<UiConfig>>(), { | ||
open: undefined, | ||
ui: () => ({}) as UiConfig, | ||
}); | ||
const emits = defineEmits<{ (e: 'update:open', value: boolean): void } & UiOverlayEmits>(); | ||
const $open = useVModel(props, 'open', emits, { | ||
defaultValue: props.defaultOpen, | ||
passive: (props.open === undefined) as any, | ||
}); | ||
const { ui } = useUI('alertDialog', toRef(props, 'ui'), config); | ||
// With config defaults | ||
const variant = computed(() => props.variant ?? ui.value.default.variant); | ||
const iconName = computed(() => props.icon ?? ui.value.variant[variant.value].icon); | ||
// Disable transitions when prefered reduced motion | ||
const reduceMotion = usePreferredReducedMotion(); | ||
const contentTransition = computed(() => | ||
reduceMotion.value === 'no-preference' ? uiToTransitionProps(ui.value.transition) : {}, | ||
); | ||
const overlayTransition = computed(() => | ||
reduceMotion.value === 'no-preference' ? uiToTransitionProps(ui.value.overlay.transition) : {}, | ||
); | ||
// Trigger functionality | ||
const loading = ref(false); | ||
async function handleConfirm() { | ||
if (!props.confirmBtn?.action) return; | ||
loading.value = true; | ||
try { | ||
await props.confirmBtn.action(); | ||
$open.value = false; | ||
} catch (error) { | ||
console.error('Unhandled error on alert dialog:', error); | ||
} | ||
loading.value = false; | ||
} | ||
</script> | ||
|
||
<template> | ||
<AlertDialog.Root v-model:open="$open"> | ||
<AlertDialog.Trigger v-if="$slots.trigger" as-child> | ||
<slot name="trigger" :open="$open" /> | ||
</AlertDialog.Trigger> | ||
|
||
<AlertDialog.Portal> | ||
<Transition v-bind="overlayTransition"> | ||
<AlertDialog.Overlay :class="ui.overlay.base" /> | ||
</Transition> | ||
|
||
<Transition | ||
v-bind="contentTransition" | ||
@before-enter="emits('before-enter')" | ||
@after-enter="emits('after-enter')" | ||
@before-leave="emits('before-leave')" | ||
@after-leave="emits('after-leave')" | ||
> | ||
<AlertDialog.Content :class="[ui.container, ui.layout, ui.size, ui.padding]"> | ||
<div :class="[ui.icon.container, ui.icon.rounded, ui.variant[variant].color]"> | ||
<UiIcon :name="iconName" :class="ui.icon.size" /> | ||
</div> | ||
|
||
<div class="flex-1"> | ||
<AlertDialog.Title :class="ui.title">{{ props.title }}</AlertDialog.Title> | ||
|
||
<AlertDialog.Description :class="ui.description"> | ||
{{ props.description }} | ||
</AlertDialog.Description> | ||
|
||
<slot name="addon" /> | ||
|
||
<div :class="ui.actions.container"> | ||
<UiButton | ||
v-if="props.confirmBtn" | ||
block | ||
:loading="loading" | ||
:class="ui.actions.btnSize" | ||
:label="props.confirmBtn.label" | ||
:variant="props.confirmBtn.variant" | ||
@click="handleConfirm" | ||
/> | ||
|
||
<AlertDialog.Cancel v-if="props.cancelBtn" as-child> | ||
<UiButton | ||
block | ||
:disabled="loading" | ||
:class="ui.actions.btnSize" | ||
:label="props.cancelBtn.label" | ||
:variant="props.cancelBtn.variant" | ||
@click="$open = false" | ||
/> | ||
</AlertDialog.Cancel> | ||
</div> | ||
</div> | ||
</AlertDialog.Content> | ||
</Transition> | ||
</AlertDialog.Portal> | ||
</AlertDialog.Root> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import type { AppConfig } from 'nuxt/schema'; | ||
import { alertDialog } from '../ui.config'; | ||
import type { ButtonVariant } from './button'; | ||
import type { ExtractDeepKey } from './utils'; | ||
|
||
export type AlertDialogVariant = | ||
| keyof typeof alertDialog.variant | ||
| ExtractDeepKey<AppConfig, ['ui', 'alertDialog', 'variant']>; | ||
|
||
export interface AlertDialogProps<T> { | ||
open?: boolean; | ||
defaultOpen?: boolean; | ||
ui?: T; | ||
|
||
title: string; | ||
description: string; | ||
icon?: string; | ||
variant?: AlertDialogVariant; | ||
|
||
confirmBtn?: { | ||
label?: string; | ||
variant?: ButtonVariant; | ||
action?: (() => void) | (() => Promise<void>); | ||
}; | ||
cancelBtn?: { | ||
label?: string; | ||
variant?: ButtonVariant; | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export type UiOverlayEmits = { | ||
(e: 'before-enter'): void; | ||
(e: 'after-enter'): void; | ||
(e: 'before-leave'): void; | ||
(e: 'after-leave'): void; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
export default /*ui*/ { | ||
container: 'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2 rounded-lg bg-white shadow-xl', | ||
layout: 'flex flex-col items-center gap-2 sm:flex-row sm:items-start sm:gap-4', | ||
size: 'w-full max-w-lg', | ||
padding: 'px-4 py-6', | ||
title: 'text-center text-lg font-medium text-gray-900 sm:text-left sm:text-xl', | ||
description: 'mt-1 text-center text-gray-600 sm:text-left', | ||
actions: { | ||
container: 'mt-4 flex flex-col gap-2 sm:flex-row', | ||
btnSize: 'sm:w-max', | ||
}, | ||
variant: { | ||
danger: { | ||
icon: 'i-heroicons-exclamation-triangle', | ||
color: 'bg-red-100 text-red-600', | ||
}, | ||
warn: { | ||
icon: 'i-heroicons-exclamation-triangle', | ||
color: 'bg-amber-100 text-amber-600', | ||
}, | ||
info: { | ||
icon: 'i-heroicons-information-circle', | ||
color: 'bg-blue-100 text-blue-600', | ||
}, | ||
}, | ||
icon: { | ||
container: 'flex size-10 shrink-0 items-center justify-center', | ||
rounded: 'rounded-full', | ||
size: 'size-6', | ||
}, | ||
overlay: { | ||
base: 'fixed inset-0 z-40 bg-black/70 backdrop-blur-sm backdrop-filter', | ||
transition: { | ||
enterActive: 'ease-out duration-200', | ||
enterFrom: 'opacity-0', | ||
enterTo: 'opacity-100', | ||
leaveActive: 'ease-in duration-200', | ||
leaveFrom: 'opacity-100', | ||
leaveTo: 'opacity-0', | ||
}, | ||
}, | ||
transition: { | ||
enterActive: 'transition-[opacity,transform] ease-out duration-300', | ||
enterFrom: 'opacity-0 -translate-y-[40%] scale-95', | ||
enterTo: 'opacity-100 scale-100', | ||
leaveActive: 'transition-[opacity,transform] ease-in duration-200', | ||
leaveFrom: 'opacity-100 scale-100', | ||
leaveTo: 'opacity-0 scale-95', | ||
}, | ||
default: { | ||
variant: 'info', | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters