diff --git a/lib/widgets/hub_section.dart b/lib/widgets/hub_section.dart index 46eb49dfe..c10254c4f 100644 --- a/lib/widgets/hub_section.dart +++ b/lib/widgets/hub_section.dart @@ -252,6 +252,10 @@ class HubSectionState extends State with MountedSetStateMixin, Skele /// Scroll this hub into view in the parent scroll view void _scrollHubIntoView() { + // Programmatic focus in touch/pointer mode (e.g. a tab switch handing focus + // to the first hub) must not scroll the page; only keyboard/D-pad focus + // scrolls, mirroring FocusableWrapper.autoScroll. + if (InputModeTracker.currentMode != InputMode.keyboard) return; WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; Scrollable.ensureVisible( diff --git a/test/widgets/hub_section_test.dart b/test/widgets/hub_section_test.dart index 5a77a609e..7ca8cb562 100644 --- a/test/widgets/hub_section_test.dart +++ b/test/widgets/hub_section_test.dart @@ -100,6 +100,55 @@ void main() { expect(longPressedItem, same(item)); }); + testWidgets('programmatic hub focus scrolls the page only in keyboard mode', (tester) async { + // Regression: switching to the Explore tab hands focus to the first hub; + // in touch mode that must not scroll the page (it hid the app bar and + // search field). Keyboard/D-pad focus keeps scrolling the hub into view. + final item = testMediaItem( + id: 'scroll_item', + backend: MediaBackend.plex, + kind: MediaKind.movie, + title: 'Scroll Movie', + ); + final hubKey = GlobalKey(); + final scrollController = ScrollController(); + addTearDown(scrollController.dispose); + + await tester.pumpWidget( + InputModeTracker( + child: MaterialApp( + theme: monoTheme(dark: true), + home: Scaffold( + body: SingleChildScrollView( + controller: scrollController, + child: Column( + children: [ + const SizedBox(height: 900), + HubSection( + key: hubKey, + hub: _hubWith(item), + focusMemory: HubFocusMemory(), + icon: Symbols.live_tv_rounded, + ), + ], + ), + ), + ), + ), + ), + ); + + hubKey.currentState!.requestFocusFromMemory(); + await tester.pumpAndSettle(); + expect(scrollController.offset, 0, reason: 'touch-mode programmatic focus must not scroll the page'); + + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); + expect(InputModeTracker.currentMode, InputMode.keyboard, reason: 'arrow key must enter keyboard mode'); + hubKey.currentState!.requestFocusFromMemory(); + await tester.pumpAndSettle(); + expect(scrollController.offset, greaterThan(0), reason: 'keyboard-mode focus scrolls the hub into view'); + }); + testWidgets('grid poster override uses dense 2:3 TV geometry', (tester) async { TvDetectionService.debugSetAppleTVOverride(true); final item = testMediaItem(