99 lines
2.9 KiB
Vue
99 lines
2.9 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
|
|
} & (ColorProps | DefaultProps)>()
|
|
|
|
const isOpen = defineModel<boolean>({ default:false })
|
|
|
|
const previouslyFocusedElement = ref();
|
|
|
|
// Handle focus and inertness of the elements behind the modal
|
|
watchEffect(() =>
|
|
isOpen.value
|
|
? ( previouslyFocusedElement.value = document.activeElement,
|
|
document.querySelector('#app')?.setAttribute('inert', 'true'))
|
|
: ( 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"
|
|
@click.exact.stop="isOpen = false"
|
|
class="funkwhale overlay"
|
|
>
|
|
<div @click.stop
|
|
class="funkwhale modal"
|
|
:class="[
|
|
{ 'is-destructive': destructive,
|
|
'has-alert': !!$slots.alert,
|
|
'over-popover': overPopover,
|
|
}
|
|
]"
|
|
v-bind="{...$attrs, ...color(props)}"
|
|
>
|
|
<Layout flex gap-12 style="padding: 12px;">
|
|
<div style="width: 48px;">
|
|
<slot name="topleft" />
|
|
</div>
|
|
<Spacer h grow />
|
|
<Heading :h2="title"
|
|
section-heading
|
|
:class="{'destructive-header': destructive}"
|
|
/>
|
|
<Spacer h grow />
|
|
<Button icon="bi-x-lg" ghost @click="isOpen = false" align-self="baseline" />
|
|
</Layout>
|
|
|
|
<!-- Content -->
|
|
|
|
<div class="modal-content">
|
|
<Transition>
|
|
<div v-if="$slots.alert" class="alert-container">
|
|
<div>
|
|
<slot name="alert" />
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
|
|
<slot />
|
|
</div>
|
|
|
|
<!-- Actions slot -->
|
|
|
|
<Layout flex gap-12 style="flex-wrap: wrap;" v-if="$slots.actions || cancel" class="modal-actions">
|
|
<slot name="actions" />
|
|
<Button v-if="cancel" secondary autofocus :onClick="()=>{ isOpen = false }">
|
|
{{ cancel }}
|
|
</Button>
|
|
</Layout>
|
|
<Spacer size-46 v-else />
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<style lang="scss">
|
|
@import './modal.scss'
|
|
</style>
|