diff --git a/packages/components/tour/__tests__/tour.spec.ts b/packages/components/tour/__tests__/tour.spec.ts index d44caf9b6..6e8a9b0c3 100644 --- a/packages/components/tour/__tests__/tour.spec.ts +++ b/packages/components/tour/__tests__/tour.spec.ts @@ -117,6 +117,7 @@ describe('Tour', () => { ) await wrapper.setProps({ activeIndex: 1 }) + await wait(50) await flushPromises() expect(document.querySelector('.active-index-test .ix-tour-panel .ix-header .ix-header-title')?.textContent).toBe( diff --git a/packages/components/tour/src/Tour.tsx b/packages/components/tour/src/Tour.tsx index d0d59efc4..48eef2308 100644 --- a/packages/components/tour/src/Tour.tsx +++ b/packages/components/tour/src/Tour.tsx @@ -29,8 +29,9 @@ import { tourProps } from './types' export default defineComponent({ name: 'IxTour', + inheritAttrs: false, props: tourProps, - setup(props, { slots }) { + setup(props, { slots, attrs }) { const common = useGlobalConfig('common') const config = useGlobalConfig('tour') const locale = useGlobalConfig('locale') @@ -52,7 +53,7 @@ export default defineComponent({ useCloseTrigger(mergedProps, positionInfo, visible, setVisible) const stepChangeContext = useStepChange(mergedProps, activeIndex, activeStep, visible, onAnimateEnd) - const { isStepChanging } = stepChangeContext + const { isStepChanging, onStepChange } = stepChangeContext const mergedContainerFallback = computed(() => `.${mergedPrefixCls.value}-overlay-container`) const mergedContainer = computed(() => mergedProps.value.overlayContainer ?? mergedContainerFallback.value) @@ -65,6 +66,7 @@ export default defineComponent({ isStepChanging, visible, currentZIndex, + onStepChange, ) const placeholderStyle = computed(() => { @@ -108,7 +110,7 @@ export default defineComponent({ - <ɵOverlay class={`${prefixCls}-overlay`} {...overlayProps.value} v-slots={overlaySlots} /> + <ɵOverlay class={`${prefixCls}-overlay`} {...overlayProps.value} {...attrs} v-slots={overlaySlots} /> ) } diff --git a/packages/components/tour/src/composables/useActiveStep.ts b/packages/components/tour/src/composables/useActiveStep.ts index ee07e9c41..c495c53a0 100644 --- a/packages/components/tour/src/composables/useActiveStep.ts +++ b/packages/components/tour/src/composables/useActiveStep.ts @@ -5,8 +5,8 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import type { MergedTourProps } from './useMergedProps' import type { ResolvedTourStep } from '../types' +import type { MergedTourProps } from './useMergedProps' import type { ButtonMode, ButtonSize } from '@idux/components/button' import type { TourLocale } from '@idux/components/locales' @@ -97,6 +97,11 @@ export function useActiveStep( } const pushCurrentUpdate = async (index: number) => { + if (index < 0) { + setActiveStep(undefined) + return + } + const promise = getActiveStep(index) destructions.push(async () => { @@ -113,15 +118,15 @@ export function useActiveStep( watch( activeIndex, - async (current, pre) => { - if (current === pre) { + async current => { + if (current === activeStep.value?.index) { return } destroySteps() pushCurrentUpdate(current) }, - { immediate: true }, + { immediate: true, flush: 'post' }, ) return activeStep diff --git a/packages/components/tour/src/composables/useMask.ts b/packages/components/tour/src/composables/useMask.ts index 7e27a2732..17d38e309 100644 --- a/packages/components/tour/src/composables/useMask.ts +++ b/packages/components/tour/src/composables/useMask.ts @@ -5,8 +5,8 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import type { MergedTourProps } from './useMergedProps' import type { ResolvedTourStep, TargetPositionInfo } from '../types' +import type { MergedTourProps } from './useMergedProps' import { type CSSProperties, @@ -20,7 +20,7 @@ import { import { isBoolean } from 'lodash-es' -import { convertCssPixel, rAF, useState } from '@idux/cdk/utils' +import { cancelRAF, convertCssPixel, rAF, useState } from '@idux/cdk/utils' import { easeInOutQuad } from '../utils' @@ -75,6 +75,13 @@ export function useMask( animateCbs.clear() }) + let rAFHandle: number + + const cancelAnimate = () => { + cancelRAF(rAFHandle) + setIsAnimating(false) + } + const animate = (from: TargetPositionInfo, to: TargetPositionInfo) => { const start = Date.now() setIsAnimating(true) @@ -95,7 +102,7 @@ export function useMask( ) if (elapsed < animateDuration) { - rAF(tick) + rAFHandle = rAF(tick) } else { setMaskPath(getMaskPath(to)) setIsAnimating(false) @@ -103,7 +110,7 @@ export function useMask( } } - rAF(tick) + rAFHandle = rAF(tick) } let _tempIndex = activeIndex.value @@ -116,10 +123,18 @@ export function useMask( } if (!activeStep.value?.mask) { + cancelAnimate() setMaskPath('') - } else if (!mergedProps.value.animatable || !prePos || !pos || _tempIndex === activeIndex.value) { + } else if (!mergedProps.value.animatable || pos?.origin !== 'index' || !prePos || !pos) { setMaskPath(getMaskPath(pos)) + + if (mergedProps.value.animatable && (isAnimating.value || activeIndex.value !== _tempIndex)) { + runAnimateCbs() + } + + cancelAnimate() } else { + cancelAnimate() animate(prePos, pos) } @@ -140,7 +155,7 @@ export function useMask( } } -function getMaskPath(positionInfo: TargetPositionInfo | null): string { +function getMaskPath(positionInfo: Omit | null): string { const viewBoxRect = (width: number, height: number) => `M${width},0L0,0L0,${height}L${width},${height}L${width},0Z` if (!positionInfo) { diff --git a/packages/components/tour/src/composables/useOverlayProps.ts b/packages/components/tour/src/composables/useOverlayProps.ts index 2b4b15eef..e842e2afd 100644 --- a/packages/components/tour/src/composables/useOverlayProps.ts +++ b/packages/components/tour/src/composables/useOverlayProps.ts @@ -5,12 +5,12 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import type { MergedTourProps } from './useMergedProps' import type { ResolvedTourStep } from '../types' +import type { MergedTourProps } from './useMergedProps' import type { ɵOverlayProps } from '@idux/components/_private/overlay' import type { CommonConfig } from '@idux/components/config' -import { type ComputedRef, computed } from 'vue' +import { type ComputedRef, computed, ref } from 'vue' export function useOverlayProps( componentCommonConfig: CommonConfig, @@ -20,13 +20,20 @@ export function useOverlayProps( isStepChanging: ComputedRef, visible: ComputedRef, currentZIndex: ComputedRef, + onStepChange: (cb: () => void) => void, ): ComputedRef<ɵOverlayProps> { + const currentActiveStep = ref(activeStep.value) + + onStepChange(() => { + currentActiveStep.value = activeStep.value + }) + return computed(() => { const { animatable, overlayContainer, offset } = mergedProps.value - const { placement = 'bottomStart', showArrow } = activeStep.value ?? {} + const { placement = 'bottomStart', showArrow } = currentActiveStep.value ?? {} return { - visible: visible.value && !!activeStep.value && !isStepChanging.value, + visible: visible.value && !!currentActiveStep.value && !isStepChanging.value, container: overlayContainer, containerFallback: containerFallback.value, trigger: 'manual', diff --git a/packages/components/tour/src/composables/useStepChange.ts b/packages/components/tour/src/composables/useStepChange.ts index 9b2c577a2..339689520 100644 --- a/packages/components/tour/src/composables/useStepChange.ts +++ b/packages/components/tour/src/composables/useStepChange.ts @@ -5,8 +5,8 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import type { MergedTourProps } from './useMergedProps' import type { ResolvedTourStep } from '../types' +import type { MergedTourProps } from './useMergedProps' import { type ComputedRef, onUnmounted, watch } from 'vue' @@ -28,6 +28,25 @@ export function useStepChange( ): StepChangeContext { const stepChangeCbs = new Set<() => void>() const [isStepChanging, _setIsStepChanging] = useState(false) + let transitionTmr: number + + const locks = { + stepChange: false, + animate: false, + } + + const lock = () => { + Object.keys(locks).forEach(key => (locks[key as keyof typeof locks] = true)) + + setIsStepChanging(true) + } + const unlock = (lock: keyof typeof locks) => { + locks[lock] = false + + if (Object.keys(locks).every(key => !locks[key as keyof typeof locks])) { + setIsStepChanging(false) + } + } const setIsStepChanging = (changing: boolean) => { if (isStepChanging.value === changing) { @@ -37,20 +56,20 @@ export function useStepChange( _setIsStepChanging(changing) if (!changing) { - stepChangeCbs.forEach(cb => cb()) + runStepChangeCbs() } } const onStepChange = (cb: () => void) => { stepChangeCbs.add(cb) } - - let transitionTmr: number + const runStepChangeCbs = () => { + stepChangeCbs.forEach(cb => cb()) + } onAnimateEnd(() => { if (mergedProps.value.animatable && activeStep.value?.mask) { - transitionTmr && clearTimeout(transitionTmr) - setIsStepChanging(false) + unlock('animate') } }) @@ -59,7 +78,7 @@ export function useStepChange( (current, pre) => { if (current !== pre) { transitionTmr && clearTimeout(transitionTmr) - setIsStepChanging(true) + lock() } }, { @@ -68,18 +87,26 @@ export function useStepChange( ) watch( activeStep, - step => { + (step, preStep) => { + if (step && !preStep) { + runStepChangeCbs() + return + } + if (!mergedProps.value.animatable || !step?.mask || !visible.value) { - transitionTmr && clearTimeout(transitionTmr) - transitionTmr = setTimeout( - () => { - setIsStepChanging(false) - }, - mergedProps.value.animatable ? transitionDuration : 0, - ) + unlock('animate') } + + transitionTmr && clearTimeout(transitionTmr) + transitionTmr = setTimeout( + () => { + unlock('stepChange') + }, + mergedProps.value.animatable ? transitionDuration : 0, + ) }, { + immediate: true, flush: 'post', }, ) diff --git a/packages/components/tour/src/composables/useTarget.ts b/packages/components/tour/src/composables/useTarget.ts index 22a0dd63d..7061edbfe 100644 --- a/packages/components/tour/src/composables/useTarget.ts +++ b/packages/components/tour/src/composables/useTarget.ts @@ -5,8 +5,8 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ +import type { ResolvedTourStep, TargetPositionInfo, TargetPositionOrigin } from '../types' import type { MergedTourProps } from './useMergedProps' -import type { ResolvedTourStep, TargetPositionInfo } from '../types' import { type ComputedRef, type Ref, onMounted, onUnmounted, shallowRef, watch } from 'vue' @@ -33,7 +33,7 @@ export function useTarget( targetRef.value = (await activeStep.value?.target()) ?? null } - const updatePopsition = (scrollIntoView = false) => { + const updatePopsition = (scrollIntoView = false, origin: TargetPositionOrigin = 'index') => { const targetEl = targetRef.value const { offset = 0, radius = 0 } = activeStep.value?.gap ?? {} @@ -46,6 +46,7 @@ export function useTarget( width: 0, height: 0, radius, + origin, }) return } @@ -61,7 +62,16 @@ export function useTarget( const { x, y, width, height } = targetEl.getBoundingClientRect() if (!offset) { - setPositionInfo({ windowWidth: window.innerWidth, windowHeight: window.innerHeight, x, y, width, height, radius }) + setPositionInfo({ + windowWidth: window.innerWidth, + windowHeight: window.innerHeight, + x, + y, + width, + height, + radius, + origin, + }) } else { setPositionInfo({ windowWidth: window.innerWidth, @@ -71,6 +81,7 @@ export function useTarget( width: width + offset * 2, height: height + offset * 2, radius, + origin, }) } } @@ -79,14 +90,15 @@ export function useTarget( let stopScrollLisiten: (() => void) | undefined onMounted(() => { - watch(() => activeStep.value?.target, updateTarget, { immediate: true }) - watch([targetRef, visible], () => updatePopsition(true), { immediate: true }) + watch([() => activeStep.value?.target, visible], updateTarget, { immediate: true }) + watch(targetRef, () => updatePopsition(true, 'index'), { immediate: true }) + watch(visible, () => updatePopsition(true, 'visible')) watch( visible, v => { if (v) { - stopResizeLisiten = useEventListener(window, 'resize', () => updatePopsition(false)) - stopScrollLisiten = useEventListener(window, 'scroll', () => updatePopsition(false)) + stopResizeLisiten = useEventListener(window, 'resize', () => updatePopsition(false, 'resize')) + stopScrollLisiten = useEventListener(window, 'scroll', () => updatePopsition(false, 'scroll')) } else { stopResizeLisiten?.() stopScrollLisiten?.() diff --git a/packages/components/tour/src/types.ts b/packages/components/tour/src/types.ts index d5c86788c..1c6e4cb86 100644 --- a/packages/components/tour/src/types.ts +++ b/packages/components/tour/src/types.ts @@ -12,6 +12,8 @@ import type { DefineComponent, HTMLAttributes, PropType } from 'vue' import { ɵOverlayPlacementDef } from '@idux/components/_private/overlay' +export type TargetPositionOrigin = 'resize' | 'scroll' | 'index' | 'visible' + export interface TargetPositionInfo { windowWidth: number windowHeight: number @@ -20,6 +22,7 @@ export interface TargetPositionInfo { width: number height: number radius: number + origin: TargetPositionOrigin } export interface TargetGap {