mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
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:
@@ -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 () => {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */
|
||||
|
||||
5
packages/components/collapse/src/instance.ts
Normal file
5
packages/components/collapse/src/instance.ts
Normal 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>
|
||||
84
packages/components/collapse/src/use-collapse-item.ts
Normal file
84
packages/components/collapse/src/use-collapse-item.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
66
packages/components/collapse/src/use-collapse.ts
Normal file
66
packages/components/collapse/src/use-collapse.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user