diff --git a/components/segmented/src/MotionThumb.tsx b/components/segmented/src/MotionThumb.tsx index 2f59f87a0..6f92d3ccd 100644 --- a/components/segmented/src/MotionThumb.tsx +++ b/components/segmented/src/MotionThumb.tsx @@ -1,6 +1,5 @@ -import { addClass, removeClass } from '../../vc-util/Dom/class'; -import type { CSSProperties, Ref, TransitionProps } from 'vue'; -import { onBeforeUnmount, nextTick, Transition, watch, defineComponent, computed, ref } from 'vue'; +import type { CSSProperties, Ref } from 'vue'; +import { onBeforeUnmount, nextTick, watch, defineComponent, computed, ref } from 'vue'; import { anyType } from '../../_util/type'; import type { SegmentedValue } from './segmented'; @@ -61,6 +60,37 @@ const MotionThumb = defineComponent({ const prevStyle = ref(null); const nextStyle = ref(null); + const thumbStart = computed(() => + props.direction === 'rtl' + ? toPX(-(prevStyle.value?.right as number)) + : toPX(prevStyle.value?.left as number), + ); + const thumbActive = computed(() => + props.direction === 'rtl' + ? toPX(-(nextStyle.value?.right as number)) + : toPX(nextStyle.value?.left as number), + ); + + const mergedStyle = computed(() => ({ + '--thumb-start-left': thumbStart.value, + '--thumb-start-width': toPX(prevStyle.value?.width), + '--thumb-active-left': thumbActive.value, + '--thumb-active-width': toPX(nextStyle.value?.width), + })); + + // 监听过渡结束 + const onTransitionEnd = (e: TransitionEvent) => { + if (e.propertyName !== 'transform' && e.propertyName !== 'width') return; + + if (thumbRef.value) { + thumbRef.value.removeEventListener('transitionend', onTransitionEnd); + } + + prevStyle.value = null; + nextStyle.value = null; + emit('motionEnd'); + }; + watch( () => props.value, (value, prevValue) => { @@ -73,66 +103,36 @@ const MotionThumb = defineComponent({ prevStyle.value = calcPrevStyle; nextStyle.value = calcNextStyle; - if (prev && next) { - emit('motionStart'); - } else { - emit('motionEnd'); - } + nextTick(() => { + if (prev && next) { + if (thumbRef.value) { + // 使用 CSS 变量设置初始位置 + thumbRef.value.style.transform = `translateX(var(--thumb-start-left))`; + thumbRef.value.style.width = `var(--thumb-start-width)`; + + // 强制重排 + thumbRef.value.offsetHeight; + + thumbRef.value.style.transform = `translateX(var(--thumb-active-left))`; + thumbRef.value.style.width = `var(--thumb-active-width)`; + + emit('motionStart'); + + thumbRef.value.addEventListener('transitionend', onTransitionEnd); + } + } else { + emit('motionEnd'); + } + }); }, { flush: 'post' }, ); - const thumbStart = computed(() => - props.direction === 'rtl' - ? toPX(-(prevStyle.value?.right as number)) - : toPX(prevStyle.value?.left as number), - ); - const thumbActive = computed(() => - props.direction === 'rtl' - ? toPX(-(nextStyle.value?.right as number)) - : toPX(nextStyle.value?.left as number), - ); - - // =========================== Motion =========================== - let timeid: any; - const onAppearStart: TransitionProps['onBeforeEnter'] = (el: HTMLDivElement) => { - clearTimeout(timeid); - nextTick(() => { - if (el) { - el.style.transform = `translateX(var(--thumb-start-left))`; - el.style.width = `var(--thumb-start-width)`; - } - }); - }; - - const onAppearActive: TransitionProps['onEnter'] = (el: HTMLDivElement) => { - timeid = setTimeout(() => { - if (el) { - addClass(el, `${props.motionName}-appear-active`); - el.style.transform = `translateX(var(--thumb-active-left))`; - el.style.width = `var(--thumb-active-width)`; - } - }); - }; - const onAppearEnd: TransitionProps['onAfterEnter'] = (el: HTMLDivElement) => { - prevStyle.value = null; - nextStyle.value = null; - if (el) { - el.style.transform = null; - el.style.width = null; - removeClass(el, `${props.motionName}-appear-active`); - } - emit('motionEnd'); - }; - const mergedStyle = computed(() => ({ - '--thumb-start-left': thumbStart.value, - '--thumb-start-width': toPX(prevStyle.value?.width), - '--thumb-active-left': thumbActive.value, - '--thumb-active-width': toPX(nextStyle.value?.width), - })); + // 清理事件监听 onBeforeUnmount(() => { - clearTimeout(timeid); + thumbRef.value?.removeEventListener('transitionend', onTransitionEnd); }); + return () => { // It's little ugly which should be refactor when @umi/test update to latest jsdom const motionProps = { @@ -145,16 +145,7 @@ const MotionThumb = defineComponent({ (motionProps as any)['data-test-style'] = JSON.stringify(mergedStyle.value); } - return ( - - {!prevStyle.value || !nextStyle.value ? null :
} -
- ); + return !prevStyle.value || !nextStyle.value ? null :
; }; }, }); diff --git a/components/segmented/style/index.ts b/components/segmented/style/index.ts index 9235959cd..db266e765 100644 --- a/components/segmented/style/index.ts +++ b/components/segmented/style/index.ts @@ -144,6 +144,8 @@ const genSegmentedStyle: GenerateStyle = (token: SegmentedToken) height: '100%', padding: `${token.paddingXXS}px 0`, borderRadius: token.borderRadiusSM, + transition: `transform ${token.motionDurationSlow} ${token.motionEaseInOut}, width ${token.motionDurationSlow} ${token.motionEaseInOut}`, + willChange: 'transform, width', [`& ~ ${componentCls}-item:not(${componentCls}-item-selected):not(${componentCls}-item-disabled)::after`]: { @@ -180,12 +182,6 @@ const genSegmentedStyle: GenerateStyle = (token: SegmentedToken) // disabled styles ...getItemDisabledStyle(`&-disabled ${componentCls}-item`, token), ...getItemDisabledStyle(`${componentCls}-item-disabled`, token), - - // transition effect when `appear-active` - [`${componentCls}-thumb-motion-appear-active`]: { - transition: `transform ${token.motionDurationSlow} ${token.motionEaseInOut}, width ${token.motionDurationSlow} ${token.motionEaseInOut}`, - willChange: 'transform, width', - }, }, }; };