funkwhale/front/src/components/ui/Modal.vue

153 lines
4.0 KiB
Vue

<script setup lang="ts">
import { type ColorProps, type DefaultProps, color } from '~/composables/color'
import { watchEffect, ref, nextTick } from 'vue'
import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
import Button from '~/components/ui/Button.vue'
import Spacer from '~/components/ui/Spacer.vue'
import Layout from '~/components/ui/Layout.vue'
import Heading from '~/components/ui/Heading.vue'
const props = defineProps<{
title: string,
overPopover?: true,
destructive?: true,
cancel?: string,
icon?: string,
autofocus?: true | 'off'
} &(ColorProps | DefaultProps)>()
const isOpen = defineModel<boolean>({ default: false })
const previouslyFocusedElement = ref()
// Handle focus and inertness of the elements behind the modal
watchEffect(() => {
if (isOpen.value) {
nextTick(()=>{
previouslyFocusedElement.value = document.activeElement
previouslyFocusedElement.value?.blur()
document.querySelector('#app')?.setAttribute('inert', 'true')
})
} else {
nextTick(() => previouslyFocusedElement.value?.focus())
document.querySelector('#app')?.removeAttribute('inert')
}
})
onKeyboardShortcut('escape', () => { isOpen.value = false })
// TODO:
// When overflowing content: Add inset shadow to indicate scrollability
</script>
<template>
<Teleport to="body">
<Transition mode="out-in">
<div
v-if="isOpen"
class="funkwhale overlay"
@click.exact.stop="isOpen = false"
>
<div
class="funkwhale modal"
:class="[
{ 'is-destructive': destructive,
'has-alert': !!$slots.alert,
'over-popover': overPopover,
}
]"
v-bind="{...$attrs, ...color(props)}"
@click.stop
>
<Layout
flex
gap-12
style="padding: 12px 12px 0 12px;"
>
<div
v-if="!$slots.topleft && !icon"
style="width: 48px;"
/>
<div
v-if="icon"
style="display: flex; justify-content: center; align-items: center; width: 48px;"
>
<i
:class="['bi', icon]"
style="font-size: 18px;"
/>
</div>
<slot name="topleft" />
<Spacer
v-if="!$slots.topleft"
grow
/>
<Heading
v-if="title !== ''"
:h2="title"
section-heading
:class="{'destructive-header': destructive}"
/>
<Spacer grow />
<Button
icon="bi-x-lg"
ghost
align-self="baseline"
:autofocus="props.autofocus === undefined ? ($slots.actions || cancel ? undefined : true) : props.autofocus !== 'off'"
@click="isOpen = false"
/>
</Layout>
<!-- Content -->
<div class="modal-shadow-top" />
<div class="modal-content">
<Transition>
<div
v-if="$slots.alert"
class="alert-container"
>
<div>
<slot name="alert" />
</div>
</div>
</Transition>
<slot />
<Spacer v-if="!$slots.actions" />
</div>
<div class="modal-shadow-bottom" />
<!-- Actions slot -->
<Layout
v-if="$slots.actions || cancel"
flex
gap-12
style="flex-wrap: wrap;"
class="modal-actions"
>
<slot name="actions" />
<Button
v-if="cancel"
secondary
autofocus
:on-click="()=>{ isOpen = false }"
>
{{ cancel }}
</Button>
</Layout>
</div>
</div>
</Transition>
</Teleport>
</template>
<style lang="scss">
@import './modal.scss'
</style>