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"))))