Transition.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. import {
  2. BaseTransition,
  3. type BaseTransitionProps,
  4. BaseTransitionPropsValidators,
  5. DeprecationTypes,
  6. type FunctionalComponent,
  7. assertNumber,
  8. compatUtils,
  9. h,
  10. isVNode,
  11. } from '@vue/runtime-core'
  12. import { extend, isArray, isObject, toNumber } from '@vue/shared'
  13. const TRANSITION = 'transition'
  14. const ANIMATION = 'animation'
  15. type AnimationTypes = typeof TRANSITION | typeof ANIMATION
  16. export interface TransitionProps extends BaseTransitionProps<Element> {
  17. name?: string
  18. type?: AnimationTypes
  19. css?: boolean
  20. duration?: number | { enter: number; leave: number }
  21. // custom transition classes
  22. enterFromClass?: string
  23. enterActiveClass?: string
  24. enterToClass?: string
  25. appearFromClass?: string
  26. appearActiveClass?: string
  27. appearToClass?: string
  28. leaveFromClass?: string
  29. leaveActiveClass?: string
  30. leaveToClass?: string
  31. }
  32. export interface VaporTransitionInterface {
  33. applyTransition: (
  34. props: TransitionProps,
  35. slots: { default: () => any },
  36. ) => void
  37. }
  38. let vaporTransitionImpl: VaporTransitionInterface | null = null
  39. export const registerVaporTransition = (
  40. impl: VaporTransitionInterface,
  41. ): void => {
  42. vaporTransitionImpl = impl
  43. }
  44. export const vtcKey: unique symbol = Symbol('_vtc')
  45. export interface ElementWithTransition extends HTMLElement {
  46. // _vtc = Vue Transition Classes.
  47. // Store the temporarily-added transition classes on the element
  48. // so that we can avoid overwriting them if the element's class is patched
  49. // during the transition.
  50. [vtcKey]?: Set<string>
  51. }
  52. const DOMTransitionPropsValidators = {
  53. name: String,
  54. type: String,
  55. css: {
  56. type: Boolean,
  57. default: true,
  58. },
  59. duration: [String, Number, Object],
  60. enterFromClass: String,
  61. enterActiveClass: String,
  62. enterToClass: String,
  63. appearFromClass: String,
  64. appearActiveClass: String,
  65. appearToClass: String,
  66. leaveFromClass: String,
  67. leaveActiveClass: String,
  68. leaveToClass: String,
  69. }
  70. export const TransitionPropsValidators: any = /*@__PURE__*/ extend(
  71. {},
  72. BaseTransitionPropsValidators as any,
  73. DOMTransitionPropsValidators,
  74. )
  75. /**
  76. * Wrap logic that attaches extra properties to Transition in a function
  77. * so that it can be annotated as pure
  78. */
  79. const decorate = (t: typeof Transition) => {
  80. t.displayName = 'Transition'
  81. t.props = TransitionPropsValidators
  82. if (__COMPAT__) {
  83. t.__isBuiltIn = true
  84. }
  85. return t
  86. }
  87. /**
  88. * DOM Transition is a higher-order-component based on the platform-agnostic
  89. * base Transition component, with DOM-specific logic.
  90. */
  91. export const Transition: FunctionalComponent<TransitionProps> =
  92. /*@__PURE__*/ decorate((props, { slots }) => {
  93. const children = slots.default && slots.default()
  94. const isVNodeChildren = isArray(children) && children.some(c => isVNode(c))
  95. const resolvedProps = resolveTransitionProps(props)
  96. if (isVNodeChildren) {
  97. return h(BaseTransition, resolvedProps, {
  98. default: () => children,
  99. })
  100. }
  101. // vapor transition
  102. return vaporTransitionImpl!.applyTransition(resolvedProps, {
  103. default: () => children,
  104. })
  105. })
  106. /**
  107. * #3227 Incoming hooks may be merged into arrays when wrapping Transition
  108. * with custom HOCs.
  109. */
  110. const callHook = (
  111. hook: Function | Function[] | undefined,
  112. args: any[] = [],
  113. ) => {
  114. if (isArray(hook)) {
  115. hook.forEach(h => h(...args))
  116. } else if (hook) {
  117. hook(...args)
  118. }
  119. }
  120. /**
  121. * Check if a hook expects a callback (2nd arg), which means the user
  122. * intends to explicitly control the end of the transition.
  123. */
  124. const hasExplicitCallback = (
  125. hook: Function | Function[] | undefined,
  126. ): boolean => {
  127. return hook
  128. ? isArray(hook)
  129. ? hook.some(h => h.length > 1)
  130. : hook.length > 1
  131. : false
  132. }
  133. export function resolveTransitionProps(
  134. rawProps: TransitionProps,
  135. ): BaseTransitionProps<Element> {
  136. const baseProps: BaseTransitionProps<Element> = {}
  137. for (const key in rawProps) {
  138. if (!(key in DOMTransitionPropsValidators)) {
  139. ;(baseProps as any)[key] = (rawProps as any)[key]
  140. }
  141. }
  142. if (rawProps.css === false) {
  143. return baseProps
  144. }
  145. const {
  146. name = 'v',
  147. type,
  148. duration,
  149. enterFromClass = `${name}-enter-from`,
  150. enterActiveClass = `${name}-enter-active`,
  151. enterToClass = `${name}-enter-to`,
  152. appearFromClass = enterFromClass,
  153. appearActiveClass = enterActiveClass,
  154. appearToClass = enterToClass,
  155. leaveFromClass = `${name}-leave-from`,
  156. leaveActiveClass = `${name}-leave-active`,
  157. leaveToClass = `${name}-leave-to`,
  158. } = rawProps
  159. // legacy transition class compat
  160. const legacyClassEnabled =
  161. __COMPAT__ &&
  162. compatUtils.isCompatEnabled(DeprecationTypes.TRANSITION_CLASSES, null)
  163. let legacyEnterFromClass: string
  164. let legacyAppearFromClass: string
  165. let legacyLeaveFromClass: string
  166. if (__COMPAT__ && legacyClassEnabled) {
  167. const toLegacyClass = (cls: string) => cls.replace(/-from$/, '')
  168. if (!rawProps.enterFromClass) {
  169. legacyEnterFromClass = toLegacyClass(enterFromClass)
  170. }
  171. if (!rawProps.appearFromClass) {
  172. legacyAppearFromClass = toLegacyClass(appearFromClass)
  173. }
  174. if (!rawProps.leaveFromClass) {
  175. legacyLeaveFromClass = toLegacyClass(leaveFromClass)
  176. }
  177. }
  178. const durations = normalizeDuration(duration)
  179. const enterDuration = durations && durations[0]
  180. const leaveDuration = durations && durations[1]
  181. const {
  182. onBeforeEnter,
  183. onEnter,
  184. onEnterCancelled,
  185. onLeave,
  186. onLeaveCancelled,
  187. onBeforeAppear = onBeforeEnter,
  188. onAppear = onEnter,
  189. onAppearCancelled = onEnterCancelled,
  190. } = baseProps
  191. const finishEnter = (
  192. el: Element & { _enterCancelled?: boolean },
  193. isAppear: boolean,
  194. done?: () => void,
  195. isCancelled?: boolean,
  196. ) => {
  197. el._enterCancelled = isCancelled
  198. removeTransitionClass(el, isAppear ? appearToClass : enterToClass)
  199. removeTransitionClass(el, isAppear ? appearActiveClass : enterActiveClass)
  200. done && done()
  201. }
  202. const finishLeave = (
  203. el: Element & { _isLeaving?: boolean },
  204. done?: () => void,
  205. ) => {
  206. el._isLeaving = false
  207. removeTransitionClass(el, leaveFromClass)
  208. removeTransitionClass(el, leaveToClass)
  209. removeTransitionClass(el, leaveActiveClass)
  210. done && done()
  211. }
  212. const makeEnterHook = (isAppear: boolean) => {
  213. return (el: Element, done: () => void) => {
  214. const hook = isAppear ? onAppear : onEnter
  215. const resolve = () => finishEnter(el, isAppear, done)
  216. callHook(hook, [el, resolve])
  217. nextFrame(() => {
  218. removeTransitionClass(el, isAppear ? appearFromClass : enterFromClass)
  219. if (__COMPAT__ && legacyClassEnabled) {
  220. const legacyClass = isAppear
  221. ? legacyAppearFromClass
  222. : legacyEnterFromClass
  223. if (legacyClass) {
  224. removeTransitionClass(el, legacyClass)
  225. }
  226. }
  227. addTransitionClass(el, isAppear ? appearToClass : enterToClass)
  228. if (!hasExplicitCallback(hook)) {
  229. whenTransitionEnds(el, type, enterDuration, resolve)
  230. }
  231. })
  232. }
  233. }
  234. return extend(baseProps, {
  235. onBeforeEnter(el) {
  236. callHook(onBeforeEnter, [el])
  237. addTransitionClass(el, enterFromClass)
  238. if (__COMPAT__ && legacyClassEnabled && legacyEnterFromClass) {
  239. addTransitionClass(el, legacyEnterFromClass)
  240. }
  241. addTransitionClass(el, enterActiveClass)
  242. },
  243. onBeforeAppear(el) {
  244. callHook(onBeforeAppear, [el])
  245. addTransitionClass(el, appearFromClass)
  246. if (__COMPAT__ && legacyClassEnabled && legacyAppearFromClass) {
  247. addTransitionClass(el, legacyAppearFromClass)
  248. }
  249. addTransitionClass(el, appearActiveClass)
  250. },
  251. onEnter: makeEnterHook(false),
  252. onAppear: makeEnterHook(true),
  253. onLeave(
  254. el: Element & { _isLeaving?: boolean; _enterCancelled?: boolean },
  255. done,
  256. ) {
  257. el._isLeaving = true
  258. const resolve = () => finishLeave(el, done)
  259. addTransitionClass(el, leaveFromClass)
  260. if (__COMPAT__ && legacyClassEnabled && legacyLeaveFromClass) {
  261. addTransitionClass(el, legacyLeaveFromClass)
  262. }
  263. // add *-leave-active class before reflow so in the case of a cancelled enter transition
  264. // the css will not get the final state (#10677)
  265. if (!el._enterCancelled) {
  266. // force reflow so *-leave-from classes immediately take effect (#2593)
  267. forceReflow()
  268. addTransitionClass(el, leaveActiveClass)
  269. } else {
  270. addTransitionClass(el, leaveActiveClass)
  271. forceReflow()
  272. }
  273. nextFrame(() => {
  274. if (!el._isLeaving) {
  275. // cancelled
  276. return
  277. }
  278. removeTransitionClass(el, leaveFromClass)
  279. if (__COMPAT__ && legacyClassEnabled && legacyLeaveFromClass) {
  280. removeTransitionClass(el, legacyLeaveFromClass)
  281. }
  282. addTransitionClass(el, leaveToClass)
  283. if (!hasExplicitCallback(onLeave)) {
  284. whenTransitionEnds(el, type, leaveDuration, resolve)
  285. }
  286. })
  287. callHook(onLeave, [el, resolve])
  288. },
  289. onEnterCancelled(el) {
  290. finishEnter(el, false, undefined, true)
  291. callHook(onEnterCancelled, [el])
  292. },
  293. onAppearCancelled(el) {
  294. finishEnter(el, true, undefined, true)
  295. callHook(onAppearCancelled, [el])
  296. },
  297. onLeaveCancelled(el) {
  298. finishLeave(el)
  299. callHook(onLeaveCancelled, [el])
  300. },
  301. } as BaseTransitionProps<Element>)
  302. }
  303. function normalizeDuration(
  304. duration: TransitionProps['duration'],
  305. ): [number, number] | null {
  306. if (duration == null) {
  307. return null
  308. } else if (isObject(duration)) {
  309. return [NumberOf(duration.enter), NumberOf(duration.leave)]
  310. } else {
  311. const n = NumberOf(duration)
  312. return [n, n]
  313. }
  314. }
  315. function NumberOf(val: unknown): number {
  316. const res = toNumber(val)
  317. if (__DEV__) {
  318. assertNumber(res, '<transition> explicit duration')
  319. }
  320. return res
  321. }
  322. export function addTransitionClass(el: Element, cls: string): void {
  323. cls.split(/\s+/).forEach(c => c && el.classList.add(c))
  324. ;(
  325. (el as ElementWithTransition)[vtcKey] ||
  326. ((el as ElementWithTransition)[vtcKey] = new Set())
  327. ).add(cls)
  328. }
  329. export function removeTransitionClass(el: Element, cls: string): void {
  330. cls.split(/\s+/).forEach(c => c && el.classList.remove(c))
  331. const _vtc = (el as ElementWithTransition)[vtcKey]
  332. if (_vtc) {
  333. _vtc.delete(cls)
  334. if (!_vtc!.size) {
  335. ;(el as ElementWithTransition)[vtcKey] = undefined
  336. }
  337. }
  338. }
  339. function nextFrame(cb: () => void) {
  340. requestAnimationFrame(() => {
  341. requestAnimationFrame(cb)
  342. })
  343. }
  344. let endId = 0
  345. function whenTransitionEnds(
  346. el: Element & { _endId?: number },
  347. expectedType: TransitionProps['type'] | undefined,
  348. explicitTimeout: number | null,
  349. resolve: () => void,
  350. ) {
  351. const id = (el._endId = ++endId)
  352. const resolveIfNotStale = () => {
  353. if (id === el._endId) {
  354. resolve()
  355. }
  356. }
  357. if (explicitTimeout != null) {
  358. return setTimeout(resolveIfNotStale, explicitTimeout)
  359. }
  360. const { type, timeout, propCount } = getTransitionInfo(el, expectedType)
  361. if (!type) {
  362. return resolve()
  363. }
  364. const endEvent = type + 'end'
  365. let ended = 0
  366. const end = () => {
  367. el.removeEventListener(endEvent, onEnd)
  368. resolveIfNotStale()
  369. }
  370. const onEnd = (e: Event) => {
  371. if (e.target === el && ++ended >= propCount) {
  372. end()
  373. }
  374. }
  375. setTimeout(() => {
  376. if (ended < propCount) {
  377. end()
  378. }
  379. }, timeout + 1)
  380. el.addEventListener(endEvent, onEnd)
  381. }
  382. interface CSSTransitionInfo {
  383. type: AnimationTypes | null
  384. propCount: number
  385. timeout: number
  386. hasTransform: boolean
  387. }
  388. type AnimationProperties = 'Delay' | 'Duration'
  389. type StylePropertiesKey =
  390. | `${AnimationTypes}${AnimationProperties}`
  391. | `${typeof TRANSITION}Property`
  392. export function getTransitionInfo(
  393. el: Element,
  394. expectedType?: TransitionProps['type'],
  395. ): CSSTransitionInfo {
  396. const styles = window.getComputedStyle(el) as Pick<
  397. CSSStyleDeclaration,
  398. StylePropertiesKey
  399. >
  400. // JSDOM may return undefined for transition properties
  401. const getStyleProperties = (key: StylePropertiesKey) =>
  402. (styles[key] || '').split(', ')
  403. const transitionDelays = getStyleProperties(`${TRANSITION}Delay`)
  404. const transitionDurations = getStyleProperties(`${TRANSITION}Duration`)
  405. const transitionTimeout = getTimeout(transitionDelays, transitionDurations)
  406. const animationDelays = getStyleProperties(`${ANIMATION}Delay`)
  407. const animationDurations = getStyleProperties(`${ANIMATION}Duration`)
  408. const animationTimeout = getTimeout(animationDelays, animationDurations)
  409. let type: CSSTransitionInfo['type'] = null
  410. let timeout = 0
  411. let propCount = 0
  412. if (expectedType === TRANSITION) {
  413. if (transitionTimeout > 0) {
  414. type = TRANSITION
  415. timeout = transitionTimeout
  416. propCount = transitionDurations.length
  417. }
  418. } else if (expectedType === ANIMATION) {
  419. if (animationTimeout > 0) {
  420. type = ANIMATION
  421. timeout = animationTimeout
  422. propCount = animationDurations.length
  423. }
  424. } else {
  425. timeout = Math.max(transitionTimeout, animationTimeout)
  426. type =
  427. timeout > 0
  428. ? transitionTimeout > animationTimeout
  429. ? TRANSITION
  430. : ANIMATION
  431. : null
  432. propCount = type
  433. ? type === TRANSITION
  434. ? transitionDurations.length
  435. : animationDurations.length
  436. : 0
  437. }
  438. const hasTransform =
  439. type === TRANSITION &&
  440. /\b(transform|all)(,|$)/.test(
  441. getStyleProperties(`${TRANSITION}Property`).toString(),
  442. )
  443. return {
  444. type,
  445. timeout,
  446. propCount,
  447. hasTransform,
  448. }
  449. }
  450. function getTimeout(delays: string[], durations: string[]): number {
  451. while (delays.length < durations.length) {
  452. delays = delays.concat(delays)
  453. }
  454. return Math.max(...durations.map((d, i) => toMs(d) + toMs(delays[i])))
  455. }
  456. // Old versions of Chromium (below 61.0.3163.100) formats floating pointer
  457. // numbers in a locale-dependent way, using a comma instead of a dot.
  458. // If comma is not replaced with a dot, the input will be rounded down
  459. // (i.e. acting as a floor function) causing unexpected behaviors
  460. function toMs(s: string): number {
  461. // #8409 default value for CSS durations can be 'auto'
  462. if (s === 'auto') return 0
  463. return Number(s.slice(0, -1).replace(',', '.')) * 1000
  464. }
  465. // synchronously force layout to put elements into a certain state
  466. export function forceReflow(): number {
  467. return document.body.offsetHeight
  468. }