@veltra/compositions 1.3.5 → 1.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,11 +5,11 @@ interface TransitionBase {
5
5
  target: ShallowRef<HTMLElement | undefined> | HTMLElement;
6
6
  /** 进入动画结束回调 */
7
7
  afterEnter?: () => void;
8
- /** 进入动画被取消回调 */
8
+ /** 进入动画被取消回调(css 实现中阶段被打断时由新阶段直接接管,不会触发) */
9
9
  enterCanceled?: () => void;
10
10
  /** 离开动画结束回调 */
11
11
  afterLeave?: () => void;
12
- /** 离开动画被取消回调 */
12
+ /** 离开动画被取消回调(css 实现中阶段被打断时由新阶段直接接管,不会触发) */
13
13
  leaveCanceled?: () => void;
14
14
  }
15
15
  interface CssTransitionOptions extends TransitionBase {
@@ -1,19 +1,23 @@
1
1
  import { createToggle } from "@veltra/utils";
2
- import { computed, isRef, onBeforeUnmount, watch } from "vue";
2
+ import { computed, isRef, onBeforeUnmount } from "vue";
3
3
  //#region src/use-transition/use-css-transition.ts
4
- const increaseTransitionCount = (el) => {
5
- el._count = (el._count ?? 0) + 1;
6
- };
7
- const decreaseTransitionCount = (el) => {
8
- el._count = (el._count ?? 0) - 1;
9
- if (el._count <= 0) delete el._count;
4
+ /** 解析 CSS 时间值(s / ms)为毫秒 */
5
+ const parseTime = (time) => {
6
+ const t = time.trim();
7
+ if (t.endsWith("ms")) return parseFloat(t);
8
+ return parseFloat(t) * 1e3;
10
9
  };
11
10
  /**
12
11
  * 使用css过渡
12
+ *
13
+ * 阶段结束不依赖 transitionend 事件:进入/离开相互打断时类共存会导致
14
+ * 过渡不触发、end 事件丢失从而卡死。这里每个阶段开始时清理相反阶段的类,
15
+ * 并以「计算过渡耗时 + 定时器」兜底,保证阶段必然终结。
16
+ *
13
17
  * @param options 过渡选项
14
18
  */
15
19
  function useCssTransition(options) {
16
- const { name, target, afterEnter, afterLeave, enterCanceled, leaveCanceled, keepEnterTo = false } = options;
20
+ const { name, target, afterEnter, afterLeave, keepEnterTo = false } = options;
17
21
  const getDom = () => isRef(target) ? target.value : target;
18
22
  const classes = computed(() => {
19
23
  const _name = typeof name === "string" ? name : name.value;
@@ -32,93 +36,84 @@ function useCssTransition(options) {
32
36
  leaveActive: `${_name}-leave-active`
33
37
  };
34
38
  });
39
+ /** 阶段序号,使过期的 rAF / 定时器回调失效 */
40
+ let phaseSeq = 0;
41
+ let finalizeTimer;
42
+ const clearFinalizeTimer = () => {
43
+ if (finalizeTimer === void 0) return;
44
+ clearTimeout(finalizeTimer);
45
+ finalizeTimer = void 0;
46
+ };
47
+ /** 目标元素当前应用的最长过渡耗时(时长 + 延迟),无过渡则为 0 */
48
+ const getTransitionDuration = (dom) => {
49
+ const { transitionDuration, transitionDelay } = getComputedStyle(dom);
50
+ const durations = transitionDuration.split(",").map(parseTime);
51
+ const delays = transitionDelay.split(",").map(parseTime);
52
+ return durations.reduce((max, d, i) => {
53
+ return Math.max(max, d + (delays[i] ?? delays[delays.length - 1] ?? 0));
54
+ }, 0);
55
+ };
56
+ /** 阶段完成:清理类并触发回调,仅当前阶段的定时器生效 */
57
+ const finalize = (seq, entering) => {
58
+ if (seq !== phaseSeq) return;
59
+ const dom = getDom();
60
+ const { enterActive, enterTo, leaveActive, leaveTo } = classes.value;
61
+ if (entering) {
62
+ if (keepEnterTo) dom?.classList.remove(enterActive);
63
+ else dom?.classList.remove(enterActive, enterTo);
64
+ afterEnter?.();
65
+ } else {
66
+ dom?.classList.remove(leaveActive, leaveTo);
67
+ afterLeave?.();
68
+ }
69
+ };
70
+ /** 过渡开始后调度兜底完成(晚 50ms 容错) */
71
+ const scheduleFinalize = (seq, entering) => {
72
+ clearFinalizeTimer();
73
+ const dom = getDom();
74
+ const duration = dom ? getTransitionDuration(dom) : 0;
75
+ finalizeTimer = setTimeout(() => finalize(seq, entering), duration + 50);
76
+ };
35
77
  /** 开始进入动画 */
36
78
  const startTransitionIn = () => {
37
- const { enterActive, enterTo, enterFrom } = classes.value;
79
+ const seq = ++phaseSeq;
80
+ const { enterActive, enterTo, enterFrom, leaveFrom, leaveActive, leaveTo } = classes.value;
38
81
  const dom = getDom();
82
+ dom?.classList.remove(leaveFrom, leaveActive, leaveTo);
39
83
  dom?.classList.add(enterFrom);
40
84
  requestAnimationFrame(() => {
85
+ if (seq !== phaseSeq) return;
41
86
  dom?.classList.add(enterActive);
42
87
  requestAnimationFrame(() => {
88
+ if (seq !== phaseSeq) return;
43
89
  dom?.classList.remove(enterFrom);
44
90
  dom?.classList.add(enterTo);
91
+ scheduleFinalize(seq, true);
45
92
  });
46
93
  });
47
94
  };
48
95
  /** 开始离开动画 */
49
96
  const startTransitionOut = () => {
50
- const { leaveTo, leaveActive, leaveFrom, enterTo } = classes.value;
97
+ const seq = ++phaseSeq;
98
+ const { leaveTo, leaveActive, leaveFrom, enterFrom, enterActive, enterTo } = classes.value;
51
99
  const dom = getDom();
52
- if (keepEnterTo) dom?.classList.remove(enterTo);
100
+ dom?.classList.remove(enterFrom, enterActive, enterTo);
53
101
  dom?.classList.add(leaveFrom, leaveActive);
54
102
  requestAnimationFrame(() => {
55
- dom?.classList.add(leaveActive);
103
+ if (seq !== phaseSeq) return;
56
104
  requestAnimationFrame(() => {
105
+ if (seq !== phaseSeq) return;
57
106
  dom?.classList.remove(leaveFrom);
58
107
  dom?.classList.add(leaveTo);
108
+ scheduleFinalize(seq, false);
59
109
  });
60
110
  });
61
111
  };
62
- const [active, toggle] = createToggle(false, (_active) => {
112
+ const [, toggle] = createToggle(false, (_active) => {
63
113
  _active ? startTransitionIn() : startTransitionOut();
64
114
  });
65
- const transitionEndHandler = (e) => {
66
- e.stopPropagation();
67
- const { leaveActive, enterActive, enterTo, leaveTo } = classes.value;
68
- const dom = getDom();
69
- if (dom !== e.target) return;
70
- decreaseTransitionCount(dom);
71
- if (dom._count) return;
72
- if (active.value) {
73
- if (keepEnterTo) dom?.classList.remove(enterActive);
74
- else dom?.classList.remove(enterActive, enterTo);
75
- afterEnter?.();
76
- } else {
77
- dom?.classList.remove(leaveActive, leaveTo);
78
- afterLeave?.();
79
- }
80
- };
81
- const transitionRunHandler = (e) => {
82
- e.stopPropagation();
83
- const dom = getDom();
84
- if (dom !== e.target) return;
85
- increaseTransitionCount(dom);
86
- };
87
- const transitionCancelHandler = (e) => {
88
- e.stopPropagation();
89
- const dom = getDom();
90
- if (dom !== e.target) return;
91
- decreaseTransitionCount(dom);
92
- if (dom._count) return;
93
- const { leaveActive, enterActive } = classes.value;
94
- if (active.value) {
95
- dom?.classList.remove(enterActive);
96
- enterCanceled?.();
97
- } else {
98
- dom?.classList.remove(leaveActive);
99
- leaveCanceled?.();
100
- }
101
- };
102
- /** 添加事件 */
103
- const addEvent = (el) => {
104
- el?.addEventListener("transitioncancel", transitionCancelHandler);
105
- el?.addEventListener("transitionend", transitionEndHandler);
106
- el?.addEventListener("transitionrun", transitionRunHandler);
107
- };
108
- /** 移除事件 */
109
- const removeEvent = (el) => {
110
- el?.removeEventListener("transitioncancel", transitionCancelHandler);
111
- el?.removeEventListener("transitionend", transitionEndHandler);
112
- el?.removeEventListener("transitionrun", transitionRunHandler);
113
- };
114
- if (isRef(target)) watch(target, (_target, oldTarget) => {
115
- if (oldTarget) removeEvent(oldTarget);
116
- _target && addEvent(_target);
117
- });
118
- else if (target) addEvent(target);
119
115
  onBeforeUnmount(() => {
120
- const dom = getDom();
121
- removeEvent(dom);
116
+ clearFinalizeTimer();
122
117
  });
123
118
  return {
124
119
  toggle,
@@ -1 +1 @@
1
- {"version":3,"file":"use-css-transition.js","names":[],"sources":["../../src/use-transition/use-css-transition.ts"],"sourcesContent":["import { createToggle } from '@veltra/utils'\nimport { isRef, watch, onBeforeUnmount, computed } from 'vue'\n\nimport type { Returned, CssTransitionOptions } from './type'\n\nconst increaseTransitionCount = (el: HTMLElement & { _count?: number }) => {\n el._count = (el._count ?? 0) + 1\n}\n\nconst decreaseTransitionCount = (el: HTMLElement & { _count?: number }) => {\n el._count = (el._count ?? 0) - 1\n if (el._count <= 0) {\n delete el._count\n }\n}\n\n/**\n * 使用css过渡\n * @param options 过渡选项\n */\nexport function useCssTransition(options: CssTransitionOptions): Returned {\n const {\n name,\n target,\n afterEnter,\n afterLeave,\n enterCanceled,\n leaveCanceled,\n keepEnterTo = false\n } = options\n\n const getDom = (): (HTMLElement & { _count?: number }) | undefined =>\n isRef(target) ? target.value : target\n\n const classes = computed(() => {\n const _name = typeof name === 'string' ? name : name.value\n return {\n /** 进入前的类 */\n enterFrom: `${_name}-enter-from`,\n /** 进入后最终的类 */\n enterTo: `${_name}-enter-to`,\n /** 【进入动画】持续时的类 */\n enterActive: `${_name}-enter-active`,\n /** 离开前的类 */\n leaveFrom: `${_name}-leave-from`,\n /** 离开类 */\n leaveTo: `${_name}-leave-to`,\n /** 【离开动画】持续时的类 */\n leaveActive: `${_name}-leave-active`\n }\n })\n\n /** 开始进入动画 */\n const startTransitionIn = () => {\n const { enterActive, enterTo, enterFrom } = classes.value\n const dom = getDom()\n\n dom?.classList.add(enterFrom)\n\n requestAnimationFrame(() => {\n dom?.classList.add(enterActive)\n requestAnimationFrame(() => {\n dom?.classList.remove(enterFrom)\n dom?.classList.add(enterTo)\n })\n })\n }\n\n /** 开始离开动画 */\n const startTransitionOut = () => {\n const { leaveTo, leaveActive, leaveFrom, enterTo } = classes.value\n const dom = getDom()\n\n // 标记动画进入离开状态\n if (keepEnterTo) {\n dom?.classList.remove(enterTo)\n }\n dom?.classList.add(leaveFrom, leaveActive)\n\n requestAnimationFrame(() => {\n dom?.classList.add(leaveActive)\n requestAnimationFrame(() => {\n dom?.classList.remove(leaveFrom)\n dom?.classList.add(leaveTo)\n })\n })\n }\n\n const [active, toggle] = createToggle(false, (_active) => {\n _active ? startTransitionIn() : startTransitionOut()\n })\n\n const transitionEndHandler = (e: TransitionEvent) => {\n e.stopPropagation()\n\n const { leaveActive, enterActive, enterTo, leaveTo } = classes.value\n const dom = getDom()\n\n if (dom !== e.target) return\n\n decreaseTransitionCount(dom)\n\n if (dom._count) return\n\n // 激活状态,移除enter-active类\n if (active.value) {\n if (keepEnterTo) {\n dom?.classList.remove(enterActive)\n } else {\n dom?.classList.remove(enterActive, enterTo)\n }\n afterEnter?.()\n } else {\n dom?.classList.remove(leaveActive, leaveTo)\n afterLeave?.()\n }\n }\n\n const transitionRunHandler = (e: TransitionEvent) => {\n e.stopPropagation()\n const dom = getDom()\n if (dom !== e.target) return\n increaseTransitionCount(dom)\n }\n\n const transitionCancelHandler = (e: TransitionEvent) => {\n e.stopPropagation()\n const dom = getDom()\n\n if (dom !== e.target) return\n decreaseTransitionCount(dom)\n\n if (dom._count) return\n\n const { leaveActive, enterActive } = classes.value\n\n // 激活状态,移除enter-active类\n if (active.value) {\n dom?.classList.remove(enterActive)\n enterCanceled?.()\n } else {\n dom?.classList.remove(leaveActive)\n leaveCanceled?.()\n }\n }\n\n /** 添加事件 */\n const addEvent = (el?: HTMLElement) => {\n el?.addEventListener('transitioncancel', transitionCancelHandler)\n el?.addEventListener('transitionend', transitionEndHandler)\n el?.addEventListener('transitionrun', transitionRunHandler)\n }\n\n /** 移除事件 */\n const removeEvent = (el?: HTMLElement) => {\n el?.removeEventListener('transitioncancel', transitionCancelHandler)\n el?.removeEventListener('transitionend', transitionEndHandler)\n el?.removeEventListener('transitionrun', transitionRunHandler)\n }\n\n if (isRef(target)) {\n watch(target, (_target, oldTarget) => {\n if (oldTarget) {\n removeEvent(oldTarget)\n }\n _target && addEvent(_target)\n })\n } else if (target) {\n addEvent(target)\n }\n\n onBeforeUnmount(() => {\n const dom = getDom()\n removeEvent(dom)\n })\n\n return {\n toggle,\n enter() {\n toggle(true)\n },\n leave() {\n toggle(false)\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-css-transition.js","names":[],"sources":["../../src/use-transition/use-css-transition.ts"],"sourcesContent":["import { createToggle } from '@veltra/utils'\nimport { isRef, onBeforeUnmount, computed } from 'vue'\n\nimport type { Returned, CssTransitionOptions } from './type'\n\n/** 解析 CSS 时间值(s / ms)为毫秒 */\nconst parseTime = (time: string): number => {\n const t = time.trim()\n if (t.endsWith('ms')) return parseFloat(t)\n return parseFloat(t) * 1000\n}\n\n/**\n * 使用css过渡\n *\n * 阶段结束不依赖 transitionend 事件:进入/离开相互打断时类共存会导致\n * 过渡不触发、end 事件丢失从而卡死。这里每个阶段开始时清理相反阶段的类,\n * 并以「计算过渡耗时 + 定时器」兜底,保证阶段必然终结。\n *\n * @param options 过渡选项\n */\nexport function useCssTransition(options: CssTransitionOptions): Returned {\n const { name, target, afterEnter, afterLeave, keepEnterTo = false } = options\n\n const getDom = (): HTMLElement | undefined => (isRef(target) ? target.value : target)\n\n const classes = computed(() => {\n const _name = typeof name === 'string' ? name : name.value\n return {\n /** 进入前的类 */\n enterFrom: `${_name}-enter-from`,\n /** 进入后最终的类 */\n enterTo: `${_name}-enter-to`,\n /** 【进入动画】持续时的类 */\n enterActive: `${_name}-enter-active`,\n /** 离开前的类 */\n leaveFrom: `${_name}-leave-from`,\n /** 离开类 */\n leaveTo: `${_name}-leave-to`,\n /** 【离开动画】持续时的类 */\n leaveActive: `${_name}-leave-active`\n }\n })\n\n /** 阶段序号,使过期的 rAF / 定时器回调失效 */\n let phaseSeq = 0\n\n let finalizeTimer: ReturnType<typeof setTimeout> | undefined\n\n const clearFinalizeTimer = () => {\n if (finalizeTimer === undefined) return\n clearTimeout(finalizeTimer)\n finalizeTimer = undefined\n }\n\n /** 目标元素当前应用的最长过渡耗时(时长 + 延迟),无过渡则为 0 */\n const getTransitionDuration = (dom: HTMLElement): number => {\n const { transitionDuration, transitionDelay } = getComputedStyle(dom)\n const durations = transitionDuration.split(',').map(parseTime)\n const delays = transitionDelay.split(',').map(parseTime)\n return durations.reduce((max, d, i) => {\n return Math.max(max, d + (delays[i] ?? delays[delays.length - 1] ?? 0))\n }, 0)\n }\n\n /** 阶段完成:清理类并触发回调,仅当前阶段的定时器生效 */\n const finalize = (seq: number, entering: boolean) => {\n if (seq !== phaseSeq) return\n\n const dom = getDom()\n const { enterActive, enterTo, leaveActive, leaveTo } = classes.value\n\n if (entering) {\n if (keepEnterTo) {\n dom?.classList.remove(enterActive)\n } else {\n dom?.classList.remove(enterActive, enterTo)\n }\n afterEnter?.()\n } else {\n dom?.classList.remove(leaveActive, leaveTo)\n afterLeave?.()\n }\n }\n\n /** 过渡开始后调度兜底完成(晚 50ms 容错) */\n const scheduleFinalize = (seq: number, entering: boolean) => {\n clearFinalizeTimer()\n const dom = getDom()\n const duration = dom ? getTransitionDuration(dom) : 0\n finalizeTimer = setTimeout(() => finalize(seq, entering), duration + 50)\n }\n\n /** 开始进入动画 */\n const startTransitionIn = () => {\n const seq = ++phaseSeq\n const { enterActive, enterTo, enterFrom, leaveFrom, leaveActive, leaveTo } = classes.value\n const dom = getDom()\n\n // 清理离开阶段的残留类,避免与进入类共存导致过渡失效\n dom?.classList.remove(leaveFrom, leaveActive, leaveTo)\n dom?.classList.add(enterFrom)\n\n requestAnimationFrame(() => {\n if (seq !== phaseSeq) return\n dom?.classList.add(enterActive)\n requestAnimationFrame(() => {\n if (seq !== phaseSeq) return\n dom?.classList.remove(enterFrom)\n dom?.classList.add(enterTo)\n scheduleFinalize(seq, true)\n })\n })\n }\n\n /** 开始离开动画 */\n const startTransitionOut = () => {\n const seq = ++phaseSeq\n const { leaveTo, leaveActive, leaveFrom, enterFrom, enterActive, enterTo } = classes.value\n const dom = getDom()\n\n // 清理进入阶段的残留类,避免与离开类共存导致过渡失效\n dom?.classList.remove(enterFrom, enterActive, enterTo)\n dom?.classList.add(leaveFrom, leaveActive)\n\n requestAnimationFrame(() => {\n if (seq !== phaseSeq) return\n requestAnimationFrame(() => {\n if (seq !== phaseSeq) return\n dom?.classList.remove(leaveFrom)\n dom?.classList.add(leaveTo)\n scheduleFinalize(seq, false)\n })\n })\n }\n\n const [, toggle] = createToggle(false, (_active) => {\n _active ? startTransitionIn() : startTransitionOut()\n })\n\n onBeforeUnmount(() => {\n clearFinalizeTimer()\n })\n\n return {\n toggle,\n enter() {\n toggle(true)\n },\n leave() {\n toggle(false)\n }\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veltra/compositions",
3
- "version": "1.3.5",
3
+ "version": "1.3.7",
4
4
  "files": [
5
5
  "dist",
6
6
  "src"
@@ -22,7 +22,7 @@
22
22
  "peerDependencies": {
23
23
  "@cat-kit/core": ">=1.1.8",
24
24
  "@cat-kit/fe": ">=1.1.8",
25
- "@veltra/utils": "1.3.5",
25
+ "@veltra/utils": "1.3.7",
26
26
  "vue": ">=3.5.40"
27
27
  }
28
28
  }
@@ -5,11 +5,11 @@ export interface TransitionBase {
5
5
  target: ShallowRef<HTMLElement | undefined> | HTMLElement
6
6
  /** 进入动画结束回调 */
7
7
  afterEnter?: () => void
8
- /** 进入动画被取消回调 */
8
+ /** 进入动画被取消回调(css 实现中阶段被打断时由新阶段直接接管,不会触发) */
9
9
  enterCanceled?: () => void
10
10
  /** 离开动画结束回调 */
11
11
  afterLeave?: () => void
12
- /** 离开动画被取消回调 */
12
+ /** 离开动画被取消回调(css 实现中阶段被打断时由新阶段直接接管,不会触发) */
13
13
  leaveCanceled?: () => void
14
14
  }
15
15
 
@@ -1,36 +1,28 @@
1
1
  import { createToggle } from '@veltra/utils'
2
- import { isRef, watch, onBeforeUnmount, computed } from 'vue'
2
+ import { isRef, onBeforeUnmount, computed } from 'vue'
3
3
 
4
4
  import type { Returned, CssTransitionOptions } from './type'
5
5
 
6
- const increaseTransitionCount = (el: HTMLElement & { _count?: number }) => {
7
- el._count = (el._count ?? 0) + 1
8
- }
9
-
10
- const decreaseTransitionCount = (el: HTMLElement & { _count?: number }) => {
11
- el._count = (el._count ?? 0) - 1
12
- if (el._count <= 0) {
13
- delete el._count
14
- }
6
+ /** 解析 CSS 时间值(s / ms)为毫秒 */
7
+ const parseTime = (time: string): number => {
8
+ const t = time.trim()
9
+ if (t.endsWith('ms')) return parseFloat(t)
10
+ return parseFloat(t) * 1000
15
11
  }
16
12
 
17
13
  /**
18
14
  * 使用css过渡
15
+ *
16
+ * 阶段结束不依赖 transitionend 事件:进入/离开相互打断时类共存会导致
17
+ * 过渡不触发、end 事件丢失从而卡死。这里每个阶段开始时清理相反阶段的类,
18
+ * 并以「计算过渡耗时 + 定时器」兜底,保证阶段必然终结。
19
+ *
19
20
  * @param options 过渡选项
20
21
  */
21
22
  export function useCssTransition(options: CssTransitionOptions): Returned {
22
- const {
23
- name,
24
- target,
25
- afterEnter,
26
- afterLeave,
27
- enterCanceled,
28
- leaveCanceled,
29
- keepEnterTo = false
30
- } = options
31
-
32
- const getDom = (): (HTMLElement & { _count?: number }) | undefined =>
33
- isRef(target) ? target.value : target
23
+ const { name, target, afterEnter, afterLeave, keepEnterTo = false } = options
24
+
25
+ const getDom = (): HTMLElement | undefined => (isRef(target) ? target.value : target)
34
26
 
35
27
  const classes = computed(() => {
36
28
  const _name = typeof name === 'string' ? name : name.value
@@ -50,60 +42,35 @@ export function useCssTransition(options: CssTransitionOptions): Returned {
50
42
  }
51
43
  })
52
44
 
53
- /** 开始进入动画 */
54
- const startTransitionIn = () => {
55
- const { enterActive, enterTo, enterFrom } = classes.value
56
- const dom = getDom()
45
+ /** 阶段序号,使过期的 rAF / 定时器回调失效 */
46
+ let phaseSeq = 0
57
47
 
58
- dom?.classList.add(enterFrom)
48
+ let finalizeTimer: ReturnType<typeof setTimeout> | undefined
59
49
 
60
- requestAnimationFrame(() => {
61
- dom?.classList.add(enterActive)
62
- requestAnimationFrame(() => {
63
- dom?.classList.remove(enterFrom)
64
- dom?.classList.add(enterTo)
65
- })
66
- })
50
+ const clearFinalizeTimer = () => {
51
+ if (finalizeTimer === undefined) return
52
+ clearTimeout(finalizeTimer)
53
+ finalizeTimer = undefined
67
54
  }
68
55
 
69
- /** 开始离开动画 */
70
- const startTransitionOut = () => {
71
- const { leaveTo, leaveActive, leaveFrom, enterTo } = classes.value
72
- const dom = getDom()
73
-
74
- // 标记动画进入离开状态
75
- if (keepEnterTo) {
76
- dom?.classList.remove(enterTo)
77
- }
78
- dom?.classList.add(leaveFrom, leaveActive)
79
-
80
- requestAnimationFrame(() => {
81
- dom?.classList.add(leaveActive)
82
- requestAnimationFrame(() => {
83
- dom?.classList.remove(leaveFrom)
84
- dom?.classList.add(leaveTo)
85
- })
86
- })
56
+ /** 目标元素当前应用的最长过渡耗时(时长 + 延迟),无过渡则为 0 */
57
+ const getTransitionDuration = (dom: HTMLElement): number => {
58
+ const { transitionDuration, transitionDelay } = getComputedStyle(dom)
59
+ const durations = transitionDuration.split(',').map(parseTime)
60
+ const delays = transitionDelay.split(',').map(parseTime)
61
+ return durations.reduce((max, d, i) => {
62
+ return Math.max(max, d + (delays[i] ?? delays[delays.length - 1] ?? 0))
63
+ }, 0)
87
64
  }
88
65
 
89
- const [active, toggle] = createToggle(false, (_active) => {
90
- _active ? startTransitionIn() : startTransitionOut()
91
- })
66
+ /** 阶段完成:清理类并触发回调,仅当前阶段的定时器生效 */
67
+ const finalize = (seq: number, entering: boolean) => {
68
+ if (seq !== phaseSeq) return
92
69
 
93
- const transitionEndHandler = (e: TransitionEvent) => {
94
- e.stopPropagation()
95
-
96
- const { leaveActive, enterActive, enterTo, leaveTo } = classes.value
97
70
  const dom = getDom()
71
+ const { enterActive, enterTo, leaveActive, leaveTo } = classes.value
98
72
 
99
- if (dom !== e.target) return
100
-
101
- decreaseTransitionCount(dom)
102
-
103
- if (dom._count) return
104
-
105
- // 激活状态,移除enter-active类
106
- if (active.value) {
73
+ if (entering) {
107
74
  if (keepEnterTo) {
108
75
  dom?.classList.remove(enterActive)
109
76
  } else {
@@ -116,62 +83,63 @@ export function useCssTransition(options: CssTransitionOptions): Returned {
116
83
  }
117
84
  }
118
85
 
119
- const transitionRunHandler = (e: TransitionEvent) => {
120
- e.stopPropagation()
86
+ /** 过渡开始后调度兜底完成(晚 50ms 容错) */
87
+ const scheduleFinalize = (seq: number, entering: boolean) => {
88
+ clearFinalizeTimer()
121
89
  const dom = getDom()
122
- if (dom !== e.target) return
123
- increaseTransitionCount(dom)
90
+ const duration = dom ? getTransitionDuration(dom) : 0
91
+ finalizeTimer = setTimeout(() => finalize(seq, entering), duration + 50)
124
92
  }
125
93
 
126
- const transitionCancelHandler = (e: TransitionEvent) => {
127
- e.stopPropagation()
94
+ /** 开始进入动画 */
95
+ const startTransitionIn = () => {
96
+ const seq = ++phaseSeq
97
+ const { enterActive, enterTo, enterFrom, leaveFrom, leaveActive, leaveTo } = classes.value
128
98
  const dom = getDom()
129
99
 
130
- if (dom !== e.target) return
131
- decreaseTransitionCount(dom)
132
-
133
- if (dom._count) return
134
-
135
- const { leaveActive, enterActive } = classes.value
100
+ // 清理离开阶段的残留类,避免与进入类共存导致过渡失效
101
+ dom?.classList.remove(leaveFrom, leaveActive, leaveTo)
102
+ dom?.classList.add(enterFrom)
136
103
 
137
- // 激活状态,移除enter-active类
138
- if (active.value) {
139
- dom?.classList.remove(enterActive)
140
- enterCanceled?.()
141
- } else {
142
- dom?.classList.remove(leaveActive)
143
- leaveCanceled?.()
144
- }
104
+ requestAnimationFrame(() => {
105
+ if (seq !== phaseSeq) return
106
+ dom?.classList.add(enterActive)
107
+ requestAnimationFrame(() => {
108
+ if (seq !== phaseSeq) return
109
+ dom?.classList.remove(enterFrom)
110
+ dom?.classList.add(enterTo)
111
+ scheduleFinalize(seq, true)
112
+ })
113
+ })
145
114
  }
146
115
 
147
- /** 添加事件 */
148
- const addEvent = (el?: HTMLElement) => {
149
- el?.addEventListener('transitioncancel', transitionCancelHandler)
150
- el?.addEventListener('transitionend', transitionEndHandler)
151
- el?.addEventListener('transitionrun', transitionRunHandler)
152
- }
116
+ /** 开始离开动画 */
117
+ const startTransitionOut = () => {
118
+ const seq = ++phaseSeq
119
+ const { leaveTo, leaveActive, leaveFrom, enterFrom, enterActive, enterTo } = classes.value
120
+ const dom = getDom()
153
121
 
154
- /** 移除事件 */
155
- const removeEvent = (el?: HTMLElement) => {
156
- el?.removeEventListener('transitioncancel', transitionCancelHandler)
157
- el?.removeEventListener('transitionend', transitionEndHandler)
158
- el?.removeEventListener('transitionrun', transitionRunHandler)
159
- }
122
+ // 清理进入阶段的残留类,避免与离开类共存导致过渡失效
123
+ dom?.classList.remove(enterFrom, enterActive, enterTo)
124
+ dom?.classList.add(leaveFrom, leaveActive)
160
125
 
161
- if (isRef(target)) {
162
- watch(target, (_target, oldTarget) => {
163
- if (oldTarget) {
164
- removeEvent(oldTarget)
165
- }
166
- _target && addEvent(_target)
126
+ requestAnimationFrame(() => {
127
+ if (seq !== phaseSeq) return
128
+ requestAnimationFrame(() => {
129
+ if (seq !== phaseSeq) return
130
+ dom?.classList.remove(leaveFrom)
131
+ dom?.classList.add(leaveTo)
132
+ scheduleFinalize(seq, false)
133
+ })
167
134
  })
168
- } else if (target) {
169
- addEvent(target)
170
135
  }
171
136
 
137
+ const [, toggle] = createToggle(false, (_active) => {
138
+ _active ? startTransitionIn() : startTransitionOut()
139
+ })
140
+
172
141
  onBeforeUnmount(() => {
173
- const dom = getDom()
174
- removeEvent(dom)
142
+ clearFinalizeTimer()
175
143
  })
176
144
 
177
145
  return {