From 4f0bceddae98ea073cb2f9e28e8755d9d1645b3e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 10 Mar 2026 14:22:45 +0000 Subject: [PATCH] :bug: Fix stale deferred DOM ops in dashboard navigation Two related issues that could cause crashes during fast navigation in the dashboard: 1. grid.cljs: On drag-start, a temporary counter element is appended to the file card node for the drag ghost image, then scheduled for removal via requestAnimationFrame. If the user navigates away before the RAF fires, React unmounts the section and removes the card node from the DOM. When the RAF fires, item-el.removeChild(counter-el) throws because counter-el is no longer a child. Fixed by guarding the removal with dom/child?. 2. sidebar.cljs: Keyboard navigation handlers used ts/schedule-on-idle (requestIdleCallback with a 30s timeout) to focus the newly rendered section title after navigation. This left a very wide window for the callback to fire against a stale DOM after a subsequent navigation. Additionally, the idle callbacks were incorrectly passed as arguments to st/emit! (which ignores non-event values), making the scheduling an accidental side effect. Fixed by replacing all occurrences with ts/schedule (setTimeout 0), which is sufficient to defer past the current render cycle, and moving the calls outside st/emit!. Signed-off-by: Andrey Antukh --- frontend/src/app/main/ui/dashboard/grid.cljs | 6 +- .../src/app/main/ui/dashboard/sidebar.cljs | 64 +++++++++---------- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index ba3304305e..a24490af59 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -328,7 +328,11 @@ ;; it right afterwards, in the next render cycle. (dom/append-child! item-el counter-el) (dnd/set-drag-image! event item-el (:x offset) (:y offset)) - (ts/raf #(dom/remove-child! item-el counter-el)))))) + ;; Guard against race condition: if the user navigates away + ;; before the RAF fires, item-el may have been unmounted and + ;; counter-el is no longer a child — removeChild would throw. + (ts/raf #(when (dom/child? counter-el item-el) + (dom/remove-child! item-el counter-el))))))) on-menu-click (mf/use-fn diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 0ca0a3514d..420cb162ac 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -104,13 +104,13 @@ (fn [event] (when (kbd/enter? event) (st/emit! - (dcm/go-to-dashboard-files :project-id project-id) - (ts/schedule-on-idle - (fn [] - (when-let [title (dom/get-element (str project-id))] - (dom/set-attribute! title "tabindex" "0") - (dom/focus! title) - (dom/set-attribute! title "tabindex" "-1")))))))) + (dcm/go-to-dashboard-files :project-id project-id)) + (ts/schedule + (fn [] + (when-let [title (dom/get-element (str project-id))] + (dom/set-attribute! title "tabindex" "0") + (dom/focus! title) + (dom/set-attribute! title "tabindex" "-1"))))))) on-menu-click (mf/use-fn @@ -234,7 +234,7 @@ (mf/use-fn (fn [e] (when (kbd/enter? e) - (ts/schedule-on-idle + (ts/schedule (fn [] (let [search-title (dom/get-element (str "dashboard-search-title"))] (when search-title @@ -713,13 +713,13 @@ (mf/deps team-id) (fn [] (st/emit! - (dcm/go-to-dashboard-recent :team-id team-id) - (ts/schedule-on-idle - (fn [] - (when-let [projects-title (dom/get-element "dashboard-projects-title")] - (dom/set-attribute! projects-title "tabindex" "0") - (dom/focus! projects-title) - (dom/set-attribute! projects-title "tabindex" "-1"))))))) + (dcm/go-to-dashboard-recent :team-id team-id)) + (ts/schedule + (fn [] + (when-let [projects-title (dom/get-element "dashboard-projects-title")] + (dom/set-attribute! projects-title "tabindex" "0") + (dom/focus! projects-title) + (dom/set-attribute! projects-title "tabindex" "-1")))))) go-fonts (mf/use-fn @@ -731,14 +731,14 @@ (mf/deps team) (fn [] (st/emit! - (dcm/go-to-dashboard-fonts :team-id team-id) - (ts/schedule-on-idle - (fn [] - (let [font-title (dom/get-element "dashboard-fonts-title")] - (when font-title - (dom/set-attribute! font-title "tabindex" "0") - (dom/focus! font-title) - (dom/set-attribute! font-title "tabindex" "-1")))))))) + (dcm/go-to-dashboard-fonts :team-id team-id)) + (ts/schedule + (fn [] + (let [font-title (dom/get-element "dashboard-fonts-title")] + (when font-title + (dom/set-attribute! font-title "tabindex" "0") + (dom/focus! font-title) + (dom/set-attribute! font-title "tabindex" "-1"))))))) go-drafts (mf/use-fn @@ -751,7 +751,7 @@ (mf/deps team-id default-project-id) (fn [] (st/emit! (dcm/go-to-dashboard-files :team-id team-id :project-id default-project-id)) - (ts/schedule-on-idle + (ts/schedule (fn [] (when-let [title (dom/get-element "dashboard-drafts-title")] (dom/set-attribute! title "tabindex" "0") @@ -768,14 +768,14 @@ (mf/deps team-id) (fn [] (st/emit! - (dcm/go-to-dashboard-libraries :team-id team-id) - (ts/schedule-on-idle - (fn [] - (let [libs-title (dom/get-element "dashboard-libraries-title")] - (when libs-title - (dom/set-attribute! libs-title "tabindex" "0") - (dom/focus! libs-title) - (dom/set-attribute! libs-title "tabindex" "-1")))))))) + (dcm/go-to-dashboard-libraries :team-id team-id)) + (ts/schedule + (fn [] + (let [libs-title (dom/get-element "dashboard-libraries-title")] + (when libs-title + (dom/set-attribute! libs-title "tabindex" "0") + (dom/focus! libs-title) + (dom/set-attribute! libs-title "tabindex" "-1"))))))) pinned-projects (mf/with-memo [projects]