feat(collapse): add collapse

This commit is contained in:
bastarder 2020-08-07 16:51:34 +08:00 committed by jeremywu
parent b7cb021561
commit c2341c72eb
12 changed files with 519 additions and 5 deletions

View File

@ -66,6 +66,7 @@
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.4.4",
"lodash-es": "^4.17.15"
"lodash-es": "^4.17.15",
"mitt": "^2.1.0"
}
}

View File

@ -0,0 +1,142 @@
import { mount } from '@vue/test-utils'
import { nextTick } from 'vue'
import Collapse from '../src/collapse.vue'
import CollapseItem from '../src/collapse-item.vue'
describe('Collapse.vue', () => {
test('create', async () => {
const wrapper = mount({
components: {
'el-collapse': Collapse,
'el-collapse-item': CollapseItem,
},
data() {
return {
activeNames: ['1'],
}
},
template: `
<el-collapse v-model="activeNames">
<el-collapse-item title="title1" name="1">
<div class="content">111</div>
</el-collapse-item>
<el-collapse-item title="title2" name="2">
<div class="content">222</div>
</el-collapse-item>
<el-collapse-item title="title3" name="3">
<div class="content">333</div>
</el-collapse-item>
<el-collapse-item title="title4" name="4">
<div class="content">444</div>
</el-collapse-item>
</el-collapse>
`,
})
const vm = wrapper.vm
const collapseWrapper = wrapper.findComponent(Collapse)
const collapseItemWrappers = collapseWrapper.findAllComponents(CollapseItem)
const collapseItemHeaderEls = vm.$el.querySelectorAll('.el-collapse-item__header')
expect(collapseItemWrappers[0].vm.isActive).toBe(true)
collapseItemHeaderEls[2].click()
await nextTick()
expect(collapseItemWrappers[0].vm.isActive).toBe(true)
expect(collapseItemWrappers[2].vm.isActive).toBe(true)
collapseItemHeaderEls[0].click()
await nextTick()
expect(collapseItemWrappers[0].vm.isActive).toBe(false)
})
test('accordion', async () => {
const wrapper = mount({
components: {
'el-collapse': Collapse,
'el-collapse-item': CollapseItem,
},
data() {
return {
activeNames: ['1'],
}
},
template: `
<el-collapse accordion v-model="activeNames">
<el-collapse-item title="title1" name="1">
<div class="content">111</div>
</el-collapse-item>
<el-collapse-item title="title2" name="2">
<div class="content">222</div>
</el-collapse-item>
<el-collapse-item title="title3" name="3">
<div class="content">333</div>
</el-collapse-item>
<el-collapse-item title="title4" name="4">
<div class="content">444</div>
</el-collapse-item>
</el-collapse>
`,
})
const vm = wrapper.vm
const collapseWrapper = wrapper.findComponent(Collapse)
const collapseItemWrappers = collapseWrapper.findAllComponents(CollapseItem)
const collapseItemHeaderEls = vm.$el.querySelectorAll('.el-collapse-item__header')
expect(collapseItemWrappers[0].vm.isActive).toBe(true)
collapseItemHeaderEls[2].click()
await nextTick()
expect(collapseItemWrappers[0].vm.isActive).toBe(false)
expect(collapseItemWrappers[2].vm.isActive).toBe(true)
collapseItemHeaderEls[0].click()
await nextTick()
expect(collapseItemWrappers[0].vm.isActive).toBe(true)
expect(collapseItemWrappers[2].vm.isActive).toBe(false)
})
test('event:change', async () => {
const wrapper = mount({
components: {
'el-collapse': Collapse,
'el-collapse-item': CollapseItem,
},
data() {
return {
activeNames: ['1'],
}
},
template: `
<el-collapse v-model="activeNames">
<el-collapse-item title="title1" name="1">
<div class="content">111</div>
</el-collapse-item>
<el-collapse-item title="title2" name="2">
<div class="content">222</div>
</el-collapse-item>
<el-collapse-item title="title3" name="3">
<div class="content">333</div>
</el-collapse-item>
<el-collapse-item title="title4" name="4">
<div class="content">444</div>
</el-collapse-item>
</el-collapse>
`,
})
const vm = wrapper.vm
const collapseWrapper = wrapper.findComponent(Collapse)
const collapseItemWrappers = collapseWrapper.findAllComponents(CollapseItem)
const collapseItemHeaderEls = vm.$el.querySelectorAll('.el-collapse-item__header')
expect(collapseItemWrappers[0].vm.isActive).toBe(true)
expect(vm.activeNames).toEqual(['1'])
collapseItemHeaderEls[2].click()
await nextTick()
expect(collapseItemWrappers[0].vm.isActive).toBe(true)
expect(collapseItemWrappers[2].vm.isActive).toBe(true)
expect(vm.activeNames).toEqual(['1', '3'])
collapseItemHeaderEls[0].click()
await nextTick()
expect(collapseItemWrappers[0].vm.isActive).toBe(false)
expect(vm.activeNames).toEqual(['3'])
})
})

