diff --git a/client/lib/ui/app/app_actions.dart b/client/lib/ui/app/app_actions.dart index d7f3c892..32cb289a 100644 --- a/client/lib/ui/app/app_actions.dart +++ b/client/lib/ui/app/app_actions.dart @@ -9,7 +9,9 @@ import 'package:provider/provider.dart'; class AppActions extends StatelessWidget { final Widget child; - const AppActions({super.key, required this.child}); + final FocusNode searchFocusNode; + + const AppActions({super.key, required this.child, required this.searchFocusNode}); @override Widget build(BuildContext context) { @@ -65,6 +67,7 @@ class AppActions extends StatelessWidget { NavForwardIntent: CallbackAction( onInvoke: (i) => context.read().goForward(), ), + FocusSearchIntent: CallbackAction(onInvoke: (i) => searchFocusNode.requestFocus()), }, child: child, ); diff --git a/client/lib/ui/app/app_layout.dart b/client/lib/ui/app/app_layout.dart index 0a228749..c162fcf1 100644 --- a/client/lib/ui/app/app_layout.dart +++ b/client/lib/ui/app/app_layout.dart @@ -13,7 +13,7 @@ import 'package:phylum/ui/menu/bottom_sheet.dart'; import 'package:phylum/ui/menu/option_groups.dart'; import 'package:provider/provider.dart'; -class AppLayout extends StatelessWidget { +class AppLayout extends StatefulWidget { const AppLayout._(); static Widget create() { @@ -24,10 +24,28 @@ class AppLayout extends StatelessWidget { ); } + @override + State createState() => _AppLayoutState(); +} + +class _AppLayoutState extends State { + final _searchFocusNode = FocusNode(); + + @override + void dispose() { + _searchFocusNode.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final size = MediaQuery.sizeOf(context); - final layout = size.width <= 600 ? CollapsedAppLayout() : ExpandedAppLayout(large: size.width > 1200); + final layout = size.width <= 600 + ? CollapsedAppLayout() + : ExpandedAppLayout( + large: size.width > 1200, + searchFocusNode: _searchFocusNode, + ); return ExternalDropRegion( buildItem: (context, dropTargetActive) { final child = (dropTargetActive) @@ -48,7 +66,10 @@ class AppLayout extends StatelessWidget { ], ) : layout; - return AppActions(child: child); + return AppActions( + searchFocusNode: _searchFocusNode, + child: child, + ); }, ); } @@ -121,8 +142,10 @@ class CollapsedAppLayout extends StatelessWidget { } class ExpandedAppLayout extends StatelessWidget { + final FocusNode searchFocusNode; final bool large; - const ExpandedAppLayout({super.key, required this.large}); + + const ExpandedAppLayout({super.key, required this.large, required this.searchFocusNode}); @override Widget build(BuildContext context) { @@ -156,7 +179,9 @@ class ExpandedAppLayout extends StatelessWidget { child: Icon(Icons.search), ), Expanded( - child: SearchField(), + child: SearchField( + focusNode: searchFocusNode, + ), ), ], ), diff --git a/client/lib/ui/app/app_shortcuts.dart b/client/lib/ui/app/app_shortcuts.dart index 38b06022..3477a70f 100644 --- a/client/lib/ui/app/app_shortcuts.dart +++ b/client/lib/ui/app/app_shortcuts.dart @@ -96,6 +96,10 @@ class NavIntent extends Intent { const NavIntent({required this.target}); } +class FocusSearchIntent extends Intent { + const FocusSearchIntent(); +} + Map get appShortcuts => const { // General SingleActivator(LogicalKeyboardKey.tab): NextFocusIntent(), @@ -142,6 +146,7 @@ Map get appShortcuts => const { // Top-level Navigation SingleActivator(LogicalKeyboardKey.arrowUp, alt: true): NavUpIntent(), SingleActivator(LogicalKeyboardKey.keyH, alt: true): NavIntent(target: ExplorerPageHome()), + SingleActivator(LogicalKeyboardKey.keyS, alt: true): FocusSearchIntent(), if (!kIsWeb) SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true): NavBackIntent(), if (!kIsWeb) SingleActivator(LogicalKeyboardKey.arrowRight, alt: true): NavForwardIntent(), }; diff --git a/client/lib/ui/app/search_field.dart b/client/lib/ui/app/search_field.dart index 25c2a26c..e099200a 100644 --- a/client/lib/ui/app/search_field.dart +++ b/client/lib/ui/app/search_field.dart @@ -5,7 +5,9 @@ import 'package:phylum/ui/explorer/page.dart'; import 'package:provider/provider.dart'; class SearchField extends StatefulWidget { - const SearchField({super.key}); + final FocusNode focusNode; + + const SearchField({super.key, required this.focusNode}); @override State createState() => _SearchFieldState(); @@ -35,19 +37,25 @@ class _SearchFieldState extends State { @override Widget build(BuildContext context) { - return TextField( - decoration: InputDecoration.collapsed( - hintText: 'Search Files...', - hintStyle: TextStyle(fontSize: 18), - ), - controller: _controller, - style: TextStyle(fontSize: 18), - onSubmitted: (text) { - final query = text.trim(); - if (query.isNotEmpty) { - context.read().go(ExplorerPageSearch(query: query)); - } + return Actions( + actions: { + DismissIntent: CallbackAction(onInvoke: (i) => widget.focusNode.nextFocus()), }, + child: TextField( + decoration: InputDecoration.collapsed( + hintText: 'Search Files...', + hintStyle: TextStyle(fontSize: 18), + ), + focusNode: widget.focusNode, + controller: _controller, + style: TextStyle(fontSize: 18), + onSubmitted: (text) { + final query = text.trim(); + if (query.isNotEmpty) { + context.read().go(ExplorerPageSearch(query: query)); + } + }, + ), ); } }