Park focus on a sink Item when leaving body

The previous clearBodyFocus() set focus = false on navRoot.activeFocusItem
and called navRoot.forceActiveFocus(), which works for plain Rectangles
but not for Controls 2 widgets like ComboBox: the combo keeps an internal
focused child that the surrounding FocusScope happily restores, so the
accent focus border on the Position/Center anchor dropdowns lingered
after backing out to the sidebar.

Add an invisible 1x1 navFocusSink Item inside navRoot and force focus
onto it whenever the user leaves the body (h/Esc/Shift-Tab, category
changes from sidebar nav, or panel reopens). The sink reliably owns
activeFocus so every body control \u2014 ComboBox included \u2014 falls back to
its unfocused style.
This commit is contained in:
Ryan Hughes
2026-05-14 02:25:05 -04:00
parent 9bb35dd7c1
commit 3595279dce
@@ -30,7 +30,7 @@ Item {
root.syncSidebarIndexFromCategory() root.syncSidebarIndexFromCategory()
root.focusZone = "sidebar" root.focusZone = "sidebar"
window.visible = true window.visible = true
Qt.callLater(function() { if (navRoot) navRoot.forceActiveFocus() }) Qt.callLater(parkFocusOnSink)
} }
function close() { function close() {
@@ -108,17 +108,17 @@ Item {
function exitBodyZone() { function exitBodyZone() {
focusZone = "sidebar" focusZone = "sidebar"
clearBodyFocus() parkFocusOnSink()
if (navRoot) navRoot.forceActiveFocus()
} }
// Drop activeFocus from any body item so its focus ring goes away when // Move activeFocus to a dedicated sink Item that lives outside the body
// the user backs out to the sidebar. Without this, FocusScope remembers // tree. Just clearing focus on the previously focused descendant isn't
// the last focused descendant and the highlight lingers. // enough — controls like ComboBox keep an internal focused child that
function clearBodyFocus() { // FocusScope happily restores. Forcing focus onto a known sink reliably
if (!navRoot) return // clears every body focus ring.
var afi = navRoot.activeFocusItem function parkFocusOnSink() {
if (afi && afi !== navRoot) afi.focus = false if (typeof navFocusSink !== "undefined" && navFocusSink) navFocusSink.forceActiveFocus()
else if (navRoot) navRoot.forceActiveFocus()
} }
// Walk the visible body subtree and collect any item with // Walk the visible body subtree and collect any item with
@@ -181,7 +181,7 @@ Item {
onActiveCategoryChanged: { onActiveCategoryChanged: {
syncSidebarIndexFromCategory() syncSidebarIndexFromCategory()
if (focusZone === "body") Qt.callLater(focusFirstBodyItem) if (focusZone === "body") Qt.callLater(focusFirstBodyItem)
else clearBodyFocus() else parkFocusOnSink()
} }
// ---------------- bundled defaults --------------------------------------- // ---------------- bundled defaults ---------------------------------------
@@ -599,7 +599,7 @@ Item {
onVisibleChanged: { onVisibleChanged: {
if (!visible && !root.closingFromHost && root.shell && typeof root.shell.hide === "function") if (!visible && !root.closingFromHost && root.shell && typeof root.shell.hide === "function")
root.shell.hide("omarchy.settings") root.shell.hide("omarchy.settings")
if (visible) Qt.callLater(function() { if (navRoot) navRoot.forceActiveFocus() }) if (visible) Qt.callLater(root.parkFocusOnSink)
} }
FocusScope { FocusScope {
@@ -607,7 +607,17 @@ Item {
anchors.fill: parent anchors.fill: parent
focus: true focus: true
Component.onCompleted: forceActiveFocus() Component.onCompleted: navFocusSink.forceActiveFocus()
// Invisible focus sink. When focus belongs to the sidebar (no
// specific body item focused), activeFocus lives on this 1px Item so
// body controls render their unfocused state cleanly.
Item {
id: navFocusSink
width: 1
height: 1
objectName: "navFocusSink"
}
Keys.priority: Keys.BeforeItem Keys.priority: Keys.BeforeItem
Keys.onPressed: function(event) { Keys.onPressed: function(event) {