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:
bqy
2022-03-01 20:44:32 +08:00
committed by GitHub
parent 31bed90a2e
commit e083447b48
11 changed files with 250 additions and 220 deletions

View File

@@ -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)
})

View File

@@ -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'

View 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>

View File

@@ -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>

View 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>

View File

@@ -1,6 +0,0 @@
import type { Ref } from 'vue'
export interface CollapseProvider {
activeNames: Ref
handleItemClick: (name: string | number) => void
}

View File

@@ -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>

View 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')

View File

@@ -1,5 +1,6 @@
export * from './form'
export * from './button'
export * from './collapse'
export * from './breadcrumb'
export * from './pagination'
export * from './config-provider'

View File

@@ -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'

View File

@@ -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[]