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 {