Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 43 additions & 3 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,45 @@
<template>
<main>
Vue-Tailwind
</main>
<div>
<button @click="openModal">Open Modal</button>

<DxhModal
v-if="isModalOpen"
:title="modalTitle"
:footer="true"
:maxWidth="'500px'"
:maxHeight="'300px'"
:open="isModalOpen"
:keyboardEsc="true"
:zIndex="1"
:persistent="false"
@ok="handleOk"
@cancel="handleCancel"
@close="isModalOpen = false"
>
<!-- Your modal content goes here -->
<p>This is the modal content.</p>
</DxhModal>
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import DxhModal from '@/components/DxhModal.vue'

const isModalOpen = ref(false)
const modalTitle = ref('Sample Modal')

const openModal = () => {
isModalOpen.value = true
}

const handleOk = () => {
// Handle OK button click
isModalOpen.value = false
}

const handleCancel = () => {
// Handle Cancel button click
isModalOpen.value = false
}
</script>
112 changes: 112 additions & 0 deletions src/components/DxhModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<template>
<div
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 cursor-pointer"
@click="handleOutsideClick"
data-test="modal-overlay"
>
<div
ref="modalRef"
class="bg-white p-6 rounded-md relative cursor-auto min-w-[250px]"
:style="{ zIndex, maxWidth: `${maxWidth}px`, maxHeight: `${maxHeight}px` }"
@keydown.esc="keyboardEsc ? handleClose : ''"
data-test="modal-content"
>
<div
class="absolute top-6 right-4 cursor-pointer"
data-test="close-icon"
>
<slot name="close" :onClose="handleClose">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
class="h-5 w-5"
@click="handleClose"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</slot>
</div>
<div v-if="title" class="text-lg font-bold mr-6" data-test="modal-title">{{ title }}</div>
<div class="my-4" data-test="modal-body">
<slot></slot>
</div>
<div v-if="footer" class="flex space-x-2 justify-end" data-test="modal-footer">
<slot name="cancel" :onCancel="handleCancel">
<button @click="handleCancel" class="border px-1" data-test="cancel-button">
Cancel
</button>
</slot>
<slot name="ok" :onOk="handleOk">
<button @click="handleOk" class="border px-1" data-test="ok-button">OK</button>
</slot>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'

interface Props {
title?: string
footer?: boolean
maxWidth?: string
maxHeight?: string
open: boolean
keyboardEsc?: boolean
zIndex?: number
persistent?: boolean
}

const { title, footer, maxWidth, maxHeight, open, keyboardEsc, zIndex, persistent } =
defineProps<Props>()
const emit = defineEmits(['ok', 'cancel', 'close'])

const modalRef = ref<HTMLElement | null>(null)

const handleOutsideClick = (event: MouseEvent) => {
if (!persistent && open) {
const modalElement = event.target as HTMLElement
if (!modalRef.value?.contains(modalElement)) {
handleClose()
}
}
}

onMounted(() => {
if (keyboardEsc) {
document.addEventListener('keydown', handleEscKey)
}
})

onUnmounted(() => {
if (keyboardEsc) {
document.removeEventListener('keydown', handleEscKey)
}
})

const handleClose = () => {
emit('close')
}

const handleCancel = () => {
emit('cancel')
}

const handleOk = () => {
emit('ok')
}

const handleEscKey = (event: KeyboardEvent) => {
if (keyboardEsc && event.key === 'Escape') {
handleClose()
}
}
</script>
67 changes: 67 additions & 0 deletions src/components/__tests__/DxhModal.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { mount } from '@vue/test-utils'
import DxhModal from '@/components/DxhModal.vue'

describe('DxhModal.vue', () => {
let wrapper: any

beforeEach(() => {
wrapper = mount(DxhModal, {
props: {
title: 'Test Modal',
footer: true,
open: true,
keyboardEsc: true,
zIndex: 1000,
persistent: false
}
})
})

afterEach(() => {
wrapper.unmount()
})

it('renders modal with title and footer', () => {
const title = wrapper.find('[data-test="modal-title"]')
const footer = wrapper.find('[data-test="modal-footer"]')

expect(title.exists()).toBe(true)
expect(title.text()).toBe('Test Modal')
expect(footer.exists()).toBe(true)
})

it('emits cancel event on cancel button click', async () => {
const cancelButton = wrapper.find('[data-test="cancel-button"]')

await cancelButton.trigger('click')

const cancelEvent = wrapper.emitted('cancel')
expect(cancelEvent).toBeTruthy()
})

it('emits ok event on ok button click', async () => {
const okButton = wrapper.find('[data-test="ok-button"]')

await okButton.trigger('click')

const okEvent = wrapper.emitted('ok')
expect(okEvent).toBeTruthy()
})

it('closes modal on outside click', async () => {
const modalOverlay = wrapper.find('[data-test="modal-overlay"]')

await modalOverlay.trigger('click')

const cancelEvent = wrapper.emitted('close')
expect(cancelEvent).toBeTruthy()
})

it('closes modal on pressing ESC key', async () => {
await wrapper.trigger('keydown.esc')

const cancelEvent = wrapper.emitted('close')
expect(cancelEvent).toBeUndefined()
})
})
7 changes: 4 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import DButton from "./components/DButton.vue"
import DInput from "./components/DInput.vue"
import DButton from './components/DButton.vue'
import DInput from './components/DInput.vue'
import DxhModal from './components/DxhModal.vue'

export default {DButton, DInput}
export default { DButton, DInput, DxhModal }