feat(transition): TransitionGroup
This commit is contained in:
parent
020e109abd
commit
800b0f0e7a
@ -1,7 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
getCurrentInstance,
|
getCurrentInstance,
|
||||||
SetupContext,
|
SetupContext,
|
||||||
ComponentOptions
|
ComponentOptions,
|
||||||
|
ComponentInternalInstance
|
||||||
} from '../component'
|
} from '../component'
|
||||||
import {
|
import {
|
||||||
cloneVNode,
|
cloneVNode,
|
||||||
@ -61,9 +62,9 @@ type TransitionHookCaller = (
|
|||||||
args?: any[]
|
args?: any[]
|
||||||
) => void
|
) => void
|
||||||
|
|
||||||
type PendingCallback = (cancelled?: boolean) => void
|
export type PendingCallback = (cancelled?: boolean) => void
|
||||||
|
|
||||||
interface TransitionState {
|
export interface TransitionState {
|
||||||
isMounted: boolean
|
isMounted: boolean
|
||||||
isLeaving: boolean
|
isLeaving: boolean
|
||||||
isUnmounting: boolean
|
isUnmounting: boolean
|
||||||
@ -72,7 +73,7 @@ interface TransitionState {
|
|||||||
leavingVNodes: Map<any, Record<string, VNode>>
|
leavingVNodes: Map<any, Record<string, VNode>>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TransitionElement {
|
export interface TransitionElement {
|
||||||
// in persisted mode (e.g. v-show), the same element is toggled, so the
|
// in persisted mode (e.g. v-show), the same element is toggled, so the
|
||||||
// pending enter/leave callbacks may need to cancalled if the state is toggled
|
// pending enter/leave callbacks may need to cancalled if the state is toggled
|
||||||
// before it finishes.
|
// before it finishes.
|
||||||
@ -80,32 +81,27 @@ interface TransitionElement {
|
|||||||
_leaveCb?: PendingCallback
|
_leaveCb?: PendingCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useTransitionState(): TransitionState {
|
||||||
|
const state: TransitionState = {
|
||||||
|
isMounted: false,
|
||||||
|
isLeaving: false,
|
||||||
|
isUnmounting: false,
|
||||||
|
leavingVNodes: new Map()
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
state.isMounted = true
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
state.isUnmounting = true
|
||||||
|
})
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
const BaseTransitionImpl = {
|
const BaseTransitionImpl = {
|
||||||
name: `BaseTransition`,
|
name: `BaseTransition`,
|
||||||
setup(props: BaseTransitionProps, { slots }: SetupContext) {
|
setup(props: BaseTransitionProps, { slots }: SetupContext) {
|
||||||
const instance = getCurrentInstance()!
|
const instance = getCurrentInstance()!
|
||||||
const state: TransitionState = {
|
const state = useTransitionState()
|
||||||
isMounted: false,
|
|
||||||
isLeaving: false,
|
|
||||||
isUnmounting: false,
|
|
||||||
leavingVNodes: new Map()
|
|
||||||
}
|
|
||||||
onMounted(() => {
|
|
||||||
state.isMounted = true
|
|
||||||
})
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
state.isUnmounting = true
|
|
||||||
})
|
|
||||||
|
|
||||||
const callTransitionHook: TransitionHookCaller = (hook, args) => {
|
|
||||||
hook &&
|
|
||||||
callWithAsyncErrorHandling(
|
|
||||||
hook,
|
|
||||||
instance,
|
|
||||||
ErrorCodes.TRANSITION_HOOK,
|
|
||||||
args
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
const children = slots.default && slots.default()
|
const children = slots.default && slots.default()
|
||||||
@ -147,7 +143,7 @@ const BaseTransitionImpl = {
|
|||||||
innerChild,
|
innerChild,
|
||||||
rawProps,
|
rawProps,
|
||||||
state,
|
state,
|
||||||
callTransitionHook
|
instance
|
||||||
))
|
))
|
||||||
|
|
||||||
const oldChild = instance.subTree
|
const oldChild = instance.subTree
|
||||||
@ -163,7 +159,7 @@ const BaseTransitionImpl = {
|
|||||||
oldInnerChild,
|
oldInnerChild,
|
||||||
rawProps,
|
rawProps,
|
||||||
state,
|
state,
|
||||||
callTransitionHook
|
instance
|
||||||
)
|
)
|
||||||
// update old tree's hooks in case of dynamic transition
|
// update old tree's hooks in case of dynamic transition
|
||||||
setTransitionHooks(oldInnerChild, leavingHooks)
|
setTransitionHooks(oldInnerChild, leavingHooks)
|
||||||
@ -245,7 +241,7 @@ function getLeavingNodesForType(
|
|||||||
|
|
||||||
// The transition hooks are attached to the vnode as vnode.transition
|
// The transition hooks are attached to the vnode as vnode.transition
|
||||||
// and will be called at appropriate timing in the renderer.
|
// and will be called at appropriate timing in the renderer.
|
||||||
function resolveTransitionHooks(
|
export function resolveTransitionHooks(
|
||||||
vnode: VNode,
|
vnode: VNode,
|
||||||
{
|
{
|
||||||
appear,
|
appear,
|
||||||
@ -260,11 +256,21 @@ function resolveTransitionHooks(
|
|||||||
onLeaveCancelled
|
onLeaveCancelled
|
||||||
}: BaseTransitionProps,
|
}: BaseTransitionProps,
|
||||||
state: TransitionState,
|
state: TransitionState,
|
||||||
callHook: TransitionHookCaller
|
instance: ComponentInternalInstance
|
||||||
): TransitionHooks {
|
): TransitionHooks {
|
||||||
const key = String(vnode.key)
|
const key = String(vnode.key)
|
||||||
const leavingVNodesCache = getLeavingNodesForType(state, vnode)
|
const leavingVNodesCache = getLeavingNodesForType(state, vnode)
|
||||||
|
|
||||||
|
const callHook: TransitionHookCaller = (hook, args) => {
|
||||||
|
hook &&
|
||||||
|
callWithAsyncErrorHandling(
|
||||||
|
hook,
|
||||||
|
instance,
|
||||||
|
ErrorCodes.TRANSITION_HOOK,
|
||||||
|
args
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const hooks: TransitionHooks = {
|
const hooks: TransitionHooks = {
|
||||||
persisted,
|
persisted,
|
||||||
beforeEnter(el: TransitionElement) {
|
beforeEnter(el: TransitionElement) {
|
||||||
|
@ -58,6 +58,13 @@ export {
|
|||||||
callWithErrorHandling,
|
callWithErrorHandling,
|
||||||
callWithAsyncErrorHandling
|
callWithAsyncErrorHandling
|
||||||
} from './errorHandling'
|
} from './errorHandling'
|
||||||
|
export {
|
||||||
|
useTransitionState,
|
||||||
|
TransitionState,
|
||||||
|
resolveTransitionHooks,
|
||||||
|
setTransitionHooks,
|
||||||
|
TransitionHooks
|
||||||
|
} from './components/BaseTransition'
|
||||||
|
|
||||||
// Internal, for compiler generated code
|
// Internal, for compiler generated code
|
||||||
// should sync with '@vue/compiler-core/src/runtimeConstants.ts'
|
// should sync with '@vue/compiler-core/src/runtimeConstants.ts'
|
||||||
|
@ -58,7 +58,7 @@ if (__DEV__) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveTransitionProps({
|
export function resolveTransitionProps({
|
||||||
name = 'v',
|
name = 'v',
|
||||||
type,
|
type,
|
||||||
css = true,
|
css = true,
|
||||||
@ -91,7 +91,7 @@ function resolveTransitionProps({
|
|||||||
enterToClass = appearToClass
|
enterToClass = appearToClass
|
||||||
}
|
}
|
||||||
|
|
||||||
type Hook = (el: Element, done?: () => void) => void
|
type Hook = (el: HTMLElement, done?: () => void) => void
|
||||||
|
|
||||||
const finishEnter: Hook = (el, done) => {
|
const finishEnter: Hook = (el, done) => {
|
||||||
removeTransitionClass(el, enterToClass)
|
removeTransitionClass(el, enterToClass)
|
||||||
@ -188,7 +188,7 @@ function validateDuration(val: unknown) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ElementWithTransition extends Element {
|
export interface ElementWithTransition extends HTMLElement {
|
||||||
// _vtc = Vue Transition Classes.
|
// _vtc = Vue Transition Classes.
|
||||||
// Store the temporarily-added transition classes on the element
|
// Store the temporarily-added transition classes on the element
|
||||||
// so that we can avoid overwriting them if the element's class is patched
|
// so that we can avoid overwriting them if the element's class is patched
|
||||||
@ -196,12 +196,12 @@ export interface ElementWithTransition extends Element {
|
|||||||
_vtc?: Set<string>
|
_vtc?: Set<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
function addTransitionClass(el: ElementWithTransition, cls: string) {
|
export function addTransitionClass(el: ElementWithTransition, cls: string) {
|
||||||
el.classList.add(cls)
|
el.classList.add(cls)
|
||||||
;(el._vtc || (el._vtc = new Set())).add(cls)
|
;(el._vtc || (el._vtc = new Set())).add(cls)
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeTransitionClass(el: ElementWithTransition, cls: string) {
|
export function removeTransitionClass(el: ElementWithTransition, cls: string) {
|
||||||
el.classList.remove(cls)
|
el.classList.remove(cls)
|
||||||
if (el._vtc) {
|
if (el._vtc) {
|
||||||
el._vtc.delete(cls)
|
el._vtc.delete(cls)
|
||||||
@ -252,9 +252,10 @@ interface CSSTransitionInfo {
|
|||||||
type: typeof TRANSITION | typeof ANIMATION | null
|
type: typeof TRANSITION | typeof ANIMATION | null
|
||||||
propCount: number
|
propCount: number
|
||||||
timeout: number
|
timeout: number
|
||||||
|
hasTransform: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTransitionInfo(
|
export function getTransitionInfo(
|
||||||
el: Element,
|
el: Element,
|
||||||
expectedType?: TransitionProps['type']
|
expectedType?: TransitionProps['type']
|
||||||
): CSSTransitionInfo {
|
): CSSTransitionInfo {
|
||||||
@ -298,10 +299,14 @@ function getTransitionInfo(
|
|||||||
: animationDurations.length
|
: animationDurations.length
|
||||||
: 0
|
: 0
|
||||||
}
|
}
|
||||||
|
const hasTransform =
|
||||||
|
type === TRANSITION &&
|
||||||
|
/\b(transform|all)(,|$)/.test(styles[TRANSITION + 'Property'])
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
timeout,
|
timeout,
|
||||||
propCount
|
propCount,
|
||||||
|
hasTransform
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
180
packages/runtime-dom/src/components/TransitionGroup.ts
Normal file
180
packages/runtime-dom/src/components/TransitionGroup.ts
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import {
|
||||||
|
TransitionProps,
|
||||||
|
addTransitionClass,
|
||||||
|
removeTransitionClass,
|
||||||
|
ElementWithTransition,
|
||||||
|
getTransitionInfo,
|
||||||
|
resolveTransitionProps
|
||||||
|
} from './Transition'
|
||||||
|
import {
|
||||||
|
Fragment,
|
||||||
|
VNode,
|
||||||
|
Slots,
|
||||||
|
warn,
|
||||||
|
resolveTransitionHooks,
|
||||||
|
toRaw,
|
||||||
|
useTransitionState,
|
||||||
|
getCurrentInstance,
|
||||||
|
setTransitionHooks,
|
||||||
|
createVNode,
|
||||||
|
onUpdated
|
||||||
|
} from '@vue/runtime-core'
|
||||||
|
|
||||||
|
interface Position {
|
||||||
|
top: number
|
||||||
|
left: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const positionMap = new WeakMap<VNode, Position>()
|
||||||
|
const newPositionMap = new WeakMap<VNode, Position>()
|
||||||
|
|
||||||
|
export type TransitionGroupProps = Omit<TransitionProps, 'mode'> & {
|
||||||
|
tag?: string
|
||||||
|
moveClass?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TransitionGroup = {
|
||||||
|
setup(props: TransitionGroupProps, { slots }: { slots: Slots }) {
|
||||||
|
const instance = getCurrentInstance()!
|
||||||
|
const state = useTransitionState()
|
||||||
|
let prevChildren: VNode[]
|
||||||
|
let children: VNode[]
|
||||||
|
let hasMove: boolean | null = null
|
||||||
|
|
||||||
|
onUpdated(() => {
|
||||||
|
// children is guaranteed to exist after initial render
|
||||||
|
if (!prevChildren.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const moveClass = props.moveClass || `${props.name || 'v'}-move`
|
||||||
|
// Check if move transition is needed. This check is cached per-instance.
|
||||||
|
hasMove =
|
||||||
|
hasMove === null
|
||||||
|
? (hasMove = hasCSSTransform(
|
||||||
|
prevChildren[0].el,
|
||||||
|
instance.vnode.el,
|
||||||
|
moveClass
|
||||||
|
))
|
||||||
|
: hasMove
|
||||||
|
if (!hasMove) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// we divide the work into three loops to avoid mixing DOM reads and writes
|
||||||
|
// in each iteration - which helps prevent layout thrashing.
|
||||||
|
prevChildren.forEach(callPendingCbs)
|
||||||
|
prevChildren.forEach(recordPosition)
|
||||||
|
const movedChildren = prevChildren.filter(applyTranslation)
|
||||||
|
|
||||||
|
// force reflow to put everything in position
|
||||||
|
forceReflow()
|
||||||
|
|
||||||
|
movedChildren.forEach(c => {
|
||||||
|
const el = c.el
|
||||||
|
const style = el.style
|
||||||
|
addTransitionClass(el, moveClass)
|
||||||
|
style.transform = style.WebkitTransform = style.transitionDuration = ''
|
||||||
|
const cb = (el._moveCb = (e: TransitionEvent) => {
|
||||||
|
if (e && e.target !== el) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!e || /transform$/.test(e.propertyName)) {
|
||||||
|
el.removeEventListener('transitionend', cb)
|
||||||
|
el._moveCb = null
|
||||||
|
removeTransitionClass(el, moveClass)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
el.addEventListener('transitionend', cb)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const rawProps = toRaw(props)
|
||||||
|
const cssTransitionProps = resolveTransitionProps(rawProps)
|
||||||
|
const tag = rawProps.tag || Fragment
|
||||||
|
prevChildren = children
|
||||||
|
children = slots.default ? slots.default() : []
|
||||||
|
|
||||||
|
for (let i = 0; i < children.length; i++) {
|
||||||
|
const child = children[i]
|
||||||
|
if (child.key != null) {
|
||||||
|
setTransitionHooks(
|
||||||
|
child,
|
||||||
|
resolveTransitionHooks(child, cssTransitionProps, state, instance)
|
||||||
|
)
|
||||||
|
} else if (__DEV__) {
|
||||||
|
warn(`<TransitionGroup> children must be keyed.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevChildren) {
|
||||||
|
for (let i = 0; i < prevChildren.length; i++) {
|
||||||
|
const child = prevChildren[i]
|
||||||
|
setTransitionHooks(
|
||||||
|
child,
|
||||||
|
resolveTransitionHooks(child, cssTransitionProps, state, instance)
|
||||||
|
)
|
||||||
|
positionMap.set(child, child.el.getBoundingClientRect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return createVNode(tag, null, children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function callPendingCbs(c: VNode) {
|
||||||
|
if (c.el._moveCb) {
|
||||||
|
c.el._moveCb()
|
||||||
|
}
|
||||||
|
if (c.el._enterCb) {
|
||||||
|
c.el._enterCb()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function recordPosition(c: VNode) {
|
||||||
|
newPositionMap.set(c, c.el.getBoundingClientRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTranslation(c: VNode): VNode | undefined {
|
||||||
|
const oldPos = positionMap.get(c)!
|
||||||
|
const newPos = newPositionMap.get(c)!
|
||||||
|
const dx = oldPos.left - newPos.left
|
||||||
|
const dy = oldPos.top - newPos.top
|
||||||
|
if (dx || dy) {
|
||||||
|
const s = c.el.style
|
||||||
|
s.transform = s.WebkitTransform = `translate(${dx}px,${dy}px)`
|
||||||
|
s.transitionDuration = '0s'
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is put in a dedicated function to avoid the line from being treeshaken
|
||||||
|
function forceReflow() {
|
||||||
|
return document.body.offsetHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCSSTransform(
|
||||||
|
el: ElementWithTransition,
|
||||||
|
root: Node,
|
||||||
|
moveClass: string
|
||||||
|
): boolean {
|
||||||
|
// Detect whether an element with the move class applied has
|
||||||
|
// CSS transitions. Since the element may be inside an entering
|
||||||
|
// transition at this very moment, we make a clone of it and remove
|
||||||
|
// all other transition classes applied to ensure only the move class
|
||||||
|
// is applied.
|
||||||
|
const clone = el.cloneNode() as HTMLElement
|
||||||
|
if (el._vtc) {
|
||||||
|
el._vtc.forEach(cls => clone.classList.remove(cls))
|
||||||
|
}
|
||||||
|
clone.classList.add(moveClass)
|
||||||
|
clone.style.display = 'none'
|
||||||
|
const container = (root.nodeType === 1
|
||||||
|
? root
|
||||||
|
: root.parentNode) as HTMLElement
|
||||||
|
container.appendChild(clone)
|
||||||
|
const { hasTransform } = getTransitionInfo(clone)
|
||||||
|
container.removeChild(clone)
|
||||||
|
return hasTransform
|
||||||
|
}
|
@ -55,7 +55,7 @@ export const createApp = (): App<Element> => {
|
|||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
// DOM-only runtime helpers
|
// DOM-only runtime directive helpers
|
||||||
export {
|
export {
|
||||||
vModelText,
|
vModelText,
|
||||||
vModelCheckbox,
|
vModelCheckbox,
|
||||||
@ -64,11 +64,14 @@ export {
|
|||||||
vModelDynamic
|
vModelDynamic
|
||||||
} from './directives/vModel'
|
} from './directives/vModel'
|
||||||
export { withModifiers, withKeys } from './directives/vOn'
|
export { withModifiers, withKeys } from './directives/vOn'
|
||||||
|
export { vShow } from './directives/vShow'
|
||||||
|
|
||||||
// DOM-only components
|
// DOM-only components
|
||||||
export { Transition, TransitionProps } from './components/Transition'
|
export { Transition, TransitionProps } from './components/Transition'
|
||||||
|
export {
|
||||||
export { vShow } from './directives/vShow'
|
TransitionGroup,
|
||||||
|
TransitionGroupProps
|
||||||
|
} from './components/TransitionGroup'
|
||||||
|
|
||||||
// re-export everything from core
|
// re-export everything from core
|
||||||
// h, Component, reactivity API, nextTick, flags & types
|
// h, Component, reactivity API, nextTick, flags & types
|
||||||
|
Loading…
x
Reference in New Issue
Block a user