fix(explore): keep the page at the top when a tab switch focuses a hub

Switching to the Explore tab hands focus to the first hub, and
HubSection unconditionally ran Scrollable.ensureVisible, scrolling the
app bar and search field off-screen in touch mode. The scroll is now
gated on keyboard/D-pad input mode, mirroring FocusableWrapper's
autoScroll.
This commit is contained in:
edde746
2026-08-23 10:12:27 +02:00
parent 627374d4b5
commit 07463eea04
2 changed files with 53 additions and 0 deletions
+4
View File
@@ -252,6 +252,10 @@ class HubSectionState extends State<HubSection> 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(
+49
View File
@@ -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<HubSectionState>();
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(