Which approach best manages DOM focus transitions in Reagent without bypassing the virtual DOM?
0 reputation · 26 Oct 2024, 13:33 UTC
0 reputation · 26 Oct 2024, 13:33 UTC
In ClojureScript applications using Reagent, accessibility (a11y) requirements often necessitate precise control over focus transitions, such as moving focus to a modal header or returning it to a triggering element after a close event.
While js-interop allows direct calls to .focus() on DOM elements, this imperative approach operates outside the declarative nature of the Reagent virtual DOM. There is a tension between using direct DOM manipulation for immediate accessibility response and maintaining a single source of truth within the application state.
What is the recommended architectural pattern for managing these transitions to ensure accessibility states remain consistent with the Reagent state? Does a declarative wrapper for focus management exist, or is manual js-interop the standard for version 1.11+?
30775 reputation · 26 Oct 2024, 16:18 UTC
The most reliable way to move focus in a Reagent app without breaking the virtual‑DOM contract is to attach a :ref to the target element, store that DOM node in local state, and call .focus() inside reagent.core/next-tick. There is no built‑in declarative wrapper in Reagent 1.11+; the pattern above is the standard practice.
:ref gives you a stable pointer to the real DOM node after React has finished rendering.next-tick defers the call until the reconciliation phase is complete, guaranteeing the node is present.(defn modal-header []
(let [node (reagent/atom nil)]
(fn []
[:h2 {:ref (fn [el] (reset! node el))} "Modal Title"]))
(reagent/next-tick
(fn []
(when @node
(.focus @node))))
reagent.core/use-effect or the component’s :component-did-mount hook.When a modal closes, store a reference to the element that opened it (e.g., the button). On unmount, schedule a focus back to that element using the same next-tick pattern.
node inside next-tick; it should be a real DOM element, not null.Do you need to return focus to a specific trigger element after the modal closes, or is the focus simply moved to the next logical element?
Use comments to ask for clarification. Post a solution as an answer.
30,775 reputation · 26 Oct 2024, 22:19 UTC
One detail worth adding to the :ref + next-tick pattern: focusing a non-interactive element like a modal's <h2> only works reliably if the element is focusable in the first place. Add :tab-index -1 to the header so .focus() succeeds without inserting the heading into the tab order:
[:h2 {:ref (fn [el] (reset! node el))
:tab-index -1}
"Modal Title"]
Without this, browsers silently ignore the focus call on most non-form elements, which looks like a Reagent timing bug but is actually an HTML behavior.
Two related points for version 1.11+:
next-tick may be unnecessary — but it's harmless insurance when focus depends on freshly rendered content.js/document.activeElement at open time rather than threading a ref through props; it stays accurate even if the opener was rendered by a parent component.Verify with DevTools' document.activeElement after each transition, and confirm with a screen reader that the move is announced — a focused node that isn't announced is still an a11y failure.