feat(ui): destructive modals

This commit is contained in:
ArneBo 2025-01-20 13:53:56 +01:00
parent a8b2368676
commit 9f69cfd9b5
3 changed files with 284 additions and 130 deletions

View File

@ -1,17 +1,26 @@
<script setup lang="ts">
import { type ColorProps, type DefaultProps, color } from '~/composables/color';
import { watchEffect } from 'vue';
import { watchEffect, computed, useSlots } from 'vue';
import onKeyboardShortcut from '~/composables/onKeyboardShortcut';
import Button from '~/components/ui/Button.vue'
import Spacer from '~/components/ui/Spacer.vue'
const { title, overPopover = false, ...colorProps } = defineProps<{
title:string,
overPopover?:true
} & (ColorProps | DefaultProps) >()
const props = defineProps<{
title: string,
overPopover?: true,
destructive?: true
} & (ColorProps | DefaultProps)>()
const slots = useSlots()
const isOpen = defineModel<boolean>({ default:false })
const modalClasses = computed(() => ({
'is-destructive': props.destructive,
'has-alert': !!slots.alert,
'over-popover': props.overPopover
}))
watchEffect(() =>
isOpen.value ?
document.querySelector('#app')?.setAttribute('inert', 'true') : document.querySelector('#app')?.removeAttribute('inert')
@ -33,10 +42,17 @@ onKeyboardShortcut('escape', () => isOpen.value = false)
>
<div @click.stop
class="funkwhale modal raised"
:class="[$slots.alert && 'has-alert', overPopover && 'over-popover']"
v-bind="color(colorProps)"
:class="[
modalClasses,
{ 'destructive': destructive }
]"
v-bind="color(props)"
>
<h2>
<h2
:class="{
'destructive-header': destructive
}"
>
{{ title }}
<Button icon="bi-x-lg" ghost @click="isOpen = false" />
</h2>

View File

