From 50c9d534ea900162fd44f57196e296cfc7a87547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Mon, 3 Feb 2025 17:29:48 +0100 Subject: [PATCH] Proper port position updates during animations (#12179) --- .../components/GraphEditor/GraphNode.vue | 2 - .../components/GraphEditor/UploadingFile.vue | 1 - .../components/GraphEditor/WidgetTreeRoot.vue | 10 ++- .../components/SizeTransition.vue | 9 +++ .../src/project-view/composables/animation.ts | 16 +++-- .../providers/animationCounter.ts | 69 +++++++++++++++++++ 6 files changed, 97 insertions(+), 10 deletions(-) create mode 100644 app/gui/src/project-view/providers/animationCounter.ts diff --git a/app/gui/src/project-view/components/GraphEditor/GraphNode.vue b/app/gui/src/project-view/components/GraphEditor/GraphNode.vue index fd0b8626c37d..d01dad3b425f 100644 --- a/app/gui/src/project-view/components/GraphEditor/GraphNode.vue +++ b/app/gui/src/project-view/components/GraphEditor/GraphNode.vue @@ -599,8 +599,6 @@ onWindowBlur(() => { align-items: center; white-space: nowrap; z-index: 24; - transition: outline 0.2s ease; - outline: 0px solid transparent; } .binding { diff --git a/app/gui/src/project-view/components/GraphEditor/UploadingFile.vue b/app/gui/src/project-view/components/GraphEditor/UploadingFile.vue index 34eb603b4b9b..251d3422bb86 100644 --- a/app/gui/src/project-view/components/GraphEditor/UploadingFile.vue +++ b/app/gui/src/project-view/components/GraphEditor/UploadingFile.vue @@ -35,7 +35,6 @@ const backgroundOffset = computed(() => 200 - props.file.sizePercentage) white-space: nowrap; padding: 4px 8px; z-index: 2; - outline: 0px solid transparent; background: linear-gradient(to right, #e0e0e0 0%, #e0e0e0 50%, #ffffff 50%, #ffffff 100%); background-size: 200% 100%; } diff --git a/app/gui/src/project-view/components/GraphEditor/WidgetTreeRoot.vue b/app/gui/src/project-view/components/GraphEditor/WidgetTreeRoot.vue index 2a773cd490d8..8d7744ccf9c1 100644 --- a/app/gui/src/project-view/components/GraphEditor/WidgetTreeRoot.vue +++ b/app/gui/src/project-view/components/GraphEditor/WidgetTreeRoot.vue @@ -1,11 +1,12 @@ diff --git a/app/gui/src/project-view/composables/animation.ts b/app/gui/src/project-view/composables/animation.ts index 6c15a469bd97..b79eca40fb4f 100644 --- a/app/gui/src/project-view/composables/animation.ts +++ b/app/gui/src/project-view/composables/animation.ts @@ -156,13 +156,19 @@ function useApproachBase( return readonly(proxyRefs({ value: current, skip })) } -/** TODO: Add docs */ +/** + * Create `events` to check if any CSS transitions of declared properties + * within a DOM subtree are currently in progress. + * + * The state is reported back using the `active` property. + */ export function useTransitioning(observedProperties?: Set) { - const hasActiveAnimations = ref(false) + const hasActiveTransitions = ref(false) + let numActiveTransitions = 0 function onTransitionStart(e: TransitionEvent) { if (!observedProperties || observedProperties.has(e.propertyName)) { - if (numActiveTransitions == 0) hasActiveAnimations.value = true + if (numActiveTransitions == 0) hasActiveTransitions.value = true numActiveTransitions += 1 } } @@ -170,12 +176,12 @@ export function useTransitioning(observedProperties?: Set) { function onTransitionEnd(e: TransitionEvent) { if (!observedProperties || observedProperties.has(e.propertyName)) { numActiveTransitions -= 1 - if (numActiveTransitions == 0) hasActiveAnimations.value = false + if (numActiveTransitions == 0) hasActiveTransitions.value = false } } return { - active: hasActiveAnimations, + active: readonly(hasActiveTransitions), events: { transitionstart: onTransitionStart, transitionend: onTransitionEnd, diff --git a/app/gui/src/project-view/providers/animationCounter.ts b/app/gui/src/project-view/providers/animationCounter.ts new file mode 100644 index 000000000000..627d8c2e37b1 --- /dev/null +++ b/app/gui/src/project-view/providers/animationCounter.ts @@ -0,0 +1,69 @@ +import { computed, onScopeDispose, proxyRefs, ref } from 'vue' +import { createContextStore } from '.' + +/** + * A counter context that is counting actively running javascript-based layout animations, + * interfaced with using `useAnimationReporter` and `useAnimationsState` composables. + * + * This context supports nesting. Any animation reports within a child context will be + * also reported back to parent contexts, to make sure that `anyAnimationActive` property + * is correctly representing the whole component subtree, even when nesting is present. + */ +const [provideAnimationCounter, injectAnimationCounter] = createContextStore( + 'animation counter', + () => { + const parent = injectAnimationCounter(true) + const reportedCount = ref(0) + + let disposed = false + onScopeDispose(() => { + modifyCount(-reportedCount.value) + disposed = true + }) + + function modifyCount(change: number) { + if (disposed) return + reportedCount.value += change + parent?.modifyCount(change) + } + + return { + modifyCount, + anyAnimationActive: computed(() => reportedCount.value > 0), + } + }, +) + +/** + * A composable that allows reporting information about currently scheduled javascript animations + * that can affect DOM element layout, so that any parent component can temporarily enable extra + * logic necessary to track them. + * + * This is NOT supposed to be used forcounting CSS-driven animations, as those can already be discovered + * using `animationstart` and similar events. + * + * See `useLayoutAnimationsState` for use on the receiver end. + */ +export const useLayoutAnimationReporter = () => { + const counter = injectAnimationCounter(true) + return { + reportAnimationStarted: () => counter?.modifyCount(1), + // RequestAnimationFrame is used to ensure that at least one frame has passed since the animation stop + // was triggered on the JS side. That way the active state is maintained for the full duration of the + // last animation frame, and any tracking logic has a chance to react to the final element position. + reportAnimationEnded: () => requestAnimationFrame(() => counter?.modifyCount(-1)), + } +} + +/** + * A composable that receives information about wheter any potentially layout-shifting animations + * are currently playing within any of the child components. + * + * See `useLayoutAnimationReporter` for use on the animation component end. + */ +export const useLayoutAnimationsState = () => { + const counter = provideAnimationCounter() + return proxyRefs({ + anyAnimationActive: counter.anyAnimationActive, + }) +}