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
114 changes: 114 additions & 0 deletions src/components/DxhCollapse.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<template>
<div>
<div v-for="item in items" :key="item.id">
<div
@click="collapsible === 'header' ? handleItemClick(item.id) : ''"
class="bg-gray-200 p-3 flex items-center font-semibold"
:class="[
{ 'space-x-3 flex-row': expandIconPosition === 'left' },
{ 'justify-between flex-row-reverse': expandIconPosition === 'right' },
{ 'cursor-pointer': collapsible === 'header' },
{ 'pointer-events-none opacity-50': collapsible === 'disabled' }
]"
:data-test="`collapse-item-header-${item.id}`"
>
<div
@click="collapsible === 'icon' ? handleItemClick(item.id) : ''"
:class="{ 'cursor-pointer': collapsible === 'icon' }"
>
<span v-if="isItemActive(item.id)">
<slot name="collapse">
<svg
xmlns="http://www.w3.org/2000/svg"
height="14"
width="14"
viewBox="0 0 448 512"
:data-test="`collapse-item-collapse-icon-${item.id}`"
>
<path
d="M432 256c0 17.7-14.3 32-32 32L48 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l352 0c17.7 0 32 14.3 32 32z"
/>
</svg>
</slot>
</span>
<span v-else>
<slot name="expand">
<svg
xmlns="http://www.w3.org/2000/svg"
height="14"
width="14"
viewBox="0 0 448 512"
:data-test="`collapse-item-expand-icon-${item.id}`"
>
<path
d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32V224H48c-17.7 0-32 14.3-32 32s14.3 32 32 32H192V432c0 17.7 14.3 32 32 32s32-14.3 32-32V288H400c17.7 0 32-14.3 32-32s-14.3-32-32-32H256V80z"
/>
</svg>
</slot>
</span>
</div>
<p :data-test="`collapse-item-header-text-${item.id}`">{{ item.header }}</p>
</div>
<div v-if="isItemActive(item.id)" class="p-3" :data-test="`collapse-item-content-${item.id}`">
{{ item.content }}
</div>
</div>
</div>
</template>

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

interface CollapseItem {
id: number
header: string
content: string
}

interface props {
items: CollapseItem[]
accordion?: boolean
defaultActiveKey: number
collapsible?: string | string[]
expandIconPosition?: string
}

const { items, accordion, defaultActiveKey, collapsible, expandIconPosition } = withDefaults(
defineProps<props>(),
{
accordion: false
}
)
const emit = defineEmits(['change'])

const activeItems = ref<any>(accordion ? defaultActiveKey : [defaultActiveKey])

const handleItemClick = (id: number) => {
if (accordion) {
activeItems.value = activeItems.value === id ? null : id
} else {
if (activeItems.value.includes(id)) {
activeItems.value = activeItems.value.filter((item: number) => item !== id)
} else {
activeItems.value = [...activeItems.value, id]
}
}
emit('change', activeItems.value)
}

const isItemActive = (id: number) => {
if (accordion) {
return activeItems.value === id
} else {
return activeItems.value.includes(id)
}
}

onMounted(() => {
if (accordion) {
activeItems.value = defaultActiveKey
} else {
activeItems.value = [defaultActiveKey]
}
})
</script>
77 changes: 77 additions & 0 deletions src/components/__tests__/DxhCollapse.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { mount } from '@vue/test-utils'
import DxhCollapse from '@/components/DxhCollapse.vue'

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

beforeEach(() => {
wrapper = mount(DxhCollapse, {
props: {
items: [
{ id: 1, header: 'Header 1', content: 'Content 1' },
{ id: 2, header: 'Header 2', content: 'Content 2' }
],
defaultActiveKey: 1,
collapsible: 'header',
expandIconPosition: 'left'
}
})
})

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

it('renders with correct initial state', () => {
const header1 = wrapper.find('[data-test="collapse-item-header-1"]')
const header2 = wrapper.find('[data-test="collapse-item-header-2"]')
const content1 = wrapper.find('[data-test="collapse-item-content-1"]')
const content2 = wrapper.find('[data-test="collapse-item-content-2"]')

expect(header1.classes()).toContain('bg-gray-200')
expect(content1.exists()).toBe(true)

expect(content2.exists()).toBe(false)
})

it('toggles active item on header click', async () => {
const header2 = wrapper.find('[data-test="collapse-item-header-2"]')
const content2 = wrapper.find('[data-test="collapse-item-content-2"]')

await header2.trigger('click')

expect(header2.classes()).toContain('bg-gray-200')
expect(content2.exists()).toBe(false)

const header1 = wrapper.find('[data-test="collapse-item-header-1"]')
const content1 = wrapper.find('[data-test="collapse-item-content-1"]')
expect(header1.classes()).toContain('bg-gray-200')
expect(content1.exists()).toBe(true)
})

it('emits change event on item click', async () => {
const header2 = wrapper.find('[data-test="collapse-item-header-2"]')

await header2.trigger('click')

expect(wrapper.emitted().change).toBeTruthy()
expect(wrapper.emitted().change[0][0]).toEqual([1, 2])
})

it('handles click on expand icon', async () => {
const expandIcon2 = wrapper.find('[data-test="collapse-item-expand-icon-2"]')
const content2 = wrapper.find('[data-test="collapse-item-content-2"]')

await expandIcon2.trigger('click')

const header2 = wrapper.find('[data-test="collapse-item-header-2"]')
expect(header2.classes()).toContain('bg-gray-200')
expect(content2.exists()).toBe(false)

const header1 = wrapper.find('[data-test="collapse-item-header-1"]')
const content1 = wrapper.find('[data-test="collapse-item-content-1"]')
expect(header1.classes()).toContain('bg-gray-200')
expect(content1.exists()).toBe(true)
})
})
3 changes: 2 additions & 1 deletion 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 DxhCollapse from './components/DxhCollapse.vue'

export default {DButton, DInput}
export default { DButton, DInput, DxhCollapse }