138 lines
3.1 KiB
Vue
138 lines
3.1 KiB
Vue
<template>
|
|
<Teleport to="body">
|
|
<Transition
|
|
:name="`drawer-${side}`"
|
|
appear
|
|
@after-leave="isRendered = false"
|
|
>
|
|
<div
|
|
v-if="isRendered && isOpen"
|
|
:id="componentId"
|
|
class="fixed inset-0 z-50 flex"
|
|
:class="side === 'right' ? 'justify-end' : 'justify-start'"
|
|
v-bind="attrs"
|
|
>
|
|
<div
|
|
:class="twMerge('absolute inset-0 bg-black/40', overlayClass)"
|
|
data-test="backdrop"
|
|
/>
|
|
|
|
<div
|
|
ref="panelRef"
|
|
:class="twMerge(
|
|
'relative z-50 flex h-full w-full max-w-md flex-col bg-white',
|
|
drawerClass,
|
|
)"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
tabindex="-1"
|
|
data-test="panel"
|
|
>
|
|
<div
|
|
:class="twMerge('flex-1 overflow-y-auto px-5', bodyClass)"
|
|
data-test="body"
|
|
>
|
|
<slot />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref, useAttrs, useId } from 'vue'
|
|
import { twMerge } from 'tailwind-merge'
|
|
|
|
defineOptions({ name: 'MalioDrawer', inheritAttrs: false })
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
id?: string
|
|
modelValue?: boolean
|
|
side?: 'right' | 'left'
|
|
showClose?: boolean
|
|
dismissable?: boolean
|
|
closeOnEscape?: boolean
|
|
ariaLabel?: string
|
|
drawerClass?: string
|
|
overlayClass?: string
|
|
headerClass?: string
|
|
bodyClass?: string
|
|
footerClass?: string
|
|
}>(),
|
|
{
|
|
id: '',
|
|
modelValue: undefined,
|
|
side: 'right',
|
|
showClose: true,
|
|
dismissable: true,
|
|
closeOnEscape: true,
|
|
ariaLabel: '',
|
|
drawerClass: '',
|
|
overlayClass: '',
|
|
headerClass: '',
|
|
bodyClass: '',
|
|
footerClass: '',
|
|
},
|
|
)
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'update:modelValue', value: boolean): void
|
|
(e: 'close'): void
|
|
}>()
|
|
|
|
const attrs = useAttrs()
|
|
const generatedId = useId()
|
|
|
|
const componentId = computed(() => props.id || `malio-drawer-${generatedId}`)
|
|
|
|
const isControlled = computed(() => props.modelValue !== undefined)
|
|
const localValue = ref(false)
|
|
const isOpen = computed(() =>
|
|
isControlled.value ? props.modelValue! : localValue.value,
|
|
)
|
|
const isRendered = ref(isOpen.value)
|
|
|
|
const panelRef = ref<HTMLElement | null>(null)
|
|
|
|
function close() {
|
|
if (!isControlled.value) localValue.value = false
|
|
emit('update:modelValue', false)
|
|
emit('close')
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.drawer-right-enter-active,
|
|
.drawer-right-leave-active,
|
|
.drawer-left-enter-active,
|
|
.drawer-left-leave-active {
|
|
transition: opacity 0.2s ease;
|
|
}
|
|
|
|
.drawer-right-enter-active > div:last-child,
|
|
.drawer-right-leave-active > div:last-child,
|
|
.drawer-left-enter-active > div:last-child,
|
|
.drawer-left-leave-active > div:last-child {
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.drawer-right-enter-from,
|
|
.drawer-right-leave-to,
|
|
.drawer-left-enter-from,
|
|
.drawer-left-leave-to {
|
|
opacity: 0;
|
|
}
|
|
|
|
.drawer-right-enter-from > div:last-child,
|
|
.drawer-right-leave-to > div:last-child {
|
|
transform: translateX(100%);
|
|
}
|
|
|
|
.drawer-left-enter-from > div:last-child,
|
|
.drawer-left-leave-to > div:last-child {
|
|
transform: translateX(-100%);
|
|
}
|
|
</style>
|