浏览代码

Add x-show.transition API

Caleb Porzio 5 年之前
父节点
当前提交
c31afc39ec
共有 8 个文件被更改,包括 372 次插入31 次删除
  1. 0 0
      dist/alpine-ie11.js
  2. 0 0
      dist/alpine-ie11.js.map
  3. 0 0
      dist/alpine.js
  4. 0 0
      dist/alpine.js.map
  5. 2 5
      examples/index.html
  6. 1 1
      package.json
  7. 198 25
      src/utils.js
  8. 171 0
      test/transition.spec.js

文件差异内容过多而无法显示
+ 0 - 0
dist/alpine-ie11.js


文件差异内容过多而无法显示
+ 0 - 0
dist/alpine-ie11.js.map


文件差异内容过多而无法显示
+ 0 - 0
dist/alpine.js


文件差异内容过多而无法显示
+ 0 - 0
dist/alpine.js.map


+ 2 - 5
examples/index.html

@@ -318,11 +318,8 @@
 
                             <div x-show="open">
                                 I shouldn't leave until the transition finishes.
-                                <div x-show="open"
-                                    x-transition:leave-start="opacity-100 scale-100"
-                                    x-transition:leave="ease-in transition-slow"
-                                    x-transition:leave-end="opacity-0 scale-90">
-                                        I'm transitioning
+                                <div x-show.transition="open">
+                                    I'm transitioning
                                 </div>
                             </div>
                         </div>

+ 1 - 1
package.json

