feat(transition): TransitionGroup
This commit is contained in:
@@ -58,7 +58,7 @@ if (__DEV__) {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveTransitionProps({
|
||||
export function resolveTransitionProps({
|
||||
name = 'v',
|
||||
type,
|
||||
css = true,
|
||||
@@ -91,7 +91,7 @@ function resolveTransitionProps({
|
||||
enterToClass = appearToClass
|
||||
}
|
||||
|
||||
type Hook = (el: Element, done?: () => void) => void
|
||||
type Hook = (el: HTMLElement, done?: () => void) => void
|
||||
|
||||
const finishEnter: Hook = (el, done) => {
|
||||
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.
|
||||
// Store the temporarily-added transition classes on the element
|
||||
// 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>
|
||||
}
|
||||
|
||||
function addTransitionClass(el: ElementWithTransition, cls: string) {
|
||||
export function addTransitionClass(el: ElementWithTransition, cls: string) {
|
||||
el.classList.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)
|
||||
if (el._vtc) {
|
||||
el._vtc.delete(cls)
|
||||
@@ -252,9 +252,10 @@ interface CSSTransitionInfo {
|
||||
type: typeof TRANSITION | typeof ANIMATION | null
|
||||
propCount: number
|
||||
timeout: number
|
||||
hasTransform: boolean
|
||||
}
|
||||
|
||||
function getTransitionInfo(
|
||||
export function getTransitionInfo(
|
||||
el: Element,
|
||||
expectedType?: TransitionProps['type']
|
||||
): CSSTransitionInfo {
|
||||
@@ -298,10 +299,14 @@ function getTransitionInfo(
|
||||
: animationDurations.length
|
||||
: 0
|
||||
}
|
||||
const hasTransform =
|
||||
type === TRANSITION &&
|
||||
/\b(transform|all)(,|$)/.test(styles[TRANSITION + 'Property'])
|
||||
return {
|
||||
type,
|
||||
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
|
||||
}
|
||||
|
||||
// DOM-only runtime helpers
|
||||
// DOM-only runtime directive helpers
|
||||
export {
|
||||
vModelText,
|
||||
vModelCheckbox,
|
||||
@@ -64,11 +64,14 @@ export {
|
||||
vModelDynamic
|
||||
} from './directives/vModel'
|
||||
export { withModifiers, withKeys } from './directives/vOn'
|
||||
export { vShow } from './directives/vShow'
|
||||
|
||||
// DOM-only components
|
||||
export { Transition, TransitionProps } from './components/Transition'
|
||||
|
||||
export { vShow } from './directives/vShow'
|
||||
export {
|
||||
TransitionGroup,
|
||||
TransitionGroupProps
|
||||
} from './components/TransitionGroup'
|
||||
|
||||
// re-export everything from core
|
||||
// h, Component, reactivity API, nextTick, flags & types
|
||||
|
||||
Reference in New Issue
Block a user