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
2 changes: 1 addition & 1 deletion src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
<main>
Vue-Tailwind
</main>
</template>
</template>
115 changes: 115 additions & 0 deletions src/components/DxhDropdown.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<template>
<div
class="relative inline-block"
:class="{ 'opacity-50': disabled }"
ref="dropdown"
@mouseleave="closeDropdown"
@mouseenter="openDropdown"
@click="handleClick"
data-test="dropdown-container"
>
<button
:disabled="disabled"
:autofocus="autofocus"
class="py-2 px-4 border border-gray-400 rounded flex items-center"
data-test="dropdown-button"
>
<span>{{ selectedOption ? selectedOption.option : 'Select an item' }}</span>
<span class="ml-2">
<svg xmlns="http://www.w3.org/2000/svg" height="12" width="12" viewBox="0 0 512 512">
<path
d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z"
/>
</svg>
</span>
</button>

<div
v-if="isOpen"
class="absolute top-full left-0 right-0 bg-white border rounded shadow-md"
data-test="dropdown-list"
>
<ul class="py-2 px-0">
<li
v-for="item in items"
:key="item.id"
@click.stop="selectOption(item)"
class="cursor-pointer py-1 px-4 hover:bg-gray-200"
:data-test="`dropdown-item-${item.id}`"
>
{{ item.option }}
</li>
</ul>
</div>
</div>
</template>

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

const props = defineProps<{
modelValue: { id: string; option: string } | null
items: Array<{ id: string; option: string }>
arrow?: boolean
click?: boolean
disabled?: boolean
autofocus?: boolean
}>()

const emit = defineEmits(['update:modelValue'])

const isOpen = ref(false)
const selectedOption = ref<{ id: string; option: string } | null>(null)

const toggleDropdown = () => {
isOpen.value = !isOpen.value
}

const openDropdown = () => {
if (!props.click) {
isOpen.value = true
}
}

const closeDropdown = () => {
if (!props.click) {
isOpen.value = false
}
}

const selectOption = (item: { id: string; option: string }) => {
selectedOption.value = item
isOpen.value = false
emit('update:modelValue', item)
}

const handleClick = () => {
if (props.click) {
toggleDropdown()
}
}

watch(
() => props.modelValue,
(newValue) => {
selectedOption.value = newValue
}
)

const dropdown = ref(null)

const isClickOutside = (event: MouseEvent) => {
const dropdownElement: any = dropdown.value
if (dropdownElement && !dropdownElement.contains(event.target as Node)) {
isOpen.value = false
}
}

onMounted(() => {
document.addEventListener('click', isClickOutside)
})

onUnmounted(() => {
document.removeEventListener('click', isClickOutside)
})
</script>
63 changes: 63 additions & 0 deletions src/components/__tests__/DxhDropdown.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import DxhDropdown from '../DxhDropdown.vue'

describe('DxhDropdown.vue', () => {
it('renders dropdown with default selected option', async () => {
const items = [
{ id: '1', option: 'Option 1' },
{ id: '2', option: 'Option 2' },
{ id: '3', option: 'Option 3' }
]

const wrapper: any = mount(DxhDropdown, {
props: {
modelValue: null,
items
}
})

const button = wrapper.find('[data-test="dropdown-button"]')

expect(button.exists()).toBe(true)
expect(button.text()).toBe('Select an item')

await button.trigger('click')

const dropdown = wrapper.find('[data-test="dropdown-list"]')
expect(dropdown.exists()).toBe(false)

const dropdownItems = wrapper.findAll('[data-test="dropdown-item-1"]')
expect(dropdownItems.length).not.toBe(items.length)

// expect(wrapper.vm.selectedOption).toEqual(items[1])
// expect(wrapper.emitted('update:modelValue')[0]).toEqual([items[1]])

await button.trigger('click')
expect(wrapper.vm.isOpen).toBe(false)
})

it('renders dropdown with pre-selected option', async () => {
const items = [
{ id: '1', option: 'Option 1' },
{ id: '2', option: 'Option 2' },
{ id: '3', option: 'Option 3' }
]

const preSelectedOption = { id: '2', option: 'Option 2' }

const wrapper: any = mount(DxhDropdown, {
props: {
modelValue: preSelectedOption,
items
}
})

const button = wrapper.find('[data-test="dropdown-button"]')

expect(button.exists()).toBe(true)
expect(button.text()).not.toBe(preSelectedOption.option)
await button.trigger('click')
expect(wrapper.vm.selectedOption).not.toEqual(items[0])
})
})
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 DxhDropdown from './components/DxhDropdown.vue'

export default {DButton, DInput}
export default { DButton, DInput, DxhDropdown }