refactor(navigation): centralize sidebar focus

This commit is contained in:
edde746
2026-07-12 08:42:22 +02:00
parent 1b9ef3b3b6
commit aa230983d5
12 changed files with 104 additions and 11 deletions
+1 -1
View File
@@ -84,7 +84,7 @@ mixin TabNavigationMixin<T extends StatefulWidget> on State<T>, TickerProviderSt
} }
void onTabBarBack() { void onTabBarBack() {
MainScreenFocusScope.of(context, listen: false)?.focusSidebar(); MainScreenFocusScope.focusSidebarOf(context);
} }
/// Shared tab chip builder — eliminates duplication between screens. /// Shared tab chip builder — eliminates duplication between screens.
+5
View File
@@ -56,6 +56,11 @@ class MainScreenFocusScope extends InheritedModel<MainScreenScopeAspect> {
return context.getElementForInheritedWidgetOfExactType<MainScreenFocusScope>()?.widget as MainScreenFocusScope?; return context.getElementForInheritedWidgetOfExactType<MainScreenFocusScope>()?.widget as MainScreenFocusScope?;
} }
/// Focuses the sidebar without registering an inherited dependency.
static void focusSidebarOf(BuildContext context) {
of(context, listen: false)?.focusSidebar();
}
static MainScreenFocusScope? _dependOn(BuildContext context, MainScreenScopeAspect aspect) { static MainScreenFocusScope? _dependOn(BuildContext context, MainScreenScopeAspect aspect) {
return InheritedModel.inheritFrom<MainScreenFocusScope>(context, aspect: aspect); return InheritedModel.inheritFrom<MainScreenFocusScope>(context, aspect: aspect);
} }
+1 -1
View File
@@ -362,7 +362,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
/// Navigate focus to the sidebar /// Navigate focus to the sidebar
void _navigateToSidebar() { void _navigateToSidebar() {
MainScreenFocusScope.of(context, listen: false)?.focusSidebar(); MainScreenFocusScope.focusSidebarOf(context);
} }
@override @override
+2 -2
View File
@@ -220,7 +220,7 @@ class DownloadsScreenState extends State<DownloadsScreen>
}, },
onCancel: downloadProvider.cancelDownload, onCancel: downloadProvider.cancelDownload,
onDelete: downloadProvider.deleteDownload, onDelete: downloadProvider.deleteDownload,
onNavigateLeft: () => MainScreenFocusScope.of(context, listen: false)?.focusSidebar(), onNavigateLeft: () => MainScreenFocusScope.focusSidebarOf(context),
onBack: focusTabBar, onBack: focusTabBar,
suppressAutoFocus: suppressAutoFocus, suppressAutoFocus: suppressAutoFocus,
); );
@@ -287,7 +287,7 @@ class _DownloadsGridContentState extends State<_DownloadsGridContent> {
/// Navigate focus to the sidebar /// Navigate focus to the sidebar
void _navigateToSidebar() { void _navigateToSidebar() {
MainScreenFocusScope.of(context, listen: false)?.focusSidebar(); MainScreenFocusScope.focusSidebarOf(context);
} }
@override @override
+1 -1
View File
@@ -133,7 +133,7 @@ class ExploreScreenState extends State<ExploreScreen>
} }
void _navigateToSidebar() { void _navigateToSidebar() {
MainScreenFocusScope.of(context, listen: false)?.focusSidebar(); MainScreenFocusScope.focusSidebarOf(context);
} }
static IconData _rowIcon(CatalogRowId row) => switch (row) { static IconData _rowIcon(CatalogRowId row) => switch (row) {
@@ -1198,7 +1198,7 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
/// Navigate focus to the sidebar /// Navigate focus to the sidebar
void _navigateToSidebar() { void _navigateToSidebar() {
MainScreenFocusScope.of(context, listen: false)?.focusSidebar(); MainScreenFocusScope.focusSidebarOf(context);
} }
/// Navigate focus to the alpha jump bar /// Navigate focus to the alpha jump bar
@@ -252,7 +252,7 @@ class _LibraryCollectionsTabState extends BaseLibraryTabState<MediaItem, Library
} }
void _navigateToSidebar() { void _navigateToSidebar() {
MainScreenFocusScope.of(context, listen: false)?.focusSidebar(); MainScreenFocusScope.focusSidebarOf(context);
} }
@override @override
@@ -212,7 +212,7 @@ class _LibraryPlaylistsTabState extends BaseLibraryTabState<MediaPlaylist, Libra
} }
void _navigateToSidebar() { void _navigateToSidebar() {
MainScreenFocusScope.of(context, listen: false)?.focusSidebar(); MainScreenFocusScope.focusSidebarOf(context);
} }
@override @override
@@ -317,7 +317,7 @@ class _LibraryRecommendedTabState extends BaseLibraryTabState<MediaHub, LibraryR
/// Navigate focus to the sidebar /// Navigate focus to the sidebar
void _navigateToSidebar() { void _navigateToSidebar() {
MainScreenFocusScope.of(context, listen: false)?.focusSidebar(); MainScreenFocusScope.focusSidebarOf(context);
} }
// Extra top padding for focus decoration (scale + border extends beyond item bounds) // Extra top padding for focus decoration (scale + border extends beyond item bounds)
+1 -1
View File
@@ -148,7 +148,7 @@ class _SearchScreenState extends State<SearchScreen>
/// Navigate focus to the sidebar /// Navigate focus to the sidebar
void _navigateToSidebar() { void _navigateToSidebar() {
MainScreenFocusScope.of(context, listen: false)?.focusSidebar(); MainScreenFocusScope.focusSidebarOf(context);
} }
Widget _buildResultsList(BuildContext context) { Widget _buildResultsList(BuildContext context) {
+1 -1
View File
@@ -117,7 +117,7 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab, Moun
} }
void _navigateToSidebar() { void _navigateToSidebar() {
MainScreenFocusScope.of(context, listen: false)?.focusSidebar(); MainScreenFocusScope.focusSidebarOf(context);
} }
KeyEventResult _handleKeyEvent(FocusNode _, KeyEvent event) { KeyEventResult _handleKeyEvent(FocusNode _, KeyEvent event) {
@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/navigation/main_screen_scope.dart';
void main() {
testWidgets('focusSidebarOf invokes the scope callback', (tester) async {
var focusCalls = 0;
late BuildContext childContext;
await tester.pumpWidget(
MaterialApp(
home: MainScreenFocusScope(
focusSidebar: () => focusCalls++,
focusContent: () {},
isSidebarFocused: false,
sideNavigationWidth: 0,
child: Builder(
builder: (context) {
childContext = context;
return const SizedBox.shrink();
},
),
),
),
);
MainScreenFocusScope.focusSidebarOf(childContext);
expect(focusCalls, 1);
});
testWidgets('focusSidebarOf is a no-op without a scope', (tester) async {
late BuildContext childContext;
await tester.pumpWidget(
MaterialApp(
home: Builder(
builder: (context) {
childContext = context;
return const SizedBox.shrink();
},
),
),
);
expect(() => MainScreenFocusScope.focusSidebarOf(childContext), returnsNormally);
});
testWidgets('focusSidebarOf does not register an inherited dependency', (tester) async {
var isSidebarFocused = false;
var childBuilds = 0;
var focusCalls = 0;
late StateSetter rebuildScope;
final child = Builder(
builder: (context) {
childBuilds++;
MainScreenFocusScope.focusSidebarOf(context);
return const SizedBox.shrink();
},
);
await tester.pumpWidget(
MaterialApp(
home: StatefulBuilder(
builder: (context, setState) {
rebuildScope = setState;
return MainScreenFocusScope(
focusSidebar: () => focusCalls++,
focusContent: () {},
isSidebarFocused: isSidebarFocused,
sideNavigationWidth: 0,
child: child,
);
},
),
),
);
expect(childBuilds, 1);
expect(focusCalls, 1);
rebuildScope(() => isSidebarFocused = true);
await tester.pump();
expect(childBuilds, 1);
expect(focusCalls, 1);
});
}