View File

@ -0,0 +1,52 @@
<template>
<div>
<div>accordion: false</div>
<el-collapse v-model="activeNames" @update:modelValue="handleChange">
<el-collapse-item title="title1" name="1">
<div>content 1</div>
</el-collapse-item>
<el-collapse-item title="title2" name="2">
<div>content 2</div>
</el-collapse-item>
<el-collapse-item title="title3" name="3">
<div>content 3</div>
</el-collapse-item>
<el-collapse-item title="title4">
<div>content 4</div>
<div>content 4-1</div>
</el-collapse-item>
</el-collapse>
<hr>
<div>accordion: true</div>
<el-collapse v-model="activeNames2" accordion>
<el-collapse-item title="title1" name="1">
<div>content 1</div>
</el-collapse-item>
<el-collapse-item title="title2" name="2">
<div>content 2</div>
</el-collapse-item>
<el-collapse-item title="title3" name="3">
<div>content 3</div>
</el-collapse-item>
<el-collapse-item title="title4">
<div>content 4</div>
<div>content 4-1</div>
</el-collapse-item>
</el-collapse>
</div>
</template>
<script>
export default {
data() {
return {
activeNames: ['1'],
activeNames2: ['1'],
}
},
methods: {
handleChange(val) {
console.log(val)
},
},
}
</script>

View File

@ -0,0 +1,5 @@
export { default as BasicUsage } from './basic.vue'
export default {
title: 'Collapse',
}

View File

@ -0,0 +1,7 @@
import { App } from 'vue'
import Collapse from './src/collapse.vue'
import CollapseItem from './src/collapse-item.vue'
export default (app: App): void => {
app.component(Collapse.name, Collapse)
app.component(CollapseItem.name, CollapseItem)
}

View File

@ -0,0 +1,15 @@
{
"name": "@element-plus/collapse",
"version": "0.0.0",
"main": "dist/index.js",
"license": "MIT",
"dependencies": {
"mitt": "^2.1.0"
},
"peerDependencies": {
"vue": "^3.0.0-rc.1"
},
"devDependencies": {
"@vue/test-utils": "^2.0.0-beta.0"
}
}

View File