@@ -7,7 +7,7 @@
         "url": "git://github.com/alpinejs/alpine.git"
     },
     "scripts": {
-        "watch": "concurrently \"rollup -c -w\" \"npx rollup -c rollup-ie11.config.js -w\"",
+        "watch": "rollup -c -w",
         "build": "concurrently \"rollup -c\" \"npx rollup -c rollup-ie11.config.js\"",
         "test": "npx jest",
         "test:debug": "node --inspect node_modules/.bin/jest --runInBand",

+ 198 - 25
src/utils.js

@@ -95,7 +95,7 @@ export function getXAttrs(el, type) {
             }
         })
         .filter(i => {
-            // If no type is passed in for filtering, bypassfilter
+            // If no type is passed in for filtering, bypass filter
             if (! type) return true
 
             return i.type === type
@@ -112,57 +112,226 @@ export function replaceAtAndColonWithStandardSyntax(name) {
     return name
 }
 
-export function transitionIn(el, callback, forceSkip = false) {
-    if (forceSkip) return callback()
+export function transitionIn(el, show, forceSkip = false) {
+    if (forceSkip) return show()
 
     const attrs = getXAttrs(el, 'transition')
+    const showAttr = getXAttrs(el, 'show')[0]
 
-    if (attrs.length < 1) return callback()
+    if (showAttr && showAttr.modifiers.includes('transition')) {
+        let modifiers = showAttr.modifiers
 
-    const enter = (attrs.find(i => i.value === 'enter') || { expression: '' }).expression.split(' ').filter(i => i !== '')
-    const enterStart = (attrs.find(i => i.value === 'enter-start') || { expression: '' }).expression.split(' ').filter(i => i !== '')
-    const enterEnd = (attrs.find(i => i.value === 'enter-end') || { expression: '' }).expression.split(' ').filter(i => i !== '')
+        if (modifiers.includes('out') && ! modifiers.includes('in')) return show()
 
-    transition(el, enter, enterStart, enterEnd, callback, () => {})
+        const settingBothSidesOfTransition = modifiers.includes('in') && modifiers.includes('out')
+
+        modifiers = settingBothSidesOfTransition
+            ? modifiers.filter((i, index) => index < modifiers.indexOf('out')) : modifiers
+
+        transitionHelperIn(el, modifiers, show)
+    } else if (attrs.length > 0) {
+        transitionClassesIn(el, attrs, show)
+    } else {
+        show()
+    }
 }
 
-export function transitionOut(el, callback, forceSkip = false) {
-    if (forceSkip) return callback()
+export function transitionOut(el, hide, forceSkip = false) {
+    if (forceSkip) return hide()
 
     const attrs = getXAttrs(el, 'transition')
+    const showAttr = getXAttrs(el, 'show')[0]
+
+    if (showAttr && showAttr.modifiers.includes('transition')) {
+        let modifiers = showAttr.modifiers
 
-    if (attrs.length < 1) return callback()
+        if (modifiers.includes('in') && ! modifiers.includes('out')) return hide()
 
-    const leave = (attrs.find(i => i.value === 'leave') || { expression: '' }).expression.split(' ').filter(i => i !== '')
-    const leaveStart = (attrs.find(i => i.value === 'leave-start') || { expression: '' }).expression.split(' ').filter(i => i !== '')
-    const leaveEnd = (attrs.find(i => i.value === 'leave-end') || { expression: '' }).expression.split(' ').filter(i => i !== '')
+        const settingBothSidesOfTransition = modifiers.includes('in') && modifiers.includes('out')
 
-    transition(el, leave, leaveStart, leaveEnd, () => {}, callback)
+        modifiers = settingBothSidesOfTransition
+            ? modifiers.filter((i, index) => index > modifiers.indexOf('out')) : modifiers
+
+        transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hide)
+    } else if (attrs.length > 0) {
+        transitionClassesOut(el, attrs, hide)
+    } else {
+        hide()
+    }
 }
 
-export function transition(el, classesDuring, classesStart, classesEnd, hook1, hook2) {
+export function transitionHelperIn(el, modifiers, showCallback) {
+    // Default values taken from: https://material.io/design/motion/speed.html#duration
+    const styleValues = {
+        duration: modifierValue(modifiers, 'duration', 150),
+        origin: modifierValue(modifiers, 'origin', 'center'),
+        first: {
+            opacity: 0,
+            scale: modifierValue(modifiers, 'scale', 95),
+        },
+        second: {
+            opacity: 1,
+            scale: 100,
+        },
+    }
+
+    transitionHelper(el, modifiers, showCallback, () => {}, styleValues)
+}
+
+export function transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hideCallback) {
+    const duration = settingBothSidesOfTransition
+        ? modifierValue(modifiers, 'duration', 150)
+        : modifierValue(modifiers, 'duration', 150) / 2
+
+    const styleValues = {
+        duration: duration,
+        origin: modifierValue(modifiers, 'origin', 'center'),
+        first: {
+            opacity: 1,
+            scale: 100,
+        },
+        second: {
+            opacity: 0,
+            scale: modifierValue(modifiers, 'scale', 95),
+        },
+    }
+
+    transitionHelper(el, modifiers, () => {}, hideCallback, styleValues)
+}
+
+function modifierValue(modifiers, key, fallback) {
+    if (modifiers.indexOf(key) === -1) return fallback
+
+    const rawValue = modifiers[modifiers.indexOf(key) + 1]
+
+    if (key === 'scale') {
+        // Check if the very next value is NOT a number and return the fallback.
+        if (! isNumeric(rawValue)) return fallback
+    }
+
+    if (! rawValue) return fallback
+
+    if (key === 'duration') {
+        let match = rawValue.match(/([0-9]+)ms/)
+        if (match) return match[1]
+    }
+
+    if (key === 'origin') {
+        if (['top', 'right', 'left', 'center', 'bottom'].includes(modifiers[modifiers.indexOf(key) + 2])) {
+            return [rawValue, modifiers[modifiers.indexOf(key) + 2]].join(' ')
+        }
+    }
+
+    return rawValue
+}
+
+export function transitionHelper(el, modifiers, hook1, hook2, styleValues) {
+    const opacityCache = el.style.opacity
+    const transformCache = el.style.transform
+    const transformOriginCache = el.style.transformOrigin
+    const noModifiers = ! modifiers.includes('opacity') && ! modifiers.includes('scale')
+    const transitionOpacity = noModifiers || modifiers.includes('opacity')
+    const transitionScale = noModifiers || modifiers.includes('scale')
+
+    const stages = {
+        start() {
+            if (transitionOpacity) el.style.opacity = styleValues.first.opacity
+            if (transitionScale) el.style.transform = `scale(${styleValues.first.scale / 100})`
+        },
+        during() {
+            if (transitionScale) el.style.transformOrigin = styleValues.origin
+            el.style.transitionProperty = [(transitionOpacity ? `opacity` : ``), (transitionScale ? `transform` : ``)].join(' ').trim()
+            el.style.transitionDuration = `${styleValues.duration / 1000}s`
+            el.style.transitionTimingFunction = `cubic-bezier(0.4, 0.0, 0.2, 1)`
+        },
+        show() {
+            hook1()
+        },
+        end() {
+            if (transitionOpacity) el.style.opacity = styleValues.second.opacity
+            if (transitionScale) el.style.transform = `scale(${styleValues.second.scale / 100})`
+        },
+        hide() {
+            hook2()
+        },
+        cleanup() {
+            if (transitionOpacity) el.style.opacity = opacityCache
+            if (transitionScale) el.style.transform = transformCache
+            if (transitionScale) el.style.transformOrigin = transformOriginCache
+            el.style.transitionProperty = null
+            el.style.transitionDuration = null
+            el.style.transitionTimingFunction = null
+        },
+    }
+
+    transition(el, stages)
+}
+
+export function transitionClassesIn(el, directives, showCallback) {
+    const enter = (directives.find(i => i.value === 'enter') || { expression: '' }).expression.split(' ').filter(i => i !== '')
+    const enterStart = (directives.find(i => i.value === 'enter-start') || { expression: '' }).expression.split(' ').filter(i => i !== '')
+    const enterEnd = (directives.find(i => i.value === 'enter-end') || { expression: '' }).expression.split(' ').filter(i => i !== '')
+
+    transitionClasses(el, enter, enterStart, enterEnd, showCallback, () => {})
+}
+
+export function transitionClassesOut(el, directives, hideCallback) {
+    const leave = (directives.find(i => i.value === 'leave') || { expression: '' }).expression.split(' ').filter(i => i !== '')
+    const leaveStart = (directives.find(i => i.value === 'leave-start') || { expression: '' }).expression.split(' ').filter(i => i !== '')
+    const leaveEnd = (directives.find(i => i.value === 'leave-end') || { expression: '' }).expression.split(' ').filter(i => i !== '')
+
+    transitionClasses(el, leave, leaveStart, leaveEnd, () => {}, hideCallback)
+}
+
+export function transitionClasses(el, classesDuring, classesStart, classesEnd, hook1, hook2) {
     const originalClasses = el.__x_original_classes || []
-    el.classList.add(...classesStart)
-    el.classList.add(...classesDuring)
+
+    const stages = {
+        start() {
+            el.classList.add(...classesStart)
+        },
+        during() {
+            el.classList.add(...classesDuring)
+        },
+        show() {
+            hook1()
+        },
+        end() {
+            // Don't remove classes that were in the original class attribute.
+            el.classList.remove(...classesStart.filter(i => !originalClasses.includes(i)))
+            el.classList.add(...classesEnd)
+        },
+        hide() {
+            hook2()
+        },
+        cleanup() {
+            el.classList.remove(...classesDuring.filter(i => !originalClasses.includes(i)))
+            el.classList.remove(...classesEnd.filter(i => !originalClasses.includes(i)))
+        },
+    }
+
+    transition(el, stages)
+}
+
+export function transition(el, stages) {
+    stages.start()
+    stages.during()
 
     requestAnimationFrame(() => {
         const duration = Number(getComputedStyle(el).transitionDuration.replace('s', '')) * 1000
 
-        hook1()
+        stages.show()
 
         requestAnimationFrame(() => {
-            // Don't remove classes that were in the original class attribute.
-            el.classList.remove(...classesStart.filter(i => !originalClasses.includes(i)))
-            el.classList.add(...classesEnd)
+            stages.end()
 
             setTimeout(() => {
-                hook2()
+                stages.hide()
 
                 // Adding an "isConnected" check, in case the callback
                 // removed the element from the DOM.
                 if (el.isConnected) {
-                    el.classList.remove(...classesDuring.filter(i => !originalClasses.includes(i)))
-                    el.classList.remove(...classesEnd.filter(i => !originalClasses.includes(i)))
+                    stages.cleanup()
                 }
             }, duration);
         })
@@ -191,3 +360,7 @@ export function deepProxy(target, proxyHandler) {
 
     return new Proxy(target, proxyHandler)
 }
+
+function isNumeric(subject){
+    return ! isNaN(subject)
+}

+ 171 - 0
test/transition.spec.js

@@ -302,3 +302,174 @@ test('transition out not called when item is already hidden', async () => {
     expect(document.querySelector('span').classList.contains('leave-end')).toEqual(false)
     expect(document.querySelector('span').getAttribute('style')).toEqual('display: none;')
 })
+
+test('transition with x-show.transition helper', async () => {
+    await assertTransitionHelperStyleAttributeValues('x-show.transition.in', [
+        'display: none; opacity: 0; transform: scale(0.95); transform-origin: center; transition-property: opacity transform; transition-duration: 0.15s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'opacity: 0; transform: scale(0.95); transform-origin: center; transition-property: opacity transform; transition-duration: 0.15s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'opacity: 1; transform: scale(1); transform-origin: center; transition-property: opacity transform; transition-duration: 0.15s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        '',
+        'display: none;',
+        'display: none;',
+    ])
+
+    await assertTransitionHelperStyleAttributeValues('x-show.transition.out', [
+        null,
+        null,
+        'opacity: 1; transform: scale(1); transform-origin: center; transition-property: opacity transform; transition-duration: 0.075s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'opacity: 1; transform: scale(1); transform-origin: center; transition-property: opacity transform; transition-duration: 0.075s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'opacity: 0; transform: scale(0.95); transform-origin: center; transition-property: opacity transform; transition-duration: 0.075s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'display: none;',
+    ])
+
+    await assertTransitionHelperStyleAttributeValues('x-show.transition', [
+        'display: none; opacity: 0; transform: scale(0.95); transform-origin: center; transition-property: opacity transform; transition-duration: 0.15s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'opacity: 0; transform: scale(0.95); transform-origin: center; transition-property: opacity transform; transition-duration: 0.15s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'opacity: 1; transform: scale(1); transform-origin: center; transition-property: opacity transform; transition-duration: 0.15s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        '',
+        'opacity: 1; transform: scale(1); transform-origin: center; transition-property: opacity transform; transition-duration: 0.075s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'opacity: 1; transform: scale(1); transform-origin: center; transition-property: opacity transform; transition-duration: 0.075s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'opacity: 0; transform: scale(0.95); transform-origin: center; transition-property: opacity transform; transition-duration: 0.075s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'display: none;',
+    ])
+
+    await assertTransitionHelperStyleAttributeValues('x-show.transition.opacity', [
+        'display: none; opacity: 0; transition-property: opacity; transition-duration: 0.15s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'opacity: 0; transition-property: opacity; transition-duration: 0.15s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'opacity: 1; transition-property: opacity; transition-duration: 0.15s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        '',
+        'opacity: 1; transition-property: opacity; transition-duration: 0.075s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'opacity: 1; transition-property: opacity; transition-duration: 0.075s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'opacity: 0; transition-property: opacity; transition-duration: 0.075s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'display: none;',
+    ])
+
+    await assertTransitionHelperStyleAttributeValues('x-show.transition.scale', [
+        'display: none; transform: scale(0.95); transform-origin: center; transition-property: transform; transition-duration: 0.15s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'transform: scale(0.95); transform-origin: center; transition-property: transform; transition-duration: 0.15s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'transform: scale(1); transform-origin: center; transition-property: transform; transition-duration: 0.15s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        '',
+        'transform: scale(1); transform-origin: center; transition-property: transform; transition-duration: 0.075s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'transform: scale(1); transform-origin: center; transition-property: transform; transition-duration: 0.075s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'transform: scale(0.95); transform-origin: center; transition-property: transform; transition-duration: 0.075s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'display: none;',
+    ])
+
+    await assertTransitionHelperStyleAttributeValues('x-show.transition.scale.85', [
+        'display: none; transform: scale(0.85); transform-origin: center; transition-property: transform; transition-duration: 0.15s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'transform: scale(0.85); transform-origin: center; transition-property: transform; transition-duration: 0.15s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'transform: scale(1); transform-origin: center; transition-property: transform; transition-duration: 0.15s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        '',
+        'transform: scale(1); transform-origin: center; transition-property: transform; transition-duration: 0.075s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'transform: scale(1); transform-origin: center; transition-property: transform; transition-duration: 0.075s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'transform: scale(0.85); transform-origin: center; transition-property: transform; transition-duration: 0.075s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'display: none;',
+    ])
+
+    await assertTransitionHelperStyleAttributeValues('x-show.transition.scale.85.duration.200ms', [
+        'display: none; transform: scale(0.85); transform-origin: center; transition-property: transform; transition-duration: 0.2s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'transform: scale(0.85); transform-origin: center; transition-property: transform; transition-duration: 0.2s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'transform: scale(1); transform-origin: center; transition-property: transform; transition-duration: 0.2s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        '',
+        'transform: scale(1); transform-origin: center; transition-property: transform; transition-duration: 0.1s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'transform: scale(1); transform-origin: center; transition-property: transform; transition-duration: 0.1s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'transform: scale(0.85); transform-origin: center; transition-property: transform; transition-duration: 0.1s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'display: none;',
+    ])
+
+    await assertTransitionHelperStyleAttributeValues('x-show.transition.scale.85.duration.200ms.origin.top', [
+        'display: none; transform: scale(0.85); transform-origin: top; transition-property: transform; transition-duration: 0.2s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'transform: scale(0.85); transform-origin: top; transition-property: transform; transition-duration: 0.2s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'transform: scale(1); transform-origin: top; transition-property: transform; transition-duration: 0.2s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        '',
+        'transform: scale(1); transform-origin: top; transition-property: transform; transition-duration: 0.1s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'transform: scale(1); transform-origin: top; transition-property: transform; transition-duration: 0.1s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'transform: scale(0.85); transform-origin: top; transition-property: transform; transition-duration: 0.1s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'display: none;',
+    ])
+
+    await assertTransitionHelperStyleAttributeValues('x-show.transition.scale.85.duration.200ms.origin.top.left', [
+        'display: none; transform: scale(0.85); transform-origin: top left; transition-property: transform; transition-duration: 0.2s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'transform: scale(0.85); transform-origin: top left; transition-property: transform; transition-duration: 0.2s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'transform: scale(1); transform-origin: top left; transition-property: transform; transition-duration: 0.2s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        '',
+        'transform: scale(1); transform-origin: top left; transition-property: transform; transition-duration: 0.1s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'transform: scale(1); transform-origin: top left; transition-property: transform; transition-duration: 0.1s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'transform: scale(0.85); transform-origin: top left; transition-property: transform; transition-duration: 0.1s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'display: none;',
+    ])
+
+    await assertTransitionHelperStyleAttributeValues('x-show.transition.in.scale.85.duration.200ms.origin.top.left.out.scale.75.duration.500ms.origin.bottom.right', [
+        'display: none; transform: scale(0.85); transform-origin: top left; transition-property: transform; transition-duration: 0.2s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'transform: scale(0.85); transform-origin: top left; transition-property: transform; transition-duration: 0.2s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'transform: scale(1); transform-origin: top left; transition-property: transform; transition-duration: 0.2s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        '',
+        'transform: scale(1); transform-origin: bottom right; transition-property: transform; transition-duration: 0.5s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'transform: scale(1); transform-origin: bottom right; transition-property: transform; transition-duration: 0.5s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'transform: scale(0.75); transform-origin: bottom right; transition-property: transform; transition-duration: 0.5s; transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);',
+        'display: none;',
+    ])
+})
+
+async function assertTransitionHelperStyleAttributeValues(xShowDirective, styleAttributeExpectations) {
+    // Hijack "requestAnimationFrame" for finer-tuned control in this test.
+    var frameStack = []
+
+    jest.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
+        frameStack.push(callback)
+    });
+
+    // Hijack "getComputeStyle" because js-dom is weird with it.
+    // (hardcoding 10ms transition time for later assertions)
+    jest.spyOn(window, 'getComputedStyle').mockImplementation(el => {
+        return { transitionDuration: '.02s' }
+    });
+
+    document.body.innerHTML = `
+        <div x-data="{ show: false }">
+            <button x-on:click="show = ! show"></button>
+
+            <span ${xShowDirective}="show"></span>
+        </div>
+    `
+
+    Alpine.start()
+
+    document.querySelector('button').click()
+
+    // Wait out the intial Alpine refresh debounce.
+    await new Promise((resolve) =>
+        setTimeout(() => {
+            resolve();
+        }, 5)
+    )
+
+    let index = 0
+
+    expect(document.querySelector('span').getAttribute('style')).toEqual(styleAttributeExpectations[index])
+
+    while(frameStack.length) {
+        frameStack.pop()()
+        expect(document.querySelector('span').getAttribute('style')).toEqual(styleAttributeExpectations[++index])
+    }
+
+    await new Promise(resolve => setTimeout(resolve, 30))
+
+    expect(document.querySelector('span').getAttribute('style')).toEqual(styleAttributeExpectations[++index])
+
+    document.querySelector('button').click()
+
+    // Wait out the intial Alpine refresh debounce.
+    await new Promise(resolve => setTimeout(resolve, 5))
+
+    expect(document.querySelector('span').getAttribute('style')).toEqual(styleAttributeExpectations[++index])
+
+    while(frameStack.length) {
+        frameStack.pop()()
+        expect(document.querySelector('span').getAttribute('style')).toEqual(styleAttributeExpectations[++index])
+    }
+
+    await new Promise(resolve => setTimeout(resolve, 50))
+
+    expect(document.querySelector('span').getAttribute('style')).toEqual(styleAttributeExpectations[++index])
+}

部分文件因为文件数量过多而无法显示