chore(components): [el-carousel] code style refactor (#6693)

* chore(components): [el-carousel] code style refactor

- Remove duplicated code
- Simplified the code for components

* Wrap resize handle into shallowRef

* Export CourouselItemState type

* Enhance itemInStage method

* Refine code
This commit is contained in:
JeremyWuuuuu 2022-03-17 22:20:25 +08:00 committed by GitHub
parent 176bc652ea
commit 6b74660a20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 242 additions and 217 deletions

View File

@ -2,27 +2,49 @@ import { nextTick, reactive } from 'vue'
import { mount } from '@vue/test-utils'
import Carousel from '../src/carousel.vue'
import CarouselItem from '../src/carousel-item.vue'
import type { VueWrapper } from '@vue/test-utils'
import type { CarouselInstance } from '../src/carousel'
const wait = (ms = 100) =>
new Promise((resolve) => setTimeout(() => resolve(0), ms))
const generateCarouselItems = (count = 3) => {
const generateCarouselItems = (count = 3, hasLabel = false) => {
const list = Array.from({ length: count }, (_, index) => index + 1)
return list.map((i) => <CarouselItem key={i}></CarouselItem>)
return list.map((i) =>
hasLabel ? <CarouselItem key={i} label={i} /> : <CarouselItem key={i} />
)
}
describe('Carousel', () => {
it('create', () => {
const wrapper = mount({
let wrapper: VueWrapper<any>
const createComponent = (
props: any = {},
count?: number,
hasLabel?: boolean
) => {
return mount({
setup() {
return () => (
<div>
<Carousel ref="carousel">{generateCarouselItems()}</Carousel>
<Carousel {...props}>
{generateCarouselItems(count, hasLabel)}
</Carousel>
</div>
)
},
})
}
afterEach(() => {
wrapper.unmount()
})
it('create', () => {
wrapper = createComponent({
ref: 'carousel',
})
const carousel = wrapper.findComponent({ ref: 'carousel' })
.vm as CarouselInstance
@ -31,11 +53,9 @@ describe('Carousel', () => {
})
it('auto play', async (done) => {
const wrapper = mount(() => (
<div>
<Carousel interval={50}>{generateCarouselItems()}</Carousel>
</div>
))
wrapper = createComponent({
interval: 50,
})
await nextTick()
await wait(10)
@ -47,13 +67,10 @@ describe('Carousel', () => {
})
it('initial index', async (done) => {
const wrapper = mount(() => (
<div>
<Carousel autoplay={false} initial-index={1}>
{generateCarouselItems()}
</Carousel>
</div>
))
wrapper = createComponent({
autoplay: false,
'initial-index': 1,
})
await nextTick()
await wait(10)
@ -67,11 +84,9 @@ describe('Carousel', () => {
})
it('reset timer', async (done) => {
const wrapper = mount(() => (
<div>
<Carousel interval={500}>{generateCarouselItems()}</Carousel>
</div>
))
wrapper = createComponent({
interval: 500,
})
await nextTick()
const items = wrapper.vm.$el.querySelectorAll('.el-carousel__item')
await wrapper.trigger('mouseenter')
@ -90,21 +105,12 @@ describe('Carousel', () => {
oldVal: -1,
})
mount({
setup() {
const handleChange = (val: number, oldVal: number) => {
state.val = val
state.oldVal = oldVal
}
return () => (
<div>
<Carousel interval={50} onChange={handleChange}>
{generateCarouselItems()}
</Carousel>
</div>
)
wrapper = createComponent({
onChange(val: number, prevVal: number) {
state.val = val
state.oldVal = prevVal
},
interval: 50,
})
await nextTick()
@ -115,15 +121,7 @@ describe('Carousel', () => {
})
it('label', async (done) => {
const wrapper = mount(() => (
<div>
<Carousel>
{[1, 2, 3].map((i) => (
<CarouselItem key={i} label={i}></CarouselItem>
))}
</Carousel>
</div>
))
wrapper = createComponent(undefined, 3, true)
await nextTick()
expect(wrapper.find('.el-carousel__button span').text()).toBe('1')
done()
@ -131,11 +129,9 @@ describe('Carousel', () => {
describe('manual control', () => {
it('hover', async (done) => {
const wrapper = mount(() => (
<div>
<Carousel autoplay={false}>{generateCarouselItems()}</Carousel>
</div>
))
wrapper = createComponent({
autoplay: false,
})
await nextTick()
await wait()
@ -152,13 +148,14 @@ describe('Carousel', () => {
})
it('card', async (done) => {
const wrapper = mount(() => (
<div>
<Carousel autoplay={false} type="card">
{generateCarouselItems(7)}
</Carousel>
</div>
))
wrapper = createComponent(
{
autoplay: false,
type: 'card',
},
7
)
await nextTick()
await wait()
const items = wrapper.vm.$el.querySelectorAll('.el-carousel__item')
@ -178,21 +175,11 @@ describe('Carousel', () => {
})
it('vertical direction', () => {
const wrapper = mount({
setup() {
return () => (
<div>
<Carousel
ref="carousel"
autoplay={false}
direction="vertical"
height="100px"
>
{generateCarouselItems()}
</Carousel>
</div>
)
},
wrapper = createComponent({
ref: 'carousel',
autoplay: false,
direction: 'vertical',
height: '100px',
})
const items = wrapper.vm.$el.querySelectorAll('.el-carousel__item')
const carousel = wrapper.findComponent({ ref: 'carousel' })
@ -202,13 +189,10 @@ describe('Carousel', () => {
})
it('pause auto play on hover', async (done) => {
const wrapper = mount(() => (
<div>
<Carousel interval={50} pause-on-hover={false}>
{generateCarouselItems()}
</Carousel>
</div>
))
wrapper = createComponent({
interval: 50,
'pause-on-hover': false,
})
await nextTick()
await wrapper.find('.el-carousel').trigger('mouseenter')

View File

@ -7,12 +7,12 @@
ns.is('in-stage', inStage),
ns.is('hover', hover),
ns.is('animating', animating),
{ [ns.em('item', 'card')]: type === 'card' },
{ [ns.em('item', 'card')]: isCardType },
]"
:style="itemStyle"
@click="handleItemClick"
>
<div v-if="type === 'card'" v-show="!active" :class="ns.e('mask')" />
<div v-if="isCardType" v-show="!active" :class="ns.e('mask')" />
<slot />
</div>
</template>
@ -25,11 +25,14 @@ import {
getCurrentInstance,
onUnmounted,
ref,
reactive,
unref,
} from 'vue'
import { debugWarn } from '@element-plus/utils'
import { debugWarn, isUndefined } from '@element-plus/utils'
import { useNamespace } from '@element-plus/hooks'
import { carouselContextKey } from '@element-plus/tokens'
import { carouselItemProps } from './carousel-item'
import type { CSSProperties } from 'vue'
defineOptions({
@ -37,16 +40,27 @@ defineOptions({
})
const props = defineProps(carouselItemProps)
// inject
const carouselContext = inject(carouselContextKey)
const ns = useNamespace('carousel')
const CARD_SCALE = 0.83
const type = carouselContext?.type
const COMPONENT_NAME = 'ElCarouselItem'
// inject
const carouselContext = inject(carouselContextKey)!
// instance
const instance = getCurrentInstance()
const instance = getCurrentInstance()!
if (!carouselContext) {
debugWarn(
COMPONENT_NAME,
'usage: <el-carousel></el-carousel-item></el-carousel>'
)
}
if (!instance) {
debugWarn(
COMPONENT_NAME,
'compositional hook can only be invoked inside setups'
)
}
const CARD_SCALE = 0.83
const hover = ref(false)
const translate = ref(0)
@ -57,37 +71,41 @@ const inStage = ref(false)
const animating = ref(false)
// computed
const parentDirection = computed(() => {
return carouselContext?.direction
})
const { isCardType, isVertical } = carouselContext
const itemStyle = computed(() => {
const translateType =
parentDirection.value === 'vertical' ? 'translateY' : 'translateX'
const value = `${translateType}(${translate.value}px) scale(${scale.value})`
const style: CSSProperties = {
transform: value,
const itemStyle = computed<CSSProperties>(() => {
const translateType = `translate${unref(isVertical) ? 'Y' : 'X'}`
const _translate = `${translateType}(${unref(translate)}px)`
const _scale = `scale(${unref(scale)})`
const transform = [_translate, _scale].join(' ')
return {
transform,
}
return style
})
// methods
function processIndex(index, activeIndex, length) {
if (activeIndex === 0 && index === length - 1) {
function processIndex(index: number, activeIndex: number, length: number) {
const lastItemIndex = length - 1
const prevItemIndex = activeIndex - 1
const nextItemIndex = activeIndex + 1
const halfItemIndex = length / 2
if (activeIndex === 0 && index === lastItemIndex) {
return -1
} else if (activeIndex === length - 1 && index === 0) {
} else if (activeIndex === lastItemIndex && index === 0) {
return length
} else if (index < activeIndex - 1 && activeIndex - index >= length / 2) {
} else if (index < prevItemIndex && activeIndex - index >= halfItemIndex) {
return length + 1
} else if (index > activeIndex + 1 && index - activeIndex >= length / 2) {
} else if (index > nextItemIndex && index - activeIndex >= halfItemIndex) {
return -2
}
return index
}
function calcCardTranslate(index, activeIndex) {
const parentWidth = carouselContext?.root.value?.offsetWidth || 0
function calcCardTranslate(index: number, activeIndex: number) {
const parentWidth = carouselContext.root.value?.offsetWidth || 0
if (inStage.value) {
return (parentWidth * ((2 - CARD_SCALE) * (index - activeIndex) + 1)) / 4
} else if (index < activeIndex) {
@ -97,58 +115,66 @@ function calcCardTranslate(index, activeIndex) {
}
}
function calcTranslate(index, activeIndex, isVertical) {
const distance =
(isVertical
? carouselContext?.root.value?.offsetHeight
: carouselContext?.root.value?.offsetWidth) || 0
function calcTranslate(
index: number,
activeIndex: number,
isVertical: boolean
) {
const rootEl = carouselContext.root.value
if (!rootEl) return 0
const distance = (isVertical ? rootEl.offsetHeight : rootEl.offsetWidth) || 0
return distance * (index - activeIndex)
}
const translateItem = (
index: number,
activeIndex: number,
oldIndex: number
oldIndex?: number
) => {
const parentType = carouselContext?.type
const length = carouselContext?.items.value.length ?? Number.NaN
if (parentType !== 'card' && oldIndex !== undefined) {
animating.value = index === activeIndex || index === oldIndex
const _isCardType = unref(isCardType)
const carouselItemLength = carouselContext.items.value.length ?? Number.NaN
const isActive = index === activeIndex
if (!_isCardType && !isUndefined(oldIndex)) {
animating.value = isActive || index === oldIndex
}
if (index !== activeIndex && length > 2 && carouselContext?.loop) {
index = processIndex(index, activeIndex, length)
if (!isActive && carouselItemLength > 2 && carouselContext.loop) {
index = processIndex(index, activeIndex, carouselItemLength)
}
if (parentType === 'card') {
if (parentDirection.value === 'vertical') {
debugWarn('Carousel', 'vertical direction is not supported in card mode')
const _isVertical = unref(isVertical)
active.value = isActive
if (_isCardType) {
if (_isVertical) {
debugWarn('Carousel', 'vertical direction is not supported for card mode')
}
inStage.value = Math.round(Math.abs(index - activeIndex)) <= 1
active.value = index === activeIndex
translate.value = calcCardTranslate(index, activeIndex)
scale.value = active.value ? 1 : CARD_SCALE
scale.value = unref(active) ? 1 : CARD_SCALE
} else {
active.value = index === activeIndex
const isVertical = parentDirection.value === 'vertical'
translate.value = calcTranslate(index, activeIndex, isVertical)
translate.value = calcTranslate(index, activeIndex, _isVertical)
}
ready.value = true
}
function handleItemClick() {
if (carouselContext && carouselContext?.type === 'card') {
const index = carouselContext?.items.value
.map((d) => d.uid)
.indexOf(instance?.uid)
carouselContext?.setActiveItem(index)
if (carouselContext && unref(isCardType)) {
const index = carouselContext.items.value.findIndex(
({ uid }) => uid === instance.uid
)
carouselContext.setActiveItem(index)
}
}
// lifecycle
onMounted(() => {
if (carouselContext?.addItem) {
carouselContext?.addItem({
uid: instance?.uid,
...props,
carouselContext.addItem({
props,
states: reactive({
hover,
translate,
scale,
@ -156,14 +182,13 @@ onMounted(() => {
ready,
inStage,
animating,
translateItem,
})
}
}),
uid: instance.uid,
translateItem,
})
})
onUnmounted(() => {
if (carouselContext?.removeItem) {
carouselContext?.removeItem(instance?.uid)
}
carouselContext.removeItem(instance.uid)
})
</script>

View File

@ -54,7 +54,7 @@
@click.stop="handleIndicatorClick(index)"
>
<button :class="ns.e('button')">
<span v-if="hasLabel">{{ item.label }}</span>
<span v-if="hasLabel">{{ item.props.label }}</span>
</button>
</li>
</ul>
@ -70,10 +70,12 @@ import {
onBeforeUnmount,
watch,
nextTick,
shallowRef,
unref,
} from 'vue'
import { throttle } from 'lodash-unified'
import { useResizeObserver } from '@vueuse/core'
import { debugWarn } from '@element-plus/utils'
import { debugWarn, isString } from '@element-plus/utils'
import { ElIcon } from '@element-plus/components/icon'
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
import { useNamespace } from '@element-plus/hooks'
@ -84,29 +86,32 @@ import type { CarouselItemContext } from '@element-plus/tokens'
defineOptions({
name: 'ElCarousel',
})
const props = defineProps(carouselProps)
const emit = defineEmits(carouselEmits)
const ns = useNamespace('carousel')
const COMPONENT_NAME = 'ElCarousel'
const THROTTLE_TIME = 300
// refs
const activeIndex = ref(-1)
const timer = ref<ReturnType<typeof setInterval> | null>(null)
const hover = ref(false)
const root = ref<HTMLDivElement>()
const items = ref<CarouselItemContext[]>([])
const items = ref<Array<CarouselItemContext>>([])
// computed
const arrowDisplay = computed(
() => props.arrow !== 'never' && props.direction !== 'vertical'
() => props.arrow !== 'never' && !unref(isVertical)
)
const hasLabel = computed(() => {
return items.value.some((item) => item.label.toString().length > 0)
return items.value.some((item) => item.props.label.toString().length > 0)
})
const carouselClasses = computed(() => {
const classes = [ns.b(), ns.m(props.direction)]
if (props.type === 'card') {
if (unref(isCardType)) {
classes.push(ns.m('card'))
}
return classes
@ -117,24 +122,27 @@ const indicatorsClasses = computed(() => {
if (hasLabel.value) {
classes.push(ns.em('indicators', 'labels'))
}
if (props.indicatorPosition === 'outside' || props.type === 'card') {
if (props.indicatorPosition === 'outside' || unref(isCardType)) {
classes.push(ns.em('indicators', 'outside'))
}
return classes
})
const isCardType = computed(() => props.type === 'card')
const isVertical = computed(() => props.direction === 'vertical')
// methods
const throttledArrowClick = throttle(
(index) => {
(index: number) => {
setActiveItem(index)
},
300,
THROTTLE_TIME,
{ trailing: true }
)
const throttledIndicatorHover = throttle((index) => {
const throttledIndicatorHover = throttle((index: number) => {
handleIndicatorHover(index)
}, 300)
}, THROTTLE_TIME)
function pauseTimer() {
if (timer.value) {
@ -156,24 +164,26 @@ const playSlides = () => {
}
}
function setActiveItem(index) {
if (typeof index === 'string') {
const filteredItems = items.value.filter((item) => item.name === index)
function setActiveItem(index: number | string) {
if (isString(index)) {
const filteredItems = items.value.filter(
(item) => item.props.name === index
)
if (filteredItems.length > 0) {
index = items.value.indexOf(filteredItems[0])
}
}
index = Number(index)
if (Number.isNaN(index) || index !== Math.floor(index)) {
debugWarn('Carousel', 'index must be an integer.')
debugWarn(COMPONENT_NAME, 'index must be integer.')
return
}
const length = items.value.length
const itemCount = items.value.length
const oldIndex = activeIndex.value
if (index < 0) {
activeIndex.value = props.loop ? length - 1 : 0
} else if (index >= length) {
activeIndex.value = props.loop ? 0 : length - 1
activeIndex.value = props.loop ? itemCount - 1 : 0
} else if (index >= itemCount) {
activeIndex.value = props.loop ? 0 : itemCount - 1
} else {
activeIndex.value = index
}
@ -182,17 +192,17 @@ function setActiveItem(index) {
}
}
function resetItemPosition(oldIndex) {
function resetItemPosition(oldIndex?: number) {
items.value.forEach((item, index) => {
item.translateItem(index, activeIndex.value, oldIndex)
})
}
function addItem(item) {
function addItem(item: CarouselItemContext) {
items.value.push(item)
}
function removeItem(uid) {
function removeItem(uid?: number) {
const index = items.value.findIndex((item) => item.uid === uid)
if (index !== -1) {
items.value.splice(index, 1)
@ -200,17 +210,21 @@ function removeItem(uid) {
}
}
function itemInStage(item, index) {
const length = items.value.length
if (
(index === length - 1 && item.inStage && items.value[0].active) ||
(item.inStage && items.value[index + 1] && items.value[index + 1].active)
) {
function itemInStage(item: CarouselItemContext, index: number) {
const _items = unref(items)
const itemCount = _items.length
if (itemCount === 0 || !item.states.inStage) return false
const nextItemIndex = index + 1
const prevItemIndex = index - 1
const lastItemIndex = itemCount - 1
const isLastItemActive = _items[lastItemIndex].states.active
const isFirstItemActive = _items[0].states.active
const isNextItemActive = _items[nextItemIndex]?.states?.active
const isPrevItemActive = _items[prevItemIndex]?.states?.active
if ((index === lastItemIndex && isFirstItemActive) || isNextItemActive) {
return 'left'
} else if (
(index === 0 && item.inStage && items.value[length - 1].active) ||
(item.inStage && items.value[index - 1] && items.value[index - 1].active)
) {
} else if ((index === 0 && isLastItemActive) || isPrevItemActive) {
return 'right'
}
return false
@ -228,27 +242,27 @@ function handleMouseLeave() {
startTimer()
}
function handleButtonEnter(arrow) {
if (props.direction === 'vertical') return
function handleButtonEnter(arrow: 'left' | 'right') {
if (unref(isVertical)) return
items.value.forEach((item, index) => {
if (arrow === itemInStage(item, index)) {
item.hover = true
item.states.hover = true
}
})
}
function handleButtonLeave() {
if (props.direction === 'vertical') return
if (unref(isVertical)) return
items.value.forEach((item) => {
item.hover = false
item.states.hover = false
})
}
function handleIndicatorClick(index) {
function handleIndicatorClick(index: number) {
activeIndex.value = index
}
function handleIndicatorHover(index) {
function handleIndicatorHover(index: number) {
if (props.trigger === 'hover' && index !== activeIndex.value) {
activeIndex.value = index
}
@ -274,8 +288,8 @@ watch(
)
watch(
() => props.autoplay,
(current) => {
current ? startTimer() : pauseTimer()
(autoplay) => {
autoplay ? startTimer() : pauseTimer()
}
)
watch(
@ -285,27 +299,30 @@ watch(
}
)
const resizeObserver = shallowRef<ReturnType<typeof useResizeObserver>>()
// lifecycle
onMounted(() => {
nextTick(() => {
const resizeObserver = useResizeObserver(root.value, resetItemPosition)
if (props.initialIndex < items.value.length && props.initialIndex >= 0) {
activeIndex.value = props.initialIndex
}
startTimer()
onMounted(async () => {
await nextTick()
onBeforeUnmount(() => {
if (root.value) resizeObserver.stop()
pauseTimer()
})
resizeObserver.value = useResizeObserver(root.value, () => {
resetItemPosition()
})
if (props.initialIndex < items.value.length && props.initialIndex >= 0) {
activeIndex.value = props.initialIndex
}
startTimer()
})
onBeforeUnmount(() => {
pauseTimer()
if (root.value && resizeObserver.value) resizeObserver.value.stop()
})
// provide
provide(carouselContextKey, {
root,
direction: props.direction,
type: props.type,
isCardType,
isVertical,
items,
loop: props.loop,
addItem,

View File

@ -1,30 +1,29 @@
import type { InjectionKey, Ref, ToRefs, UnwrapRef } from 'vue'
import type { InjectionKey, Ref } from 'vue'
import type { CarouselItemProps } from '@element-plus/components/carousel'
export type CarouselItemContext = CarouselItemProps &
ToRefs<{
hover: boolean
translate: number
scale: number
active: boolean
ready: boolean
inStage: boolean
animating: boolean
}> & {
uid: number | undefined
translateItem: (
index: number,
activeIndex: number,
oldIndex: number
) => void
}
export type CarouselItemStates = {
hover: boolean
translate: number
scale: number
active: boolean
ready: boolean
inStage: boolean
animating: boolean
}
export type CarouselItemContext = {
props: CarouselItemProps
states: CarouselItemStates
uid: number | undefined
translateItem: (index: number, activeIndex: number, oldIndex?: number) => void
}
export type CarouselContext = {
root: Ref<HTMLElement | undefined>
direction: string
type: string
items: Ref<UnwrapRef<CarouselItemContext[]>>
items: Ref<CarouselItemContext[]>
isCardType: Ref<boolean>
isVertical: Ref<boolean>
loop: boolean
addItem: (item: CarouselItemContext) => void
removeItem: (uid: number | undefined) => void