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.
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.
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:
- The element
restoreFocus(targets)returns. - The trigger that asked.
- 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