From efc3ace44864882c21baaaeb76412c2aed53aeff Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 10 Jul 2026 11:38:47 +0000 Subject: [PATCH] :bug: Fix leaked focus timers in dashboard sidebar navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six sidebar navigation handlers scheduled setTimeout callbacks to mutate tabindex/focus on React-managed title elements without cancelling prior pending callbacks. During rapid keyboard navigation (Projects→Fonts→Libs→Drafts) the stale callbacks fired against unmounted DOM, desyncing React fiber tree and triggering "removeChild" NotFoundError. - sidebar-project*: cancel prior timer in on-key-down - sidebar-search*: cancel prior timer in on-key-press - sidebar-content*: cancel prior timer in go-projects-with-key, go-fonts-with-key, go-drafts-with-key, go-libs-with-key Each handler now stores the timer handle in a component-level ref and disposes any pending handle before scheduling a new one. AI-assisted-by: opencode-go/mimo-v2.5-pro --- .../src/app/main/ui/dashboard/sidebar.cljs | 102 +++++++++++------- 1 file changed, 63 insertions(+), 39 deletions(-) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index eeda604490..ae6271abd5 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -112,6 +112,8 @@ project-id (get item :id) + focus-timer-ref (mf/use-ref nil) + on-click (mf/use-fn (mf/deps project-id) @@ -125,12 +127,15 @@ (when (kbd/enter? event) (st/emit! (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"))))))) + (when-let [h @focus-timer-ref] + (ts/dispose! h)) + (mf/set-ref-val! focus-timer-ref + (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 @@ -228,6 +233,8 @@ focused? (mf/use-state false) emit! (mf/use-memo #(f/debounce st/emit! 500)) + focus-timer-ref (mf/use-ref nil) + on-search-blur (mf/use-fn (fn [_] @@ -254,13 +261,16 @@ (mf/use-fn (fn [e] (when (kbd/enter? e) - (ts/schedule - (fn [] - (let [search-title (dom/get-element (str "dashboard-search-title"))] - (when search-title - (dom/set-attribute! search-title "tabindex" "0") - (dom/focus! search-title) - (dom/set-attribute! search-title "tabindex" "-1"))))) + (when-let [h @focus-timer-ref] + (ts/dispose! h)) + (mf/set-ref-val! focus-timer-ref + (ts/schedule + (fn [] + (let [search-title (dom/get-element (str "dashboard-search-title"))] + (when search-title + (dom/set-attribute! search-title "tabindex" "0") + (dom/focus! search-title) + (dom/set-attribute! search-title "tabindex" "-1")))))) (dom/prevent-default e) (dom/stop-propagation e)))) @@ -948,6 +958,8 @@ nitrate? (contains? cf/flags :nitrate) + focus-timer-ref (mf/use-ref nil) + go-projects (mf/use-fn #(st/emit! (dcm/go-to-dashboard-recent))) @@ -957,12 +969,15 @@ (fn [] (st/emit! (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")))))) + (when-let [h @focus-timer-ref] + (ts/dispose! h)) + (mf/set-ref-val! focus-timer-ref + (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 @@ -975,13 +990,16 @@ (fn [] (st/emit! (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"))))))) + (when-let [h @focus-timer-ref] + (ts/dispose! h)) + (mf/set-ref-val! focus-timer-ref + (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 @@ -994,12 +1012,15 @@ (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 - (fn [] - (when-let [title (dom/get-element "dashboard-drafts-title")] - (dom/set-attribute! title "tabindex" "0") - (dom/focus! title) - (dom/set-attribute! title "tabindex" "-1")))))) + (when-let [h @focus-timer-ref] + (ts/dispose! h)) + (mf/set-ref-val! focus-timer-ref + (ts/schedule + (fn [] + (when-let [title (dom/get-element "dashboard-drafts-title")] + (dom/set-attribute! title "tabindex" "0") + (dom/focus! title) + (dom/set-attribute! title "tabindex" "-1"))))))) go-libs (mf/use-fn @@ -1012,13 +1033,16 @@ (fn [] (st/emit! (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"))))))) + (when-let [h @focus-timer-ref] + (ts/dispose! h)) + (mf/set-ref-val! focus-timer-ref + (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]