mirror of
https://github.com/element-plus/element-plus.git
synced 2025-01-24 11:05:17 +08:00
feat(collapse): add collapse
This commit is contained in:
parent
b7cb021561
commit
c2341c72eb
@ -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"
|
||||
}
|
||||
}
|
||||
|
142
packages/collapse/__tests__/collapse.spec.ts
Normal file
142
packages/collapse/__tests__/collapse.spec.ts
Normal 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'])
|
||||
})
|
||||
})
|
52
packages/collapse/doc/basic.vue
Normal file
52
packages/collapse/doc/basic.vue
Normal 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>
|
5
packages/collapse/doc/index.stories.ts
Normal file
5
packages/collapse/doc/index.stories.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export { default as BasicUsage } from './basic.vue'
|
||||
|
||||
export default {
|
||||
title: 'Collapse',
|
||||
}
|
7
packages/collapse/index.ts
Normal file
7
packages/collapse/index.ts
Normal 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)
|
||||
}
|
15
packages/collapse/package.json
Normal file
15
packages/collapse/package.json
Normal 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"
|
||||
}
|
||||
}
|
126
packages/collapse/src/collapse-item.vue
Normal file
126
packages/collapse/src/collapse-item.vue
Normal 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>
|
76
packages/collapse/src/collapse.vue
Normal file
76
packages/collapse/src/collapse.vue
Normal 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>
|
@ -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)
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
81
packages/transitions/collapse-transition.vue
Normal file
81
packages/transitions/collapse-transition.vue
Normal 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>
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user