Alert Dialog

A modal dialog that interrupts the user with important content and expects a response.

Preview

Installation

Copy and paste this into your project

// ~/components/ui/alert-dialog.tsx

import {
  provide,
  defineComponent,
  Transition,
  Teleport,
  ComputedRef,
  InjectionKey,
  inject,
  computed,
  PropType,
} from "vue";
import { Button, ButtonProps } from "~/components/ui/button";
import { ExtendProps, cn, renderAsChild } from "~/lib/utils";
import {
  Dialog,
  DialogBackdrop,
  DialogCloseTrigger,
  DialogContainer,
  DialogContent,
  DialogDescription,
  DialogProps,
  DialogTitle,
  DialogTrigger,
} from "@ark-ui/vue";

const isOpenKey = Symbol() as InjectionKey<ComputedRef<boolean>>;

const AlertDialogContext = defineComponent({
  props: {
    isOpen: {
      type: Boolean,
      default: false,
    },
  },
  setup(props, { slots }) {
    provide(
      isOpenKey,
      computed(() => props.isOpen)
    );
    return () => slots.default?.();
  },
});

const AlertDialog = defineComponent({
  props: {} as ExtendProps<DialogProps>,
  setup(props, { slots }) {
    return () => (
      <Dialog {...props} preventScroll>
        {({ isOpen }: { isOpen: boolean }) => (
          <AlertDialogContext isOpen={isOpen}>
            {slots.default?.()}
          </AlertDialogContext>
        )}
      </Dialog>
    );
  },
});

const AlertDialogTrigger = DialogTrigger;

const AlertDialogContent = defineComponent({
  setup(_, context) {
    const isOpen = inject(isOpenKey);

    return () => (
      <Teleport to="body">
        <Transition
          enter-from-class="opacity-0"
          leave-to-class="opacity-0"
          enter-active-class="transition-opacity duration-150 ease-out"
          leave-active-class="transition-opacity duration-100 ease-in"
        >
          {isOpen?.value && (
            <DialogBackdrop class="fixed inset-0 z-50 bg-background/90" />
          )}
        </Transition>

        <DialogContainer class="fixed inset-0 z-50 flex items-end justify-center sm:items-center">
          <Transition
            enter-from-class="sm:opacity-0 sm:scale-90 translate-y-full sm:translate-y-0"
            leave-to-class="sm:opacity-0 sm:scale-90 translate-y-full sm:translate-y-0"
            enter-active-class="transition duration-200 sm:duration-150 ease-out"
            leave-active-class="transition duration-150 sm:duration-100 ease-in"
          >
            {isOpen?.value && (
              <DialogContent class="fixed z-50 grid w-full max-w-lg gap-4 border bg-background p-6 shadow-lg sm:rounded-lg md:w-full">
                {context.slots.default?.()}
              </DialogContent>
            )}
          </Transition>
        </DialogContainer>
      </Teleport>
    );
  },
});

const AlertDialogHeader = defineComponent({
  setup(_, { slots, attrs }) {
    return () => (
      <div
        class={cn(
          "flex flex-col space-y-2 text-center sm:text-left",
          attrs.class ?? ""
        )}
      >
        {slots.default?.()}
      </div>
    );
  },
});

const AlertDialogTitle = defineComponent({
  setup(_, { slots, attrs }) {
    return () => (
      <DialogTitle class={cn("text-lg font-semibold", attrs.class ?? "")}>
        {slots.default?.()}
      </DialogTitle>
    );
  },
});

const AlertDialogDescription = defineComponent({
  setup(_, { slots, attrs }) {
    return () => (
      <DialogDescription
        class={cn("text-sm text-muted-foreground", attrs ?? "")}
      >
        {slots.default?.()}
      </DialogDescription>
    );
  },
});

const AlertDialogFooter = defineComponent({
  setup(_, { slots, attrs }) {
    return () => (
      <div
        class={cn(
          "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
          attrs ?? ""
        )}
      >
        {slots.default?.()}
      </div>
    );
  },
});

const AlertDialogCancel = defineComponent({
  setup(_, context) {
    return () => (
      <DialogCloseTrigger>
        <Button variant="secondary" class="mt-2 sm:mt-0">
          {context.slots.default?.()}
        </Button>
      </DialogCloseTrigger>
    );
  },
});

const AlertDialogAction = defineComponent({
  props: {
    asChild: {
      type: Boolean,
    },
  },
  emits: {
    click: null,
  },
  setup(props, { slots, attrs, emit }) {
    return () => (
      <DialogCloseTrigger>
        {props.asChild ? (
          renderAsChild(slots, { ...attrs })
        ) : (
          <Button onClick={() => emit("click")} {...attrs}>
            {slots.default?.()}
          </Button>
        )}
      </DialogCloseTrigger>
    );
  },
});

export {
  AlertDialog,
  AlertDialogTrigger,
  AlertDialogContent,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogAction,
  AlertDialogCancel,
};

Usage

import {
  AlertDialog,
  AlertDialogTrigger,
  AlertDialogContent,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogDescription,
  AlertDialogCancel,
  AlertDialogAction,
  AlertDialogFooter,
} from "~/components/ui/alert-dialog";
import { Button } from "~/components/ui/button";
<AlertDialog>
  <AlertDialogTrigger>
    <Button>Open</Button>
  </AlertDialogTrigger>
  <AlertDialogContent>
    <AlertDialogHeader>
      <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
      <AlertDialogDescription>
        This action cannot be undone. This will permanently delete your
        account and remove your data from our servers.
      </AlertDialogDescription>
    </AlertDialogHeader>
    <AlertDialogFooter>
      <AlertDialogCancel>Cancel</AlertDialogCancel>
      <AlertDialogAction>Continue</AlertDialogAction>
    </AlertDialogFooter>
  </AlertDialogContent>
</AlertDialog>