refactor: optimize segmented motion thumb rendering and transition

pull/8045/head
Feynman 2025-02-20 19:01:11 +08:00
parent b80bd37f6e
commit 26446f988e
2 changed files with 60 additions and 73 deletions

View File

@ -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<ThumbRect>(null);
const nextStyle = ref<ThumbRect>(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<CSSProperties>(() => ({
'--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<CSSProperties>(() => ({
'--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 (
<Transition
appear
onBeforeEnter={onAppearStart}
onEnter={onAppearActive}
onAfterEnter={onAppearEnd}
>
{!prevStyle.value || !nextStyle.value ? null : <div {...motionProps}></div>}
</Transition>
);
return !prevStyle.value || !nextStyle.value ? null : <div {...motionProps}></div>;
};
},
});

View File

@ -144,6 +144,8 @@ const genSegmentedStyle: GenerateStyle<SegmentedToken> = (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<SegmentedToken> = (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',
},
},
};
};