Files
plezy/lib/widgets/focused_scroll_scaffold.dart
T

111 lines
3.3 KiB
Dart
Raw Normal View History

2025-12-07 15:00:28 +01:00
import 'package:flutter/material.dart';
import '../focus/input_mode_tracker.dart';
2025-12-07 15:00:28 +01:00
import '../focus/key_event_utils.dart';
import 'desktop_app_bar.dart';
/// A scaffold widget that wraps Focus + Scaffold + CustomScrollView
/// with consistent keyboard navigation handling and app bar styling.
///
/// This widget reduces boilerplate for screens that need:
/// - Keyboard navigation (back key handling)
/// - Custom scrollable content with slivers
/// - Consistent app bar with title and optional actions
///
/// Automatically focuses the first content item (skipping the app bar)
/// when in keyboard navigation mode.
class FocusedScrollScaffold extends StatefulWidget {
2025-12-07 15:00:28 +01:00
/// The title to display in the app bar.
/// Can be a Text widget or a more complex widget like Column.
final Widget title;
/// The list of slivers to display in the scroll view.
/// Should not include the app bar (it's added automatically).
final List<Widget> slivers;
/// Optional actions to display in the app bar (e.g., IconButton widgets).
final List<Widget>? actions;
/// Whether the app bar should remain visible when scrolling.
/// Defaults to true.
final bool pinned;
2025-12-18 08:42:18 +01:00
/// Whether to automatically add a back button.
/// Defaults to true.
final bool automaticallyImplyLeading;
2026-03-22 09:02:56 +01:00
/// Optional override for the back key handler.
/// When set, this callback is invoked instead of the default
/// [handleBackKeyNavigation] (which pops the current route).
final VoidCallback? onBackPressed;
2025-12-07 15:00:28 +01:00
const FocusedScrollScaffold({
super.key,
required this.title,
required this.slivers,
this.actions,
this.pinned = true,
2025-12-18 08:42:18 +01:00
this.automaticallyImplyLeading = true,
2026-03-22 09:02:56 +01:00
this.onBackPressed,
2025-12-07 15:00:28 +01:00
});
@override
State<FocusedScrollScaffold> createState() => _FocusedScrollScaffoldState();
}
class _FocusedScrollScaffoldState extends State<FocusedScrollScaffold> {
final _scopeNode = FocusScopeNode();
bool _focusRequested = false;
@override
void dispose() {
_scopeNode.dispose();
super.dispose();
}
void _requestInitialFocus() {
if (_focusRequested || !mounted || !InputModeTracker.isKeyboardMode(context)) return;
_focusRequested = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
2026-05-07 00:27:58 +02:00
if (_scopeNode.focusedChild != null) return;
_scopeNode.requestFocus();
_scopeNode.nextFocus();
});
}
2025-12-07 15:00:28 +01:00
@override
Widget build(BuildContext context) {
if (!_focusRequested && InputModeTracker.isKeyboardMode(context)) {
WidgetsBinding.instance.addPostFrameCallback((_) => _requestInitialFocus());
}
2025-12-07 15:00:28 +01:00
return Focus(
2026-02-22 11:46:04 +01:00
canRequestFocus: false,
2026-03-22 09:02:56 +01:00
onKeyEvent: (_, event) {
if (widget.onBackPressed != null) {
return handleBackKeyAction(event, widget.onBackPressed!);
}
return handleBackKeyNavigation(context, event);
},
2026-02-22 11:46:04 +01:00
child: FocusScope(
node: _scopeNode,
2026-02-22 11:46:04 +01:00
child: Scaffold(
body: CustomScrollView(
slivers: [
ExcludeFocus(
child: CustomAppBar(
title: widget.title,
pinned: widget.pinned,
actions: widget.actions,
automaticallyImplyLeading: widget.automaticallyImplyLeading,
),
2026-02-22 11:46:04 +01:00
),
...widget.slivers,
2026-02-22 11:46:04 +01:00
],
),
2025-12-07 15:00:28 +01:00
),
),
);
}
}