|
| 1 | +import * as Stories from './destructive.stories'; |
| 2 | + |
| 3 | +# Destructive |
| 4 | + |
| 5 | +A type-to-confirm dialog for an action that cannot be undone. The action stays inert until the user types the confirmation phrase back. |
| 6 | + |
| 7 | +## Example |
| 8 | + |
| 9 | +<Story |
| 10 | + name='Default' |
| 11 | + storyModule={Stories} |
| 12 | + composition={[ |
| 13 | + { name: 'Dialog', href: '/components/dialog', layer: 'Components' }, |
| 14 | + { name: 'Card', href: '/components/card', layer: 'Components' }, |
| 15 | + { name: 'Field', href: '/components/field', layer: 'Components' }, |
| 16 | + { name: 'Button', href: '/components/button', layer: 'Components' }, |
| 17 | + ]} |
| 18 | +/> |
| 19 | + |
| 20 | +## Usage |
| 21 | + |
| 22 | +The block holds one thing: the phrase the user types. Nothing outside the dialog can use a half-typed string, so keeping it inside removes the keystroke plumbing a caller would otherwise write. |
| 23 | + |
| 24 | +Everything that decides what the dialog does next belongs to the caller. `open` closes it, `isDeleting` marks it busy, `errorMessage` explains a failure. |
| 25 | + |
| 26 | +```tsx |
| 27 | +import { Destructive } from '@clerk/ui/mosaic/blocks/destructive'; |
| 28 | +import { Button } from '@clerk/ui/mosaic/components/button'; |
| 29 | + |
| 30 | +const [open, setOpen] = useState(false); |
| 31 | +const [isDeleting, setIsDeleting] = useState(false); |
| 32 | + |
| 33 | +const handleDelete = async () => { |
| 34 | + setIsDeleting(true); |
| 35 | + await deleteAccount(); |
| 36 | + setIsDeleting(false); |
| 37 | + setOpen(false); |
| 38 | +}; |
| 39 | + |
| 40 | +<Destructive |
| 41 | + open={open} |
| 42 | + onOpenChange={setOpen} |
| 43 | + trigger={<Button color='negative' variant='outline'>Delete account</Button>} |
| 44 | + title='Delete account?' |
| 45 | + description='Are you sure you want to delete your account? All of your data will be permanently deleted.' |
| 46 | + fieldLabel='Type “Delete account” below to continue' |
| 47 | + confirmationValue='Delete account' |
| 48 | + actionLabel='Delete account' |
| 49 | + onDelete={() => void handleDelete()} |
| 50 | + isDeleting={isDeleting} |
| 51 | +/>; |
| 52 | +``` |
| 53 | + |
| 54 | +## Failure |
| 55 | + |
| 56 | +A failed attempt leaves the dialog up. Pass the sentence the user should read as `errorMessage`, and clear it when the next attempt starts. The field is marked invalid for as long as a message is set. |
| 57 | + |
| 58 | +<Story |
| 59 | + name='WithError' |
| 60 | + storyModule={Stories} |
| 61 | +/> |
| 62 | + |
| 63 | +## Props |
| 64 | + |
| 65 | +| Prop | Type | Description | |
| 66 | +| ------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------- | |
| 67 | +| `open` | `boolean` | Whether the confirmation is showing. Controlled, the way any dialog is. | |
| 68 | +| `onOpenChange` | `(open: boolean) => void` | Asks to open or close. Fired by the trigger, Cancel, Escape, and the backdrop. | |
| 69 | +| `trigger` | `ReactNode` | Optional. The button that asks to open the dialog. | |
| 70 | +| `title` | `string` | Names what is about to be destroyed. | |
| 71 | +| `description` | `string` | Spells out what is lost. Sits above the confirmation field. | |
| 72 | +| `fieldLabel` | `string` | Labels the confirmation field. | |
| 73 | +| `confirmationValue` | `string` | The phrase the user has to type back. Also the field's placeholder. | |
| 74 | +| `actionLabel` | `string` | The destructive button's label. | |
| 75 | +| `cancelLabel` | `string` | Optional. Defaults to `Cancel`. | |
| 76 | +| `onDelete` | `() => void` | Asks the caller to run the action. Reached by the button or by Enter in the field, once the typed phrase matches. | |
| 77 | +| `isDeleting` | `boolean` | Optional. Disables the field and renders the action pending. | |
| 78 | +| `errorMessage` | `string` | Optional. Marks the field invalid and renders under it. | |
| 79 | + |
| 80 | +## Driving it from a machine |
| 81 | + |
| 82 | +`UserProfileDeleteSection` wires the same block to a state machine rather than to `useState`. The machine's state maps onto the same props: |
| 83 | + |
| 84 | +```tsx |
| 85 | +<Destructive |
| 86 | + open={snapshot.value === 'confirming' || snapshot.value === 'deleting'} |
| 87 | + onOpenChange={open => send({ type: open ? 'OPEN' : 'CANCEL' })} |
| 88 | + onDelete={() => send({ type: 'CONFIRM' })} |
| 89 | + isDeleting={snapshot.value === 'deleting'} |
| 90 | + errorMessage={snapshot.context.errorMessage} |
| 91 | + {...copy} |
| 92 | +/> |
| 93 | +``` |
0 commit comments