@ -10,6 +10,16 @@
max-height: 90vh;
grid-template-rows: auto 1fr auto;
&.is-destructive {
border: 2px solid var(--fw-red-500);
> h2 {
&.destructive-header {
color: var(--fw-red-400);
}
}
}
> h2 {
font-size: 1.25em;
padding: 1.625rem 4.5rem;

View File

@ -20,6 +20,8 @@ watchEffect(() => {
const isOpen4 = ref(false)
const isOpen5 = ref(false)
const isOpen6 = ref(false)
const isOpen7 = ref(false)
const isOpen8 = ref(false)
</script>
@ -47,7 +49,7 @@ import Modal from "~/components/ui/Modal.vue"
```
<div class="preview">
<Button @click="isOpen = true">
<Button primary @click="isOpen = true">
Open modal
</Button>
<Modal v-model="isOpen" title="My modal">
@ -86,126 +88,20 @@ Make sure to add `autofocus` to the preferred button.
```
<div class="preview">
<Button @click="isOpen2 = true">
Open modal
</Button>
<Modal v-model="isOpen2" title="My modal">
Modal content
<template #actions>
<Button @click="isOpen2 = false">
Cancel
</Button>
<Button autofocus @click="isOpen2 = false">
Ok
</Button>
</template>
</Modal>
</div>
</Layout>
### Confirm a dangerous action
Note that confirmation dialogs interrupt the user's workflow. Consider adding a recovery functionality such as "undo" instead.
::: tip Read more about designing user experiences around dangerous actions:
- [How to use visual signals and spacing to differentiate between benign and dangerous options](https://www.nngroup.com/articles/proximity-consequential-options/)
> If you need to implement dangerous actions, make sure to place them apart from other actions to prevent accidental clicks. Add contextual hints and information so that the user understands the consequences of the action.
- [How to design a confirmation dialog](https://www.nngroup.com/articles/confirmation-dialog/)
> 1. Let the user confirm potentially destructive actions
> 2. Do not use confirmation dialogs for routine tasks
> 3. Be specific about the action and its potential consequences
> 4. Label the response buttons with their result: "Delete my account" instead of "Yes"
> 5. Make sure to give the user all information they need to decide
:::
```vue-html
<Button @click="isOpen = true" destructive>
Delete my account ...
</Button>
<Modal v-model="isOpen" title="Delete account?">
<template #alert>
<Alert red>
1 082 music files that you uploaded will be deleted.<br />
7 879 items in your collections will be unlinked.
</Alert>
</template>
Do you want to delete your account forever?
You will not be able to restore your account.
<template #actions>
<Button autofocus @click="isOpen = false" >
Keep my account
</Button>
<Button destructive @click="isOpen = false">
I understand. Delete my account now!
</Button>
</template>
</Modal>
```
<Button @click="isOpen6 = true" destructive>
Delete my account ...
</Button>
<Modal v-model="isOpen6" title="Delete account?">
<template #alert>
<Alert red>
1 082 music files that you uploaded will be deleted.<br />
7 879 items in your collections will be unlinked.
</Alert>
</template>
Do you want to delete your account forever?
You will not be able to restore your account.
<template #actions>
<Button autofocus @click="isOpen6 = false">
Keep my account
</Button>
<Button destructive @click="isOpen6 = false">
I understand. Delete my account now!
</Button>
</template>
</Modal>
## Nested modals
You can nest modals to allow users to isOpen a modal from inside another modal. This can be useful when creating a multi-step workflow.
<Layout flex>
```vue-html
<Button @click="isOpen = true">
Open modal
</Button>
<Modal v-model="isOpen" title="My modal">
<Modal v-model="isOpenNested" title="My modal">
Nested modal content
</Modal>
<Button autofocus @click="isOpenNested = true">
Open nested modal
<Button primary @click="isOpen2 = true">
Open modal
</Button>
</Modal>
```
<div class="preview">
<Button @click="isOpen4 = true">
Open modal
</Button>
<Modal v-model="isOpen4" title="My modal">
<Modal v-model="isOpen5" title="My modal">
Nested modal content
<Modal v-model="isOpen2" title="My modal">
Modal content
<template #actions>
<Button @click="isOpen2 = false">
Cancel
</Button>
<Button autofocus @click="isOpen2 = false">
Ok
</Button>
</template>
</Modal>
<Button autofocus @click="isOpen5 = true">
Open nested modal
</Button>
</Modal>
</div>
</Layout>
@ -237,10 +133,13 @@ You can nest [Funkwhale alerts](./alert) to visually highlight content within th
```
<div class="preview">
<Button @click="isOpen3 = true">
<Button
primary
@click="isOpen8 = true"
>
Open modal
</Button>
<Modal v-model="isOpen3" title="My modal">
<Modal v-model="isOpen8" title="My modal">
Modal content
<template #alert v-if="alertOpen">
<Alert blue>
@ -254,7 +153,7 @@ You can nest [Funkwhale alerts](./alert) to visually highlight content within th
<Button @click="isOpen3 = false" secondary>
Cancel
</Button>
<Button primary @click="isOpen3 = false">
<Button primary @click="isOpen8 = false">
Ok
</Button>
</template>
@ -262,3 +161,232 @@ You can nest [Funkwhale alerts](./alert) to visually highlight content within th
</div>
</Layout>
# Destructive Modal
The `destructive` prop is used to visually indicate a potentially dangerous or irreversible action. When set to `true`, the modal will have a distinctive red styling to draw the user's attention and highlight the critical nature of the action.
| Prop | Data type | Required? | Default | Description |
|---------------|-----------|-----------|---------|--------------------------------------------------|
| `destructive` | `true` | No | `false` | Applies a red styling to emphasize dangerous actions |
<Layout flex>
```vue-html
<Modal
v-model="isOpen"
title="Delete Account"
destructive
>
Dangerous action consequences
</Modal>
```
<div class="preview">
<Button
destructive
@click="isOpen3 = true"
>
Delete Account
</Button>
<Modal
v-model="isOpen3"
title="Delete Account"
destructive
>
Dangerous action consequences
</Modal>
</div>
</Layout>
### Styling Effects
When the `destructive` prop is set to `true`, the modal will:
- Add a red border
- Style the title in red
To visually distinguish the modal from standard modals
### Best Practices
- Use the `destructive` prop sparingly
- Clearly explain the consequences of the action unsing `<Alert red>`
- Provide a clear way to cancel the action
- Use descriptive action button labels
The example in the "Confirm a dangerous action" section demonstrates the recommended usage of the destructive modal.
<Layout flex>
```vue-html
<Modal
v-model="isOpen"
title="Delete Account?"
destructive
>
<template #alert>
<Alert red>
Detailed consequences of the action
</Alert>
</template>
<template #actions>
<Button @click="isOpen = false">
Cancel
</Button>
<Button
destructive
@click="deleteAccount()"
>
Confirm Deletion
</Button>
</template>
</Modal>
```
<div class="preview">
<Button
@click="isOpen4 = true"
destructive
>
Delete my account
</Button>
<Modal
v-model="isOpen4"
title="Delete Account?"
destructive
>
<template #alert>
<Alert red>
Detailed consequences of the action
</Alert>
</template>
<template #actions>
<Button @click="isOpen4 = false">
Cancel
</Button>
<Button
destructive
@click="isOpen4 = false"
>
Confirm Deletion
</Button>
</template>
</Modal>
</div>
</Layout>
## Confirm a dangerous action
Note that confirmation dialogs interrupt the user's workflow. Consider adding a recovery functionality such as "undo" instead.
::: tip Read more about designing user experiences around dangerous actions:
- [How to use visual signals and spacing to differentiate between benign and dangerous options](https://www.nngroup.com/articles/proximity-consequential-options/)
> If you need to implement dangerous actions, make sure to place them apart from other actions to prevent accidental clicks. Add contextual hints and information so that the user understands the consequences of the action.
- [How to design a confirmation dialog](https://www.nngroup.com/articles/confirmation-dialog/)
> 1. Let the user confirm potentially destructive actions
> 2. Do not use confirmation dialogs for routine tasks
> 3. Be specific about the action and its potential consequences
> 4. Label the response buttons with their result: "Delete my account" instead of "Yes"
> 5. Make sure to give the user all information they need to decide
:::
<Layout flex>
```vue-html
<Button @click="isOpen = true" destructive>
Delete my account ...
</Button>
<Modal v-model="isOpen" title="Delete account?">
<template #alert>
<Alert red>
1 082 music files that you uploaded will be deleted.<br />
7 879 items in your collections will be unlinked.
</Alert>
</template>
Do you want to delete your account forever?
You will not be able to restore your account.
<template #actions>
<Button autofocus @click="isOpen = false" >
Keep my account
</Button>
<Button destructive @click="isOpen = false">
I understand. Delete my account now!
</Button>
</template>
</Modal>
```
<Button @click="isOpen5 = true" destructive>
Delete my account ...
</Button>
<Modal v-model="isOpen5" title="Delete account?">
<template #alert>
<Alert red>
1 082 music files that you uploaded will be deleted.<br />
7 879 items in your collections will be unlinked.
</Alert>
</template>
Do you want to delete your account forever?
You will not be able to restore your account.
<template #actions>
<Button autofocus @click="isOpen5 = false">
Keep my account
</Button>
<Button destructive @click="isOpen5 = false">
I understand. Delete my account now!
</Button>
</template>
</Modal>
</Layout>
## Nested modals
You can nest modals to allow users to isOpen a modal from inside another modal. This can be useful when creating a multi-step workflow.
<Layout flex>
```vue-html
<Button @click="isOpen = true">
Open modal
</Button>
<Modal v-model="isOpen" title="My modal">
<Modal v-model="isOpenNested" title="My modal">
Nested modal content
</Modal>
<Button autofocus @click="isOpenNested = true">
Open nested modal
</Button>
</Modal>
```
<div class="preview">
<Button
primary
@click="isOpen6 = true"
>
Open modal
</Button>
<Modal v-model="isOpen6" title="My modal">
<Modal v-model="isOpen7" title="My modal">
Nested modal content
</Modal>
<Button autofocus @click="isOpen7 = true">
Open nested modal
</Button>
</Modal>
</div>
</Layout>