diff --git a/.changeset/giant-roses-press.md b/.changeset/giant-roses-press.md new file mode 100644 index 0000000000..68069e0273 --- /dev/null +++ b/.changeset/giant-roses-press.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: support dynamic transition functions diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 4ec77b167d..1330141a8e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -1767,7 +1767,9 @@ export const template_visitors = { b.call( '$.animate', state.node, - /** @type {import('estree').Expression} */ (visit(parse_directive_name(node.name))), + b.thunk( + /** @type {import('estree').Expression} */ (visit(parse_directive_name(node.name))) + ), expression ) ) @@ -1791,7 +1793,9 @@ export const template_visitors = { b.call( type, state.node, - /** @type {import('estree').Expression} */ (visit(parse_directive_name(node.name))), + b.thunk( + /** @type {import('estree').Expression} */ (visit(parse_directive_name(node.name))) + ), expression, node.modifiers.includes('global') ? b.true : b.false ) diff --git a/packages/svelte/src/internal/client/block.js b/packages/svelte/src/internal/client/block.js index 8f70b0fcf1..5ca2bdd850 100644 --- a/packages/svelte/src/internal/client/block.js +++ b/packages/svelte/src/internal/client/block.js @@ -175,10 +175,13 @@ export function create_each_block(flags, anchor) { */ export function create_each_item_block(item, index, key) { return { + // animate transition + a: null, // dom d: null, // effect e: null, + // index i: index, // key k: key, diff --git a/packages/svelte/src/internal/client/each.js b/packages/svelte/src/internal/client/each.js index 21911ba975..0602d789f7 100644 --- a/packages/svelte/src/internal/client/each.js +++ b/packages/svelte/src/internal/client/each.js @@ -686,7 +686,7 @@ function destroy_active_transition_blocks(active_transitions) { * @param {import('./types.js').Block} block * @returns {Text | Element | Comment} */ -function get_first_element(block) { +export function get_first_element(block) { const current = block.d; if (is_array(current)) { @@ -717,25 +717,9 @@ function update_each_item_block(block, item, index, type) { const transitions = block.s; const index_is_reactive = (type & EACH_INDEX_REACTIVE) !== 0; // Handle each item animations - if (transitions !== null && (type & EACH_KEYED) !== 0) { - let prev_index = block.i; - if (index_is_reactive) { - prev_index = /** @type {import('./types.js').Signal} */ (prev_index).v; - } - const items = block.p.v; - if (prev_index !== index && /** @type {number} */ (index) < items.length) { - const from_dom = /** @type {Element} */ (get_first_element(block)); - const from = from_dom.getBoundingClientRect(); - // Cancel any existing key transitions - for (const transition of transitions) { - if (transition.r === 'key') { - transition.c(); - } - } - schedule_task(() => { - trigger_transitions(transitions, 'key', from); - }); - } + const each_animation = block.a; + if (transitions !== null && (type & EACH_KEYED) !== 0 && each_animation !== null) { + each_animation(block, transitions, index, index_is_reactive); } if (index_is_reactive) { set_signal_value(/** @type {import('./types.js').Signal} */ (block.i), index); diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index c4161018dc..3033209629 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -2086,49 +2086,49 @@ export function html(dom, get_value, svg) { /** * @template P * @param {HTMLElement} dom - * @param {import('./types.js').TransitionFn

} transition_fn + * @param {() => import('./types.js').TransitionFn

} get_transition_fn * @param {(() => P) | null} props * @param {any} global * @returns {void} */ -export function transition(dom, transition_fn, props, global = false) { - bind_transition(dom, transition_fn, props, 'both', global); +export function transition(dom, get_transition_fn, props, global = false) { + bind_transition(dom, get_transition_fn, props, 'both', global); } /** * @template P * @param {HTMLElement} dom - * @param {import('./types.js').TransitionFn

} transition_fn + * @param {() => import('./types.js').TransitionFn

} get_transition_fn * @param {(() => P) | null} props * @returns {void} */ -export function animate(dom, transition_fn, props) { - bind_transition(dom, transition_fn, props, 'key', false); +export function animate(dom, get_transition_fn, props) { + bind_transition(dom, get_transition_fn, props, 'key', false); } /** * @template P * @param {HTMLElement} dom - * @param {import('./types.js').TransitionFn

} transition_fn + * @param {() => import('./types.js').TransitionFn

} get_transition_fn * @param {(() => P) | null} props * @param {any} global * @returns {void} */ -function in_fn(dom, transition_fn, props, global = false) { - bind_transition(dom, transition_fn, props, 'in', global); +function in_fn(dom, get_transition_fn, props, global = false) { + bind_transition(dom, get_transition_fn, props, 'in', global); } export { in_fn as in }; /** * @template P * @param {HTMLElement} dom - * @param {import('./types.js').TransitionFn

} transition_fn + * @param {() => import('./types.js').TransitionFn

} get_transition_fn * @param {(() => P) | null} props * @param {any} global * @returns {void} */ -export function out(dom, transition_fn, props, global = false) { - bind_transition(dom, transition_fn, props, 'out', global); +export function out(dom, get_transition_fn, props, global = false) { + bind_transition(dom, get_transition_fn, props, 'out', global); } /** diff --git a/packages/svelte/src/internal/client/transitions.js b/packages/svelte/src/internal/client/transitions.js index 52d09f39cb..c1631f8c9a 100644 --- a/packages/svelte/src/internal/client/transitions.js +++ b/packages/svelte/src/internal/client/transitions.js @@ -9,7 +9,7 @@ import { KEY_BLOCK, ROOT_BLOCK } from './block.js'; -import { destroy_each_item_block } from './each.js'; +import { destroy_each_item_block, get_first_element } from './each.js'; import { append_child } from './operations.js'; import { empty } from './render.js'; import { @@ -21,6 +21,7 @@ import { managed_effect, managed_pre_effect, mark_subtree_inert, + schedule_task, untrack } from './runtime.js'; import { raf } from './timing.js'; @@ -411,13 +412,13 @@ function is_transition_block(block) { /** * @template P * @param {HTMLElement} dom - * @param {import('./types.js').TransitionFn

| import('./types.js').AnimateFn

} transition_fn + * @param {() => import('./types.js').TransitionFn

| import('./types.js').AnimateFn

} get_transition_fn * @param {(() => P) | null} props_fn * @param {'in' | 'out' | 'both' | 'key'} direction * @param {boolean} global * @returns {void} */ -export function bind_transition(dom, transition_fn, props_fn, direction, global) { +export function bind_transition(dom, get_transition_fn, props_fn, direction, global) { const transition_effect = /** @type {import('./types.js').EffectSignal} */ (current_effect); const block = current_block; const props = props_fn === null ? {} : props_fn(); @@ -432,6 +433,7 @@ export function bind_transition(dom, transition_fn, props_fn, direction, global) if (transition_block.t === EACH_ITEM_BLOCK) { // Lazily apply the each block transition transition_block.r = each_item_transition; + transition_block.a = each_item_animate; transition_block = transition_block.p; } else if (transition_block.t === AWAIT_BLOCK && transition_block.n /* pending */) { can_show_intro_on_mount = false; @@ -458,6 +460,11 @@ export function bind_transition(dom, transition_fn, props_fn, direction, global) let transition; effect(() => { + if (transition !== undefined) { + // Destroy any existing transitions first + transition.x(); + } + const transition_fn = get_transition_fn(); /** @param {DOMRect} [from] */ const init = (from) => untrack(() => @@ -641,3 +648,31 @@ function each_item_transition(transition) { }); transitions.add(transition); } + +/** + * + * @param {import('./types.js').EachItemBlock} block + * @param {Set} transitions + * @param {number} index + * @param {boolean} index_is_reactive + */ +function each_item_animate(block, transitions, index, index_is_reactive) { + let prev_index = block.i; + if (index_is_reactive) { + prev_index = /** @type {import('./types.js').Signal} */ (prev_index).v; + } + const items = block.p.v; + if (prev_index !== index && /** @type {number} */ (index) < items.length) { + const from_dom = /** @type {Element} */ (get_first_element(block)); + const from = from_dom.getBoundingClientRect(); + // Cancel any existing key transitions + for (const transition of transitions) { + if (transition.r === 'key') { + transition.c(); + } + } + schedule_task(() => { + trigger_transitions(transitions, 'key', from); + }); + } +} diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 55f37c66d7..3a73f797e3 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -285,6 +285,15 @@ export type EachBlock = { }; export type EachItemBlock = { + /** transition */ + a: + | null + | (( + block: EachItemBlock, + transitions: Set, + index: number, + index_is_reactive: boolean + ) => void); /** dom */ d: null | TemplateNode | Array; /** effect */ diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-transition/_config.js b/packages/svelte/tests/runtime-runes/samples/dynamic-transition/_config.js new file mode 100644 index 0000000000..ed9e52b347 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-transition/_config.js @@ -0,0 +1,25 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; +import { log } from './log.js'; + +export default test({ + before_test() { + log.length = 0; + }, + + async test({ assert, target }) { + const [b1, b2] = target.querySelectorAll('button'); + + flushSync(() => { + b1.click(); + }); + + assert.deepEqual(log, ['transition 2']); + + flushSync(() => { + b2.click(); + }); + + assert.deepEqual(log, ['transition 2', 'transition 1', 'transition 1']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-transition/log.js b/packages/svelte/tests/runtime-runes/samples/dynamic-transition/log.js new file mode 100644 index 0000000000..d3df521f4d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-transition/log.js @@ -0,0 +1,2 @@ +/** @type {any[]} */ +export const log = []; diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-transition/main.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-transition/main.svelte new file mode 100644 index 0000000000..8355a20d71 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-transition/main.svelte @@ -0,0 +1,33 @@ + + + + + + +{#if toggle}

{/if} +