Skip to content

Support the :focus-within pseudo-class - #615

Open
maurerdietmar wants to merge 3 commits into
DioxusLabs:mainfrom
maurerdietmar:focus-within
Open

Support the :focus-within pseudo-class#615
maurerdietmar wants to merge 3 commits into
DioxusLabs:mainfrom
maurerdietmar:focus-within

Conversation

@maurerdietmar

@maurerdietmar maurerdietmar commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Selector matching for :focus-within was stubbed to false and no element state was maintained for it, so styles keyed on it never applied - a common pattern for form field focus rings, where the visible border lives on a wrapper around the input.

Track ElementState::FOCUS_WITHIN on the focused element and all of its DOM ancestors. The chain comes from a new BaseDocument::node_ancestors helper next to node_layout_ancestors. The layout chain is not usable here because focus pertains to the DOM tree: it skips display: contents ancestors, and its links do not exist before the first layout pass, which is when autofocus fires. compare_document_order already did the same walk through a private helper, which is now that public one. The specification defines the containment over the flat tree; without a shadow DOM implementation to flatten, that is the DOM tree.

The old and new chains are diffed the way hover's are, so a focus move only restyles the parts they do not share. The state follows tree changes around the focused node: it is cleared on refocus, clear_focus and removal, and transferred from the old ancestor chain to the new one in DocumentMutator::add_children_to_parent when a subtree containing the focus moves to a different parent - including a move out of the document, where the focus is cleared with it and only the old chain is unset.

The first commit is a pre-existing gap this feature runs into rather than causes: set_focus_to and clear_focus dirty selector state without asking for a frame, so a keyboard focus change - which touches neither layout nor content - leaves the old focus styling on screen until an unrelated event brings a redraw along. Focus changes driven by a mutation, autofocus in particular, ride along on the mutator's redraw. Happy to split it into its own PR if you would rather keep this one to the pseudo-class.

Tests: focus_within.rs covers ancestors and blur, a display: contents ancestor, focus set before the first layout pass, removal of the subtree holding the focus, a move within the document, and a move out of it. focus_redraw.rs counts redraw requests through a recording shell for focus, blur, a repeated focus and a Tab keypress.

@nicoburns nicoburns left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to use regular ancestors (node.parent) rather than layout ancestors (node.layout_parent) as focus pertains to the DOM elements tree rather than the layout tree.

This also needs to handle updating :focus-within when an element is moved within the tree.

@maurerdietmar

maurerdietmar commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, both points are addressed in the force-push (76c17eb).

Moving the focus changes what is on screen, because the focus is
styled, but neither set_focus_to nor clear_focus asked for a frame, and
a keyboard focus change touches neither layout nor content, so nothing
else did either. The focus ring stayed on the element that had it until
an unrelated event brought a redraw along: tabbing through a group of
controls appeared to do nothing at all, and the cell cursor of a data
grid sat where it started while the arrow keys moved the focus behind
it. Focus changes driven by a mutation, autofocus in particular, are
unaffected, since the mutator asks for the frame.

Hover changes already request a frame the same way.

Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
compare_document_order builds root-first DOM ancestor chains with a
private helper. Interaction state updates need the same walk, so give
it a public name next to node_layout_ancestors, an Option-taking
variant, and tolerance for freed slots via get_node, matching
nearest_surviving_element_ancestor.

Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
Selector matching for :focus-within was stubbed to false and no
element state was maintained for it, so styles keyed on it (a common
pattern for form field focus rings, where the visible border lives on
a wrapper around the input) never applied.

Track ElementState::FOCUS_WITHIN on the focused element and all of
its DOM ancestors, using the same ancestor-path diffing as hover so a
focus move only restyles the non-shared parts of the two chains. The
DOM chain rather than the layout chain, because focus pertains to the
DOM tree: the layout chain skips display: contents ancestors, and its
links do not exist before the first layout pass, which is when
autofocus fires. The specification defines the containment over the
flat tree; without a shadow DOM implementation to flatten, that is the
DOM tree.

The state follows tree changes around the focused node: it is cleared
on refocus, clear_focus and removal, and transferred from the old
ancestor chain to the new one when a subtree containing the focus is
moved to a different parent, including out of the document.

Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
@maurerdietmar

Copy link
Copy Markdown
Contributor Author

The series grew a commit in front: moving the focus did not request a frame, which is also fixed now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants