🐛 Fix leaked focus timers in dashboard sidebar navigation

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
This commit is contained in:
Andrey Antukh 2026-07-10 11:38:47 +00:00
parent b372e89282
commit efc3ace448

View File

@ -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]