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,
+ })
+}