fix(tv): keep sidebar focused when Back returns to the home tab

Backing out to home selected the home tab but let focusActiveTabIfReady
steal real focus into the content while _isSidebarFocused stayed true,
leaving the rail stuck expanded over a shifted content box that clipped
the end of every hub row. Skip the content-focus grab while the sidebar
owns focus, move the sidebar focus ring to the Home item on back-to-home,
and self-heal the flag whenever the content scope actually gains focus.

close #1411
This commit is contained in:
edde746
2026-07-04 20:56:04 +02:00
parent eb8796fa26
commit 6af4289289
2 changed files with 28 additions and 2 deletions
+21 -2
View File
@@ -280,6 +280,7 @@ class _MainScreenState extends State<MainScreen>
_offlineUntilConnected = widget.isOfflineMode;
WidgetsBinding.instance.addObserver(this);
_contentFocusScope.addListener(_syncSidebarFocusWithContent);
if (PlatformDetector.isDesktopOS()) {
windowManager.addListener(this);
@@ -823,6 +824,7 @@ class _MainScreenState extends State<MainScreen>
_startupSettleTimeout?.cancel();
_startupSettleTimeout = null;
_sidebarFocusScope.dispose();
_contentFocusScope.removeListener(_syncSidebarFocusWithContent);
_contentFocusScope.dispose();
_setTvosMenuPassthrough(false);
@@ -1089,6 +1091,15 @@ class _MainScreenState extends State<MainScreen>
});
}
/// _isSidebarFocused is hand-toggled; if anything moves real focus into the
/// content scope without going through _focusContent (e.g. a deferred
/// focusActiveTabIfReady), collapse the rail to match reality (#1411).
void _syncSidebarFocusWithContent() {
if (!mounted || !_isSidebarFocused || !_contentFocusScope.hasFocus) return;
setState(() => _isSidebarFocused = false);
_updateTvosMenuPassthrough();
}
void _handleSidebarInteractionExpandedChanged(bool expanded) {
if (_isSidebarInteractionExpanded == expanded) return;
setState(() => _isSidebarInteractionExpanded = expanded);
@@ -1142,6 +1153,9 @@ class _MainScreenState extends State<MainScreen>
final homeTab = tabs.first.id;
if (_currentTab != homeTab) {
_selectTab(homeTab);
// Keep the focus ring in step with the new selection; the sidebar scope
// already has focus, so no post-frame deferral is needed.
_sideNavKey.currentState?.focusHomeItem();
_lastBackPressAt = null;
return KeyEventResult.handled;
}
@@ -1390,8 +1404,13 @@ class _MainScreenState extends State<MainScreen>
if (newState case final TabVisibilityAware aware) {
aware.onTabShown();
}
if (newState case final FocusableTab focusable) {
focusable.focusActiveTabIfReady();
// Back-to-home keeps the sidebar focused (chain: content → sidebar →
// home → exit); stealing focus here left _isSidebarFocused stuck true
// while real focus sat on a content card (#1411).
if (!_isSidebarFocused) {
if (newState case final FocusableTab focusable) {
focusable.focusActiveTabIfReady();
}
}
}
+7
View File
@@ -341,6 +341,13 @@ class SideNavigationRailState extends State<SideNavigationRail> with MountedSetS
_requestFocusAndReveal(node);
}
/// Focus the Home nav item (Back returning to the home tab).
void focusHomeItem() {
final node = _mountedFocusNodeFor(_kHome);
if (node == null) return;
_requestFocusAndReveal(node);
}
/// Resolve the best mounted focus node in priority order:
/// 1. Explicit [targetKey] (captured before scope switch)
/// 2. Last focused key still in the tracker