@ -0,0 +1,126 @@
<template>
<div
class="el-collapse-item"
:class="{'is-active': isActive, 'is-disabled': disabled }"
>
<div
role="tab"
:aria-expanded="isActive"
:aria-controls="`el-collapse-content-${id}`"
:aria-describedby="`el-collapse-content-${id}`"
>
<div
:id="`el-collapse-head-${id}`"
class="el-collapse-item__header"
role="button"
:tabindex="disabled ? undefined : 0"
:class="{
'focusing': focusing,
'is-active': isActive
}"
@click="handleHeaderClick"
@keyup.space.enter.stop="handleEnterClick"
@focus="handleFocus"
@blur="focusing = false"
>
<slot name="title">{{ title }}</slot>
<i
class="el-collapse-item__arrow el-icon-arrow-right"
:class="{'is-active': isActive}"
>
</i>
</div>
</div>
<el-collapse-transition>
<div
v-show="isActive"
:id="`el-collapse-content-${id}`"
class="el-collapse-item__wrap"
role="tabpanel"
:aria-hidden="!isActive"
:aria-labelledby="`el-collapse-head-${id}`"
>
<div class="el-collapse-item__content">
<slot></slot>
</div>
</div>
</el-collapse-transition>
</div>
</template>
<script lang='ts'>
import { defineComponent, PropType, inject, computed, ref } from 'vue'
import { CollapseProvider } from './collapse.vue'
import { generateId } from '../../utils/util'
import ElCollapseTransition from '../../transitions/collapse-transition.vue'
export default defineComponent({
name: 'ElCollapseItem',
components: { ElCollapseTransition },
props: {
title: {
type: String,
default: '',
},
name: {
type: [String, Number] as PropType<string | number>,
default: () => {
return generateId()
},
},
disabled: Boolean,
},
setup(props) {
const collapse = inject<CollapseProvider>('collapse')
const collapseMitt = collapse?.collapseMitt
const contentWrapStyle = ref({
height: 'auto',
display: 'block',
})
const contentHeight = ref(0)
const focusing = ref(false)
const isClick = ref(false)
const id = ref(generateId())
const isActive = computed(() => {
return collapse?.activeNames.value.indexOf(props.name) > -1
})
const handleFocus = () => {
setTimeout(() => {
if(!isClick.value) {
focusing.value = true
} else {
isClick.value = false
}
}, 50)
}
const handleHeaderClick = () => {
if(props.disabled) return
collapseMitt && collapseMitt.emit('item-click', props.name)
focusing.value = false
isClick.value = true
}
const handleEnterClick = () => {
collapseMitt && collapseMitt.emit('item-click', props.name)
}
return {
isActive,
contentWrapStyle,
contentHeight,
focusing,
isClick,
id,
handleFocus,
handleHeaderClick,
handleEnterClick,
collapse,
}
},
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,76 @@
<template>
<div class="el-collapse" role="tablist" aria-multiselectable="true">
<slot></slot>
</div>
</template>
<script lang='ts'>
import { defineComponent, ref, watch, provide, PropType, Ref, onUnmounted } from 'vue'
import mitt from 'mitt'
export interface CollapseProvider {
activeNames: Ref
collapseMitt: mitt.Emitter
}
export default defineComponent({
name: 'ElCollapse',
props: {
accordion: Boolean,
modelValue: {
type: [Array, String, Number] as PropType<string | number | Array<string|number>>,
default: () => [],
},
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const activeNames = ref([].concat(props.modelValue))
const collapseMitt: mitt.Emitter = mitt()
const setActiveNames = (_activeNames) => {
activeNames.value = [].concat(_activeNames)
const value = props.accordion ? activeNames.value[0] : activeNames.value
emit('update:modelValue', value)
}
const handleItemClick = (name) => {
if(props.accordion) {
setActiveNames(
(activeNames.value[0] || activeNames.value[0] === 0) &&
activeNames.value[0] === name ? '' : name,
)
} else {
let _activeNames = activeNames.value.slice(0)
const index = _activeNames.indexOf(name)
if(index > -1) {
_activeNames.splice(index, 1)
} else {
_activeNames.push(name)
}
setActiveNames(_activeNames)
}
}
watch(() => props.modelValue, () => {
activeNames.value = [].concat(props.modelValue)
})
collapseMitt.on('item-click', handleItemClick)
onUnmounted(() => {
collapseMitt.all.clear()
})
provide('collapse', {
activeNames,
collapseMitt,
})
return {
activeNames,
setActiveNames,
handleItemClick,
}
},
})
</script>

View File

@ -17,6 +17,8 @@ import ElSwitch from '@element-plus/switch'
import ElContainer from '@element-plus/container'
import ElNotification from '@element-plus/notification'
import ElRadio from '@element-plus/radio'
import ElPageHeader from '@element-plus/page-header'
import ElCollapse from '@element-plus/collapse'
export {
ElAvatar,
@ -37,6 +39,8 @@ export {
ElContainer,
ElNotification,
ElRadio,
ElPageHeader,
ElCollapse,
}
export default function install(app: App): void {
@ -58,4 +62,6 @@ export default function install(app: App): void {
ElContainer(app)
ElNotification(app)
ElRadio(app)
ElPageHeader(app)
ElCollapse(app)
}

View File

@ -27,12 +27,10 @@
"@element-plus/layout": "^0.0.0",
"@element-plus/link": "^0.0.0",
"@element-plus/progress": "^0.0.0",
"@element-plus/tag": "^0.0.0",
"@element-plus/rate": "^0.0.0",
"@element-plus/breadcrumb": "^0.0.0",
"@element-plus/icon": "^0.0.0",
"@element-plus/switch": "^0.0.0",
"@element-plus/notification": "^0.0.0",
"@element-plus/radio": "^0.0.0"
"@element-plus/radio": "^0.0.0",
"@element-plus/collapse": "^0.0.0"
}
}

View File

@ -0,0 +1,81 @@
<template>
<transition v-on="on">
<slot></slot>
</transition>
</template>
<script lang='ts'>
import { addClass, removeClass } from '../utils/dom'
export default {
name: 'ElCollapseTransition',
setup() {
return {
on: {
beforeEnter(el) {
addClass(el, 'collapse-transition')
if (!el.dataset) el.dataset = {}
el.dataset.oldPaddingTop = el.style.paddingTop
el.dataset.oldPaddingBottom = el.style.paddingBottom
el.style.height = '0'
el.style.paddingTop = 0
el.style.paddingBottom = 0
},
enter(el) {
el.dataset.oldOverflow = el.style.overflow
if (el.scrollHeight !== 0) {
el.style.height = el.scrollHeight + 'px'
el.style.paddingTop = el.dataset.oldPaddingTop
el.style.paddingBottom = el.dataset.oldPaddingBottom
} else {
el.style.height = ''
el.style.paddingTop = el.dataset.oldPaddingTop
el.style.paddingBottom = el.dataset.oldPaddingBottom
}
el.style.overflow = 'hidden'
},
afterEnter(el) {
// for safari: remove class then reset height is necessary
removeClass(el, 'collapse-transition')
el.style.height = ''
el.style.overflow = el.dataset.oldOverflow
},
beforeLeave(el) {
if (!el.dataset) el.dataset = {}
el.dataset.oldPaddingTop = el.style.paddingTop
el.dataset.oldPaddingBottom = el.style.paddingBottom
el.dataset.oldOverflow = el.style.overflow
el.style.height = el.scrollHeight + 'px'
el.style.overflow = 'hidden'
},
leave(el) {
if (el.scrollHeight !== 0) {
// for safari: add class after set height, or it will jump to zero height suddenly, weired
addClass(el, 'collapse-transition')
el.style.height = 0
el.style.paddingTop = 0
el.style.paddingBottom = 0
}
},
afterLeave(el) {
removeClass(el, 'collapse-transition')
el.style.height = ''
el.style.overflow = el.dataset.oldOverflow
el.style.paddingTop = el.dataset.oldPaddingTop
el.style.paddingBottom = el.dataset.oldPaddingBottom
},
},
}
},
}
</script>

View File

@ -9742,6 +9742,11 @@ mississippi@^3.0.0:
stream-each "^1.1.0"
through2 "^2.0.0"
mitt@^2.1.0:
version "2.1.0"
resolved "https://registry.npm.taobao.org/mitt/download/mitt-2.1.0.tgz#f740577c23176c6205b121b2973514eade1b2230"
integrity sha1-90BXfCMXbGIFsSGylzUU6t4bIjA=
mixin-deep@^1.2.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"