From 5981d1ee66a73d199385490bf2c0bf7625d59033 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 6 Oct 2026 15:40:24 +0200 Subject: [PATCH] :bug: Open a never scrolled token set at the top of the list (#12119) The tokens list container stays mounted when another token set is selected; only the restore key changes. With no saved position for the new set, use-restore-scroll did nothing, so the list kept the scroll position of the previous set. Scroll to the top when nothing is saved for the key. Freshly mounted panels are already at the top, so Layers and Assets do not change. Closes #11973 AI-assisted-by: claude-opus-5-5 --- .../app/main/ui/workspace/sidebar/scroll.cljs | 15 ++++++++++++--- .../ui/sidebar_scroll_test.cljs | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/scroll.cljs b/frontend/src/app/main/ui/workspace/sidebar/scroll.cljs index 7bcaa73bef..235e29dec5 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/scroll.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/scroll.cljs @@ -45,17 +45,26 @@ (when (needs-restore-retry? prev-height height attempts) (vreset! raf* (tm/raf #(restore-loop! node saved height (inc attempts) raf*)))))) +(defn- restore-position! + "Restores saved, or scrolls to the top when there is none: the node can + stay mounted while the key changes (token sets), so it may still hold + another list's position." + [node saved raf*] + (if (some? saved) + (restore-loop! node saved -1 0 raf*) + (restore-scroll! node 0))) + (defn use-restore-scroll "Restores the scrollTop saved under [panel id] on mount and whenever panel or id change. Retries while the content height keeps changing so deep positions in lazily rendered lists are not clamped to the first - chunk. Missing key or node is a silent no-op." + chunk. With no saved position the node goes to the top. Missing node is + a silent no-op." [store* panel id node-ref] (mf/with-effect [panel id] (let [raf* (volatile! nil)] (when-let [node (mf/ref-val node-ref)] - (when-let [saved (get @store* [panel id])] - (restore-loop! node saved -1 0 raf*))) + (restore-position! node (get @store* [panel id]) raf*)) (fn [] (when-some [raf @raf*] (tm/cancel-af! raf)))))) diff --git a/frontend/test/frontend_tests/ui/sidebar_scroll_test.cljs b/frontend/test/frontend_tests/ui/sidebar_scroll_test.cljs index 9cf7bd8bcc..609ed6c35b 100644 --- a/frontend/test/frontend_tests/ui/sidebar_scroll_test.cljs +++ b/frontend/test/frontend_tests/ui/sidebar_scroll_test.cljs @@ -11,6 +11,7 @@ [cljs.test :as t :include-macros true])) (def ^:private restore-loop! @#'sc/restore-loop!) +(def ^:private restore-position! @#'sc/restore-position!) (defn- scroll-event [scroll-top] @@ -64,3 +65,21 @@ ((first @scheduled)) (t/is (= 250 (.-scrollTop node))) (t/is (= 1 (count @scheduled)) "no further frames once settled")))) + +(t/deftest restore-position-without-saved-value-goes-to-top + (let [node #js {:scrollTop 400 :scrollHeight 900} + scheduled (atom []) + raf* (volatile! nil)] + (with-redefs [tm/raf (fn [f] (swap! scheduled conj f) 1)] + (restore-position! node nil raf*) + (t/is (= 0 (.-scrollTop node)) "a node kept mounted drops the old position") + (t/is (empty? @scheduled) "no retry loop for the top")))) + +(t/deftest restore-position-with-saved-value-runs-loop + (let [node #js {:scrollTop 0 :scrollHeight 900} + scheduled (atom []) + raf* (volatile! nil)] + (with-redefs [tm/raf (fn [f] (swap! scheduled conj f) 1)] + (restore-position! node 250 raf*) + (t/is (= 250 (.-scrollTop node))) + (t/is (= 1 (count @scheduled)) "first pass schedules a settle check"))))