mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
refactor(components): [collapse] refactor (#6307)
* refactor(components): [collapse] refactor * refactor(components): [collapse] refactor * refactor: rename Co-authored-by: 三咲智子 <sxzz@sxzz.moe>
This commit is contained in:
@@ -2,40 +2,42 @@ import { nextTick } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Collapse from '../src/collapse.vue'
|
||||
import CollapseItem from '../src/collapse-item.vue'
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import type { CollapseItemInstance } from '../src/collapse-item'
|
||||
|
||||
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>
|
||||
`,
|
||||
render() {
|
||||
return (
|
||||
<Collapse v-model={this.activeNames}>
|
||||
<CollapseItem title="title1" name="1">
|
||||
<div class="content">111</div>
|
||||
</CollapseItem>
|
||||
<CollapseItem title="title2" name="2">
|
||||
<div class="content">222</div>
|
||||
</CollapseItem>
|
||||
<CollapseItem title="title3" name="3">
|
||||
<div class="content">333</div>
|
||||
</CollapseItem>
|
||||
<CollapseItem title="title4" name="4">
|
||||
<div class="content">444</div>
|
||||
</CollapseItem>
|
||||
</Collapse>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const vm = wrapper.vm
|
||||
const collapseWrapper = wrapper.findComponent(Collapse)
|
||||
const collapseItemWrappers = collapseWrapper.findAllComponents(CollapseItem)
|
||||
const collapseItemWrappers = collapseWrapper.findAllComponents(
|
||||
CollapseItem
|
||||
) as VueWrapper<CollapseItemInstance>[]
|
||||
const collapseItemHeaderEls = vm.$el.querySelectorAll(
|
||||
'.el-collapse-item__header'
|
||||
)
|
||||
@@ -52,36 +54,36 @@ describe('Collapse.vue', () => {
|
||||
|
||||
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>
|
||||
`,
|
||||
render() {
|
||||
return (
|
||||
<Collapse accordion v-model={this.activeNames}>
|
||||
<CollapseItem title="title1" name="1">
|
||||
<div class="content">111</div>
|
||||
</CollapseItem>
|
||||
<CollapseItem title="title2" name="2">
|
||||
<div class="content">222</div>
|
||||
</CollapseItem>
|
||||
<CollapseItem title="title3" name="3">
|
||||
<div class="content">333</div>
|
||||
</CollapseItem>
|
||||
<CollapseItem title="title4" name="4">
|
||||
<div class="content">444</div>
|
||||
</CollapseItem>
|
||||
</Collapse>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const vm = wrapper.vm
|
||||
const collapseWrapper = wrapper.findComponent(Collapse)
|
||||
const collapseItemWrappers = collapseWrapper.findAllComponents(CollapseItem)
|
||||
const collapseItemWrappers = collapseWrapper.findAllComponents(
|
||||
CollapseItem
|
||||
) as VueWrapper<CollapseItemInstance>[]
|
||||
const collapseItemHeaderEls = vm.$el.querySelectorAll(
|
||||
'.el-collapse-item__header'
|
||||
)
|
||||
@@ -100,39 +102,36 @@ describe('Collapse.vue', () => {
|
||||
test('event:change', async () => {
|
||||
const onChange = jest.fn()
|
||||
const wrapper = mount({
|
||||
components: {
|
||||
'el-collapse': Collapse,
|
||||
'el-collapse-item': CollapseItem,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeNames: ['1'],
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<el-collapse v-model="activeNames" @change="onChange">
|
||||
<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>
|
||||
`,
|
||||
methods: {
|
||||
onChange,
|
||||
render() {
|
||||
return (
|
||||
<Collapse v-model={this.activeNames} onChange={onChange}>
|
||||
<CollapseItem title="title1" name="1">
|
||||
<div class="content">111</div>
|
||||
</CollapseItem>
|
||||
<CollapseItem title="title2" name="2">
|
||||
<div class="content">222</div>
|
||||
</CollapseItem>
|
||||
<CollapseItem title="title3" name="3">
|
||||
<div class="content">333</div>
|
||||
</CollapseItem>
|
||||
<CollapseItem title="title4" name="4">
|
||||
<div class="content">444</div>
|
||||
</CollapseItem>
|
||||
</Collapse>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const vm = wrapper.vm
|
||||
const collapseWrapper = wrapper.findComponent(Collapse)
|
||||
const collapseItemWrappers = collapseWrapper.findAllComponents(CollapseItem)
|
||||
const collapseItemWrappers = collapseWrapper.findAllComponents(
|
||||
CollapseItem
|
||||
) as VueWrapper<CollapseItemInstance>[]
|
||||
const collapseItemHeaderEls = vm.$el.querySelectorAll(
|
||||
'.el-collapse-item__header'
|
||||
)
|
||||
@@ -154,10 +153,6 @@ describe('Collapse.vue', () => {
|
||||
|
||||
test('deep watch modelValue', async () => {
|
||||
const wrapper = mount({
|
||||
components: {
|
||||
'el-collapse': Collapse,
|
||||
'el-collapse-item': CollapseItem,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeNames: ['1'],
|
||||
@@ -166,27 +161,31 @@ describe('Collapse.vue', () => {
|
||||
mounted() {
|
||||
this.activeNames.push('2')
|
||||
},
|
||||
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>
|
||||
`,
|
||||
render() {
|
||||
return (
|
||||
<Collapse v-model={this.activeNames}>
|
||||
<CollapseItem title="title1" name="1">
|
||||
<div class="content">111</div>
|
||||
</CollapseItem>
|
||||
<CollapseItem title="title2" name="2">
|
||||
<div class="content">222</div>
|
||||
</CollapseItem>
|
||||
<CollapseItem title="title3" name="3">
|
||||
<div class="content">333</div>
|
||||
</CollapseItem>
|
||||
<CollapseItem title="title4" name="4">
|
||||
<div class="content">444</div>
|
||||
</CollapseItem>
|
||||
</Collapse>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
const collapseWrapper = wrapper.findComponent(Collapse)
|
||||
const collapseItemWrappers = collapseWrapper.findAllComponents(CollapseItem)
|
||||
const collapseItemWrappers = collapseWrapper.findAllComponents(
|
||||
CollapseItem
|
||||
) as VueWrapper<CollapseItemInstance>[]
|
||||
expect(collapseItemWrappers[0].vm.isActive).toBe(true)
|
||||
expect(collapseItemWrappers[1].vm.isActive).toBe(true)
|
||||
})
|
||||
@@ -8,3 +8,6 @@ export const ElCollapse = withInstall(Collapse, {
|
||||
})
|
||||
export default ElCollapse
|
||||
export const ElCollapseItem = withNoopInstall(CollapseItem)
|
||||
|
||||
export * from './src/collapse'
|
||||
export * from './src/collapse-item'
|
||||
|
||||
19
packages/components/collapse/src/collapse-item.ts
Normal file
19
packages/components/collapse/src/collapse-item.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { buildProps, definePropType, generateId } from '@element-plus/utils'
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import type { CollapseActiveName } from './collapse'
|
||||
import type CollapseItem from './collapse-item.vue'
|
||||
|
||||
export const collapseItemProps = buildProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: definePropType<CollapseActiveName>([String, Number]),
|
||||
default: () => generateId(),
|
||||
},
|
||||
disabled: Boolean,
|
||||
} as const)
|
||||
export type CollapseItemProps = ExtractPropTypes<typeof collapseItemProps>
|
||||
|
||||
export type CollapseItemInstance = InstanceType<typeof CollapseItem>
|
||||
@@ -48,84 +48,57 @@
|
||||
</el-collapse-transition>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, inject, computed, ref } from 'vue'
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { inject, computed, ref } from 'vue'
|
||||
import { generateId } from '@element-plus/utils'
|
||||
import ElCollapseTransition from '@element-plus/components/collapse-transition'
|
||||
import ElIcon from '@element-plus/components/icon'
|
||||
import { ArrowRight } from '@element-plus/icons-vue'
|
||||
import { useNamespace } from '@element-plus/hooks'
|
||||
import { collapseContextKey } from '@element-plus/tokens'
|
||||
import { collapseItemProps } from './collapse-item'
|
||||
|
||||
import type { PropType } from 'vue'
|
||||
import type { CollapseProvider } from './collapse.type'
|
||||
|
||||
export default defineComponent({
|
||||
defineOptions({
|
||||
name: 'ElCollapseItem',
|
||||
components: { ElCollapseTransition, ElIcon, ArrowRight },
|
||||
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 ns = useNamespace('collapse')
|
||||
})
|
||||
|
||||
const contentWrapStyle = ref({
|
||||
height: 'auto',
|
||||
display: 'block',
|
||||
})
|
||||
const contentHeight = ref(0)
|
||||
const focusing = ref(false)
|
||||
const isClick = ref(false)
|
||||
const id = ref(generateId())
|
||||
const props = defineProps(collapseItemProps)
|
||||
|
||||
const isActive = computed(() => {
|
||||
return collapse?.activeNames.value.indexOf(props.name) > -1
|
||||
})
|
||||
const collapse = inject(collapseContextKey)
|
||||
const ns = useNamespace('collapse')
|
||||
|
||||
const handleFocus = () => {
|
||||
setTimeout(() => {
|
||||
if (!isClick.value) {
|
||||
focusing.value = true
|
||||
} else {
|
||||
isClick.value = false
|
||||
}
|
||||
}, 50)
|
||||
const focusing = ref(false)
|
||||
const isClick = ref(false)
|
||||
const id = ref(generateId())
|
||||
|
||||
const isActive = computed(() =>
|
||||
collapse?.activeNames.value.includes(props.name)
|
||||
)
|
||||
|
||||
const handleFocus = () => {
|
||||
setTimeout(() => {
|
||||
if (!isClick.value) {
|
||||
focusing.value = true
|
||||
} else {
|
||||
isClick.value = false
|
||||
}
|
||||
}, 50)
|
||||
}
|
||||
|
||||
const handleHeaderClick = () => {
|
||||
if (props.disabled) return
|
||||
collapse?.handleItemClick(props.name)
|
||||
focusing.value = false
|
||||
isClick.value = true
|
||||
}
|
||||
const handleHeaderClick = () => {
|
||||
if (props.disabled) return
|
||||
collapse?.handleItemClick(props.name)
|
||||
focusing.value = false
|
||||
isClick.value = true
|
||||
}
|
||||
|
||||
const handleEnterClick = () => {
|
||||
collapse?.handleItemClick(props.name)
|
||||
}
|
||||
const handleEnterClick = () => {
|
||||
collapse?.handleItemClick(props.name)
|
||||
}
|
||||
|
||||
return {
|
||||
isActive,
|
||||
contentWrapStyle,
|
||||
contentHeight,
|
||||
focusing,
|
||||
isClick,
|
||||
id,
|
||||
ns,
|
||||
handleFocus,
|
||||
handleHeaderClick,
|
||||
handleEnterClick,
|
||||
collapse,
|
||||
}
|
||||
},
|
||||
defineExpose({
|
||||
/** @description current collapse-item whether active */
|
||||
isActive,
|
||||
})
|
||||
</script>
|
||||
|
||||
34
packages/components/collapse/src/collapse.ts
Normal file
34
packages/components/collapse/src/collapse.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
buildProps,
|
||||
definePropType,
|
||||
mutable,
|
||||
isNumber,
|
||||
isString,
|
||||
} from '@element-plus/utils'
|
||||
import { UPDATE_MODEL_EVENT, CHANGE_EVENT } from '@element-plus/constants'
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import type Collapse from './collapse.vue'
|
||||
import type { Arrayable } from '@element-plus/utils'
|
||||
|
||||
export type CollapseActiveName = string | number
|
||||
export type CollapseModelValue = Arrayable<CollapseActiveName>
|
||||
|
||||
export const emitChangeFn = (value: CollapseModelValue) =>
|
||||
typeof isNumber(value) || isString(value) || Array.isArray(value)
|
||||
|
||||
export const collapseProps = buildProps({
|
||||
accordion: Boolean,
|
||||
modelValue: {
|
||||
type: definePropType<CollapseModelValue>([Array, String, Number]),
|
||||
default: () => mutable([] as const),
|
||||
},
|
||||
} as const)
|
||||
export type CollapseProps = ExtractPropTypes<typeof collapseProps>
|
||||
|
||||
export const collapseEmits = {
|
||||
[UPDATE_MODEL_EVENT]: emitChangeFn,
|
||||
[CHANGE_EVENT]: emitChangeFn,
|
||||
}
|
||||
export type CollapseEmits = typeof collapseEmits
|
||||
|
||||
export type CollapseInstance = InstanceType<typeof Collapse>
|
||||
@@ -1,6 +0,0 @@
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
export interface CollapseProvider {
|
||||
activeNames: Ref
|
||||
handleItemClick: (name: string | number) => void
|
||||
}
|
||||
@@ -4,77 +4,67 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, watch, provide } from 'vue'
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, provide } from 'vue'
|
||||
import { ensureArray } from '@element-plus/utils'
|
||||
import { UPDATE_MODEL_EVENT, CHANGE_EVENT } from '@element-plus/constants'
|
||||
import { useNamespace } from '@element-plus/hooks'
|
||||
import type { CollapseProvider } from './collapse.type'
|
||||
import { collapseContextKey } from '@element-plus/tokens'
|
||||
import { collapseProps, collapseEmits } from './collapse'
|
||||
import type { CollapseActiveName } from './collapse'
|
||||
|
||||
export default defineComponent({
|
||||
defineOptions({
|
||||
name: 'ElCollapse',
|
||||
props: {
|
||||
accordion: Boolean,
|
||||
modelValue: {
|
||||
type: [Array, String, Number] as PropType<
|
||||
string | number | Array<string | number>
|
||||
>,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
emits: [UPDATE_MODEL_EVENT, CHANGE_EVENT],
|
||||
setup(props, { emit }) {
|
||||
const ns = useNamespace('collapse')
|
||||
const activeNames = ref([].concat(props.modelValue))
|
||||
})
|
||||
const props = defineProps(collapseProps)
|
||||
const emit = defineEmits(collapseEmits)
|
||||
|
||||
const setActiveNames = (_activeNames) => {
|
||||
activeNames.value = [].concat(_activeNames)
|
||||
const value = props.accordion ? activeNames.value[0] : activeNames.value
|
||||
emit(UPDATE_MODEL_EVENT, value)
|
||||
emit(CHANGE_EVENT, value)
|
||||
const ns = useNamespace('collapse')
|
||||
const activeNames = ref(ensureArray(props.modelValue))
|
||||
|
||||
const setActiveNames = (_activeNames: CollapseActiveName[]) => {
|
||||
activeNames.value = _activeNames
|
||||
const value = props.accordion ? activeNames.value[0] : activeNames.value
|
||||
emit(UPDATE_MODEL_EVENT, value)
|
||||
emit(CHANGE_EVENT, value)
|
||||
}
|
||||
|
||||
const handleItemClick = (name: CollapseActiveName) => {
|
||||
if (props.accordion) {
|
||||
setActiveNames([
|
||||
(activeNames.value[0] || activeNames.value[0] === 0) &&
|
||||
activeNames.value[0] === name
|
||||
? ''
|
||||
: name,
|
||||
])
|
||||
} else {
|
||||
const _activeNames = [...activeNames.value]
|
||||
const index = _activeNames.indexOf(name)
|
||||
|
||||
if (index > -1) {
|
||||
_activeNames.splice(index, 1)
|
||||
} else {
|
||||
_activeNames.push(name)
|
||||
}
|
||||
setActiveNames(_activeNames)
|
||||
}
|
||||
}
|
||||
|
||||
const handleItemClick = (name) => {
|
||||
if (props.accordion) {
|
||||
setActiveNames(
|
||||
(activeNames.value[0] || activeNames.value[0] === 0) &&
|
||||
activeNames.value[0] === name
|
||||
? ''
|
||||
: name
|
||||
)
|
||||
} else {
|
||||
const _activeNames = activeNames.value.slice(0)
|
||||
const index = _activeNames.indexOf(name)
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => (activeNames.value = ensureArray(props.modelValue)),
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
if (index > -1) {
|
||||
_activeNames.splice(index, 1)
|
||||
} else {
|
||||
_activeNames.push(name)
|
||||
}
|
||||
setActiveNames(_activeNames)
|
||||
}
|
||||
}
|
||||
provide(collapseContextKey, {
|
||||
activeNames,
|
||||
handleItemClick,
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
activeNames.value = [].concat(props.modelValue)
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
)
|
||||
|
||||
provide<CollapseProvider>('collapse', {
|
||||
activeNames,
|
||||
handleItemClick,
|
||||
})
|
||||
|
||||
return {
|
||||
ns,
|
||||
activeNames,
|
||||
setActiveNames,
|
||||
handleItemClick,
|
||||
}
|
||||
},
|
||||
defineExpose({
|
||||
/** @description active names */
|
||||
activeNames,
|
||||
/** @description set active names */
|
||||
setActiveNames,
|
||||
})
|
||||
</script>
|
||||
|
||||
10
packages/tokens/collapse.ts
Normal file
10
packages/tokens/collapse.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { CollapseActiveName } from '@element-plus/components/collapse'
|
||||
import type { InjectionKey, Ref } from 'vue'
|
||||
|
||||
export interface CollapseContext {
|
||||
activeNames: Ref<CollapseActiveName[]>
|
||||
handleItemClick: (name: CollapseActiveName) => void
|
||||
}
|
||||
|
||||
export const collapseContextKey: InjectionKey<CollapseContext> =
|
||||
Symbol('collapseContextKey')
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './form'
|
||||
export * from './button'
|
||||
export * from './collapse'
|
||||
export * from './breadcrumb'
|
||||
export * from './pagination'
|
||||
export * from './config-provider'
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
export const unique = <T>(arr: T[]) => [...new Set(arr)]
|
||||
|
||||
// TODO: rename to `ensureArray`
|
||||
/** like `_.castArray`, except falsy value returns empty array. */
|
||||
export const castArray = (arr: any): any[] => {
|
||||
if (!arr && arr !== 0) return []
|
||||
return Array.isArray(arr) ? arr : [arr]
|
||||
}
|
||||
|
||||
// TODO: remove import alias
|
||||
// avoid naming conflicts
|
||||
export { castArray as ensureArray } from 'lodash-unified'
|
||||
|
||||
@@ -10,3 +10,5 @@ export type HTMLElementCustomized<T> = HTMLElement & T
|
||||
* @see {@link https://github.com/sindresorhus/meta/discussions/7}
|
||||
*/
|
||||
export type Nullable<T> = T | null
|
||||
|
||||
export type Arrayable<T> = T | T[]
|
||||
|
||||
Reference in New Issue
Block a user