2.0 beta — new releases can include breaking changes. Stable 1.x docs
dimah-s3v2.0.0-beta.0
ComponentsDownload & Delete

Delete

Delete buttons with one confirm dialog per root, and toasts for useDelete.

The delete family renders one useDelete hook. Delete holds the hook and one confirm dialog. Every DeleteTrigger inside it asks the root, and the root asks before it deletes.

Preview

Every row's trigger asks the same root, which confirms, deletes, and reports the result. The rows leave once their delete succeeds.

  • meeting-notes.docx46.9 KB
  • old-logo.png214.8 KB
  • draft-v1.pdf1.3 MB

Install

Complete UI Setup first: the stylesheet and one <Toaster />. Then add this component:

import { Delete, DeleteToasts, DeleteTrigger } from "@dimah-s3/ui";

Usage

"use client";

import { useDelete } from "@dimah-s3/react";
import { Delete, DeleteToasts, DeleteTrigger } from "@dimah-s3/ui";

type Attachment = { key: string; name: string; size: number };

export function AttachmentList({
  attachments,
  onRemoved,
}: {
  attachments: Attachment[];
  onRemoved: (keys: string[]) => void;
}) {
  const remove = useDelete({ route: "attachments" });

  return (
    <>
      <Delete delete={remove} onDeleted={(result) => onRemoved(result.deleted)}>
        <ul>
          {attachments.map((file) => (
            <li key={file.key}>
              {file.name}
              <DeleteTrigger
                objectKey={file.key}
                fileName={file.name}
                fileSize={file.size}
                size="sm"
              />
            </li>
          ))}
        </ul>
      </Delete>
      <DeleteToasts delete={remove} />
    </>
  );
}

DeleteTrigger takes every Button prop and defaults to the destructive variant. While its key is deleting, it is disabled, stays focusable, and shows a spinner. children replace its icon and label.

Examples

Triggers and dialogs

DeleteTrigger is a Button with every prop forwarded. note adds a line to the dialog. A root's dialog replaces the dialog copy, and confirm={false} deletes on click.

Delete several files

Your own trigger calls request from useDeleteContext() with every target. One dialog asks for all of them.

  • scan-001.pdf
  • scan-002.pdf
  • scan-003.pdf

Confirmation

The root renders one DeleteConfirmDialog for all of its triggers. It is an alert dialog with initial focus on Cancel. It names the file and its size for one target, or the count for several. Confirm stays busy, and the dialog stays open, until the delete settles. Cancel and Escape do nothing until then.

Replace the copy with your own dialog element:

<Delete
  delete={remove}
  dialog={
    <DeleteConfirmDialog
      title="Remove profile picture?"
      description="Your profile goes back to the default avatar."
      confirmLabel="Remove"
    />
  }
>
  <DeleteTrigger objectKey={avatarKey} />
</Delete>

note on the trigger adds a line to the dialog's description, read out when it opens. confirm={false} deletes on click, without a dialog.

Results and focus

onDeleted runs once per confirmed delete with the DeleteResult: deleted, failed, and blocked keys. It runs before the dialog closes, so rows you remove in it are gone by the time focus moves.

When the dialog closes, focus goes to the first of these that is still in the document:

  1. The element restoreFocus(targets) returns.
  2. The trigger that asked.
  3. The trigger's nearest [data-dimah-s3-focus-root] container.

Mark the container of a list whose rows unmount with data-dimah-s3-focus-root and tabIndex={-1}, so focus has a place to go. A custom dialog gets the same focus handling from the root.

Your own triggers

useDeleteContext() returns the hook, the pending request, and request, confirm, and cancel. request takes one target or a list, so one confirmation covers a bulk delete:

function DeleteSelected({ files }: { files: Attachment[] }) {
  const { request } = useDeleteContext();

  return (
    <Button
      variant="destructive"
      onClick={(event) =>
        request(
          files.map((file) => ({ objectKey: file.key, fileName: file.name })),
          { trigger: event.currentTarget },
        )
      }
    >
      Delete {files.length} files
    </Button>
  );
}

Pass trigger when the element that asked can unmount, such as a menu item: pass the menu's trigger button instead. Without it, focus returns to the element that had focus. A new request replaces one that is waiting.

Toasts

Mount DeleteToasts once per hook, next to one <Toaster />. It shows one success toast and one error toast per call, once every key of the call has settled. A second DeleteToasts for the same hook renders nothing and warns in development.


Props

import type {
  DeleteProps,
  DeleteTriggerProps,
  DeleteConfirmDialogProps,
  DeleteContextValue,
  DeleteRequestOptions,
  DeleteToastsProps,
} from "@dimah-s3/ui";

Delete

Prop

Type

DeleteTrigger

Prop

Type

DeleteConfirmDialog

Prop

Type

useDeleteContext

Prop

Type

DeleteRequestOptions

Prop

Type

DeleteToasts

Prop

Type

On this page