Skip to content

Commit

Permalink
feat(AlertDialog): New component (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
ChrisGV04 authored Mar 23, 2024
1 parent 717882e commit 2d0db2a
Show file tree
Hide file tree
Showing 8 changed files with 257 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/components/TheHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const dataLinks: DropdownItem[] = [
const layoutLinks: DropdownItem[] = [{ label: 'Marquee', to: '/marquee' }];
const overlayLinks: DropdownItem[] = [
{ label: 'Alert Dialog', to: '/alert-dialog' },
{ label: 'Dialog', to: '/dialog' },
{ label: 'Slideover', to: '/slideover' },
{ label: 'Tooltip', to: '/tooltip' },
Expand Down
36 changes: 36 additions & 0 deletions docs/pages/alert-dialog.vue
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>
129 changes: 129 additions & 0 deletions src/runtime/components/overlays/AlertDialog.vue
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>
29 changes: 29 additions & 0 deletions src/runtime/types/alert-dialog.d.ts
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;
};
}
2 changes: 2 additions & 0 deletions src/runtime/types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './alert-dialog';
export * from './badge';
export * from './button';
export * from './combobox';
Expand All @@ -6,4 +7,5 @@ export * from './formField';
export * from './formInput';
export * from './formSelect';
export * from './link';
export * from './overlays';
export * from './utils';
6 changes: 6 additions & 0 deletions src/runtime/types/overlays.d.ts
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;
};
53 changes: 53 additions & 0 deletions src/runtime/ui.config/alert-dialog.ts
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',
},
};
1 change: 1 addition & 0 deletions src/runtime/ui.config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export { default as dropdown } from './dropdown';
export { default as container } from './container';

// Overlays
export { default as alertDialog } from './alert-dialog';
export { default as dialog } from './dialog';
export { default as slideover } from './slideover';
export { default as tooltip } from './tooltip';
Expand Down

0 comments on commit 2d0db2a

Please sign in to comment.