153 lines
4.0 KiB
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>
|