refactor(components): [collapse] (#10289)

* refactor(components): [collapse]

* Extract logic code out of component file.
* Extract DOM code out of component file.
* Reorganize code for better readabilities.

* chore: apply suggestions

Co-authored-by: JeremyWuuuuu <15975785+JeremyWuuuuu@users.noreply.github.com>
This commit is contained in:
Jeremy
2022-11-01 00:27:47 +08:00
committed by GitHub
parent b45346cc93
commit 4ff8f28ebc
9 changed files with 189 additions and 109 deletions

View File

@@ -4,7 +4,7 @@ import { describe, expect, test, vi } from 'vitest'
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'
import type { CollapseItemInstance } from '../src/instance'
describe('Collapse.vue', () => {
test('create', async () => {

View File

@@ -11,3 +11,4 @@ export const ElCollapseItem = withNoopInstall(CollapseItem)
export * from './src/collapse'
export * from './src/collapse-item'
export type { CollapseInstance, CollapseItemInstance } from './src/instance'

View File

@@ -1,7 +1,6 @@
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: {
@@ -15,5 +14,3 @@ export const collapseItemProps = buildProps({
disabled: Boolean,
} as const)
export type CollapseItemProps = ExtractPropTypes<typeof collapseItemProps>
export type CollapseItemInstance = InstanceType<typeof CollapseItem>

View File

@@ -1,24 +1,14 @@
<template>
<div
:class="[
ns.b('item'),
ns.is('active', isActive),
ns.is('disabled', disabled),
]"
>
<div :class="rootKls">
<div
role="tab"
:aria-expanded="isActive"
:aria-controls="ns.b(`content-${id}`)"
:aria-describedby="ns.b(`content-${id}`)"
:aria-controls="scopedContentId"
:aria-describedby="scopedContentId"
>
<div
:id="ns.b(`head-${id}`)"
:class="[
ns.be('item', 'header'),
ns.is('active', isActive),
{ focusing: focusing && !disabled },
]"
:id="scopedHeadId"
:class="headKls"
role="button"
:tabindex="disabled ? -1 : 0"
@click="handleHeaderClick"
@@ -27,7 +17,7 @@
@blur="focusing = false"
>
<slot name="title">{{ title }}</slot>
<el-icon :class="[ns.be('item', 'arrow'), ns.is('active', isActive)]">
<el-icon :class="arrowKls">
<arrow-right />
</el-icon>
</div>
@@ -35,13 +25,13 @@
<el-collapse-transition>
<div
v-show="isActive"
:id="ns.b(`content-${id}`)"
:class="ns.be('item', 'wrap')"
:id="scopedContentId"
:class="itemWrapperKls"
role="tabpanel"
:aria-hidden="!isActive"
:aria-labelledby="ns.b(`head-${id}`)"
:aria-labelledby="scopedHeadId"
>
<div :class="ns.be('item', 'content')">
<div :class="itemContentKls">
<slot />
</div>
</div>
@@ -50,52 +40,35 @@
</template>
<script lang="ts" setup>
import { computed, inject, 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 { useCollapseItem, useCollapseItemDOM } from './use-collapse-item'
defineOptions({
name: 'ElCollapseItem',
})
const props = defineProps(collapseItemProps)
const {
focusing,
id,
isActive,
handleFocus,
handleHeaderClick,
handleEnterClick,
} = useCollapseItem(props)
const collapse = inject(collapseContextKey)
const ns = useNamespace('collapse')
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 handleEnterClick = () => {
collapse?.handleItemClick(props.name)
}
const {
arrowKls,
headKls,
rootKls,
itemWrapperKls,
itemContentKls,
scopedContentId,
scopedHeadId,
} = useCollapseItemDOM(props, { focusing, isActive, id })
defineExpose({
/** @description current collapse-item whether active */

View File

@@ -7,7 +7,6 @@ import {
} from '@element-plus/utils'
import { CHANGE_EVENT, UPDATE_MODEL_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
@@ -30,5 +29,3 @@ export const collapseEmits = {
[CHANGE_EVENT]: emitChangeFn,
}
export type CollapseEmits = typeof collapseEmits
export type CollapseInstance = InstanceType<typeof Collapse>

View File

@@ -1,17 +1,12 @@
<template>
<div :class="ns.b()" role="tablist" aria-multiselectable="true">
<div :class="rootKls" role="tablist" aria-multiselectable="true">
<slot />
</div>
</template>
<script lang="ts" setup>
import { provide, ref, watch } from 'vue'
import { ensureArray } from '@element-plus/utils'
import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '@element-plus/constants'
import { useNamespace } from '@element-plus/hooks'
import { collapseContextKey } from '@element-plus/tokens'
import { collapseEmits, collapseProps } from './collapse'
import type { CollapseActiveName } from './collapse'
import { useCollapse, useCollapseDOM } from './use-collapse'
defineOptions({
name: 'ElCollapse',
@@ -19,47 +14,9 @@ defineOptions({
const props = defineProps(collapseProps)
const emit = defineEmits(collapseEmits)
const ns = useNamespace('collapse')
const activeNames = ref(ensureArray(props.modelValue))
const { activeNames, setActiveNames } = useCollapse(props, emit)
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)
}
}
watch(
() => props.modelValue,
() => (activeNames.value = ensureArray(props.modelValue)),
{ deep: true }
)
provide(collapseContextKey, {
activeNames,
handleItemClick,
})
const { rootKls } = useCollapseDOM()
defineExpose({
/** @description active names */

View File

@@ -0,0 +1,5 @@
import type Collapse from './collapse.vue'
import type CollapseItem from './collapse-item.vue'
export type CollapseInstance = InstanceType<typeof Collapse>
export type CollapseItemInstance = InstanceType<typeof CollapseItem>

View File

@@ -0,0 +1,84 @@
import { computed, inject, ref, unref } from 'vue'
import { useNamespace } from '@element-plus/hooks'
import { generateId } from '@element-plus/utils'
import { collapseContextKey } from '@element-plus/tokens'
import type { CollapseItemProps } from './collapse-item'
export const useCollapseItem = (props: CollapseItemProps) => {
const collapse = inject(collapseContextKey)
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 handleEnterClick = () => {
collapse?.handleItemClick(props.name)
}
return {
focusing,
id,
isActive,
handleFocus,
handleHeaderClick,
handleEnterClick,
}
}
export const useCollapseItemDOM = (
props: CollapseItemProps,
{ focusing, isActive, id }: Partial<ReturnType<typeof useCollapseItem>>
) => {
const ns = useNamespace('collapse')
const rootKls = computed(() => [
ns.b('item'),
ns.is('active', unref(isActive)),
ns.is('disabled', props.disabled),
])
const headKls = computed(() => [
ns.be('item', 'header'),
ns.is('active', unref(isActive)),
{ focusing: unref(focusing) && !props.disabled },
])
const arrowKls = computed(() => [
ns.be('item', 'arrow'),
ns.is('active', unref(isActive)),
])
const itemWrapperKls = computed(() => ns.be('item', 'wrap'))
const itemContentKls = computed(() => ns.be('item', 'content'))
const scopedContentId = computed(() => ns.b(`content-${unref(id)}`))
const scopedHeadId = computed(() => ns.b(`head-${unref(id)}`))
return {
arrowKls,
headKls,
rootKls,
itemWrapperKls,
itemContentKls,
scopedContentId,
scopedHeadId,
}
}

View File

@@ -0,0 +1,66 @@
import { computed, provide, ref, watch } from 'vue'
import { ensureArray } from '@element-plus/utils'
import { useNamespace } from '@element-plus/hooks'
import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '@element-plus/constants'
import { collapseContextKey } from '@element-plus/tokens'
import type { SetupContext } from 'vue'
import type {
CollapseActiveName,
CollapseEmits,
CollapseProps,
} from './collapse'
export const useCollapse = (
props: CollapseProps,
emit: SetupContext<CollapseEmits>['emit']
) => {
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] === 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)
}
}
watch(
() => props.modelValue,
() => (activeNames.value = ensureArray(props.modelValue)),
{ deep: true }
)
provide(collapseContextKey, {
activeNames,
handleItemClick,
})
return {
activeNames,
setActiveNames,
}
}
export const useCollapseDOM = () => {
const ns = useNamespace('collapse')
const rootKls = computed(() => ns.b())
return {
rootKls,
}
}