+
+ const createComponent = (
+ props: any = {},
+ count?: number,
+ hasLabel?: boolean
+ ) => {
+ return mount({
setup() {
return () => (
- {generateCarouselItems()}
+
+ {generateCarouselItems(count, hasLabel)}
+
)
},
})
+ }
+
+ 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(() => (
-
- {generateCarouselItems()}
-
- ))
+ wrapper = createComponent({
+ interval: 50,
+ })
await nextTick()
await wait(10)
@@ -47,13 +67,10 @@ describe('Carousel', () => {
})
it('initial index', async (done) => {
- const wrapper = mount(() => (
-
-
- {generateCarouselItems()}
-
-
- ))
+ 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(() => (
-
- {generateCarouselItems()}
-
- ))
+ 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 () => (
-
-
- {generateCarouselItems()}
-
-
- )
+ 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(() => (
-
-
- {[1, 2, 3].map((i) => (
-
- ))}
-
-
- ))
+ 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(() => (
-
- {generateCarouselItems()}
-
- ))
+ wrapper = createComponent({
+ autoplay: false,
+ })
await nextTick()
await wait()
@@ -152,13 +148,14 @@ describe('Carousel', () => {
})
it('card', async (done) => {
- const wrapper = mount(() => (
-
-
- {generateCarouselItems(7)}
-
-
- ))
+ 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 () => (
-
-
- {generateCarouselItems()}
-
-
- )
- },
+ 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(() => (
-
-
- {generateCarouselItems()}
-
-
- ))
+ wrapper = createComponent({
+ interval: 50,
+ 'pause-on-hover': false,
+ })
await nextTick()
await wrapper.find('.el-carousel').trigger('mouseenter')
diff --git a/packages/components/carousel/src/carousel-item.vue b/packages/components/carousel/src/carousel-item.vue
index 622cd06dda..fc1ec114e8 100644
--- a/packages/components/carousel/src/carousel-item.vue
+++ b/packages/components/carousel/src/carousel-item.vue
@@ -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"
>
-
+
@@ -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: '
+ )
+}
+
+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(() => {
+ 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)
})
diff --git a/packages/components/carousel/src/carousel.vue b/packages/components/carousel/src/carousel.vue
index a51833d9d3..f57afbff67 100644
--- a/packages/components/carousel/src/carousel.vue
+++ b/packages/components/carousel/src/carousel.vue
@@ -54,7 +54,7 @@
@click.stop="handleIndicatorClick(index)"
>
@@ -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 | null>(null)
const hover = ref(false)
const root = ref()
-const items = ref([])
+const items = ref>([])
// 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>()
// 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,
diff --git a/packages/tokens/carousel.ts b/packages/tokens/carousel.ts
index c711c33bc4..a53bc8e81d 100644
--- a/packages/tokens/carousel.ts
+++ b/packages/tokens/carousel.ts
@@ -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
- direction: string
- type: string
- items: Ref>
+ items: Ref
+ isCardType: Ref
+ isVertical: Ref
loop: boolean
addItem: (item: CarouselItemContext) => void
removeItem: (uid: number | undefined) => void